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};