[go: up one dir, main page]

kube-core 1.0.0

Kube shared types, traits and client-less behavior
Documentation
//! 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};