1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
//! Traits and tyes for CustomResources

use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions as apiexts;

/// Types for v1 CustomResourceDefinitions
pub mod v1 {
    use super::apiexts::v1::CustomResourceDefinition as Crd;
    /// Extension trait that is implemented by kube-derive
    pub trait CustomResourceExt {
        /// Helper to generate the CRD including the JsonSchema
        ///
        /// This is using the stable v1::CustomResourceDefinitions (present in kubernetes >= 1.16)
        fn crd() -> Crd;
        /// Helper to return the name of this `CustomResourceDefinition` in kubernetes.
        ///
        /// This is not the name of an _instance_ of this custom resource but the `CustomResourceDefinition` object itself.
        fn crd_name() -> &'static str;
        /// Helper to generate the api information type for use with the dynamic `Api`
        fn api_resource() -> crate::discovery::ApiResource;
        /// Shortnames of this resource type.
        ///
        /// For example: [`Pod`] has the shortname alias `po`.
        ///
        /// NOTE: This function returns *declared* short names (at compile-time, using the `#[kube(shortname = "foo")]`), not the
        /// shortnames registered with the Kubernetes API (which is what tools such as `kubectl` look at).
        ///
        /// [`Pod`]: `k8s_openapi::api::core::v1::Pod`
        fn shortnames() -> &'static [&'static str];
    }

    /// Possible errors when merging CRDs
    #[derive(Debug, thiserror::Error)]
    pub enum MergeError {
        /// No crds given
        #[error("empty list of CRDs cannot be merged")]
        MissingCrds,

        /// Stored api not present
        #[error("stored api version {0} not found")]
        MissingStoredApi(String),

        /// Root api not present
        #[error("root api version {0} not found")]
        MissingRootVersion(String),

        /// No versions given in one crd to merge
        #[error("given CRD must have versions")]
        MissingVersions,

        /// Too many versions given to individual crds
        #[error("mergeable CRDs cannot have multiple versions")]
        MultiVersionCrd,

        /// Mismatching spec properties on crds
        #[error("mismatching {0} property from given CRDs")]
        PropertyMismatch(String),
    }

    /// Merge a collection of crds into a single multiversion crd
    ///
    /// Given multiple [`CustomResource`] derived types granting [`CRD`]s via [`CustomResourceExt::crd`],
    /// we can merge them into a single [`CRD`] with multiple [`CRDVersion`] objects, marking only
    /// the specified apiversion as `storage: true`.
    ///
    /// This merge algorithm assumes that every [`CRD`]:
    ///
    /// - exposes exactly one [`CRDVersion`]
    /// - uses identical values for `spec.group`, `spec.scope`, and `spec.names.kind`
    ///
    /// This is always true for [`CustomResource`] derives.
    ///
    /// ## Usage
    ///
    /// ```no_run
    /// # use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition;
    /// use kube::core::crd::merge_crds;
    /// # let mycrd_v1: CustomResourceDefinition = todo!(); // v1::MyCrd::crd();
    /// # let mycrd_v2: CustomResourceDefinition = todo!(); // v2::MyCrd::crd();
    /// let crds = vec![mycrd_v1, mycrd_v2];
    /// let multi_version_crd = merge_crds(crds, "v1").unwrap();
    /// ```
    ///
    /// Note the merge is done by marking the:
    ///
    /// - crd containing the `stored_apiversion` as the place the other crds merge their [`CRDVersion`] items
    /// - stored version is marked with `storage: true`, while all others get `storage: false`
    ///
    /// [`CustomResourceExt::crd`]: crate::CustomResourceExt::crd
    /// [`CRD`]: https://docs.rs/k8s-openapi/latest/k8s_openapi/apiextensions_apiserver/pkg/apis/apiextensions/v1/struct.CustomResourceDefinition.html
    /// [`CRDVersion`]: https://docs.rs/k8s-openapi/latest/k8s_openapi/apiextensions_apiserver/pkg/apis/apiextensions/v1/struct.CustomResourceDefinitionVersion.html
    /// [`CustomResource`]: https://docs.rs/kube/latest/kube/derive.CustomResource.html
    pub fn merge_crds(mut crds: Vec<Crd>, stored_apiversion: &str) -> Result<Crd, MergeError> {
        if crds.is_empty() {
            return Err(MergeError::MissingCrds);
        }
        for crd in crds.iter() {
            if crd.spec.versions.is_empty() {
                return Err(MergeError::MissingVersions);
            }
            if crd.spec.versions.len() != 1 {
                return Err(MergeError::MultiVersionCrd);
            }
        }
        let ver = stored_apiversion;
        let found = crds.iter().position(|c| c.spec.versions[0].name == ver);
        // Extract the root/first object to start with (the one we will merge into)
        let mut root = match found {
            None => return Err(MergeError::MissingRootVersion(ver.into())),
            Some(idx) => crds.remove(idx),
        };
        root.spec.versions[0].storage = true; // main version - set true in case modified

        // Values that needs to be identical across crds:
        let group = &root.spec.group;
        let kind = &root.spec.names.kind;
        let scope = &root.spec.scope;
        // sanity; don't merge crds with mismatching groups, versions, or other core properties
        for crd in crds.iter() {
            if &crd.spec.group != group {
                return Err(MergeError::PropertyMismatch("group".to_string()));
            }
            if &crd.spec.names.kind != kind {
                return Err(MergeError::PropertyMismatch("kind".to_string()));
            }
            if &crd.spec.scope != scope {
                return Err(MergeError::PropertyMismatch("scope".to_string()));
            }
        }

        // combine all version objects into the root object
        let versions = &mut root.spec.versions;
        while let Some(mut crd) = crds.pop() {
            while let Some(mut v) = crd.spec.versions.pop() {
                v.storage = false; // secondary versions
                versions.push(v);
            }
        }
        Ok(root)
    }

    mod tests {
        #[test]
        fn crd_merge() {
            use super::{merge_crds, Crd};
            let crd1 = r#"
            apiVersion: apiextensions.k8s.io/v1
            kind: CustomResourceDefinition
            metadata:
              name: multiversions.kube.rs
            spec:
              group: kube.rs
              names:
                categories: []
                kind: MultiVersion
                plural: multiversions
                shortNames: []
                singular: multiversion
              scope: Namespaced
              versions:
              - additionalPrinterColumns: []
                name: v1
                schema:
                  openAPIV3Schema:
                    type: object
                    x-kubernetes-preserve-unknown-fields: true
                served: true
                storage: true"#;

            let crd2 = r#"
            apiVersion: apiextensions.k8s.io/v1
            kind: CustomResourceDefinition
            metadata:
              name: multiversions.kube.rs
            spec:
              group: kube.rs
              names:
                categories: []
                kind: MultiVersion
                plural: multiversions
                shortNames: []
                singular: multiversion
              scope: Namespaced
              versions:
              - additionalPrinterColumns: []
                name: v2
                schema:
                  openAPIV3Schema:
                    type: object
                    x-kubernetes-preserve-unknown-fields: true
                served: true
                storage: true"#;

            let expected = r#"
            apiVersion: apiextensions.k8s.io/v1
            kind: CustomResourceDefinition
            metadata:
              name: multiversions.kube.rs
            spec:
              group: kube.rs
              names:
                categories: []
                kind: MultiVersion
                plural: multiversions
                shortNames: []
                singular: multiversion
              scope: Namespaced
              versions:
              - additionalPrinterColumns: []
                name: v2
                schema:
                  openAPIV3Schema:
                    type: object
                    x-kubernetes-preserve-unknown-fields: true
                served: true
                storage: true
              - additionalPrinterColumns: []
                name: v1
                schema:
                  openAPIV3Schema:
                    type: object
                    x-kubernetes-preserve-unknown-fields: true
                served: true
                storage: false"#;

            let c1: Crd = serde_yaml::from_str(crd1).unwrap();
            let c2: Crd = serde_yaml::from_str(crd2).unwrap();
            let ce: Crd = serde_yaml::from_str(expected).unwrap();
            let combined = merge_crds(vec![c1, c2], "v2").unwrap();

            let combo_json = serde_json::to_value(&combined).unwrap();
            let exp_json = serde_json::to_value(&ce).unwrap();
            assert_json_diff::assert_json_eq!(combo_json, exp_json);
        }
    }
}

// re-export current latest (v1)
pub use v1::{merge_crds, CustomResourceExt, MergeError};