239 lines
9.2 KiB
Rust
239 lines
9.2 KiB
Rust
//! 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};
|