kube_core/
crd.rs

1//! Traits and tyes for CustomResources
2
3use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions as apiexts;
4
5/// Types for v1 CustomResourceDefinitions
6pub mod v1 {
7    use super::apiexts::v1::CustomResourceDefinition as Crd;
8    /// Extension trait that is implemented by kube-derive
9    pub trait CustomResourceExt {
10        /// Helper to generate the CRD including the JsonSchema
11        ///
12        /// This is using the stable v1::CustomResourceDefinitions (present in kubernetes >= 1.16)
13        fn crd() -> Crd;
14        /// Helper to return the name of this `CustomResourceDefinition` in kubernetes.
15        ///
16        /// This is not the name of an _instance_ of this custom resource but the `CustomResourceDefinition` object itself.
17        fn crd_name() -> &'static str;
18        /// Helper to generate the api information type for use with the dynamic `Api`
19        fn api_resource() -> crate::discovery::ApiResource;
20        /// Shortnames of this resource type.
21        ///
22        /// For example: [`Pod`] has the shortname alias `po`.
23        ///
24        /// NOTE: This function returns *declared* short names (at compile-time, using the `#[kube(shortname = "foo")]`), not the
25        /// shortnames registered with the Kubernetes API (which is what tools such as `kubectl` look at).
26        ///
27        /// [`Pod`]: `k8s_openapi::api::core::v1::Pod`
28        fn shortnames() -> &'static [&'static str];
29    }
30
31    /// Possible errors when merging CRDs
32    #[derive(Debug, thiserror::Error)]
33    pub enum MergeError {
34        /// No crds given
35        #[error("empty list of CRDs cannot be merged")]
36        MissingCrds,
37
38        /// Stored api not present
39        #[error("stored api version {0} not found")]
40        MissingStoredApi(String),
41
42        /// Root api not present
43        #[error("root api version {0} not found")]
44        MissingRootVersion(String),
45
46        /// No versions given in one crd to merge
47        #[error("given CRD must have versions")]
48        MissingVersions,
49
50        /// Too many versions given to individual crds
51        #[error("mergeable CRDs cannot have multiple versions")]
52        MultiVersionCrd,
53
54        /// Mismatching spec properties on crds
55        #[error("mismatching {0} property from given CRDs")]
56        PropertyMismatch(String),
57    }
58
59    /// Merge a collection of crds into a single multiversion crd
60    ///
61    /// Given multiple [`CustomResource`] derived types granting [`CRD`]s via [`CustomResourceExt::crd`],
62    /// we can merge them into a single [`CRD`] with multiple [`CRDVersion`] objects, marking only
63    /// the specified apiversion as `storage: true`.
64    ///
65    /// This merge algorithm assumes that every [`CRD`]:
66    ///
67    /// - exposes exactly one [`CRDVersion`]
68    /// - uses identical values for `spec.group`, `spec.scope`, and `spec.names.kind`
69    ///
70    /// This is always true for [`CustomResource`] derives.
71    ///
72    /// ## Usage
73    ///
74    /// ```no_run
75    /// # use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition;
76    /// use kube::core::crd::merge_crds;
77    /// # let mycrd_v1: CustomResourceDefinition = todo!(); // v1::MyCrd::crd();
78    /// # let mycrd_v2: CustomResourceDefinition = todo!(); // v2::MyCrd::crd();
79    /// let crds = vec![mycrd_v1, mycrd_v2];
80    /// let multi_version_crd = merge_crds(crds, "v1").unwrap();
81    /// ```
82    ///
83    /// Note the merge is done by marking the:
84    ///
85    /// - crd containing the `stored_apiversion` as the place the other crds merge their [`CRDVersion`] items
86    /// - stored version is marked with `storage: true`, while all others get `storage: false`
87    ///
88    /// [`CustomResourceExt::crd`]: crate::CustomResourceExt::crd
89    /// [`CRD`]: https://docs.rs/k8s-openapi/latest/k8s_openapi/apiextensions_apiserver/pkg/apis/apiextensions/v1/struct.CustomResourceDefinition.html
90    /// [`CRDVersion`]: https://docs.rs/k8s-openapi/latest/k8s_openapi/apiextensions_apiserver/pkg/apis/apiextensions/v1/struct.CustomResourceDefinitionVersion.html
91    /// [`CustomResource`]: https://docs.rs/kube/latest/kube/derive.CustomResource.html
92    pub fn merge_crds(mut crds: Vec<Crd>, stored_apiversion: &str) -> Result<Crd, MergeError> {
93        if crds.is_empty() {
94            return Err(MergeError::MissingCrds);
95        }
96        for crd in crds.iter() {
97            if crd.spec.versions.is_empty() {
98                return Err(MergeError::MissingVersions);
99            }
100            if crd.spec.versions.len() != 1 {
101                return Err(MergeError::MultiVersionCrd);
102            }
103        }
104        let ver = stored_apiversion;
105        let found = crds.iter().position(|c| c.spec.versions[0].name == ver);
106        // Extract the root/first object to start with (the one we will merge into)
107        let mut root = match found {
108            None => return Err(MergeError::MissingRootVersion(ver.into())),
109            Some(idx) => crds.remove(idx),
110        };
111        root.spec.versions[0].storage = true; // main version - set true in case modified
112
113        // Values that needs to be identical across crds:
114        let group = &root.spec.group;
115        let kind = &root.spec.names.kind;
116        let scope = &root.spec.scope;
117        // sanity; don't merge crds with mismatching groups, versions, or other core properties
118        for crd in crds.iter() {
119            if &crd.spec.group != group {
120                return Err(MergeError::PropertyMismatch("group".to_string()));
121            }
122            if &crd.spec.names.kind != kind {
123                return Err(MergeError::PropertyMismatch("kind".to_string()));
124            }
125            if &crd.spec.scope != scope {
126                return Err(MergeError::PropertyMismatch("scope".to_string()));
127            }
128        }
129
130        // combine all version objects into the root object
131        let versions = &mut root.spec.versions;
132        while let Some(mut crd) = crds.pop() {
133            while let Some(mut v) = crd.spec.versions.pop() {
134                v.storage = false; // secondary versions
135                versions.push(v);
136            }
137        }
138        Ok(root)
139    }
140
141    mod tests {
142        #[test]
143        fn crd_merge() {
144            use super::{merge_crds, Crd};
145            let crd1 = r#"
146            apiVersion: apiextensions.k8s.io/v1
147            kind: CustomResourceDefinition
148            metadata:
149              name: multiversions.kube.rs
150            spec:
151              group: kube.rs
152              names:
153                categories: []
154                kind: MultiVersion
155                plural: multiversions
156                shortNames: []
157                singular: multiversion
158              scope: Namespaced
159              versions:
160              - additionalPrinterColumns: []
161                name: v1
162                schema:
163                  openAPIV3Schema:
164                    type: object
165                    x-kubernetes-preserve-unknown-fields: true
166                served: true
167                storage: true"#;
168
169            let crd2 = r#"
170            apiVersion: apiextensions.k8s.io/v1
171            kind: CustomResourceDefinition
172            metadata:
173              name: multiversions.kube.rs
174            spec:
175              group: kube.rs
176              names:
177                categories: []
178                kind: MultiVersion
179                plural: multiversions
180                shortNames: []
181                singular: multiversion
182              scope: Namespaced
183              versions:
184              - additionalPrinterColumns: []
185                name: v2
186                schema:
187                  openAPIV3Schema:
188                    type: object
189                    x-kubernetes-preserve-unknown-fields: true
190                served: true
191                storage: true"#;
192
193            let expected = r#"
194            apiVersion: apiextensions.k8s.io/v1
195            kind: CustomResourceDefinition
196            metadata:
197              name: multiversions.kube.rs
198            spec:
199              group: kube.rs
200              names:
201                categories: []
202                kind: MultiVersion
203                plural: multiversions
204                shortNames: []
205                singular: multiversion
206              scope: Namespaced
207              versions:
208              - additionalPrinterColumns: []
209                name: v2
210                schema:
211                  openAPIV3Schema:
212                    type: object
213                    x-kubernetes-preserve-unknown-fields: true
214                served: true
215                storage: true
216              - additionalPrinterColumns: []
217                name: v1
218                schema:
219                  openAPIV3Schema:
220                    type: object
221                    x-kubernetes-preserve-unknown-fields: true
222                served: true
223                storage: false"#;
224
225            let c1: Crd = serde_yaml::from_str(crd1).unwrap();
226            let c2: Crd = serde_yaml::from_str(crd2).unwrap();
227            let ce: Crd = serde_yaml::from_str(expected).unwrap();
228            let combined = merge_crds(vec![c1, c2], "v2").unwrap();
229
230            let combo_json = serde_json::to_value(&combined).unwrap();
231            let exp_json = serde_json::to_value(&ce).unwrap();
232            assert_json_diff::assert_json_eq!(combo_json, exp_json);
233        }
234    }
235}
236
237// re-export current latest (v1)
238pub use v1::{merge_crds, CustomResourceExt, MergeError};