kube_core/
object.rs

1//! Generic object and objectlist wrappers.
2use crate::{
3    discovery::ApiResource,
4    metadata::{ListMeta, ObjectMeta, TypeMeta},
5    resource::{DynamicResourceScope, Resource},
6};
7use serde::{Deserialize, Deserializer, Serialize};
8use std::borrow::Cow;
9
10/// A generic Kubernetes object list
11///
12/// This is used instead of a full struct for `DeploymentList`, `PodList`, etc.
13/// Kubernetes' API [always seem to expose list structs in this manner](https://docs.rs/k8s-openapi/0.10.0/k8s_openapi/apimachinery/pkg/apis/meta/v1/struct.ObjectMeta.html?search=List).
14///
15/// Note that this is only used internally within reflectors and informers,
16/// and is generally produced from list/watch/delete collection queries on an [`Resource`](super::Resource).
17///
18/// This is almost equivalent to [`k8s_openapi::List<T>`](k8s_openapi::List), but iterable.
19#[derive(Serialize, Deserialize, Debug, Clone)]
20pub struct ObjectList<T>
21where
22    T: Clone,
23{
24    /// The type fields, always present
25    #[serde(flatten, deserialize_with = "deserialize_v1_list_as_default")]
26    pub types: TypeMeta,
27
28    /// ListMeta - only really used for its `resourceVersion`
29    ///
30    /// See [ListMeta](k8s_openapi::apimachinery::pkg::apis::meta::v1::ListMeta)
31    #[serde(default)]
32    pub metadata: ListMeta,
33
34    /// The items we are actually interested in. In practice; `T := Resource<T,U>`.
35    #[serde(
36        deserialize_with = "deserialize_null_as_default",
37        bound(deserialize = "Vec<T>: Deserialize<'de>")
38    )]
39    pub items: Vec<T>,
40}
41
42fn deserialize_v1_list_as_default<'de, D>(deserializer: D) -> Result<TypeMeta, D::Error>
43where
44    D: Deserializer<'de>,
45{
46    let meta = Option::<TypeMeta>::deserialize(deserializer)?;
47    Ok(meta.unwrap_or(TypeMeta {
48        api_version: "v1".to_owned(),
49        kind: "List".to_owned(),
50    }))
51}
52
53fn deserialize_null_as_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
54where
55    T: Default + Deserialize<'de>,
56    D: Deserializer<'de>,
57{
58    let opt = Option::deserialize(deserializer)?;
59    Ok(opt.unwrap_or_default())
60}
61
62impl<T: Clone> ObjectList<T> {
63    /// `iter` returns an Iterator over the elements of this ObjectList
64    ///
65    /// # Example
66    ///
67    /// ```
68    /// use kube::api::{ListMeta, ObjectList, TypeMeta};
69    /// use k8s_openapi::api::core::v1::Pod;
70    ///
71    /// let types: TypeMeta = TypeMeta::list::<Pod>();
72    /// let metadata: ListMeta = Default::default();
73    /// let items = vec![1, 2, 3];
74    /// # let objectlist = ObjectList { types, metadata, items };
75    ///
76    /// let first = objectlist.iter().next();
77    /// println!("First element: {:?}", first); // prints "First element: Some(1)"
78    /// ```
79    pub fn iter(&self) -> impl Iterator<Item = &T> {
80        self.items.iter()
81    }
82
83    /// `iter_mut` returns an Iterator of mutable references to the elements of this ObjectList
84    ///
85    /// # Example
86    ///
87    /// ```
88    /// use kube::api::{ListMeta, ObjectList, TypeMeta};
89    /// use k8s_openapi::api::core::v1::Pod;
90    ///
91    /// let types: TypeMeta = TypeMeta::list::<Pod>();
92    /// let metadata: ListMeta = Default::default();
93    /// let items = vec![1, 2, 3];
94    /// # let mut objectlist = ObjectList { types, metadata, items };
95    ///
96    /// let mut first = objectlist.iter_mut().next();
97    ///
98    /// // Reassign the value in first
99    /// if let Some(elem) = first {
100    ///     *elem = 2;
101    ///     println!("First element: {:?}", elem); // prints "First element: 2"
102    /// }
103    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
104        self.items.iter_mut()
105    }
106}
107
108impl<T: Clone> IntoIterator for ObjectList<T> {
109    type IntoIter = ::std::vec::IntoIter<Self::Item>;
110    type Item = T;
111
112    fn into_iter(self) -> Self::IntoIter {
113        self.items.into_iter()
114    }
115}
116
117impl<'a, T: Clone> IntoIterator for &'a ObjectList<T> {
118    type IntoIter = ::std::slice::Iter<'a, T>;
119    type Item = &'a T;
120
121    fn into_iter(self) -> Self::IntoIter {
122        self.items.iter()
123    }
124}
125
126impl<'a, T: Clone> IntoIterator for &'a mut ObjectList<T> {
127    type IntoIter = ::std::slice::IterMut<'a, T>;
128    type Item = &'a mut T;
129
130    fn into_iter(self) -> Self::IntoIter {
131        self.items.iter_mut()
132    }
133}
134
135/// A trait to access the `spec` of a Kubernetes resource.
136///
137/// Some built-in Kubernetes resources and all custom resources do have a `spec` field.
138/// This trait can be used to access this field.
139///
140/// This trait is automatically implemented by the kube-derive macro and is _not_ currently
141/// implemented for the Kubernetes API objects from `k8s_openapi`.
142///
143/// Note: Not all Kubernetes resources have a spec (e.g. `ConfigMap`, `Secret`, ...).
144pub trait HasSpec {
145    /// The type of the `spec` of this resource
146    type Spec;
147
148    /// Returns a reference to the `spec` of the object
149    fn spec(&self) -> &Self::Spec;
150
151    /// Returns a mutable reference to the `spec` of the object
152    fn spec_mut(&mut self) -> &mut Self::Spec;
153}
154
155/// A trait to access the `status` of a Kubernetes resource.
156///
157/// Some built-in Kubernetes resources and custom resources do have a `status` field.
158/// This trait can be used to access this field.
159///
160/// This trait is automatically implemented by the kube-derive macro and is _not_ currently
161/// implemented for the Kubernetes API objects from `k8s_openapi`.
162///
163/// Note: Not all Kubernetes resources have a status (e.g. `ConfigMap`, `Secret`, ...).
164pub trait HasStatus {
165    /// The type of the `status` object
166    type Status;
167
168    /// Returns an optional reference to the `status` of the object
169    fn status(&self) -> Option<&Self::Status>;
170
171    /// Returns an optional mutable reference to the `status` of the object
172    fn status_mut(&mut self) -> &mut Option<Self::Status>;
173}
174
175// -------------------------------------------------------
176
177/// A standard Kubernetes object with `.spec` and `.status`.
178///
179/// This is a convenience struct provided for serialization/deserialization.
180/// It is slightly stricter than ['DynamicObject`] in that it enforces the spec/status convention,
181/// and as such will not in general work with all api-discovered resources.
182///
183/// This can be used to tie existing resources to smaller, local struct variants to optimize for memory use.
184/// E.g. if you are only interested in a few fields, but you store tons of them in memory with reflectors.
185#[derive(Deserialize, Serialize, Clone, Debug)]
186pub struct Object<P, U>
187where
188    P: Clone,
189    U: Clone,
190{
191    /// The type fields, not always present
192    #[serde(flatten, default)]
193    pub types: Option<TypeMeta>,
194
195    /// Resource metadata
196    ///
197    /// Contains information common to most resources about the Resource,
198    /// including the object name, annotations, labels and more.
199    pub metadata: ObjectMeta,
200
201    /// The Spec struct of a resource. I.e. `PodSpec`, `DeploymentSpec`, etc.
202    ///
203    /// This defines the desired state of the Resource as specified by the user.
204    pub spec: P,
205
206    /// The Status of a resource. I.e. `PodStatus`, `DeploymentStatus`, etc.
207    ///
208    /// This publishes the state of the Resource as observed by the controller.
209    /// Use `U = NotUsed` when a status does not exist.
210    #[serde(default, skip_serializing_if = "Option::is_none")]
211    pub status: Option<U>,
212}
213
214impl<P, U> Object<P, U>
215where
216    P: Clone,
217    U: Clone,
218{
219    /// A constructor that takes Resource values from an `ApiResource`
220    pub fn new(name: &str, ar: &ApiResource, spec: P) -> Self {
221        Self {
222            types: Some(TypeMeta {
223                api_version: ar.api_version.clone(),
224                kind: ar.kind.clone(),
225            }),
226            metadata: ObjectMeta {
227                name: Some(name.to_string()),
228                ..Default::default()
229            },
230            spec,
231            status: None,
232        }
233    }
234
235    /// Attach a namespace to an Object
236    #[must_use]
237    pub fn within(mut self, ns: &str) -> Self {
238        self.metadata.namespace = Some(ns.into());
239        self
240    }
241}
242
243impl<P, U> Resource for Object<P, U>
244where
245    P: Clone,
246    U: Clone,
247{
248    type DynamicType = ApiResource;
249    type Scope = DynamicResourceScope;
250
251    fn group(dt: &ApiResource) -> Cow<'_, str> {
252        dt.group.as_str().into()
253    }
254
255    fn version(dt: &ApiResource) -> Cow<'_, str> {
256        dt.version.as_str().into()
257    }
258
259    fn kind(dt: &ApiResource) -> Cow<'_, str> {
260        dt.kind.as_str().into()
261    }
262
263    fn plural(dt: &ApiResource) -> Cow<'_, str> {
264        dt.plural.as_str().into()
265    }
266
267    fn api_version(dt: &ApiResource) -> Cow<'_, str> {
268        dt.api_version.as_str().into()
269    }
270
271    fn meta(&self) -> &ObjectMeta {
272        &self.metadata
273    }
274
275    fn meta_mut(&mut self) -> &mut ObjectMeta {
276        &mut self.metadata
277    }
278}
279
280impl<P, U> HasSpec for Object<P, U>
281where
282    P: Clone,
283    U: Clone,
284{
285    type Spec = P;
286
287    fn spec(&self) -> &Self::Spec {
288        &self.spec
289    }
290
291    fn spec_mut(&mut self) -> &mut Self::Spec {
292        &mut self.spec
293    }
294}
295
296impl<P, U> HasStatus for Object<P, U>
297where
298    P: Clone,
299    U: Clone,
300{
301    type Status = U;
302
303    fn status(&self) -> Option<&Self::Status> {
304        self.status.as_ref()
305    }
306
307    fn status_mut(&mut self) -> &mut Option<Self::Status> {
308        &mut self.status
309    }
310}
311
312/// Empty struct for when data should be discarded
313///
314/// Not using [`()`](https://doc.rust-lang.org/stable/std/primitive.unit.html), because serde's
315/// [`Deserialize`](serde::Deserialize) `impl` is too strict.
316#[derive(Clone, Deserialize, Serialize, Default, Debug)]
317pub struct NotUsed {}
318
319#[cfg(test)]
320mod test {
321    use k8s_openapi::apimachinery::pkg::apis::meta::v1::{ListMeta, ObjectMeta};
322
323    use super::{ApiResource, HasSpec, HasStatus, NotUsed, Object, ObjectList, Resource, TypeMeta};
324    use crate::resource::ResourceExt;
325
326    #[test]
327    fn simplified_k8s_object() {
328        use k8s_openapi::api::core::v1::Pod;
329        // Replacing heavy type k8s_openapi::api::core::v1::PodSpec with:
330        #[derive(Clone)]
331        struct PodSpecSimple {
332            #[allow(dead_code)]
333            containers: Vec<ContainerSimple>,
334        }
335        #[derive(Clone, Debug, PartialEq)]
336        struct ContainerSimple {
337            #[allow(dead_code)]
338            image: String,
339        }
340        type PodSimple = Object<PodSpecSimple, NotUsed>;
341        // by grabbing the ApiResource info from the Resource trait
342        let ar = ApiResource::erase::<Pod>(&());
343        assert_eq!(ar.group, "");
344        assert_eq!(ar.kind, "Pod");
345        let data = PodSpecSimple {
346            containers: vec![ContainerSimple { image: "blog".into() }],
347        };
348        let mypod = PodSimple::new("blog", &ar, data).within("dev");
349
350        let meta = mypod.meta();
351        assert_eq!(&mypod.metadata, meta);
352        assert_eq!(meta.namespace.as_ref().unwrap(), "dev");
353        assert_eq!(meta.name.as_ref().unwrap(), "blog");
354        assert_eq!(mypod.types.as_ref().unwrap().kind, "Pod");
355        assert_eq!(mypod.types.as_ref().unwrap().api_version, "v1");
356
357        assert_eq!(mypod.namespace().unwrap(), "dev");
358        assert_eq!(mypod.name_unchecked(), "blog");
359        assert!(mypod.status().is_none());
360        assert_eq!(mypod.spec().containers[0], ContainerSimple {
361            image: "blog".into()
362        });
363
364        assert_eq!(PodSimple::api_version(&ar), "v1");
365        assert_eq!(PodSimple::version(&ar), "v1");
366        assert_eq!(PodSimple::plural(&ar), "pods");
367        assert_eq!(PodSimple::kind(&ar), "Pod");
368        assert_eq!(PodSimple::group(&ar), "");
369    }
370
371    #[test]
372    fn k8s_object_list() {
373        use k8s_openapi::api::core::v1::Pod;
374        // by grabbing the ApiResource info from the Resource trait
375        let ar = ApiResource::erase::<Pod>(&());
376        assert_eq!(ar.group, "");
377        assert_eq!(ar.kind, "Pod");
378        let podlist: ObjectList<Pod> = ObjectList {
379            types: TypeMeta {
380                api_version: ar.api_version,
381                kind: ar.kind + "List",
382            },
383            metadata: ListMeta { ..Default::default() },
384            items: vec![Pod {
385                metadata: ObjectMeta {
386                    name: Some("test".into()),
387                    namespace: Some("dev".into()),
388                    ..ObjectMeta::default()
389                },
390                spec: None,
391                status: None,
392            }],
393        };
394
395        assert_eq!(&podlist.types.kind, "PodList");
396        assert_eq!(&podlist.types.api_version, "v1");
397
398        let mypod = &podlist.items[0];
399        let meta = mypod.meta();
400        assert_eq!(&mypod.metadata, meta);
401        assert_eq!(meta.namespace.as_ref().unwrap(), "dev");
402        assert_eq!(meta.name.as_ref().unwrap(), "test");
403        assert_eq!(mypod.namespace().unwrap(), "dev");
404        assert_eq!(mypod.name_unchecked(), "test");
405        assert!(mypod.status.is_none());
406        assert!(mypod.spec.is_none());
407    }
408
409    #[test]
410    fn k8s_object_list_default_types() {
411        use k8s_openapi::api::core::v1::Pod;
412
413        let raw_value = serde_json::json!({
414            "metadata": {
415                "resourceVersion": ""
416            },
417            "items": []
418        });
419        let pod_list: ObjectList<Pod> = serde_json::from_value(raw_value).unwrap();
420        assert_eq!(
421            TypeMeta {
422                api_version: "v1".to_owned(),
423                kind: "List".to_owned(),
424            },
425            pod_list.types,
426        );
427    }
428}