diff --git a/api/core/v1beta1/conversion.go b/api/core/v1beta1/conversion.go index 9f67740c47ac..a2550bd97a01 100644 --- a/api/core/v1beta1/conversion.go +++ b/api/core/v1beta1/conversion.go @@ -44,94 +44,6 @@ func SetAPIVersionGetter(f func(gk schema.GroupKind) (string, error)) { apiVersionGetter = f } -func (src *Cluster) ConvertTo(dstRaw conversion.Hub) error { - dst := dstRaw.(*clusterv1.Cluster) - - if err := Convert_v1beta1_Cluster_To_v1beta2_Cluster(src, dst, nil); err != nil { - return err - } - - if src.Spec.InfrastructureRef != nil { - infraRef, err := convertToContractVersionedObjectReference(src.Spec.InfrastructureRef) - if err != nil { - return err - } - dst.Spec.InfrastructureRef = infraRef - } - - if src.Spec.ControlPlaneRef != nil { - controlPlaneRef, err := convertToContractVersionedObjectReference(src.Spec.ControlPlaneRef) - if err != nil { - return err - } - dst.Spec.ControlPlaneRef = controlPlaneRef - } - - restored := &clusterv1.Cluster{} - ok, err := utilconversion.UnmarshalData(src, restored) - if err != nil { - return err - } - - // Recover intent for bool values converted to *bool. - clusterv1.Convert_bool_To_Pointer_bool(src.Spec.Paused, ok, restored.Spec.Paused, &dst.Spec.Paused) - - initialization := clusterv1.ClusterInitializationStatus{} - restoredControlPlaneInitialized := restored.Status.Initialization.ControlPlaneInitialized - restoredInfrastructureProvisioned := restored.Status.Initialization.InfrastructureProvisioned - clusterv1.Convert_bool_To_Pointer_bool(src.Status.ControlPlaneReady, ok, restoredControlPlaneInitialized, &initialization.ControlPlaneInitialized) - clusterv1.Convert_bool_To_Pointer_bool(src.Status.InfrastructureReady, ok, restoredInfrastructureProvisioned, &initialization.InfrastructureProvisioned) - if !reflect.DeepEqual(initialization, clusterv1.ClusterInitializationStatus{}) { - dst.Status.Initialization = initialization - } - return nil -} - -func (dst *Cluster) ConvertFrom(srcRaw conversion.Hub) error { - src := srcRaw.(*clusterv1.Cluster) - if err := Convert_v1beta2_Cluster_To_v1beta1_Cluster(src, dst, nil); err != nil { - return err - } - - if src.Spec.InfrastructureRef.IsDefined() { - infraRef, err := convertToObjectReference(src.Spec.InfrastructureRef, src.Namespace) - if err != nil { - return err - } - dst.Spec.InfrastructureRef = infraRef - } - - if src.Spec.ControlPlaneRef.IsDefined() { - controlPlaneRef, err := convertToObjectReference(src.Spec.ControlPlaneRef, src.Namespace) - if err != nil { - return err - } - dst.Spec.ControlPlaneRef = controlPlaneRef - } - - if dst.Spec.ClusterNetwork != nil && dst.Spec.ClusterNetwork.APIServerPort != nil && - *dst.Spec.ClusterNetwork.APIServerPort == 0 { - dst.Spec.ClusterNetwork.APIServerPort = nil - } - - if dst.Spec.Topology != nil { - if dst.Spec.Topology.ControlPlane.MachineHealthCheck != nil && dst.Spec.Topology.ControlPlane.MachineHealthCheck.RemediationTemplate != nil { - dst.Spec.Topology.ControlPlane.MachineHealthCheck.RemediationTemplate.Namespace = dst.Namespace - } - if dst.Spec.Topology.Workers != nil { - for _, md := range dst.Spec.Topology.Workers.MachineDeployments { - if md.MachineHealthCheck != nil && md.MachineHealthCheck.RemediationTemplate != nil { - md.MachineHealthCheck.RemediationTemplate.Namespace = dst.Namespace - } - } - } - } - - dropEmptyStringsCluster(dst) - - return utilconversion.MarshalData(src, dst) -} - func (src *ClusterClass) ConvertTo(dstRaw conversion.Hub) error { dst := dstRaw.(*clusterv1.ClusterClass) diff --git a/api/core/v1beta1/conversion_test.go b/api/core/v1beta1/conversion_test.go index 10d557048460..59ef73e8cd33 100644 --- a/api/core/v1beta1/conversion_test.go +++ b/api/core/v1beta1/conversion_test.go @@ -55,6 +55,7 @@ func TestFuzzyConversion(t *testing.T) { return "", fmt.Errorf("failed to map GroupKind %s to version", gk.String()) }) + // FIXME: implement a new test util for the new conversion. t.Run("for Cluster", utilconversion.FuzzTestFunc(utilconversion.FuzzTestFuncInput{ Hub: &clusterv1.Cluster{}, Spoke: &Cluster{}, diff --git a/api/core/v1beta2/conversion.go b/api/core/v1beta2/conversion.go index ad42bde84bec..0f7ed22cdf6a 100644 --- a/api/core/v1beta2/conversion.go +++ b/api/core/v1beta2/conversion.go @@ -24,7 +24,6 @@ import ( "k8s.io/utils/ptr" ) -func (*Cluster) Hub() {} func (*ClusterClass) Hub() {} func (*Machine) Hub() {} func (*MachineSet) Hub() {} diff --git a/api/runtime/v1alpha1/.import-restrictions b/api/runtime/v1alpha1/.import-restrictions deleted file mode 100644 index f6f10b3ff544..000000000000 --- a/api/runtime/v1alpha1/.import-restrictions +++ /dev/null @@ -1,5 +0,0 @@ -rules: - - selectorRegexp: sigs[.]k8s[.]io/controller-runtime - allowedPrefixes: - - "sigs.k8s.io/controller-runtime/pkg/conversion" - forbiddenPrefixes: [] diff --git a/api/runtime/v1alpha1/conversion.go b/api/runtime/v1alpha1/conversion.go index 8b39e5bc0078..93b81f812fe7 100644 --- a/api/runtime/v1alpha1/conversion.go +++ b/api/runtime/v1alpha1/conversion.go @@ -22,35 +22,11 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" apimachineryconversion "k8s.io/apimachinery/pkg/conversion" "k8s.io/utils/ptr" - "sigs.k8s.io/controller-runtime/pkg/conversion" clusterv1beta1 "sigs.k8s.io/cluster-api/api/core/v1beta1" runtimev1 "sigs.k8s.io/cluster-api/api/runtime/v1beta2" ) -func (src *ExtensionConfig) ConvertTo(dstRaw conversion.Hub) error { - dst := dstRaw.(*runtimev1.ExtensionConfig) - - return Convert_v1alpha1_ExtensionConfig_To_v1beta2_ExtensionConfig(src, dst, nil) -} - -func (dst *ExtensionConfig) ConvertFrom(srcRaw conversion.Hub) error { - src := srcRaw.(*runtimev1.ExtensionConfig) - - if err := Convert_v1beta2_ExtensionConfig_To_v1alpha1_ExtensionConfig(src, dst, nil); err != nil { - return err - } - - dropEmptyStringsExtensionConfig(dst) - for i, h := range dst.Status.Handlers { - if h.TimeoutSeconds != nil && *h.TimeoutSeconds == 0 { - h.TimeoutSeconds = nil - } - dst.Status.Handlers[i] = h - } - return nil -} - func Convert_v1beta2_ExtensionConfigStatus_To_v1alpha1_ExtensionConfigStatus(in *runtimev1.ExtensionConfigStatus, out *ExtensionConfigStatus, s apimachineryconversion.Scope) error { if err := autoConvert_v1beta2_ExtensionConfigStatus_To_v1alpha1_ExtensionConfigStatus(in, out, s); err != nil { return err diff --git a/api/runtime/v1beta2/conversion.go b/api/runtime/v1beta2/conversion.go deleted file mode 100644 index df9c0f8a8e6f..000000000000 --- a/api/runtime/v1beta2/conversion.go +++ /dev/null @@ -1,19 +0,0 @@ -/* -Copyright 2025 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. -*/ - -package v1beta2 - -func (*ExtensionConfig) Hub() {} diff --git a/go.mod b/go.mod index 17a52aa1cd8c..d832d94cbd14 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module sigs.k8s.io/cluster-api go 1.24.0 +replace sigs.k8s.io/controller-runtime => ../controller-runtime + require ( github.com/MakeNowJust/heredoc v1.0.0 github.com/Masterminds/sprig/v3 v3.3.0 diff --git a/go.sum b/go.sum index 729332dba2f6..bc4505a69c36 100644 --- a/go.sum +++ b/go.sum @@ -606,8 +606,6 @@ rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= -sigs.k8s.io/controller-runtime v0.22.1 h1:Ah1T7I+0A7ize291nJZdS1CabF/lB4E++WizgV24Eqg= -sigs.k8s.io/controller-runtime v0.22.1/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= diff --git a/hack/tools/go.mod b/hack/tools/go.mod index bb161a4a016a..704fb16629a6 100644 --- a/hack/tools/go.mod +++ b/hack/tools/go.mod @@ -6,6 +6,8 @@ replace sigs.k8s.io/cluster-api => ../../ replace sigs.k8s.io/cluster-api/test => ../../test +replace sigs.k8s.io/controller-runtime => ../../../controller-runtime + require ( cloud.google.com/go/storage v1.57.0 github.com/blang/semver/v4 v4.0.0 diff --git a/hack/tools/go.sum b/hack/tools/go.sum index 2d93b2ee5aaa..7eaf892a01f9 100644 --- a/hack/tools/go.sum +++ b/hack/tools/go.sum @@ -544,8 +544,6 @@ k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8 k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= -sigs.k8s.io/controller-runtime v0.22.1 h1:Ah1T7I+0A7ize291nJZdS1CabF/lB4E++WizgV24Eqg= -sigs.k8s.io/controller-runtime v0.22.1/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY= sigs.k8s.io/controller-tools v0.19.0 h1:OU7jrPPiZusryu6YK0jYSjPqg8Vhf8cAzluP9XGI5uk= sigs.k8s.io/controller-tools v0.19.0/go.mod h1:y5HY/iNDFkmFla2CfQoVb2AQXMsBk4ad84iR1PLANB0= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= diff --git a/internal/api/core/v1alpha3/conversion.go b/internal/api/core/v1alpha3/conversion.go index 8cda6f6a25f1..02ce81047c3a 100644 --- a/internal/api/core/v1alpha3/conversion.go +++ b/internal/api/core/v1alpha3/conversion.go @@ -35,7 +35,6 @@ import ( clusterv1beta1 "sigs.k8s.io/cluster-api/api/core/v1beta1" clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" - v1beta1conditions "sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1" utilconversion "sigs.k8s.io/cluster-api/util/conversion" ) @@ -47,158 +46,6 @@ func SetAPIVersionGetter(f func(gk schema.GroupKind) (string, error)) { apiVersionGetter = f } -func (src *Cluster) ConvertTo(dstRaw conversion.Hub) error { - dst := dstRaw.(*clusterv1.Cluster) - - if err := Convert_v1alpha3_Cluster_To_v1beta2_Cluster(src, dst, nil); err != nil { - return err - } - - if src.Spec.InfrastructureRef != nil { - infraRef, err := convertToContractVersionedObjectReference(src.Spec.InfrastructureRef) - if err != nil { - return err - } - dst.Spec.InfrastructureRef = infraRef - } - - if src.Spec.ControlPlaneRef != nil { - controlPlaneRef, err := convertToContractVersionedObjectReference(src.Spec.ControlPlaneRef) - if err != nil { - return err - } - dst.Spec.ControlPlaneRef = controlPlaneRef - } - - // Reset conditions from autogenerated conversions - // NOTE: v1alpha3 conditions should not be automatically be converted into v1beta2 conditions. - dst.Status.Conditions = nil - - // Move legacy conditions (v1alpha3), failureReason and failureMessage to the deprecated field. - if src.Status.Conditions != nil || src.Status.FailureReason != nil || src.Status.FailureMessage != nil { - dst.Status.Deprecated = &clusterv1.ClusterDeprecatedStatus{} - dst.Status.Deprecated.V1Beta1 = &clusterv1.ClusterV1Beta1DeprecatedStatus{} - if src.Status.Conditions != nil { - Convert_v1alpha3_Conditions_To_v1beta2_Deprecated_V1Beta1_Conditions(&src.Status.Conditions, &dst.Status.Deprecated.V1Beta1.Conditions) - } - dst.Status.Deprecated.V1Beta1.FailureReason = src.Status.FailureReason - dst.Status.Deprecated.V1Beta1.FailureMessage = src.Status.FailureMessage - } - - // Given this is a bool and there is no timestamp associated with it, when this condition is set, its timestamp - // will be "now". See https://github.com/kubernetes-sigs/cluster-api/issues/3798#issuecomment-708619826 for more - // discussion. - if src.Status.ControlPlaneInitialized { - v1beta1conditions.MarkTrue(dst, clusterv1.ControlPlaneInitializedV1Beta1Condition) - } - - // Manually restore data. - restored := &clusterv1.Cluster{} - ok, err := utilconversion.UnmarshalData(src, restored) - if err != nil { - return err - } - - // Recover intent for bool values converted to *bool. - clusterv1.Convert_bool_To_Pointer_bool(src.Spec.Paused, ok, restored.Spec.Paused, &dst.Spec.Paused) - - initialization := clusterv1.ClusterInitializationStatus{} - restoredControlPlaneInitialized := restored.Status.Initialization.ControlPlaneInitialized - restoredInfrastructureProvisioned := restored.Status.Initialization.InfrastructureProvisioned - clusterv1.Convert_bool_To_Pointer_bool(src.Status.ControlPlaneReady, ok, restoredControlPlaneInitialized, &initialization.ControlPlaneInitialized) - clusterv1.Convert_bool_To_Pointer_bool(src.Status.InfrastructureReady, ok, restoredInfrastructureProvisioned, &initialization.InfrastructureProvisioned) - if !reflect.DeepEqual(initialization, clusterv1.ClusterInitializationStatus{}) { - dst.Status.Initialization = initialization - } - - for i, fd := range dst.Status.FailureDomains { - srcFD, ok := src.Status.FailureDomains[fd.Name] - if !ok { - return fmt.Errorf("failure domain %q not found in source data", fd.Name) - } - var restoredFDControlPlane *bool - for _, restoredFD := range restored.Status.FailureDomains { - if restoredFD.Name == fd.Name { - restoredFDControlPlane = restoredFD.ControlPlane - break - } - } - clusterv1.Convert_bool_To_Pointer_bool(srcFD.ControlPlane, ok, restoredFDControlPlane, &fd.ControlPlane) - dst.Status.FailureDomains[i] = fd - } - - // Recover other values - if ok { - dst.Spec.AvailabilityGates = restored.Spec.AvailabilityGates - dst.Spec.Topology = restored.Spec.Topology - dst.Status.Conditions = restored.Status.Conditions - dst.Status.ControlPlane = restored.Status.ControlPlane - dst.Status.Workers = restored.Status.Workers - } - - return nil -} - -func (dst *Cluster) ConvertFrom(srcRaw conversion.Hub) error { - src := srcRaw.(*clusterv1.Cluster) - - if err := Convert_v1beta2_Cluster_To_v1alpha3_Cluster(src, dst, nil); err != nil { - return err - } - - if src.Spec.InfrastructureRef.IsDefined() { - infraRef, err := convertToObjectReference(src.Spec.InfrastructureRef, src.Namespace) - if err != nil { - return err - } - dst.Spec.InfrastructureRef = infraRef - } - - if src.Spec.ControlPlaneRef.IsDefined() { - controlPlaneRef, err := convertToObjectReference(src.Spec.ControlPlaneRef, src.Namespace) - if err != nil { - return err - } - dst.Spec.ControlPlaneRef = controlPlaneRef - } - - if dst.Spec.ClusterNetwork != nil && dst.Spec.ClusterNetwork.APIServerPort != nil && - *dst.Spec.ClusterNetwork.APIServerPort == 0 { - dst.Spec.ClusterNetwork.APIServerPort = nil - } - - // Reset conditions from autogenerated conversions - // NOTE: v1beta2 conditions should not be automatically be converted into legacy conditions (v1alpha3). - dst.Status.Conditions = nil - - // Retrieve legacy conditions (v1alpha3), failureReason and failureMessage from the deprecated field. - if src.Status.Deprecated != nil { - if src.Status.Deprecated.V1Beta1 != nil { - if src.Status.Deprecated.V1Beta1.Conditions != nil { - Convert_v1beta2_Deprecated_V1Beta1_Conditions_To_v1alpha3_Conditions(&src.Status.Deprecated.V1Beta1.Conditions, &dst.Status.Conditions) - } - dst.Status.FailureReason = src.Status.Deprecated.V1Beta1.FailureReason - dst.Status.FailureMessage = src.Status.Deprecated.V1Beta1.FailureMessage - } - } - - // Set the v1alpha3 boolean status field if the v1alpha4 condition was true - if v1beta1conditions.IsTrue(src, clusterv1.ControlPlaneInitializedV1Beta1Condition) { - dst.Status.ControlPlaneInitialized = true - } - - // Move initialization to old fields - dst.Status.ControlPlaneReady = ptr.Deref(src.Status.Initialization.ControlPlaneInitialized, false) - dst.Status.InfrastructureReady = ptr.Deref(src.Status.Initialization.InfrastructureProvisioned, false) - - // Preserve Hub data on down-conversion except for metadata - if err := utilconversion.MarshalData(src, dst); err != nil { - return err - } - - return nil -} - func (src *Machine) ConvertTo(dstRaw conversion.Hub) error { dst := dstRaw.(*clusterv1.Machine) diff --git a/internal/api/core/v1alpha4/conversion.go b/internal/api/core/v1alpha4/conversion.go index 57ac08a4ea98..7a94c999f450 100644 --- a/internal/api/core/v1alpha4/conversion.go +++ b/internal/api/core/v1alpha4/conversion.go @@ -45,181 +45,6 @@ func SetAPIVersionGetter(f func(gk schema.GroupKind) (string, error)) { apiVersionGetter = f } -func (src *Cluster) ConvertTo(dstRaw conversion.Hub) error { - dst := dstRaw.(*clusterv1.Cluster) - - if err := Convert_v1alpha4_Cluster_To_v1beta2_Cluster(src, dst, nil); err != nil { - return err - } - - if src.Spec.InfrastructureRef != nil { - infraRef, err := convertToContractVersionedObjectReference(src.Spec.InfrastructureRef) - if err != nil { - return err - } - dst.Spec.InfrastructureRef = infraRef - } - - if src.Spec.ControlPlaneRef != nil { - controlPlaneRef, err := convertToContractVersionedObjectReference(src.Spec.ControlPlaneRef) - if err != nil { - return err - } - dst.Spec.ControlPlaneRef = controlPlaneRef - } - - // Reset conditions from autogenerated conversions - // NOTE: v1alpha4 conditions should not be automatically be converted into v1beta2 conditions. - dst.Status.Conditions = nil - - // Move legacy conditions (v1alpha4), failureReason and failureMessage to the deprecated field. - if src.Status.Conditions != nil || src.Status.FailureReason != nil || src.Status.FailureMessage != nil { - dst.Status.Deprecated = &clusterv1.ClusterDeprecatedStatus{} - dst.Status.Deprecated.V1Beta1 = &clusterv1.ClusterV1Beta1DeprecatedStatus{} - if src.Status.Conditions != nil { - Convert_v1alpha4_Conditions_To_v1beta2_Deprecated_V1Beta1_Conditions(&src.Status.Conditions, &dst.Status.Deprecated.V1Beta1.Conditions) - } - dst.Status.Deprecated.V1Beta1.FailureReason = src.Status.FailureReason - dst.Status.Deprecated.V1Beta1.FailureMessage = src.Status.FailureMessage - } - - // Move ControlPlaneReady and InfrastructureReady to initialization is implemented in ConvertTo. - - // Manually restore data. - restored := &clusterv1.Cluster{} - ok, err := utilconversion.UnmarshalData(src, restored) - if err != nil { - return err - } - - // Recover intent for bool values converted to *bool. - clusterv1.Convert_bool_To_Pointer_bool(src.Spec.Paused, ok, restored.Spec.Paused, &dst.Spec.Paused) - - initialization := clusterv1.ClusterInitializationStatus{} - restoredControlPlaneInitialized := restored.Status.Initialization.ControlPlaneInitialized - restoredInfrastructureProvisioned := restored.Status.Initialization.InfrastructureProvisioned - clusterv1.Convert_bool_To_Pointer_bool(src.Status.ControlPlaneReady, ok, restoredControlPlaneInitialized, &initialization.ControlPlaneInitialized) - clusterv1.Convert_bool_To_Pointer_bool(src.Status.InfrastructureReady, ok, restoredInfrastructureProvisioned, &initialization.InfrastructureProvisioned) - if !reflect.DeepEqual(initialization, clusterv1.ClusterInitializationStatus{}) { - dst.Status.Initialization = initialization - } - - for i, fd := range dst.Status.FailureDomains { - srcFD, ok := src.Status.FailureDomains[fd.Name] - if !ok { - return fmt.Errorf("failure domain %q not found in source data", fd.Name) - } - var restoredFDControlPlane *bool - for _, restoredFD := range restored.Status.FailureDomains { - if restoredFD.Name == fd.Name { - restoredFDControlPlane = restoredFD.ControlPlane - break - } - } - clusterv1.Convert_bool_To_Pointer_bool(srcFD.ControlPlane, ok, restoredFDControlPlane, &fd.ControlPlane) - dst.Status.FailureDomains[i] = fd - } - - // Recover other values - if ok { - dst.Spec.AvailabilityGates = restored.Spec.AvailabilityGates - dst.Spec.Topology.ClassRef.Namespace = restored.Spec.Topology.ClassRef.Namespace - dst.Spec.Topology.Variables = restored.Spec.Topology.Variables - dst.Spec.Topology.ControlPlane.Variables = restored.Spec.Topology.ControlPlane.Variables - - dst.Spec.Topology.ControlPlane.HealthCheck = restored.Spec.Topology.ControlPlane.HealthCheck - - if restored.Spec.Topology.ControlPlane.Deletion.NodeDrainTimeoutSeconds != nil { - dst.Spec.Topology.ControlPlane.Deletion.NodeDrainTimeoutSeconds = restored.Spec.Topology.ControlPlane.Deletion.NodeDrainTimeoutSeconds - } - - if restored.Spec.Topology.ControlPlane.Deletion.NodeVolumeDetachTimeoutSeconds != nil { - dst.Spec.Topology.ControlPlane.Deletion.NodeVolumeDetachTimeoutSeconds = restored.Spec.Topology.ControlPlane.Deletion.NodeVolumeDetachTimeoutSeconds - } - - if restored.Spec.Topology.ControlPlane.Deletion.NodeDeletionTimeoutSeconds != nil { - dst.Spec.Topology.ControlPlane.Deletion.NodeDeletionTimeoutSeconds = restored.Spec.Topology.ControlPlane.Deletion.NodeDeletionTimeoutSeconds - } - dst.Spec.Topology.ControlPlane.ReadinessGates = restored.Spec.Topology.ControlPlane.ReadinessGates - - for i := range restored.Spec.Topology.Workers.MachineDeployments { - dst.Spec.Topology.Workers.MachineDeployments[i].FailureDomain = restored.Spec.Topology.Workers.MachineDeployments[i].FailureDomain - dst.Spec.Topology.Workers.MachineDeployments[i].Variables = restored.Spec.Topology.Workers.MachineDeployments[i].Variables - dst.Spec.Topology.Workers.MachineDeployments[i].ReadinessGates = restored.Spec.Topology.Workers.MachineDeployments[i].ReadinessGates - dst.Spec.Topology.Workers.MachineDeployments[i].Deletion.Order = restored.Spec.Topology.Workers.MachineDeployments[i].Deletion.Order - dst.Spec.Topology.Workers.MachineDeployments[i].Deletion.NodeDrainTimeoutSeconds = restored.Spec.Topology.Workers.MachineDeployments[i].Deletion.NodeDrainTimeoutSeconds - dst.Spec.Topology.Workers.MachineDeployments[i].Deletion.NodeVolumeDetachTimeoutSeconds = restored.Spec.Topology.Workers.MachineDeployments[i].Deletion.NodeVolumeDetachTimeoutSeconds - dst.Spec.Topology.Workers.MachineDeployments[i].Deletion.NodeDeletionTimeoutSeconds = restored.Spec.Topology.Workers.MachineDeployments[i].Deletion.NodeDeletionTimeoutSeconds - dst.Spec.Topology.Workers.MachineDeployments[i].MinReadySeconds = restored.Spec.Topology.Workers.MachineDeployments[i].MinReadySeconds - dst.Spec.Topology.Workers.MachineDeployments[i].Rollout.Strategy = restored.Spec.Topology.Workers.MachineDeployments[i].Rollout.Strategy - dst.Spec.Topology.Workers.MachineDeployments[i].HealthCheck = restored.Spec.Topology.Workers.MachineDeployments[i].HealthCheck - } - - dst.Spec.Topology.Workers.MachinePools = restored.Spec.Topology.Workers.MachinePools - - dst.Status.Conditions = restored.Status.Conditions - dst.Status.ControlPlane = restored.Status.ControlPlane - dst.Status.Workers = restored.Status.Workers - } - - return nil -} - -func (dst *Cluster) ConvertFrom(srcRaw conversion.Hub) error { - src := srcRaw.(*clusterv1.Cluster) - - if err := Convert_v1beta2_Cluster_To_v1alpha4_Cluster(src, dst, nil); err != nil { - return err - } - - if src.Spec.InfrastructureRef.IsDefined() { - infraRef, err := convertToObjectReference(src.Spec.InfrastructureRef, src.Namespace) - if err != nil { - return err - } - dst.Spec.InfrastructureRef = infraRef - } - - if src.Spec.ControlPlaneRef.IsDefined() { - controlPlaneRef, err := convertToObjectReference(src.Spec.ControlPlaneRef, src.Namespace) - if err != nil { - return err - } - dst.Spec.ControlPlaneRef = controlPlaneRef - } - - if dst.Spec.ClusterNetwork != nil && dst.Spec.ClusterNetwork.APIServerPort != nil && - *dst.Spec.ClusterNetwork.APIServerPort == 0 { - dst.Spec.ClusterNetwork.APIServerPort = nil - } - - // Reset conditions from autogenerated conversions - // NOTE: v1beta2 conditions should not be automatically be converted into legacy conditions (v1alpha4). - dst.Status.Conditions = nil - - // Retrieve legacy conditions (v1alpha4), failureReason and failureMessage from the deprecated field. - if src.Status.Deprecated != nil { - if src.Status.Deprecated.V1Beta1 != nil { - if src.Status.Deprecated.V1Beta1.Conditions != nil { - Convert_v1beta2_Deprecated_V1Beta1_Conditions_To_v1alpha4_Conditions(&src.Status.Deprecated.V1Beta1.Conditions, &dst.Status.Conditions) - } - dst.Status.FailureReason = src.Status.Deprecated.V1Beta1.FailureReason - dst.Status.FailureMessage = src.Status.Deprecated.V1Beta1.FailureMessage - } - } - - // Move initialization to old fields - dst.Status.ControlPlaneReady = ptr.Deref(src.Status.Initialization.ControlPlaneInitialized, false) - dst.Status.InfrastructureReady = ptr.Deref(src.Status.Initialization.InfrastructureProvisioned, false) - - // Preserve Hub data on down-conversion except for metadata - if err := utilconversion.MarshalData(src, dst); err != nil { - return err - } - - return nil -} - func (src *ClusterClass) ConvertTo(dstRaw conversion.Hub) error { dst := dstRaw.(*clusterv1.ClusterClass) diff --git a/internal/test/envtest/environment.go b/internal/test/envtest/environment.go index 48baf781b846..40ad66cfe313 100644 --- a/internal/test/envtest/environment.go +++ b/internal/test/envtest/environment.go @@ -59,16 +59,31 @@ import ( metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" + addonsv1beta1 "sigs.k8s.io/cluster-api/api/addons/v1beta1" addonsv1 "sigs.k8s.io/cluster-api/api/addons/v1beta2" + bootstrapv1beta1 "sigs.k8s.io/cluster-api/api/bootstrap/kubeadm/v1beta1" bootstrapv1 "sigs.k8s.io/cluster-api/api/bootstrap/kubeadm/v1beta2" + controlplanev1beta1 "sigs.k8s.io/cluster-api/api/controlplane/kubeadm/v1beta1" controlplanev1 "sigs.k8s.io/cluster-api/api/controlplane/kubeadm/v1beta2" + clusterv1beta1 "sigs.k8s.io/cluster-api/api/core/v1beta1" clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" + ipamv1alpha1 "sigs.k8s.io/cluster-api/api/ipam/v1alpha1" + ipamv1beta1 "sigs.k8s.io/cluster-api/api/ipam/v1beta1" ipamv1 "sigs.k8s.io/cluster-api/api/ipam/v1beta2" + runtimev1alpha1 "sigs.k8s.io/cluster-api/api/runtime/v1alpha1" runtimev1 "sigs.k8s.io/cluster-api/api/runtime/v1beta2" bootstrapwebhooks "sigs.k8s.io/cluster-api/bootstrap/kubeadm/webhooks" "sigs.k8s.io/cluster-api/cmd/clusterctl/log" controlplanewebhooks "sigs.k8s.io/cluster-api/controlplane/kubeadm/webhooks" "sigs.k8s.io/cluster-api/feature" + addonsv1alpha3 "sigs.k8s.io/cluster-api/internal/api/addons/v1alpha3" + addonsv1alpha4 "sigs.k8s.io/cluster-api/internal/api/addons/v1alpha4" + bootstrapv1alpha3 "sigs.k8s.io/cluster-api/internal/api/bootstrap/kubeadm/v1alpha3" + bootstrapv1alpha4 "sigs.k8s.io/cluster-api/internal/api/bootstrap/kubeadm/v1alpha4" + controlplanev1alpha3 "sigs.k8s.io/cluster-api/internal/api/controlplane/kubeadm/v1alpha3" + controlplanev1alpha4 "sigs.k8s.io/cluster-api/internal/api/controlplane/kubeadm/v1alpha4" + clusterv1alpha3 "sigs.k8s.io/cluster-api/internal/api/core/v1alpha3" + clusterv1alpha4 "sigs.k8s.io/cluster-api/internal/api/core/v1alpha4" internalwebhooks "sigs.k8s.io/cluster-api/internal/webhooks" "sigs.k8s.io/cluster-api/util/kubeconfig" "sigs.k8s.io/cluster-api/util/test/builder" @@ -112,12 +127,33 @@ func registerSchemes(s *runtime.Scheme) { utilruntime.Must(admissionv1.AddToScheme(s)) utilruntime.Must(apiextensionsv1.AddToScheme(s)) - utilruntime.Must(addonsv1.AddToScheme(s)) - utilruntime.Must(bootstrapv1.AddToScheme(s)) - utilruntime.Must(clusterv1.AddToScheme(s)) - utilruntime.Must(controlplanev1.AddToScheme(s)) - utilruntime.Must(ipamv1.AddToScheme(s)) - utilruntime.Must(runtimev1.AddToScheme(s)) + // Register all versions so conversion is set up correctly. + _ = clusterv1alpha3.AddToScheme(s) + _ = clusterv1alpha4.AddToScheme(s) + _ = clusterv1beta1.AddToScheme(s) + _ = clusterv1.AddToScheme(s) + + _ = addonsv1alpha3.AddToScheme(s) + _ = addonsv1alpha4.AddToScheme(s) + _ = addonsv1beta1.AddToScheme(s) + _ = addonsv1.AddToScheme(s) + + _ = runtimev1alpha1.AddToScheme(s) + _ = runtimev1.AddToScheme(s) + + _ = ipamv1alpha1.AddToScheme(s) + _ = ipamv1beta1.AddToScheme(s) + _ = ipamv1.AddToScheme(s) + + _ = bootstrapv1alpha3.AddToScheme(s) + _ = bootstrapv1alpha4.AddToScheme(s) + _ = bootstrapv1beta1.AddToScheme(s) + _ = bootstrapv1.AddToScheme(s) + + _ = controlplanev1alpha3.AddToScheme(s) + _ = controlplanev1alpha4.AddToScheme(s) + _ = controlplanev1beta1.AddToScheme(s) + _ = controlplanev1.AddToScheme(s) } // RunInput is the input for Run. diff --git a/internal/webhooks/cluster.go b/internal/webhooks/cluster.go index f93c24e6c265..4c34e63456e6 100644 --- a/internal/webhooks/cluster.go +++ b/internal/webhooks/cluster.go @@ -36,10 +36,14 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + "sigs.k8s.io/controller-runtime/pkg/webhook/conversion" + clusterv1beta1 "sigs.k8s.io/cluster-api/api/core/v1beta1" clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" "sigs.k8s.io/cluster-api/controllers/external" "sigs.k8s.io/cluster-api/feature" + clusterv1alpha3 "sigs.k8s.io/cluster-api/internal/api/core/v1alpha3" + clusterv1alpha4 "sigs.k8s.io/cluster-api/internal/api/core/v1alpha4" "sigs.k8s.io/cluster-api/internal/contract" "sigs.k8s.io/cluster-api/internal/topology/check" "sigs.k8s.io/cluster-api/internal/topology/variables" @@ -57,6 +61,11 @@ func (webhook *Cluster) SetupWebhookWithManager(mgr ctrl.Manager) error { For(&clusterv1.Cluster{}). WithDefaulter(webhook). WithValidator(webhook). + WithConverters( + conversion.NewConverter(&clusterv1.Cluster{}, &clusterv1beta1.Cluster{}, ConvertClusterHubToV1Beta1, ConvertClusterV1Beta1ToHub), + conversion.NewConverter(&clusterv1.Cluster{}, &clusterv1alpha4.Cluster{}, ConvertClusterHubToV1Alpha4, ConvertClusterV1Alpha4ToHub), + conversion.NewConverter(&clusterv1.Cluster{}, &clusterv1alpha3.Cluster{}, ConvertClusterHubToV1Alpha3, ConvertClusterV1Alpha3ToHub), + ). Complete() } diff --git a/internal/webhooks/cluster_conversion.go b/internal/webhooks/cluster_conversion.go new file mode 100644 index 000000000000..549a3f185aff --- /dev/null +++ b/internal/webhooks/cluster_conversion.go @@ -0,0 +1,495 @@ +/* +Copyright 2025 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. +*/ + +package webhooks + +import ( + "errors" + "fmt" + "reflect" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/utils/ptr" + + clusterv1beta1 "sigs.k8s.io/cluster-api/api/core/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" + clusterv1alpha3 "sigs.k8s.io/cluster-api/internal/api/core/v1alpha3" + clusterv1alpha4 "sigs.k8s.io/cluster-api/internal/api/core/v1alpha4" + v1beta1conditions "sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1" + utilconversion "sigs.k8s.io/cluster-api/util/conversion" +) + +var apiVersionGetter = func(_ schema.GroupKind) (string, error) { + return "", errors.New("apiVersionGetter not set") +} + +func SetAPIVersionGetter(f func(gk schema.GroupKind) (string, error)) { + apiVersionGetter = f +} + +func ConvertClusterV1Beta1ToHub(src *clusterv1beta1.Cluster, dst *clusterv1.Cluster) error { + if err := clusterv1beta1.Convert_v1beta1_Cluster_To_v1beta2_Cluster(src, dst, nil); err != nil { + return err + } + + if src.Spec.InfrastructureRef != nil { + infraRef, err := convertToContractVersionedObjectReference(src.Spec.InfrastructureRef) + if err != nil { + return err + } + dst.Spec.InfrastructureRef = infraRef + } + + if src.Spec.ControlPlaneRef != nil { + controlPlaneRef, err := convertToContractVersionedObjectReference(src.Spec.ControlPlaneRef) + if err != nil { + return err + } + dst.Spec.ControlPlaneRef = controlPlaneRef + } + + restored := &clusterv1.Cluster{} + ok, err := utilconversion.UnmarshalData(src, restored) + if err != nil { + return err + } + + // Recover intent for bool values converted to *bool. + clusterv1.Convert_bool_To_Pointer_bool(src.Spec.Paused, ok, restored.Spec.Paused, &dst.Spec.Paused) + + initialization := clusterv1.ClusterInitializationStatus{} + restoredControlPlaneInitialized := restored.Status.Initialization.ControlPlaneInitialized + restoredInfrastructureProvisioned := restored.Status.Initialization.InfrastructureProvisioned + clusterv1.Convert_bool_To_Pointer_bool(src.Status.ControlPlaneReady, ok, restoredControlPlaneInitialized, &initialization.ControlPlaneInitialized) + clusterv1.Convert_bool_To_Pointer_bool(src.Status.InfrastructureReady, ok, restoredInfrastructureProvisioned, &initialization.InfrastructureProvisioned) + if !reflect.DeepEqual(initialization, clusterv1.ClusterInitializationStatus{}) { + dst.Status.Initialization = initialization + } + return nil +} + +func ConvertClusterHubToV1Beta1(src *clusterv1.Cluster, dst *clusterv1beta1.Cluster) error { + if err := clusterv1beta1.Convert_v1beta2_Cluster_To_v1beta1_Cluster(src, dst, nil); err != nil { + return err + } + + if src.Spec.InfrastructureRef.IsDefined() { + infraRef, err := convertToObjectReference(src.Spec.InfrastructureRef, src.Namespace) + if err != nil { + return err + } + dst.Spec.InfrastructureRef = infraRef + } + + if src.Spec.ControlPlaneRef.IsDefined() { + controlPlaneRef, err := convertToObjectReference(src.Spec.ControlPlaneRef, src.Namespace) + if err != nil { + return err + } + dst.Spec.ControlPlaneRef = controlPlaneRef + } + + if dst.Spec.ClusterNetwork != nil && dst.Spec.ClusterNetwork.APIServerPort != nil && + *dst.Spec.ClusterNetwork.APIServerPort == 0 { + dst.Spec.ClusterNetwork.APIServerPort = nil + } + + if dst.Spec.Topology != nil { + if dst.Spec.Topology.ControlPlane.MachineHealthCheck != nil && dst.Spec.Topology.ControlPlane.MachineHealthCheck.RemediationTemplate != nil { + dst.Spec.Topology.ControlPlane.MachineHealthCheck.RemediationTemplate.Namespace = dst.Namespace + } + if dst.Spec.Topology.Workers != nil { + for _, md := range dst.Spec.Topology.Workers.MachineDeployments { + if md.MachineHealthCheck != nil && md.MachineHealthCheck.RemediationTemplate != nil { + md.MachineHealthCheck.RemediationTemplate.Namespace = dst.Namespace + } + } + } + } + + dropEmptyStringsCluster(dst) + + return utilconversion.MarshalData(src, dst) +} + +func ConvertClusterV1Alpha4ToHub(src *clusterv1alpha4.Cluster, dst *clusterv1.Cluster) error { + if err := clusterv1alpha4.Convert_v1alpha4_Cluster_To_v1beta2_Cluster(src, dst, nil); err != nil { + return err + } + + if src.Spec.InfrastructureRef != nil { + infraRef, err := convertToContractVersionedObjectReference(src.Spec.InfrastructureRef) + if err != nil { + return err + } + dst.Spec.InfrastructureRef = infraRef + } + + if src.Spec.ControlPlaneRef != nil { + controlPlaneRef, err := convertToContractVersionedObjectReference(src.Spec.ControlPlaneRef) + if err != nil { + return err + } + dst.Spec.ControlPlaneRef = controlPlaneRef + } + + // Reset conditions from autogenerated conversions + // NOTE: v1alpha4 conditions should not be automatically be converted into v1beta2 conditions. + dst.Status.Conditions = nil + + // Move legacy conditions (v1alpha4), failureReason and failureMessage to the deprecated field. + if src.Status.Conditions != nil || src.Status.FailureReason != nil || src.Status.FailureMessage != nil { + dst.Status.Deprecated = &clusterv1.ClusterDeprecatedStatus{} + dst.Status.Deprecated.V1Beta1 = &clusterv1.ClusterV1Beta1DeprecatedStatus{} + if src.Status.Conditions != nil { + clusterv1alpha4.Convert_v1alpha4_Conditions_To_v1beta2_Deprecated_V1Beta1_Conditions(&src.Status.Conditions, &dst.Status.Deprecated.V1Beta1.Conditions) + } + dst.Status.Deprecated.V1Beta1.FailureReason = src.Status.FailureReason + dst.Status.Deprecated.V1Beta1.FailureMessage = src.Status.FailureMessage + } + + // Move ControlPlaneReady and InfrastructureReady to initialization is implemented in ConvertTo. + + // Manually restore data. + restored := &clusterv1.Cluster{} + ok, err := utilconversion.UnmarshalData(src, restored) + if err != nil { + return err + } + + // Recover intent for bool values converted to *bool. + clusterv1.Convert_bool_To_Pointer_bool(src.Spec.Paused, ok, restored.Spec.Paused, &dst.Spec.Paused) + + initialization := clusterv1.ClusterInitializationStatus{} + restoredControlPlaneInitialized := restored.Status.Initialization.ControlPlaneInitialized + restoredInfrastructureProvisioned := restored.Status.Initialization.InfrastructureProvisioned + clusterv1.Convert_bool_To_Pointer_bool(src.Status.ControlPlaneReady, ok, restoredControlPlaneInitialized, &initialization.ControlPlaneInitialized) + clusterv1.Convert_bool_To_Pointer_bool(src.Status.InfrastructureReady, ok, restoredInfrastructureProvisioned, &initialization.InfrastructureProvisioned) + if !reflect.DeepEqual(initialization, clusterv1.ClusterInitializationStatus{}) { + dst.Status.Initialization = initialization + } + + for i, fd := range dst.Status.FailureDomains { + srcFD, ok := src.Status.FailureDomains[fd.Name] + if !ok { + return fmt.Errorf("failure domain %q not found in source data", fd.Name) + } + var restoredFDControlPlane *bool + for _, restoredFD := range restored.Status.FailureDomains { + if restoredFD.Name == fd.Name { + restoredFDControlPlane = restoredFD.ControlPlane + break + } + } + clusterv1.Convert_bool_To_Pointer_bool(srcFD.ControlPlane, ok, restoredFDControlPlane, &fd.ControlPlane) + dst.Status.FailureDomains[i] = fd + } + + // Recover other values + if ok { + dst.Spec.AvailabilityGates = restored.Spec.AvailabilityGates + dst.Spec.Topology.ClassRef.Namespace = restored.Spec.Topology.ClassRef.Namespace + dst.Spec.Topology.Variables = restored.Spec.Topology.Variables + dst.Spec.Topology.ControlPlane.Variables = restored.Spec.Topology.ControlPlane.Variables + + dst.Spec.Topology.ControlPlane.HealthCheck = restored.Spec.Topology.ControlPlane.HealthCheck + + if restored.Spec.Topology.ControlPlane.Deletion.NodeDrainTimeoutSeconds != nil { + dst.Spec.Topology.ControlPlane.Deletion.NodeDrainTimeoutSeconds = restored.Spec.Topology.ControlPlane.Deletion.NodeDrainTimeoutSeconds + } + + if restored.Spec.Topology.ControlPlane.Deletion.NodeVolumeDetachTimeoutSeconds != nil { + dst.Spec.Topology.ControlPlane.Deletion.NodeVolumeDetachTimeoutSeconds = restored.Spec.Topology.ControlPlane.Deletion.NodeVolumeDetachTimeoutSeconds + } + + if restored.Spec.Topology.ControlPlane.Deletion.NodeDeletionTimeoutSeconds != nil { + dst.Spec.Topology.ControlPlane.Deletion.NodeDeletionTimeoutSeconds = restored.Spec.Topology.ControlPlane.Deletion.NodeDeletionTimeoutSeconds + } + dst.Spec.Topology.ControlPlane.ReadinessGates = restored.Spec.Topology.ControlPlane.ReadinessGates + + for i := range restored.Spec.Topology.Workers.MachineDeployments { + dst.Spec.Topology.Workers.MachineDeployments[i].FailureDomain = restored.Spec.Topology.Workers.MachineDeployments[i].FailureDomain + dst.Spec.Topology.Workers.MachineDeployments[i].Variables = restored.Spec.Topology.Workers.MachineDeployments[i].Variables + dst.Spec.Topology.Workers.MachineDeployments[i].ReadinessGates = restored.Spec.Topology.Workers.MachineDeployments[i].ReadinessGates + dst.Spec.Topology.Workers.MachineDeployments[i].Deletion.Order = restored.Spec.Topology.Workers.MachineDeployments[i].Deletion.Order + dst.Spec.Topology.Workers.MachineDeployments[i].Deletion.NodeDrainTimeoutSeconds = restored.Spec.Topology.Workers.MachineDeployments[i].Deletion.NodeDrainTimeoutSeconds + dst.Spec.Topology.Workers.MachineDeployments[i].Deletion.NodeVolumeDetachTimeoutSeconds = restored.Spec.Topology.Workers.MachineDeployments[i].Deletion.NodeVolumeDetachTimeoutSeconds + dst.Spec.Topology.Workers.MachineDeployments[i].Deletion.NodeDeletionTimeoutSeconds = restored.Spec.Topology.Workers.MachineDeployments[i].Deletion.NodeDeletionTimeoutSeconds + dst.Spec.Topology.Workers.MachineDeployments[i].MinReadySeconds = restored.Spec.Topology.Workers.MachineDeployments[i].MinReadySeconds + dst.Spec.Topology.Workers.MachineDeployments[i].Rollout.Strategy = restored.Spec.Topology.Workers.MachineDeployments[i].Rollout.Strategy + dst.Spec.Topology.Workers.MachineDeployments[i].HealthCheck = restored.Spec.Topology.Workers.MachineDeployments[i].HealthCheck + } + + dst.Spec.Topology.Workers.MachinePools = restored.Spec.Topology.Workers.MachinePools + + dst.Status.Conditions = restored.Status.Conditions + dst.Status.ControlPlane = restored.Status.ControlPlane + dst.Status.Workers = restored.Status.Workers + } + + return nil +} + +func ConvertClusterHubToV1Alpha4(src *clusterv1.Cluster, dst *clusterv1alpha4.Cluster) error { + if err := clusterv1alpha4.Convert_v1beta2_Cluster_To_v1alpha4_Cluster(src, dst, nil); err != nil { + return err + } + + if src.Spec.InfrastructureRef.IsDefined() { + infraRef, err := convertToObjectReference(src.Spec.InfrastructureRef, src.Namespace) + if err != nil { + return err + } + dst.Spec.InfrastructureRef = infraRef + } + + if src.Spec.ControlPlaneRef.IsDefined() { + controlPlaneRef, err := convertToObjectReference(src.Spec.ControlPlaneRef, src.Namespace) + if err != nil { + return err + } + dst.Spec.ControlPlaneRef = controlPlaneRef + } + + if dst.Spec.ClusterNetwork != nil && dst.Spec.ClusterNetwork.APIServerPort != nil && + *dst.Spec.ClusterNetwork.APIServerPort == 0 { + dst.Spec.ClusterNetwork.APIServerPort = nil + } + + // Reset conditions from autogenerated conversions + // NOTE: v1beta2 conditions should not be automatically be converted into legacy conditions (v1alpha4). + dst.Status.Conditions = nil + + // Retrieve legacy conditions (v1alpha4), failureReason and failureMessage from the deprecated field. + if src.Status.Deprecated != nil { + if src.Status.Deprecated.V1Beta1 != nil { + if src.Status.Deprecated.V1Beta1.Conditions != nil { + clusterv1alpha4.Convert_v1beta2_Deprecated_V1Beta1_Conditions_To_v1alpha4_Conditions(&src.Status.Deprecated.V1Beta1.Conditions, &dst.Status.Conditions) + } + dst.Status.FailureReason = src.Status.Deprecated.V1Beta1.FailureReason + dst.Status.FailureMessage = src.Status.Deprecated.V1Beta1.FailureMessage + } + } + + // Move initialization to old fields + dst.Status.ControlPlaneReady = ptr.Deref(src.Status.Initialization.ControlPlaneInitialized, false) + dst.Status.InfrastructureReady = ptr.Deref(src.Status.Initialization.InfrastructureProvisioned, false) + + // Preserve Hub data on down-conversion except for metadata + if err := utilconversion.MarshalData(src, dst); err != nil { + return err + } + + return nil +} + +func ConvertClusterV1Alpha3ToHub(src *clusterv1alpha3.Cluster, dst *clusterv1.Cluster) error { + if err := clusterv1alpha3.Convert_v1alpha3_Cluster_To_v1beta2_Cluster(src, dst, nil); err != nil { + return err + } + + if src.Spec.InfrastructureRef != nil { + infraRef, err := convertToContractVersionedObjectReference(src.Spec.InfrastructureRef) + if err != nil { + return err + } + dst.Spec.InfrastructureRef = infraRef + } + + if src.Spec.ControlPlaneRef != nil { + controlPlaneRef, err := convertToContractVersionedObjectReference(src.Spec.ControlPlaneRef) + if err != nil { + return err + } + dst.Spec.ControlPlaneRef = controlPlaneRef + } + + // Reset conditions from autogenerated conversions + // NOTE: v1alpha3 conditions should not be automatically be converted into v1beta2 conditions. + dst.Status.Conditions = nil + + // Move legacy conditions (v1alpha3), failureReason and failureMessage to the deprecated field. + if src.Status.Conditions != nil || src.Status.FailureReason != nil || src.Status.FailureMessage != nil { + dst.Status.Deprecated = &clusterv1.ClusterDeprecatedStatus{} + dst.Status.Deprecated.V1Beta1 = &clusterv1.ClusterV1Beta1DeprecatedStatus{} + if src.Status.Conditions != nil { + clusterv1alpha3.Convert_v1alpha3_Conditions_To_v1beta2_Deprecated_V1Beta1_Conditions(&src.Status.Conditions, &dst.Status.Deprecated.V1Beta1.Conditions) + } + dst.Status.Deprecated.V1Beta1.FailureReason = src.Status.FailureReason + dst.Status.Deprecated.V1Beta1.FailureMessage = src.Status.FailureMessage + } + + // Given this is a bool and there is no timestamp associated with it, when this condition is set, its timestamp + // will be "now". See https://github.com/kubernetes-sigs/cluster-api/issues/3798#issuecomment-708619826 for more + // discussion. + if src.Status.ControlPlaneInitialized { + v1beta1conditions.MarkTrue(dst, clusterv1.ControlPlaneInitializedV1Beta1Condition) + } + + // Manually restore data. + restored := &clusterv1.Cluster{} + ok, err := utilconversion.UnmarshalData(src, restored) + if err != nil { + return err + } + + // Recover intent for bool values converted to *bool. + clusterv1.Convert_bool_To_Pointer_bool(src.Spec.Paused, ok, restored.Spec.Paused, &dst.Spec.Paused) + + initialization := clusterv1.ClusterInitializationStatus{} + restoredControlPlaneInitialized := restored.Status.Initialization.ControlPlaneInitialized + restoredInfrastructureProvisioned := restored.Status.Initialization.InfrastructureProvisioned + clusterv1.Convert_bool_To_Pointer_bool(src.Status.ControlPlaneReady, ok, restoredControlPlaneInitialized, &initialization.ControlPlaneInitialized) + clusterv1.Convert_bool_To_Pointer_bool(src.Status.InfrastructureReady, ok, restoredInfrastructureProvisioned, &initialization.InfrastructureProvisioned) + if !reflect.DeepEqual(initialization, clusterv1.ClusterInitializationStatus{}) { + dst.Status.Initialization = initialization + } + + for i, fd := range dst.Status.FailureDomains { + srcFD, ok := src.Status.FailureDomains[fd.Name] + if !ok { + return fmt.Errorf("failure domain %q not found in source data", fd.Name) + } + var restoredFDControlPlane *bool + for _, restoredFD := range restored.Status.FailureDomains { + if restoredFD.Name == fd.Name { + restoredFDControlPlane = restoredFD.ControlPlane + break + } + } + clusterv1.Convert_bool_To_Pointer_bool(srcFD.ControlPlane, ok, restoredFDControlPlane, &fd.ControlPlane) + dst.Status.FailureDomains[i] = fd + } + + // Recover other values + if ok { + dst.Spec.AvailabilityGates = restored.Spec.AvailabilityGates + dst.Spec.Topology = restored.Spec.Topology + dst.Status.Conditions = restored.Status.Conditions + dst.Status.ControlPlane = restored.Status.ControlPlane + dst.Status.Workers = restored.Status.Workers + } + + return nil +} + +func ConvertClusterHubToV1Alpha3(src *clusterv1.Cluster, dst *clusterv1alpha3.Cluster) error { + if err := clusterv1alpha3.Convert_v1beta2_Cluster_To_v1alpha3_Cluster(src, dst, nil); err != nil { + return err + } + + if src.Spec.InfrastructureRef.IsDefined() { + infraRef, err := convertToObjectReference(src.Spec.InfrastructureRef, src.Namespace) + if err != nil { + return err + } + dst.Spec.InfrastructureRef = infraRef + } + + if src.Spec.ControlPlaneRef.IsDefined() { + controlPlaneRef, err := convertToObjectReference(src.Spec.ControlPlaneRef, src.Namespace) + if err != nil { + return err + } + dst.Spec.ControlPlaneRef = controlPlaneRef + } + + if dst.Spec.ClusterNetwork != nil && dst.Spec.ClusterNetwork.APIServerPort != nil && + *dst.Spec.ClusterNetwork.APIServerPort == 0 { + dst.Spec.ClusterNetwork.APIServerPort = nil + } + + // Reset conditions from autogenerated conversions + // NOTE: v1beta2 conditions should not be automatically be converted into legacy conditions (v1alpha3). + dst.Status.Conditions = nil + + // Retrieve legacy conditions (v1alpha3), failureReason and failureMessage from the deprecated field. + if src.Status.Deprecated != nil { + if src.Status.Deprecated.V1Beta1 != nil { + if src.Status.Deprecated.V1Beta1.Conditions != nil { + clusterv1alpha3.Convert_v1beta2_Deprecated_V1Beta1_Conditions_To_v1alpha3_Conditions(&src.Status.Deprecated.V1Beta1.Conditions, &dst.Status.Conditions) + } + dst.Status.FailureReason = src.Status.Deprecated.V1Beta1.FailureReason + dst.Status.FailureMessage = src.Status.Deprecated.V1Beta1.FailureMessage + } + } + + // Set the v1alpha3 boolean status field if the v1alpha4 condition was true + if v1beta1conditions.IsTrue(src, clusterv1.ControlPlaneInitializedV1Beta1Condition) { + dst.Status.ControlPlaneInitialized = true + } + + // Move initialization to old fields + dst.Status.ControlPlaneReady = ptr.Deref(src.Status.Initialization.ControlPlaneInitialized, false) + dst.Status.InfrastructureReady = ptr.Deref(src.Status.Initialization.InfrastructureProvisioned, false) + + // Preserve Hub data on down-conversion except for metadata + if err := utilconversion.MarshalData(src, dst); err != nil { + return err + } + + return nil +} + +func convertToContractVersionedObjectReference(ref *corev1.ObjectReference) (clusterv1.ContractVersionedObjectReference, error) { + var apiGroup string + if ref.APIVersion != "" { + gv, err := schema.ParseGroupVersion(ref.APIVersion) + if err != nil { + return clusterv1.ContractVersionedObjectReference{}, fmt.Errorf("failed to convert object: failed to parse apiVersion: %v", err) + } + apiGroup = gv.Group + } + return clusterv1.ContractVersionedObjectReference{ + APIGroup: apiGroup, + Kind: ref.Kind, + Name: ref.Name, + }, nil +} + +func convertToObjectReference(ref clusterv1.ContractVersionedObjectReference, namespace string) (*corev1.ObjectReference, error) { + apiVersion, err := apiVersionGetter(schema.GroupKind{ + Group: ref.APIGroup, + Kind: ref.Kind, + }) + if err != nil { + return nil, fmt.Errorf("failed to convert object: %v", err) + } + return &corev1.ObjectReference{ + APIVersion: apiVersion, + Kind: ref.Kind, + Namespace: namespace, + Name: ref.Name, + }, nil +} + +func dropEmptyStringsCluster(dst *clusterv1beta1.Cluster) { + if dst.Spec.Topology != nil { + if dst.Spec.Topology.Workers != nil { + for i, md := range dst.Spec.Topology.Workers.MachineDeployments { + dropEmptyString(&md.FailureDomain) + dst.Spec.Topology.Workers.MachineDeployments[i] = md + } + } + } +} + +func dropEmptyString(s **string) { + if *s != nil && **s == "" { + *s = nil + } +} diff --git a/internal/webhooks/extensionconfig.go b/internal/webhooks/extensionconfig.go index ac8390de88e5..76509134a10a 100644 --- a/internal/webhooks/extensionconfig.go +++ b/internal/webhooks/extensionconfig.go @@ -31,7 +31,9 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + "sigs.k8s.io/controller-runtime/pkg/webhook/conversion" + runtimev1alpha1 "sigs.k8s.io/cluster-api/api/runtime/v1alpha1" runtimev1 "sigs.k8s.io/cluster-api/api/runtime/v1beta2" "sigs.k8s.io/cluster-api/feature" ) @@ -44,6 +46,9 @@ func (webhook *ExtensionConfig) SetupWebhookWithManager(mgr ctrl.Manager) error For(&runtimev1.ExtensionConfig{}). WithDefaulter(webhook). WithValidator(webhook). + WithConverters( + conversion.NewConverter(&runtimev1.ExtensionConfig{}, &runtimev1alpha1.ExtensionConfig{}, ConvertExtensionConfigHubToV1Alpha1, ConvertExtensionConfigV1Alpha1ToHub), + ). Complete() } diff --git a/internal/webhooks/extensionconfig_conversion.go b/internal/webhooks/extensionconfig_conversion.go new file mode 100644 index 000000000000..6072ac4b9e08 --- /dev/null +++ b/internal/webhooks/extensionconfig_conversion.go @@ -0,0 +1,48 @@ +/* +Copyright 2025 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. +*/ + +package webhooks + +import ( + runtimev1alpha1 "sigs.k8s.io/cluster-api/api/runtime/v1alpha1" + runtimev1 "sigs.k8s.io/cluster-api/api/runtime/v1beta2" +) + +func ConvertExtensionConfigV1Alpha1ToHub(src *runtimev1alpha1.ExtensionConfig, dst *runtimev1.ExtensionConfig) error { + return runtimev1alpha1.Convert_v1alpha1_ExtensionConfig_To_v1beta2_ExtensionConfig(src, dst, nil) +} + +func ConvertExtensionConfigHubToV1Alpha1(src *runtimev1.ExtensionConfig, dst *runtimev1alpha1.ExtensionConfig) error { + if err := runtimev1alpha1.Convert_v1beta2_ExtensionConfig_To_v1alpha1_ExtensionConfig(src, dst, nil); err != nil { + return err + } + + dropEmptyStringsExtensionConfig(dst) + for i, h := range dst.Status.Handlers { + if h.TimeoutSeconds != nil && *h.TimeoutSeconds == 0 { + h.TimeoutSeconds = nil + } + dst.Status.Handlers[i] = h + } + return nil +} + +func dropEmptyStringsExtensionConfig(dst *runtimev1alpha1.ExtensionConfig) { + dropEmptyString(&dst.Spec.ClientConfig.URL) + if dst.Spec.ClientConfig.Service != nil { + dropEmptyString(&dst.Spec.ClientConfig.Service.Path) + } +} diff --git a/main.go b/main.go index f8056b6efc17..1f41e569469a 100644 --- a/main.go +++ b/main.go @@ -79,6 +79,7 @@ import ( "sigs.k8s.io/cluster-api/internal/contract" internalruntimeclient "sigs.k8s.io/cluster-api/internal/runtime/client" runtimeregistry "sigs.k8s.io/cluster-api/internal/runtime/registry" + internalwebhooks "sigs.k8s.io/cluster-api/internal/webhooks" "sigs.k8s.io/cluster-api/util/apiwarnings" "sigs.k8s.io/cluster-api/util/flags" "sigs.k8s.io/cluster-api/version" diff --git a/test/go.mod b/test/go.mod index 72109ee3e970..aae8cf7aa354 100644 --- a/test/go.mod +++ b/test/go.mod @@ -4,6 +4,8 @@ go 1.24.0 replace sigs.k8s.io/cluster-api => ../ +replace sigs.k8s.io/controller-runtime => ../../controller-runtime + require ( github.com/blang/semver/v4 v4.0.0 github.com/docker/docker v28.4.0+incompatible diff --git a/test/go.sum b/test/go.sum index de9ff79cf12b..e8d194dccf6e 100644 --- a/test/go.sum +++ b/test/go.sum @@ -474,8 +474,6 @@ k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8 k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= -sigs.k8s.io/controller-runtime v0.22.1 h1:Ah1T7I+0A7ize291nJZdS1CabF/lB4E++WizgV24Eqg= -sigs.k8s.io/controller-runtime v0.22.1/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kind v0.30.0 h1:2Xi1KFEfSMm0XDcvKnUt15ZfgRPCT0OnCBbpgh8DztY=