diff --git a/Cargo.toml b/Cargo.toml index 8b987f7..915757d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,19 @@ serde = "1.0.204" serde_json = "1.0.122" + [dependencies.thiserror] + optional = true + version = "2.0.11" + + [dependencies.jsonptr] + optional = true + version = "0.7.1" + + [dependencies.semver] + optional = true + features = ["serde"] + version = "1.0" + [dependencies.k8s-openapi] features = ["schemars", "latest"] version = "0.24.0" @@ -20,3 +33,6 @@ readme = "README.md" repository = "https://github.com/capi-samples/cluster-api-rs" version = "1.9.5" + +[features] +contract = ["dep:semver", "dep:jsonptr", "dep:thiserror"] diff --git a/src/contract/bootstrap.rs b/src/contract/bootstrap.rs new file mode 100644 index 0000000..dca0465 --- /dev/null +++ b/src/contract/bootstrap.rs @@ -0,0 +1,56 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +use super::types::Path; + +// BootstrapContract encodes information about the Cluster API contract for bootstrap objects. +pub struct BootstrapContract; + +// Bootstrap provide access to the information about the Cluster API contract for bootstrap objects. +pub fn bootstrap() -> BootstrapContract { + BootstrapContract +} + +impl BootstrapContract { + // Ready provide access to status.ready field in a bootstrap object. + pub fn ready(&self) -> Path { + Path::from_tokens(["status", "ready"].into_iter()) + } + + // ReadyConditionType returns the type of the ready condition. + pub fn ready_condition_type(&self) -> String { + "Ready".to_string() + } + + // DataSecretName provide access to status.dataSecretName field in a bootstrap object. + pub fn data_secret_name(&self) -> Path { + Path::from_tokens(["status", "dataSecretName"].into_iter()) + } + + // FailureReason provides access to the status.failureReason field in an bootstrap object. Note that this field is optional. + // + // Deprecated: This function is deprecated and is going to be removed. Please see https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20240916-improve-status-in-CAPI-resources.md for more details. + pub fn failure_reason(&self) -> Path { + Path::from_tokens(["status", "failureReason"].into_iter()) + } + + // FailureMessage provides access to the status.failureMessage field in an bootstrap object. Note that this field is optional. + // + // Deprecated: This function is deprecated and is going to be removed. Please see https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20240916-improve-status-in-CAPI-resources.md for more details. + pub fn failure_message(&self) -> Path { + Path::from_tokens(["status", "failureMessage"].into_iter()) + } +} diff --git a/src/contract/bootstrap_config_template.rs b/src/contract/bootstrap_config_template.rs new file mode 100644 index 0000000..8bd246e --- /dev/null +++ b/src/contract/bootstrap_config_template.rs @@ -0,0 +1,44 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +use super::{metadata::Metadata, types::Path}; + +// BootstrapConfigTemplateContract encodes information about the Cluster API contract for BootstrapConfigTemplate objects +// like KubeadmConfigTemplate, etc. +pub struct BootstrapConfigTemplateContract; + +pub fn bootstrap_config_template() -> *const BootstrapConfigTemplateContract { + &BootstrapConfigTemplateContract +} + +impl BootstrapConfigTemplateContract { + // Template provides access to the template. + pub fn template(&self) -> *const BootstrapConfigTemplateTemplate { + &BootstrapConfigTemplateTemplate + } +} + +// BootstrapConfigTemplateTemplate provides a helper struct for working with the template in an BootstrapConfigTemplate. +pub struct BootstrapConfigTemplateTemplate; + +impl BootstrapConfigTemplateTemplate { + // Metadata provides access to the metadata of a template. + pub fn metadata(&self) -> Metadata { + Metadata::new(Path::from_tokens( + ["spec", "template", "metadata"].into_iter(), + )) + } +} diff --git a/src/contract/controlplane.rs b/src/contract/controlplane.rs new file mode 100644 index 0000000..482e2e6 --- /dev/null +++ b/src/contract/controlplane.rs @@ -0,0 +1,279 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +use k8s_openapi::api::core::v1::ObjectReference; +use kube::{api::DynamicObject, core::Duration}; +use semver::Version; + +use super::{ + metadata::Metadata, + types::{Error, Path, Paths, Result}, +}; + +// ControlPlaneContract encodes information about the Cluster API contract for ControlPlane objects +// like e.g the KubeadmControlPlane etc. +pub struct ControlPlaneContract; + +// ControlPlane provide access to the information about the Cluster API contract for ControlPlane objects. +pub fn control_plane() -> ControlPlaneContract { + ControlPlaneContract +} + +impl ControlPlaneContract { + // MachineTemplate provides access to MachineTemplate in a ControlPlane object, if any. + // NOTE: When working with unstructured there is no way to understand if the ControlPlane provider + // do support a field in the type definition from the fact that a field is not set in a given instance. + // This is why in we are deriving if MachineTemplate is required from the ClusterClass in the topology reconciler code. + pub fn machine_template(&self) -> ControlPlaneMachineTemplate { + ControlPlaneMachineTemplate + } + + // Version provide access to version field in a ControlPlane object, if any. + // NOTE: When working with unstructured there is no way to understand if the ControlPlane provider + // do support a field in the type definition from the fact that a field is not set in a given instance. + // This is why in we are deriving if version is required from the ClusterClass in the topology reconciler code. + pub fn version(&self) -> Path { + Path::from_tokens(["spec", "version"].into_iter()) + } + + // StatusVersion provide access to the version field in a ControlPlane object status, if any. + pub fn status_version(&self) -> Path { + Path::from_tokens(["status", "version"].into_iter()) + } + + // Ready provide access to the status.ready field in a ControlPlane object. + pub fn ready(&self) -> Path { + Path::from_tokens(["status", "ready"].into_iter()) + } + + // Initialized provide access to status.initialized field in a ControlPlane object. + pub fn initialized(&self) -> Path { + Path::from_tokens(["status", "initialized"].into_iter()) + } + + // Replicas provide access to replicas field in a ControlPlane object, if any. + // NOTE: When working with unstructured there is no way to understand if the ControlPlane provider + // do support a field in the type definition from the fact that a field is not set in a given instance. + // This is why in we are deriving if replicas is required from the ClusterClass in the topology reconciler code. + pub fn replicas(&self) -> Path { + Path::from_tokens(["spec", "replicas"].into_iter()) + } + + // StatusReplicas provide access to the status.replicas field in a ControlPlane object, if any. Applies to implementations using replicas. + pub fn status_replicas(&self) -> Path { + Path::from_tokens(["status", "replicas"].into_iter()) + } + + // UpdatedReplicas provide access to the status.updatedReplicas field in a ControlPlane object, if any. Applies to implementations using replicas. + pub fn updated_replicas(&self) -> Path { + Path::from_tokens(["status", "updatedReplicas"].into_iter()) + } + + // ReadyReplicas provide access to the status.readyReplicas field in a ControlPlane object, if any. Applies to implementations using replicas. + pub fn ready_replicas(&self) -> Path { + Path::from_tokens(["status", "readyReplicas"].into_iter()) + } + + // UnavailableReplicas provide access to the status.unavailableReplicas field in a ControlPlane object, if any. Applies to implementations using replicas. + pub fn unavailable_replicas(&self) -> Path { + Path::from_tokens(["status", "unavailableReplicas"].into_iter()) + } + + // V1Beta2ReadyReplicas provide access to readyReplicas field in a ControlPlane object, if any. Applies to implementations using replicas. + pub fn v1_beta2_ready_replicas(&self) -> Paths { + Paths::new(vec![ + Path::from_tokens(["status", "v1beta2", "readyReplicas"].into_iter()), + Path::from_tokens(["status", "readyReplicas"].into_iter()), + ]) + } + + // V1Beta2AvailableReplicas provide access to the availableReplicas field in a ControlPlane object, if any. Applies to implementations using replicas. + pub fn v1_beta2_available_replicas(&self) -> Paths { + Paths::new(vec![ + Path::from_tokens(["status", "v1beta2", "availableReplicas"].into_iter()), + Path::from_tokens(["status", "availableReplicas"].into_iter()), + ]) + } + + // V1Beta2UpToDateReplicas provide access to the upToDateReplicas field in a ControlPlane object, if any. Applies to implementations using replicas. + pub fn v1_beta2_up_to_date_replicas(&self) -> Paths { + Paths::new(vec![ + Path::from_tokens(["status", "v1beta2", "upToDateReplicas"].into_iter()), + Path::from_tokens(["status", "upToDateReplicas"].into_iter()), + ]) + } + + // AvailableConditionType returns the type of the available condition. + pub fn available_condition_type(&self) -> String { + "Available".to_string() + } + + // Selector provide access to the status.selector field in a ControlPlane object, if any. Applies to implementations using replicas. + pub fn selector(&self) -> Path { + Path::from_tokens(["status", "selector"].into_iter()) + } + + // FailureReason provides access to the status.failureReason field in an ControlPlane object. Note that this field is optional. + // + // Deprecated: This function is deprecated and is going to be removed. Please see https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20240916-improve-status-in-CAPI-resources.md for more details. + pub fn failure_reason(&self) -> Path { + Path::from_tokens(["status", "failureReason"].into_iter()) + } + + // FailureMessage provides access to the status.failureMessage field in an ControlPlane object. Note that this field is optional. + // + // Deprecated: This function is deprecated and is going to be removed. Please see https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20240916-improve-status-in-CAPI-resources.md for more details. + pub fn failure_message(&self) -> Path { + Path::from_tokens(["status", "failureMessage"].into_iter()) + } + + // ExternalManagedControlPlane provides access to the status.externalManagedControlPlane field in an ControlPlane object. + // Note that this field is optional. + pub fn external_managed_control_plane(&self) -> Path { + Path::from_tokens(["status", "externalManagedControlPlane"].into_iter()) + } + + // IsProvisioning returns true if the control plane is being created for the first time. + // Returns false, if the control plane was already previously provisioned. + pub fn is_provisioning(&self, obj: &DynamicObject) -> Result { + // We can know if the control plane was previously created or is being cretaed for the first + // time by looking at controlplane.status.version. If the version in status is set to a valid + // value then the control plane was already provisioned at a previous time. If not, we can + // assume that the control plane is being created for the first time. + match self.status_version().get(obj) { + Ok(_) => Ok(false), + // Empty version + Err(Error::Serde(_)) => Ok(true), + // Missing version + Err(Error::ResolveErr(_)) => Ok(true), + Err(e) => Err(e)?, + } + } + + // IsUpgrading returns true if the control plane is in the middle of an upgrade, false otherwise. + // A control plane is considered upgrading if: + // - if spec.version is greater than status.version. + // Note: A control plane is considered not upgrading if the status or status.version is not set. + pub fn is_upgrading(&self, obj: &DynamicObject) -> Result { + let spec_version = self.version().get(obj)?; + match self.status_version().get(obj) { + Ok(version) => Ok(spec_version > version), + // status version is not yet set + // If the status.version is not yet present in the object, it implies the + // first machine of the control plane is provisioning. We can reasonably assume + // that the control plane is not upgrading at this stage. + Err(Error::ResolveErr(_)) => Ok(false), + Err(e) => Err(e)?, + } + } + + // IsScaling returns true if the control plane is in the middle of a scale operation, false otherwise. + // A control plane is considered scaling if: + // - status.replicas is not yet set. + // - spec.replicas != status.replicas. + // - spec.replicas != status.updatedReplicas. + // - spec.replicas != status.readyReplicas. + // - status.unavailableReplicas > 0. + pub fn is_scaling(&self, obj: &DynamicObject) -> Result { + let desired_replicas = self.replicas().get(obj)?; + + let status_replicas = match self.status_replicas().get(obj) { + Ok(replicas) => replicas, + // status is probably not yet set on the control plane + // if status is missing we can consider the control plane to be scaling + // so that we can block any operations that expect control plane to be stable. + Err(Error::ResolveErr(_)) => return Ok(true), + e => e?, + }; + + let updated_replicas = match self.updated_replicas().get(obj) { + Ok(replicas) => replicas, + // If updatedReplicas is not set on the control plane + // we should consider the control plane to be scaling so that + // we block any operation that expect the control plane to be stable. + Err(Error::ResolveErr(_)) => return Ok(true), + e => e?, + }; + + let ready_replicas = match self.ready_replicas().get(obj) { + Ok(replicas) => replicas, + // If readyReplicas is not set on the control plane + // we should consider the control plane to be scaling so that + // we block any operation that expect the control plane to be stable. + Err(Error::ResolveErr(_)) => return Ok(true), + e => e?, + }; + + let unavailable_replicas = match self.unavailable_replicas().get(obj) { + Ok(replicas) => replicas, + // If unavailableReplicas is not set on the control plane we assume it is 0. + // We have to do this as the following happens after clusterctl move with KCP: + // * clusterctl move creates the KCP object without status + // * the KCP controller won't patch the field to 0 if it doesn't exist + // * This is because the patchHelper marshals before/after object to JSON to calculate a diff + // and as the unavailableReplicas field is not a pointer, not set and 0 are both rendered as 0. + // If before/after of the field is the same (i.e. 0), there is no diff and thus also no patch to set it to 0. + Err(Error::ResolveErr(_)) => 0, + e => e?, + }; + + // Control plane is still scaling if: + // * .spec.replicas, .status.replicas, .status.updatedReplicas, + // .status.readyReplicas are not equal and + // * unavailableReplicas > 0 + if status_replicas != desired_replicas + || updated_replicas != desired_replicas + || ready_replicas != desired_replicas + || unavailable_replicas > 0 + { + return Ok(true); + } + + return Ok(false); + } +} + +// ControlPlaneMachineTemplate provides a helper struct for working with MachineTemplate in ClusterClass. +pub struct ControlPlaneMachineTemplate; + +impl ControlPlaneMachineTemplate { + // InfrastructureRef provides access to the infrastructureRef of a MachineTemplate. + pub fn infrastructure_ref(&self) -> Path { + Path::from_tokens(["spec", "machineTemplate", "infrastructureRef"].into_iter()) + } + + // Metadata provides access to the metadata of a MachineTemplate. + pub fn metadata(&self) -> Metadata { + Metadata::new(Path::from_tokens( + ["spec", "machineTemplate", "metadata"].into_iter(), + )) + } + + // NodeDrainTimeout provides access to the nodeDrainTimeout of a MachineTemplate. + pub fn node_drain_timeout(&self) -> Path { + Path::from_tokens(["spec", "machineTemplate", "nodeDrainTimeout"].into_iter()) + } + + // NodeVolumeDetachTimeout provides access to the nodeVolumeDetachTimeout of a MachineTemplate. + pub fn node_volume_detach_timeout(&self) -> Path { + Path::from_tokens(["spec", "machineTemplate", "nodeVolumeDetachTimeout"].into_iter()) + } + + // NodeDeletionTimeout provides access to the nodeDeletionTimeout of a MachineTemplate. + pub fn node_deletion_timeout(&self) -> Path { + Path::from_tokens(["spec", "machineTemplate", "nodeDeletionTimeout"].into_iter()) + } +} diff --git a/src/contract/controlplane_template.rs b/src/contract/controlplane_template.rs new file mode 100644 index 0000000..24f23c1 --- /dev/null +++ b/src/contract/controlplane_template.rs @@ -0,0 +1,124 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +use k8s_openapi::api::core::v1::ObjectReference; +use kube::core::Duration; + +use super::{metadata::Metadata, types::Path}; + +// ControlPlaneTemplateContract encodes information about the Cluster API contract for ControlPlaneTemplate objects +// like e.g. the KubeadmControlPlane etc. +pub struct ControlPlaneTemplateContract; + +// ControlPlaneTemplate provide access to the information about the Cluster API contract for ControlPlaneTemplate objects. +pub fn control_plane_template() -> ControlPlaneTemplateContract { + ControlPlaneTemplateContract +} + +impl ControlPlaneTemplateContract { + // InfrastructureMachineTemplate provide access to InfrastructureMachineTemplate reference, if any. + // NOTE: When working with unstructured there is no way to understand if the ControlPlane provider + // do support a field in the type definition from the fact that a field is not set in a given instance. + // This is why in we are deriving if this field is required from the ClusterClass in the topology reconciler code. + pub fn infrastructure_machine_template(&self) -> Path { + Path::from_tokens( + [ + "spec", + "template", + "spec", + "machineTemplate", + "infrastructureRef", + ] + .into_iter(), + ) + } + + // Template provides access to the template. + pub fn template(&self) -> ControlPlaneTemplateTemplate { + ControlPlaneTemplateTemplate + } +} + +// ControlPlaneTemplateTemplate provides a helper struct for working with the template in an ControlPlaneTemplate. +pub struct ControlPlaneTemplateTemplate; + +impl ControlPlaneTemplateTemplate { + // Metadata provides access to the metadata of a template. + pub fn metadata(&self) -> Metadata { + Metadata::new(Path::from_tokens( + ["spec", "template", "metadata"].into_iter(), + )) + } + + // MachineTemplate provides access to MachineTemplate in a ControlPlaneTemplate object, if any. + pub fn machine_template(&self) -> ControlPlaneTemplateMachineTemplate { + ControlPlaneTemplateMachineTemplate + } +} + +// ControlPlaneTemplateMachineTemplate provides a helper struct for working with MachineTemplate. +pub struct ControlPlaneTemplateMachineTemplate; + +impl ControlPlaneTemplateMachineTemplate { + // Metadata provides access to the metadata of the MachineTemplate of a ControlPlaneTemplate. + pub fn metadata(&self) -> Metadata { + Metadata::new(Path::from_tokens( + ["spec", "template", "spec", "machineTemplate", "metadata"].into_iter(), + )) + } + + // NodeDrainTimeout provides access to the nodeDrainTimeout of a MachineTemplate. + pub fn node_drain_timeout(&self) -> Path { + Path::from_tokens( + [ + "spec", + "template", + "spec", + "machineTemplate", + "nodeDrainTimeout", + ] + .into_iter(), + ) + } + + // NodeVolumeDetachTimeout provides access to the nodeVolumeDetachTimeout of a MachineTemplate. + pub fn node_volume_detach_timeout(&self) -> Path { + Path::from_tokens( + [ + "spec", + "template", + "spec", + "machineTemplate", + "nodeVolumeDetachTimeout", + ] + .into_iter(), + ) + } + + // NodeDeletionTimeout provides access to the nodeDeletionTimeout of a MachineTemplate. + pub fn node_deletion_timeout(&self) -> Path { + Path::from_tokens( + [ + "spec", + "template", + "spec", + "machineTemplate", + "nodeDeletionTimeout", + ] + .into_iter(), + ) + } +} diff --git a/src/contract/infrastructure_cluster.rs b/src/contract/infrastructure_cluster.rs new file mode 100644 index 0000000..29430dc --- /dev/null +++ b/src/contract/infrastructure_cluster.rs @@ -0,0 +1,124 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +use std::collections::BTreeMap; + +use jsonptr::PointerBuf; +use kube::api::DynamicObject; + +use crate::capi_cluster::ClusterStatusFailureDomains; + +use super::types::{Path, Result}; + +type FailureDomains = BTreeMap; + +// InfrastructureClusterContract encodes information about the Cluster API contract for InfrastructureCluster objects +// like DockerClusters, AWS Clusters, etc. +pub struct InfrastructureClusterContract; + +pub fn infrastructure_cluster() -> InfrastructureClusterContract { + InfrastructureClusterContract +} + +impl InfrastructureClusterContract { + // ControlPlaneEndpoint provides access to ControlPlaneEndpoint in an InfrastructureCluster object. + pub fn control_plane_endpoint(&self) -> InfrastructureClusterControlPlaneEndpoint { + InfrastructureClusterControlPlaneEndpoint + } +} + +// InfrastructureClusterControlPlaneEndpoint provides a helper struct for working with ControlPlaneEndpoint +// in an InfrastructureCluster object. +pub struct InfrastructureClusterControlPlaneEndpoint; + +impl InfrastructureClusterControlPlaneEndpoint { + // Host provides access to the host field in the ControlPlaneEndpoint in an InfrastructureCluster object. + pub fn host(&self) -> Path { + Path::from_tokens(["spec", "controlPlaneEndpoint", "host"].into_iter()) + } + + // Port provides access to the port field in the ControlPlaneEndpoint in an InfrastructureCluster object. + pub fn port(&self) -> Path { + Path::from_tokens(["spec", "controlPlaneEndpoint", "port"].into_iter()) + } +} + +impl InfrastructureClusterContract { + // Ready provides access to the status.ready field in an InfrastructureCluster object. + pub fn ready(&self) -> Path { + Path::from_tokens(["status", "ready"].into_iter()) + } + + // ReadyConditionType returns the type of the ready condition. + pub fn ready_condition_type(&self) -> String { + "Ready".to_string() + } + + // FailureReason provides access to the status.failureReason field in an InfrastructureCluster object. Note that this field is optional. + // + // Deprecated: This function is deprecated and is going to be removed. Please see https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20240916-improve-status-in-CAPI-resources.md for more details. + pub fn failure_reason(&self) -> Path { + Path::from_tokens(["status", "failureReason"].into_iter()) + } + + // FailureMessage provides access to the status.failureMessage field in an InfrastructureCluster object. Note that this field is optional. + // + // Deprecated: This function is deprecated and is going to be removed. Please see https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20240916-improve-status-in-CAPI-resources.md for more details. + pub fn failure_message(&self) -> Path { + Path::from_tokens(["status", "failureMessage"].into_iter()) + } + + // FailureDomains provides access to the status.failureDomains field in an InfrastructureCluster object. Note that this field is optional. + pub fn failure_domains(&self) -> Path { + Path::from_tokens(["status", "failureDomains"].into_iter()) + } + + // IgnorePaths returns a list of paths to be ignored when reconciling an InfrastructureCluster. + // NOTE: The controlPlaneEndpoint struct currently contains two mandatory fields (host and port). + // As the host and port fields are not using omitempty, they are automatically set to their zero values + // if they are not set by the user. We don't want to reconcile the zero values as we would then overwrite + // changes applied by the infrastructure provider controller. + pub fn ignore_paths(infrastructure_cl: &DynamicObject) -> Result> { + let mut ignore_paths = vec![]; + let host = infrastructure_cluster() + .control_plane_endpoint() + .host() + .get(infrastructure_cl)?; + if host.is_empty() { + ignore_paths.push( + infrastructure_cluster() + .control_plane_endpoint() + .host() + .path(), + ) + } + + let port = infrastructure_cluster() + .control_plane_endpoint() + .port() + .get(infrastructure_cl)?; + if port == 0 { + ignore_paths.push( + infrastructure_cluster() + .control_plane_endpoint() + .port() + .path(), + ) + } + + Ok(ignore_paths) + } +} diff --git a/src/contract/infrastructure_cluster_template.rs b/src/contract/infrastructure_cluster_template.rs new file mode 100644 index 0000000..17efa8b --- /dev/null +++ b/src/contract/infrastructure_cluster_template.rs @@ -0,0 +1,45 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +use super::{metadata::Metadata, types::Path}; + +// InfrastructureClusterTemplateContract encodes information about the Cluster API contract for InfrastructureClusterTemplate objects +// like DockerClusterTemplates, AWSClusterTemplates, etc. +pub struct InfrastructureClusterTemplateContract; + +// InfrastructureClusterTemplate provides access to the information about the Cluster API contract for InfrastructureClusterTemplate objects. +pub fn infrastructure_cluster_template() -> InfrastructureClusterTemplateContract { + InfrastructureClusterTemplateContract +} + +impl InfrastructureClusterTemplateContract { + // Template provides access to the template. + pub fn template(&self) -> InfrastructureClusterTemplateTemplate { + InfrastructureClusterTemplateTemplate + } +} + +// InfrastructureClusterTemplateTemplate provides a helper struct for working with the template in an InfrastructureClusterTemplate.. +pub struct InfrastructureClusterTemplateTemplate; + +impl InfrastructureClusterTemplateTemplate { + // Metadata provides access to the metadata of a template. + pub fn metadata(&self) -> Metadata { + Metadata::new(Path::from_tokens( + ["spec", "template", "metadata"].into_iter(), + )) + } +} diff --git a/src/contract/infrastructure_machine.rs b/src/contract/infrastructure_machine.rs new file mode 100644 index 0000000..f39b691 --- /dev/null +++ b/src/contract/infrastructure_machine.rs @@ -0,0 +1,71 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +use crate::capi_machine::MachineStatusAddresses; + +use super::types::Path; + +pub type MachineAddresses = Vec; + +// InfrastructureMachineContract encodes information about the Cluster API contract for InfrastructureMachine objects +// like DockerMachines, AWS Machines, etc. +pub struct InfrastructureMachineContract; + +// InfrastructureMachine provide access to the information about the Cluster API contract for InfrastructureMachine objects. +pub fn infrastructure_machine() -> InfrastructureMachineContract { + InfrastructureMachineContract +} + +impl InfrastructureMachineContract { + // Ready provides access to status.ready field in an InfrastructureMachine object. + pub fn ready(&self) -> Path { + Path::from_tokens(["status", "ready"].into_iter()) + } + + // ReadyConditionType returns the type of the ready condition. + pub fn ready_condition_type(&self) -> String { + "Ready".to_string() + } + + // FailureReason provides access to the status.failureReason field in an InfrastructureMachine object. Note that this field is optional. + // + // Deprecated: This function is deprecated and is going to be removed. Please see https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20240916-improve-status-in-CAPI-resources.md for more details. + pub fn failure_reason(&self) -> Path { + Path::from_tokens(["status", "failureReason"].into_iter()) + } + + // FailureMessage provides access to the status.failureMessage field in an InfrastructureMachine object. Note that this field is optional. + // + // Deprecated: This function is deprecated and is going to be removed. Please see https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20240916-improve-status-in-CAPI-resources.md for more details. + pub fn failure_message(&self) -> Path { + Path::from_tokens(["status", "failureMessage"].into_iter()) + } + + // Addresses provides access to the status.addresses field in an InfrastructureMachine object. Note that this field is optional. + pub fn addresses(&self) -> Path { + Path::from_tokens(["status", "addresses"].into_iter()) + } + + // ProviderID provides access to the spec.providerID field in an InfrastructureMachine object. + pub fn provider_id(&self) -> Path { + Path::from_tokens(["spec", "providerID"].into_iter()) + } + + // FailureDomain provides access to the spec.failureDomain field in an InfrastructureMachine object. Note that this field is optional. + pub fn failure_domain(&self) -> Path { + Path::from_tokens(["spec", "failureDomain"].into_iter()) + } +} diff --git a/src/contract/infrastructure_machine_template.rs b/src/contract/infrastructure_machine_template.rs new file mode 100644 index 0000000..9b745b0 --- /dev/null +++ b/src/contract/infrastructure_machine_template.rs @@ -0,0 +1,43 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +use super::{metadata::Metadata, types::Path}; + +// InfrastructureMachineTemplateContract encodes information about the Cluster API contract for InfrastructureMachineTemplate objects +// like DockerMachineTemplates, AWSMachineTemplates, etc. +pub struct InfrastructureMachineTemplateContract; + +// InfrastructureMachineTemplate provide access to the information about the Cluster API contract for InfrastructureMachineTemplate objects. +pub fn infrastructure_machine_template() -> InfrastructureMachineTemplateContract { + InfrastructureMachineTemplateContract +} + +// Template provides access to the template. +fn template() -> InfrastructureMachineTemplateTemplate { + InfrastructureMachineTemplateTemplate +} + +// InfrastructureMachineTemplateTemplate provides a helper struct for working with the template in an InfrastructureMachineTemplate. +pub struct InfrastructureMachineTemplateTemplate; + +impl InfrastructureMachineTemplateTemplate { + // Metadata provides access to the metadata of a template. + fn metadata(&self) -> Metadata { + Metadata::new(Path::from_tokens( + ["spec", "template", "metadata"].into_iter(), + )) + } +} diff --git a/src/contract/metadata.rs b/src/contract/metadata.rs new file mode 100644 index 0000000..b209117 --- /dev/null +++ b/src/contract/metadata.rs @@ -0,0 +1,77 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +use serde::{Deserialize, Serialize}; + +use super::types::{Error, Path}; + +#[derive(Serialize, Deserialize)] +pub struct ObjectMeta { + // labels is a map of string keys and values that can be used to organize and categorize + // (scope and select) objects. May match selectors of replication controllers + // and services. + // More info: http://kubernetes.io/docs/user-guide/labels + // +optional + labels: std::collections::HashMap, + + // annotations is an unstructured key value map stored with a resource that may be + // set by external tools to store and retrieve arbitrary metadata. They are not + // queryable and should be preserved when modifying objects. + // More info: http://kubernetes.io/docs/user-guide/annotations + // +optional + annotations: std::collections::HashMap, +} + +// Metadata provides a helper struct for working with Metadata. +pub struct Metadata { + pub path: Path, +} + +impl Metadata { + pub fn new(path: Path) -> Self { + Self { path } + } + + // Path returns the path of the metadata. + pub fn path(&self) -> &jsonptr::PointerBuf { + &self.path.0 + } + + // Get gets the metadata object. + pub fn get(&self, obj: &kube::api::DynamicObject) -> Result { + self.path.get(obj) + } + + // Set sets the metadata value. + // Note: We are blanking out empty label annotations, thus avoiding triggering infinite reconcile + // given that at json level label: {} or annotation: {} is different from no field, which is the + // corresponding value stored in etcd given that those fields are defined as omitempty. + pub fn set(&self, obj: &mut kube::api::DynamicObject, metadata: ObjectMeta) -> Result<(), Error> { + if !metadata.labels.is_empty() { + let mut labels_path = self.path().clone(); + labels_path.push_front("labels"); + Path::new(labels_path).set(obj, metadata.labels)?; + } + + if !metadata.annotations.is_empty() { + let mut annotations_path = self.path().clone(); + annotations_path.push_front("annotations"); + Path::new(annotations_path).set(obj, metadata.annotations)?; + } + + Ok(()) + } +} diff --git a/src/contract/mod.rs b/src/contract/mod.rs new file mode 100644 index 0000000..3f53624 --- /dev/null +++ b/src/contract/mod.rs @@ -0,0 +1,10 @@ +pub mod types; +pub mod bootstrap_config_template; +pub mod metadata; +pub mod bootstrap; +pub mod controlplane_template; +pub mod controlplane; +pub mod infrastructure_cluster_template; +pub mod infrastructure_cluster; +pub mod infrastructure_machine_template; +pub mod infrastructure_machine; diff --git a/src/contract/types.rs b/src/contract/types.rs new file mode 100644 index 0000000..7d378e4 --- /dev/null +++ b/src/contract/types.rs @@ -0,0 +1,99 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Path defines a how to access a field in an Unstructured object. + +use std::{marker::PhantomData, path}; + +use jsonptr::{PointerBuf, Token}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Unable to resolve path: {0}")] + ResolveErr(#[from] jsonptr::resolve::Error), + + #[error("Unable to assign to path: {0}")] + AssignErr(#[from] jsonptr::assign::Error), + + #[error("Unable to decode reference: {0}")] + Serde(#[from] serde_json::Error), + + #[error("No available references to resolve")] + NoRefError, +} + +pub type Result = std::result::Result; + +#[derive(PartialEq, Eq)] +pub struct Path(pub jsonptr::PointerBuf, PhantomData); + +impl Path { + pub fn new(path: jsonptr::PointerBuf) -> Self { + Self(path, PhantomData::default()) + } + + pub fn from_tokens<'t>(tokens: impl IntoIterator>>) -> Self { + Self::new(PointerBuf::from_tokens(tokens)) + } + + pub fn path(&self) -> jsonptr::PointerBuf { + self.0.clone() + } +} + +impl Path { + pub fn get(&self, obj: &kube::api::DynamicObject) -> Result { + Ok(serde_json::from_value(self.0.resolve(&obj.data)?.clone())?) + } + + pub fn set(&self, obj: &mut kube::api::DynamicObject, data: R) -> Result<()> { + self.0.assign(&mut obj.data, serde_json::to_value(data)?)?; + Ok(()) + } +} + +impl ToString for Path { + fn to_string(&self) -> String { + self.0.to_string() + } +} + +pub struct Paths(Vec>); + +impl Paths { + pub fn new(paths: Vec>) -> Self { + Self(paths) + } +} + +impl Paths { + pub fn get(&self, obj: &kube::api::DynamicObject) -> Result { + let p = self.0.first().ok_or(Error::NoRefError)?; + for path in &self.0 { + if let Ok(data) = path.get(obj) { + return Ok(data); + }; + } + + p.get(obj) + } + + pub fn set(&self, obj: &mut kube::api::DynamicObject, data: R) -> Result<()> { + let p = self.0.first().ok_or(Error::NoRefError)?; + p.set(obj, data) + } +} diff --git a/src/lib.rs b/src/lib.rs index f2d6eb5..c21a61a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ pub mod api; +#[cfg(feature = "contract")] +pub mod contract; pub use api::*;