kube_core/
dynamic.rs

1//! Contains types for using resource kinds not known at compile-time.
2//!
3//! For concrete usage see [examples prefixed with dynamic_](https://github.com/kube-rs/kube/tree/main/examples).
4pub use crate::discovery::ApiResource;
5use crate::{
6    metadata::TypeMeta,
7    resource::{DynamicResourceScope, Resource},
8};
9
10use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta;
11use std::borrow::Cow;
12use thiserror::Error;
13
14#[derive(Debug, Error)]
15#[error("failed to parse this DynamicObject into a Resource: {source}")]
16/// Failed to parse `DynamicObject` into `Resource`
17pub struct ParseDynamicObjectError {
18    #[from]
19    source: serde_json::Error,
20}
21
22/// A dynamic representation of a kubernetes object
23///
24/// This will work with any non-list type object.
25#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq)]
26pub struct DynamicObject {
27    /// The type fields, not always present
28    #[serde(flatten, default)]
29    pub types: Option<TypeMeta>,
30    /// Object metadata
31    #[serde(default)]
32    pub metadata: ObjectMeta,
33
34    /// All other keys
35    #[serde(flatten)]
36    pub data: serde_json::Value,
37}
38
39impl DynamicObject {
40    /// Create a DynamicObject with minimal values set from ApiResource.
41    #[must_use]
42    pub fn new(name: &str, resource: &ApiResource) -> Self {
43        Self {
44            types: Some(TypeMeta {
45                api_version: resource.api_version.to_string(),
46                kind: resource.kind.to_string(),
47            }),
48            metadata: ObjectMeta {
49                name: Some(name.to_string()),
50                ..Default::default()
51            },
52            data: Default::default(),
53        }
54    }
55
56    /// Attach dynamic data to a DynamicObject
57    #[must_use]
58    pub fn data(mut self, data: serde_json::Value) -> Self {
59        self.data = data;
60        self
61    }
62
63    /// Attach a namespace to a DynamicObject
64    #[must_use]
65    pub fn within(mut self, ns: &str) -> Self {
66        self.metadata.namespace = Some(ns.into());
67        self
68    }
69
70    /// Attempt to convert this `DynamicObject` to a `Resource`
71    pub fn try_parse<K: Resource + for<'a> serde::Deserialize<'a>>(
72        self,
73    ) -> Result<K, ParseDynamicObjectError> {
74        Ok(serde_json::from_value(serde_json::to_value(self)?)?)
75    }
76}
77
78impl Resource for DynamicObject {
79    type DynamicType = ApiResource;
80    type Scope = DynamicResourceScope;
81
82    fn group(dt: &ApiResource) -> Cow<'_, str> {
83        dt.group.as_str().into()
84    }
85
86    fn version(dt: &ApiResource) -> Cow<'_, str> {
87        dt.version.as_str().into()
88    }
89
90    fn kind(dt: &ApiResource) -> Cow<'_, str> {
91        dt.kind.as_str().into()
92    }
93
94    fn api_version(dt: &ApiResource) -> Cow<'_, str> {
95        dt.api_version.as_str().into()
96    }
97
98    fn plural(dt: &ApiResource) -> Cow<'_, str> {
99        dt.plural.as_str().into()
100    }
101
102    fn meta(&self) -> &ObjectMeta {
103        &self.metadata
104    }
105
106    fn meta_mut(&mut self) -> &mut ObjectMeta {
107        &mut self.metadata
108    }
109}
110
111#[cfg(test)]
112mod test {
113    use crate::{
114        dynamic::{ApiResource, DynamicObject},
115        gvk::GroupVersionKind,
116        params::{Patch, PatchParams, PostParams},
117        request::Request,
118        resource::Resource,
119    };
120    use k8s_openapi::api::core::v1::Pod;
121
122    #[test]
123    fn raw_custom_resource() {
124        let gvk = GroupVersionKind::gvk("clux.dev", "v1", "Foo");
125        let res = ApiResource::from_gvk(&gvk);
126        let url = DynamicObject::url_path(&res, Some("myns"));
127
128        let pp = PostParams::default();
129        let req = Request::new(&url).create(&pp, vec![]).unwrap();
130        assert_eq!(req.uri(), "/apis/clux.dev/v1/namespaces/myns/foos?");
131        let patch_params = PatchParams::default();
132        let req = Request::new(url)
133            .patch("baz", &patch_params, &Patch::Merge(()))
134            .unwrap();
135        assert_eq!(req.uri(), "/apis/clux.dev/v1/namespaces/myns/foos/baz?");
136        assert_eq!(req.method(), "PATCH");
137    }
138
139    #[test]
140    fn raw_resource_in_default_group() {
141        let gvk = GroupVersionKind::gvk("", "v1", "Service");
142        let api_resource = ApiResource::from_gvk(&gvk);
143        let url = DynamicObject::url_path(&api_resource, None);
144        let pp = PostParams::default();
145        let req = Request::new(url).create(&pp, vec![]).unwrap();
146        assert_eq!(req.uri(), "/api/v1/services?");
147    }
148
149    #[test]
150    fn can_parse_dynamic_object_into_pod() -> Result<(), serde_json::Error> {
151        let original_pod: Pod = serde_json::from_value(serde_json::json!({
152            "apiVersion": "v1",
153            "kind": "Pod",
154            "metadata": { "name": "example" },
155            "spec": {
156                "containers": [{
157                    "name": "example",
158                    "image": "alpine",
159                    // Do nothing
160                    "command": ["tail", "-f", "/dev/null"],
161                }],
162            }
163        }))?;
164        let dynamic_pod: DynamicObject = serde_json::from_str(&serde_json::to_string(&original_pod)?)?;
165        let parsed_pod: Pod = dynamic_pod.try_parse().unwrap();
166
167        assert_eq!(parsed_pod, original_pod);
168
169        Ok(())
170    }
171}