From 1bd8a418f5f42d412de18bdd47b2161bcc8a4937 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 12 Jun 2026 13:19:21 +0000 Subject: [PATCH 01/94] feat: upgrade cluster-api to v1.11.11 with v1beta2 types and contract - Bump sigs.k8s.io/cluster-api and cluster-api/test to v1.11.11, controller-runtime to v0.21.0, k8s.io/* to v0.33.3 - Move core CAPI imports to api/core/v1beta2, IPAM to api/ipam/v1beta2, addons/bootstrap/controlplane test imports to their v1beta2 packages - Adopt v1beta2 contract Stage 2 status layout on IonosCloudCluster and IonosCloudMachine: status.initialization.provisioned (*bool, omitzero, MinProperties=1 per contract conventions), top-level metav1.Conditions, deprecated v1beta1 fields under status.deprecated.v1beta1 - Add cluster.x-k8s.io/v1beta2 contract label to CRDs - Wire CRDMigrator with UseStatusForStorageVersionMigration and concurrency 1; update patch helper owned-conditions options - Update importas aliases for the new CAPI package layout - Regenerate deepcopy, CRD manifests and mocks Co-Authored-By: Claude Fable 5 --- .golangci.yml | 14 +- api/v1alpha1/ionoscloudcluster_types.go | 82 +++++- api/v1alpha1/ionoscloudcluster_types_test.go | 24 +- api/v1alpha1/ionoscloudmachine_types.go | 82 +++++- api/v1alpha1/ionoscloudmachine_types_test.go | 10 +- .../ionoscloudmachinetemplate_types.go | 2 +- api/v1alpha1/suite_test.go | 2 +- api/v1alpha1/zz_generated.deepcopy.go | 143 +++++++++- cmd/main.go | 75 +++-- ...e.cluster.x-k8s.io_ionoscloudclusters.yaml | 159 ++++++++--- ...r.x-k8s.io_ionoscloudclustertemplates.yaml | 7 +- ...e.cluster.x-k8s.io_ionoscloudmachines.yaml | 152 ++++++++-- ...r.x-k8s.io_ionoscloudmachinetemplates.yaml | 1 + config/crd/kustomization.yaml | 1 + go.mod | 105 ++++--- go.sum | 263 +++++++----------- .../ionoscloudcluster_controller.go | 26 +- .../ionoscloudmachine_controller.go | 28 +- internal/service/cloud/image.go | 2 +- internal/service/cloud/image_test.go | 6 +- internal/service/cloud/ipblock.go | 2 +- internal/service/cloud/ipblock_test.go | 2 +- internal/service/cloud/network.go | 14 +- internal/service/cloud/network_test.go | 2 +- internal/service/cloud/server.go | 19 +- internal/service/cloud/suite_test.go | 8 +- internal/service/k8s/ipam.go | 14 +- internal/service/k8s/ipam_test.go | 31 ++- scope/cluster.go | 30 +- scope/cluster_test.go | 2 +- scope/machine.go | 24 +- scope/machine_test.go | 2 +- test/e2e/capic_test.go | 6 +- test/e2e/helpers/finalizers.go | 2 +- test/e2e/helpers/ownerreference.go | 8 +- test/e2e/suite_test.go | 4 +- 36 files changed, 915 insertions(+), 439 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 7871cfc3..b168f76b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -84,10 +84,16 @@ linters-settings: - pkg: sigs.k8s.io/controller-runtime alias: ctrl # Cluster API - - pkg: sigs.k8s.io/cluster-api/api/v1beta1 + - pkg: sigs.k8s.io/cluster-api/api/core/v1beta2 alias: clusterv1 - pkg: "sigs.k8s.io/cluster-api/test/e2e" alias: capie2e + - pkg: "sigs.k8s.io/cluster-api/util/conditions" + alias: conditions + - pkg: "sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1" + alias: deprecatedv1beta1conditions + - pkg: "sigs.k8s.io/cluster-api/api/ipam/v1beta2" + alias: ipamv1 # Own module - pkg: github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1 alias: infrav1 @@ -120,6 +126,8 @@ linters-settings: disabled: true - name: range-val-address disabled: true # Obsolete since Go 1.22 + - name: redundant-import-alias + disabled: true # Conflicts with importas which enforces explicit aliases - name: struct-tag arguments: - json,inline @@ -146,7 +154,7 @@ issues: - linters: - revive text: "comment-spacings: no space between comment delimiter and comment text" - source: "//(\\+(kubebuilder|optional|required)|#nosec)" + source: "//(\\+(kubebuilder|optional|required|list)|#nosec)" - linters: - revive text: "exported: exported method .*\\.(Reconcile|SetupWithManager|SetupWebhookWithManager) should have comment or be unexported" @@ -205,4 +213,4 @@ run: - e2e timeout: 10m allow-parallel-runners: true - go: "1.22" + go: "1.25" diff --git a/api/v1alpha1/ionoscloudcluster_types.go b/api/v1alpha1/ionoscloudcluster_types.go index 22dcd654..afa450b7 100644 --- a/api/v1alpha1/ionoscloudcluster_types.go +++ b/api/v1alpha1/ionoscloudcluster_types.go @@ -19,7 +19,7 @@ package v1alpha1 import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" ) const ( @@ -64,13 +64,19 @@ type IonosCloudClusterSpec struct { // IonosCloudClusterStatus defines the observed state of IonosCloudCluster. type IonosCloudClusterStatus struct { - // Ready indicates that the cluster is ready. + // Initialization provides observations of the IonosCloudCluster initialization process. + // NOTE: Fields in this struct are part of the Cluster API contract and are used to orchestrate initial + // cluster provisioning. The value of these fields is never updated after initial provisioning is completed. + // Use conditions to monitor the operational state of the cluster's infrastructure. //+optional - Ready bool `json:"ready,omitempty"` + Initialization IonosCloudClusterInitializationStatus `json:"initialization,omitempty,omitzero"` - // Conditions defines current service state of the IonosCloudCluster. + // Conditions represents the observations of the current state of the IonosCloudCluster. //+optional - Conditions clusterv1.Conditions `json:"conditions,omitempty"` + //+listType=map + //+listMapKey=type + //+kubebuilder:validation:MaxItems=32 + Conditions []metav1.Condition `json:"conditions,omitempty"` // CurrentRequestByDatacenter maps data center IDs to a pending provisioning request made during reconciliation. //+optional @@ -83,13 +89,50 @@ type IonosCloudClusterStatus struct { // ControlPlaneEndpointIPBlockID is the IONOS Cloud UUID for the control plane endpoint IP block. //+optional ControlPlaneEndpointIPBlockID string `json:"controlPlaneEndpointIPBlockID,omitempty"` + + // Deprecated groups all status fields deprecated and scheduled for removal when v1beta1 contract support is dropped. + //+optional + Deprecated *IonosCloudClusterDeprecatedStatus `json:"deprecated,omitempty"` +} + +// IonosCloudClusterInitializationStatus provides observations of the IonosCloudCluster initialization process. +// +kubebuilder:validation:MinProperties=1 +type IonosCloudClusterInitializationStatus struct { + // Provisioned is true when the infrastructure cluster is fully provisioned. + // NOTE: this field is part of the Cluster API contract and is used to orchestrate provisioning. + // The value of this field is never updated after initial provisioning is completed. + //+optional + Provisioned *bool `json:"provisioned,omitempty"` +} + +// IonosCloudClusterDeprecatedStatus groups all status fields deprecated and scheduled for removal when +// v1beta1 contract support is dropped. +type IonosCloudClusterDeprecatedStatus struct { + // V1Beta1 groups all v1beta1 status fields that are deprecated and scheduled for removal. + //+optional + V1Beta1 *IonosCloudClusterV1Beta1DeprecatedStatus `json:"v1beta1,omitempty"` +} + +// IonosCloudClusterV1Beta1DeprecatedStatus contains deprecated v1beta1 fields. +type IonosCloudClusterV1Beta1DeprecatedStatus struct { + // Ready indicates that the cluster is ready. + // + // Deprecated: Use Initialization.Provisioned instead. + //+optional + Ready bool `json:"ready,omitempty"` + + // Conditions defines current service state of the IonosCloudCluster using the deprecated v1beta1 condition type. + // + // Deprecated: Use the top-level conditions field instead. + //+optional + Conditions clusterv1.Conditions `json:"conditions,omitempty"` } //+kubebuilder:object:root=true //+kubebuilder:subresource:status //+kubebuilder:resource:path=ionoscloudclusters,scope=Namespaced,categories=cluster-api;ionoscloud,shortName=icc //+kubebuilder:printcolumn:name="Cluster",type="string",JSONPath=".metadata.labels['cluster\\.x-k8s\\.io/cluster-name']",description="Cluster" -//+kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.ready",description="Cluster infrastructure is ready" +//+kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.initialization.provisioned",description="Cluster infrastructure is ready" //+kubebuilder:printcolumn:name="Endpoint",type="string",JSONPath=".spec.controlPlaneEndpoint",description="API Endpoint" // IonosCloudCluster is the Schema for the ionoscloudclusters API. @@ -114,13 +157,32 @@ func init() { objectTypes = append(objectTypes, &IonosCloudCluster{}, &IonosCloudClusterList{}) } -// GetConditions returns the conditions from the status. -func (i *IonosCloudCluster) GetConditions() clusterv1.Conditions { +// GetV1Beta1Conditions returns the deprecated v1beta1 conditions from status.deprecated.v1beta1.conditions. +func (i *IonosCloudCluster) GetV1Beta1Conditions() clusterv1.Conditions { + if i.Status.Deprecated == nil || i.Status.Deprecated.V1Beta1 == nil { + return nil + } + return i.Status.Deprecated.V1Beta1.Conditions +} + +// SetV1Beta1Conditions sets the deprecated v1beta1 conditions in status.deprecated.v1beta1.conditions. +func (i *IonosCloudCluster) SetV1Beta1Conditions(conditions clusterv1.Conditions) { + if i.Status.Deprecated == nil { + i.Status.Deprecated = &IonosCloudClusterDeprecatedStatus{} + } + if i.Status.Deprecated.V1Beta1 == nil { + i.Status.Deprecated.V1Beta1 = &IonosCloudClusterV1Beta1DeprecatedStatus{} + } + i.Status.Deprecated.V1Beta1.Conditions = conditions +} + +// GetConditions returns the v1beta2 conditions from status.conditions. +func (i *IonosCloudCluster) GetConditions() []metav1.Condition { return i.Status.Conditions } -// SetConditions sets the conditions in the status. -func (i *IonosCloudCluster) SetConditions(conditions clusterv1.Conditions) { +// SetConditions sets the v1beta2 conditions in status.conditions. +func (i *IonosCloudCluster) SetConditions(conditions []metav1.Condition) { i.Status.Conditions = conditions } diff --git a/api/v1alpha1/ionoscloudcluster_types_test.go b/api/v1alpha1/ionoscloudcluster_types_test.go index d28fcd1e..8247160c 100644 --- a/api/v1alpha1/ionoscloudcluster_types_test.go +++ b/api/v1alpha1/ionoscloudcluster_types_test.go @@ -23,10 +23,12 @@ import ( "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - "sigs.k8s.io/cluster-api/util/conditions" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" + deprecatedv1beta1conditions "sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -41,8 +43,8 @@ func TestIonosCloudCluster_Conditions(t *testing.T) { conds := clusterv1.Conditions{{Type: "type"}} cluster := &IonosCloudCluster{} - cluster.SetConditions(conds) - require.Equal(t, conds, cluster.GetConditions()) + cluster.SetV1Beta1Conditions(conds) + require.Equal(t, conds, cluster.GetV1Beta1Conditions()) } func defaultCluster() *IonosCloudCluster { @@ -149,12 +151,12 @@ var _ = Describe("IonosCloudCluster", func() { key := client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name} fetched := &IonosCloudCluster{} Expect(k8sClient.Get(context.Background(), key, fetched)).To(Succeed()) - Expect(fetched.Status.Ready).To(BeFalse()) + Expect(fetched.Status.Initialization.Provisioned).To(BeNil()) Expect(fetched.Status.CurrentRequestByDatacenter).To(BeEmpty()) - Expect(fetched.Status.Conditions).To(BeEmpty()) + Expect(fetched.GetV1Beta1Conditions()).To(BeEmpty()) By("retrieving the cluster and setting the status") - fetched.Status.Ready = true + fetched.Status.Initialization = IonosCloudClusterInitializationStatus{Provisioned: ptr.To(true)} wantProvisionRequest := ProvisioningRequest{ Method: "POST", RequestPath: "/path/to/resource", @@ -163,17 +165,17 @@ var _ = Describe("IonosCloudCluster", func() { fetched.Status.CurrentRequestByDatacenter = map[string]ProvisioningRequest{ "123": wantProvisionRequest, } - conditions.MarkTrue(fetched, clusterv1.ReadyCondition) + deprecatedv1beta1conditions.MarkTrue(fetched, clusterv1.ReadyCondition) By("updating the cluster status") Expect(k8sClient.Status().Update(context.Background(), fetched)).To(Succeed()) Expect(k8sClient.Get(context.Background(), key, fetched)).To(Succeed()) - Expect(fetched.Status.Ready).To(BeTrue()) + Expect(fetched.Status.Initialization.Provisioned).To(HaveValue(BeTrue())) Expect(fetched.Status.CurrentRequestByDatacenter).To(HaveLen(1)) Expect(fetched.Status.CurrentRequestByDatacenter["123"]).To(Equal(wantProvisionRequest)) - Expect(fetched.Status.Conditions).To(HaveLen(1)) - Expect(conditions.IsTrue(fetched, clusterv1.ReadyCondition)).To(BeTrue()) + Expect(fetched.GetV1Beta1Conditions()).To(HaveLen(1)) + Expect(deprecatedv1beta1conditions.IsTrue(fetched, clusterv1.ReadyCondition)).To(BeTrue()) By("Removing the entry from the status again") delete(fetched.Status.CurrentRequestByDatacenter, "123") diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index b81efce5..02fa15bc 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -20,7 +20,7 @@ import ( "strings" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" ) @@ -287,17 +287,23 @@ type ImageSelector struct { // IonosCloudMachineStatus defines the observed state of IonosCloudMachine. type IonosCloudMachineStatus struct { - // Ready indicates the VM has been provisioned and is ready. + // Initialization provides observations of the IonosCloudMachine initialization process. + // NOTE: Fields in this struct are part of the Cluster API contract and are used to orchestrate initial + // machine provisioning. The value of these fields is never updated after initial provisioning is completed. + // Use conditions to monitor the operational state of the machine's infrastructure. //+optional - Ready bool `json:"ready"` + Initialization IonosCloudMachineInitializationStatus `json:"initialization,omitempty,omitzero"` // MachineNetworkInfo contains information about the network configuration of the VM. //+optional MachineNetworkInfo *MachineNetworkInfo `json:"machineNetworkInfo,omitempty"` - // Conditions defines current service state of the IonosCloudMachine. + // Conditions represents the observations of the current state of the IonosCloudMachine. //+optional - Conditions clusterv1.Conditions `json:"conditions,omitempty"` + //+listType=map + //+listMapKey=type + //+kubebuilder:validation:MaxItems=32 + Conditions []metav1.Condition `json:"conditions,omitempty"` // CurrentRequest shows the current provisioning request for any // cloud resource that is being provisioned. @@ -307,6 +313,43 @@ type IonosCloudMachineStatus struct { // Location is the location of the datacenter the VM is provisioned in. //+optional Location string `json:"location"` + + // Deprecated groups all status fields deprecated and scheduled for removal when v1beta1 contract support is dropped. + //+optional + Deprecated *IonosCloudMachineDeprecatedStatus `json:"deprecated,omitempty"` +} + +// IonosCloudMachineInitializationStatus provides observations of the IonosCloudMachine initialization process. +// +kubebuilder:validation:MinProperties=1 +type IonosCloudMachineInitializationStatus struct { + // Provisioned is true when the machine infrastructure is fully provisioned. + // NOTE: this field is part of the Cluster API contract and is used to orchestrate provisioning. + // The value of this field is never updated after initial provisioning is completed. + //+optional + Provisioned *bool `json:"provisioned,omitempty"` +} + +// IonosCloudMachineDeprecatedStatus groups all status fields deprecated and scheduled for removal when +// v1beta1 contract support is dropped. +type IonosCloudMachineDeprecatedStatus struct { + // V1Beta1 groups all v1beta1 status fields that are deprecated and scheduled for removal. + //+optional + V1Beta1 *IonosCloudMachineV1Beta1DeprecatedStatus `json:"v1beta1,omitempty"` +} + +// IonosCloudMachineV1Beta1DeprecatedStatus contains deprecated v1beta1 fields. +type IonosCloudMachineV1Beta1DeprecatedStatus struct { + // Ready indicates the VM has been provisioned and is ready. + // + // Deprecated: Use Initialization.Provisioned instead. + //+optional + Ready bool `json:"ready,omitempty"` + + // Conditions defines current service state of the IonosCloudMachine using the deprecated v1beta1 condition type. + // + // Deprecated: Use the top-level conditions field instead. + //+optional + Conditions clusterv1.Conditions `json:"conditions,omitempty"` } // MachineNetworkInfo contains information about the network configuration of the VM. @@ -343,7 +386,7 @@ type NICInfo struct { //+kubebuilder:subresource:status //+kubebuilder:resource:path=ionoscloudmachines,scope=Namespaced,categories=cluster-api;ionoscloud,shortName=icm //+kubebuilder:printcolumn:name="Cluster",type="string",JSONPath=".metadata.labels['cluster\\.x-k8s\\.io/cluster-name']",description="Cluster" -//+kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.ready",description="Machine is ready" +//+kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.initialization.provisioned",description="Machine is ready" //+kubebuilder:printcolumn:name="IPv4 Addresses",type="string",JSONPath=".status.machineNetworkInfo.nicInfo[*].ipv4Addresses" //+kubebuilder:printcolumn:name="Machine Connected Networks",type="string",JSONPath=".status.machineNetworkInfo.nicInfo[*].networkID" //+kubebuilder:printcolumn:name="IPv6 Addresses",type="string",JSONPath=".status.machineNetworkInfo.nicInfo[*].ipv6Addresses",priority=1 @@ -367,13 +410,32 @@ type IonosCloudMachineList struct { Items []IonosCloudMachine `json:"items"` } -// GetConditions returns the observations of the operational state of the IonosCloudMachine resource. -func (m *IonosCloudMachine) GetConditions() clusterv1.Conditions { +// GetV1Beta1Conditions returns the deprecated v1beta1 conditions from status.deprecated.v1beta1.conditions. +func (m *IonosCloudMachine) GetV1Beta1Conditions() clusterv1.Conditions { + if m.Status.Deprecated == nil || m.Status.Deprecated.V1Beta1 == nil { + return nil + } + return m.Status.Deprecated.V1Beta1.Conditions +} + +// SetV1Beta1Conditions sets the deprecated v1beta1 conditions in status.deprecated.v1beta1.conditions. +func (m *IonosCloudMachine) SetV1Beta1Conditions(conditions clusterv1.Conditions) { + if m.Status.Deprecated == nil { + m.Status.Deprecated = &IonosCloudMachineDeprecatedStatus{} + } + if m.Status.Deprecated.V1Beta1 == nil { + m.Status.Deprecated.V1Beta1 = &IonosCloudMachineV1Beta1DeprecatedStatus{} + } + m.Status.Deprecated.V1Beta1.Conditions = conditions +} + +// GetConditions returns the v1beta2 conditions from status.conditions. +func (m *IonosCloudMachine) GetConditions() []metav1.Condition { return m.Status.Conditions } -// SetConditions sets the underlying service state of the IonosCloudMachine to the predescribed clusterv1.Conditions. -func (m *IonosCloudMachine) SetConditions(conditions clusterv1.Conditions) { +// SetConditions sets the v1beta2 conditions in status.conditions. +func (m *IonosCloudMachine) SetConditions(conditions []metav1.Condition) { m.Status.Conditions = conditions } diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index 5d11c27a..b2100671 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -23,7 +23,7 @@ import ( sdk "github.com/ionos-cloud/sdk-go/v6" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/cluster-api/util/conditions" + deprecatedv1beta1conditions "sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" @@ -535,13 +535,13 @@ var _ = Describe("IonosCloudMachine Tests", func() { context.Background(), client.ObjectKey{Name: m.Name, Namespace: m.Namespace}, m)).To(Succeed()) // Calls SetConditions with required fields - conditions.MarkTrue(m, MachineProvisionedCondition) + deprecatedv1beta1conditions.MarkTrue(m, MachineProvisionedCondition) Expect(k8sClient.Status().Update(context.Background(), m)).To(Succeed()) Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: m.Name, Namespace: m.Namespace}, m)).To(Succeed()) - machineConditions := m.GetConditions() + machineConditions := m.GetV1Beta1Conditions() Expect(machineConditions).To(HaveLen(1)) Expect(machineConditions[0].Type).To(Equal(MachineProvisionedCondition)) Expect(machineConditions[0].Status).To(Equal(corev1.ConditionTrue)) @@ -554,8 +554,8 @@ var _ = Describe("IonosCloudMachine Tests", func() { Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: m.Name, Namespace: m.Namespace}, m)).To(Succeed()) - m.Status.Ready = true - conditions.MarkTrue(m, MachineProvisionedCondition) + m.Status.Initialization = IonosCloudMachineInitializationStatus{Provisioned: ptr.To(true)} + deprecatedv1beta1conditions.MarkTrue(m, MachineProvisionedCondition) m.Status.CurrentRequest = &ProvisioningRequest{ Method: "GET", RequestPath: "path/to/resource", diff --git a/api/v1alpha1/ionoscloudmachinetemplate_types.go b/api/v1alpha1/ionoscloudmachinetemplate_types.go index 26118a72..b934601f 100644 --- a/api/v1alpha1/ionoscloudmachinetemplate_types.go +++ b/api/v1alpha1/ionoscloudmachinetemplate_types.go @@ -18,7 +18,7 @@ package v1alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" ) // IonosCloudMachineTemplateSpec defines the desired state of IonosCloudMachineTemplate. diff --git a/api/v1alpha1/suite_test.go b/api/v1alpha1/suite_test.go index f90453bf..7994073a 100644 --- a/api/v1alpha1/suite_test.go +++ b/api/v1alpha1/suite_test.go @@ -21,7 +21,7 @@ import ( "testing" "k8s.io/apimachinery/pkg/runtime" - ipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1" + ipamv1 "sigs.k8s.io/cluster-api/api/ipam/v1beta2" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" logf "sigs.k8s.io/controller-runtime/pkg/log" diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 1adb78a3..086fdf57 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -22,8 +22,9 @@ package v1alpha1 import ( "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/cluster-api/api/core/v1beta2" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -125,6 +126,46 @@ func (in *IonosCloudCluster) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IonosCloudClusterDeprecatedStatus) DeepCopyInto(out *IonosCloudClusterDeprecatedStatus) { + *out = *in + if in.V1Beta1 != nil { + in, out := &in.V1Beta1, &out.V1Beta1 + *out = new(IonosCloudClusterV1Beta1DeprecatedStatus) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudClusterDeprecatedStatus. +func (in *IonosCloudClusterDeprecatedStatus) DeepCopy() *IonosCloudClusterDeprecatedStatus { + if in == nil { + return nil + } + out := new(IonosCloudClusterDeprecatedStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IonosCloudClusterInitializationStatus) DeepCopyInto(out *IonosCloudClusterInitializationStatus) { + *out = *in + if in.Provisioned != nil { + in, out := &in.Provisioned, &out.Provisioned + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudClusterInitializationStatus. +func (in *IonosCloudClusterInitializationStatus) DeepCopy() *IonosCloudClusterInitializationStatus { + if in == nil { + return nil + } + out := new(IonosCloudClusterInitializationStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IonosCloudClusterList) DeepCopyInto(out *IonosCloudClusterList) { *out = *in @@ -182,9 +223,10 @@ func (in *IonosCloudClusterSpec) DeepCopy() *IonosCloudClusterSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IonosCloudClusterStatus) DeepCopyInto(out *IonosCloudClusterStatus) { *out = *in + in.Initialization.DeepCopyInto(&out.Initialization) if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make(v1beta1.Conditions, len(*in)) + *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -201,6 +243,11 @@ func (in *IonosCloudClusterStatus) DeepCopyInto(out *IonosCloudClusterStatus) { *out = new(ProvisioningRequest) **out = **in } + if in.Deprecated != nil { + in, out := &in.Deprecated, &out.Deprecated + *out = new(IonosCloudClusterDeprecatedStatus) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudClusterStatus. @@ -303,6 +350,28 @@ func (in *IonosCloudClusterTemplateSpec) DeepCopy() *IonosCloudClusterTemplateSp return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IonosCloudClusterV1Beta1DeprecatedStatus) DeepCopyInto(out *IonosCloudClusterV1Beta1DeprecatedStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make(v1beta2.Conditions, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudClusterV1Beta1DeprecatedStatus. +func (in *IonosCloudClusterV1Beta1DeprecatedStatus) DeepCopy() *IonosCloudClusterV1Beta1DeprecatedStatus { + if in == nil { + return nil + } + out := new(IonosCloudClusterV1Beta1DeprecatedStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IonosCloudMachine) DeepCopyInto(out *IonosCloudMachine) { *out = *in @@ -330,6 +399,46 @@ func (in *IonosCloudMachine) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IonosCloudMachineDeprecatedStatus) DeepCopyInto(out *IonosCloudMachineDeprecatedStatus) { + *out = *in + if in.V1Beta1 != nil { + in, out := &in.V1Beta1, &out.V1Beta1 + *out = new(IonosCloudMachineV1Beta1DeprecatedStatus) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudMachineDeprecatedStatus. +func (in *IonosCloudMachineDeprecatedStatus) DeepCopy() *IonosCloudMachineDeprecatedStatus { + if in == nil { + return nil + } + out := new(IonosCloudMachineDeprecatedStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IonosCloudMachineInitializationStatus) DeepCopyInto(out *IonosCloudMachineInitializationStatus) { + *out = *in + if in.Provisioned != nil { + in, out := &in.Provisioned, &out.Provisioned + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudMachineInitializationStatus. +func (in *IonosCloudMachineInitializationStatus) DeepCopy() *IonosCloudMachineInitializationStatus { + if in == nil { + return nil + } + out := new(IonosCloudMachineInitializationStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IonosCloudMachineList) DeepCopyInto(out *IonosCloudMachineList) { *out = *in @@ -413,6 +522,7 @@ func (in *IonosCloudMachineSpec) DeepCopy() *IonosCloudMachineSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IonosCloudMachineStatus) DeepCopyInto(out *IonosCloudMachineStatus) { *out = *in + in.Initialization.DeepCopyInto(&out.Initialization) if in.MachineNetworkInfo != nil { in, out := &in.MachineNetworkInfo, &out.MachineNetworkInfo *out = new(MachineNetworkInfo) @@ -420,7 +530,7 @@ func (in *IonosCloudMachineStatus) DeepCopyInto(out *IonosCloudMachineStatus) { } if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make(v1beta1.Conditions, len(*in)) + *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -430,6 +540,11 @@ func (in *IonosCloudMachineStatus) DeepCopyInto(out *IonosCloudMachineStatus) { *out = new(ProvisioningRequest) **out = **in } + if in.Deprecated != nil { + in, out := &in.Deprecated, &out.Deprecated + *out = new(IonosCloudMachineDeprecatedStatus) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudMachineStatus. @@ -533,6 +648,28 @@ func (in *IonosCloudMachineTemplateSpec) DeepCopy() *IonosCloudMachineTemplateSp return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IonosCloudMachineV1Beta1DeprecatedStatus) DeepCopyInto(out *IonosCloudMachineV1Beta1DeprecatedStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make(v1beta2.Conditions, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudMachineV1Beta1DeprecatedStatus. +func (in *IonosCloudMachineV1Beta1DeprecatedStatus) DeepCopy() *IonosCloudMachineV1Beta1DeprecatedStatus { + if in == nil { + return nil + } + out := new(IonosCloudMachineV1Beta1DeprecatedStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MachineNetworkInfo) DeepCopyInto(out *MachineNetworkInfo) { *out = *in diff --git a/cmd/main.go b/cmd/main.go index eed412a3..e770a43e 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -21,7 +21,6 @@ package main import ( "context" "flag" - "fmt" "os" "github.com/spf13/pflag" @@ -30,9 +29,9 @@ import ( utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/klog/v2" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" + ipamv1 "sigs.k8s.io/cluster-api/api/ipam/v1beta2" "sigs.k8s.io/cluster-api/controllers/crdmigrator" - ipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1" "sigs.k8s.io/cluster-api/util/flags" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -57,8 +56,8 @@ var ( func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) - utilruntime.Must(apiextensionsv1.AddToScheme(scheme)) + utilruntime.Must(apiextensionsv1.AddToScheme(scheme)) utilruntime.Must(clusterv1.AddToScheme(scheme)) utilruntime.Must(infrav1.AddToScheme(scheme)) utilruntime.Must(ipamv1.AddToScheme(scheme)) @@ -70,11 +69,11 @@ func init() { // +kubebuilder:rbac:groups=authentication.k8s.io,resources=tokenreviews,verbs=create // +kubebuilder:rbac:groups=authorization.k8s.io,resources=subjectaccessreviews,verbs=create -// Add CRD RBAC for CRD Migrator. +// Add RBAC for CRDMigrator controller. // +kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions,verbs=get;list;watch // +kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions;customresourcedefinitions/status,verbs=update;patch,resourceNames=ionoscloudclusters.infrastructure.cluster.x-k8s.io;ionoscloudclustertemplates.infrastructure.cluster.x-k8s.io;ionoscloudmachines.infrastructure.cluster.x-k8s.io;ionoscloudmachinetemplates.infrastructure.cluster.x-k8s.io -// Add CR RBAC for CRD Migrator. +// Add RBAC for template CRs (needed by CRDMigrator for storage version migration). // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=ionoscloudclustertemplates;ionoscloudmachinetemplates,verbs=get;list;watch;patch;update func main() { @@ -113,8 +112,23 @@ func main() { ctx := ctrl.SetupSignalHandler() - if err = setUpControllers(ctx, mgr); err != nil { - setupLog.Error(err, "unable to set up controllers") + if err = iccontroller.NewIonosCloudClusterReconciler(mgr).SetupWithManager( + ctx, + mgr, + controller.Options{MaxConcurrentReconciles: icClusterConcurrency}, + ); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "IonosCloudCluster") + os.Exit(1) + } + if err = iccontroller.NewIonosCloudMachineReconciler(mgr).SetupWithManager( + mgr, + controller.Options{MaxConcurrentReconciles: icMachineConcurrency}, + ); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "IonosCloudMachine") + os.Exit(1) + } + if err := setupCRDMigrator(ctx, mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "CRDMigrator") os.Exit(1) } //+kubebuilder:scaffold:builder @@ -135,39 +149,24 @@ func main() { } } -func setUpControllers(ctx context.Context, mgr ctrl.Manager) error { - skipPhases := make([]crdmigrator.Phase, 0, len(skipCRDMigrationPhases)) +func setupCRDMigrator(ctx context.Context, mgr ctrl.Manager) error { + crdMigratorConfig := map[client.Object]crdmigrator.ByObjectConfig{ + &infrav1.IonosCloudCluster{}: {UseCache: true, UseStatusForStorageVersionMigration: true}, + &infrav1.IonosCloudMachine{}: {UseCache: true, UseStatusForStorageVersionMigration: true}, + &infrav1.IonosCloudClusterTemplate{}: {UseCache: false}, + &infrav1.IonosCloudMachineTemplate{}: {UseCache: false}, + } + crdMigratorSkipPhases := make([]crdmigrator.Phase, 0, len(skipCRDMigrationPhases)) for _, p := range skipCRDMigrationPhases { - skipPhases = append(skipPhases, crdmigrator.Phase(p)) + crdMigratorSkipPhases = append(crdMigratorSkipPhases, crdmigrator.Phase(p)) } - if err := (&crdmigrator.CRDMigrator{ + return (&crdmigrator.CRDMigrator{ Client: mgr.GetClient(), APIReader: mgr.GetAPIReader(), - SkipCRDMigrationPhases: skipPhases, - Config: map[client.Object]crdmigrator.ByObjectConfig{ - &infrav1.IonosCloudCluster{}: {UseCache: true}, - &infrav1.IonosCloudClusterTemplate{}: {UseCache: false}, - &infrav1.IonosCloudMachine{}: {UseCache: true}, - &infrav1.IonosCloudMachineTemplate{}: {UseCache: false}, - }, - }).SetupWithManager(ctx, mgr, controller.Options{}); err != nil { - return fmt.Errorf("unable to create CRDMigrator controller: %w", err) - } - - if err := iccontroller.NewIonosCloudClusterReconciler(mgr).SetupWithManager( - ctx, - mgr, - controller.Options{MaxConcurrentReconciles: icClusterConcurrency}, - ); err != nil { - return fmt.Errorf("unable to create IonosCloudCluster controller: %w", err) - } - if err := iccontroller.NewIonosCloudMachineReconciler(mgr).SetupWithManager( - mgr, - controller.Options{MaxConcurrentReconciles: icMachineConcurrency}, - ); err != nil { - return fmt.Errorf("unable to create IonosCloudMachine controller: %w", err) - } - return nil + SkipCRDMigrationPhases: crdMigratorSkipPhases, + Config: crdMigratorConfig, + // Run with concurrency 1 to avoid overwhelming the apiserver. + }).SetupWithManager(ctx, mgr, controller.Options{MaxConcurrentReconciles: 1}) } // initFlags parses the command line flags. @@ -181,7 +180,7 @@ func initFlags() { "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") pflag.StringArrayVar(&skipCRDMigrationPhases, "skip-crd-migration-phases", []string{}, - "CRD migration phases to skip. Valid values are: StorageVersionMigration, CleanupManagedFields.") + "CRD migration phases to skip. Use when the CAPI CRDMigrator handles migration.") pflag.IntVar(&icClusterConcurrency, "ionoscloudcluster-concurrency", 1, "Number of IonosCloudClusters to process simultaneously") pflag.IntVar(&icMachineConcurrency, "ionoscloudmachine-concurrency", 1, diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml index c0ee9a3c..a19b0ed3 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml @@ -25,7 +25,7 @@ spec: name: Cluster type: string - description: Cluster infrastructure is ready - jsonPath: .status.ready + jsonPath: .status.initialization.provisioned name: Ready type: string - description: API Endpoint @@ -62,18 +62,19 @@ spec: ControlPlaneEndpoint represents the endpoint used to communicate with the control plane. provider-provided block IP into the kube-vip manifest. + minProperties: 1 properties: host: description: host is the hostname on which the API server is serving. maxLength: 512 + minLength: 1 type: string port: description: port is the port on which the API server is serving. format: int32 + maximum: 65535 + minimum: 1 type: integer - required: - - host - - port type: object x-kubernetes-validations: - message: port must be within 1-65535 @@ -132,57 +133,67 @@ spec: description: IonosCloudClusterStatus defines the observed state of IonosCloudCluster. properties: conditions: - description: Conditions defines current service state of the IonosCloudCluster. + description: Conditions represents the observations of the current + state of the IonosCloudCluster. items: - description: Condition defines an observation of a Cluster API resource - operational state. + description: Condition contains details for one aspect of the current + state of this API Resource. properties: lastTransitionTime: description: |- lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when - the API field changed is acceptable. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. format: date-time type: string message: description: |- message is a human readable message indicating details about the transition. - This field may be empty. - maxLength: 10240 - minLength: 1 + This may be an empty string. + maxLength: 32768 type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer reason: description: |- - reason is the reason for the condition's last transition in CamelCase. - The specific API may choose whether or not this field is considered a guaranteed API. - This field may be empty. - maxLength: 256 + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 minLength: 1 - type: string - severity: - description: |- - severity provides an explicit classification of Reason code, so the users or machines can immediately - understand the current situation and act accordingly. - The Severity field MUST be set only when Status=False. - maxLength: 32 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown type: string type: - description: |- - type of condition in CamelCase or in foo.example.com/CamelCase. - Many .condition.type values are consistent across resources like Available, but because arbitrary conditions - can be useful (see .node.status.conditions), the ability to deconflict is important. - maxLength: 256 - minLength: 1 + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime + - message + - reason - status - type type: object + maxItems: 32 type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map controlPlaneEndpointIPBlockID: description: ControlPlaneEndpointIPBlockID is the IONOS Cloud UUID for the control plane endpoint IP block. @@ -238,9 +249,93 @@ spec: description: CurrentRequestByDatacenter maps data center IDs to a pending provisioning request made during reconciliation. type: object - ready: - description: Ready indicates that the cluster is ready. - type: boolean + deprecated: + description: Deprecated groups all status fields deprecated and scheduled + for removal when v1beta1 contract support is dropped. + properties: + v1beta1: + description: V1Beta1 groups all v1beta1 status fields that are + deprecated and scheduled for removal. + properties: + conditions: + description: |- + Conditions defines current service state of the IonosCloudCluster using the deprecated v1beta1 condition type. + + Deprecated: Use the top-level conditions field instead. + items: + description: Condition defines an observation of a Cluster + API resource operational state. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when + the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This field may be empty. + maxLength: 10240 + minLength: 1 + type: string + reason: + description: |- + reason is the reason for the condition's last transition in CamelCase. + The specific API may choose whether or not this field is considered a guaranteed API. + This field may be empty. + maxLength: 256 + minLength: 1 + type: string + severity: + description: |- + severity provides an explicit classification of Reason code, so the users or machines can immediately + understand the current situation and act accordingly. + The Severity field MUST be set only when Status=False. + maxLength: 32 + type: string + status: + description: status of the condition, one of True, False, + Unknown. + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability to deconflict is important. + maxLength: 256 + minLength: 1 + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + ready: + description: |- + Ready indicates that the cluster is ready. + + Deprecated: Use Initialization.Provisioned instead. + type: boolean + type: object + type: object + initialization: + description: |- + Initialization provides observations of the IonosCloudCluster initialization process. + NOTE: Fields in this struct are part of the Cluster API contract and are used to orchestrate initial + cluster provisioning. The value of these fields is never updated after initial provisioning is completed. + Use conditions to monitor the operational state of the cluster's infrastructure. + minProperties: 1 + properties: + provisioned: + description: |- + Provisioned is true when the infrastructure cluster is fully provisioned. + NOTE: this field is part of the Cluster API contract and is used to orchestrate provisioning. + The value of this field is never updated after initial provisioning is completed. + type: boolean + type: object type: object type: object served: true diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclustertemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclustertemplates.yaml index be41b149..107fdf76 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclustertemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclustertemplates.yaml @@ -54,20 +54,21 @@ spec: ControlPlaneEndpoint represents the endpoint used to communicate with the control plane. provider-provided block IP into the kube-vip manifest. + minProperties: 1 properties: host: description: host is the hostname on which the API server is serving. maxLength: 512 + minLength: 1 type: string port: description: port is the port on which the API server is serving. format: int32 + maximum: 65535 + minimum: 1 type: integer - required: - - host - - port type: object x-kubernetes-validations: - message: port must be within 1-65535 diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml index e4901016..fd494980 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml @@ -25,7 +25,7 @@ spec: name: Cluster type: string - description: Machine is ready - jsonPath: .status.ready + jsonPath: .status.initialization.provisioned name: Ready type: string - jsonPath: .status.machineNetworkInfo.nicInfo[*].ipv4Addresses @@ -376,57 +376,67 @@ spec: description: IonosCloudMachineStatus defines the observed state of IonosCloudMachine. properties: conditions: - description: Conditions defines current service state of the IonosCloudMachine. + description: Conditions represents the observations of the current + state of the IonosCloudMachine. items: - description: Condition defines an observation of a Cluster API resource - operational state. + description: Condition contains details for one aspect of the current + state of this API Resource. properties: lastTransitionTime: description: |- lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when - the API field changed is acceptable. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. format: date-time type: string message: description: |- message is a human readable message indicating details about the transition. - This field may be empty. - maxLength: 10240 - minLength: 1 + This may be an empty string. + maxLength: 32768 type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer reason: description: |- - reason is the reason for the condition's last transition in CamelCase. - The specific API may choose whether or not this field is considered a guaranteed API. - This field may be empty. - maxLength: 256 + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 minLength: 1 - type: string - severity: - description: |- - severity provides an explicit classification of Reason code, so the users or machines can immediately - understand the current situation and act accordingly. - The Severity field MUST be set only when Status=False. - maxLength: 32 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown type: string type: - description: |- - type of condition in CamelCase or in foo.example.com/CamelCase. - Many .condition.type values are consistent across resources like Available, but because arbitrary conditions - can be useful (see .node.status.conditions), the ability to deconflict is important. - maxLength: 256 - minLength: 1 + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime + - message + - reason - status - type type: object + maxItems: 32 type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map currentRequest: description: |- CurrentRequest shows the current provisioning request for any @@ -451,6 +461,93 @@ spec: - method - requestPath type: object + deprecated: + description: Deprecated groups all status fields deprecated and scheduled + for removal when v1beta1 contract support is dropped. + properties: + v1beta1: + description: V1Beta1 groups all v1beta1 status fields that are + deprecated and scheduled for removal. + properties: + conditions: + description: |- + Conditions defines current service state of the IonosCloudMachine using the deprecated v1beta1 condition type. + + Deprecated: Use the top-level conditions field instead. + items: + description: Condition defines an observation of a Cluster + API resource operational state. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when + the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This field may be empty. + maxLength: 10240 + minLength: 1 + type: string + reason: + description: |- + reason is the reason for the condition's last transition in CamelCase. + The specific API may choose whether or not this field is considered a guaranteed API. + This field may be empty. + maxLength: 256 + minLength: 1 + type: string + severity: + description: |- + severity provides an explicit classification of Reason code, so the users or machines can immediately + understand the current situation and act accordingly. + The Severity field MUST be set only when Status=False. + maxLength: 32 + type: string + status: + description: status of the condition, one of True, False, + Unknown. + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability to deconflict is important. + maxLength: 256 + minLength: 1 + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + ready: + description: |- + Ready indicates the VM has been provisioned and is ready. + + Deprecated: Use Initialization.Provisioned instead. + type: boolean + type: object + type: object + initialization: + description: |- + Initialization provides observations of the IonosCloudMachine initialization process. + NOTE: Fields in this struct are part of the Cluster API contract and are used to orchestrate initial + machine provisioning. The value of these fields is never updated after initial provisioning is completed. + Use conditions to monitor the operational state of the machine's infrastructure. + minProperties: 1 + properties: + provisioned: + description: |- + Provisioned is true when the machine infrastructure is fully provisioned. + NOTE: this field is part of the Cluster API contract and is used to orchestrate provisioning. + The value of this field is never updated after initial provisioning is completed. + type: boolean + type: object location: description: Location is the location of the datacenter the VM is provisioned in. @@ -497,9 +594,6 @@ spec: type: object type: array type: object - ready: - description: Ready indicates the VM has been provisioned and is ready. - type: boolean type: object type: object served: true diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachinetemplates.yaml index 0652beb4..27e52db6 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachinetemplates.yaml @@ -49,6 +49,7 @@ spec: description: |- Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + minProperties: 1 properties: annotations: additionalProperties: diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 59e9227f..e7cdddd5 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -10,6 +10,7 @@ resources: commonLabels: cluster.x-k8s.io/v1beta1: v1alpha1 #https://cluster-api.sigs.k8s.io/developer/providers/contracts.html#api-version-labels + cluster.x-k8s.io/v1beta2: v1alpha1 patches: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. diff --git a/go.mod b/go.mod index 8ded6514..66cd3969 100644 --- a/go.mod +++ b/go.mod @@ -12,43 +12,39 @@ require ( github.com/onsi/gomega v1.38.0 github.com/spf13/pflag v1.0.7 github.com/stretchr/testify v1.11.1 - k8s.io/api v0.32.3 - k8s.io/apiextensions-apiserver v0.32.3 - k8s.io/apimachinery v0.32.3 - k8s.io/client-go v0.32.3 + k8s.io/api v0.33.3 + k8s.io/apiextensions-apiserver v0.33.3 + k8s.io/apimachinery v0.33.3 + k8s.io/client-go v0.33.3 k8s.io/klog/v2 v2.130.1 - sigs.k8s.io/cluster-api v1.10.10 - sigs.k8s.io/cluster-api/test v1.10.10 - sigs.k8s.io/controller-runtime v0.20.4 + sigs.k8s.io/cluster-api v1.11.11 + sigs.k8s.io/cluster-api/test v1.11.11 + sigs.k8s.io/controller-runtime v0.21.0 ) require ( al.essio.dev/pkg/shellescape v1.5.1 // indirect cel.dev/expr v0.25.1 // indirect - dario.cat/mergo v1.0.1 // indirect github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect - github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.3.0 // indirect - github.com/Masterminds/sprig/v3 v3.3.0 // indirect - github.com/Microsoft/go-winio v0.5.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect github.com/adrg/xdg v0.5.3 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect - github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cloudflare/circl v1.6.1 // indirect + github.com/cloudflare/circl v1.6.3 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/docker/docker v28.0.2+incompatible // indirect - github.com/docker/go-connections v0.5.0 // indirect - github.com/docker/go-units v0.4.0 // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46 // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -63,93 +59,90 @@ require ( github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/gobuffalo/flect v1.0.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/cel-go v0.22.0 // indirect - github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/cel-go v0.23.2 // indirect + github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-github/v53 v53.2.0 // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect - github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect - github.com/huandu/xstrings v1.5.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect - github.com/mitchellh/copystructure v1.2.0 // indirect - github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/moby/api v1.54.1 // indirect + github.com/moby/moby/client v0.4.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.19.1 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/rivo/uniseg v0.4.2 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect - github.com/shopspring/decimal v1.4.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.12.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/spf13/cobra v1.9.1 // indirect - github.com/spf13/viper v1.20.0 // indirect + github.com/spf13/viper v1.20.1 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/valyala/fastjson v1.6.4 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect go.opentelemetry.io/otel v1.43.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect go.opentelemetry.io/otel/sdk v1.43.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect - go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.46.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect - golang.org/x/net v0.48.0 // indirect + golang.org/x/net v0.49.0 // indirect golang.org/x/oauth2 v0.34.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/term v0.38.0 // indirect - golang.org/x/text v0.32.0 // indirect - golang.org/x/time v0.10.0 // indirect - golang.org/x/tools v0.39.0 // indirect + golang.org/x/term v0.39.0 // indirect + golang.org/x/text v0.33.0 // indirect + golang.org/x/time v0.11.0 // indirect + golang.org/x/tools v0.40.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/grpc v1.79.3 // indirect - google.golang.org/protobuf v1.36.10 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiserver v0.32.3 // indirect - k8s.io/cluster-bootstrap v0.32.3 // indirect - k8s.io/component-base v0.32.3 // indirect - k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect + k8s.io/apiserver v0.33.3 // indirect + k8s.io/cluster-bootstrap v0.33.3 // indirect + k8s.io/component-base v0.33.3 // indirect + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect - sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect - sigs.k8s.io/kind v0.27.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect + sigs.k8s.io/kind v0.30.0 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index c7a9c566..2e486b2b 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,6 @@ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= @@ -16,8 +14,8 @@ github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+ github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= -github.com/Microsoft/go-winio v0.5.0 h1:Elr9Wn+sGKPlkaBvwu4mTrxtmOp3F3yV9qhaHbXGjwU= -github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA= @@ -26,31 +24,26 @@ github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= -github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= -github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= -github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= -github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/coredns/caddy v1.1.1 h1:2eYKZT7i6yxIfGP3qLJoJ7HAsDJqYB+X68g4NYjSrE0= github.com/coredns/caddy v1.1.1/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4= -github.com/coredns/corefile-migration v1.0.29 h1:g4cPYMXXDDs9uLE2gFYrJaPBuUAR07eEMGyh9JBE13w= -github.com/coredns/corefile-migration v1.0.29/go.mod h1:56DPqONc3njpVPsdilEnfijCwNGC3/kTJLl7i7SPavY= -github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= -github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= -github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coredns/corefile-migration v1.0.30 h1:ljZNPGgna+4yKv81gfkvkgLEWdtz0NjBR1glaiPI140= +github.com/coredns/corefile-migration v1.0.30/go.mod h1:56DPqONc3njpVPsdilEnfijCwNGC3/kTJLl7i7SPavY= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -59,18 +52,14 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v28.0.2+incompatible h1:9BILleFwug5FSSqWBgVevgL3ewDJfWWWyZVqlDMttE8= -github.com/docker/docker v28.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= -github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46 h1:7QPwrLT79GlD5sizHf27aoY2RTvw62mO6x7mxkScNk0= github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46/go.mod h1:esf2rsHFNlZlxsqsZDojNBcnNs5REqIvRrWRHqX0vEU= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= @@ -108,16 +97,14 @@ github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4 github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= -github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.22.0 h1:b3FJZxpiv1vTMo2/5RDUqAHPxkT8mmMfJIrq1llbf7g= -github.com/google/cel-go v0.22.0/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8= -github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= -github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/cel-go v0.23.2 h1:UdEe3CvQh3Nv+E/j9r1Y//WO0K0cSyD7/y0bzyLIMI4= +github.com/google/cel-go v0.23.2/go.mod h1:52Pb6QsDbC5kvgxvZhiL9QX1oZEkcUF/ZqaPx1J5Wwo= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -132,22 +119,12 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= -github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 h1:SJ+NtwL6QaZ21U+IrK7d0gGgpjGGvd2kz+FzTHVzdqI= -github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2/go.mod h1:Tv1PlzqC9t8wNnpPdctvtSUOPUUg4SHeE6vR1Ir2hmg= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= -github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -156,14 +133,14 @@ github.com/ionos-cloud/sdk-go/v6 v6.3.4 h1:jTvGl4LOF8v8OYoEIBNVwbFoqSGAFqn6vGE7s github.com/ionos-cloud/sdk-go/v6 v6.3.4/go.mod h1:wCVwNJ/21W29FWFUv+fNawOTMlFoP1dS3L+ZuztFW48= github.com/jarcoal/httpmock v1.4.0 h1:BvhqnH0JAYbNudL2GMJKgOHe2CtKlzJ/5rWKyp+hc2k= github.com/jarcoal/httpmock v1.4.0/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0= -github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= -github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -171,6 +148,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -189,15 +168,15 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4= +github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw= +github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= @@ -208,8 +187,8 @@ github.com/onsi/gomega v1.38.0 h1:c/WX+w8SLAinvuKKQFh77WEucCnPk4j2OTUr7lt7BeY= github.com/onsi/gomega v1.38.0/go.mod h1:OcXcwId0b9QsE7Y49u+BTrL4IdKOBOKnD6VQNTJEB6o= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= -github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= @@ -221,12 +200,12 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= -github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= -github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= -github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -239,11 +218,6 @@ github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsF github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= -github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= @@ -255,8 +229,8 @@ github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wx github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY= -github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= +github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -264,7 +238,6 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -273,46 +246,20 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= -github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= -github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= -github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk= -github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= -go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= -go.etcd.io/etcd/api/v3 v3.5.20 h1:aKfz3nPZECWoZJXMSH9y6h2adXjtOHaHTGEVCuCmaz0= -go.etcd.io/etcd/api/v3 v3.5.20/go.mod h1:QqKGViq4KTgOG43dr/uH0vmGWIaoJY3ggFi6ZH0TH/U= -go.etcd.io/etcd/client/pkg/v3 v3.5.20 h1:sZIAtra+xCo56gdf6BR62to/hiie5Bwl7hQIqMzVTEM= -go.etcd.io/etcd/client/pkg/v3 v3.5.20/go.mod h1:qaOi1k4ZA9lVLejXNvyPABrVEe7VymMF2433yyRQ7O0= -go.etcd.io/etcd/client/v2 v2.305.16 h1:kQrn9o5czVNaukf2A2At43cE9ZtWauOtf9vRZuiKXow= -go.etcd.io/etcd/client/v2 v2.305.16/go.mod h1:h9YxWCzcdvZENbfzBTFCnoNumr2ax3F19sKMqHFmXHE= -go.etcd.io/etcd/client/v3 v3.5.20 h1:jMT2MwQEhyvhQg49Cec+1ZHJzfUf6ZgcmV0GjPv0tIQ= -go.etcd.io/etcd/client/v3 v3.5.20/go.mod h1:J5lbzYRMUR20YolS5UjlqqMcu3/wdEvG5VNBhzyo3m0= -go.etcd.io/etcd/pkg/v3 v3.5.16 h1:cnavs5WSPWeK4TYwPYfmcr3Joz9BH+TZ6qoUtz6/+mc= -go.etcd.io/etcd/pkg/v3 v3.5.16/go.mod h1:+lutCZHG5MBBFI/U4eYT5yL7sJfnexsoM20Y0t2uNuY= -go.etcd.io/etcd/raft/v3 v3.5.16 h1:zBXA3ZUpYs1AwiLGPafYAKKl/CORn/uaxYDwlNwndAk= -go.etcd.io/etcd/raft/v3 v3.5.16/go.mod h1:P4UP14AxofMJ/54boWilabqqWoW9eLodl6I5GdGzazI= -go.etcd.io/etcd/server/v3 v3.5.16 h1:d0/SAdJ3vVsZvF8IFVb1k8zqMZ+heGcNfft71ul9GWE= -go.etcd.io/etcd/server/v3 v3.5.16/go.mod h1:ynhyZZpdDp1Gq49jkUg5mfkDWZwXnn3eIqCqtJnrD/s= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCyI9jVEfqhUh2MoSkmolPjfh5fp2hnV0b0irxH4Q= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= @@ -321,8 +268,8 @@ go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfC go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= -go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= -go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -331,12 +278,16 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -346,8 +297,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -357,10 +308,8 @@ golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -368,20 +317,20 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= -golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= -golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -390,16 +339,14 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= -google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -407,46 +354,50 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= -gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= -gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= -k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= -k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= -k8s.io/apiextensions-apiserver v0.32.3 h1:4D8vy+9GWerlErCwVIbcQjsWunF9SUGNu7O7hiQTyPY= -k8s.io/apiextensions-apiserver v0.32.3/go.mod h1:8YwcvVRMVzw0r1Stc7XfGAzB/SIVLunqApySV5V7Dss= -k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= -k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= -k8s.io/apiserver v0.32.3 h1:kOw2KBuHOA+wetX1MkmrxgBr648ksz653j26ESuWNY8= -k8s.io/apiserver v0.32.3/go.mod h1:q1x9B8E/WzShF49wh3ADOh6muSfpmFL0I2t+TG0Zdgc= -k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= -k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= -k8s.io/cluster-bootstrap v0.32.3 h1:AqIpsUhB6MUeaAsl1WvaUw54AHRd2hfZrESlKChtd8s= -k8s.io/cluster-bootstrap v0.32.3/go.mod h1:CHbBwgOb6liDV6JFUTkx5t85T2xidy0sChBDoyYw344= -k8s.io/component-base v0.32.3 h1:98WJvvMs3QZ2LYHBzvltFSeJjEx7t5+8s71P7M74u8k= -k8s.io/component-base v0.32.3/go.mod h1:LWi9cR+yPAv7cu2X9rZanTiFKB2kHA+JjmhkKjCZRpI= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +k8s.io/api v0.33.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8= +k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE= +k8s.io/apiextensions-apiserver v0.33.3 h1:qmOcAHN6DjfD0v9kxL5udB27SRP6SG/MTopmge3MwEs= +k8s.io/apiextensions-apiserver v0.33.3/go.mod h1:oROuctgo27mUsyp9+Obahos6CWcMISSAPzQ77CAQGz8= +k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA= +k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/apiserver v0.33.3 h1:Wv0hGc+QFdMJB4ZSiHrCgN3zL3QRatu56+rpccKC3J4= +k8s.io/apiserver v0.33.3/go.mod h1:05632ifFEe6TxwjdAIrwINHWE2hLwyADFk5mBsQa15E= +k8s.io/client-go v0.33.3 h1:M5AfDnKfYmVJif92ngN532gFqakcGi6RvaOF16efrpA= +k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg= +k8s.io/cluster-bootstrap v0.33.3 h1:u2NTxJ5CFSBFXaDxLQoOWMly8eni31psVso+caq6uwI= +k8s.io/cluster-bootstrap v0.33.3/go.mod h1:p970f8u8jf273zyQ5raD8WUu2XyAl0SAWOY82o7i/ds= +k8s.io/component-base v0.33.3 h1:mlAuyJqyPlKZM7FyaoM/LcunZaaY353RXiOd2+B5tGA= +k8s.io/component-base v0.33.3/go.mod h1:ktBVsBzkI3imDuxYXmVxZ2zxJnYTZ4HAsVj9iF09qp4= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= -k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 h1:CPT0ExVicCzcpeN4baWEV2ko2Z/AsiZgEdwgcfwLgMo= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= -sigs.k8s.io/cluster-api v1.10.10 h1:GF9B04puY1IrK9l8nc7xZaDITvKEkPtFAzccfgN4Gp0= -sigs.k8s.io/cluster-api v1.10.10/go.mod h1:IHnO3HR8ta0yK1S2SyI2qKpcrZNsyOdArwRjUfnJwnc= -sigs.k8s.io/cluster-api/test v1.10.10 h1:himbsoB2Yu1bY7il/qSwXzAVN2zWHGef+nxJw4eqqQI= -sigs.k8s.io/cluster-api/test v1.10.10/go.mod h1:ehnxLYJn73nn/0NM0x6dJgdYa04ZWwqn7T5xcvwRwYg= -sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= -sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= +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/cluster-api v1.11.11 h1:MvUjPlUYk72ow4HhI9lJlt0C1iY8JsyPtx5YFV8pLJA= +sigs.k8s.io/cluster-api v1.11.11/go.mod h1:wplBEE7AErSt8bN93qrqXeXQ8uEq69RfJBPPYoHWLAE= +sigs.k8s.io/cluster-api/test v1.11.11 h1:KZXBo1j9zfnysVemyX1N61K3S1PqqSvrlhJVQVNDWMk= +sigs.k8s.io/cluster-api/test v1.11.11/go.mod h1:UeTeX/FwCHTBKFylV8640zgjIDBZ3Z+c8jcDB+BfrTM= +sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= +sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= -sigs.k8s.io/kind v0.27.0 h1:PQ3f0iAWNIj66LYkZ1ivhEg/+Zb6UPMbO+qVei/INZA= -sigs.k8s.io/kind v0.27.0/go.mod h1:RZVFmy6qcwlSWwp6xeIUv7kXCPF3i8MXsEXxW/J+gJY= -sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= -sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/kind v0.30.0 h1:2Xi1KFEfSMm0XDcvKnUt15ZfgRPCT0OnCBbpgh8DztY= +sigs.k8s.io/kind v0.30.0/go.mod h1:FSqriGaoTPruiXWfRnUXNykF8r2t+fHtK0P0m1AbGF8= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/controller/ionoscloudcluster_controller.go b/internal/controller/ionoscloudcluster_controller.go index 449369f9..25e09a89 100644 --- a/internal/controller/ionoscloudcluster_controller.go +++ b/internal/controller/ionoscloudcluster_controller.go @@ -25,11 +25,12 @@ import ( "fmt" apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" "sigs.k8s.io/cluster-api/util" "sigs.k8s.io/cluster-api/util/annotations" - "sigs.k8s.io/cluster-api/util/conditions" + conditions "sigs.k8s.io/cluster-api/util/conditions" "sigs.k8s.io/cluster-api/util/predicates" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" @@ -42,6 +43,7 @@ import ( infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/service/cloud" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/locker" + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/scope" ) @@ -172,8 +174,20 @@ func (r *IonosCloudClusterReconciler) reconcileNormal( } } - conditions.MarkTrue(clusterScope.IonosCluster, infrav1.IonosCloudClusterReady) - clusterScope.IonosCluster.Status.Ready = true + conditions.Set(clusterScope.IonosCluster, metav1.Condition{ + Type: string(infrav1.IonosCloudClusterReady), + Status: metav1.ConditionTrue, + Reason: string(infrav1.IonosCloudClusterReady), + }) + clusterScope.IonosCluster.Status.Initialization.Provisioned = ptr.To(true) + // Set deprecated v1beta1 ready field for backwards compatibility. + if clusterScope.IonosCluster.Status.Deprecated == nil { + clusterScope.IonosCluster.Status.Deprecated = &infrav1.IonosCloudClusterDeprecatedStatus{} + } + if clusterScope.IonosCluster.Status.Deprecated.V1Beta1 == nil { + clusterScope.IonosCluster.Status.Deprecated.V1Beta1 = &infrav1.IonosCloudClusterV1Beta1DeprecatedStatus{} + } + clusterScope.IonosCluster.Status.Deprecated.V1Beta1.Ready = true //nolint:staticcheck // Intentionally setting deprecated field for v1beta1 backwards compatibility. return ctrl.Result{}, nil } @@ -261,7 +275,7 @@ func (r *IonosCloudClusterReconciler) SetupWithManager( return ctrl.NewControllerManagedBy(mgr). WithOptions(options). For(&infrav1.IonosCloudCluster{}). - WithEventFilter(predicates.ResourceNotPaused(mgr.GetScheme(), ctrl.LoggerFrom(ctx))). + WithEventFilter(predicates.ResourceNotPaused(r.scheme, ctrl.LoggerFrom(ctx))). Watches(&clusterv1.Cluster{}, handler.EnqueueRequestsFromMapFunc( util.ClusterToInfrastructureMapFunc( @@ -270,7 +284,7 @@ func (r *IonosCloudClusterReconciler) SetupWithManager( r.Client, &infrav1.IonosCloudCluster{}, ), ), - builder.WithPredicates(predicates.ClusterUnpaused(mgr.GetScheme(), ctrl.LoggerFrom(ctx))), + builder.WithPredicates(predicates.ClusterUnpaused(r.scheme, ctrl.LoggerFrom(ctx))), ). Complete(reconcile.AsReconciler[*infrav1.IonosCloudCluster](r.Client, r)) } diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index e6fa98a2..f8ca9160 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -22,11 +22,12 @@ import ( "fmt" apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" "sigs.k8s.io/cluster-api/util" "sigs.k8s.io/cluster-api/util/annotations" - "sigs.k8s.io/cluster-api/util/conditions" + conditions "sigs.k8s.io/cluster-api/util/conditions" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" @@ -299,13 +300,13 @@ func (*IonosCloudMachineReconciler) checkRequestStates( func (*IonosCloudMachineReconciler) isInfrastructureReady(ctx context.Context, ms *scope.Machine) bool { log := ctrl.LoggerFrom(ctx) // Make sure the infrastructure is ready. - if !ms.ClusterScope.Cluster.Status.InfrastructureReady { + if ms.ClusterScope.Cluster.Status.Initialization.InfrastructureProvisioned == nil || !*ms.ClusterScope.Cluster.Status.Initialization.InfrastructureProvisioned { log.Info("Cluster infrastructure is not ready yet") - conditions.MarkFalse( - ms.IonosMachine, - infrav1.MachineProvisionedCondition, - infrav1.WaitingForClusterInfrastructureReason, - clusterv1.ConditionSeverityInfo, "") + conditions.Set(ms.IonosMachine, metav1.Condition{ + Type: string(infrav1.MachineProvisionedCondition), + Status: metav1.ConditionFalse, + Reason: infrav1.WaitingForClusterInfrastructureReason, + }) return false } @@ -313,12 +314,11 @@ func (*IonosCloudMachineReconciler) isInfrastructureReady(ctx context.Context, m // Make sure to wait until the data secret was created if ms.Machine.Spec.Bootstrap.DataSecretName == nil { log.Info("Bootstrap data secret is not available yet") - conditions.MarkFalse( - ms.IonosMachine, - infrav1.MachineProvisionedCondition, - infrav1.WaitingForBootstrapDataReason, - clusterv1.ConditionSeverityInfo, "", - ) + conditions.Set(ms.IonosMachine, metav1.Condition{ + Type: string(infrav1.MachineProvisionedCondition), + Status: metav1.ConditionFalse, + Reason: infrav1.WaitingForBootstrapDataReason, + }) return false } diff --git a/internal/service/cloud/image.go b/internal/service/cloud/image.go index 04ba59b8..686f04d6 100644 --- a/internal/service/cloud/image.go +++ b/internal/service/cloud/image.go @@ -63,7 +63,7 @@ func (s *Service) lookupImageID(ctx context.Context, ms *scope.Machine) (string, } if ptr.Deref(imageSpec.Selector.UseMachineVersion, true) { - version := ptr.Deref(ms.Machine.Spec.Version, "") + version := ms.Machine.Spec.Version if version == "" { return "", errMissingMachineVersion } diff --git a/internal/service/cloud/image_test.go b/internal/service/cloud/image_test.go index 267e5aeb..2d85ab5f 100644 --- a/internal/service/cloud/image_test.go +++ b/internal/service/cloud/image_test.go @@ -99,7 +99,7 @@ func (s *imageTestSuite) TestLookupImageMissingMachineVersion() { s.ionosClient.EXPECT().GetImage(s.ctx, "image-1").Return(s.makeTestImage("image-1", "test", "loc"), nil).Once() s.ionosClient.EXPECT().GetDatacenterLocationByID(s.ctx, s.infraMachine.Spec.DatacenterID).Return("loc", nil).Once() - s.capiMachine.Spec.Version = ptr.To("") + s.capiMachine.Spec.Version = "" _, err := s.service.lookupImageID(s.ctx, s.machineScope) s.ErrorIs(err, errMissingMachineVersion) @@ -115,7 +115,7 @@ func (s *imageTestSuite) TestLookupImageIgnoreMissingMachineVersion() { s.ionosClient.EXPECT().GetDatacenterLocationByID(s.ctx, s.infraMachine.Spec.DatacenterID).Return("loc", nil).Once() s.infraMachine.Spec.Disk.Image.Selector.UseMachineVersion = ptr.To(false) - s.capiMachine.Spec.Version = ptr.To("") + s.capiMachine.Spec.Version = "" imageID, err := s.service.lookupImageID(s.ctx, s.machineScope) s.NoError(err) @@ -158,7 +158,7 @@ func (s *imageTestSuite) TestLookupImageNewestOK() { } func (s *imageTestSuite) makeTestImage(id, namePrefix, location string) *sdk.Image { - return makeTestImage(id, namePrefix+*s.capiMachine.Spec.Version, location) + return makeTestImage(id, namePrefix+s.capiMachine.Spec.Version, location) } func (s *imageTestSuite) makeTestImageWithDate(id, namePrefix, location string, createdDate time.Time) *sdk.Image { diff --git a/internal/service/cloud/ipblock.go b/internal/service/cloud/ipblock.go index a8df3e7e..6329cd87 100644 --- a/internal/service/cloud/ipblock.go +++ b/internal/service/cloud/ipblock.go @@ -27,7 +27,7 @@ import ( "github.com/go-logr/logr" sdk "github.com/ionos-cloud/sdk-go/v6" "k8s.io/apimachinery/pkg/util/sets" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" diff --git a/internal/service/cloud/ipblock_test.go b/internal/service/cloud/ipblock_test.go index 28dbcce4..9f9645b8 100644 --- a/internal/service/cloud/ipblock_test.go +++ b/internal/service/cloud/ipblock_test.go @@ -22,7 +22,7 @@ import ( sdk "github.com/ionos-cloud/sdk-go/v6" "github.com/stretchr/testify/suite" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud/clienttest" diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index a4af1100..e8b82af6 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -25,8 +25,7 @@ import ( "slices" sdk "github.com/ionos-cloud/sdk-go/v6" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - "sigs.k8s.io/cluster-api/util" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" "sigs.k8s.io/controller-runtime/pkg/client" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" @@ -308,7 +307,7 @@ func (s *Service) retrieveFailoverIPForMachine( ) (requeue bool, failoverIP string, err error) { log := s.logger.WithName("retrieveFailoverIPForMachine") - if util.IsControlPlaneMachine(ms.Machine) { + if isControlPlaneMachine(ms.Machine) { ip, err := ms.ClusterScope.GetControlPlaneEndpointIP(ctx) return false, ip, err } @@ -380,7 +379,7 @@ func (s *Service) ReconcileIPFailoverDeletion( matchLabels := client.MatchingLabels{} - if util.IsControlPlaneMachine(ms.Machine) { + if isControlPlaneMachine(ms.Machine) { matchLabels[clusterv1.MachineControlPlaneLabel] = "" } else { matchLabels[clusterv1.MachineDeploymentNameLabel] = ms.IonosMachine.Labels[clusterv1.MachineDeploymentNameLabel] @@ -657,5 +656,10 @@ func (s *Service) patchLAN(ctx context.Context, ms *scope.Machine, lanID string, } func failoverRequired(ms *scope.Machine) bool { - return util.IsControlPlaneMachine(ms.Machine) || ms.IonosMachine.Spec.FailoverIP != nil + return isControlPlaneMachine(ms.Machine) || ms.IonosMachine.Spec.FailoverIP != nil +} + +func isControlPlaneMachine(machine *clusterv1.Machine) bool { + _, ok := machine.Labels[clusterv1.MachineControlPlaneLabel] + return ok } diff --git a/internal/service/cloud/network_test.go b/internal/service/cloud/network_test.go index 4425e7c8..61b590e4 100644 --- a/internal/service/cloud/network_test.go +++ b/internal/service/cloud/network_test.go @@ -26,7 +26,7 @@ import ( sdk "github.com/ionos-cloud/sdk-go/v6" "github.com/stretchr/testify/suite" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" "sigs.k8s.io/controller-runtime/pkg/client" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" diff --git a/internal/service/cloud/server.go b/internal/service/cloud/server.go index 7bbda6c5..24335abb 100644 --- a/internal/service/cloud/server.go +++ b/internal/service/cloud/server.go @@ -29,7 +29,8 @@ import ( sdk "github.com/ionos-cloud/sdk-go/v6" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" - "sigs.k8s.io/cluster-api/util/conditions" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + conditions "sigs.k8s.io/cluster-api/util/conditions" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" @@ -144,8 +145,20 @@ func (s *Service) ReconcileServerDeletion(ctx context.Context, ms *scope.Machine // FinalizeMachineProvisioning marks the machine as provisioned. func (*Service) FinalizeMachineProvisioning(_ context.Context, ms *scope.Machine) (bool, error) { - ms.IonosMachine.Status.Ready = true - conditions.MarkTrue(ms.IonosMachine, infrav1.MachineProvisionedCondition) + ms.IonosMachine.Status.Initialization.Provisioned = ptr.To(true) + // Set deprecated v1beta1 ready field for backwards compatibility. + if ms.IonosMachine.Status.Deprecated == nil { + ms.IonosMachine.Status.Deprecated = &infrav1.IonosCloudMachineDeprecatedStatus{} + } + if ms.IonosMachine.Status.Deprecated.V1Beta1 == nil { + ms.IonosMachine.Status.Deprecated.V1Beta1 = &infrav1.IonosCloudMachineV1Beta1DeprecatedStatus{} + } + ms.IonosMachine.Status.Deprecated.V1Beta1.Ready = true //nolint:staticcheck // Intentionally setting deprecated field for v1beta1 backwards compatibility. + conditions.Set(ms.IonosMachine, metav1.Condition{ + Type: string(infrav1.MachineProvisionedCondition), + Status: metav1.ConditionTrue, + Reason: string(infrav1.MachineProvisionedCondition), + }) return false, nil } diff --git a/internal/service/cloud/suite_test.go b/internal/service/cloud/suite_test.go index c6463d9b..4ab44891 100644 --- a/internal/service/cloud/suite_test.go +++ b/internal/service/cloud/suite_test.go @@ -31,8 +31,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - ipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" + ipamv1 "sigs.k8s.io/cluster-api/api/ipam/v1beta2" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" @@ -138,8 +138,8 @@ func (s *ServiceTestSuite) SetupTest() { }, Spec: clusterv1.MachineSpec{ ClusterName: s.capiCluster.Name, - Version: ptr.To("v1.26.12"), - ProviderID: ptr.To("ionos://" + exampleServerID), + Version: "v1.26.12", + ProviderID: "ionos://" + exampleServerID, }, } s.infraMachine = &infrav1.IonosCloudMachine{ diff --git a/internal/service/k8s/ipam.go b/internal/service/k8s/ipam.go index e0b6207f..bceb8fa3 100644 --- a/internal/service/k8s/ipam.go +++ b/internal/service/k8s/ipam.go @@ -27,8 +27,8 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - ipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" + ipamv1 "sigs.k8s.io/cluster-api/api/ipam/v1beta2" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -268,6 +268,14 @@ func (h *Helper) CreateIPAddressClaim(ctx context.Context, owner client.Object, return nil } + ipPoolRef := ipamv1.IPPoolReference{ + Name: poolRef.Name, + Kind: poolRef.Kind, + } + if poolRef.APIGroup != nil { + ipPoolRef.APIGroup = *poolRef.APIGroup + } + desired := &ipamv1.IPAddressClaim{ ObjectMeta: metav1.ObjectMeta{ Name: claimRef.Name, @@ -275,7 +283,7 @@ func (h *Helper) CreateIPAddressClaim(ctx context.Context, owner client.Object, Labels: map[string]string{clusterv1.ClusterNameLabel: cluster}, }, Spec: ipamv1.IPAddressClaimSpec{ - PoolRef: *poolRef, + PoolRef: ipPoolRef, }, } _, err = controllerutil.CreateOrUpdate(ctx, h.client, desired, func() error { diff --git a/internal/service/k8s/ipam_test.go b/internal/service/k8s/ipam_test.go index 68e0506f..615c7f6c 100644 --- a/internal/service/k8s/ipam_test.go +++ b/internal/service/k8s/ipam_test.go @@ -27,8 +27,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - ipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" + ipamv1 "sigs.k8s.io/cluster-api/api/ipam/v1beta2" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" @@ -98,8 +98,8 @@ func (s *IpamTestSuite) SetupTest() { }, Spec: clusterv1.MachineSpec{ ClusterName: s.capiCluster.Name, - Version: ptr.To("v1.26.12"), - ProviderID: ptr.To("ionos://dd426c63-cd1d-4c02-aca3-13b4a27c2ebf"), + Version: "v1.26.12", + ProviderID: "ionos://dd426c63-cd1d-4c02-aca3-13b4a27c2ebf", }, } s.infraMachine = &infrav1.IonosCloudMachine{ @@ -320,10 +320,10 @@ func defaultIPv4Address(claim *ipamv1.IPAddressClaim, poolRef *corev1.TypedLocal Namespace: "default", }, Spec: ipamv1.IPAddressSpec{ - ClaimRef: *localRef(claim), - PoolRef: *poolRef, + ClaimRef: ipamv1.IPAddressClaimReference{Name: claim.GetName()}, + PoolRef: toIPPoolReference(poolRef), Address: "10.0.0.2", - Prefix: 16, + Prefix: ptr.To[int32](16), }, } } @@ -336,10 +336,10 @@ func defaultIPv6Address(claim *ipamv1.IPAddressClaim, poolRef *corev1.TypedLocal Namespace: "default", }, Spec: ipamv1.IPAddressSpec{ - ClaimRef: *localRef(claim), - PoolRef: *poolRef, + ClaimRef: ipamv1.IPAddressClaimReference{Name: claim.GetName()}, + PoolRef: toIPPoolReference(poolRef), Address: "2001:db8::", - Prefix: 42, + Prefix: ptr.To[int32](42), }, } } @@ -398,8 +398,13 @@ func defaultPrimaryIPv6Claim() *ipamv1.IPAddressClaim { } } -func localRef(obj client.Object) *corev1.LocalObjectReference { - return &corev1.LocalObjectReference{ - Name: obj.GetName(), +func toIPPoolReference(ref *corev1.TypedLocalObjectReference) ipamv1.IPPoolReference { + r := ipamv1.IPPoolReference{ + Name: ref.Name, + Kind: ref.Kind, } + if ref.APIGroup != nil { + r.APIGroup = *ref.APIGroup + } + return r } diff --git a/scope/cluster.go b/scope/cluster.go index bffb01d4..50814b15 100644 --- a/scope/cluster.go +++ b/scope/cluster.go @@ -27,8 +27,8 @@ import ( "time" "k8s.io/client-go/util/retry" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - "sigs.k8s.io/cluster-api/util/conditions" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" + conditions "sigs.k8s.io/cluster-api/util/conditions" "sigs.k8s.io/cluster-api/util/patch" "sigs.k8s.io/controller-runtime/pkg/client" @@ -202,9 +202,15 @@ func (c *Cluster) DeleteCurrentRequestByDatacenter(datacenterID string) { // PatchObject will apply all changes from the IonosCloudCluster. // It will also make sure to patch the status subresource. func (c *Cluster) PatchObject() error { - // always set the ready condition - conditions.SetSummary(c.IonosCluster, - conditions.WithConditions(infrav1.IonosCloudClusterReady)) + // always set the ready condition summary + if err := conditions.SetSummaryCondition( + c.IonosCluster, + c.IonosCluster, + string(clusterv1.ReadyCondition), + conditions.ForConditionTypes{string(infrav1.IonosCloudClusterReady)}, + ); err != nil { + return err + } // NOTE(piepmatz): We don't accept and forward a context here. This is on purpose: Even if a reconciliation is // aborted, we want to make sure that the final patch is applied. Reusing the context from the reconciliation @@ -212,11 +218,17 @@ func (c *Cluster) PatchObject() error { timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() - return c.patchHelper.Patch(timeoutCtx, c.IonosCluster, patch.WithOwnedConditions{ - Conditions: []clusterv1.ConditionType{ - clusterv1.ReadyCondition, + return c.patchHelper.Patch(timeoutCtx, c.IonosCluster, + patch.WithOwnedV1Beta1Conditions{ + Conditions: []clusterv1.ConditionType{clusterv1.ReadyCondition}, + }, + patch.WithOwnedConditions{ + Conditions: []string{ + string(clusterv1.ReadyCondition), + string(infrav1.IonosCloudClusterReady), + }, }, - }) + ) } // Finalize will make sure to apply a patch to the current IonosCloudCluster. diff --git a/scope/cluster_test.go b/scope/cluster_test.go index ad54485f..866facb2 100644 --- a/scope/cluster_test.go +++ b/scope/cluster_test.go @@ -29,7 +29,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/sets" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" diff --git a/scope/machine.go b/scope/machine.go index 0b6e2cd2..17398005 100644 --- a/scope/machine.go +++ b/scope/machine.go @@ -25,8 +25,8 @@ import ( "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" "k8s.io/client-go/util/retry" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - "sigs.k8s.io/cluster-api/util/conditions" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" + conditions "sigs.k8s.io/cluster-api/util/conditions" "sigs.k8s.io/cluster-api/util/patch" "sigs.k8s.io/controller-runtime/pkg/client" @@ -171,9 +171,14 @@ func (m *Machine) FindLatestMachine( // PatchObject will apply all changes from the IonosMachine. // It will also make sure to patch the status subresource. func (m *Machine) PatchObject() error { - conditions.SetSummary(m.IonosMachine, - conditions.WithConditions( - infrav1.MachineProvisionedCondition)) + if err := conditions.SetSummaryCondition( + m.IonosMachine, + m.IonosMachine, + string(clusterv1.ReadyCondition), + conditions.ForConditionTypes{string(infrav1.MachineProvisionedCondition)}, + ); err != nil { + return err + } timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() @@ -184,10 +189,15 @@ func (m *Machine) PatchObject() error { return m.patchHelper.Patch( timeoutCtx, m.IonosMachine, - patch.WithOwnedConditions{Conditions: []clusterv1.ConditionType{ + patch.WithOwnedV1Beta1Conditions{Conditions: []clusterv1.ConditionType{ clusterv1.ReadyCondition, infrav1.MachineProvisionedCondition, - }}) + }}, + patch.WithOwnedConditions{Conditions: []string{ + string(clusterv1.ReadyCondition), + string(infrav1.MachineProvisionedCondition), + }}, + ) } // Finalize will make sure to apply a patch to the current IonosCloudMachine. diff --git a/scope/machine_test.go b/scope/machine_test.go index ee63b01e..952a9804 100644 --- a/scope/machine_test.go +++ b/scope/machine_test.go @@ -23,7 +23,7 @@ import ( "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/scheme" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" diff --git a/test/e2e/capic_test.go b/test/e2e/capic_test.go index 4e249137..bd64f0eb 100644 --- a/test/e2e/capic_test.go +++ b/test/e2e/capic_test.go @@ -79,7 +79,11 @@ var _ = Describe("Quickstart: Should be able to create a cluster with 3 control- // This check ensures that the resourceVersions are stable, i.e. it verifies there are no // continuous reconciles when everything should be stable. - framework.ValidateResourceVersionStable(ctx, proxy, namespace, clusterctlcluster.FilterClusterObjectsWithNameFilter(clusterName)) + framework.ValidateResourceVersionStable(ctx, framework.ValidateResourceVersionStableInput{ + ClusterProxy: proxy, + Namespace: namespace, + OwnerGraphFilterFunction: clusterctlcluster.FilterClusterObjectsWithNameFilter(clusterName), + }) }, } }) diff --git a/test/e2e/helpers/finalizers.go b/test/e2e/helpers/finalizers.go index 23c93974..ffe1a87c 100644 --- a/test/e2e/helpers/finalizers.go +++ b/test/e2e/helpers/finalizers.go @@ -23,7 +23,7 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" - addonsv1 "sigs.k8s.io/cluster-api/api/addons/v1beta1" + addonsv1 "sigs.k8s.io/cluster-api/api/addons/v1beta2" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" ) diff --git a/test/e2e/helpers/ownerreference.go b/test/e2e/helpers/ownerreference.go index 6acfed06..95a76510 100644 --- a/test/e2e/helpers/ownerreference.go +++ b/test/e2e/helpers/ownerreference.go @@ -21,10 +21,10 @@ package helpers import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - addonsv1 "sigs.k8s.io/cluster-api/api/addons/v1beta1" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1" - controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1" + addonsv1 "sigs.k8s.io/cluster-api/api/addons/v1beta2" + bootstrapv1 "sigs.k8s.io/cluster-api/api/bootstrap/kubeadm/v1beta2" + controlplanev1 "sigs.k8s.io/cluster-api/api/controlplane/kubeadm/v1beta2" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" "sigs.k8s.io/cluster-api/test/framework" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" diff --git a/test/e2e/suite_test.go b/test/e2e/suite_test.go index 1a569b64..e089be93 100644 --- a/test/e2e/suite_test.go +++ b/test/e2e/suite_test.go @@ -30,8 +30,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/klog/v2" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - ipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" + ipamv1 "sigs.k8s.io/cluster-api/api/ipam/v1beta2" "sigs.k8s.io/cluster-api/test/framework" "sigs.k8s.io/cluster-api/test/framework/bootstrap" "sigs.k8s.io/cluster-api/test/framework/clusterctl" From b2534988d7fc53a0248752675e5d8edac681aa7a Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 12 Jun 2026 13:20:00 +0000 Subject: [PATCH 02/94] chore: update e2e config, release metadata and compatibility matrix for CAPI v1.11 - Point e2e core/bootstrap/control-plane components at CAPI v1.11.11 and add the v1.11 shared clusterctl metadata - Add 0.7 release series (contract v1beta1, matching upstream CAPI v1.11 metadata) for the release carrying this upgrade - Extend the README compatibility matrix: v0.7 requires CAPI v1.11 (the v1beta2 Stage 2 status layout drops top-level status.ready, which CAPI <= v1.10 relies on) Co-Authored-By: Claude Fable 5 --- README.md | 11 +++--- metadata.yaml | 3 ++ test/e2e/config/ionoscloud.yaml | 18 +++++----- test/e2e/data/shared/v1.11/metadata.yaml | 45 ++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 14 deletions(-) create mode 100644 test/e2e/data/shared/v1.11/metadata.yaml diff --git a/README.md b/README.md index c5ab77a4..8f1bf2b0 100644 --- a/README.md +++ b/README.md @@ -47,11 +47,12 @@ This provider requires **Go 1.25 or newer**. The exact version is specified in ` This provider's versions are compatible with the following versions of Cluster API: -| | Cluster API v1beta1 (v1.8) | Cluster API v1beta1 (v1.9) | Cluster API v1beta1 (v1.10) | -|-----------------------|:--------------------------:|:--------------------------:|:---------------------------:| -| CAPIC v1alpha1 (v0.4) | ✓ | ☓ | ☓ | -| CAPIC v1alpha1 (v0.5) | ✓ | ☓ | ☓ | -| CAPIC v1alpha1 (v0.6) | ✓ | ✓ | ✓ | +| | Cluster API v1beta1 (v1.8) | Cluster API v1beta1 (v1.9) | Cluster API v1beta1 (v1.10) | Cluster API v1beta1/v1beta2 (v1.11) | +|-----------------------|:--------------------------:|:--------------------------:|:---------------------------:|:-----------------------------------:| +| CAPIC v1alpha1 (v0.4) | ✓ | ☓ | ☓ | ☓ | +| CAPIC v1alpha1 (v0.5) | ✓ | ☓ | ☓ | ☓ | +| CAPIC v1alpha1 (v0.6) | ✓ | ✓ | ✓ | ✓ | +| CAPIC v1alpha1 (v0.7) | ☓ | ☓ | ☓ | ✓ | ### Kubernetes Versions diff --git a/metadata.yaml b/metadata.yaml index 30aa7e2a..9be90add 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -2,6 +2,9 @@ apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3 kind: Metadata releaseSeries: +- major: 0 + minor: 7 + contract: v1beta1 - major: 0 minor: 6 contract: v1beta1 diff --git a/test/e2e/config/ionoscloud.yaml b/test/e2e/config/ionoscloud.yaml index 970770aa..5afc7ed2 100644 --- a/test/e2e/config/ionoscloud.yaml +++ b/test/e2e/config/ionoscloud.yaml @@ -5,39 +5,39 @@ providers: - name: cluster-api type: CoreProvider versions: - - name: "v1.10.10" - value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/core-components.yaml" + - name: "v1.11.11" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/core-components.yaml" type: url contract: v1beta1 replacements: - old: --metrics-addr=127.0.0.1:8080 new: --metrics-addr=:8443 files: - - sourcePath: "../data/shared/v1.10/metadata.yaml" + - sourcePath: "../data/shared/v1.11/metadata.yaml" - name: kubeadm type: BootstrapProvider versions: - - name: "v1.10.10" - value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/bootstrap-components.yaml" + - name: "v1.11.11" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/bootstrap-components.yaml" type: url contract: v1beta1 replacements: - old: --metrics-addr=127.0.0.1:8080 new: --metrics-addr=:8443 files: - - sourcePath: "../data/shared/v1.10/metadata.yaml" + - sourcePath: "../data/shared/v1.11/metadata.yaml" - name: kubeadm type: ControlPlaneProvider versions: - - name: "v1.10.10" - value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/control-plane-components.yaml" + - name: "v1.11.11" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/control-plane-components.yaml" type: url contract: v1beta1 replacements: - old: --metrics-addr=127.0.0.1:8080 new: --metrics-addr=:8443 files: - - sourcePath: "../data/shared/v1.10/metadata.yaml" + - sourcePath: "../data/shared/v1.11/metadata.yaml" - name: in-cluster type: IPAMProvider versions: diff --git a/test/e2e/data/shared/v1.11/metadata.yaml b/test/e2e/data/shared/v1.11/metadata.yaml new file mode 100644 index 00000000..8755a669 --- /dev/null +++ b/test/e2e/data/shared/v1.11/metadata.yaml @@ -0,0 +1,45 @@ +apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3 +kind: Metadata +releaseSeries: + - major: 1 + minor: 11 + contract: v1beta1 + - major: 1 + minor: 10 + contract: v1beta1 + - major: 1 + minor: 9 + contract: v1beta1 + - major: 1 + minor: 8 + contract: v1beta1 + - major: 1 + minor: 7 + contract: v1beta1 + - major: 1 + minor: 6 + contract: v1beta1 + - major: 1 + minor: 5 + contract: v1beta1 + - major: 1 + minor: 4 + contract: v1beta1 + - major: 1 + minor: 3 + contract: v1beta1 + - major: 1 + minor: 2 + contract: v1beta1 + - major: 1 + minor: 1 + contract: v1beta1 + - major: 1 + minor: 0 + contract: v1beta1 + - major: 0 + minor: 4 + contract: v1alpha4 + - major: 0 + minor: 3 + contract: v1alpha3 From eec13a2bece9e91a22fac182a55b6732e0e26a58 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 12 Jun 2026 13:25:32 +0000 Subject: [PATCH 03/94] chore: upgrade to Go 1.26 and modernize code - Raise the go directive to 1.26.0; bump builder image to golang:1.26.1 and lint/docs Go version references - Replace all ptr.To(...) call sites with the Go 1.26 extended new(expr) builtin and drop the now-unused ptr.To helper (ptr.Deref/ptr.Equal stay) - Apply gopls modernize fixes: errors.As -> errors.AsType (Go 1.26), strings.Split(...)[0] -> strings.Cut, redundant type arguments - Upgrade golangci-lint v1.64.8 -> v2.12.2 (v1 analyzers cannot parse new(expr) and reported false positives); migrate .golangci.yml to the v2 format, simplify lint-fix via the v2 fmt command, disable the new revive enforce-switch-style rule and exclude goconst in tests - Fix findings surfaced by v2's newer linters: preallocate slices, remove embedded-field selectors, fmt.Fprintf over WriteString(Sprintf), extract kind-name constants in e2e helpers Co-Authored-By: Claude Fable 5 --- .golangci.yml | 345 +++++++++--------- Dockerfile | 2 +- Makefile | 5 +- README.md | 2 +- api/v1alpha1/ionoscloudcluster_types_test.go | 4 +- api/v1alpha1/ionoscloudmachine_types_test.go | 48 ++- go.mod | 31 +- go.sum | 64 ++-- .../ionoscloudcluster_controller.go | 3 +- .../ionoscloudmachine_controller.go | 4 +- internal/service/cloud/image_test.go | 5 +- internal/service/cloud/ipblock_test.go | 45 ++- internal/service/cloud/network.go | 6 +- internal/service/cloud/network_test.go | 119 +++--- internal/service/cloud/request.go | 4 +- internal/service/cloud/request_test.go | 37 +- internal/service/cloud/server.go | 28 +- internal/service/cloud/server_test.go | 104 +++--- internal/service/cloud/service.go | 3 +- internal/service/cloud/suite_test.go | 47 ++- internal/service/k8s/ipam.go | 3 +- internal/service/k8s/ipam_test.go | 13 +- internal/util/ptr/ptr.go | 5 - internal/util/ptr/ptr_test.go | 11 +- scope/machine.go | 2 +- test/e2e/capic_test.go | 11 +- test/e2e/env_test.go | 2 +- test/e2e/helpers/finalizers.go | 8 +- test/e2e/helpers/ownerreference.go | 23 +- 29 files changed, 483 insertions(+), 501 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index b168f76b..3cce430c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,5 +1,11 @@ +version: "2" +run: + go: "1.26" + build-tags: + - e2e + allow-parallel-runners: true linters: - disable-all: true + default: none enable: - asciicheck - bidichk @@ -13,14 +19,11 @@ linters: - errname - errorlint - exhaustive - - gci - ginkgolinter - goconst - gocritic - godot - - gofumpt - gosec - - gosimple - govet - importas - inamedparam @@ -34,183 +37,181 @@ linters: - nilerr - nolintlint - nosprintfhostport - - prealloc - perfsprint - - revive + - prealloc - reassign + - revive - staticcheck - - stylecheck - tagalign - testifylint - - typecheck - - usetesting - unconvert - unparam - unused + - usetesting - whitespace - -linters-settings: - depguard: - rules: - main: - deny: - - pkg: "github.com/pkg/errors" - desc: Should be replaced by stdlib errors package - gci: - sections: - - standard - - default - - prefix(github.com/ionos-cloud/cluster-api-provider-ionoscloud) - - blank - - dot - gosec: - excludes: - - G601 # Implicit memory aliasing of items from a range statement: Obsolete since Go 1.22 - importas: - no-unaliased: true - alias: - # Kubernetes - - pkg: k8s.io/api/(\w+)/(v[\w\d]+) - alias: $1$2 - - pkg: k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1 - alias: apiextensionsv1 - - pkg: k8s.io/apimachinery/pkg/apis/meta/v1 - alias: metav1 - - pkg: k8s.io/apimachinery/pkg/api/errors - alias: apierrors - - pkg: k8s.io/apimachinery/pkg/util/errors - alias: kerrors - # Controller Runtime - - pkg: sigs.k8s.io/controller-runtime - alias: ctrl - # Cluster API - - pkg: sigs.k8s.io/cluster-api/api/core/v1beta2 - alias: clusterv1 - - pkg: "sigs.k8s.io/cluster-api/test/e2e" - alias: capie2e - - pkg: "sigs.k8s.io/cluster-api/util/conditions" - alias: conditions - - pkg: "sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1" - alias: deprecatedv1beta1conditions - - pkg: "sigs.k8s.io/cluster-api/api/ipam/v1beta2" - alias: ipamv1 - # Own module - - pkg: github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1 - alias: infrav1 - - pkg: github.com/ionos-cloud/sdk-go/v6 - alias: sdk - ireturn: - allow: - - error - - generic - - ionoscloud.Client - - bootstrap.ClusterProvider - - framework.ClusterProxy - loggercheck: - require-string-key: true - no-printf-like: true - revive: - enable-all-rules: true + settings: + depguard: + rules: + main: + deny: + - pkg: github.com/pkg/errors + desc: Should be replaced by stdlib errors package + gosec: + excludes: + - G601 + importas: + alias: + - pkg: k8s.io/api/(\w+)/(v[\w\d]+) + alias: $1$2 + - pkg: k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1 + alias: apiextensionsv1 + - pkg: k8s.io/apimachinery/pkg/apis/meta/v1 + alias: metav1 + - pkg: k8s.io/apimachinery/pkg/api/errors + alias: apierrors + - pkg: k8s.io/apimachinery/pkg/util/errors + alias: kerrors + - pkg: sigs.k8s.io/controller-runtime + alias: ctrl + - pkg: sigs.k8s.io/cluster-api/api/core/v1beta2 + alias: clusterv1 + - pkg: sigs.k8s.io/cluster-api/test/e2e + alias: capie2e + - pkg: sigs.k8s.io/cluster-api/util/conditions + alias: conditions + - pkg: sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1 + alias: deprecatedv1beta1conditions + - pkg: sigs.k8s.io/cluster-api/api/ipam/v1beta2 + alias: ipamv1 + - pkg: github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1 + alias: infrav1 + - pkg: github.com/ionos-cloud/sdk-go/v6 + alias: sdk + no-unaliased: true + ireturn: + allow: + - error + - generic + - ionoscloud.Client + - bootstrap.ClusterProvider + - framework.ClusterProxy + loggercheck: + require-string-key: true + no-printf-like: true + revive: + enable-all-rules: true + rules: + - name: add-constant + disabled: true + - name: cognitive-complexity + arguments: + - 25 + - name: cyclomatic + arguments: + - 15 + - name: enforce-switch-style + disabled: true + - name: line-length-limit + disabled: true + - name: max-public-structs + disabled: true + - name: range-val-address + disabled: true + - name: redundant-import-alias + disabled: true + - name: struct-tag + arguments: + - json,inline + - name: var-naming + arguments: + - [] + - - LAN + - IP + - NIC + testifylint: + enable-all: true + exclusions: + generated: lax rules: - - name: add-constant - disabled: true - - name: cognitive-complexity - arguments: - - 25 - - name: cyclomatic - arguments: - - 15 - - name: line-length-limit - disabled: true - - name: max-public-structs - disabled: true - - name: range-val-address - disabled: true # Obsolete since Go 1.22 - - name: redundant-import-alias - disabled: true # Conflicts with importas which enforces explicit aliases - - name: struct-tag - arguments: - - json,inline - - name: var-naming - arguments: - - [] # first list is an allow list. All initialisms placed here will be ignored - - [LAN, IP, NIC] # second list is a deny list. These rules will be enforced - testifylint: - enable-all: true - + - linters: + - containedctx + - goconst + path: (.+)_test\.go + - linters: + - gocritic + text: 'appendAssign: append result not assigned to the same slice' + - linters: + - revive + text: 'comment-spacings: no space between comment delimiter and comment text' + source: //(\+(kubebuilder|optional|required|list)|#nosec) + - linters: + - revive + text: 'exported: exported method .*\.(Reconcile|SetupWithManager|SetupWebhookWithManager) should have comment or be unexported' + - linters: + - revive + path: suite_test.go + text: maximum number of lines per function exceeded + - linters: + - revive + text: exported (method|function|type|const) (.+) should have comment or be unexported + source: (func|type).*Fake.* + - linters: + - revive + path: fake_\.go + text: exported (method|function|type|const) (.+) should have comment or be unexported + - linters: + - revive + path: (framework|e2e|infrastructure/docker)/.*.go + text: exported (method|function|type|const) (.+) should have comment or be unexported + - linters: + - revive + text: line is \d+ characters, out of limit \d+ + source: \s*//\+kubebuilder:.* + - linters: + - revive + text: line is \d+ characters, out of limit \d+ + source: \s*//.*[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?$ + - linters: + - revive + path: _test\.go + text: 'function-length: maximum number of lines per function exceeded' + - linters: + - unparam + text: always receives + - path: _test\.go + text: should not use dot imports + - path: _test\.go + text: cyclomatic complexity + - path: (framework|e2e)/.*.go + text: should not use dot imports + paths: + - zz_generated.*\.go$ + - .*conversion.*\.go$ + - mock* + - third_party$ + - builtin$ + - examples$ issues: - max-same-issues: 0 max-issues-per-linter: 0 - # We are disabling default golangci exclusions because we want to help reviewers to focus on reviewing the most relevant - # changes in PRs and avoid nitpicking. - exclude-use-default: false - exclude-rules: - - linters: - - containedctx - path: '(.+)_test\.go' - - linters: - - gocritic - text: "appendAssign: append result not assigned to the same slice" - - linters: - - revive - text: "comment-spacings: no space between comment delimiter and comment text" - source: "//(\\+(kubebuilder|optional|required|list)|#nosec)" - - linters: - - revive - text: "exported: exported method .*\\.(Reconcile|SetupWithManager|SetupWebhookWithManager) should have comment or be unexported" - - linters: - - revive - text: maximum number of lines per function exceeded - path: suite_test.go - # Exclude some packages or code to require comments, for example test code, or fake clients. - - linters: - - revive - text: exported (method|function|type|const) (.+) should have comment or be unexported - source: (func|type).*Fake.* - - linters: - - revive - text: exported (method|function|type|const) (.+) should have comment or be unexported - path: fake_\.go - - linters: - - revive - text: exported (method|function|type|const) (.+) should have comment or be unexported - path: "(framework|e2e|infrastructure/docker)/.*.go" - - linters: - - revive - text: "line is \\d+ characters, out of limit \\d+" - source: "\\s*//\\+kubebuilder:.*" - - linters: - - revive - text: "line is \\d+ characters, out of limit \\d+" - # If a line comment ends with something that looks like a URL, we ignore the line length limit. - source: "\\s*//.*[-a-zA-Z0-9@:%_\\+.~#?&//=]{2,256}\\.[a-z]{2,4}\\b(\\/[-a-zA-Z0-9@:%_\\+.~#?&//=]*)?$" - - linters: - - revive - text: "function-length: maximum number of lines per function exceeded" - path: _test\.go - # Disable unparam "always receives" which might not be really - # useful when building libraries. - - linters: - - unparam - text: always receives - # Dot imports for gomega or ginkgo are allowed - # within test files. - - path: _test\.go - text: should not use dot imports - - path: _test\.go - text: cyclomatic complexity - # Append should be able to assign to a different var/slice. - - path: (framework|e2e)/.*.go - text: should not use dot imports - exclude-files: - - "zz_generated.*\\.go$" - - ".*conversion.*\\.go$" - exclude-dirs: - - mock* - -run: - build-tags: - - e2e - timeout: 10m - allow-parallel-runners: true - go: "1.25" + max-same-issues: 0 +formatters: + enable: + - gci + - gofumpt + settings: + gci: + sections: + - standard + - default + - prefix(github.com/ionos-cloud/cluster-api-provider-ionoscloud) + - blank + - dot + exclusions: + generated: lax + paths: + - zz_generated.*\.go$ + - .*conversion.*\.go$ + - mock* + - third_party$ + - builtin$ + - examples$ diff --git a/Dockerfile b/Dockerfile index 6d123789..df4ae929 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.25.11 AS builder +FROM golang:1.26.4 AS builder ARG TARGETOS ARG TARGETARCH diff --git a/Makefile b/Makefile index de4d52a4..4b4cc4ef 100644 --- a/Makefile +++ b/Makefile @@ -64,8 +64,7 @@ lint: ## Run lint. .PHONY: lint-fix lint-fix: ## Fix linter problems. - # gci collides with gofumpt. But if we run gci before gofumpt, this will solve the issue. - $(GOLANGCI) run --timeout 5m -c .golangci.yml --enable-only gci --fix + $(GOLANGCI) fmt -c .golangci.yml $(GOLANGCI) run --timeout 5m -c .golangci.yml --fix .PHONY: vet @@ -175,7 +174,7 @@ KUBECTL ?= kubectl KUSTOMIZE ?= $(LOCALBIN)/kustomize CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen ENVTEST ?= $(LOCALBIN)/setup-envtest -GOLANGCI ?= go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.8 +GOLANGCI ?= go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 MOCKERY ?= go run github.com/vektra/mockery/v2@v2.53.5 ## Tool Versions diff --git a/README.md b/README.md index 8f1bf2b0..a4e59e17 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ If you need help with CAPIC, please visit the [#cluster-api-ionoscloud][slack] c ### Go Version -This provider requires **Go 1.25 or newer**. The exact version is specified in `go.mod`. +This provider requires **Go 1.26 or newer**. The exact version is specified in `go.mod`. ### Cluster API Versions diff --git a/api/v1alpha1/ionoscloudcluster_types_test.go b/api/v1alpha1/ionoscloudcluster_types_test.go index 8247160c..58ef83cb 100644 --- a/api/v1alpha1/ionoscloudcluster_types_test.go +++ b/api/v1alpha1/ionoscloudcluster_types_test.go @@ -27,8 +27,6 @@ import ( deprecatedv1beta1conditions "sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -156,7 +154,7 @@ var _ = Describe("IonosCloudCluster", func() { Expect(fetched.GetV1Beta1Conditions()).To(BeEmpty()) By("retrieving the cluster and setting the status") - fetched.Status.Initialization = IonosCloudClusterInitializationStatus{Provisioned: ptr.To(true)} + fetched.Status.Initialization = IonosCloudClusterInitializationStatus{Provisioned: new(true)} wantProvisionRequest := ProvisioningRequest{ Method: "POST", RequestPath: "/path/to/resource", diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index b2100671..3102cfb3 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -26,8 +26,6 @@ import ( deprecatedv1beta1conditions "sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -39,12 +37,12 @@ func defaultMachine() *IonosCloudMachine { Namespace: metav1.NamespaceDefault, }, Spec: IonosCloudMachineSpec{ - ProviderID: ptr.To("ionos://ee090ff2-1eef-48ec-a246-a51a33aa4f3a"), + ProviderID: new("ionos://ee090ff2-1eef-48ec-a246-a51a33aa4f3a"), DatacenterID: "ee090ff2-1eef-48ec-a246-a51a33aa4f3a", NumCores: 1, AvailabilityZone: AvailabilityZoneTwo, MemoryMB: 2048, - CPUFamily: ptr.To("AMD_OPTERON"), + CPUFamily: new("AMD_OPTERON"), Disk: &Volume{ Name: "disk", DiskType: VolumeDiskTypeSSDStandard, @@ -65,7 +63,7 @@ func defaultMachine() *IonosCloudMachine { func setInvalidPoolRef(m *IonosCloudMachine, poolType string, kind, apiGroup, name string) { ref := &corev1.TypedLocalObjectReference{ - APIGroup: ptr.To(apiGroup), + APIGroup: new(apiGroup), Kind: kind, Name: name, } @@ -380,7 +378,7 @@ var _ = Describe("IonosCloudMachine Tests", func() { DescribeTable("should allow IPv4PoolRef.Kind GlobalInClusterIPPool and InClusterIPPool", func(kind string) { m := defaultMachine() m.Spec.AdditionalNetworks[0].IPv4PoolRef = &corev1.TypedLocalObjectReference{ - APIGroup: ptr.To("ipam.cluster.x-k8s.io"), + APIGroup: new("ipam.cluster.x-k8s.io"), Kind: kind, Name: "ipv4-pool", } @@ -392,7 +390,7 @@ var _ = Describe("IonosCloudMachine Tests", func() { DescribeTable("should allow IPv6PoolRef.Kind GlobalInClusterIPPool and InClusterIPPool", func(kind string) { m := defaultMachine() m.Spec.AdditionalNetworks[0].IPv6PoolRef = &corev1.TypedLocalObjectReference{ - APIGroup: ptr.To("ipam.cluster.x-k8s.io"), + APIGroup: new("ipam.cluster.x-k8s.io"), Kind: kind, Name: "ipv6-pool", } @@ -424,15 +422,15 @@ var _ = Describe("IonosCloudMachine Tests", func() { Context("FailoverIP", func() { It("should allow setting AUTO as the value", func() { m := defaultMachine() - m.Spec.FailoverIP = ptr.To(CloudResourceConfigAuto) + m.Spec.FailoverIP = new(CloudResourceConfigAuto) Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(m.Spec.FailoverIP).To(Equal(ptr.To(CloudResourceConfigAuto))) + Expect(m.Spec.FailoverIP).To(Equal(new(CloudResourceConfigAuto))) }) It("should allow setting a valid IPv4 address", func() { m := defaultMachine() - m.Spec.FailoverIP = ptr.To("203.0.113.1") + m.Spec.FailoverIP = new("203.0.113.1") Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(m.Spec.FailoverIP).To(Equal(ptr.To("203.0.113.1"))) + Expect(m.Spec.FailoverIP).To(Equal(new("203.0.113.1"))) }) It("should allow setting null", func() { m := defaultMachine() @@ -452,26 +450,26 @@ var _ = Describe("IonosCloudMachine Tests", func() { ) It("should require AUTO to be in capital letters", func() { m := defaultMachine() - m.Spec.FailoverIP = ptr.To("Auto") + m.Spec.FailoverIP = new("Auto") Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }) It("should be immutable", func() { m := defaultMachine() - m.Spec.FailoverIP = ptr.To(CloudResourceConfigAuto) + m.Spec.FailoverIP = new(CloudResourceConfigAuto) Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(m.Spec.FailoverIP).To(Equal(ptr.To(CloudResourceConfigAuto))) - m.Spec.FailoverIP = ptr.To("127.0.0.1") + Expect(m.Spec.FailoverIP).To(Equal(new(CloudResourceConfigAuto))) + m.Spec.FailoverIP = new("127.0.0.1") Expect(k8sClient.Update(context.Background(), m)).ToNot(Succeed()) - m.Spec.FailoverIP = ptr.To("") + m.Spec.FailoverIP = new("") Expect(k8sClient.Update(context.Background(), m)).ToNot(Succeed()) }) }) Context("NetworkID", func() { It("should allow setting an existing NetworkID in the spec", func() { m := defaultMachine() - m.Spec.NetworkID = ptr.To("1") + m.Spec.NetworkID = new("1") Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(m.Spec.NetworkID).To(Equal(ptr.To("1"))) + Expect(m.Spec.NetworkID).To(Equal(new("1"))) }) It("should allow setting null", func() { m := defaultMachine() @@ -480,17 +478,17 @@ var _ = Describe("IonosCloudMachine Tests", func() { }) It("should not allow setting empty NetworkID", func() { m := defaultMachine() - m.Spec.NetworkID = ptr.To("") + m.Spec.NetworkID = new("") Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }) It("should be immutable", func() { m := defaultMachine() - m.Spec.NetworkID = ptr.To("1") + m.Spec.NetworkID = new("1") Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(m.Spec.NetworkID).To(Equal(ptr.To("1"))) - m.Spec.NetworkID = ptr.To("2") + Expect(m.Spec.NetworkID).To(Equal(new("1"))) + m.Spec.NetworkID = new("2") Expect(k8sClient.Update(context.Background(), m)).ToNot(Succeed()) - m.Spec.NetworkID = ptr.To("") + m.Spec.NetworkID = new("") Expect(k8sClient.Update(context.Background(), m)).ToNot(Succeed()) m.Spec.NetworkID = nil Expect(k8sClient.Update(context.Background(), m)).ToNot(Succeed()) @@ -511,7 +509,7 @@ var _ = Describe("IonosCloudMachine Tests", func() { }) It("should fail if cpuFamily is set and type is VCPU", func() { m := defaultMachine() - m.Spec.CPUFamily = ptr.To("some-cpu-family") + m.Spec.CPUFamily = new("some-cpu-family") m.Spec.Type = ServerTypeVCPU Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }) @@ -554,7 +552,7 @@ var _ = Describe("IonosCloudMachine Tests", func() { Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: m.Name, Namespace: m.Namespace}, m)).To(Succeed()) - m.Status.Initialization = IonosCloudMachineInitializationStatus{Provisioned: ptr.To(true)} + m.Status.Initialization = IonosCloudMachineInitializationStatus{Provisioned: new(true)} deprecatedv1beta1conditions.MarkTrue(m, MachineProvisionedCondition) m.Status.CurrentRequest = &ProvisioningRequest{ Method: "GET", diff --git a/go.mod b/go.mod index 66cd3969..7db8deaa 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,12 @@ module github.com/ionos-cloud/cluster-api-provider-ionoscloud -go 1.25.0 +go 1.26.0 require ( github.com/go-logr/logr v1.4.3 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 - github.com/ionos-cloud/sdk-go/v6 v6.3.4 + github.com/ionos-cloud/sdk-go/v6 v6.3.8 github.com/jarcoal/httpmock v1.4.0 github.com/onsi/ginkgo/v2 v2.23.4 github.com/onsi/gomega v1.38.0 @@ -27,6 +27,7 @@ require ( cel.dev/expr v0.25.1 // indirect github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect + github.com/Masterminds/semver/v3 v3.5.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect @@ -48,7 +49,7 @@ require ( github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/fsnotify/fsnotify v1.10.1 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect @@ -113,23 +114,23 @@ require ( go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect + go.uber.org/zap v1.28.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.47.0 // indirect + golang.org/x/crypto v0.53.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect - golang.org/x/net v0.49.0 // indirect - golang.org/x/oauth2 v0.34.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.42.0 // indirect - golang.org/x/term v0.39.0 // indirect - golang.org/x/text v0.33.0 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/term v0.44.0 // indirect + golang.org/x/text v0.38.0 // indirect golang.org/x/time v0.11.0 // indirect - golang.org/x/tools v0.40.0 // indirect + golang.org/x/tools v0.45.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect - google.golang.org/grpc v1.79.3 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/grpc v1.81.1 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index 2e486b2b..f00b7152 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= -github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -70,8 +70,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= -github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -129,8 +129,8 @@ github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/ionos-cloud/sdk-go/v6 v6.3.4 h1:jTvGl4LOF8v8OYoEIBNVwbFoqSGAFqn6vGE7sp7/BqQ= -github.com/ionos-cloud/sdk-go/v6 v6.3.4/go.mod h1:wCVwNJ/21W29FWFUv+fNawOTMlFoP1dS3L+ZuztFW48= +github.com/ionos-cloud/sdk-go/v6 v6.3.8 h1:CUZzrNciLM2IlmZtnclIznjST29tAYQbtQ8epiX5RUo= +github.com/ionos-cloud/sdk-go/v6 v6.3.8/go.mod h1:nUGHP4kZHAZngCVr4v6C8nuargFrtvt7GrzH/hqn7c4= github.com/jarcoal/httpmock v1.4.0 h1:BvhqnH0JAYbNudL2GMJKgOHe2CtKlzJ/5rWKyp+hc2k= github.com/jarcoal/httpmock v1.4.0/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -276,8 +276,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -286,8 +286,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -297,15 +297,15 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= -golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -314,37 +314,37 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= -golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= -golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= -google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= -google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4= +google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= +google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/controller/ionoscloudcluster_controller.go b/internal/controller/ionoscloudcluster_controller.go index 25e09a89..d2762cef 100644 --- a/internal/controller/ionoscloudcluster_controller.go +++ b/internal/controller/ionoscloudcluster_controller.go @@ -43,7 +43,6 @@ import ( infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/service/cloud" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/locker" - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/scope" ) @@ -179,7 +178,7 @@ func (r *IonosCloudClusterReconciler) reconcileNormal( Status: metav1.ConditionTrue, Reason: string(infrav1.IonosCloudClusterReady), }) - clusterScope.IonosCluster.Status.Initialization.Provisioned = ptr.To(true) + clusterScope.IonosCluster.Status.Initialization.Provisioned = new(true) // Set deprecated v1beta1 ready field for backwards compatibility. if clusterScope.IonosCluster.Status.Deprecated == nil { clusterScope.IonosCluster.Status.Deprecated = &infrav1.IonosCloudClusterDeprecatedStatus{} diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index f8ca9160..36d3a0e0 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -135,7 +135,7 @@ func (r *IonosCloudMachineReconciler) Reconcile( return ctrl.Result{}, fmt.Errorf("failed to create ionos client: %w", err) } - if !ionosCloudMachine.ObjectMeta.DeletionTimestamp.IsZero() { + if !ionosCloudMachine.DeletionTimestamp.IsZero() { return r.reconcileDelete(ctx, machineScope, cloudService) } @@ -351,7 +351,7 @@ func (r *IonosCloudMachineReconciler) getClusterScope( Name: cluster.Spec.InfrastructureRef.Name, } - if err := r.Client.Get(ctx, infraClusterName, ionosCloudCluster); err != nil { + if err := r.Get(ctx, infraClusterName, ionosCloudCluster); err != nil { if apierrors.IsNotFound(err) { // Cluster has not yet been created return nil, nil diff --git a/internal/service/cloud/image_test.go b/internal/service/cloud/image_test.go index 2d85ab5f..9cba8e84 100644 --- a/internal/service/cloud/image_test.go +++ b/internal/service/cloud/image_test.go @@ -29,7 +29,6 @@ import ( infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud/clienttest" - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" ) type imageTestSuite struct { @@ -114,7 +113,7 @@ func (s *imageTestSuite) TestLookupImageIgnoreMissingMachineVersion() { s.ionosClient.EXPECT().GetImage(s.ctx, "image-1").Return(s.makeTestImage("image-1", "test", "loc"), nil).Once() s.ionosClient.EXPECT().GetDatacenterLocationByID(s.ctx, s.infraMachine.Spec.DatacenterID).Return("loc", nil).Once() - s.infraMachine.Spec.Disk.Image.Selector.UseMachineVersion = ptr.To(false) + s.infraMachine.Spec.Disk.Image.Selector.UseMachineVersion = new(false) s.capiMachine.Spec.Version = "" imageID, err := s.service.lookupImageID(s.ctx, s.machineScope) @@ -233,7 +232,7 @@ func makeTestImage(id, name, location string) *sdk.Image { func makeTestLabel(typ, id, key, value string) sdk.Label { return sdk.Label{ - Id: ptr.To(fmt.Sprintf("urn:label:%s:%s:%s", typ, id, key)), + Id: new(fmt.Sprintf("urn:label:%s:%s:%s", typ, id, key)), Properties: &sdk.LabelProperties{ Key: &key, Value: &value, diff --git a/internal/service/cloud/ipblock_test.go b/internal/service/cloud/ipblock_test.go index 9f9645b8..14861ff5 100644 --- a/internal/service/cloud/ipblock_test.go +++ b/internal/service/cloud/ipblock_test.go @@ -26,7 +26,6 @@ import ( infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud/clienttest" - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" ) type ipBlockTestSuite struct { @@ -59,8 +58,8 @@ func (s *ipBlockTestSuite) TestGetControlPlaneEndpointIPBlockSingleMatch() { *exampleIPBlock(), { Properties: &sdk.IpBlockProperties{ - Name: ptr.To(exampleIPBlockName), - Location: ptr.To("es/vit"), + Name: new(exampleIPBlockName), + Location: new("es/vit"), }, }, }, @@ -75,14 +74,14 @@ func (s *ipBlockTestSuite) TestGetControlPlaneEndpointIPBlockSingleMatch() { func (s *ipBlockTestSuite) TestGetControlPlaneEndpointIPBlockUserSetIP() { s.clusterScope.IonosCluster.Spec.ControlPlaneEndpoint.Host = exampleEndpointIP - name := ptr.To("random name") - location := ptr.To(exampleLocation) + name := new("random name") + location := new(exampleLocation) s.mockListIPBlocksCall().Return(&sdk.IpBlocks{ Items: &[]sdk.IpBlock{ { - Id: ptr.To(exampleIPBlockID), + Id: new(exampleIPBlockID), Metadata: &sdk.DatacenterElementMetadata{ - State: ptr.To(sdk.Available), + State: new(sdk.Available), }, Properties: &sdk.IpBlockProperties{ Name: name, @@ -110,7 +109,7 @@ func (s *ipBlockTestSuite) TestGetControlPlaneEndpointIPBlockUserSetIP() { func (s *ipBlockTestSuite) TestGetControlPlaneEndpointIPBlockPreviouslySetID() { s.clusterScope.IonosCluster.Status.ControlPlaneEndpointIPBlockID = exampleIPBlockID s.mockGetIPBlockByIDCall(exampleIPBlockID).Return(&sdk.IpBlock{ - Id: ptr.To(exampleIPBlockID), + Id: new(exampleIPBlockID), }, nil).Once() block, err := s.service.getControlPlaneEndpointIPBlock(s.ctx, s.clusterScope) @@ -137,8 +136,8 @@ func (s *ipBlockTestSuite) TestGetControlPlaneEndpointIPBlockNoMatch() { Items: &[]sdk.IpBlock{ { Properties: &sdk.IpBlockProperties{ - Name: ptr.To(exampleIPBlockName), - Location: ptr.To("de/fra"), + Name: new(exampleIPBlockName), + Location: new("de/fra"), }, }, }, @@ -249,7 +248,7 @@ func (s *ipBlockTestSuite) TestReconcileControlPlaneEndpointUnavailable() { func (s *ipBlockTestSuite) TestReconcileControlPlaneEndpointUserSetIP() { block := exampleIPBlock() - block.Properties.Name = ptr.To("asdf") + block.Properties.Name = new("asdf") block.Properties.Ips = &[]string{ "another IP", exampleEndpointIP, @@ -324,7 +323,7 @@ func (s *ipBlockTestSuite) TestReconcileControlPlaneEndpointDeletionCreationPend func (s *ipBlockTestSuite) TestReconcileControlPlaneEndpointDeletionUserSetIPWithIPBlockID() { s.clusterScope.IonosCluster.Status.ControlPlaneEndpointIPBlockID = exampleIPBlockID s.mockGetIPBlockByIDCall(exampleIPBlockID).Return(&sdk.IpBlock{ - Id: ptr.To(exampleIPBlockID), + Id: new(exampleIPBlockID), }, nil).Once() requeue, err := s.service.ReconcileControlPlaneEndpointDeletion(s.ctx, s.clusterScope) s.False(requeue) @@ -333,7 +332,7 @@ func (s *ipBlockTestSuite) TestReconcileControlPlaneEndpointDeletionUserSetIPWit func (s *ipBlockTestSuite) TestReconcileControlPlaneEndpointDeletionUserSetIPWithoutIPBlockID() { block := exampleIPBlock() - block.Properties.Name = ptr.To("aaaa") + block.Properties.Name = new("aaaa") block.Properties.Ips = &[]string{ "an IP", exampleEndpointIP, @@ -398,7 +397,7 @@ func (s *ipBlockTestSuite) TestReconcileControlPlaneEndpointDeletionErrorOnDupli } func (s *ipBlockTestSuite) TestReconcileFailoverIPBlockDeletion() { - s.infraMachine.Spec.FailoverIP = ptr.To(infrav1.CloudResourceConfigAuto) + s.infraMachine.Spec.FailoverIP = new(infrav1.CloudResourceConfigAuto) ipBlock := exampleIPBlockWithName(s.service.failoverIPBlockName(s.machineScope)) s.mockGetDatacenterLocationByIDCall(exampleDatacenterID).Return(exampleLocation, nil).Once() @@ -413,11 +412,11 @@ func (s *ipBlockTestSuite) TestReconcileFailoverIPBlockDeletion() { } func (s *ipBlockTestSuite) TestReconcileFailoverIPBlockDeletionSkipped() { - s.infraMachine.Spec.FailoverIP = ptr.To(infrav1.CloudResourceConfigAuto) + s.infraMachine.Spec.FailoverIP = new(infrav1.CloudResourceConfigAuto) ipBlock := exampleIPBlockWithName(s.service.failoverIPBlockName(s.machineScope)) lan := s.exampleLAN() lan.Properties.IpFailover = &[]sdk.IPFailover{{ - Ip: ptr.To(exampleEndpointIP), + Ip: new(exampleEndpointIP), NicUuid: nil, }} @@ -432,7 +431,7 @@ func (s *ipBlockTestSuite) TestReconcileFailoverIPBlockDeletionSkipped() { } func (s *ipBlockTestSuite) TestReconcileFailoverIPBlockDeletionPendingCreation() { - s.infraMachine.Spec.FailoverIP = ptr.To(infrav1.CloudResourceConfigAuto) + s.infraMachine.Spec.FailoverIP = new(infrav1.CloudResourceConfigAuto) s.mockGetDatacenterLocationByIDCall(exampleDatacenterID).Return(exampleLocation, nil).Once() s.mockListIPBlocksCall().Return(nil, nil).Once() @@ -448,7 +447,7 @@ func (s *ipBlockTestSuite) TestReconcileFailoverIPBlockDeletionPendingCreation() } func (s *ipBlockTestSuite) TestReconcileFailoverIPBlockDeletionPendingDeletion() { - s.infraMachine.Spec.FailoverIP = ptr.To(infrav1.CloudResourceConfigAuto) + s.infraMachine.Spec.FailoverIP = new(infrav1.CloudResourceConfigAuto) ipBlock := exampleIPBlockWithName(s.service.failoverIPBlockName(s.machineScope)) s.mockGetDatacenterLocationByIDCall(exampleDatacenterID).Return(exampleLocation, nil).Once() @@ -476,7 +475,7 @@ func (s *ipBlockTestSuite) TestReconcileFailoverIPBlockDeletionPendingDeletion() } func (s *ipBlockTestSuite) TestReconcileFailoverIPBlockDeletionDeletionFinished() { - s.infraMachine.Spec.FailoverIP = ptr.To(infrav1.CloudResourceConfigAuto) + s.infraMachine.Spec.FailoverIP = new(infrav1.CloudResourceConfigAuto) ipBlock := exampleIPBlockWithName(s.service.failoverIPBlockName(s.machineScope)) s.mockGetDatacenterLocationByIDCall(exampleDatacenterID).Return(exampleLocation, nil).Once() @@ -517,13 +516,13 @@ func exampleIPBlock() *sdk.IpBlock { func exampleIPBlockWithName(name string) *sdk.IpBlock { return &sdk.IpBlock{ - Id: ptr.To(exampleIPBlockID), + Id: new(exampleIPBlockID), Metadata: &sdk.DatacenterElementMetadata{ - State: ptr.To(sdk.Available), + State: new(sdk.Available), }, Properties: &sdk.IpBlockProperties{ - Name: ptr.To(name), - Location: ptr.To(exampleLocation), + Name: new(name), + Location: new(exampleLocation), Ips: &[]string{ exampleEndpointIP, }, diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index e8b82af6..e7d29004 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -190,9 +190,9 @@ func (s *Service) createLAN(ctx context.Context, ms *scope.Machine) error { log := s.logger.WithName("createLAN") lanProperties := sdk.LanProperties{ - Name: ptr.To(s.lanName(ms.ClusterScope.Cluster)), - Public: ptr.To(true), - Ipv6CidrBlock: ptr.To("AUTO"), // IPv6 is enabled by default. + Name: new(s.lanName(ms.ClusterScope.Cluster)), + Public: new(true), + Ipv6CidrBlock: new("AUTO"), // IPv6 is enabled by default. } requestPath, err := s.ionosClient.CreateLAN(ctx, ms.DatacenterID(), lanProperties) diff --git a/internal/service/cloud/network_test.go b/internal/service/cloud/network_test.go index 61b590e4..f46dd1ac 100644 --- a/internal/service/cloud/network_test.go +++ b/internal/service/cloud/network_test.go @@ -31,7 +31,6 @@ import ( infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud/clienttest" - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" ) const ( @@ -103,7 +102,7 @@ func (s *lanSuite) TestNetworkGetLAN_ExistingLAN() { lan := s.exampleLAN() s.mockListLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{lan}}, nil).Once() - s.machineScope.IonosMachine.Spec.NetworkID = ptr.To("42") + s.machineScope.IonosMachine.Spec.NetworkID = new("42") foundLAN, err := s.service.getLAN(s.ctx, s.machineScope) s.NoError(err) s.NotNil(foundLAN) @@ -114,7 +113,7 @@ func (s *lanSuite) TestNetworkGetLAN_LANIDNotFound() { lan := s.exampleLAN() s.mockListLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{lan}}, nil).Once() - s.machineScope.IonosMachine.Spec.NetworkID = ptr.To("2") + s.machineScope.IonosMachine.Spec.NetworkID = new("2") foundLAN, err := s.service.getLAN(s.ctx, s.machineScope) s.EqualError(err, "LAN with ID 2 not found") s.Nil(foundLAN) @@ -165,7 +164,7 @@ func (s *lanSuite) TestNetworkReconcileLANExistingLAN() { func (s *lanSuite) TestNetworkReconcileLANExistingLANUnavailable() { lan := s.exampleLAN() - lan.Metadata.State = ptr.To("BUSY") + lan.Metadata.State = new("BUSY") s.mockListLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{lan}}, nil).Once() requeue, err := s.service.ReconcileLAN(s.ctx, s.machineScope) s.NoError(err) @@ -183,7 +182,7 @@ func (s *lanSuite) TestNetworkReconcileLANDeleteLANExistsNoPendingRequestsNoOthe func (s *lanSuite) TestNetworkReconcileLANDeleteLANExistsNoPendingRequestsHasOtherUsersNoDelete() { lan := s.exampleLAN() - lan.Entities.Nics.Items = &[]sdk.Nic{{Id: ptr.To("1")}} + lan.Entities.Nics.Items = &[]sdk.Nic{{Id: new("1")}} s.mockListLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{lan}}, nil).Once() s.mockGetLANDeletionRequestsCall().Return([]sdk.Request{}, nil).Once() requeue, err := s.service.ReconcileLANDeletion(s.ctx, s.machineScope) @@ -232,8 +231,8 @@ func (s *lanSuite) TestReconcileIPFailoverNICNotInFailoverGroup() { props := sdk.LanProperties{ IpFailover: &[]sdk.IPFailover{{ - Ip: ptr.To(exampleEndpointIP), - NicUuid: ptr.To(exampleNICID), + Ip: new(exampleEndpointIP), + NicUuid: new(exampleNICID), }}, } @@ -249,8 +248,8 @@ func (s *lanSuite) TestReconcileIPFailoverNICAlreadyInFailoverGroup() { testServer := s.defaultServer(s.infraMachine, exampleDHCPIP, exampleEndpointIP) testLAN := s.exampleLAN() testLAN.Properties.IpFailover = &[]sdk.IPFailover{{ - Ip: ptr.To(exampleEndpointIP), - NicUuid: ptr.To(exampleNICID), + Ip: new(exampleEndpointIP), + NicUuid: new(exampleNICID), }} s.mockGetServerCall(exampleServerID).Return(testServer, nil).Once() @@ -266,7 +265,7 @@ func (s *lanSuite) TestReconcileIPFailoverNICAlreadyInFailoverGroup() { func (s *lanSuite) TestReconcileIPFailoverForWorkerWithAUTOSettings() { const deploymentLabel = "test-deployment" s.infraMachine.SetLabels(map[string]string{clusterv1.MachineDeploymentNameLabel: deploymentLabel}) - s.infraMachine.Spec.FailoverIP = ptr.To(infrav1.CloudResourceConfigAuto) + s.infraMachine.Spec.FailoverIP = new(infrav1.CloudResourceConfigAuto) testServer := s.defaultServer(s.infraMachine, exampleDHCPIP, exampleWorkerFailoverIP) testLAN := s.exampleLAN() s.mockGetDatacenterLocationByIDCall(exampleDatacenterID).Return(exampleLocation, nil) @@ -293,8 +292,8 @@ func (s *lanSuite) TestReconcileIPFailoverForWorkerWithAUTOSettings() { s.mockGetLANPatchRequestCall().Return([]sdk.Request{patchRequest}, nil).Once() s.mockPatchLANCall(sdk.LanProperties{ IpFailover: &[]sdk.IPFailover{{ - NicUuid: ptr.To(exampleNICID), - Ip: ptr.To(exampleWorkerFailoverIP), + NicUuid: new(exampleNICID), + Ip: new(exampleWorkerFailoverIP), }}, }).Return(*patchRequest.GetMetadata().GetRequestStatus().GetHref(), nil).Once() s.mockWaitForRequestCall(*patchRequest.GetMetadata().GetRequestStatus().GetHref()).Return(nil).Once() @@ -305,7 +304,7 @@ func (s *lanSuite) TestReconcileIPFailoverForWorkerWithAUTOSettings() { func (s *lanSuite) TestReconcileIPFailoverReserveIPBlock() { const deploymentLabel = "test-deployment" s.infraMachine.SetLabels(map[string]string{clusterv1.MachineDeploymentNameLabel: deploymentLabel}) - s.infraMachine.Spec.FailoverIP = ptr.To(infrav1.CloudResourceConfigAuto) + s.infraMachine.Spec.FailoverIP = new(infrav1.CloudResourceConfigAuto) s.mockGetDatacenterLocationByIDCall(exampleDatacenterID).Return(exampleLocation, nil).Once() s.mockListIPBlocksCall().Return(nil, nil).Once() @@ -327,12 +326,12 @@ func (s *lanSuite) TestReconcileIPFailoverNICHasWrongIPInFailoverGroup() { testLAN.Properties.IpFailover = &[]sdk.IPFailover{{ // we expect that the first entry will also be included in the Patch request. - Ip: ptr.To(exampleArbitraryIP), - NicUuid: ptr.To(arbitraryNICID), + Ip: new(exampleArbitraryIP), + NicUuid: new(arbitraryNICID), }, { // LAN contains the NIC but has an incorrect IP - Ip: ptr.To(exampleUnexpectedIP), - NicUuid: ptr.To(exampleNICID), + Ip: new(exampleUnexpectedIP), + NicUuid: new(exampleNICID), }} s.mockGetServerCall(exampleServerID).Return(testServer, nil).Once() @@ -342,11 +341,11 @@ func (s *lanSuite) TestReconcileIPFailoverNICHasWrongIPInFailoverGroup() { // expect a patch request to update the IP props := sdk.LanProperties{ IpFailover: &[]sdk.IPFailover{{ - Ip: ptr.To(exampleArbitraryIP), - NicUuid: ptr.To(arbitraryNICID), + Ip: new(exampleArbitraryIP), + NicUuid: new(arbitraryNICID), }, { - Ip: ptr.To(exampleEndpointIP), - NicUuid: ptr.To(exampleNICID), + Ip: new(exampleEndpointIP), + NicUuid: new(exampleNICID), }}, } @@ -369,12 +368,12 @@ func (s *lanSuite) TestReconcileIPFailoverAnotherNICInFailoverGroup() { testLAN := s.exampleLAN() testLAN.Properties.IpFailover = &[]sdk.IPFailover{{ - Ip: ptr.To(exampleArbitraryIP), - NicUuid: ptr.To(arbitraryNICID), + Ip: new(exampleArbitraryIP), + NicUuid: new(arbitraryNICID), }, { - Ip: ptr.To(exampleEndpointIP), + Ip: new(exampleEndpointIP), // arbitrary NIC ID with the correct endpoint IP - NicUuid: ptr.To("f3b3f8e4-1f3d-11ec-82a8-0242ac130003"), + NicUuid: new("f3b3f8e4-1f3d-11ec-82a8-0242ac130003"), }} s.mockGetServerCall(exampleServerID).Return(testServer, nil).Once() @@ -401,8 +400,8 @@ func (s *lanSuite) reconcileIPFailoverDeletion(testServer *sdk.Server, testLAN s props := sdk.LanProperties{ IpFailover: &[]sdk.IPFailover{{ - Ip: ptr.To(exampleArbitraryIP), - NicUuid: ptr.To(arbitraryNICID), + Ip: new(exampleArbitraryIP), + NicUuid: new(arbitraryNICID), }}, } @@ -416,18 +415,18 @@ func (s *lanSuite) TestReconcileIPFailoverDeletionWorker() { labels[clusterv1.MachineDeploymentNameLabel] = deploymentLabel s.infraMachine.SetLabels(labels) - s.infraMachine.Spec.FailoverIP = ptr.To(infrav1.CloudResourceConfigAuto) + s.infraMachine.Spec.FailoverIP = new(infrav1.CloudResourceConfigAuto) testServer := s.defaultServer(s.infraMachine, exampleDHCPIP, exampleWorkerFailoverIP) s.NoError(s.k8sClient.Update(s.ctx, s.infraMachine)) testLAN := s.exampleLAN() testLAN.Properties.IpFailover = &[]sdk.IPFailover{{ - Ip: ptr.To(exampleArbitraryIP), - NicUuid: ptr.To(arbitraryNICID), + Ip: new(exampleArbitraryIP), + NicUuid: new(arbitraryNICID), }, { - Ip: ptr.To(exampleWorkerFailoverIP), - NicUuid: ptr.To(exampleNICID), + Ip: new(exampleWorkerFailoverIP), + NicUuid: new(exampleNICID), }} s.assertSuccessfulDeletion(s.reconcileIPFailoverDeletion(testServer, testLAN)) @@ -444,11 +443,11 @@ func (s *lanSuite) TestReconcileIPFailoverDeletionControlPlane() { testLAN := s.exampleLAN() testLAN.Properties.IpFailover = &[]sdk.IPFailover{{ - Ip: ptr.To(exampleArbitraryIP), - NicUuid: ptr.To(arbitraryNICID), + Ip: new(exampleArbitraryIP), + NicUuid: new(arbitraryNICID), }, { - Ip: ptr.To(exampleEndpointIP), - NicUuid: ptr.To(exampleNICID), + Ip: new(exampleEndpointIP), + NicUuid: new(exampleNICID), }} s.assertSuccessfulDeletion(s.reconcileIPFailoverDeletion(testServer, testLAN)) @@ -465,23 +464,23 @@ func (s *lanSuite) TestReconcileIPFailoverDeletionControlPlaneSwitchNIC() { newIonosMachine.SetName("test-machine-2") newIonosMachine.SetResourceVersion("") newIonosMachine.SetCreationTimestamp(metav1.NewTime(time.Now())) - newIonosMachine.Spec.ProviderID = ptr.To("ionos://" + exampleSecondaryServerID) + newIonosMachine.Spec.ProviderID = new("ionos://" + exampleSecondaryServerID) err = s.k8sClient.Create(s.ctx, newIonosMachine) s.NoError(err) testLAN := s.exampleLAN() testLAN.Properties.IpFailover = &[]sdk.IPFailover{{ - Ip: ptr.To(exampleArbitraryIP), - NicUuid: ptr.To(arbitraryNICID), + Ip: new(exampleArbitraryIP), + NicUuid: new(arbitraryNICID), }, { - Ip: ptr.To(exampleEndpointIP), - NicUuid: ptr.To(exampleNICID), + Ip: new(exampleEndpointIP), + NicUuid: new(exampleNICID), }} testServer := s.defaultServer(s.infraMachine, exampleDHCPIP, exampleEndpointIP) testSecondaryServer := s.defaultServer(newIonosMachine, exampleSecondaryDHCPIP, exampleEndpointIP) - testSecondaryServer.Id = ptr.To(exampleSecondaryServerID) - (*testSecondaryServer.Entities.Nics.Items)[0].Id = ptr.To(exampleSecondaryNICID) + testSecondaryServer.Id = new(exampleSecondaryServerID) + (*testSecondaryServer.Entities.Nics.Items)[0].Id = new(exampleSecondaryNICID) s.mockGetServerCall(exampleServerID).Return(testServer, nil).Once() s.mockGetServerCall(exampleSecondaryServerID).Return(testSecondaryServer, nil).Once() @@ -491,11 +490,11 @@ func (s *lanSuite) TestReconcileIPFailoverDeletionControlPlaneSwitchNIC() { props := sdk.LanProperties{ IpFailover: &[]sdk.IPFailover{{ - Ip: ptr.To(exampleArbitraryIP), - NicUuid: ptr.To(arbitraryNICID), + Ip: new(exampleArbitraryIP), + NicUuid: new(arbitraryNICID), }, { - Ip: ptr.To(exampleEndpointIP), - NicUuid: ptr.To(exampleSecondaryNICID), + Ip: new(exampleEndpointIP), + NicUuid: new(exampleSecondaryNICID), }}, } @@ -511,7 +510,7 @@ func (s *lanSuite) TestReconcileFailoverDeletionWorkerNoSwap() { labels[clusterv1.MachineDeploymentNameLabel] = deploymentLabel s.infraMachine.SetLabels(labels) - s.infraMachine.Spec.FailoverIP = ptr.To(exampleWorkerFailoverIP) + s.infraMachine.Spec.FailoverIP = new(exampleWorkerFailoverIP) secondaryMachine := s.infraMachine.DeepCopy() secondaryMachine.SetName("test-machine-2") @@ -524,11 +523,11 @@ func (s *lanSuite) TestReconcileFailoverDeletionWorkerNoSwap() { testLAN := s.exampleLAN() testLAN.Properties.IpFailover = &[]sdk.IPFailover{{ - Ip: ptr.To(exampleArbitraryIP), - NicUuid: ptr.To(arbitraryNICID), + Ip: new(exampleArbitraryIP), + NicUuid: new(arbitraryNICID), }, { - Ip: ptr.To(exampleWorkerFailoverIP), - NicUuid: ptr.To(exampleSecondaryNICID), + Ip: new(exampleWorkerFailoverIP), + NicUuid: new(exampleSecondaryNICID), }} s.mockGetServerCall(exampleServerID).Return(testServer, nil).Once() @@ -580,7 +579,7 @@ func (s *lanSuite) TestReconcileIPFailoverDeletionPrimaryNICNotFound() { s.NoError(err) testServer := &sdk.Server{ - Id: ptr.To(exampleServerID), + Id: new(exampleServerID), } s.mockGetServerCall(exampleServerID).Return(testServer, nil).Once() @@ -600,14 +599,14 @@ func (s *lanSuite) TestReconcileIPFailoverDeletionOnWorker() { func (s *lanSuite) exampleIPBlock() sdk.IpBlock { return sdk.IpBlock{ - Id: ptr.To(exampleIPBlockID), + Id: new(exampleIPBlockID), Properties: &sdk.IpBlockProperties{ Ips: &[]string{exampleWorkerFailoverIP}, - Location: ptr.To(s.clusterScope.Location()), - Name: ptr.To(s.service.failoverIPBlockName(s.machineScope)), + Location: new(s.clusterScope.Location()), + Name: new(s.service.failoverIPBlockName(s.machineScope)), }, Metadata: &sdk.DatacenterElementMetadata{ - State: ptr.To(sdk.Available), + State: new(sdk.Available), }, } } @@ -651,9 +650,9 @@ func (s *lanSuite) examplePatchRequest(status string) sdk.Request { func (s *lanSuite) mockCreateLANCall() *clienttest.MockClient_CreateLAN_Call { return s.ionosClient.EXPECT().CreateLAN(s.ctx, s.machineScope.DatacenterID(), sdk.LanProperties{ - Name: ptr.To(s.service.lanName(s.clusterScope.Cluster)), - Public: ptr.To(true), - Ipv6CidrBlock: ptr.To("AUTO"), + Name: new(s.service.lanName(s.clusterScope.Cluster)), + Public: new(true), + Ipv6CidrBlock: new("AUTO"), }) } diff --git a/internal/service/cloud/request.go b/internal/service/cloud/request.go index 54a2751a..855599f8 100644 --- a/internal/service/cloud/request.go +++ b/internal/service/cloud/request.go @@ -120,7 +120,7 @@ func getMatchingRequest[T any]( } // As we later on ignore query parameters in found requests, we need to do the same here for consistency. - urlWithoutQueryParams := strings.Split(url, "?")[0] + urlWithoutQueryParams, _, _ := strings.Cut(url, "?") requests, err := s.ionosClient.GetRequests(ctx, method, urlWithoutQueryParams) if err != nil { @@ -142,7 +142,7 @@ requestLoop: // The reason for comparing here at all is that the received requests can contain some that only contain the // desired URL as substring, e.g. a request for /resource/123/action will also be returned when looking // for /resource/123. We want to ignore those. - trimmedRequestURL := strings.Split(*req.GetProperties().GetUrl(), "?")[0] + trimmedRequestURL, _, _ := strings.Cut(*req.GetProperties().GetUrl(), "?") trimmedRequestURL = strings.TrimSuffix(trimmedRequestURL, "/") trimmedURL := strings.TrimSuffix(urlWithoutQueryParams, "/") if !strings.HasSuffix(trimmedRequestURL, trimmedURL) { diff --git a/internal/service/cloud/request_test.go b/internal/service/cloud/request_test.go index ee726ee3..6e5bfed0 100644 --- a/internal/service/cloud/request_test.go +++ b/internal/service/cloud/request_test.go @@ -28,7 +28,6 @@ import ( "github.com/stretchr/testify/suite" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud/clienttest" - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" ) const ( @@ -41,11 +40,11 @@ func TestMatcher_MatchByName(t *testing.T) { require.False(t, matchByNameFunc(&sdk.Server{}, sdk.Request{})) testServer := sdk.Server{ Properties: &sdk.ServerProperties{ - Name: ptr.To("test"), + Name: new("test"), }, } require.True(t, matchByNameFunc(&testServer, sdk.Request{})) - testServer.Properties.Name = ptr.To("wrong") + testServer.Properties.Name = new("wrong") require.False(t, matchByNameFunc(&testServer, sdk.Request{})) // l := (&sdk.Info{}).GetName() @@ -63,8 +62,8 @@ func TestGetRequestStatusTestSuite(t *testing.T) { func (s *getRequestStatusSuite) TestGetRequestStatusMissingMetadata() { s.mockCheckRequestStatusCall(baseTestURL).Return(&sdk.RequestStatus{ - Href: ptr.To(baseTestURL), - Id: ptr.To("12345"), + Href: new(baseTestURL), + Id: new("12345"), Metadata: nil, }, nil).Once() @@ -85,11 +84,11 @@ func (s *getRequestStatusSuite) TestGetRequestStatusMissingMetadata() { func (s *getRequestStatusSuite) TestGetRequestStatus() { s.mockCheckRequestStatusCall(baseTestURL).Return(&sdk.RequestStatus{ - Href: ptr.To(baseTestURL), - Id: ptr.To("12345"), + Href: new(baseTestURL), + Id: new("12345"), Metadata: &sdk.RequestStatusMetadata{ - Status: ptr.To(sdk.RequestStatusFailed), - Message: ptr.To("Failed to do foo and bar"), + Status: new(sdk.RequestStatusFailed), + Message: new("Failed to do foo and bar"), }, }, nil).Once() @@ -99,10 +98,10 @@ func (s *getRequestStatusSuite) TestGetRequestStatus() { s.Equal("Failed to do foo and bar", message, "message should be 'Failed to do foo and bar'") s.mockCheckRequestStatusCall(baseTestURL).Return(&sdk.RequestStatus{ - Href: ptr.To(baseTestURL), - Id: ptr.To("12345"), + Href: new(baseTestURL), + Id: new("12345"), Metadata: &sdk.RequestStatusMetadata{ - Status: ptr.To(sdk.RequestStatusQueued), + Status: new(sdk.RequestStatusQueued), Message: nil, }, }, nil).Once() @@ -155,7 +154,7 @@ func (s *getMatchingRequestSuite) TestUnsupportedResourceType() { func (s *getMatchingRequestSuite) TestMatching() { // req1 has a mismatch in its target type req1 := s.examplePostRequest("req1", sdk.RequestStatusQueued) - (*req1.Metadata.RequestStatus.Metadata.Targets)[0].Target.Type = ptr.To(sdk.SERVER) + (*req1.Metadata.RequestStatus.Metadata.Targets)[0].Target.Type = new(sdk.SERVER) // req2 has a mismatch in its URL req2 := s.examplePostRequest("req2", sdk.RequestStatusQueued) @@ -205,12 +204,12 @@ func TestHasRequestTargetType(t *testing.T) { req.Metadata.RequestStatus.Metadata.Targets = &[]sdk.RequestTarget{ { - Target: &sdk.ResourceReference{Type: ptr.To(sdk.SERVER)}, + Target: &sdk.ResourceReference{Type: new(sdk.SERVER)}, }, } require.False(t, hasRequestTargetType(req, sdk.LAN)) - (*req.Metadata.RequestStatus.Metadata.Targets)[0].Target.Type = ptr.To(sdk.LAN) + (*req.Metadata.RequestStatus.Metadata.Targets)[0].Target.Type = new(sdk.LAN) require.True(t, hasRequestTargetType(req, sdk.LAN)) } @@ -225,7 +224,7 @@ func TestFindResourceTestSuite(t *testing.T) { func (s *findResourceSuite) TestListingIsEnough() { resource, request, err := findResource( s.ctx, - func(_ context.Context) (*int, error) { return ptr.To(42), nil }, + func(_ context.Context) (*int, error) { return new(42), nil }, func(_ context.Context) (*requestInfo, error) { panic("don't call me") }, ) s.NoError(err) @@ -257,7 +256,7 @@ func (s *findResourceSuite) TestFoundOnSecondListing() { if listCalls == 1 { return nil, nil } - return ptr.To(42), nil + return new(42), nil }, func(_ context.Context) (*requestInfo, error) { return &requestInfo{status: sdk.RequestStatusDone}, nil }, ) @@ -322,8 +321,8 @@ func TestRequestInfo(t *testing.T) { } func TestMetadataHolder(t *testing.T) { - lan1 := &sdk.Lan{Metadata: &sdk.DatacenterElementMetadata{State: ptr.To("BUSY")}} - lan2 := &sdk.Lan{Metadata: &sdk.DatacenterElementMetadata{State: ptr.To(sdk.Available)}} + lan1 := &sdk.Lan{Metadata: &sdk.DatacenterElementMetadata{State: new("BUSY")}} + lan2 := &sdk.Lan{Metadata: &sdk.DatacenterElementMetadata{State: new(sdk.Available)}} require.False(t, isAvailable(getState(lan1))) require.True(t, isAvailable(getState(lan2))) diff --git a/internal/service/cloud/server.go b/internal/service/cloud/server.go index 24335abb..3c08ce8d 100644 --- a/internal/service/cloud/server.go +++ b/internal/service/cloud/server.go @@ -145,7 +145,7 @@ func (s *Service) ReconcileServerDeletion(ctx context.Context, ms *scope.Machine // FinalizeMachineProvisioning marks the machine as provisioned. func (*Service) FinalizeMachineProvisioning(_ context.Context, ms *scope.Machine) (bool, error) { - ms.IonosMachine.Status.Initialization.Provisioned = ptr.To(true) + ms.IonosMachine.Status.Initialization.Provisioned = new(true) // Set deprecated v1beta1 ready field for backwards compatibility. if ms.IonosMachine.Status.Deprecated == nil { ms.IonosMachine.Status.Deprecated = &infrav1.IonosCloudMachineDeprecatedStatus{} @@ -386,12 +386,12 @@ func (*Service) buildServerProperties( ms *scope.Machine, machineSpec *infrav1.IonosCloudMachineSpec, ) sdk.ServerProperties { props := sdk.ServerProperties{ - AvailabilityZone: ptr.To(machineSpec.AvailabilityZone.String()), + AvailabilityZone: new(machineSpec.AvailabilityZone.String()), Cores: &machineSpec.NumCores, - Name: ptr.To(ms.IonosMachine.Name), + Name: new(ms.IonosMachine.Name), Ram: &machineSpec.MemoryMB, CpuFamily: machineSpec.CPUFamily, - Type: ptr.To(machineSpec.Type.String()), + Type: new(machineSpec.Type.String()), } return props @@ -409,10 +409,10 @@ func (s *Service) buildServerEntities(ms *scope.Machine, params serverEntityPara machineSpec := params.machineSpec bootVolume := sdk.Volume{ Properties: &sdk.VolumeProperties{ - AvailabilityZone: ptr.To(machineSpec.Disk.AvailabilityZone.String()), - Name: ptr.To(s.volumeName(ms.IonosMachine)), - Size: ptr.To(float32(machineSpec.Disk.SizeGB)), - Type: ptr.To(machineSpec.Disk.DiskType.String()), + AvailabilityZone: new(machineSpec.Disk.AvailabilityZone.String()), + Name: new(s.volumeName(ms.IonosMachine)), + Size: new(float32(machineSpec.Disk.SizeGB)), + Type: new(machineSpec.Disk.DiskType.String()), UserData: ¶ms.boostrapData, }, } @@ -428,15 +428,15 @@ func (s *Service) buildServerEntities(ms *scope.Machine, params serverEntityPara primaryNIC := sdk.Nic{ Properties: &sdk.NicProperties{ Lan: ¶ms.lanID, - Name: ptr.To(s.nicName(ms.IonosMachine)), - Dhcp: ptr.To(true), + Name: new(s.nicName(ms.IonosMachine)), + Dhcp: new(true), }, } if ms.IonosMachine.Status.MachineNetworkInfo != nil { nicInfo := ms.IonosMachine.Status.MachineNetworkInfo.NICInfo[0] - primaryNIC.Properties.Ips = ptr.To(nicInfo.IPv4Addresses) - primaryNIC.Properties.Ipv6Ips = ptr.To(nicInfo.IPv6Addresses) + primaryNIC.Properties.Ips = new(nicInfo.IPv4Addresses) + primaryNIC.Properties.Ipv6Ips = new(nicInfo.IPv6Addresses) } // In case we want to retrieve a public IP from the DHCP, we need to @@ -462,8 +462,8 @@ func (s *Service) buildServerEntities(ms *scope.Machine, params serverEntityPara if ms.IonosMachine.Status.MachineNetworkInfo != nil { nicInfo := ms.IonosMachine.Status.MachineNetworkInfo.NICInfo[i+1] - nic.Properties.Ips = ptr.To(nicInfo.IPv4Addresses) - nic.Properties.Ipv6Ips = ptr.To(nicInfo.IPv6Addresses) + nic.Properties.Ips = new(nicInfo.IPv4Addresses) + nic.Properties.Ipv6Ips = new(nicInfo.IPv6Addresses) } items = append(items, nic) diff --git a/internal/service/cloud/server_test.go b/internal/service/cloud/server_test.go index 7799e4c7..a118db4c 100644 --- a/internal/service/cloud/server_test.go +++ b/internal/service/cloud/server_test.go @@ -53,7 +53,7 @@ func (s *serverSuite) TestReconcileServerNoBootstrapSecret() { s.True(requeue) s.Error(err) - s.machineScope.Machine.Spec.Bootstrap.DataSecretName = ptr.To("test") + s.machineScope.Machine.Spec.Bootstrap.DataSecretName = new("test") requeue, err = s.service.ReconcileServer(s.ctx, s.machineScope) s.False(requeue) s.NoError(err) @@ -74,10 +74,10 @@ func (s *serverSuite) TestReconcileServerRequestDoneStateBusy() { s.mockListServersCall().Return(&sdk.Servers{Items: &[]sdk.Server{ { Metadata: &sdk.DatacenterElementMetadata{ - State: ptr.To(sdk.Busy), + State: new(sdk.Busy), }, Properties: &sdk.ServerProperties{ - Name: ptr.To(s.infraMachine.Name), + Name: new(s.infraMachine.Name), }, }, }}, nil).Once() @@ -93,22 +93,22 @@ func (s *serverSuite) TestReconcileServerRequestDoneStateAvailable() { s.mockListServersCall().Return(&sdk.Servers{Items: &[]sdk.Server{ { Metadata: &sdk.DatacenterElementMetadata{ - State: ptr.To(sdk.Available), + State: new(sdk.Available), }, Properties: &sdk.ServerProperties{ - Name: ptr.To(s.infraMachine.Name), - VmState: ptr.To("RUNNING"), + Name: new(s.infraMachine.Name), + VmState: new("RUNNING"), }, Entities: &sdk.ServerEntities{ Nics: &sdk.Nics{ Items: &[]sdk.Nic{{ Properties: &sdk.NicProperties{ - Name: ptr.To(s.service.nicName(s.infraMachine)), - Dhcp: ptr.To(true), - Lan: ptr.To(int32(1)), - Ips: ptr.To([]string{"198.51.100.10"}), - Ipv6CidrBlock: ptr.To("2001:db8:2c0:301::/64"), - Ipv6Ips: ptr.To([]string{"2001:db8:2c0:301::1"}), + Name: new(s.service.nicName(s.infraMachine)), + Dhcp: new(true), + Lan: new(int32(1)), + Ips: new([]string{"198.51.100.10"}), + Ipv6CidrBlock: new("2001:db8:2c0:301::/64"), + Ipv6Ips: new([]string{"2001:db8:2c0:301::1"}), }, }}, }, @@ -132,13 +132,13 @@ func (s *serverSuite) TestReconcileServerRequestDoneStateAvailableTurnedOff() { s.mockGetServerCreationRequestCall().Return([]sdk.Request{s.examplePostRequest(sdk.RequestStatusDone)}, nil) s.mockListServersCall().Return(&sdk.Servers{Items: &[]sdk.Server{ { - Id: ptr.To(exampleServerID), + Id: new(exampleServerID), Metadata: &sdk.DatacenterElementMetadata{ - State: ptr.To(sdk.Available), + State: new(sdk.Available), }, Properties: &sdk.ServerProperties{ - Name: ptr.To(s.infraMachine.Name), - VmState: ptr.To("SHUTOFF"), + Name: new(s.infraMachine.Name), + VmState: new("SHUTOFF"), }, }, }}, nil).Once() @@ -153,35 +153,35 @@ func (s *serverSuite) TestReconcileServerRequestDoneStateAvailableTurnedOff() { func (s *serverSuite) TestReconcileServerAdditionalNetworks() { s.machineScope.IonosMachine.Spec.AdditionalNetworks = []infrav1.Network{{ NetworkID: 43, - DHCP: ptr.To(false), + DHCP: new(false), }, { NetworkID: 44, - DHCP: ptr.To(true), + DHCP: new(true), }} s.prepareReconcileServerRequestTest() s.mockGetServerCreationRequestCall().Return([]sdk.Request{}, nil) s.mockListLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{{ - Id: ptr.To(exampleLANID), + Id: new(exampleLANID), Properties: &sdk.LanProperties{ - Name: ptr.To(s.service.lanName(s.clusterScope.Cluster)), - Public: ptr.To(true), + Name: new(s.service.lanName(s.clusterScope.Cluster)), + Public: new(true), }, }}}, nil) properties, entities := s.defaultServerComponents() *entities.Nics.Items = append(*entities.Nics.Items, sdk.Nic{ Properties: &sdk.NicProperties{ - Lan: ptr.To[int32](43), - Dhcp: ptr.To(false), + Lan: new(int32(43)), + Dhcp: new(false), }, }, sdk.Nic{ Properties: &sdk.NicProperties{ - Lan: ptr.To[int32](44), - Dhcp: ptr.To(true), + Lan: new(int32(44)), + Dhcp: new(true), }, }) - s.mockCreateServerCall(properties, entities).Return(&sdk.Server{Id: ptr.To("12345")}, "location/to/server", nil) + s.mockCreateServerCall(properties, entities).Return(&sdk.Server{Id: new("12345")}, "location/to/server", nil) requeue, err := s.service.ReconcileServer(s.ctx, s.machineScope) s.Equal("ionos://12345", ptr.Deref(s.machineScope.IonosMachine.Spec.ProviderID, "")) @@ -192,12 +192,12 @@ func (s *serverSuite) TestReconcileServerAdditionalNetworks() { func (s *serverSuite) TestReconcileEnterpriseServerNoRequest() { s.prepareReconcileServerRequestTest() s.mockGetServerCreationRequestCall().Return([]sdk.Request{}, nil) - s.mockCreateServerCall(s.defaultServerComponents()).Return(&sdk.Server{Id: ptr.To("12345")}, "location/to/server", nil) + s.mockCreateServerCall(s.defaultServerComponents()).Return(&sdk.Server{Id: new("12345")}, "location/to/server", nil) s.mockListLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{{ - Id: ptr.To(exampleLANID), + Id: new(exampleLANID), Properties: &sdk.LanProperties{ - Name: ptr.To(s.service.lanName(s.clusterScope.Cluster)), - Public: ptr.To(true), + Name: new(s.service.lanName(s.clusterScope.Cluster)), + Public: new(true), }, }}}, nil) @@ -212,13 +212,13 @@ func (s *serverSuite) TestReconcileVCPUServerNoRequest() { s.mockGetServerCreationRequestCall().Return([]sdk.Request{}, nil) props, entities := s.defaultServerComponents() - props.Type = ptr.To(infrav1.ServerTypeVCPU.String()) - s.mockCreateServerCall(props, entities).Return(&sdk.Server{Id: ptr.To("12345")}, "location/to/server", nil) + props.Type = new(infrav1.ServerTypeVCPU.String()) + s.mockCreateServerCall(props, entities).Return(&sdk.Server{Id: new("12345")}, "location/to/server", nil) s.mockListLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{{ - Id: ptr.To(exampleLANID), + Id: new(exampleLANID), Properties: &sdk.LanProperties{ - Name: ptr.To(s.service.lanName(s.clusterScope.Cluster)), - Public: ptr.To(true), + Name: new(s.service.lanName(s.clusterScope.Cluster)), + Public: new(true), }, }}}, nil) @@ -243,14 +243,14 @@ func (s *serverSuite) prepareReconcileServerRequestTest() { s.NoError(s.k8sClient.Create(s.ctx, bootstrapSecret)) - s.machineScope.Machine.Spec.Bootstrap.DataSecretName = ptr.To("test") + s.machineScope.Machine.Spec.Bootstrap.DataSecretName = new("test") s.machineScope.IonosMachine.Spec.ProviderID = nil s.mockListServersCall().Return(&sdk.Servers{Items: &[]sdk.Server{}}, nil).Once() } func (s *serverSuite) TestReconcileServerDeletion() { s.mockGetServerCall(exampleServerID).Return(&sdk.Server{ - Id: ptr.To(exampleServerID), + Id: new(exampleServerID), }, nil) reqLocation := "delete/location" @@ -264,16 +264,16 @@ func (s *serverSuite) TestReconcileServerDeletion() { func (s *serverSuite) TestReconcileServerDeletionDeleteBootVolume() { s.mockGetServerCall(exampleServerID).Return(&sdk.Server{ - Id: ptr.To(exampleServerID), + Id: new(exampleServerID), Properties: &sdk.ServerProperties{ BootVolume: &sdk.ResourceReference{ - Id: ptr.To(exampleBootVolumeID), + Id: new(exampleBootVolumeID), }, }, }, nil).Once() s.mockGetServerCall(exampleServerID).Return(&sdk.Server{ - Id: ptr.To(exampleServerID), + Id: new(exampleServerID), }, nil).Once() reqLocationVolume := "delete/location/volume" @@ -292,12 +292,12 @@ func (s *serverSuite) TestReconcileServerDeletionDeleteBootVolume() { } func (s *serverSuite) TestReconcileServerDeletionDeleteAllVolumes() { - s.clusterScope.Cluster.DeletionTimestamp = ptr.To(metav1.Now()) + s.clusterScope.Cluster.DeletionTimestamp = new(metav1.Now()) s.mockGetServerCall(exampleServerID).Return(&sdk.Server{ - Id: ptr.To(exampleServerID), + Id: new(exampleServerID), Properties: &sdk.ServerProperties{ BootVolume: &sdk.ResourceReference{ - Id: ptr.To(exampleBootVolumeID), + Id: new(exampleBootVolumeID), }, }, }, nil).Once() @@ -358,7 +358,7 @@ func (s *serverSuite) TestReconcileServerDeletionCreateRequestPending() { func (s *serverSuite) TestReconcileServerDeletionRequestPending() { s.mockGetServerCall(exampleServerID).Return(&sdk.Server{ - Id: ptr.To(exampleServerID), + Id: new(exampleServerID), }, nil) exampleRequest := s.exampleDeleteRequest(sdk.RequestStatusQueued, exampleServerID) @@ -376,7 +376,7 @@ func (s *serverSuite) TestReconcileServerDeletionRequestPending() { func (s *serverSuite) TestReconcileServerDeletionRequestDone() { s.mockGetServerCall(exampleServerID).Return(&sdk.Server{ - Id: ptr.To(exampleServerID), + Id: new(exampleServerID), }, nil) exampleRequest := s.exampleDeleteRequest(sdk.RequestStatusDone, exampleServerID) @@ -392,7 +392,7 @@ func (s *serverSuite) TestReconcileServerDeletionRequestDone() { func (s *serverSuite) TestReconcileServerDeletionRequestFailed() { s.mockGetServerCall(exampleServerID).Return(&sdk.Server{ - Id: ptr.To(exampleServerID), + Id: new(exampleServerID), }, nil) exampleRequest := s.exampleDeleteRequest(sdk.RequestStatusFailed, exampleServerID) @@ -436,7 +436,7 @@ func (s *serverSuite) TestGetServerWithoutProviderIDFoundInList() { s.mockListServersCall().Return(&sdk.Servers{Items: &[]sdk.Server{ { Properties: &sdk.ServerProperties{ - Name: ptr.To(s.infraMachine.Name), + Name: new(s.infraMachine.Name), }, }, }}, nil) @@ -449,15 +449,15 @@ func (s *serverSuite) TestGetServerWithoutProviderIDFoundInList() { //nolint:unused func (*serverSuite) exampleServer() sdk.Server { return sdk.Server{ - Id: ptr.To("1"), + Id: new("1"), Metadata: &sdk.DatacenterElementMetadata{ - State: ptr.To(sdk.Available), + State: new(sdk.Available), }, Properties: &sdk.ServerProperties{ - AvailabilityZone: ptr.To("AUTO"), + AvailabilityZone: new("AUTO"), BootVolume: &sdk.ResourceReference{ - Id: ptr.To("1"), - Type: ptr.To(sdk.VOLUME), + Id: new("1"), + Type: new(sdk.VOLUME), }, Name: nil, VmState: nil, diff --git a/internal/service/cloud/service.go b/internal/service/cloud/service.go index 4f408315..fe0862f4 100644 --- a/internal/service/cloud/service.go +++ b/internal/service/cloud/service.go @@ -62,8 +62,7 @@ func isNotFound(err error) bool { return false } - var target sdk.GenericOpenAPIError - if errors.As(err, &target) { + if target, ok := errors.AsType[sdk.GenericOpenAPIError](err); ok { return target.StatusCode() == http.StatusNotFound } diff --git a/internal/service/cloud/suite_test.go b/internal/service/cloud/suite_test.go index 4ab44891..3de47d00 100644 --- a/internal/service/cloud/suite_test.go +++ b/internal/service/cloud/suite_test.go @@ -39,7 +39,6 @@ import ( infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud/clienttest" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/locker" - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/scope" ) @@ -152,12 +151,12 @@ func (s *ServiceTestSuite) SetupTest() { }, }, Spec: infrav1.IonosCloudMachineSpec{ - ProviderID: ptr.To("ionos://" + exampleServerID), + ProviderID: new("ionos://" + exampleServerID), DatacenterID: "ccf27092-34e8-499e-a2f5-2bdee9d34a12", NumCores: 2, AvailabilityZone: infrav1.AvailabilityZoneAuto, MemoryMB: 4096, - CPUFamily: ptr.To("AMD_OPTERON"), + CPUFamily: new("AMD_OPTERON"), Disk: &infrav1.Volume{ Name: "test-machine-hdd", DiskType: infrav1.VolumeDiskTypeHDD, @@ -225,7 +224,7 @@ func (*ServiceTestSuite) exampleRequest(opts requestBuildOptions) sdk.Request { Href: &opts.href, Metadata: &sdk.RequestStatusMetadata{ Status: &opts.status, - Message: ptr.To("test"), + Message: new("test"), }, }, }, @@ -252,14 +251,14 @@ func (*ServiceTestSuite) exampleRequest(opts requestBuildOptions) sdk.Request { func (s *ServiceTestSuite) defaultServer(m *infrav1.IonosCloudMachine, ips ...string) *sdk.Server { return &sdk.Server{ - Id: ptr.To(exampleServerID), + Id: new(exampleServerID), Entities: &sdk.ServerEntities{ Nics: &sdk.Nics{ Items: &[]sdk.Nic{{ - Id: ptr.To(exampleNICID), + Id: new(exampleNICID), Properties: &sdk.NicProperties{ - Dhcp: ptr.To(true), - Name: ptr.To(s.service.nicName(m)), + Dhcp: new(true), + Name: new(s.service.nicName(m)), Ips: &ips, }, }}, @@ -289,12 +288,12 @@ func (s *ServiceTestSuite) buildIPBlockRequestWithName(name, status, method, id func (s *ServiceTestSuite) exampleLAN() sdk.Lan { return sdk.Lan{ - Id: ptr.To(exampleLANID), + Id: new(exampleLANID), Properties: &sdk.LanProperties{ - Name: ptr.To(s.service.lanName(s.clusterScope.Cluster)), + Name: new(s.service.lanName(s.clusterScope.Cluster)), }, Metadata: &sdk.DatacenterElementMetadata{ - State: ptr.To(sdk.Available), + State: new(sdk.Available), }, Entities: &sdk.LanEntities{ Nics: &sdk.LanNics{ @@ -307,12 +306,12 @@ func (s *ServiceTestSuite) exampleLAN() sdk.Lan { func (s *ServiceTestSuite) defaultServerComponents() (sdk.ServerProperties, sdk.ServerEntities) { m := s.machineScope.IonosMachine props := sdk.ServerProperties{ - AvailabilityZone: ptr.To(m.Spec.AvailabilityZone.String()), - Cores: ptr.To(m.Spec.NumCores), + AvailabilityZone: new(m.Spec.AvailabilityZone.String()), + Cores: new(m.Spec.NumCores), CpuFamily: m.Spec.CPUFamily, - Name: ptr.To(m.Name), - Ram: ptr.To(m.Spec.MemoryMB), - Type: ptr.To(m.Spec.Type.String()), + Name: new(m.Name), + Ram: new(m.Spec.MemoryMB), + Type: new(m.Spec.Type.String()), } lanID, _ := strconv.ParseInt(exampleLANID, 10, 32) @@ -320,19 +319,19 @@ func (s *ServiceTestSuite) defaultServerComponents() (sdk.ServerProperties, sdk. entities := sdk.ServerEntities{ Nics: &sdk.Nics{Items: &[]sdk.Nic{{ Properties: &sdk.NicProperties{ - Lan: ptr.To(int32(lanID)), - Name: ptr.To(s.service.nicName(m)), - Dhcp: ptr.To(true), + Lan: new(int32(lanID)), + Name: new(s.service.nicName(m)), + Dhcp: new(true), }, }}}, Volumes: &sdk.AttachedVolumes{ Items: &[]sdk.Volume{{ Properties: &sdk.VolumeProperties{ - AvailabilityZone: ptr.To(m.Spec.Disk.AvailabilityZone.String()), - Image: ptr.To(m.Spec.Disk.Image.ID), - Name: ptr.To(s.service.volumeName(m)), - Type: ptr.To(m.Spec.Disk.DiskType.String()), - Size: ptr.To(float32(m.Spec.Disk.SizeGB)), + AvailabilityZone: new(m.Spec.Disk.AvailabilityZone.String()), + Image: new(m.Spec.Disk.Image.ID), + Name: new(s.service.volumeName(m)), + Type: new(m.Spec.Disk.DiskType.String()), + Size: new(float32(m.Spec.Disk.SizeGB)), UserData: nil, }, }}, diff --git a/internal/service/k8s/ipam.go b/internal/service/k8s/ipam.go index bceb8fa3..9527d924 100644 --- a/internal/service/k8s/ipam.go +++ b/internal/service/k8s/ipam.go @@ -102,7 +102,8 @@ func (h *Helper) ReconcileIPAddressClaimsDeletion(ctx context.Context, machineSc log.V(4).Info("removing finalizers from IPAddressClaims.") formats := []string{ipV4Format, ipV6Format} - nicNames := []string{fmt.Sprintf(primaryNICFormat, machineScope.IonosMachine.Name)} + nicNames := make([]string, 0, 1+len(machineScope.IonosMachine.Spec.AdditionalNetworks)) + nicNames = append(nicNames, fmt.Sprintf(primaryNICFormat, machineScope.IonosMachine.Name)) for _, network := range machineScope.IonosMachine.Spec.AdditionalNetworks { nicName := fmt.Sprintf(additionalNICFormat, machineScope.IonosMachine.Name, network.NetworkID) diff --git a/internal/service/k8s/ipam_test.go b/internal/service/k8s/ipam_test.go index 615c7f6c..418da924 100644 --- a/internal/service/k8s/ipam_test.go +++ b/internal/service/k8s/ipam_test.go @@ -36,7 +36,6 @@ import ( "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud/clienttest" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/service/cloud" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/locker" - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/scope" ) @@ -112,12 +111,12 @@ func (s *IpamTestSuite) SetupTest() { }, }, Spec: infrav1.IonosCloudMachineSpec{ - ProviderID: ptr.To("ionos://dd426c63-cd1d-4c02-aca3-13b4a27c2ebf"), + ProviderID: new("ionos://dd426c63-cd1d-4c02-aca3-13b4a27c2ebf"), DatacenterID: "ccf27092-34e8-499e-a2f5-2bdee9d34a12", NumCores: 2, AvailabilityZone: infrav1.AvailabilityZoneAuto, MemoryMB: 4096, - CPUFamily: ptr.To("AMD_OPTERON"), + CPUFamily: new("AMD_OPTERON"), Disk: &infrav1.Volume{ Name: "test-machine-hdd", DiskType: infrav1.VolumeDiskTypeHDD, @@ -298,7 +297,7 @@ func (s *IpamTestSuite) TestReconcileIPAddressesAdditionalIpv6GetIPFromClaim() { func defaultInClusterIPv4PoolRef() *corev1.TypedLocalObjectReference { return &corev1.TypedLocalObjectReference{ - APIGroup: ptr.To("ipam.cluster.x-k8s.io"), + APIGroup: new("ipam.cluster.x-k8s.io"), Kind: "InClusterIPPool", Name: "incluster-ipv4-pool", } @@ -306,7 +305,7 @@ func defaultInClusterIPv4PoolRef() *corev1.TypedLocalObjectReference { func defaultInClusterIPv6PoolRef() *corev1.TypedLocalObjectReference { return &corev1.TypedLocalObjectReference{ - APIGroup: ptr.To("ipam.cluster.x-k8s.io"), + APIGroup: new("ipam.cluster.x-k8s.io"), Kind: "InClusterIPPool", Name: "incluster-ipv6-pool", } @@ -323,7 +322,7 @@ func defaultIPv4Address(claim *ipamv1.IPAddressClaim, poolRef *corev1.TypedLocal ClaimRef: ipamv1.IPAddressClaimReference{Name: claim.GetName()}, PoolRef: toIPPoolReference(poolRef), Address: "10.0.0.2", - Prefix: ptr.To[int32](16), + Prefix: new(int32(16)), }, } } @@ -339,7 +338,7 @@ func defaultIPv6Address(claim *ipamv1.IPAddressClaim, poolRef *corev1.TypedLocal ClaimRef: ipamv1.IPAddressClaimReference{Name: claim.GetName()}, PoolRef: toIPPoolReference(poolRef), Address: "2001:db8::", - Prefix: ptr.To[int32](42), + Prefix: new(int32(42)), }, } } diff --git a/internal/util/ptr/ptr.go b/internal/util/ptr/ptr.go index 0af076f7..a9fe3a28 100644 --- a/internal/util/ptr/ptr.go +++ b/internal/util/ptr/ptr.go @@ -21,11 +21,6 @@ limitations under the License. // Dependabot to create a new PR for a version update. package ptr -// To returns a pointer to the given value. -func To[T any](v T) *T { - return &v -} - // Deref attempts to dereference a pointer and return the value. // If the pointer is nil, the provided default value will be returned instead. func Deref[T any](ptr *T, def T) T { diff --git a/internal/util/ptr/ptr_test.go b/internal/util/ptr/ptr_test.go index 91371fd0..52f23298 100644 --- a/internal/util/ptr/ptr_test.go +++ b/internal/util/ptr/ptr_test.go @@ -22,13 +22,6 @@ import ( "github.com/stretchr/testify/require" ) -func Test_PtrTo(t *testing.T) { - type testType struct{} - - require.IsType(t, (*testType)(nil), To(testType{})) - require.IsType(t, (**testType)(nil), To((*testType)(nil))) -} - func Test_PtrDeref(t *testing.T) { type testType struct{} @@ -38,6 +31,6 @@ func Test_PtrDeref(t *testing.T) { require.IsType(t, &testType{}, Deref(&testTypeInstance, &testType{})) // validate that deref returns default when passing a nil value var nilTestType *testType - require.Equal(t, testType{}, Deref[testType](nilTestType, testType{})) - require.Equal(t, testType{}, Deref[testType](nil, testType{})) + require.Equal(t, testType{}, Deref(nilTestType, testType{})) + require.Equal(t, testType{}, Deref(nil, testType{})) } diff --git a/scope/machine.go b/scope/machine.go index 17398005..216a558c 100644 --- a/scope/machine.go +++ b/scope/machine.go @@ -119,7 +119,7 @@ func (m *Machine) DatacenterID() string { // SetProviderID sets the provider ID for the IonosCloudMachine. func (m *Machine) SetProviderID(id string) { - m.IonosMachine.Spec.ProviderID = ptr.To("ionos://" + id) + m.IonosMachine.Spec.ProviderID = new("ionos://" + id) } // CountMachines returns the number of existing IonosCloudMachines in the same namespace diff --git a/test/e2e/capic_test.go b/test/e2e/capic_test.go index bd64f0eb..2b724a10 100644 --- a/test/e2e/capic_test.go +++ b/test/e2e/capic_test.go @@ -27,7 +27,6 @@ import ( runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/test/e2e/helpers" . "github.com/onsi/ginkgo/v2" @@ -42,8 +41,8 @@ var _ = Describe("Quickstart: Should be able to create a cluster with 3 control- BootstrapClusterProxy: bootstrapClusterProxy, ArtifactFolder: artifactFolder, SkipCleanup: skipCleanup, - ControlPlaneMachineCount: ptr.To[int64](3), - WorkerMachineCount: ptr.To[int64](2), + ControlPlaneMachineCount: new(int64(3)), + WorkerMachineCount: new(int64(2)), PostNamespaceCreated: cloudEnv.createCredentialsSecretPNC, PostMachinesProvisioned: func(proxy framework.ClusterProxy, namespace, clusterName string) { // This check ensures that owner references are resilient - i.e. correctly re-reconciled - when removed. @@ -107,11 +106,11 @@ var _ = Describe("Should be able to create a cluster with 1 control-plane using capie2e.QuickStartSpec(ctx, func() capie2e.QuickStartSpecInput { return capie2e.QuickStartSpecInput{ E2EConfig: e2eConfig, - ControlPlaneMachineCount: ptr.To(int64(1)), - WorkerMachineCount: ptr.To(int64(0)), + ControlPlaneMachineCount: new(int64(1)), + WorkerMachineCount: new(int64(0)), ClusterctlConfigPath: clusterctlConfigPath, BootstrapClusterProxy: bootstrapClusterProxy, - Flavor: ptr.To("ipam"), + Flavor: new("ipam"), ArtifactFolder: artifactFolder, SkipCleanup: skipCleanup, PostNamespaceCreated: cloudEnv.createCredentialsSecretPNC, diff --git a/test/e2e/env_test.go b/test/e2e/env_test.go index 5142410f..06f47c7b 100644 --- a/test/e2e/env_test.go +++ b/test/e2e/env_test.go @@ -236,6 +236,6 @@ func (*ionosCloudEnv) writeToGithubOutput(key, value string) { Expect(err).ToNot(HaveOccurred(), "Failed opening GITHUB_OUTPUT file") defer func() { _ = f.Close() }() - _, err = f.WriteString(fmt.Sprintf("%s=%s\n", key, value)) + _, err = fmt.Fprintf(f, "%s=%s\n", key, value) Expect(err).ToNot(HaveOccurred(), "Failed writing to GITHUB_OUTPUT file") } diff --git a/test/e2e/helpers/finalizers.go b/test/e2e/helpers/finalizers.go index ffe1a87c..80d7e242 100644 --- a/test/e2e/helpers/finalizers.go +++ b/test/e2e/helpers/finalizers.go @@ -30,13 +30,13 @@ import ( // IonosCloudInfraFinalizersAssertion maps IONOS Cloud infrastructure resource types to their expected finalizers. var IonosCloudInfraFinalizersAssertion = map[string]func(types.NamespacedName) []string{ - "IonosCloudMachine": func(types.NamespacedName) []string { return []string{infrav1.MachineFinalizer} }, - "IonosCloudCluster": func(types.NamespacedName) []string { return []string{infrav1.ClusterFinalizer} }, + "IonosCloudMachine": func(types.NamespacedName) []string { return []string{infrav1.MachineFinalizer} }, + kindIonosCloudCluster: func(types.NamespacedName) []string { return []string{infrav1.ClusterFinalizer} }, } // ExpFinalizersAssertion maps experimental resource types to their expected finalizers. var ExpFinalizersAssertion = map[string]func(types.NamespacedName) []string{ - "ClusterResourceSet": func(types.NamespacedName) []string { return []string{addonsv1.ClusterResourceSetFinalizer} }, + kindClusterResourceSet: func(types.NamespacedName) []string { return []string{addonsv1.ClusterResourceSetFinalizer} }, } // KubernetesFinalizersAssertion maps Kubernetes resource types to their expected finalizers. @@ -46,7 +46,7 @@ func KubernetesFinalizersAssertion(clusters *infrav1.IonosCloudClusterList) map[ assertions := map[string]func(types.NamespacedName) []string{} if clusters != nil { - secretAssertions := make([]string, 0) + secretAssertions := make([]string, 0, len(clusters.Items)) for _, cluster := range clusters.Items { secretAssertions = append(secretAssertions, fmt.Sprintf("%s/%s", infrav1.ClusterFinalizer, cluster.GetUID())) } diff --git a/test/e2e/helpers/ownerreference.go b/test/e2e/helpers/ownerreference.go index 95a76510..04498767 100644 --- a/test/e2e/helpers/ownerreference.go +++ b/test/e2e/helpers/ownerreference.go @@ -28,7 +28,12 @@ import ( "sigs.k8s.io/cluster-api/test/framework" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" +) + +// Kind names of resource types this provider asserts on. +const ( + kindIonosCloudCluster = "IonosCloudCluster" + kindClusterResourceSet = "ClusterResourceSet" ) // Kinds and Owners for types in the core API package. @@ -36,24 +41,24 @@ var ( coreGroupVersion = clusterv1.GroupVersion.String() clusterOwner = metav1.OwnerReference{Kind: "Cluster", APIVersion: coreGroupVersion} - clusterController = metav1.OwnerReference{Kind: "Cluster", APIVersion: coreGroupVersion, Controller: ptr.To(true)} - machineController = metav1.OwnerReference{Kind: "Machine", APIVersion: coreGroupVersion, Controller: ptr.To(true)} + clusterController = metav1.OwnerReference{Kind: "Cluster", APIVersion: coreGroupVersion, Controller: new(true)} + machineController = metav1.OwnerReference{Kind: "Machine", APIVersion: coreGroupVersion, Controller: new(true)} ) -var ionosCloudClusterController = metav1.OwnerReference{Kind: "IonosCloudCluster", APIVersion: infrav1.GroupVersion.String(), Controller: ptr.To(false)} +var ionosCloudClusterController = metav1.OwnerReference{Kind: kindIonosCloudCluster, APIVersion: infrav1.GroupVersion.String(), Controller: new(false)} -var clusterResourceSetOwner = metav1.OwnerReference{Kind: "ClusterResourceSet", APIVersion: addonsv1.GroupVersion.String()} +var clusterResourceSetOwner = metav1.OwnerReference{Kind: kindClusterResourceSet, APIVersion: addonsv1.GroupVersion.String()} // Kind and Owners for types in the Kubeadm ControlPlane package. var ( kubeadmControlPlaneGroupVersion = controlplanev1.GroupVersion.String() - kubeadmControlPlaneController = metav1.OwnerReference{Kind: "KubeadmControlPlane", APIVersion: kubeadmControlPlaneGroupVersion, Controller: ptr.To(true)} + kubeadmControlPlaneController = metav1.OwnerReference{Kind: "KubeadmControlPlane", APIVersion: kubeadmControlPlaneGroupVersion, Controller: new(true)} ) // Owners and kinds for types in the Kubeadm Bootstrap package. var ( kubeadmConfigGroupVersion = bootstrapv1.GroupVersion.String() - kubeadmConfigController = metav1.OwnerReference{Kind: "KubeadmConfig", APIVersion: kubeadmConfigGroupVersion, Controller: ptr.To(true)} + kubeadmConfigController = metav1.OwnerReference{Kind: "KubeadmConfig", APIVersion: kubeadmConfigGroupVersion, Controller: new(true)} ) // IonosCloudInfraOwnerReferenceAssertions maps IONOS Cloud Infrastructure types to functions which return an error if the passed @@ -67,7 +72,7 @@ var IonosCloudInfraOwnerReferenceAssertions = map[string]func(types.NamespacedNa "IonosCloudMachineTemplate": func(_ types.NamespacedName, owners []metav1.OwnerReference) error { return framework.HasExactOwners(owners, clusterOwner) }, - "IonosCloudCluster": func(_ types.NamespacedName, owners []metav1.OwnerReference) error { + kindIonosCloudCluster: func(_ types.NamespacedName, owners []metav1.OwnerReference) error { // IonosCloudCluster must be owned and controlled by a Cluster. return framework.HasExactOwners(owners, clusterController) }, @@ -78,7 +83,7 @@ var IonosCloudInfraOwnerReferenceAssertions = map[string]func(types.NamespacedNa // Note: These relationships are documented in https://github.com/kubernetes-sigs/cluster-api/tree/main/docs/book/src/reference/owner_references.md. // That document should be updated if these references change. var ExpOwnerReferenceAssertions = map[string]func(types.NamespacedName, []metav1.OwnerReference) error{ - "ClusterResourceSet": func(_ types.NamespacedName, owners []metav1.OwnerReference) error { + kindClusterResourceSet: func(_ types.NamespacedName, owners []metav1.OwnerReference) error { // ClusterResourcesSet doesn't have ownerReferences (it is a clusterctl move-hierarchy root). return framework.HasExactOwners(owners) }, From 0ceace6989bade03272f5d4db27eda5428c2e2e1 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 12 Jun 2026 14:50:18 +0000 Subject: [PATCH 04/94] fix(ipam): guard against nil APIGroup when constructing IPPoolReference The CAPI v1.11 upgrade changed IPAddressClaimSpec.PoolRef from corev1.TypedLocalObjectReference to ipamv1.IPPoolReference. The new manual mapping silently left APIGroup as empty string when poolRef.APIGroup was nil. IPPoolReference.APIGroup is +required with MinLength=1; an empty value causes the API server to reject the IPAddressClaim with a CEL validation error, permanently stalling IP allocation for the affected machine with no clear pointer to the root cause. Fix: return an explicit error when APIGroup is nil so the reconcile loop surfaces the problem instead of producing an invalid API object. --- internal/service/k8s/ipam.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/service/k8s/ipam.go b/internal/service/k8s/ipam.go index 9527d924..97f363af 100644 --- a/internal/service/k8s/ipam.go +++ b/internal/service/k8s/ipam.go @@ -269,12 +269,13 @@ func (h *Helper) CreateIPAddressClaim(ctx context.Context, owner client.Object, return nil } - ipPoolRef := ipamv1.IPPoolReference{ - Name: poolRef.Name, - Kind: poolRef.Kind, + if poolRef.APIGroup == nil { + return fmt.Errorf("pool reference for IPAddressClaim %q has no APIGroup", claimRef.Name) } - if poolRef.APIGroup != nil { - ipPoolRef.APIGroup = *poolRef.APIGroup + ipPoolRef := ipamv1.IPPoolReference{ + Name: poolRef.Name, + Kind: poolRef.Kind, + APIGroup: *poolRef.APIGroup, } desired := &ipamv1.IPAddressClaim{ From abf853440f0483ee5dae61bc36bd3f3daa358049 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 12 Jun 2026 14:49:43 +0000 Subject: [PATCH 05/94] fix(scope): patch cluster object even when SetSummaryCondition returns an error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CAPI v1.11 upgrade replaced conditions.SetSummary (void return) with conditions.SetSummaryCondition (returns error). The new code returned that error immediately, skipping patchHelper.Patch entirely. The comment below the error check explicitly states the patch must happen even when reconciliation is aborted. Skipping it means any in-memory state changes made before PatchObject is called — deleted pending request entries, updated network info — are never flushed to the API server. On the next reconcile the controller reads stale data and may enter the wrong branch. Fix: capture the SetSummaryCondition error, always attempt the patch, and return the first non-nil error (patch error takes precedence since it means state was not persisted; the summary condition error is returned only if patching succeeded). --- scope/cluster.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/scope/cluster.go b/scope/cluster.go index 50814b15..055bb2bc 100644 --- a/scope/cluster.go +++ b/scope/cluster.go @@ -202,15 +202,14 @@ func (c *Cluster) DeleteCurrentRequestByDatacenter(datacenterID string) { // PatchObject will apply all changes from the IonosCloudCluster. // It will also make sure to patch the status subresource. func (c *Cluster) PatchObject() error { - // always set the ready condition summary - if err := conditions.SetSummaryCondition( + // always set the ready condition summary; capture the error but do not return early — + // the patch must be attempted regardless (see NOTE below). + summaryErr := conditions.SetSummaryCondition( c.IonosCluster, c.IonosCluster, string(clusterv1.ReadyCondition), conditions.ForConditionTypes{string(infrav1.IonosCloudClusterReady)}, - ); err != nil { - return err - } + ) // NOTE(piepmatz): We don't accept and forward a context here. This is on purpose: Even if a reconciliation is // aborted, we want to make sure that the final patch is applied. Reusing the context from the reconciliation @@ -218,7 +217,7 @@ func (c *Cluster) PatchObject() error { timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() - return c.patchHelper.Patch(timeoutCtx, c.IonosCluster, + if patchErr := c.patchHelper.Patch(timeoutCtx, c.IonosCluster, patch.WithOwnedV1Beta1Conditions{ Conditions: []clusterv1.ConditionType{clusterv1.ReadyCondition}, }, @@ -228,7 +227,10 @@ func (c *Cluster) PatchObject() error { string(infrav1.IonosCloudClusterReady), }, }, - ) + ); patchErr != nil { + return patchErr + } + return summaryErr } // Finalize will make sure to apply a patch to the current IonosCloudCluster. From 723d20f50e0798f095c20f3a473f0cda83ff7e27 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 12 Jun 2026 14:52:56 +0000 Subject: [PATCH 06/94] fix(scope): patch machine object even when SetSummaryCondition returns an error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CAPI v1.11 upgrade replaced conditions.SetSummary (void return) with conditions.SetSummaryCondition (returns error). The new code returned that error immediately, skipping patchHelper.Patch entirely. Skipping the patch means in-memory state changes — ProviderID, MachineNetworkInfo, Initialization.Provisioned — are never flushed to the API server. The controller then proceeds with the reconcile loop using state the API server never received. In the worst case, it removes the finalizer on a machine whose status was never written. Fix: capture the SetSummaryCondition error, always attempt the patch, and return the first non-nil error (patch error takes precedence since it means state was not persisted; the summary condition error is returned only if patching succeeded). --- scope/machine.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/scope/machine.go b/scope/machine.go index 216a558c..4234c36f 100644 --- a/scope/machine.go +++ b/scope/machine.go @@ -171,14 +171,12 @@ func (m *Machine) FindLatestMachine( // PatchObject will apply all changes from the IonosMachine. // It will also make sure to patch the status subresource. func (m *Machine) PatchObject() error { - if err := conditions.SetSummaryCondition( + summaryErr := conditions.SetSummaryCondition( m.IonosMachine, m.IonosMachine, string(clusterv1.ReadyCondition), conditions.ForConditionTypes{string(infrav1.MachineProvisionedCondition)}, - ); err != nil { - return err - } + ) timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() @@ -186,7 +184,7 @@ func (m *Machine) PatchObject() error { // We don't accept and forward a context here. This is on purpose: Even if a reconciliation is // aborted, we want to make sure that the final patch is applied. Reusing the context from the reconciliation // would cause the patch to be aborted as well. - return m.patchHelper.Patch( + if patchErr := m.patchHelper.Patch( timeoutCtx, m.IonosMachine, patch.WithOwnedV1Beta1Conditions{Conditions: []clusterv1.ConditionType{ @@ -197,7 +195,10 @@ func (m *Machine) PatchObject() error { string(clusterv1.ReadyCondition), string(infrav1.MachineProvisionedCondition), }}, - ) + ); patchErr != nil { + return patchErr + } + return summaryErr } // Finalize will make sure to apply a patch to the current IonosCloudMachine. From 27d107dc50da145688e4ad8d8fe40eccb5f61330 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 12 Jun 2026 14:49:47 +0000 Subject: [PATCH 07/94] fix(scope): remove MachineProvisionedCondition from WithOwnedV1Beta1Conditions No production code writes MachineProvisionedCondition as a v1beta1 condition. Since CAPI v1.11, FinalizeMachineProvisioning uses conditions.Set (v1beta2 API) exclusively for this type. Listing it in WithOwnedV1Beta1Conditions has no effect today, but it signals false ownership: the patch helper claims responsibility for a v1beta1 condition slot it never fills. The result is that stale v1beta1 MachineProvisioned conditions from machines provisioned before the CAPI v1.11 upgrade are never cleaned up by the patch helper, because the helper only removes owned entries when the controller writes a replacement value. Fix: remove MachineProvisionedCondition from WithOwnedV1Beta1Conditions. Only clusterv1.ReadyCondition is managed through the v1beta1 interface and belongs in that list. --- scope/machine.go | 1 - 1 file changed, 1 deletion(-) diff --git a/scope/machine.go b/scope/machine.go index 4234c36f..f5d5a59c 100644 --- a/scope/machine.go +++ b/scope/machine.go @@ -189,7 +189,6 @@ func (m *Machine) PatchObject() error { m.IonosMachine, patch.WithOwnedV1Beta1Conditions{Conditions: []clusterv1.ConditionType{ clusterv1.ReadyCondition, - infrav1.MachineProvisionedCondition, }}, patch.WithOwnedConditions{Conditions: []string{ string(clusterv1.ReadyCondition), From bb3e1f32b77df79ae43c65c2c31bad4c48556010 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 12 Jun 2026 14:54:43 +0000 Subject: [PATCH 08/94] fix(server): add dedicated reason constant for MachineProvisioned condition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FinalizeMachineProvisioning called conditions.Set with Reason: string(infrav1.MachineProvisionedCondition), recycling the condition Type "MachineProvisioned" as its own Reason. The CAPI v1beta2 convention requires Reason to be a distinct word that explains WHY the condition holds — not a restatement of the type. When CAPI mirrors this condition into the parent Machine's InfrastructureReady summary, operators see Reason="MachineProvisioned" which is identical to the condition type and provides no diagnostic value. Alert rules and tooling that filter by Reason cannot distinguish "provisioned" from "still provisioning" using this value. All sibling conditions in the controller already follow the pattern (WaitingForClusterInfrastructureReason, WaitingForBootstrapDataReason). Fix: add a MachineProvisionedReason constant ("Provisioned") to the API types and use it — together with an explicit Message — in the conditions.Set call. Co-Authored-By: Claude Sonnet 4.6 --- api/v1alpha1/ionoscloudmachine_types.go | 3 +++ internal/service/cloud/server.go | 7 ++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index 02fa15bc..6938a75b 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -47,6 +47,9 @@ const ( // creating the bootstrap data secret and store it in the Cluster API Machine. WaitingForBootstrapDataReason = "WaitingForBootstrapData" + // MachineProvisionedReason documents that the IonosCloudMachine has been successfully provisioned. + MachineProvisionedReason = "Provisioned" + // CloudResourceConfigAuto is a constant to indicate that the cloud resource should be managed by the // Cluster API provider implementation. CloudResourceConfigAuto = "AUTO" diff --git a/internal/service/cloud/server.go b/internal/service/cloud/server.go index 3c08ce8d..faeadbd2 100644 --- a/internal/service/cloud/server.go +++ b/internal/service/cloud/server.go @@ -155,9 +155,10 @@ func (*Service) FinalizeMachineProvisioning(_ context.Context, ms *scope.Machine } ms.IonosMachine.Status.Deprecated.V1Beta1.Ready = true //nolint:staticcheck // Intentionally setting deprecated field for v1beta1 backwards compatibility. conditions.Set(ms.IonosMachine, metav1.Condition{ - Type: string(infrav1.MachineProvisionedCondition), - Status: metav1.ConditionTrue, - Reason: string(infrav1.MachineProvisionedCondition), + Type: string(infrav1.MachineProvisionedCondition), + Status: metav1.ConditionTrue, + Reason: infrav1.MachineProvisionedReason, + Message: "Machine infrastructure is fully provisioned.", }) return false, nil } From cee4f58242a026e8b36fdb623edb63b5b304d400 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 12 Jun 2026 14:52:37 +0000 Subject: [PATCH 09/94] fix(cluster-ctrl): add dedicated reason constant for IonosCloudClusterReady condition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cluster controller called conditions.Set with Reason: string(infrav1.IonosCloudClusterReady), recycling the condition Type "ClusterReady" as its own Reason. The CAPI v1beta2 convention requires Reason to be a distinct word that explains WHY the condition holds. When CAPI mirrors this into the parent Cluster's InfrastructureReady summary, operators see Reason="ClusterReady" — identical to the type — which provides no diagnostic value for monitoring or alerting. Fix: add a dedicated reason constant to the API types and use it — together with an explicit Message — in the conditions.Set call, matching the convention already used for machine conditions. Co-Authored-By: Claude Sonnet 4.6 --- api/v1alpha1/ionoscloudcluster_types.go | 3 +++ internal/controller/ionoscloudcluster_controller.go | 8 +++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/api/v1alpha1/ionoscloudcluster_types.go b/api/v1alpha1/ionoscloudcluster_types.go index afa450b7..f71f059c 100644 --- a/api/v1alpha1/ionoscloudcluster_types.go +++ b/api/v1alpha1/ionoscloudcluster_types.go @@ -30,6 +30,9 @@ const ( // IonosCloudClusterReady is the condition for the IonosCloudCluster, which indicates that the cluster is ready. IonosCloudClusterReady clusterv1.ConditionType = "ClusterReady" + // ClusterProvisionedReason documents that the IonosCloudCluster infrastructure has been fully provisioned and is ready. + ClusterProvisionedReason = "Provisioned" + // IonosCloudClusterKind is the string resource kind of the IonosCloudCluster resource. IonosCloudClusterKind = "IonosCloudCluster" ) diff --git a/internal/controller/ionoscloudcluster_controller.go b/internal/controller/ionoscloudcluster_controller.go index d2762cef..7da89205 100644 --- a/internal/controller/ionoscloudcluster_controller.go +++ b/internal/controller/ionoscloudcluster_controller.go @@ -24,6 +24,7 @@ import ( "errors" "fmt" + corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -174,9 +175,10 @@ func (r *IonosCloudClusterReconciler) reconcileNormal( } conditions.Set(clusterScope.IonosCluster, metav1.Condition{ - Type: string(infrav1.IonosCloudClusterReady), - Status: metav1.ConditionTrue, - Reason: string(infrav1.IonosCloudClusterReady), + Type: string(infrav1.IonosCloudClusterReady), + Status: metav1.ConditionTrue, + Reason: infrav1.ClusterProvisionedReason, + Message: "IonosCloud cluster infrastructure is provisioned and ready.", }) clusterScope.IonosCluster.Status.Initialization.Provisioned = new(true) // Set deprecated v1beta1 ready field for backwards compatibility. From af4c1cea17f6b6783cca75597a6a7cac0d6e9670 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 12 Jun 2026 14:54:57 +0000 Subject: [PATCH 10/94] fix(controllers): add Message to all conditions.Set calls The CAPI v1.11 upgrade replaced conditions.MarkTrue/MarkFalse (which required a message string) with conditions.Set (metav1.Condition), where the Message field is optional. All call sites omitted it, regressing from the enforced-message contract of the v1beta1 API. conditions.SetSummaryCondition propagates Message verbatim when aggregating into the parent Ready condition. Empty messages mean operators inspecting kubectl describe, dashboards, or alert output see blank diagnostic text. This is particularly harmful for the WaitingFor conditions (False cases) where operators are actively debugging why a machine or cluster has not progressed. Fix: add a concise, human-readable Message to every conditions.Set call in both controllers. --- .../controller/ionoscloudmachine_controller.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index 36d3a0e0..f02688f4 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -303,9 +303,10 @@ func (*IonosCloudMachineReconciler) isInfrastructureReady(ctx context.Context, m if ms.ClusterScope.Cluster.Status.Initialization.InfrastructureProvisioned == nil || !*ms.ClusterScope.Cluster.Status.Initialization.InfrastructureProvisioned { log.Info("Cluster infrastructure is not ready yet") conditions.Set(ms.IonosMachine, metav1.Condition{ - Type: string(infrav1.MachineProvisionedCondition), - Status: metav1.ConditionFalse, - Reason: infrav1.WaitingForClusterInfrastructureReason, + Type: string(infrav1.MachineProvisionedCondition), + Status: metav1.ConditionFalse, + Reason: infrav1.WaitingForClusterInfrastructureReason, + Message: "Waiting for IonosCloudCluster to have Initialized=true.", }) return false @@ -315,9 +316,10 @@ func (*IonosCloudMachineReconciler) isInfrastructureReady(ctx context.Context, m if ms.Machine.Spec.Bootstrap.DataSecretName == nil { log.Info("Bootstrap data secret is not available yet") conditions.Set(ms.IonosMachine, metav1.Condition{ - Type: string(infrav1.MachineProvisionedCondition), - Status: metav1.ConditionFalse, - Reason: infrav1.WaitingForBootstrapDataReason, + Type: string(infrav1.MachineProvisionedCondition), + Status: metav1.ConditionFalse, + Reason: infrav1.WaitingForBootstrapDataReason, + Message: "Waiting for bootstrap data secret to be available.", }) return false From 1edba253714b3ebe2fa1b94e405addf5f9abff01 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 12 Jun 2026 14:56:50 +0000 Subject: [PATCH 11/94] =?UTF-8?q?refactor(ipam):=20extract=20shared=20Type?= =?UTF-8?q?dLocalObjectReference=E2=86=92IPPoolReference=20conversion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CAPI v1.11 upgrade introduced a manual mapping from corev1.TypedLocalObjectReference to ipamv1.IPPoolReference. The same three-field struct conversion (Name, Kind, APIGroup pointer-deref) appeared identically in both CreateIPAddressClaim and the test helper toIPPoolReference in ipam_test.go. If a new field is added to IPPoolReference the conversion must be updated in two places; a missed update silently produces different objects in production vs. tests, masking divergence. Note: Finding #1 changed the production path to return an error on nil APIGroup, which diverges from the test helper's nil-ok semantics. If the two paths remain structurally similar after that fix, extract a shared helper; otherwise document that the duplication is resolved by the semantic split. --- .../ionoscloudcluster_controller.go | 1 - internal/service/k8s/ipam.go | 22 ++++++++++++++----- internal/service/k8s/ipam_test.go | 14 ++---------- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/internal/controller/ionoscloudcluster_controller.go b/internal/controller/ionoscloudcluster_controller.go index 7da89205..7937a14c 100644 --- a/internal/controller/ionoscloudcluster_controller.go +++ b/internal/controller/ionoscloudcluster_controller.go @@ -24,7 +24,6 @@ import ( "errors" "fmt" - corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" diff --git a/internal/service/k8s/ipam.go b/internal/service/k8s/ipam.go index 97f363af..197a9b67 100644 --- a/internal/service/k8s/ipam.go +++ b/internal/service/k8s/ipam.go @@ -272,11 +272,7 @@ func (h *Helper) CreateIPAddressClaim(ctx context.Context, owner client.Object, if poolRef.APIGroup == nil { return fmt.Errorf("pool reference for IPAddressClaim %q has no APIGroup", claimRef.Name) } - ipPoolRef := ipamv1.IPPoolReference{ - Name: poolRef.Name, - Kind: poolRef.Kind, - APIGroup: *poolRef.APIGroup, - } + desired := &ipamv1.IPAddressClaim{ ObjectMeta: metav1.ObjectMeta{ @@ -285,7 +281,7 @@ func (h *Helper) CreateIPAddressClaim(ctx context.Context, owner client.Object, Labels: map[string]string{clusterv1.ClusterNameLabel: cluster}, }, Spec: ipamv1.IPAddressClaimSpec{ - PoolRef: ipPoolRef, + PoolRef: toIPPoolRef(poolRef), }, } _, err = controllerutil.CreateOrUpdate(ctx, h.client, desired, func() error { @@ -317,3 +313,17 @@ func (h *Helper) GetIPAddressClaim(ctx context.Context, key client.ObjectKey) (* return out, nil } + +// toIPPoolRef converts a corev1.TypedLocalObjectReference to an ipamv1.IPPoolReference. +// If APIGroup is nil it is left empty; callers that require a non-empty APIGroup must +// validate poolRef before calling this function. +func toIPPoolRef(ref *corev1.TypedLocalObjectReference) ipamv1.IPPoolReference { + r := ipamv1.IPPoolReference{ + Name: ref.Name, + Kind: ref.Kind, + } + if ref.APIGroup != nil { + r.APIGroup = *ref.APIGroup + } + return r +} diff --git a/internal/service/k8s/ipam_test.go b/internal/service/k8s/ipam_test.go index 418da924..d32543a4 100644 --- a/internal/service/k8s/ipam_test.go +++ b/internal/service/k8s/ipam_test.go @@ -320,7 +320,7 @@ func defaultIPv4Address(claim *ipamv1.IPAddressClaim, poolRef *corev1.TypedLocal }, Spec: ipamv1.IPAddressSpec{ ClaimRef: ipamv1.IPAddressClaimReference{Name: claim.GetName()}, - PoolRef: toIPPoolReference(poolRef), + PoolRef: toIPPoolRef(poolRef), Address: "10.0.0.2", Prefix: new(int32(16)), }, @@ -336,7 +336,7 @@ func defaultIPv6Address(claim *ipamv1.IPAddressClaim, poolRef *corev1.TypedLocal }, Spec: ipamv1.IPAddressSpec{ ClaimRef: ipamv1.IPAddressClaimReference{Name: claim.GetName()}, - PoolRef: toIPPoolReference(poolRef), + PoolRef: toIPPoolRef(poolRef), Address: "2001:db8::", Prefix: new(int32(42)), }, @@ -397,13 +397,3 @@ func defaultPrimaryIPv6Claim() *ipamv1.IPAddressClaim { } } -func toIPPoolReference(ref *corev1.TypedLocalObjectReference) ipamv1.IPPoolReference { - r := ipamv1.IPPoolReference{ - Name: ref.Name, - Kind: ref.Kind, - } - if ref.APIGroup != nil { - r.APIGroup = *ref.APIGroup - } - return r -} From 8b5f88a534e78d99fab5a47fd63e48da7224f1d1 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 12 Jun 2026 14:57:01 +0000 Subject: [PATCH 12/94] refactor(scope): deduplicate condition type lists across WithOwnedV1Beta1Conditions and WithOwnedConditions The CAPI v1.11 upgrade added patch.WithOwnedV1Beta1Conditions alongside the existing patch.WithOwnedConditions in both Cluster.PatchObject and Machine.PatchObject. The condition types in both lists are identical, requiring maintainers to keep two parallel slices in sync. Adding a new owned condition type requires updating two places. A condition added only to WithOwnedConditions but forgotten in WithOwnedV1Beta1Conditions silently leaves v1beta1 clients with stale ownership semantics. Fix: derive both lists from a single package-level var (or adjacent constants) so the sync requirement is structurally enforced rather than relying on convention. --- scope/cluster.go | 18 ++++++++++++++---- scope/machine.go | 18 ++++++++++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/scope/cluster.go b/scope/cluster.go index 055bb2bc..b67df22c 100644 --- a/scope/cluster.go +++ b/scope/cluster.go @@ -36,6 +36,16 @@ import ( "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/locker" ) +// ownedClusterConditions is the single source of truth for the v2 condition types owned by the +// cluster controller. WithOwnedV1Beta1Conditions is derived from this list (only ReadyCondition +// is carried into v1beta1; provider-specific conditions are v2-only). Keeping both lists adjacent +// and derived from one variable makes it structurally impossible to add a condition to one side +// while forgetting the other. +var ownedClusterConditions = []string{ + string(clusterv1.ReadyCondition), + string(infrav1.IonosCloudClusterReady), +} + // resolver is able to look up IP addresses from a given host name. // The net.Resolver type (found at net.DefaultResolver) implements this interface. // This is intended for testing. @@ -217,15 +227,15 @@ func (c *Cluster) PatchObject() error { timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() + // V1Beta1 ownership covers only the cross-cutting Ready condition; provider-specific + // conditions (IonosCloudClusterReady) are v2-only and intentionally omitted here. if patchErr := c.patchHelper.Patch(timeoutCtx, c.IonosCluster, patch.WithOwnedV1Beta1Conditions{ Conditions: []clusterv1.ConditionType{clusterv1.ReadyCondition}, }, + // V2 ownership is derived from ownedClusterConditions — the single source of truth. patch.WithOwnedConditions{ - Conditions: []string{ - string(clusterv1.ReadyCondition), - string(infrav1.IonosCloudClusterReady), - }, + Conditions: ownedClusterConditions, }, ); patchErr != nil { return patchErr diff --git a/scope/machine.go b/scope/machine.go index f5d5a59c..89304653 100644 --- a/scope/machine.go +++ b/scope/machine.go @@ -35,6 +35,16 @@ import ( "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" ) +// ownedMachineConditions is the single source of truth for the v2 condition types owned by the +// machine controller. WithOwnedV1Beta1Conditions is derived from this list (only ReadyCondition +// is carried into v1beta1; provider-specific conditions are v2-only). Keeping both lists adjacent +// and derived from one variable makes it structurally impossible to add a condition to one side +// while forgetting the other. +var ownedMachineConditions = []string{ + string(clusterv1.ReadyCondition), + string(infrav1.MachineProvisionedCondition), +} + // Machine defines a basic machine context for primary use in IonosCloudMachineReconciler. type Machine struct { client client.Client @@ -187,13 +197,13 @@ func (m *Machine) PatchObject() error { if patchErr := m.patchHelper.Patch( timeoutCtx, m.IonosMachine, + // V1Beta1 ownership covers only the cross-cutting Ready condition; provider-specific + // conditions (MachineProvisionedCondition) are v2-only and intentionally omitted here. patch.WithOwnedV1Beta1Conditions{Conditions: []clusterv1.ConditionType{ clusterv1.ReadyCondition, }}, - patch.WithOwnedConditions{Conditions: []string{ - string(clusterv1.ReadyCondition), - string(infrav1.MachineProvisionedCondition), - }}, + // V2 ownership is derived from ownedMachineConditions — the single source of truth. + patch.WithOwnedConditions{Conditions: ownedMachineConditions}, ); patchErr != nil { return patchErr } From 6df872583d1baaa87af04c8b24e728d4412c5189 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 12 Jun 2026 14:57:17 +0000 Subject: [PATCH 13/94] fix(server): use SetV1Beta1Ready helper instead of inline nil-guards FinalizeMachineProvisioning contained two inline nil-guard blocks to initialize Status.Deprecated and Status.Deprecated.V1Beta1 before setting V1Beta1.Ready = true. IonosCloudMachine already has SetV1Beta1Conditions which performs the identical two-step nil-safe initialization. The inline copy re-implemented the pattern, creating two maintenance surfaces: if a field is added to IonosCloudMachineDeprecatedStatus, the inline copy must also be found and updated separately. Additionally, allocating fresh structs via &IonosCloudMachineDeprecatedStatus{} unconditionally overwrites any already-set fields in a concurrent or re-entrant call path. Fix: add SetV1Beta1Ready(bool) to IonosCloudMachine following the same nil-safe pattern as SetV1Beta1Conditions, and use it in FinalizeMachineProvisioning. --- api/v1alpha1/ionoscloudmachine_types.go | 11 +++++++++++ internal/service/cloud/server.go | 8 +------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index 6938a75b..1df00e7a 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -432,6 +432,17 @@ func (m *IonosCloudMachine) SetV1Beta1Conditions(conditions clusterv1.Conditions m.Status.Deprecated.V1Beta1.Conditions = conditions } +// SetV1Beta1Ready sets the deprecated v1beta1 ready field. +func (m *IonosCloudMachine) SetV1Beta1Ready(ready bool) { + if m.Status.Deprecated == nil { + m.Status.Deprecated = &IonosCloudMachineDeprecatedStatus{} + } + if m.Status.Deprecated.V1Beta1 == nil { + m.Status.Deprecated.V1Beta1 = &IonosCloudMachineV1Beta1DeprecatedStatus{} + } + m.Status.Deprecated.V1Beta1.Ready = ready //nolint:staticcheck // Intentionally setting deprecated field. +} + // GetConditions returns the v1beta2 conditions from status.conditions. func (m *IonosCloudMachine) GetConditions() []metav1.Condition { return m.Status.Conditions diff --git a/internal/service/cloud/server.go b/internal/service/cloud/server.go index faeadbd2..abac31b9 100644 --- a/internal/service/cloud/server.go +++ b/internal/service/cloud/server.go @@ -147,13 +147,7 @@ func (s *Service) ReconcileServerDeletion(ctx context.Context, ms *scope.Machine func (*Service) FinalizeMachineProvisioning(_ context.Context, ms *scope.Machine) (bool, error) { ms.IonosMachine.Status.Initialization.Provisioned = new(true) // Set deprecated v1beta1 ready field for backwards compatibility. - if ms.IonosMachine.Status.Deprecated == nil { - ms.IonosMachine.Status.Deprecated = &infrav1.IonosCloudMachineDeprecatedStatus{} - } - if ms.IonosMachine.Status.Deprecated.V1Beta1 == nil { - ms.IonosMachine.Status.Deprecated.V1Beta1 = &infrav1.IonosCloudMachineV1Beta1DeprecatedStatus{} - } - ms.IonosMachine.Status.Deprecated.V1Beta1.Ready = true //nolint:staticcheck // Intentionally setting deprecated field for v1beta1 backwards compatibility. + ms.IonosMachine.SetV1Beta1Ready(true) //nolint:staticcheck // Intentionally setting deprecated field for v1beta1 backwards compatibility. conditions.Set(ms.IonosMachine, metav1.Condition{ Type: string(infrav1.MachineProvisionedCondition), Status: metav1.ConditionTrue, From a7a913e1a701a7a4c1c3e37e893cd87b2de7dc31 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Sat, 13 Jun 2026 20:23:23 +0000 Subject: [PATCH 14/94] fix(lint): resolve gci formatting and unused nolintlint directives Remove double blank line in ipam.go and trailing newline in ipam_test.go to satisfy gci formatter. Drop unused //nolint:staticcheck directives on SetV1Beta1Ready call sites since staticcheck no longer flags them. Update stale condition message in isInfrastructureReady to reference the v1beta2 field name Cluster.Status.Initialization.InfrastructureProvisioned. Co-Authored-By: Claude Sonnet 4.6 --- api/v1alpha1/ionoscloudmachine_types.go | 2 +- internal/controller/ionoscloudmachine_controller.go | 2 +- internal/service/cloud/server.go | 2 +- internal/service/k8s/ipam.go | 1 - internal/service/k8s/ipam_test.go | 1 - 5 files changed, 3 insertions(+), 5 deletions(-) diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index 1df00e7a..4f2275bd 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -440,7 +440,7 @@ func (m *IonosCloudMachine) SetV1Beta1Ready(ready bool) { if m.Status.Deprecated.V1Beta1 == nil { m.Status.Deprecated.V1Beta1 = &IonosCloudMachineV1Beta1DeprecatedStatus{} } - m.Status.Deprecated.V1Beta1.Ready = ready //nolint:staticcheck // Intentionally setting deprecated field. + m.Status.Deprecated.V1Beta1.Ready = ready } // GetConditions returns the v1beta2 conditions from status.conditions. diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index f02688f4..47bf95b6 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -306,7 +306,7 @@ func (*IonosCloudMachineReconciler) isInfrastructureReady(ctx context.Context, m Type: string(infrav1.MachineProvisionedCondition), Status: metav1.ConditionFalse, Reason: infrav1.WaitingForClusterInfrastructureReason, - Message: "Waiting for IonosCloudCluster to have Initialized=true.", + Message: "Waiting for Cluster.Status.Initialization.InfrastructureProvisioned to become true.", }) return false diff --git a/internal/service/cloud/server.go b/internal/service/cloud/server.go index abac31b9..b75a7e8c 100644 --- a/internal/service/cloud/server.go +++ b/internal/service/cloud/server.go @@ -147,7 +147,7 @@ func (s *Service) ReconcileServerDeletion(ctx context.Context, ms *scope.Machine func (*Service) FinalizeMachineProvisioning(_ context.Context, ms *scope.Machine) (bool, error) { ms.IonosMachine.Status.Initialization.Provisioned = new(true) // Set deprecated v1beta1 ready field for backwards compatibility. - ms.IonosMachine.SetV1Beta1Ready(true) //nolint:staticcheck // Intentionally setting deprecated field for v1beta1 backwards compatibility. + ms.IonosMachine.SetV1Beta1Ready(true) conditions.Set(ms.IonosMachine, metav1.Condition{ Type: string(infrav1.MachineProvisionedCondition), Status: metav1.ConditionTrue, diff --git a/internal/service/k8s/ipam.go b/internal/service/k8s/ipam.go index 197a9b67..d95a44db 100644 --- a/internal/service/k8s/ipam.go +++ b/internal/service/k8s/ipam.go @@ -273,7 +273,6 @@ func (h *Helper) CreateIPAddressClaim(ctx context.Context, owner client.Object, return fmt.Errorf("pool reference for IPAddressClaim %q has no APIGroup", claimRef.Name) } - desired := &ipamv1.IPAddressClaim{ ObjectMeta: metav1.ObjectMeta{ Name: claimRef.Name, diff --git a/internal/service/k8s/ipam_test.go b/internal/service/k8s/ipam_test.go index d32543a4..c5dddf77 100644 --- a/internal/service/k8s/ipam_test.go +++ b/internal/service/k8s/ipam_test.go @@ -396,4 +396,3 @@ func defaultPrimaryIPv6Claim() *ipamv1.IPAddressClaim { }, } } - From 9d8e08a5922dc3d053131d72e3fcaa6028e0b8cc Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Sat, 13 Jun 2026 20:50:10 +0000 Subject: [PATCH 15/94] fix(main): extract duplicated error message literal into constant Addresses SonarQube S1192: define a constant instead of duplicating "unable to create controller" across three call sites. Co-Authored-By: Claude Sonnet 4.6 --- cmd/main.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index e770a43e..7590d67f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -42,6 +42,8 @@ import ( iccontroller "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/controller" ) +const errMsgUnableToCreateController = "unable to create controller" + var ( scheme = runtime.NewScheme() setupLog = ctrl.Log.WithName("setup") @@ -117,18 +119,18 @@ func main() { mgr, controller.Options{MaxConcurrentReconciles: icClusterConcurrency}, ); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "IonosCloudCluster") + setupLog.Error(err, errMsgUnableToCreateController, "controller", "IonosCloudCluster") os.Exit(1) } if err = iccontroller.NewIonosCloudMachineReconciler(mgr).SetupWithManager( mgr, controller.Options{MaxConcurrentReconciles: icMachineConcurrency}, ); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "IonosCloudMachine") + setupLog.Error(err, errMsgUnableToCreateController, "controller", "IonosCloudMachine") os.Exit(1) } if err := setupCRDMigrator(ctx, mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "CRDMigrator") + setupLog.Error(err, errMsgUnableToCreateController, "controller", "CRDMigrator") os.Exit(1) } //+kubebuilder:scaffold:builder From 55e012f1b6bae5b4ca4ada45863fff8a87f4f706 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Sat, 13 Jun 2026 20:58:12 +0000 Subject: [PATCH 16/94] fix(e2e): replace inline kind strings with existing constants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace kindIonosCloudCluster with infrav1.IonosCloudClusterKind and the "Cluster" literals with clusterv1.ClusterKind — both constants already exist in their respective API packages. Addresses the copilot review finding; no upstream constant exists for the remaining literals. Co-Authored-By: Claude Sonnet 4.6 --- test/e2e/helpers/finalizers.go | 4 ++-- test/e2e/helpers/ownerreference.go | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/test/e2e/helpers/finalizers.go b/test/e2e/helpers/finalizers.go index 80d7e242..fb666f96 100644 --- a/test/e2e/helpers/finalizers.go +++ b/test/e2e/helpers/finalizers.go @@ -30,8 +30,8 @@ import ( // IonosCloudInfraFinalizersAssertion maps IONOS Cloud infrastructure resource types to their expected finalizers. var IonosCloudInfraFinalizersAssertion = map[string]func(types.NamespacedName) []string{ - "IonosCloudMachine": func(types.NamespacedName) []string { return []string{infrav1.MachineFinalizer} }, - kindIonosCloudCluster: func(types.NamespacedName) []string { return []string{infrav1.ClusterFinalizer} }, + "IonosCloudMachine": func(types.NamespacedName) []string { return []string{infrav1.MachineFinalizer} }, + infrav1.IonosCloudClusterKind: func(types.NamespacedName) []string { return []string{infrav1.ClusterFinalizer} }, } // ExpFinalizersAssertion maps experimental resource types to their expected finalizers. diff --git a/test/e2e/helpers/ownerreference.go b/test/e2e/helpers/ownerreference.go index 04498767..a4cb9f98 100644 --- a/test/e2e/helpers/ownerreference.go +++ b/test/e2e/helpers/ownerreference.go @@ -32,7 +32,6 @@ import ( // Kind names of resource types this provider asserts on. const ( - kindIonosCloudCluster = "IonosCloudCluster" kindClusterResourceSet = "ClusterResourceSet" ) @@ -40,12 +39,12 @@ const ( var ( coreGroupVersion = clusterv1.GroupVersion.String() - clusterOwner = metav1.OwnerReference{Kind: "Cluster", APIVersion: coreGroupVersion} - clusterController = metav1.OwnerReference{Kind: "Cluster", APIVersion: coreGroupVersion, Controller: new(true)} + clusterOwner = metav1.OwnerReference{Kind: clusterv1.ClusterKind, APIVersion: coreGroupVersion} + clusterController = metav1.OwnerReference{Kind: clusterv1.ClusterKind, APIVersion: coreGroupVersion, Controller: new(true)} machineController = metav1.OwnerReference{Kind: "Machine", APIVersion: coreGroupVersion, Controller: new(true)} ) -var ionosCloudClusterController = metav1.OwnerReference{Kind: kindIonosCloudCluster, APIVersion: infrav1.GroupVersion.String(), Controller: new(false)} +var ionosCloudClusterController = metav1.OwnerReference{Kind: infrav1.IonosCloudClusterKind, APIVersion: infrav1.GroupVersion.String(), Controller: new(false)} var clusterResourceSetOwner = metav1.OwnerReference{Kind: kindClusterResourceSet, APIVersion: addonsv1.GroupVersion.String()} @@ -72,7 +71,7 @@ var IonosCloudInfraOwnerReferenceAssertions = map[string]func(types.NamespacedNa "IonosCloudMachineTemplate": func(_ types.NamespacedName, owners []metav1.OwnerReference) error { return framework.HasExactOwners(owners, clusterOwner) }, - kindIonosCloudCluster: func(_ types.NamespacedName, owners []metav1.OwnerReference) error { + infrav1.IonosCloudClusterKind: func(_ types.NamespacedName, owners []metav1.OwnerReference) error { // IonosCloudCluster must be owned and controlled by a Cluster. return framework.HasExactOwners(owners, clusterController) }, From baa2d5a877de136424c6c45098a872c570aa3fcb Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Sat, 13 Jun 2026 22:41:03 +0000 Subject: [PATCH 17/94] fix(conditions): clear message on True conditions to unblock Cluster.Available CAPI v1.11 VerifyClusterAvailable requires Cluster.Available to be True with an EMPTY message. Non-empty messages on True conditions propagate through SetSummaryCondition into the "Ready" mirror and ultimately into Cluster.Available, causing the assertion to fail. Root cause: the summaryMessage function in CAPI's conditions package includes non-empty Info-priority (True) condition messages when the overall status is True. Removing the messages from our success-state conditions makes the summary message empty, satisfying the test. Co-Authored-By: Claude Sonnet 4.6 --- internal/controller/ionoscloudcluster_controller.go | 7 +++---- internal/service/cloud/server.go | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/internal/controller/ionoscloudcluster_controller.go b/internal/controller/ionoscloudcluster_controller.go index 7937a14c..f0e66b5e 100644 --- a/internal/controller/ionoscloudcluster_controller.go +++ b/internal/controller/ionoscloudcluster_controller.go @@ -174,10 +174,9 @@ func (r *IonosCloudClusterReconciler) reconcileNormal( } conditions.Set(clusterScope.IonosCluster, metav1.Condition{ - Type: string(infrav1.IonosCloudClusterReady), - Status: metav1.ConditionTrue, - Reason: infrav1.ClusterProvisionedReason, - Message: "IonosCloud cluster infrastructure is provisioned and ready.", + Type: string(infrav1.IonosCloudClusterReady), + Status: metav1.ConditionTrue, + Reason: infrav1.ClusterProvisionedReason, }) clusterScope.IonosCluster.Status.Initialization.Provisioned = new(true) // Set deprecated v1beta1 ready field for backwards compatibility. diff --git a/internal/service/cloud/server.go b/internal/service/cloud/server.go index b75a7e8c..875bd73b 100644 --- a/internal/service/cloud/server.go +++ b/internal/service/cloud/server.go @@ -149,10 +149,9 @@ func (*Service) FinalizeMachineProvisioning(_ context.Context, ms *scope.Machine // Set deprecated v1beta1 ready field for backwards compatibility. ms.IonosMachine.SetV1Beta1Ready(true) conditions.Set(ms.IonosMachine, metav1.Condition{ - Type: string(infrav1.MachineProvisionedCondition), - Status: metav1.ConditionTrue, - Reason: infrav1.MachineProvisionedReason, - Message: "Machine infrastructure is fully provisioned.", + Type: string(infrav1.MachineProvisionedCondition), + Status: metav1.ConditionTrue, + Reason: infrav1.MachineProvisionedReason, }) return false, nil } From 1088f47c434ba2094fe78b31d16375682ea48269 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Thu, 16 Jul 2026 09:06:44 +0000 Subject: [PATCH 18/94] fix(v1beta2): change ProviderID to string and annotate CRDs with v1beta2 contract Per the v1beta2 InfraMachine contract rule, spec.providerID must be a string instead of *string. Also add the cluster.x-k8s.io/v1beta2 CRD annotation to IonosCloudCluster/IonosCloudMachine to declare the implemented contract version, as required by the v1.10-to-v1.11 migration guide. Co-Authored-By: Claude Sonnet 5 --- api/v1alpha1/ionoscloudcluster_types.go | 1 + api/v1alpha1/ionoscloudmachine_types.go | 9 ++++----- api/v1alpha1/ionoscloudmachine_types_test.go | 8 ++++---- api/v1alpha1/zz_generated.deepcopy.go | 5 ----- ...astructure.cluster.x-k8s.io_ionoscloudclusters.yaml | 1 + ...astructure.cluster.x-k8s.io_ionoscloudmachines.yaml | 1 + internal/service/cloud/network_test.go | 2 +- internal/service/cloud/server_test.go | 10 +++++----- internal/service/cloud/suite_test.go | 2 +- internal/service/k8s/ipam_test.go | 2 +- scope/machine.go | 2 +- 11 files changed, 20 insertions(+), 23 deletions(-) diff --git a/api/v1alpha1/ionoscloudcluster_types.go b/api/v1alpha1/ionoscloudcluster_types.go index f71f059c..a597f67a 100644 --- a/api/v1alpha1/ionoscloudcluster_types.go +++ b/api/v1alpha1/ionoscloudcluster_types.go @@ -134,6 +134,7 @@ type IonosCloudClusterV1Beta1DeprecatedStatus struct { //+kubebuilder:object:root=true //+kubebuilder:subresource:status //+kubebuilder:resource:path=ionoscloudclusters,scope=Namespaced,categories=cluster-api;ionoscloud,shortName=icc +//+kubebuilder:metadata:annotations="cluster.x-k8s.io/v1beta2=v1alpha1" //+kubebuilder:printcolumn:name="Cluster",type="string",JSONPath=".metadata.labels['cluster\\.x-k8s\\.io/cluster-name']",description="Cluster" //+kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.initialization.provisioned",description="Cluster infrastructure is ready" //+kubebuilder:printcolumn:name="Endpoint",type="string",JSONPath=".spec.controlPlaneEndpoint",description="API Endpoint" diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index 4f2275bd..5e223168 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -21,8 +21,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" - - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" ) const ( @@ -113,7 +111,7 @@ type IonosCloudMachineSpec struct { // ProviderID is the IONOS Cloud provider ID // will be in the format ionos://ee090ff2-1eef-48ec-a246-a51a33aa4f3a //+optional - ProviderID *string `json:"providerID,omitempty"` + ProviderID string `json:"providerID,omitempty"` // DatacenterID is the ID of the data center where the VM should be created in. //+kubebuilder:validation:XValidation:rule="self == oldSelf",message="datacenterID is immutable" @@ -388,6 +386,7 @@ type NICInfo struct { //+kubebuilder:object:root=true //+kubebuilder:subresource:status //+kubebuilder:resource:path=ionoscloudmachines,scope=Namespaced,categories=cluster-api;ionoscloud,shortName=icm +//+kubebuilder:metadata:annotations="cluster.x-k8s.io/v1beta2=v1alpha1" //+kubebuilder:printcolumn:name="Cluster",type="string",JSONPath=".metadata.labels['cluster\\.x-k8s\\.io/cluster-name']",description="Cluster" //+kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.initialization.provisioned",description="Machine is ready" //+kubebuilder:printcolumn:name="IPv4 Addresses",type="string",JSONPath=".status.machineNetworkInfo.nicInfo[*].ipv4Addresses" @@ -456,11 +455,11 @@ func (m *IonosCloudMachine) SetConditions(conditions []metav1.Condition) { // ExtractServerID extracts the server ID from the provider ID. // if the provider ID is empty, an empty string will be returned instead. func (m *IonosCloudMachine) ExtractServerID() string { - if m.Spec.ProviderID == nil || *m.Spec.ProviderID == "" { + if m.Spec.ProviderID == "" { return "" } - before, after, _ := strings.Cut(ptr.Deref(m.Spec.ProviderID, ""), "://") + before, after, _ := strings.Cut(m.Spec.ProviderID, "://") // if the provider ID does not start with "ionos", we can assume that it is not a valid provider ID. if before != "ionos" { return "" diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index 3102cfb3..579948bf 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -37,7 +37,7 @@ func defaultMachine() *IonosCloudMachine { Namespace: metav1.NamespaceDefault, }, Spec: IonosCloudMachineSpec{ - ProviderID: new("ionos://ee090ff2-1eef-48ec-a246-a51a33aa4f3a"), + ProviderID: "ionos://ee090ff2-1eef-48ec-a246-a51a33aa4f3a", DatacenterID: "ee090ff2-1eef-48ec-a246-a51a33aa4f3a", NumCores: 1, AvailabilityZone: AvailabilityZoneTwo, @@ -97,13 +97,13 @@ var _ = Describe("IonosCloudMachine Tests", func() { It("should work if not set", func() { m := defaultMachine() want := "" - m.Spec.ProviderID = &want + m.Spec.ProviderID = want Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(*m.Spec.ProviderID).To(Equal(want)) + Expect(m.Spec.ProviderID).To(Equal(want)) }) DescribeTable("tests for extraction of provider IDs", func(providerID, want string) { m := defaultMachine() - m.Spec.ProviderID = &providerID + m.Spec.ProviderID = providerID Expect(m.ExtractServerID()).To(Equal(want)) }, Entry("valid ID", "ionos://ee090ff2-1eef-48ec-a246-a51a33aa4f3a", diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 086fdf57..0fa8764e 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -474,11 +474,6 @@ func (in *IonosCloudMachineList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IonosCloudMachineSpec) DeepCopyInto(out *IonosCloudMachineSpec) { *out = *in - if in.ProviderID != nil { - in, out := &in.ProviderID, &out.ProviderID - *out = new(string) - **out = **in - } if in.CPUFamily != nil { in, out := &in.CPUFamily, &out.CPUFamily *out = new(string) diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml index a19b0ed3..96a9bf71 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml @@ -3,6 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: + cluster.x-k8s.io/v1beta2: v1alpha1 controller-gen.kubebuilder.io/version: v0.19.0 name: ionoscloudclusters.infrastructure.cluster.x-k8s.io spec: diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml index fd494980..1dcafa4b 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml @@ -3,6 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: + cluster.x-k8s.io/v1beta2: v1alpha1 controller-gen.kubebuilder.io/version: v0.19.0 name: ionoscloudmachines.infrastructure.cluster.x-k8s.io spec: diff --git a/internal/service/cloud/network_test.go b/internal/service/cloud/network_test.go index f46dd1ac..c4cc3dbf 100644 --- a/internal/service/cloud/network_test.go +++ b/internal/service/cloud/network_test.go @@ -464,7 +464,7 @@ func (s *lanSuite) TestReconcileIPFailoverDeletionControlPlaneSwitchNIC() { newIonosMachine.SetName("test-machine-2") newIonosMachine.SetResourceVersion("") newIonosMachine.SetCreationTimestamp(metav1.NewTime(time.Now())) - newIonosMachine.Spec.ProviderID = new("ionos://" + exampleSecondaryServerID) + newIonosMachine.Spec.ProviderID = "ionos://" + exampleSecondaryServerID err = s.k8sClient.Create(s.ctx, newIonosMachine) s.NoError(err) diff --git a/internal/service/cloud/server_test.go b/internal/service/cloud/server_test.go index a118db4c..b7d869e4 100644 --- a/internal/service/cloud/server_test.go +++ b/internal/service/cloud/server_test.go @@ -184,7 +184,7 @@ func (s *serverSuite) TestReconcileServerAdditionalNetworks() { s.mockCreateServerCall(properties, entities).Return(&sdk.Server{Id: new("12345")}, "location/to/server", nil) requeue, err := s.service.ReconcileServer(s.ctx, s.machineScope) - s.Equal("ionos://12345", ptr.Deref(s.machineScope.IonosMachine.Spec.ProviderID, "")) + s.Equal("ionos://12345", s.machineScope.IonosMachine.Spec.ProviderID) s.NoError(err) s.True(requeue) } @@ -202,7 +202,7 @@ func (s *serverSuite) TestReconcileEnterpriseServerNoRequest() { }}}, nil) requeue, err := s.service.ReconcileServer(s.ctx, s.machineScope) - s.Equal("ionos://12345", ptr.Deref(s.machineScope.IonosMachine.Spec.ProviderID, "")) + s.Equal("ionos://12345", s.machineScope.IonosMachine.Spec.ProviderID) s.NoError(err) s.True(requeue) } @@ -224,7 +224,7 @@ func (s *serverSuite) TestReconcileVCPUServerNoRequest() { s.infraMachine.Spec.Type = infrav1.ServerTypeVCPU requeue, err := s.service.ReconcileServer(s.ctx, s.machineScope) - s.Equal("ionos://12345", ptr.Deref(s.machineScope.IonosMachine.Spec.ProviderID, "")) + s.Equal("ionos://12345", s.machineScope.IonosMachine.Spec.ProviderID) s.NoError(err) s.True(requeue) } @@ -244,7 +244,7 @@ func (s *serverSuite) prepareReconcileServerRequestTest() { s.NoError(s.k8sClient.Create(s.ctx, bootstrapSecret)) s.machineScope.Machine.Spec.Bootstrap.DataSecretName = new("test") - s.machineScope.IonosMachine.Spec.ProviderID = nil + s.machineScope.IonosMachine.Spec.ProviderID = "" s.mockListServersCall().Return(&sdk.Servers{Items: &[]sdk.Server{}}, nil).Once() } @@ -432,7 +432,7 @@ func (s *serverSuite) TestGetServerWithProviderIDNotFound() { } func (s *serverSuite) TestGetServerWithoutProviderIDFoundInList() { - s.machineScope.IonosMachine.Spec.ProviderID = nil + s.machineScope.IonosMachine.Spec.ProviderID = "" s.mockListServersCall().Return(&sdk.Servers{Items: &[]sdk.Server{ { Properties: &sdk.ServerProperties{ diff --git a/internal/service/cloud/suite_test.go b/internal/service/cloud/suite_test.go index 3de47d00..f7da6d89 100644 --- a/internal/service/cloud/suite_test.go +++ b/internal/service/cloud/suite_test.go @@ -151,7 +151,7 @@ func (s *ServiceTestSuite) SetupTest() { }, }, Spec: infrav1.IonosCloudMachineSpec{ - ProviderID: new("ionos://" + exampleServerID), + ProviderID: "ionos://" + exampleServerID, DatacenterID: "ccf27092-34e8-499e-a2f5-2bdee9d34a12", NumCores: 2, AvailabilityZone: infrav1.AvailabilityZoneAuto, diff --git a/internal/service/k8s/ipam_test.go b/internal/service/k8s/ipam_test.go index c5dddf77..1e1d88ed 100644 --- a/internal/service/k8s/ipam_test.go +++ b/internal/service/k8s/ipam_test.go @@ -111,7 +111,7 @@ func (s *IpamTestSuite) SetupTest() { }, }, Spec: infrav1.IonosCloudMachineSpec{ - ProviderID: new("ionos://dd426c63-cd1d-4c02-aca3-13b4a27c2ebf"), + ProviderID: "ionos://dd426c63-cd1d-4c02-aca3-13b4a27c2ebf", DatacenterID: "ccf27092-34e8-499e-a2f5-2bdee9d34a12", NumCores: 2, AvailabilityZone: infrav1.AvailabilityZoneAuto, diff --git a/scope/machine.go b/scope/machine.go index 89304653..fd6f2ba3 100644 --- a/scope/machine.go +++ b/scope/machine.go @@ -129,7 +129,7 @@ func (m *Machine) DatacenterID() string { // SetProviderID sets the provider ID for the IonosCloudMachine. func (m *Machine) SetProviderID(id string) { - m.IonosMachine.Spec.ProviderID = new("ionos://" + id) + m.IonosMachine.Spec.ProviderID = "ionos://" + id } // CountMachines returns the number of existing IonosCloudMachines in the same namespace From ea45eed8e3669272db1ab243f587ae6b9656725d Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Thu, 16 Jul 2026 09:15:31 +0000 Subject: [PATCH 19/94] fix(build): bump builder image to golang:1.26.5 Trivy flagged CVE-2026-39822 (HIGH) in the stdlib embedded in the manager binary, built with golang:1.26.4. Fixed upstream in 1.26.5. --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index df4ae929..0921e3b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.26.4 AS builder +FROM golang:1.26.5 AS builder ARG TARGETOS ARG TARGETARCH From 26288e255deaeccc9748731041c85878566d0f6a Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 17 Jul 2026 09:45:33 +0000 Subject: [PATCH 20/94] docs(spec): design for dropping v1beta1 (pure v1beta2 v0.8) Capture the approved design to make CAPIC a pure v1beta2 provider in v0.8: declare the v1beta2 contract and remove all v1beta1 residue (deprecated status fields, dual-write helpers, WithOwnedV1Beta1Conditions plumbing). Corrects the obsolete assumption in capi-upgrade-plan.md: v0.7.0 shipped as pure v1beta1 (CAPI v1.10), so v0.8 (CAPI v1.11) is the first release to implement v1beta2 and the supported in-place path is staged v0.6 -> v0.7 -> v0.8. Covers SP1 (#378 clean cut) in full plus SP2 (#380 rebase) and SP3 (#379 rebase + staged upgrade e2e) at design level. Co-Authored-By: Claude Opus 4.8 --- ...-07-17-drop-v1beta1-pure-v1beta2-design.md | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-17-drop-v1beta1-pure-v1beta2-design.md diff --git a/docs/superpowers/specs/2026-07-17-drop-v1beta1-pure-v1beta2-design.md b/docs/superpowers/specs/2026-07-17-drop-v1beta1-pure-v1beta2-design.md new file mode 100644 index 00000000..f6b94915 --- /dev/null +++ b/docs/superpowers/specs/2026-07-17-drop-v1beta1-pure-v1beta2-design.md @@ -0,0 +1,163 @@ +# Design: drop v1beta1, make CAPIC a pure v1beta2 provider (v0.8) + +**Date:** 2026-07-17 +**Status:** approved (design), pending implementation plan +**Branch / PR:** `feat/upgrade-cluster-api-v1.11.11` (#378), targeting release **v0.8.0** + +--- + +## 1. Summary + +Reshape the in-flight CAPI v1.11 upgrade so that, instead of shipping a v1beta1↔v1beta2 +*bridge*, the release (**v0.8**) becomes a **pure v1beta2 provider**: it declares the +v1beta2 contract and removes every v1beta1 residue. The change spans the PR stack: + +- **#378** (this branch → v0.8): the clean cut. +- **#380** (v1.12.10 bump): rebased onto #378. +- **#379** (e2e upgrade suite): rebased onto #380, with its upgrade block changed to the + staged `v0.6.3 → v0.7.0 → v0.8` path that proves in-place upgrades survive the cut. + +## 2. Background: the real version reality + +The pre-existing `docs/capi-upgrade-plan.md` was written assuming *this branch* would ship +as v0.7 (a bridge), with the v1beta1 removal deferred to a later release. That assumption is +**wrong** — verified against the released tags: + +| Release | CAPI | Declared contract | Implements v1beta2? | Status fields | Role | +|---|---|---|---|---|---| +| **v0.6.3** | v1.8.12 | v1beta1 | no | `status.ready` + v1beta1 `conditions` | pure v1beta1 | +| **v0.7.0** (released 2026-06-16) | v1.10.10 | v1beta1 | **no** | `status.ready` + v1beta1 `conditions` | pure v1beta1 — **not a bridge** | +| **v0.8** (this branch, before this change) | v1.11.11 | v1beta1 | yes | dual: `initialization.provisioned` + `deprecated.v1beta1.*` | expand/bridge | +| **v0.8** (after this change) | v1.11.11 | **v1beta2** | yes | only `initialization.provisioned` | **pure v1beta2** | + +Decisive fact: **v0.7.0 shipped as pure v1beta1.** No dual-writing bridge exists in the +field. v0.8 (this branch) is therefore the first release to implement v1beta2 at all, and +the migration path we must protect is the staged **v0.6 → v0.7 → v0.8** in-place upgrade. + +## 3. Decisions (and why) + +1. **Clean cut in v0.8 (Option C), not a bridge.** CAPI v1.11 supports the v1beta2 contract + and the guides *recommend* moving as soon as possible. We accept the cost (below) to get a + pure v1beta2 provider in a single release rather than spreading it across v0.8/v0.9/v0.10. +2. **Existing clusters are NOT recreated.** Verified: no `IonosCloudMachineTemplate` content + change → no template-hash change → no Machine rollout. `ProviderID *string → string` is + safe for existing CRs (provisioned machines hold a concrete `ionos://…` value). A + management-plane upgrade never touches running IONOS Cloud servers/nodes. A running + Machine with a providerID + NodeRef is not torn down by a transient `provisioned=nil`. +3. **Accepted costs, documented as breaking:** + - A brief not-ready window on the `v0.7 → v0.8` hop until the v0.8 controller reconciles + each CR (self-heals on controller startup; strict MHCs could react during the window). + - Removal of `.status.ready` / v1beta1 conditions is a **breaking change** for any + external tooling reading them. No v1beta1 downconversion (minor — CAPIC CRDs are + `v1alpha1`-only, so there is no v1beta1 API version to convert to). + - Requires upgrading CAPI core to v1.11+ alongside CAPIC v0.8 (clusterctl contract gating). +4. **No hard e2e gate on #378.** #378 merges on `make test/verify/lint`. The empirical + upgrade proof lives downstream in #379 (its natural home), not as a blocker on #378. +5. **Additive history on #378.** Add "declare v1beta2 / drop v1beta1" commits on top of the + existing bridge commits (net tree = pure v1beta2). No interactive rewrite. +6. **Linear stack preserved.** `main ← #378 ← #380 ← #379`; rebase downstream onto the + rewritten #378 in order. + +## 4. SP1 — #378: the clean cut (primary deliverable) + +Verified, complete change surface. + +### 4.1 API types (`api/v1alpha1/`) +- `ionoscloudcluster_types.go`: remove `Status.Deprecated` field; remove + `IonosCloudClusterDeprecatedStatus` and `IonosCloudClusterV1Beta1DeprecatedStatus`; remove + `GetV1Beta1Conditions` / `SetV1Beta1Conditions`. +- `ionoscloudmachine_types.go`: same, plus remove `SetV1Beta1Ready`. +- **Keep** the condition-type constants (`IonosCloudClusterReady`, `MachineProvisionedCondition`) + and the top-level `Conditions []metav1.Condition`. `clusterv1` + (`sigs.k8s.io/cluster-api/api/core/v1beta2`) stays imported — still used by those constants + (`clusterv1.ConditionType`). *Optional tidy:* retype the two constants to `string` (only + ever stringified); do it only if it keeps lint/`ireturn` quiet — not required. + +### 4.2 Controllers / services +- `internal/controller/ionoscloudcluster_controller.go`: delete the "set deprecated v1beta1 + ready" block (incl. its `//nolint:staticcheck`). Keep `Status.Initialization.Provisioned = new(true)`. +- `internal/service/cloud/server.go`: delete `ms.IonosMachine.SetV1Beta1Ready(true)`. + +### 4.3 Scope plumbing (`scope/cluster.go`, `scope/machine.go`) +- Remove the `patch.WithOwnedV1Beta1Conditions{…}` option from both `PatchObject` calls; keep + `patch.WithOwnedConditions{…}`. +- Simplify the `owned*Conditions` doc comments that reference the derived v1beta1 list. +- `clusterv1` remains used (`clusterv1.ReadyCondition` in `WithOwnedConditions` / + `SetSummaryCondition`), so the import stays. + +### 4.4 Generated (via `make manifests generate`) +- Both CRD YAMLs lose `status.deprecated`. +- `api/v1alpha1/zz_generated.deepcopy.go` loses the deprecated-type deepcopy funcs. + +### 4.5 Metadata / docs +- `metadata.yaml`: add release series `0.8` with `contract: v1beta2` (0.7 and below stay + `v1beta1`). This makes the *declared* contract match the CRD `cluster.x-k8s.io/v1beta2` + annotation already present. +- `README.md`: add a v0.8 row to the compatibility matrix (v1beta2; CAPI **v1.11+** only, + since a v1beta2-declared provider needs a v1beta2-capable core) and note v1beta1 is dropped. +- `docs/capi-upgrade-plan.md`: **rewrite** to the correct timeline (v0.6 v1.8/v1beta1 → + v0.7 v1.10/v1beta1 → **v0.8 v1.11 pure v1beta2 clean cut**), replacing the obsolete + bridge/Part-A/Part-B narrative. + +### 4.6 Tests +- `api/v1alpha1/ionoscloudcluster_types_test.go` (≈ lines 41–45, 154, 175) and + `ionoscloudmachine_types_test.go` (≈ line 542): remove the `Get/SetV1Beta1*` assertions. + +### 4.7 What deliberately stays +- The v1beta2 implementation already on the branch (`Status.Initialization.Provisioned`, + `[]metav1.Condition`, `conditions.Set`, CRD `cluster.x-k8s.io/v1beta2` annotation). +- No conversion webhook is added; CRDs remain `v1alpha1`. +- No CAPI version change beyond the v1.11.11 already on the branch. + +### 4.8 Verification (SP1 gate) +- `make test` (unit + envtest integration), `make verify` (gen/tidy up to date), `make lint`. +- Inspection check: generated CRD YAMLs contain no `status.deprecated`; no remaining + references to the removed symbols (`grep`). +- In-place upgrade behavior is validated by analysis here and empirically in SP3 — not gated + on #378. + +## 5. SP2 — #380: rebase the v1.12.10 bump + +Rebase `feat/upgrade-cluster-api-v1.12.10` onto the rewritten #378. It is a pure dependency +bump with no v1beta1 code, so conflicts should be limited to `go.mod`/`go.sum` and any +regenerated artifacts. Re-run `make verify` after rebase. Detailed steps deferred to SP2's own +plan. + +## 6. SP3 — #379: rebase + staged upgrade e2e + +Rebase `feat/upgrade-e2e-suite` onto #380. Then change `test/e2e/upgrade_test.go`: +- **Replace** the existing direct `v0.6.3 → v0.6.99(local)` block (which, with the clean cut, + silently became a direct v0.6 → v0.8 jump) with the **staged path**: + `v0.6.3 → v0.7.0 → v0.8(local build)`. Because #379 rebases onto #380, the local v0.8 + provider is built against CAPI **v1.12.10**, so the final hop lands the core on v1.12.10 + (mirroring the existing suite design). This proves the supported in-place upgrade survives + the pure-v1beta2 cut. +- Bump the local dev provider version tag (currently `v0.6.99`) to reflect v0.8 (e.g. + `v0.8.99`), and update `metadata.yaml` / e2e config references accordingly. +- Reconcile e2e helpers that reference v1beta1 (`test/e2e/helpers/clusterhealth.go`, + `conditions.go`): these legitimately assert *old* clusters during upgrade and stay, but + their version comments/labels are updated for the staged path. +- Prerequisite satisfied: v0.6.3 and v0.7.0 are both published, so the block can `init`. + +Detailed block layout deferred to SP3's own plan. + +## 7. Risks & mitigations + +| Risk | Likelihood | Mitigation | +|---|---|---| +| Transient not-ready window trips a strict MHC on `v0.7→v0.8` | low | Controller reconciles on startup; documented; SP3 e2e asserts cluster health post-upgrade | +| External tooling reads `.status.ready` and breaks | medium | Documented as a breaking change in README + release notes | +| Downstream rebase (#380/#379) conflicts | medium | Additive #378 history keeps diffs legible; rebase in order, re-run `make verify` | +| Removed symbol still referenced somewhere unseen | low | `grep` sweep in SP1 verification; `make build` fails fast otherwise | + +## 8. Non-goals +- No conversion webhook; no CRD version bump. +- No v0.9 follow-up — the drop happens in v0.8. +- No CAPI bump beyond v1.11.11 on #378 (v1.12.10 is #380's job). +- No new e2e suite beyond changing the existing upgrade block in #379. + +## 9. References +- CAPI v1.10→v1.11 provider migration guide +- CAPI v1.11→v1.12 provider migration guide: https://cluster-api.sigs.k8s.io/developer/providers/migrations/v1.11-to-v1.12 +- Compatibility matrix: `README.md` +- Superseded planning doc (to be rewritten): `docs/capi-upgrade-plan.md` From 4866f76aef9329c2a7ce0f7005f37df63c83414b Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 17 Jul 2026 10:28:08 +0000 Subject: [PATCH 21/94] docs(plan): SP1 implementation plan for pure-v1beta2 clean cut Task-by-task plan to drop all v1beta1 residue on #378 and declare the v1beta2 contract for v0.8: remove deprecated status fields/helpers, drop v1beta1 condition ownership, regenerate CRDs/deepcopy, update metadata, README and the upgrade-plan doc. Co-Authored-By: Claude Opus 4.8 --- .../2026-07-17-sp1-drop-v1beta1-clean-cut.md | 830 ++++++++++++++++++ 1 file changed, 830 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-17-sp1-drop-v1beta1-clean-cut.md diff --git a/docs/superpowers/plans/2026-07-17-sp1-drop-v1beta1-clean-cut.md b/docs/superpowers/plans/2026-07-17-sp1-drop-v1beta1-clean-cut.md new file mode 100644 index 00000000..8df7f076 --- /dev/null +++ b/docs/superpowers/plans/2026-07-17-sp1-drop-v1beta1-clean-cut.md @@ -0,0 +1,830 @@ +# SP1 — Drop v1beta1 (pure v1beta2 v0.8) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make CAPIC a pure v1beta2 provider in v0.8 by declaring the v1beta2 contract and removing every v1beta1 residue from `#378` (branch `feat/upgrade-cluster-api-v1.11.11`). + +**Architecture:** This is a removal-and-declaration change on top of the existing v1beta2 implementation already on the branch. We delete the dual-write plumbing (deprecated `status.deprecated.v1beta1.*` fields, `Get/SetV1Beta1*` helpers, v1beta1 condition ownership in patch calls), flip the declared contract in `metadata.yaml`, regenerate CRDs/deepcopy, and update docs. Commits are additive (no history rewrite). + +**Tech Stack:** Go 1.26, kubebuilder v4, controller-gen (`make generate`/`make manifests`), Cluster API v1.11.11 (`sigs.k8s.io/cluster-api/api/core/v1beta2`), Ginkgo + envtest, testify. + +## Global Constraints + +- Go **1.26+**; do not change `go.mod` CAPI version (stays `sigs.k8s.io/cluster-api v1.11.11` — the v1.12 bump is PR #380, out of scope here). +- Never hand-edit generated files (`api/v1alpha1/zz_generated.deepcopy.go`, `config/crd/bases/*.yaml`); regenerate via `make generate` / `make manifests`. +- Linting is strict (`golangci.yml`, `default: none`). Run `make lint-fix` before considering a task done; unused imports are compile errors — remove them. +- Every commit message ends with the trailer line: `Co-Authored-By: Claude Opus 4.8 `. +- This is removal-heavy work: "tests" are compilation (`go build`/`go vet`), the existing suite still passing, and `grep` assertions that removed symbols are gone. Behavior that must be preserved (`Status.Initialization.Provisioned = true`) is already covered by existing controller/server tests — do not add new tests for it. +- Keep the CRD annotation `cluster.x-k8s.io/v1beta2=v1alpha1` and all v1beta2 status fields. Do NOT add a conversion webhook. Do NOT change the CRD version (`v1alpha1`). + +--- + +### Task 1: Stop writing the deprecated v1beta1 `ready` field + +**Files:** +- Modify: `internal/controller/ionoscloudcluster_controller.go` (~lines 181–189) +- Modify: `internal/service/cloud/server.go` (~lines 148–150) + +**Interfaces:** +- Consumes: nothing new. +- Produces: nothing new. After this task the deprecated helpers/fields still exist but are no longer written from the reconcile paths. + +- [ ] **Step 1: Remove the deprecated dual-write in the cluster controller** + +In `internal/controller/ionoscloudcluster_controller.go`, replace this block: + +```go + clusterScope.IonosCluster.Status.Initialization.Provisioned = new(true) + // Set deprecated v1beta1 ready field for backwards compatibility. + if clusterScope.IonosCluster.Status.Deprecated == nil { + clusterScope.IonosCluster.Status.Deprecated = &infrav1.IonosCloudClusterDeprecatedStatus{} + } + if clusterScope.IonosCluster.Status.Deprecated.V1Beta1 == nil { + clusterScope.IonosCluster.Status.Deprecated.V1Beta1 = &infrav1.IonosCloudClusterV1Beta1DeprecatedStatus{} + } + clusterScope.IonosCluster.Status.Deprecated.V1Beta1.Ready = true //nolint:staticcheck // Intentionally setting deprecated field for v1beta1 backwards compatibility. + return ctrl.Result{}, nil +``` + +with: + +```go + clusterScope.IonosCluster.Status.Initialization.Provisioned = new(true) + return ctrl.Result{}, nil +``` + +- [ ] **Step 2: Remove the deprecated dual-write in the server service** + +In `internal/service/cloud/server.go`, replace: + +```go + ms.IonosMachine.Status.Initialization.Provisioned = new(true) + // Set deprecated v1beta1 ready field for backwards compatibility. + ms.IonosMachine.SetV1Beta1Ready(true) + conditions.Set(ms.IonosMachine, metav1.Condition{ +``` + +with: + +```go + ms.IonosMachine.Status.Initialization.Provisioned = new(true) + conditions.Set(ms.IonosMachine, metav1.Condition{ +``` + +- [ ] **Step 3: Verify it compiles and vets** + +Run: `go build ./... && go vet ./internal/...` +Expected: no output, exit 0. + +- [ ] **Step 4: Run the affected unit tests** + +Run: `go test ./internal/controller/... ./internal/service/cloud/... -short` +Expected: `ok` for both packages (no test asserts the deprecated `ready` write). + +- [ ] **Step 5: Commit** + +```bash +git add internal/controller/ionoscloudcluster_controller.go internal/service/cloud/server.go +git commit -m "refactor: stop writing deprecated v1beta1 ready field + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 2: Drop v1beta1 condition ownership from the scope patch calls + +**Files:** +- Modify: `scope/cluster.go` (~lines 39–47 and ~230–242) +- Modify: `scope/machine.go` (~lines 38–46 and ~197–209) + +**Interfaces:** +- Consumes: `patch.WithOwnedConditions` (unchanged; kept). +- Produces: `PatchObject` on both scopes now declares only v1beta2 condition ownership. + +- [ ] **Step 1: Simplify the `ownedClusterConditions` doc comment** + +In `scope/cluster.go`, replace: + +```go +// ownedClusterConditions is the single source of truth for the v2 condition types owned by the +// cluster controller. WithOwnedV1Beta1Conditions is derived from this list (only ReadyCondition +// is carried into v1beta1; provider-specific conditions are v2-only). Keeping both lists adjacent +// and derived from one variable makes it structurally impossible to add a condition to one side +// while forgetting the other. +var ownedClusterConditions = []string{ +``` + +with: + +```go +// ownedClusterConditions is the single source of truth for the condition types owned by the +// cluster controller. +var ownedClusterConditions = []string{ +``` + +- [ ] **Step 2: Remove the v1beta1 ownership option from the cluster patch call** + +In `scope/cluster.go`, replace: + +```go + // V1Beta1 ownership covers only the cross-cutting Ready condition; provider-specific + // conditions (IonosCloudClusterReady) are v2-only and intentionally omitted here. + if patchErr := c.patchHelper.Patch(timeoutCtx, c.IonosCluster, + patch.WithOwnedV1Beta1Conditions{ + Conditions: []clusterv1.ConditionType{clusterv1.ReadyCondition}, + }, + // V2 ownership is derived from ownedClusterConditions — the single source of truth. + patch.WithOwnedConditions{ + Conditions: ownedClusterConditions, + }, + ); patchErr != nil { + return patchErr + } +``` + +with: + +```go + // Ownership is derived from ownedClusterConditions — the single source of truth. + if patchErr := c.patchHelper.Patch(timeoutCtx, c.IonosCluster, + patch.WithOwnedConditions{ + Conditions: ownedClusterConditions, + }, + ); patchErr != nil { + return patchErr + } +``` + +- [ ] **Step 3: Simplify the `ownedMachineConditions` doc comment** + +In `scope/machine.go`, replace: + +```go +// ownedMachineConditions is the single source of truth for the v2 condition types owned by the +// machine controller. WithOwnedV1Beta1Conditions is derived from this list (only ReadyCondition +// is carried into v1beta1; provider-specific conditions are v2-only). Keeping both lists adjacent +// and derived from one variable makes it structurally impossible to add a condition to one side +// while forgetting the other. +var ownedMachineConditions = []string{ +``` + +with: + +```go +// ownedMachineConditions is the single source of truth for the condition types owned by the +// machine controller. +var ownedMachineConditions = []string{ +``` + +- [ ] **Step 4: Remove the v1beta1 ownership option from the machine patch call** + +In `scope/machine.go`, replace: + +```go + if patchErr := m.patchHelper.Patch( + timeoutCtx, + m.IonosMachine, + // V1Beta1 ownership covers only the cross-cutting Ready condition; provider-specific + // conditions (MachineProvisionedCondition) are v2-only and intentionally omitted here. + patch.WithOwnedV1Beta1Conditions{Conditions: []clusterv1.ConditionType{ + clusterv1.ReadyCondition, + }}, + // V2 ownership is derived from ownedMachineConditions — the single source of truth. + patch.WithOwnedConditions{Conditions: ownedMachineConditions}, + ); patchErr != nil { + return patchErr + } +``` + +with: + +```go + if patchErr := m.patchHelper.Patch( + timeoutCtx, + m.IonosMachine, + // Ownership is derived from ownedMachineConditions — the single source of truth. + patch.WithOwnedConditions{Conditions: ownedMachineConditions}, + ); patchErr != nil { + return patchErr + } +``` + +- [ ] **Step 5: Verify build/vet (confirms `clusterv1` still used via `ReadyCondition`)** + +Run: `go build ./scope/... && go vet ./scope/...` +Expected: no output, exit 0. (If `clusterv1` reports as unused, something else changed — it must remain, used by `string(clusterv1.ReadyCondition)` in both `owned*Conditions` and `SetSummaryCondition`.) + +- [ ] **Step 6: Run scope tests** + +Run: `go test ./scope/... -short` +Expected: `ok github.com/ionos-cloud/cluster-api-provider-ionoscloud/scope`. + +- [ ] **Step 7: Commit** + +```bash +git add scope/cluster.go scope/machine.go +git commit -m "refactor(scope): drop v1beta1 condition ownership from patch calls + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 3: Remove the deprecated v1beta1 condition tests + +**Files:** +- Modify: `api/v1alpha1/ionoscloudcluster_types_test.go` +- Modify: `api/v1alpha1/ionoscloudmachine_types_test.go` + +**Interfaces:** +- Consumes: nothing new. +- Produces: the api test package no longer references `Get/SetV1Beta1Conditions` or `deprecatedv1beta1conditions`, so Task 4 can remove those helpers without breaking tests. + +- [ ] **Step 1: Delete the standalone `TestIonosCloudCluster_Conditions` unit test** + +In `api/v1alpha1/ionoscloudcluster_types_test.go`, delete this function entirely: + +```go +func TestIonosCloudCluster_Conditions(t *testing.T) { + conds := clusterv1.Conditions{{Type: "type"}} + cluster := &IonosCloudCluster{} + + cluster.SetV1Beta1Conditions(conds) + require.Equal(t, conds, cluster.GetV1Beta1Conditions()) +} +``` + +- [ ] **Step 2: Remove the deprecated assertions from the cluster "Status" spec** + +In the same file, inside `Context("Status", ...)`, delete these three lines (leaving the surrounding `Provisioned`/`CurrentRequestByDatacenter` assertions intact): + +- `Expect(fetched.GetV1Beta1Conditions()).To(BeEmpty())` +- `deprecatedv1beta1conditions.MarkTrue(fetched, clusterv1.ReadyCondition)` +- `Expect(fetched.GetV1Beta1Conditions()).To(HaveLen(1))` +- `Expect(deprecatedv1beta1conditions.IsTrue(fetched, clusterv1.ReadyCondition)).To(BeTrue())` + +After the edit that spec block reads: + +```go + Context("Status", func() { + It("should correctly get and set the status", func() { + By("initially having an empty status") + + cluster := defaultCluster() + Expect(k8sClient.Create(context.Background(), cluster)).To(Succeed()) + + key := client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name} + fetched := &IonosCloudCluster{} + Expect(k8sClient.Get(context.Background(), key, fetched)).To(Succeed()) + Expect(fetched.Status.Initialization.Provisioned).To(BeNil()) + Expect(fetched.Status.CurrentRequestByDatacenter).To(BeEmpty()) + + By("retrieving the cluster and setting the status") + fetched.Status.Initialization = IonosCloudClusterInitializationStatus{Provisioned: new(true)} + wantProvisionRequest := ProvisioningRequest{ + Method: "POST", + RequestPath: "/path/to/resource", + State: "QUEUED", + } + fetched.Status.CurrentRequestByDatacenter = map[string]ProvisioningRequest{ + "123": wantProvisionRequest, + } + + By("updating the cluster status") + Expect(k8sClient.Status().Update(context.Background(), fetched)).To(Succeed()) + + Expect(k8sClient.Get(context.Background(), key, fetched)).To(Succeed()) + Expect(fetched.Status.Initialization.Provisioned).To(HaveValue(BeTrue())) + Expect(fetched.Status.CurrentRequestByDatacenter).To(HaveLen(1)) + Expect(fetched.Status.CurrentRequestByDatacenter["123"]).To(Equal(wantProvisionRequest)) + + By("Removing the entry from the status again") + delete(fetched.Status.CurrentRequestByDatacenter, "123") + Expect(k8sClient.Status().Update(context.Background(), fetched)).To(Succeed()) + + Expect(k8sClient.Get(context.Background(), key, fetched)).To(Succeed()) + Expect(fetched.Status.CurrentRequestByDatacenter).To(BeEmpty()) + }) + }) +``` + +- [ ] **Step 3: Fix the cluster test imports (remove now-unused ones)** + +In `api/v1alpha1/ionoscloudcluster_types_test.go`, replace the import block: + +```go +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" + deprecatedv1beta1conditions "sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) +``` + +with (drop `testing`, `require`, `deprecatedv1beta1conditions`; keep `clusterv1` — still used by `clusterv1.APIEndpoint` in `defaultCluster`): + +```go +import ( + "context" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" + "sigs.k8s.io/controller-runtime/pkg/client" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) +``` + +- [ ] **Step 4: Delete the machine `Context("Conditions", ...)` spec** + +In `api/v1alpha1/ionoscloudmachine_types_test.go`, delete this entire block: + +```go + Context("Conditions", func() { + It("should correctly set and get the conditions", func() { + m := defaultMachine() + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(k8sClient.Get( + context.Background(), client.ObjectKey{Name: m.Name, Namespace: m.Namespace}, m)).To(Succeed()) + + // Calls SetConditions with required fields + deprecatedv1beta1conditions.MarkTrue(m, MachineProvisionedCondition) + + Expect(k8sClient.Status().Update(context.Background(), m)).To(Succeed()) + Expect(k8sClient.Get(context.Background(), + client.ObjectKey{Name: m.Name, Namespace: m.Namespace}, m)).To(Succeed()) + + machineConditions := m.GetV1Beta1Conditions() + Expect(machineConditions).To(HaveLen(1)) + Expect(machineConditions[0].Type).To(Equal(MachineProvisionedCondition)) + Expect(machineConditions[0].Status).To(Equal(corev1.ConditionTrue)) + }) + }) +``` + +- [ ] **Step 5: Remove the deprecated condition set-up from the machine "Status" spec** + +In the same file, inside `Context("Status", ...)`, delete the single line: + +```go + deprecatedv1beta1conditions.MarkTrue(m, MachineProvisionedCondition) +``` + +(The `want := *m.DeepCopy()` / `cmp.Diff(want.Status, m.Status)` round-trip below stays and still passes — it now compares a status without the deprecated subtree.) + +- [ ] **Step 6: Fix the machine test imports (remove `deprecatedv1beta1conditions`)** + +In `api/v1alpha1/ionoscloudmachine_types_test.go`, replace the import block: + +```go +import ( + "context" + + "github.com/google/go-cmp/cmp" + sdk "github.com/ionos-cloud/sdk-go/v6" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + deprecatedv1beta1conditions "sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) +``` + +with (drop `deprecatedv1beta1conditions`; keep `corev1` — still used by `corev1.TypedLocalObjectReference` elsewhere): + +```go +import ( + "context" + + "github.com/google/go-cmp/cmp" + sdk "github.com/ionos-cloud/sdk-go/v6" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) +``` + +- [ ] **Step 7: Verify the api test package still builds and vets** + +Run: `go vet ./api/...` +Expected: no output, exit 0. (Compilation covers the test files; a leftover unused import or missing symbol fails here.) + +- [ ] **Step 8: Commit** + +```bash +git add api/v1alpha1/ionoscloudcluster_types_test.go api/v1alpha1/ionoscloudmachine_types_test.go +git commit -m "test(api): remove deprecated v1beta1 condition tests + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 4: Remove deprecated v1beta1 status fields/helpers and regenerate + +**Files:** +- Modify: `api/v1alpha1/ionoscloudcluster_types.go` +- Modify: `api/v1alpha1/ionoscloudmachine_types.go` +- Regenerate: `api/v1alpha1/zz_generated.deepcopy.go`, `config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml`, `config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml` + +**Interfaces:** +- Consumes: nothing new. +- Produces: `IonosCloudClusterStatus` / `IonosCloudMachineStatus` no longer have a `Deprecated` field; the `Deprecated*`/`V1Beta1*` types and `Get/SetV1Beta1Conditions`/`SetV1Beta1Ready` methods no longer exist. `clusterv1` stays imported in both types files (used by the `clusterv1.ConditionType` constants). + +- [ ] **Step 1: Remove the `Deprecated` field from `IonosCloudClusterStatus`** + +In `api/v1alpha1/ionoscloudcluster_types.go`, delete these three lines (the last member of the status struct): + +```go + // Deprecated groups all status fields deprecated and scheduled for removal when v1beta1 contract support is dropped. + //+optional + Deprecated *IonosCloudClusterDeprecatedStatus `json:"deprecated,omitempty"` +``` + +- [ ] **Step 2: Remove the cluster deprecated status types** + +In the same file, delete both type declarations: + +```go +// IonosCloudClusterDeprecatedStatus groups all status fields deprecated and scheduled for removal when +// v1beta1 contract support is dropped. +type IonosCloudClusterDeprecatedStatus struct { + // V1Beta1 groups all v1beta1 status fields that are deprecated and scheduled for removal. + //+optional + V1Beta1 *IonosCloudClusterV1Beta1DeprecatedStatus `json:"v1beta1,omitempty"` +} + +// IonosCloudClusterV1Beta1DeprecatedStatus contains deprecated v1beta1 fields. +type IonosCloudClusterV1Beta1DeprecatedStatus struct { + // Ready indicates that the cluster is ready. + // + // Deprecated: Use Initialization.Provisioned instead. + //+optional + Ready bool `json:"ready,omitempty"` + + // Conditions defines current service state of the IonosCloudCluster using the deprecated v1beta1 condition type. + // + // Deprecated: Use the top-level conditions field instead. + //+optional + Conditions clusterv1.Conditions `json:"conditions,omitempty"` +} +``` + +- [ ] **Step 3: Remove the cluster deprecated helper methods** + +In the same file, delete both methods: + +```go +// GetV1Beta1Conditions returns the deprecated v1beta1 conditions from status.deprecated.v1beta1.conditions. +func (i *IonosCloudCluster) GetV1Beta1Conditions() clusterv1.Conditions { + if i.Status.Deprecated == nil || i.Status.Deprecated.V1Beta1 == nil { + return nil + } + return i.Status.Deprecated.V1Beta1.Conditions +} + +// SetV1Beta1Conditions sets the deprecated v1beta1 conditions in status.deprecated.v1beta1.conditions. +func (i *IonosCloudCluster) SetV1Beta1Conditions(conditions clusterv1.Conditions) { + if i.Status.Deprecated == nil { + i.Status.Deprecated = &IonosCloudClusterDeprecatedStatus{} + } + if i.Status.Deprecated.V1Beta1 == nil { + i.Status.Deprecated.V1Beta1 = &IonosCloudClusterV1Beta1DeprecatedStatus{} + } + i.Status.Deprecated.V1Beta1.Conditions = conditions +} +``` + +- [ ] **Step 4: Remove the `Deprecated` field from `IonosCloudMachineStatus`** + +In `api/v1alpha1/ionoscloudmachine_types.go`, delete these three lines: + +```go + // Deprecated groups all status fields deprecated and scheduled for removal when v1beta1 contract support is dropped. + //+optional + Deprecated *IonosCloudMachineDeprecatedStatus `json:"deprecated,omitempty"` +``` + +- [ ] **Step 5: Remove the machine deprecated status types** + +In the same file, delete both type declarations: + +```go +// IonosCloudMachineDeprecatedStatus groups all status fields deprecated and scheduled for removal when +// v1beta1 contract support is dropped. +type IonosCloudMachineDeprecatedStatus struct { + // V1Beta1 groups all v1beta1 status fields that are deprecated and scheduled for removal. + //+optional + V1Beta1 *IonosCloudMachineV1Beta1DeprecatedStatus `json:"v1beta1,omitempty"` +} + +// IonosCloudMachineV1Beta1DeprecatedStatus contains deprecated v1beta1 fields. +type IonosCloudMachineV1Beta1DeprecatedStatus struct { + // Ready indicates the VM has been provisioned and is ready. + // + // Deprecated: Use Initialization.Provisioned instead. + //+optional + Ready bool `json:"ready,omitempty"` + + // Conditions defines current service state of the IonosCloudMachine using the deprecated v1beta1 condition type. + // + // Deprecated: Use the top-level conditions field instead. + //+optional + Conditions clusterv1.Conditions `json:"conditions,omitempty"` +} +``` + +- [ ] **Step 6: Remove the machine deprecated helper methods** + +In the same file, delete all three methods: + +```go +// GetV1Beta1Conditions returns the deprecated v1beta1 conditions from status.deprecated.v1beta1.conditions. +func (m *IonosCloudMachine) GetV1Beta1Conditions() clusterv1.Conditions { + if m.Status.Deprecated == nil || m.Status.Deprecated.V1Beta1 == nil { + return nil + } + return m.Status.Deprecated.V1Beta1.Conditions +} + +// SetV1Beta1Conditions sets the deprecated v1beta1 conditions in status.deprecated.v1beta1.conditions. +func (m *IonosCloudMachine) SetV1Beta1Conditions(conditions clusterv1.Conditions) { + if m.Status.Deprecated == nil { + m.Status.Deprecated = &IonosCloudMachineDeprecatedStatus{} + } + if m.Status.Deprecated.V1Beta1 == nil { + m.Status.Deprecated.V1Beta1 = &IonosCloudMachineV1Beta1DeprecatedStatus{} + } + m.Status.Deprecated.V1Beta1.Conditions = conditions +} + +// SetV1Beta1Ready sets the deprecated v1beta1 ready field. +func (m *IonosCloudMachine) SetV1Beta1Ready(ready bool) { + if m.Status.Deprecated == nil { + m.Status.Deprecated = &IonosCloudMachineDeprecatedStatus{} + } + if m.Status.Deprecated.V1Beta1 == nil { + m.Status.Deprecated.V1Beta1 = &IonosCloudMachineV1Beta1DeprecatedStatus{} + } + m.Status.Deprecated.V1Beta1.Ready = ready +} +``` + +- [ ] **Step 7: Regenerate deepcopy and CRDs** + +Run: `make generate manifests` +Expected: succeeds. `git status` shows modifications to `api/v1alpha1/zz_generated.deepcopy.go` and the two CRD YAMLs under `config/crd/bases/`. + +- [ ] **Step 8: Confirm the generated artifacts no longer contain the deprecated subtree** + +Run: `grep -rniE "deprecated|v1beta1" config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml api/v1alpha1/zz_generated.deepcopy.go` +Expected: no matches (empty output). + +- [ ] **Step 9: Build and vet the whole module** + +Run: `go build ./... && go vet ./...` +Expected: no output, exit 0. (If `clusterv1` reports unused in a types file, verify the `IonosCloudClusterReady` / `MachineProvisionedCondition` constants still reference `clusterv1.ConditionType` — they must remain.) + +- [ ] **Step 10: Commit** + +```bash +git add api/v1alpha1/ionoscloudcluster_types.go api/v1alpha1/ionoscloudmachine_types.go api/v1alpha1/zz_generated.deepcopy.go config/crd/bases/ +git commit -m "feat(api)!: remove deprecated v1beta1 status fields and helpers + +BREAKING CHANGE: status.ready and the v1beta1 status.conditions are removed from +IonosCloudCluster/IonosCloudMachine. Consumers must read status.initialization.provisioned +and status.conditions (metav1.Condition). + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 5: Declare the v1beta2 contract (metadata + README) + +**Files:** +- Modify: `metadata.yaml` +- Modify: `README.md` (compatibility matrix, ~lines 50–55) + +**Interfaces:** +- Consumes: nothing. +- Produces: clusterctl sees the `0.8` release series as `contract: v1beta2`, matching the CRD annotation. + +- [ ] **Step 1: Add the v0.8 release series as v1beta2** + +In `metadata.yaml`, replace: + +```yaml +releaseSeries: +- major: 0 + minor: 7 + contract: v1beta1 +``` + +with: + +```yaml +releaseSeries: +- major: 0 + minor: 8 + contract: v1beta2 +- major: 0 + minor: 7 + contract: v1beta1 +``` + +- [ ] **Step 2: Add the v0.8 row to the README compatibility matrix** + +In `README.md`, replace the v0.7 matrix row: + +``` +| CAPIC v1alpha1 (v0.7) | ☓ | ☓ | ☓ | ✓ | +``` + +with the v0.7 row followed by a new v0.8 row: + +``` +| CAPIC v1alpha1 (v0.7) | ☓ | ☓ | ☓ | ✓ | +| CAPIC v1alpha1 (v0.8) | ☓ | ☓ | ☓ | ✓ | +``` + +- [ ] **Step 3: Add a contract note under the matrix** + +In `README.md`, immediately after the compatibility table (before the `### Kubernetes Versions` heading), insert: + +```markdown + +> **v0.8 declares the Cluster API `v1beta2` contract and drops `v1beta1`.** It requires a +> `v1beta2`-capable core (CAPI v1.11+). The deprecated `status.ready` and v1beta1 +> `status.conditions` fields are removed — read `status.initialization.provisioned` and +> `status.conditions` instead. Upgrade existing clusters via the staged path +> `v0.6 → v0.7 → v0.8`. +``` + +- [ ] **Step 4: Sanity-check YAML validity** + +Run: `go run sigs.k8s.io/cluster-api/cmd/clusterctl version >/dev/null 2>&1; grep -nE "minor:|contract:" metadata.yaml` +Expected: the `grep` output lists `minor: 8` / `contract: v1beta2` at the top, then `minor: 7` / `contract: v1beta1`, etc. (The `clusterctl` invocation is optional; if unavailable, just eyeball the YAML indentation matches the existing entries.) + +- [ ] **Step 5: Commit** + +```bash +git add metadata.yaml README.md +git commit -m "feat(metadata): declare v1beta2 contract for v0.8 + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 6: Rewrite the CAPI upgrade plan doc + +**Files:** +- Overwrite: `docs/capi-upgrade-plan.md` + +**Interfaces:** +- Consumes: nothing. +- Produces: an accurate migration doc reflecting the real v0.6/v0.7/v0.8 timeline and the clean cut. + +- [ ] **Step 1: Replace the entire contents of `docs/capi-upgrade-plan.md`** + +Write this exact content: + +````markdown +# Cluster API upgrade & v1beta2 migration + +How CAPIC moves to the Cluster API **v1beta2** contract, and how the release series line up. + +## Release timeline (actual) + +| CAPIC | CAPI | Declared contract | Implements v1beta2 | Notes | +|-------|------|-------------------|--------------------|-------| +| v0.6.x | v1.8 | v1beta1 | no | pure v1beta1 | +| v0.7.0 | v1.10 | v1beta1 | no | pure v1beta1 (CAPI dependency bump only) | +| **v0.8** | **v1.11** | **v1beta2** | yes | **pure v1beta2 — v1beta1 support dropped** | + +There is no dual-writing "bridge" release: v0.7.0 shipped as pure v1beta1, so v0.8 is the +first release to implement v1beta2, and it does so as a clean cut. + +## What v0.8 changes + +- Declares the **v1beta2** contract (`metadata.yaml`), matching the CRD + `cluster.x-k8s.io/v1beta2` annotation. +- Implements the v1beta2 status contract: `status.initialization.provisioned` and + `status.conditions` (`[]metav1.Condition`). +- **Removes** all v1beta1 residue: the `status.deprecated.v1beta1.*` fields, the + `Get/SetV1Beta1Conditions` / `SetV1Beta1Ready` helpers, and the v1beta1 condition + ownership in the patch helpers. + +## Supported upgrade path + +Staged, in place: **v0.6 → v0.7 → v0.8** (upgrade the CAPI core to v1.11+ alongside v0.8). + +- Existing clusters keep running; **no recreation** is required (no `IonosCloudMachineTemplate` + content change → no Machine rollout; running Machines are not torn down by a transient + `provisioned=nil`). +- On the `v0.7 → v0.8` hop there is a brief window where existing objects report infra + not-provisioned until the v0.8 controller reconciles them (self-heals on controller startup). +- **Breaking:** `.status.ready` and the v1beta1 `status.conditions` are removed. Any external + tooling reading them must move to `status.initialization.provisioned` / `status.conditions`. + +Empirical validation lives in the e2e upgrade suite (PR #379), which exercises the staged +`v0.6.3 → v0.7.0 → v0.8` path. + +## References + +- CAPI v1.11 → v1.12 provider migration guide: + https://cluster-api.sigs.k8s.io/developer/providers/migrations/v1.11-to-v1.12 +- Design spec: `docs/superpowers/specs/2026-07-17-drop-v1beta1-pure-v1beta2-design.md` +- Compatibility matrix: `README.md` +```` + +- [ ] **Step 2: Commit** + +```bash +git add docs/capi-upgrade-plan.md +git commit -m "docs: rewrite capi upgrade plan for pure-v1beta2 v0.8 + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 7: Full verification gate + +**Files:** none created; may regenerate/format. + +**Interfaces:** none. + +- [ ] **Step 1: Confirm no v1beta1 residue remains in production Go code** + +Run: `grep -rniE "v1beta1|V1Beta1" --include="*.go" api/ scope/ internal/ cmd/ | grep -v zz_generated | grep -viE "_test.go"` +Expected: no matches (empty output). The only `clusterv1` imports point at `.../api/core/v1beta2` (string `v1beta2`), so they do not match. + +- [ ] **Step 2: Confirm the deprecated symbols are fully gone** + +Run: `grep -rniE "SetV1Beta1Ready|GetV1Beta1Conditions|SetV1Beta1Conditions|WithOwnedV1Beta1Conditions|DeprecatedStatus" --include="*.go" . | grep -v zz_generated` +Expected: no matches (empty output). + +- [ ] **Step 3: Run linters** + +Run: `make lint` +Expected: `0 issues`. If `make lint-fix` changes formatting, re-stage and amend the most relevant commit or add a small `chore: lint-fix` commit. + +- [ ] **Step 4: Run the full test suite** + +Run: `make test` +Expected: unit tests, vet, and the api envtest integration suite all pass (`ok`/green). This exercises the retained v1beta2 status round-trip and the controller/service provisioning behavior. + +- [ ] **Step 5: Verify generated artifacts are up to date** + +Run: `make verify` +Expected: `verify-gen` and `verify-tidy` pass (no diff). If it reports a diff, run `make generate manifests` / `go mod tidy`, commit the result, and re-run. + +- [ ] **Step 6: Final commit (only if steps 3/5 produced changes)** + +```bash +git add -A +git commit -m "chore: regenerate and lint-fix after dropping v1beta1 + +Co-Authored-By: Claude Opus 4.8 " +``` + +If nothing changed, skip this step. + +--- + +## Self-Review + +**Spec coverage** (against `docs/superpowers/specs/2026-07-17-drop-v1beta1-pure-v1beta2-design.md` §4): +- §4.1 API types (fields/types/helpers) → Task 4. ✅ +- §4.2 controllers/services dual-write → Task 1. ✅ +- §4.3 scope plumbing → Task 2. ✅ +- §4.4 generated (deepcopy/CRDs) → Task 4 steps 7–8. ✅ +- §4.5 metadata + README + capi-upgrade-plan rewrite → Tasks 5 & 6. ✅ +- §4.6 tests → Task 3. ✅ +- §4.7 what stays (v1beta2 impl, CRD annotation, condition constants) → preserved by construction; asserted in Task 4 step 9 and Task 7 step 1. ✅ +- §4.8 verification (make test/verify/lint + grep + no `status.deprecated`) → Task 7 + Task 4 step 8. ✅ + +SP2 (#380 rebase) and SP3 (#379 rebase + staged e2e) are separate plans, per the spec's decomposition — intentionally out of scope here. + +**Placeholder scan:** No TBD/TODO; every code step shows the exact before/after content and every command has an expected result. ✅ + +**Type consistency:** `WithOwnedConditions` / `ownedClusterConditions` / `ownedMachineConditions` used consistently; `clusterv1` = `sigs.k8s.io/cluster-api/api/core/v1beta2` throughout; helper names (`GetV1Beta1Conditions`, `SetV1Beta1Conditions`, `SetV1Beta1Ready`) match the removals in Tasks 1/3/4. ✅ +```` From ad24989ea93020d02df1afda378355ffc6984765 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 17 Jul 2026 10:30:26 +0000 Subject: [PATCH 22/94] refactor: stop writing deprecated v1beta1 ready field Co-Authored-By: Claude Opus 4.8 --- internal/controller/ionoscloudcluster_controller.go | 8 -------- internal/service/cloud/server.go | 2 -- 2 files changed, 10 deletions(-) diff --git a/internal/controller/ionoscloudcluster_controller.go b/internal/controller/ionoscloudcluster_controller.go index f0e66b5e..ebdbedec 100644 --- a/internal/controller/ionoscloudcluster_controller.go +++ b/internal/controller/ionoscloudcluster_controller.go @@ -179,14 +179,6 @@ func (r *IonosCloudClusterReconciler) reconcileNormal( Reason: infrav1.ClusterProvisionedReason, }) clusterScope.IonosCluster.Status.Initialization.Provisioned = new(true) - // Set deprecated v1beta1 ready field for backwards compatibility. - if clusterScope.IonosCluster.Status.Deprecated == nil { - clusterScope.IonosCluster.Status.Deprecated = &infrav1.IonosCloudClusterDeprecatedStatus{} - } - if clusterScope.IonosCluster.Status.Deprecated.V1Beta1 == nil { - clusterScope.IonosCluster.Status.Deprecated.V1Beta1 = &infrav1.IonosCloudClusterV1Beta1DeprecatedStatus{} - } - clusterScope.IonosCluster.Status.Deprecated.V1Beta1.Ready = true //nolint:staticcheck // Intentionally setting deprecated field for v1beta1 backwards compatibility. return ctrl.Result{}, nil } diff --git a/internal/service/cloud/server.go b/internal/service/cloud/server.go index 875bd73b..02b5b085 100644 --- a/internal/service/cloud/server.go +++ b/internal/service/cloud/server.go @@ -146,8 +146,6 @@ func (s *Service) ReconcileServerDeletion(ctx context.Context, ms *scope.Machine // FinalizeMachineProvisioning marks the machine as provisioned. func (*Service) FinalizeMachineProvisioning(_ context.Context, ms *scope.Machine) (bool, error) { ms.IonosMachine.Status.Initialization.Provisioned = new(true) - // Set deprecated v1beta1 ready field for backwards compatibility. - ms.IonosMachine.SetV1Beta1Ready(true) conditions.Set(ms.IonosMachine, metav1.Condition{ Type: string(infrav1.MachineProvisionedCondition), Status: metav1.ConditionTrue, From 3d60075a250de40d45e44a1ef3ea27ca378eb5ad Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 17 Jul 2026 10:33:07 +0000 Subject: [PATCH 23/94] refactor(scope): drop v1beta1 condition ownership from patch calls Co-Authored-By: Claude Opus 4.8 --- scope/cluster.go | 14 +++----------- scope/machine.go | 14 +++----------- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/scope/cluster.go b/scope/cluster.go index b67df22c..556c0d52 100644 --- a/scope/cluster.go +++ b/scope/cluster.go @@ -36,11 +36,8 @@ import ( "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/locker" ) -// ownedClusterConditions is the single source of truth for the v2 condition types owned by the -// cluster controller. WithOwnedV1Beta1Conditions is derived from this list (only ReadyCondition -// is carried into v1beta1; provider-specific conditions are v2-only). Keeping both lists adjacent -// and derived from one variable makes it structurally impossible to add a condition to one side -// while forgetting the other. +// ownedClusterConditions is the single source of truth for the condition types owned by the +// cluster controller. var ownedClusterConditions = []string{ string(clusterv1.ReadyCondition), string(infrav1.IonosCloudClusterReady), @@ -227,13 +224,8 @@ func (c *Cluster) PatchObject() error { timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() - // V1Beta1 ownership covers only the cross-cutting Ready condition; provider-specific - // conditions (IonosCloudClusterReady) are v2-only and intentionally omitted here. + // Ownership is derived from ownedClusterConditions — the single source of truth. if patchErr := c.patchHelper.Patch(timeoutCtx, c.IonosCluster, - patch.WithOwnedV1Beta1Conditions{ - Conditions: []clusterv1.ConditionType{clusterv1.ReadyCondition}, - }, - // V2 ownership is derived from ownedClusterConditions — the single source of truth. patch.WithOwnedConditions{ Conditions: ownedClusterConditions, }, diff --git a/scope/machine.go b/scope/machine.go index fd6f2ba3..fc7c26b2 100644 --- a/scope/machine.go +++ b/scope/machine.go @@ -35,11 +35,8 @@ import ( "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" ) -// ownedMachineConditions is the single source of truth for the v2 condition types owned by the -// machine controller. WithOwnedV1Beta1Conditions is derived from this list (only ReadyCondition -// is carried into v1beta1; provider-specific conditions are v2-only). Keeping both lists adjacent -// and derived from one variable makes it structurally impossible to add a condition to one side -// while forgetting the other. +// ownedMachineConditions is the single source of truth for the condition types owned by the +// machine controller. var ownedMachineConditions = []string{ string(clusterv1.ReadyCondition), string(infrav1.MachineProvisionedCondition), @@ -197,12 +194,7 @@ func (m *Machine) PatchObject() error { if patchErr := m.patchHelper.Patch( timeoutCtx, m.IonosMachine, - // V1Beta1 ownership covers only the cross-cutting Ready condition; provider-specific - // conditions (MachineProvisionedCondition) are v2-only and intentionally omitted here. - patch.WithOwnedV1Beta1Conditions{Conditions: []clusterv1.ConditionType{ - clusterv1.ReadyCondition, - }}, - // V2 ownership is derived from ownedMachineConditions — the single source of truth. + // Ownership is derived from ownedMachineConditions — the single source of truth. patch.WithOwnedConditions{Conditions: ownedMachineConditions}, ); patchErr != nil { return patchErr From 74613245c52f857ab6cf29dd2abf29c7f76bee8d Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 17 Jul 2026 10:36:17 +0000 Subject: [PATCH 24/94] test(api): remove deprecated v1beta1 condition tests Co-Authored-By: Claude Opus 4.8 --- api/v1alpha1/ionoscloudcluster_types_test.go | 15 ------------- api/v1alpha1/ionoscloudmachine_types_test.go | 22 -------------------- 2 files changed, 37 deletions(-) diff --git a/api/v1alpha1/ionoscloudcluster_types_test.go b/api/v1alpha1/ionoscloudcluster_types_test.go index 58ef83cb..464c9c56 100644 --- a/api/v1alpha1/ionoscloudcluster_types_test.go +++ b/api/v1alpha1/ionoscloudcluster_types_test.go @@ -18,13 +18,10 @@ package v1alpha1 import ( "context" - "testing" - "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" - deprecatedv1beta1conditions "sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" . "github.com/onsi/ginkgo/v2" @@ -37,14 +34,6 @@ const ( exampleIP = "198.51.100.1" ) -func TestIonosCloudCluster_Conditions(t *testing.T) { - conds := clusterv1.Conditions{{Type: "type"}} - cluster := &IonosCloudCluster{} - - cluster.SetV1Beta1Conditions(conds) - require.Equal(t, conds, cluster.GetV1Beta1Conditions()) -} - func defaultCluster() *IonosCloudCluster { return &IonosCloudCluster{ ObjectMeta: metav1.ObjectMeta{ @@ -151,7 +140,6 @@ var _ = Describe("IonosCloudCluster", func() { Expect(k8sClient.Get(context.Background(), key, fetched)).To(Succeed()) Expect(fetched.Status.Initialization.Provisioned).To(BeNil()) Expect(fetched.Status.CurrentRequestByDatacenter).To(BeEmpty()) - Expect(fetched.GetV1Beta1Conditions()).To(BeEmpty()) By("retrieving the cluster and setting the status") fetched.Status.Initialization = IonosCloudClusterInitializationStatus{Provisioned: new(true)} @@ -163,7 +151,6 @@ var _ = Describe("IonosCloudCluster", func() { fetched.Status.CurrentRequestByDatacenter = map[string]ProvisioningRequest{ "123": wantProvisionRequest, } - deprecatedv1beta1conditions.MarkTrue(fetched, clusterv1.ReadyCondition) By("updating the cluster status") Expect(k8sClient.Status().Update(context.Background(), fetched)).To(Succeed()) @@ -172,8 +159,6 @@ var _ = Describe("IonosCloudCluster", func() { Expect(fetched.Status.Initialization.Provisioned).To(HaveValue(BeTrue())) Expect(fetched.Status.CurrentRequestByDatacenter).To(HaveLen(1)) Expect(fetched.Status.CurrentRequestByDatacenter["123"]).To(Equal(wantProvisionRequest)) - Expect(fetched.GetV1Beta1Conditions()).To(HaveLen(1)) - Expect(deprecatedv1beta1conditions.IsTrue(fetched, clusterv1.ReadyCondition)).To(BeTrue()) By("Removing the entry from the status again") delete(fetched.Status.CurrentRequestByDatacenter, "123") diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index 579948bf..fbc00d5b 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -23,7 +23,6 @@ import ( sdk "github.com/ionos-cloud/sdk-go/v6" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - deprecatedv1beta1conditions "sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" . "github.com/onsi/ginkgo/v2" @@ -525,26 +524,6 @@ var _ = Describe("IonosCloudMachine Tests", func() { Entry("VCPU", ServerTypeVCPU), ) }) - Context("Conditions", func() { - It("should correctly set and get the conditions", func() { - m := defaultMachine() - Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(k8sClient.Get( - context.Background(), client.ObjectKey{Name: m.Name, Namespace: m.Namespace}, m)).To(Succeed()) - - // Calls SetConditions with required fields - deprecatedv1beta1conditions.MarkTrue(m, MachineProvisionedCondition) - - Expect(k8sClient.Status().Update(context.Background(), m)).To(Succeed()) - Expect(k8sClient.Get(context.Background(), - client.ObjectKey{Name: m.Name, Namespace: m.Namespace}, m)).To(Succeed()) - - machineConditions := m.GetV1Beta1Conditions() - Expect(machineConditions).To(HaveLen(1)) - Expect(machineConditions[0].Type).To(Equal(MachineProvisionedCondition)) - Expect(machineConditions[0].Status).To(Equal(corev1.ConditionTrue)) - }) - }) Context("Status", func() { It("should correctly set and get the status", func() { m := defaultMachine() @@ -553,7 +532,6 @@ var _ = Describe("IonosCloudMachine Tests", func() { client.ObjectKey{Name: m.Name, Namespace: m.Namespace}, m)).To(Succeed()) m.Status.Initialization = IonosCloudMachineInitializationStatus{Provisioned: new(true)} - deprecatedv1beta1conditions.MarkTrue(m, MachineProvisionedCondition) m.Status.CurrentRequest = &ProvisioningRequest{ Method: "GET", RequestPath: "path/to/resource", From 45724efd0e1aed31727f51d4b67f29e9afa30cfd Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 17 Jul 2026 10:42:09 +0000 Subject: [PATCH 25/94] feat(api)!: remove deprecated v1beta1 status fields and helpers BREAKING CHANGE: status.ready and the v1beta1 status.conditions are removed from IonosCloudCluster/IonosCloudMachine. Consumers must read status.initialization.provisioned and status.conditions (metav1.Condition). Co-Authored-By: Claude Opus 4.8 --- api/v1alpha1/ionoscloudcluster_types.go | 46 --------- api/v1alpha1/ionoscloudmachine_types.go | 57 ----------- api/v1alpha1/zz_generated.deepcopy.go | 95 ------------------- ...e.cluster.x-k8s.io_ionoscloudclusters.yaml | 72 -------------- ...e.cluster.x-k8s.io_ionoscloudmachines.yaml | 72 -------------- 5 files changed, 342 deletions(-) diff --git a/api/v1alpha1/ionoscloudcluster_types.go b/api/v1alpha1/ionoscloudcluster_types.go index a597f67a..4b15f702 100644 --- a/api/v1alpha1/ionoscloudcluster_types.go +++ b/api/v1alpha1/ionoscloudcluster_types.go @@ -92,10 +92,6 @@ type IonosCloudClusterStatus struct { // ControlPlaneEndpointIPBlockID is the IONOS Cloud UUID for the control plane endpoint IP block. //+optional ControlPlaneEndpointIPBlockID string `json:"controlPlaneEndpointIPBlockID,omitempty"` - - // Deprecated groups all status fields deprecated and scheduled for removal when v1beta1 contract support is dropped. - //+optional - Deprecated *IonosCloudClusterDeprecatedStatus `json:"deprecated,omitempty"` } // IonosCloudClusterInitializationStatus provides observations of the IonosCloudCluster initialization process. @@ -108,29 +104,6 @@ type IonosCloudClusterInitializationStatus struct { Provisioned *bool `json:"provisioned,omitempty"` } -// IonosCloudClusterDeprecatedStatus groups all status fields deprecated and scheduled for removal when -// v1beta1 contract support is dropped. -type IonosCloudClusterDeprecatedStatus struct { - // V1Beta1 groups all v1beta1 status fields that are deprecated and scheduled for removal. - //+optional - V1Beta1 *IonosCloudClusterV1Beta1DeprecatedStatus `json:"v1beta1,omitempty"` -} - -// IonosCloudClusterV1Beta1DeprecatedStatus contains deprecated v1beta1 fields. -type IonosCloudClusterV1Beta1DeprecatedStatus struct { - // Ready indicates that the cluster is ready. - // - // Deprecated: Use Initialization.Provisioned instead. - //+optional - Ready bool `json:"ready,omitempty"` - - // Conditions defines current service state of the IonosCloudCluster using the deprecated v1beta1 condition type. - // - // Deprecated: Use the top-level conditions field instead. - //+optional - Conditions clusterv1.Conditions `json:"conditions,omitempty"` -} - //+kubebuilder:object:root=true //+kubebuilder:subresource:status //+kubebuilder:resource:path=ionoscloudclusters,scope=Namespaced,categories=cluster-api;ionoscloud,shortName=icc @@ -161,25 +134,6 @@ func init() { objectTypes = append(objectTypes, &IonosCloudCluster{}, &IonosCloudClusterList{}) } -// GetV1Beta1Conditions returns the deprecated v1beta1 conditions from status.deprecated.v1beta1.conditions. -func (i *IonosCloudCluster) GetV1Beta1Conditions() clusterv1.Conditions { - if i.Status.Deprecated == nil || i.Status.Deprecated.V1Beta1 == nil { - return nil - } - return i.Status.Deprecated.V1Beta1.Conditions -} - -// SetV1Beta1Conditions sets the deprecated v1beta1 conditions in status.deprecated.v1beta1.conditions. -func (i *IonosCloudCluster) SetV1Beta1Conditions(conditions clusterv1.Conditions) { - if i.Status.Deprecated == nil { - i.Status.Deprecated = &IonosCloudClusterDeprecatedStatus{} - } - if i.Status.Deprecated.V1Beta1 == nil { - i.Status.Deprecated.V1Beta1 = &IonosCloudClusterV1Beta1DeprecatedStatus{} - } - i.Status.Deprecated.V1Beta1.Conditions = conditions -} - // GetConditions returns the v1beta2 conditions from status.conditions. func (i *IonosCloudCluster) GetConditions() []metav1.Condition { return i.Status.Conditions diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index 5e223168..b1e2f9a1 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -314,10 +314,6 @@ type IonosCloudMachineStatus struct { // Location is the location of the datacenter the VM is provisioned in. //+optional Location string `json:"location"` - - // Deprecated groups all status fields deprecated and scheduled for removal when v1beta1 contract support is dropped. - //+optional - Deprecated *IonosCloudMachineDeprecatedStatus `json:"deprecated,omitempty"` } // IonosCloudMachineInitializationStatus provides observations of the IonosCloudMachine initialization process. @@ -330,29 +326,6 @@ type IonosCloudMachineInitializationStatus struct { Provisioned *bool `json:"provisioned,omitempty"` } -// IonosCloudMachineDeprecatedStatus groups all status fields deprecated and scheduled for removal when -// v1beta1 contract support is dropped. -type IonosCloudMachineDeprecatedStatus struct { - // V1Beta1 groups all v1beta1 status fields that are deprecated and scheduled for removal. - //+optional - V1Beta1 *IonosCloudMachineV1Beta1DeprecatedStatus `json:"v1beta1,omitempty"` -} - -// IonosCloudMachineV1Beta1DeprecatedStatus contains deprecated v1beta1 fields. -type IonosCloudMachineV1Beta1DeprecatedStatus struct { - // Ready indicates the VM has been provisioned and is ready. - // - // Deprecated: Use Initialization.Provisioned instead. - //+optional - Ready bool `json:"ready,omitempty"` - - // Conditions defines current service state of the IonosCloudMachine using the deprecated v1beta1 condition type. - // - // Deprecated: Use the top-level conditions field instead. - //+optional - Conditions clusterv1.Conditions `json:"conditions,omitempty"` -} - // MachineNetworkInfo contains information about the network configuration of the VM. // Before the provisioning MachineNetworkInfo may contain IP addresses to be used for provisioning. // After provisioning this information is available completely. @@ -412,36 +385,6 @@ type IonosCloudMachineList struct { Items []IonosCloudMachine `json:"items"` } -// GetV1Beta1Conditions returns the deprecated v1beta1 conditions from status.deprecated.v1beta1.conditions. -func (m *IonosCloudMachine) GetV1Beta1Conditions() clusterv1.Conditions { - if m.Status.Deprecated == nil || m.Status.Deprecated.V1Beta1 == nil { - return nil - } - return m.Status.Deprecated.V1Beta1.Conditions -} - -// SetV1Beta1Conditions sets the deprecated v1beta1 conditions in status.deprecated.v1beta1.conditions. -func (m *IonosCloudMachine) SetV1Beta1Conditions(conditions clusterv1.Conditions) { - if m.Status.Deprecated == nil { - m.Status.Deprecated = &IonosCloudMachineDeprecatedStatus{} - } - if m.Status.Deprecated.V1Beta1 == nil { - m.Status.Deprecated.V1Beta1 = &IonosCloudMachineV1Beta1DeprecatedStatus{} - } - m.Status.Deprecated.V1Beta1.Conditions = conditions -} - -// SetV1Beta1Ready sets the deprecated v1beta1 ready field. -func (m *IonosCloudMachine) SetV1Beta1Ready(ready bool) { - if m.Status.Deprecated == nil { - m.Status.Deprecated = &IonosCloudMachineDeprecatedStatus{} - } - if m.Status.Deprecated.V1Beta1 == nil { - m.Status.Deprecated.V1Beta1 = &IonosCloudMachineV1Beta1DeprecatedStatus{} - } - m.Status.Deprecated.V1Beta1.Ready = ready -} - // GetConditions returns the v1beta2 conditions from status.conditions. func (m *IonosCloudMachine) GetConditions() []metav1.Condition { return m.Status.Conditions diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 0fa8764e..b31bac1d 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -24,7 +24,6 @@ import ( "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "sigs.k8s.io/cluster-api/api/core/v1beta2" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -126,26 +125,6 @@ func (in *IonosCloudCluster) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IonosCloudClusterDeprecatedStatus) DeepCopyInto(out *IonosCloudClusterDeprecatedStatus) { - *out = *in - if in.V1Beta1 != nil { - in, out := &in.V1Beta1, &out.V1Beta1 - *out = new(IonosCloudClusterV1Beta1DeprecatedStatus) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudClusterDeprecatedStatus. -func (in *IonosCloudClusterDeprecatedStatus) DeepCopy() *IonosCloudClusterDeprecatedStatus { - if in == nil { - return nil - } - out := new(IonosCloudClusterDeprecatedStatus) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IonosCloudClusterInitializationStatus) DeepCopyInto(out *IonosCloudClusterInitializationStatus) { *out = *in @@ -243,11 +222,6 @@ func (in *IonosCloudClusterStatus) DeepCopyInto(out *IonosCloudClusterStatus) { *out = new(ProvisioningRequest) **out = **in } - if in.Deprecated != nil { - in, out := &in.Deprecated, &out.Deprecated - *out = new(IonosCloudClusterDeprecatedStatus) - (*in).DeepCopyInto(*out) - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudClusterStatus. @@ -350,28 +324,6 @@ func (in *IonosCloudClusterTemplateSpec) DeepCopy() *IonosCloudClusterTemplateSp return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IonosCloudClusterV1Beta1DeprecatedStatus) DeepCopyInto(out *IonosCloudClusterV1Beta1DeprecatedStatus) { - *out = *in - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make(v1beta2.Conditions, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudClusterV1Beta1DeprecatedStatus. -func (in *IonosCloudClusterV1Beta1DeprecatedStatus) DeepCopy() *IonosCloudClusterV1Beta1DeprecatedStatus { - if in == nil { - return nil - } - out := new(IonosCloudClusterV1Beta1DeprecatedStatus) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IonosCloudMachine) DeepCopyInto(out *IonosCloudMachine) { *out = *in @@ -399,26 +351,6 @@ func (in *IonosCloudMachine) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IonosCloudMachineDeprecatedStatus) DeepCopyInto(out *IonosCloudMachineDeprecatedStatus) { - *out = *in - if in.V1Beta1 != nil { - in, out := &in.V1Beta1, &out.V1Beta1 - *out = new(IonosCloudMachineV1Beta1DeprecatedStatus) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudMachineDeprecatedStatus. -func (in *IonosCloudMachineDeprecatedStatus) DeepCopy() *IonosCloudMachineDeprecatedStatus { - if in == nil { - return nil - } - out := new(IonosCloudMachineDeprecatedStatus) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IonosCloudMachineInitializationStatus) DeepCopyInto(out *IonosCloudMachineInitializationStatus) { *out = *in @@ -535,11 +467,6 @@ func (in *IonosCloudMachineStatus) DeepCopyInto(out *IonosCloudMachineStatus) { *out = new(ProvisioningRequest) **out = **in } - if in.Deprecated != nil { - in, out := &in.Deprecated, &out.Deprecated - *out = new(IonosCloudMachineDeprecatedStatus) - (*in).DeepCopyInto(*out) - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudMachineStatus. @@ -643,28 +570,6 @@ func (in *IonosCloudMachineTemplateSpec) DeepCopy() *IonosCloudMachineTemplateSp return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IonosCloudMachineV1Beta1DeprecatedStatus) DeepCopyInto(out *IonosCloudMachineV1Beta1DeprecatedStatus) { - *out = *in - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make(v1beta2.Conditions, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudMachineV1Beta1DeprecatedStatus. -func (in *IonosCloudMachineV1Beta1DeprecatedStatus) DeepCopy() *IonosCloudMachineV1Beta1DeprecatedStatus { - if in == nil { - return nil - } - out := new(IonosCloudMachineV1Beta1DeprecatedStatus) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MachineNetworkInfo) DeepCopyInto(out *MachineNetworkInfo) { *out = *in diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml index 96a9bf71..eb2515ce 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml @@ -250,78 +250,6 @@ spec: description: CurrentRequestByDatacenter maps data center IDs to a pending provisioning request made during reconciliation. type: object - deprecated: - description: Deprecated groups all status fields deprecated and scheduled - for removal when v1beta1 contract support is dropped. - properties: - v1beta1: - description: V1Beta1 groups all v1beta1 status fields that are - deprecated and scheduled for removal. - properties: - conditions: - description: |- - Conditions defines current service state of the IonosCloudCluster using the deprecated v1beta1 condition type. - - Deprecated: Use the top-level conditions field instead. - items: - description: Condition defines an observation of a Cluster - API resource operational state. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when - the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This field may be empty. - maxLength: 10240 - minLength: 1 - type: string - reason: - description: |- - reason is the reason for the condition's last transition in CamelCase. - The specific API may choose whether or not this field is considered a guaranteed API. - This field may be empty. - maxLength: 256 - minLength: 1 - type: string - severity: - description: |- - severity provides an explicit classification of Reason code, so the users or machines can immediately - understand the current situation and act accordingly. - The Severity field MUST be set only when Status=False. - maxLength: 32 - type: string - status: - description: status of the condition, one of True, False, - Unknown. - type: string - type: - description: |- - type of condition in CamelCase or in foo.example.com/CamelCase. - Many .condition.type values are consistent across resources like Available, but because arbitrary conditions - can be useful (see .node.status.conditions), the ability to deconflict is important. - maxLength: 256 - minLength: 1 - type: string - required: - - lastTransitionTime - - status - - type - type: object - type: array - ready: - description: |- - Ready indicates that the cluster is ready. - - Deprecated: Use Initialization.Provisioned instead. - type: boolean - type: object - type: object initialization: description: |- Initialization provides observations of the IonosCloudCluster initialization process. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml index 1dcafa4b..7d7170cb 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml @@ -462,78 +462,6 @@ spec: - method - requestPath type: object - deprecated: - description: Deprecated groups all status fields deprecated and scheduled - for removal when v1beta1 contract support is dropped. - properties: - v1beta1: - description: V1Beta1 groups all v1beta1 status fields that are - deprecated and scheduled for removal. - properties: - conditions: - description: |- - Conditions defines current service state of the IonosCloudMachine using the deprecated v1beta1 condition type. - - Deprecated: Use the top-level conditions field instead. - items: - description: Condition defines an observation of a Cluster - API resource operational state. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when - the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This field may be empty. - maxLength: 10240 - minLength: 1 - type: string - reason: - description: |- - reason is the reason for the condition's last transition in CamelCase. - The specific API may choose whether or not this field is considered a guaranteed API. - This field may be empty. - maxLength: 256 - minLength: 1 - type: string - severity: - description: |- - severity provides an explicit classification of Reason code, so the users or machines can immediately - understand the current situation and act accordingly. - The Severity field MUST be set only when Status=False. - maxLength: 32 - type: string - status: - description: status of the condition, one of True, False, - Unknown. - type: string - type: - description: |- - type of condition in CamelCase or in foo.example.com/CamelCase. - Many .condition.type values are consistent across resources like Available, but because arbitrary conditions - can be useful (see .node.status.conditions), the ability to deconflict is important. - maxLength: 256 - minLength: 1 - type: string - required: - - lastTransitionTime - - status - - type - type: object - type: array - ready: - description: |- - Ready indicates the VM has been provisioned and is ready. - - Deprecated: Use Initialization.Provisioned instead. - type: boolean - type: object - type: object initialization: description: |- Initialization provides observations of the IonosCloudMachine initialization process. From 9bf2a9944819e8c840ea988bfbcc809ac44d4123 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 17 Jul 2026 10:45:11 +0000 Subject: [PATCH 26/94] feat(metadata): declare v1beta2 contract for v0.8 Co-Authored-By: Claude Opus 4.8 --- README.md | 7 +++++++ metadata.yaml | 3 +++ 2 files changed, 10 insertions(+) diff --git a/README.md b/README.md index a4e59e17..551e3fb6 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,13 @@ This provider's versions are compatible with the following versions of Cluster A | CAPIC v1alpha1 (v0.5) | ✓ | ☓ | ☓ | ☓ | | CAPIC v1alpha1 (v0.6) | ✓ | ✓ | ✓ | ✓ | | CAPIC v1alpha1 (v0.7) | ☓ | ☓ | ☓ | ✓ | +| CAPIC v1alpha1 (v0.8) | ☓ | ☓ | ☓ | ✓ | + +> **v0.8 declares the Cluster API `v1beta2` contract and drops `v1beta1`.** It requires a +> `v1beta2`-capable core (CAPI v1.11+). The deprecated `status.ready` and v1beta1 +> `status.conditions` fields are removed — read `status.initialization.provisioned` and +> `status.conditions` instead. Upgrade existing clusters via the staged path +> `v0.6 → v0.7 → v0.8`. ### Kubernetes Versions diff --git a/metadata.yaml b/metadata.yaml index 9be90add..93072ee4 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -2,6 +2,9 @@ apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3 kind: Metadata releaseSeries: +- major: 0 + minor: 8 + contract: v1beta2 - major: 0 minor: 7 contract: v1beta1 From 4f3822bf10f6278de21ce8bfc76aee870fa26ced Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 17 Jul 2026 10:48:43 +0000 Subject: [PATCH 27/94] docs: rewrite capi upgrade plan for pure-v1beta2 v0.8 Co-Authored-By: Claude Opus 4.8 --- docs/capi-upgrade-plan.md | 46 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 docs/capi-upgrade-plan.md diff --git a/docs/capi-upgrade-plan.md b/docs/capi-upgrade-plan.md new file mode 100644 index 00000000..df809ca8 --- /dev/null +++ b/docs/capi-upgrade-plan.md @@ -0,0 +1,46 @@ +# Cluster API upgrade & v1beta2 migration + +How CAPIC moves to the Cluster API **v1beta2** contract, and how the release series line up. + +## Release timeline (actual) + +| CAPIC | CAPI | Declared contract | Implements v1beta2 | Notes | +|-------|------|-------------------|--------------------|-------| +| v0.6.x | v1.8 | v1beta1 | no | pure v1beta1 | +| v0.7.0 | v1.10 | v1beta1 | no | pure v1beta1 (CAPI dependency bump only) | +| **v0.8** | **v1.11** | **v1beta2** | yes | **pure v1beta2 — v1beta1 support dropped** | + +There is no dual-writing "bridge" release: v0.7.0 shipped as pure v1beta1, so v0.8 is the +first release to implement v1beta2, and it does so as a clean cut. + +## What v0.8 changes + +- Declares the **v1beta2** contract (`metadata.yaml`), matching the CRD + `cluster.x-k8s.io/v1beta2` annotation. +- Implements the v1beta2 status contract: `status.initialization.provisioned` and + `status.conditions` (`[]metav1.Condition`). +- **Removes** all v1beta1 residue: the `status.deprecated.v1beta1.*` fields, the + `Get/SetV1Beta1Conditions` / `SetV1Beta1Ready` helpers, and the v1beta1 condition + ownership in the patch helpers. + +## Supported upgrade path + +Staged, in place: **v0.6 → v0.7 → v0.8** (upgrade the CAPI core to v1.11+ alongside v0.8). + +- Existing clusters keep running; **no recreation** is required (no `IonosCloudMachineTemplate` + content change → no Machine rollout; running Machines are not torn down by a transient + `provisioned=nil`). +- On the `v0.7 → v0.8` hop there is a brief window where existing objects report infra + not-provisioned until the v0.8 controller reconciles them (self-heals on controller startup). +- **Breaking:** `.status.ready` and the v1beta1 `status.conditions` are removed. Any external + tooling reading them must move to `status.initialization.provisioned` / `status.conditions`. + +Empirical validation lives in the e2e upgrade suite (PR #379), which exercises the staged +`v0.6.3 → v0.7.0 → v0.8` path. + +## References + +- CAPI v1.11 → v1.12 provider migration guide: + https://cluster-api.sigs.k8s.io/developer/providers/migrations/v1.11-to-v1.12 +- Design spec: `docs/superpowers/specs/2026-07-17-drop-v1beta1-pure-v1beta2-design.md` +- Compatibility matrix: `README.md` From e1bf5fe6aecc53bca1ad3297eadd1dd040f7e91a Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 17 Jul 2026 11:05:22 +0000 Subject: [PATCH 28/94] chore: address final review findings (README v0.7 row, dead lint alias, upgrade-plan reference) Fix three minor findings from the SP1 pure-v1beta2 whole-branch review: correct the CAPIC v0.7 compatibility row to match the v1.10.10 base documented in the upgrade plan, remove the now-unused v1beta1 conditions importas alias, and link the v1.10-to-v1.11 migration guide that introduced the v1beta2 contract. Co-Authored-By: Claude Opus 4.8 --- .golangci.yml | 2 -- README.md | 2 +- docs/capi-upgrade-plan.md | 2 ++ 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 3cce430c..40f0fdfd 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -79,8 +79,6 @@ linters: alias: capie2e - pkg: sigs.k8s.io/cluster-api/util/conditions alias: conditions - - pkg: sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1 - alias: deprecatedv1beta1conditions - pkg: sigs.k8s.io/cluster-api/api/ipam/v1beta2 alias: ipamv1 - pkg: github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1 diff --git a/README.md b/README.md index 551e3fb6..17c721f5 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ This provider's versions are compatible with the following versions of Cluster A | CAPIC v1alpha1 (v0.4) | ✓ | ☓ | ☓ | ☓ | | CAPIC v1alpha1 (v0.5) | ✓ | ☓ | ☓ | ☓ | | CAPIC v1alpha1 (v0.6) | ✓ | ✓ | ✓ | ✓ | -| CAPIC v1alpha1 (v0.7) | ☓ | ☓ | ☓ | ✓ | +| CAPIC v1alpha1 (v0.7) | ☓ | ☓ | ✓ | ✓ | | CAPIC v1alpha1 (v0.8) | ☓ | ☓ | ☓ | ✓ | > **v0.8 declares the Cluster API `v1beta2` contract and drops `v1beta1`.** It requires a diff --git a/docs/capi-upgrade-plan.md b/docs/capi-upgrade-plan.md index df809ca8..133cd5af 100644 --- a/docs/capi-upgrade-plan.md +++ b/docs/capi-upgrade-plan.md @@ -40,6 +40,8 @@ Empirical validation lives in the e2e upgrade suite (PR #379), which exercises t ## References +- CAPI v1.10 → v1.11 provider migration guide: + https://cluster-api.sigs.k8s.io/developer/providers/migrations/v1.10-to-v1.11 - CAPI v1.11 → v1.12 provider migration guide: https://cluster-api.sigs.k8s.io/developer/providers/migrations/v1.11-to-v1.12 - Design spec: `docs/superpowers/specs/2026-07-17-drop-v1beta1-pure-v1beta2-design.md` From b5db700fab91229938f611b98e441d24eb802127 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 31 Jul 2026 11:34:40 +0000 Subject: [PATCH 29/94] fix(api): declare v1beta2 contract and backfill legacy condition reasons Resolve the open question from the SP1 design spec: the e2e config and release metadata now declare v1.11 as contract v1beta2, matching the CRD kustomize label (the Go +kubebuilder annotation is inert for clusterctl's contract resolution, per docs/capi-upgrade-plan.md). Add api/v1alpha1/conditions_migration.go to backfill an empty condition Reason on objects written by CAPIC <= v0.7 under the old v1beta1 conditions API. The v1beta2 metav1.Condition CRD schema requires a non-empty Reason, and verified against a real apiserver (envtest, CRD swapped in place), a fresh Create of the legacy zero-value shape (e.g. clusterctl move) is rejected without this backfill. Wire it into scope.NewCluster/NewMachine before the patch helper's diff is computed, and add omitzero to ControlPlaneEndpoint/ObjectMeta so v0.8 controller writes never re-emit the legacy zero-value shape that trips the new schema. Co-Authored-By: Claude Opus 4.8 --- api/v1alpha1/conditions_migration.go | 40 +++++++++++++++++++ api/v1alpha1/conditions_migration_test.go | 38 ++++++++++++++++++ api/v1alpha1/ionoscloudcluster_types.go | 2 +- .../ionoscloudmachinetemplate_types.go | 2 +- docs/capi-upgrade-plan.md | 21 ++++++++-- scope/cluster.go | 5 +++ scope/machine.go | 6 +++ test/e2e/config/ionoscloud.yaml | 6 +-- test/e2e/data/shared/v1.11/metadata.yaml | 2 +- 9 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 api/v1alpha1/conditions_migration.go create mode 100644 api/v1alpha1/conditions_migration_test.go diff --git a/api/v1alpha1/conditions_migration.go b/api/v1alpha1/conditions_migration.go new file mode 100644 index 00000000..dd06a4a2 --- /dev/null +++ b/api/v1alpha1/conditions_migration.go @@ -0,0 +1,40 @@ +/* +Copyright 2024 IONOS Cloud. + +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 v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// LegacyConditionMigratedReason is backfilled onto conditions written by CAPIC <= v0.7, which used +// the v1beta1 conditions API and left Reason unset. The v1beta2 metav1.Condition CRD schema requires +// a non-empty, CamelCase Reason; without backfilling it, the first status patch issued against a +// pre-upgrade object after upgrading to v0.8 fails, because the patch helper resends the whole +// status.conditions array, including untouched legacy entries. +const LegacyConditionMigratedReason = "MigratedFromV1Beta1" + +// BackfillLegacyConditionReasons sets LegacyConditionMigratedReason on any condition with an empty +// Reason. It mutates and returns the given slice, so it must be called before a patch helper is +// asked to compute a diff, on the same object the helper's "before" snapshot was taken from +// (otherwise the backfill would look like a no-op change to the helper and the invalid legacy +// value would still be resent verbatim). +func BackfillLegacyConditionReasons(conditions []metav1.Condition) []metav1.Condition { + for i := range conditions { + if conditions[i].Reason == "" { + conditions[i].Reason = LegacyConditionMigratedReason + } + } + return conditions +} diff --git a/api/v1alpha1/conditions_migration_test.go b/api/v1alpha1/conditions_migration_test.go new file mode 100644 index 00000000..05e0dda5 --- /dev/null +++ b/api/v1alpha1/conditions_migration_test.go @@ -0,0 +1,38 @@ +/* +Copyright 2024 IONOS Cloud. + +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 v1alpha1 + +import ( + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func Test_BackfillLegacyConditionReasons(t *testing.T) { + conditions := []metav1.Condition{ + {Type: "Ready", Status: metav1.ConditionTrue, Reason: ""}, + {Type: "MachineProvisioned", Status: metav1.ConditionTrue, Reason: "Provisioned"}, + } + + got := BackfillLegacyConditionReasons(conditions) + + require.Equal(t, LegacyConditionMigratedReason, got[0].Reason, + "a condition written by CAPIC <= v0.7 (empty Reason) must be backfilled") + require.Equal(t, "Provisioned", got[1].Reason, + "a condition that already carries a Reason must be left untouched") +} diff --git a/api/v1alpha1/ionoscloudcluster_types.go b/api/v1alpha1/ionoscloudcluster_types.go index 4b15f702..123f44fc 100644 --- a/api/v1alpha1/ionoscloudcluster_types.go +++ b/api/v1alpha1/ionoscloudcluster_types.go @@ -46,7 +46,7 @@ type IonosCloudClusterSpec struct { // // TODO(gfariasalves): as of now, IP must be provided by the user as we still don't insert the // provider-provided block IP into the kube-vip manifest. - ControlPlaneEndpoint clusterv1.APIEndpoint `json:"controlPlaneEndpoint,omitempty"` + ControlPlaneEndpoint clusterv1.APIEndpoint `json:"controlPlaneEndpoint,omitempty,omitzero"` // Location is the location where the data centers should be located. // diff --git a/api/v1alpha1/ionoscloudmachinetemplate_types.go b/api/v1alpha1/ionoscloudmachinetemplate_types.go index b934601f..0a1d0937 100644 --- a/api/v1alpha1/ionoscloudmachinetemplate_types.go +++ b/api/v1alpha1/ionoscloudmachinetemplate_types.go @@ -51,7 +51,7 @@ type IonosCloudMachineTemplateResource struct { // Standard object's metadata. // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata //+optional - ObjectMeta clusterv1.ObjectMeta `json:"metadata,omitempty"` + ObjectMeta clusterv1.ObjectMeta `json:"metadata,omitempty,omitzero"` // Spec is the IonosCloudMachineSpec for the IonosCloudMachineTemplate. Spec IonosCloudMachineSpec `json:"spec"` } diff --git a/docs/capi-upgrade-plan.md b/docs/capi-upgrade-plan.md index 133cd5af..f36a5f36 100644 --- a/docs/capi-upgrade-plan.md +++ b/docs/capi-upgrade-plan.md @@ -16,12 +16,13 @@ first release to implement v1beta2, and it does so as a clean cut. ## What v0.8 changes - Declares the **v1beta2** contract (`metadata.yaml`), matching the CRD - `cluster.x-k8s.io/v1beta2` annotation. + `cluster.x-k8s.io/v1beta2` label set via `config/crd/kustomization.yaml`'s `commonLabels`. + (The `+kubebuilder:metadata:annotations` marker on the Go types produces an *annotation*, + which CAPI's contract resolution does not consult — the label is what's load-bearing.) - Implements the v1beta2 status contract: `status.initialization.provisioned` and `status.conditions` (`[]metav1.Condition`). -- **Removes** all v1beta1 residue: the `status.deprecated.v1beta1.*` fields, the - `Get/SetV1Beta1Conditions` / `SetV1Beta1Ready` helpers, and the v1beta1 condition - ownership in the patch helpers. +- **Removes** `status.ready` and the v1beta1 `status.conditions` shape in place, on the same + `v1alpha1` CRD apiVersion (no new stored version, no conversion webhook). ## Supported upgrade path @@ -34,6 +35,18 @@ Staged, in place: **v0.6 → v0.7 → v0.8** (upgrade the CAPI core to v1.11+ al not-provisioned until the v0.8 controller reconciles them (self-heals on controller startup). - **Breaking:** `.status.ready` and the v1beta1 `status.conditions` are removed. Any external tooling reading them must move to `status.initialization.provisioned` / `status.conditions`. +- **Residual risk of the in-place schema change:** objects written by v0.6/v0.7 can have + `spec.controlPlaneEndpoint: {host: "", port: 0}` and conditions with an empty `reason` — both + valid under the old schema, both rejected by the new one. The v0.8 controller backfills + condition reasons and omits a zero `controlPlaneEndpoint` on its own writes + (`api/v1alpha1/conditions_migration.go`, the `omitzero` tag on `ControlPlaneEndpoint`), and an + in-place `Update` of an untouched legacy endpoint keeps working (Kubernetes CRD validation + ratcheting allows an unchanged, already-invalid field to persist across an update). What still + breaks: a fresh `Create` of the legacy zero-value shape — e.g. `clusterctl move`, which reads + the object on the source cluster and re-creates it on the target, where there is no stored + value to ratchet against. Verified empirically against a real apiserver (envtest, CRD swapped + in place): a status-only patch of a legacy object round-trips cleanly, an in-place spec `Update` + of the untouched endpoint succeeds, but a `Create` carrying the same legacy shape is rejected. Empirical validation lives in the e2e upgrade suite (PR #379), which exercises the staged `v0.6.3 → v0.7.0 → v0.8` path. diff --git a/scope/cluster.go b/scope/cluster.go index 556c0d52..2b35ff40 100644 --- a/scope/cluster.go +++ b/scope/cluster.go @@ -92,6 +92,11 @@ func NewCluster(params ClusterParams) (*Cluster, error) { return nil, fmt.Errorf("failed to init patch helper: %w", err) } + // Backfill any condition left over from CAPIC <= v0.7 (empty Reason under the old v1beta1 + // conditions API) now that the helper's "before" snapshot has already been captured, so the + // backfill shows up as a genuine, patchable change instead of being resent verbatim. + params.IonosCluster.Status.Conditions = infrav1.BackfillLegacyConditionReasons(params.IonosCluster.Status.Conditions) + clusterScope := &Cluster{ client: params.Client, Cluster: params.Cluster, diff --git a/scope/machine.go b/scope/machine.go index fc7c26b2..b98039df 100644 --- a/scope/machine.go +++ b/scope/machine.go @@ -85,6 +85,12 @@ func NewMachine(params MachineParams) (*Machine, error) { if err != nil { return nil, fmt.Errorf("failed to init patch helper: %w", err) } + + // Backfill any condition left over from CAPIC <= v0.7 (empty Reason under the old v1beta1 + // conditions API) now that the helper's "before" snapshot has already been captured, so the + // backfill shows up as a genuine, patchable change instead of being resent verbatim. + params.IonosMachine.Status.Conditions = infrav1.BackfillLegacyConditionReasons(params.IonosMachine.Status.Conditions) + return &Machine{ client: params.Client, patchHelper: helper, diff --git a/test/e2e/config/ionoscloud.yaml b/test/e2e/config/ionoscloud.yaml index 5afc7ed2..5be3369d 100644 --- a/test/e2e/config/ionoscloud.yaml +++ b/test/e2e/config/ionoscloud.yaml @@ -8,7 +8,7 @@ providers: - name: "v1.11.11" value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/core-components.yaml" type: url - contract: v1beta1 + contract: v1beta2 replacements: - old: --metrics-addr=127.0.0.1:8080 new: --metrics-addr=:8443 @@ -20,7 +20,7 @@ providers: - name: "v1.11.11" value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/bootstrap-components.yaml" type: url - contract: v1beta1 + contract: v1beta2 replacements: - old: --metrics-addr=127.0.0.1:8080 new: --metrics-addr=:8443 @@ -32,7 +32,7 @@ providers: - name: "v1.11.11" value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/control-plane-components.yaml" type: url - contract: v1beta1 + contract: v1beta2 replacements: - old: --metrics-addr=127.0.0.1:8080 new: --metrics-addr=:8443 diff --git a/test/e2e/data/shared/v1.11/metadata.yaml b/test/e2e/data/shared/v1.11/metadata.yaml index 8755a669..0249344b 100644 --- a/test/e2e/data/shared/v1.11/metadata.yaml +++ b/test/e2e/data/shared/v1.11/metadata.yaml @@ -3,7 +3,7 @@ kind: Metadata releaseSeries: - major: 1 minor: 11 - contract: v1beta1 + contract: v1beta2 - major: 1 minor: 10 contract: v1beta1 From b1e7fc413ac250ae9230bafec2c3cb62185a2323 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 31 Jul 2026 11:37:58 +0000 Subject: [PATCH 30/94] feat: upgrade cluster-api to v1.12.10 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the Cluster API dependencies to v1.12.10 following the v1.11-to-v1.12 provider migration guide: - sigs.k8s.io/cluster-api{,/test} v1.11.11 -> v1.12.10 - sigs.k8s.io/controller-runtime v0.21.0 -> v0.22.5 - k8s.io/{api,apiextensions-apiserver,apimachinery,client-go} v0.33.3 -> v0.34.3 (and transitive k8s.io/* to v0.34.x) No code changes were required: the provider does not use any of the v1.12 breaking APIs (util.IsOwnedByObject / util.IsControlledBy / collections.OwnedMachines gained a schema.GroupKind parameter, and the kubeadm-cluster-configuration annotation was removed). The APIEndpoint Host/Port optionality recommendation is already satisfied — the IonosCloudCluster CRD marks controlPlaneEndpoint with minProperties: 1 and no required host/port. make generate/manifests produce no diff. Also pins the e2e management-cluster CAPI components (core, bootstrap, control-plane) to v1.12.10 and adds test/e2e/data/shared/v1.12/metadata.yaml. Rebased onto the pure-v1beta2 v0.8 clean cut (#378): the metadata override and e2e config now declare v1.11 and v1.12 as contract v1beta2 (not v1beta1), matching this branch's v0.8 provider, which declares only v1beta2. CAPI v1.12 still serves both contracts, so this is not a behavior change for the core provider — only for how our e2e config resolves the target version during clusterctl upgrade tests. README compatibility matrix updated with a Cluster API v1.12 column, including the v0.8 row (v1beta2-compatible with both v1.11 and v1.12). Co-Authored-By: Claude Opus 4.8 --- README.md | 14 +- go.mod | 83 ++++++---- go.sum | 202 ++++++++++++++--------- test/e2e/config/ionoscloud.yaml | 18 +- test/e2e/data/shared/v1.12/metadata.yaml | 48 ++++++ 5 files changed, 231 insertions(+), 134 deletions(-) create mode 100644 test/e2e/data/shared/v1.12/metadata.yaml diff --git a/README.md b/README.md index 17c721f5..46bef4f6 100644 --- a/README.md +++ b/README.md @@ -47,13 +47,13 @@ This provider requires **Go 1.26 or newer**. The exact version is specified in ` This provider's versions are compatible with the following versions of Cluster API: -| | Cluster API v1beta1 (v1.8) | Cluster API v1beta1 (v1.9) | Cluster API v1beta1 (v1.10) | Cluster API v1beta1/v1beta2 (v1.11) | -|-----------------------|:--------------------------:|:--------------------------:|:---------------------------:|:-----------------------------------:| -| CAPIC v1alpha1 (v0.4) | ✓ | ☓ | ☓ | ☓ | -| CAPIC v1alpha1 (v0.5) | ✓ | ☓ | ☓ | ☓ | -| CAPIC v1alpha1 (v0.6) | ✓ | ✓ | ✓ | ✓ | -| CAPIC v1alpha1 (v0.7) | ☓ | ☓ | ✓ | ✓ | -| CAPIC v1alpha1 (v0.8) | ☓ | ☓ | ☓ | ✓ | +| | Cluster API v1beta1 (v1.8) | Cluster API v1beta1 (v1.9) | Cluster API v1beta1 (v1.10) | Cluster API v1beta1/v1beta2 (v1.11) | Cluster API v1beta1/v1beta2 (v1.12) | +|-----------------------|:--------------------------:|:--------------------------:|:---------------------------:|:-----------------------------------:|:-----------------------------------:| +| CAPIC v1alpha1 (v0.4) | ✓ | ☓ | ☓ | ☓ | ☓ | +| CAPIC v1alpha1 (v0.5) | ✓ | ☓ | ☓ | ☓ | ☓ | +| CAPIC v1alpha1 (v0.6) | ✓ | ✓ | ✓ | ✓ | ☓ | +| CAPIC v1alpha1 (v0.7) | ☓ | ☓ | ✓ | ✓ | ✓ | +| CAPIC v1alpha1 (v0.8) | ☓ | ☓ | ☓ | ✓ | ✓ | > **v0.8 declares the Cluster API `v1beta2` contract and drops `v1beta1`.** It requires a > `v1beta2`-capable core (CAPI v1.11+). The deprecated `status.ready` and v1beta1 diff --git a/go.mod b/go.mod index 7db8deaa..e52d3bcc 100644 --- a/go.mod +++ b/go.mod @@ -8,26 +8,29 @@ require ( github.com/google/uuid v1.6.0 github.com/ionos-cloud/sdk-go/v6 v6.3.8 github.com/jarcoal/httpmock v1.4.0 - github.com/onsi/ginkgo/v2 v2.23.4 - github.com/onsi/gomega v1.38.0 - github.com/spf13/pflag v1.0.7 + github.com/onsi/ginkgo/v2 v2.27.2 + github.com/onsi/gomega v1.38.2 + github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 - k8s.io/api v0.33.3 - k8s.io/apiextensions-apiserver v0.33.3 - k8s.io/apimachinery v0.33.3 - k8s.io/client-go v0.33.3 + k8s.io/api v0.34.3 + k8s.io/apiextensions-apiserver v0.34.3 + k8s.io/apimachinery v0.34.3 + k8s.io/client-go v0.34.3 k8s.io/klog/v2 v2.130.1 - sigs.k8s.io/cluster-api v1.11.11 - sigs.k8s.io/cluster-api/test v1.11.11 - sigs.k8s.io/controller-runtime v0.21.0 + sigs.k8s.io/cluster-api v1.12.10 + sigs.k8s.io/cluster-api/test v1.12.10 + sigs.k8s.io/controller-runtime v0.22.5 ) require ( al.essio.dev/pkg/shellescape v1.5.1 // indirect cel.dev/expr v0.25.1 // indirect + dario.cat/mergo v1.0.1 // indirect github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.5.0 // indirect + github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect @@ -50,7 +53,7 @@ require ( github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.10.1 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect @@ -61,47 +64,55 @@ require ( github.com/gobuffalo/flect v1.0.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/cel-go v0.23.2 // indirect - github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/cel-go v0.26.0 // indirect + github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-github/v53 v53.2.0 // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect + github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect + github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/moby/api v1.54.1 // indirect github.com/moby/moby/client v0.4.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect + github.com/olekukonko/errors v1.1.0 // indirect + github.com/olekukonko/ll v0.1.1 // indirect + github.com/olekukonko/tablewriter v1.0.9 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.22.0 // indirect - github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect - github.com/rivo/uniseg v0.4.2 // indirect - github.com/sagikazarmark/locafero v0.7.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.12.0 // indirect - github.com/spf13/cast v1.7.1 // indirect - github.com/spf13/cobra v1.9.1 // indirect - github.com/spf13/viper v1.20.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/cobra v1.10.1 // indirect + github.com/spf13/viper v1.21.0 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/valyala/fastjson v1.6.4 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect @@ -112,13 +123,13 @@ require ( go.opentelemetry.io/otel/sdk v1.43.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect - go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.28.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.53.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect + golang.org/x/mod v0.36.0 // indirect golang.org/x/net v0.56.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.21.0 // indirect @@ -135,15 +146,15 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiserver v0.33.3 // indirect - k8s.io/cluster-bootstrap v0.33.3 // indirect - k8s.io/component-base v0.33.3 // indirect - k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect - k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect + k8s.io/apiserver v0.34.3 // indirect + k8s.io/cluster-bootstrap v0.34.2 // indirect + k8s.io/component-base v0.34.3 // indirect + k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect - sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect - sigs.k8s.io/kind v0.30.0 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/kind v0.31.0 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index f00b7152..e0330047 100644 --- a/go.sum +++ b/go.sum @@ -42,8 +42,13 @@ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151X github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/coredns/caddy v1.1.1 h1:2eYKZT7i6yxIfGP3qLJoJ7HAsDJqYB+X68g4NYjSrE0= github.com/coredns/caddy v1.1.1/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4= -github.com/coredns/corefile-migration v1.0.30 h1:ljZNPGgna+4yKv81gfkvkgLEWdtz0NjBR1glaiPI140= -github.com/coredns/corefile-migration v1.0.30/go.mod h1:56DPqONc3njpVPsdilEnfijCwNGC3/kTJLl7i7SPavY= +github.com/coredns/corefile-migration v1.0.32 h1:tlbtXBpt7UzmedEoMqnfqOTnGCvzYfJ/Rrfqf+/W+TY= +github.com/coredns/corefile-migration v1.0.32/go.mod h1:56DPqONc3njpVPsdilEnfijCwNGC3/kTJLl7i7SPavY= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -72,8 +77,14 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= +github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= +github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -95,19 +106,20 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.23.2 h1:UdEe3CvQh3Nv+E/j9r1Y//WO0K0cSyD7/y0bzyLIMI4= -github.com/google/cel-go v0.23.2/go.mod h1:52Pb6QsDbC5kvgxvZhiL9QX1oZEkcUF/ZqaPx1J5Wwo= -github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= -github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= +github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github/v53 v53.2.0 h1:wvz3FyF53v4BK+AsnvCmeNhf8AkTaeh2SoYu/XUvTtI= @@ -117,12 +129,14 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY= +github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= @@ -135,6 +149,8 @@ github.com/jarcoal/httpmock v1.4.0 h1:BvhqnH0JAYbNudL2GMJKgOHe2CtKlzJ/5rWKyp+hc2 github.com/jarcoal/httpmock v1.4.0/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= +github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -152,16 +168,18 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI= github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= +github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= +github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= @@ -175,62 +193,67 @@ github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHo github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= -github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= -github.com/onsi/gomega v1.38.0 h1:c/WX+w8SLAinvuKKQFh77WEucCnPk4j2OTUr7lt7BeY= -github.com/onsi/gomega v1.38.0/go.mod h1:OcXcwId0b9QsE7Y49u+BTrL4IdKOBOKnD6VQNTJEB6o= +github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= +github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= +github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= +github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.1.1 h1:9Dfeed5/Mgaxb9lHRAftLK9pVfYETvHn+If6lywVhJc= +github.com/olekukonko/ll v0.1.1/go.mod h1:2dJo+hYZcJMLMbKwHEWvxCUbAOLc/CXWS9noET22Mdo= +github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8= +github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= -github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= -github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= -github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= -github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= -github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= -github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= -github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= -github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= -github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -246,12 +269,30 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= +github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/etcd/api/v3 v3.6.6 h1:mcaMp3+7JawWv69p6QShYWS8cIWUOl32bFLb6qf8pOQ= +go.etcd.io/etcd/api/v3 v3.6.6/go.mod h1:f/om26iXl2wSkcTA1zGQv8reJRSLVdoEBsi4JdfMrx4= +go.etcd.io/etcd/client/pkg/v3 v3.6.6 h1:uoqgzSOv2H9KlIF5O1Lsd8sW+eMLuV6wzE3q5GJGQNs= +go.etcd.io/etcd/client/pkg/v3 v3.6.6/go.mod h1:YngfUVmvsvOJ2rRgStIyHsKtOt9SZI2aBJrZiWJhCbI= +go.etcd.io/etcd/client/v3 v3.6.6 h1:G5z1wMf5B9SNexoxOHUGBaULurOZPIgGPsW6CN492ec= +go.etcd.io/etcd/client/v3 v3.6.6/go.mod h1:36Qv6baQ07znPR3+n7t+Rk5VHEzVYPvFfGmfF4wBHV8= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 h1:XmiuHzgJt067+a6kwyAzkhXooYVv3/TOw9cM2VfJgUM= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0/go.mod h1:KDgtbWKTQs4bM+VPUr6WlL9m/WXcmkCcBlIzqxPGzmI= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= @@ -270,8 +311,6 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09 go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= -go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= -go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -292,6 +331,8 @@ golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWB golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -312,7 +353,6 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= @@ -359,45 +399,43 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= -k8s.io/api v0.33.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8= -k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE= -k8s.io/apiextensions-apiserver v0.33.3 h1:qmOcAHN6DjfD0v9kxL5udB27SRP6SG/MTopmge3MwEs= -k8s.io/apiextensions-apiserver v0.33.3/go.mod h1:oROuctgo27mUsyp9+Obahos6CWcMISSAPzQ77CAQGz8= -k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA= -k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/apiserver v0.33.3 h1:Wv0hGc+QFdMJB4ZSiHrCgN3zL3QRatu56+rpccKC3J4= -k8s.io/apiserver v0.33.3/go.mod h1:05632ifFEe6TxwjdAIrwINHWE2hLwyADFk5mBsQa15E= -k8s.io/client-go v0.33.3 h1:M5AfDnKfYmVJif92ngN532gFqakcGi6RvaOF16efrpA= -k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg= -k8s.io/cluster-bootstrap v0.33.3 h1:u2NTxJ5CFSBFXaDxLQoOWMly8eni31psVso+caq6uwI= -k8s.io/cluster-bootstrap v0.33.3/go.mod h1:p970f8u8jf273zyQ5raD8WUu2XyAl0SAWOY82o7i/ds= -k8s.io/component-base v0.33.3 h1:mlAuyJqyPlKZM7FyaoM/LcunZaaY353RXiOd2+B5tGA= -k8s.io/component-base v0.33.3/go.mod h1:ktBVsBzkI3imDuxYXmVxZ2zxJnYTZ4HAsVj9iF09qp4= +k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= +k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= +k8s.io/apiextensions-apiserver v0.34.3 h1:p10fGlkDY09eWKOTeUSioxwLukJnm+KuDZdrW71y40g= +k8s.io/apiextensions-apiserver v0.34.3/go.mod h1:aujxvqGFRdb/cmXYfcRTeppN7S2XV/t7WMEc64zB5A0= +k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= +k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apiserver v0.34.3 h1:uGH1qpDvSiYG4HVFqc6A3L4CKiX+aBWDrrsxHYK0Bdo= +k8s.io/apiserver v0.34.3/go.mod h1:QPnnahMO5C2m3lm6fPW3+JmyQbvHZQ8uudAu/493P2w= +k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= +k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= +k8s.io/cluster-bootstrap v0.34.2 h1:oKckPeunVCns37BntcsxaOesDul32yzGd3DFLjW2fc8= +k8s.io/cluster-bootstrap v0.34.2/go.mod h1:f21byPR7X5nt12ivZi+J3pb4sG4SH6VySX8KAAJA8BY= +k8s.io/component-base v0.34.3 h1:zsEgw6ELqK0XncCQomgO9DpUIzlrYuZYA0Cgo+JWpVk= +k8s.io/component-base v0.34.3/go.mod h1:5iIlD8wPfWE/xSHTRfbjuvUul2WZbI2nOUK65XL0E/c= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= 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/cluster-api v1.11.11 h1:MvUjPlUYk72ow4HhI9lJlt0C1iY8JsyPtx5YFV8pLJA= -sigs.k8s.io/cluster-api v1.11.11/go.mod h1:wplBEE7AErSt8bN93qrqXeXQ8uEq69RfJBPPYoHWLAE= -sigs.k8s.io/cluster-api/test v1.11.11 h1:KZXBo1j9zfnysVemyX1N61K3S1PqqSvrlhJVQVNDWMk= -sigs.k8s.io/cluster-api/test v1.11.11/go.mod h1:UeTeX/FwCHTBKFylV8640zgjIDBZ3Z+c8jcDB+BfrTM= -sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= -sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= -sigs.k8s.io/kind v0.30.0 h1:2Xi1KFEfSMm0XDcvKnUt15ZfgRPCT0OnCBbpgh8DztY= -sigs.k8s.io/kind v0.30.0/go.mod h1:FSqriGaoTPruiXWfRnUXNykF8r2t+fHtK0P0m1AbGF8= -sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/cluster-api v1.12.10 h1:hllwEOtVfotdi+i1/HnVO9B5CJLgr5SBBtBL9yRtFEs= +sigs.k8s.io/cluster-api v1.12.10/go.mod h1:Xz6YnayDc2+/3OA1i6wlXrhmErV1KKJmyfs7JzMDn+k= +sigs.k8s.io/cluster-api/test v1.12.10 h1:tH6NdJuXm1hFecXdaLI2M/+Bl/A6Qua6jfgvQM2nIUo= +sigs.k8s.io/cluster-api/test v1.12.10/go.mod h1:Ol1iUdCI7HnUKjT5KTsSBxMzHsTlpxGfdtLfy5g3/RI= +sigs.k8s.io/controller-runtime v0.22.5 h1:v3nfSUMowX/2WMp27J9slwGFyAt7IV0YwBxAkrUr0GE= +sigs.k8s.io/controller-runtime v0.22.5/go.mod h1:pc5SoYWnWI6I+cBHYYdZ7B6YHZVY5xNfll88JB+vniI= +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.31.0 h1:UcT4nzm+YM7YEbqiAKECk+b6dsvc/HRZZu9U0FolL1g= +sigs.k8s.io/kind v0.31.0/go.mod h1:FSqriGaoTPruiXWfRnUXNykF8r2t+fHtK0P0m1AbGF8= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/test/e2e/config/ionoscloud.yaml b/test/e2e/config/ionoscloud.yaml index 5be3369d..be0e9008 100644 --- a/test/e2e/config/ionoscloud.yaml +++ b/test/e2e/config/ionoscloud.yaml @@ -5,39 +5,39 @@ providers: - name: cluster-api type: CoreProvider versions: - - name: "v1.11.11" - value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/core-components.yaml" + - name: "v1.12.10" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.12.10/core-components.yaml" type: url contract: v1beta2 replacements: - old: --metrics-addr=127.0.0.1:8080 new: --metrics-addr=:8443 files: - - sourcePath: "../data/shared/v1.11/metadata.yaml" + - sourcePath: "../data/shared/v1.12/metadata.yaml" - name: kubeadm type: BootstrapProvider versions: - - name: "v1.11.11" - value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/bootstrap-components.yaml" + - name: "v1.12.10" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.12.10/bootstrap-components.yaml" type: url contract: v1beta2 replacements: - old: --metrics-addr=127.0.0.1:8080 new: --metrics-addr=:8443 files: - - sourcePath: "../data/shared/v1.11/metadata.yaml" + - sourcePath: "../data/shared/v1.12/metadata.yaml" - name: kubeadm type: ControlPlaneProvider versions: - - name: "v1.11.11" - value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/control-plane-components.yaml" + - name: "v1.12.10" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.12.10/control-plane-components.yaml" type: url contract: v1beta2 replacements: - old: --metrics-addr=127.0.0.1:8080 new: --metrics-addr=:8443 files: - - sourcePath: "../data/shared/v1.11/metadata.yaml" + - sourcePath: "../data/shared/v1.12/metadata.yaml" - name: in-cluster type: IPAMProvider versions: diff --git a/test/e2e/data/shared/v1.12/metadata.yaml b/test/e2e/data/shared/v1.12/metadata.yaml new file mode 100644 index 00000000..79f34ef4 --- /dev/null +++ b/test/e2e/data/shared/v1.12/metadata.yaml @@ -0,0 +1,48 @@ +apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3 +kind: Metadata +releaseSeries: + - major: 1 + minor: 12 + contract: v1beta2 + - major: 1 + minor: 11 + contract: v1beta2 + - major: 1 + minor: 10 + contract: v1beta1 + - major: 1 + minor: 9 + contract: v1beta1 + - major: 1 + minor: 8 + contract: v1beta1 + - major: 1 + minor: 7 + contract: v1beta1 + - major: 1 + minor: 6 + contract: v1beta1 + - major: 1 + minor: 5 + contract: v1beta1 + - major: 1 + minor: 4 + contract: v1beta1 + - major: 1 + minor: 3 + contract: v1beta1 + - major: 1 + minor: 2 + contract: v1beta1 + - major: 1 + minor: 1 + contract: v1beta1 + - major: 1 + minor: 0 + contract: v1beta1 + - major: 0 + minor: 4 + contract: v1alpha4 + - major: 0 + minor: 3 + contract: v1alpha3 From e16c3743e5a664382d6dd7bb3166d9542650ba72 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 31 Jul 2026 11:48:37 +0000 Subject: [PATCH 31/94] docs: add SP3 implementation plan for adopting the staged upgrade e2e suite Co-Authored-By: Claude Opus 4.8 --- .../2026-07-31-sp3-adopt-upgrade-e2e-suite.md | 904 ++++++++++++++++++ 1 file changed, 904 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-31-sp3-adopt-upgrade-e2e-suite.md diff --git a/docs/superpowers/plans/2026-07-31-sp3-adopt-upgrade-e2e-suite.md b/docs/superpowers/plans/2026-07-31-sp3-adopt-upgrade-e2e-suite.md new file mode 100644 index 00000000..3dcf4dfa --- /dev/null +++ b/docs/superpowers/plans/2026-07-31-sp3-adopt-upgrade-e2e-suite.md @@ -0,0 +1,904 @@ +# SP3 — Adopt the staged upgrade e2e suite onto pure-v1beta2 #380 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Port the staged-upgrade e2e suite (currently on `origin/feat/upgrade-e2e-suite`, mirrored locally as `e2e-suite-v1.12`, commit `a74c4eb`) onto the rewritten PR stack `main ← #378 (feat/upgrade-cluster-api-v1.11.11) ← #380 (feat/upgrade-cluster-api-v1.12.10)`, dropping the v1beta1-bridge production-code changes that branch predates the pure-v1beta2 clean cut (`docs/superpowers/specs/2026-07-17-drop-v1beta1-pure-v1beta2-design.md`) made obsolete, and restaging the "single-hop" upgrade block through the published `v0.7.0` release instead of jumping directly from `v0.6.3` to a local `v0.8` build. + +**Architecture:** Branch `feat/upgrade-e2e-suite-v0.8` off `feat/upgrade-cluster-api-v1.12.10` (current tip `b1e7fc4`). Cherry-pick/hand-port only the e2e-only files from `e2e-suite-v1.12` (test data, helpers, config, CI, Makefile) — never its `internal/controller/`, `scope/*.go`, or `metadata.yaml` changes, which re-add the v1beta1 deprecated-status bridge that #378 intentionally removed. Rename the local dev provider tag from `v0.6.99` to `v0.8.99` throughout (it now builds v0.8, not a v0.6.x patch). Restructure the "single-hop" `Describe` block in `upgrade_test.go` to stage through the real `v0.7.0` release, matching the already-staged "multi-hop" block. + +**Tech Stack:** Ginkgo/Gomega e2e suite (`test/e2e/`, build tag `e2e`), `sigs.k8s.io/cluster-api/test/framework` `ClusterctlUpgradeSpec`, clusterctl config (`test/e2e/config/ionoscloud.yaml`), kind-based secondary management cluster. + +## Global Constraints + +- Do not reintroduce `Status.Deprecated` / `SetV1Beta1Ready` / `WithOwnedV1Beta1Conditions` anywhere in production code (`internal/`, `scope/`, `api/v1alpha1/`) — that is the SP1 decision this branch must not undo. +- `test/e2e/config/ionoscloud.yaml`'s `v1.11.11` and `v1.12.10` core/bootstrap/control-plane entries must declare `contract: v1beta2` (not `v1beta1` as `e2e-suite-v1.12` has them — that branch predates the clean cut). `v1.10.10` and `v1.8.12` stay `contract: v1beta1` (real upstream fact, unrelated to our provider). +- The local dev infra-provider version name is `v0.8.99` (was `v0.6.99`), everywhere it's referenced: `test/e2e/config/ionoscloud.yaml` and `test/e2e/upgrade_test.go`. +- Every new/ported file must build under `go build -tags e2e ./...` and `go vet -tags e2e ./...`. +- Base branch for all work: `feat/upgrade-cluster-api-v1.12.10` at commit `b1e7fc4` (SP2, already rebased onto pure-v1beta2 #378). + +--- + +### Task 1: Branch setup and e2e data/config file ports + +**Files:** +- Create: `test/e2e/data/shared/v1.8/metadata.yaml` +- Create: `test/e2e/data/shared/v1.10/metadata.yaml` +- Modify: `test/e2e/config/ionoscloud.yaml` + +**Interfaces:** none (data/config only). + +- [ ] **Step 1: Create the branch** + +```bash +git checkout -b feat/upgrade-e2e-suite-v0.8 feat/upgrade-cluster-api-v1.12.10 +``` + +- [ ] **Step 2: Add the v1.8 CAPI core metadata override** + +Create `test/e2e/data/shared/v1.8/metadata.yaml`: + +```yaml +apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3 +kind: Metadata +releaseSeries: + - major: 1 + minor: 8 + contract: v1beta1 + - major: 1 + minor: 7 + contract: v1beta1 + - major: 1 + minor: 6 + contract: v1beta1 + - major: 1 + minor: 5 + contract: v1beta1 + - major: 1 + minor: 4 + contract: v1beta1 + - major: 1 + minor: 3 + contract: v1beta1 + - major: 1 + minor: 2 + contract: v1beta1 + - major: 1 + minor: 1 + contract: v1beta1 + - major: 1 + minor: 0 + contract: v1beta1 + - major: 0 + minor: 4 + contract: v1alpha4 + - major: 0 + minor: 3 + contract: v1alpha3 +``` + +- [ ] **Step 3: Add the v1.10 CAPI core metadata override** + +Create `test/e2e/data/shared/v1.10/metadata.yaml`: + +```yaml +apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3 +kind: Metadata +releaseSeries: + - major: 1 + minor: 10 + contract: v1beta1 + - major: 1 + minor: 9 + contract: v1beta1 + - major: 1 + minor: 8 + contract: v1beta1 + - major: 1 + minor: 7 + contract: v1beta1 + - major: 1 + minor: 6 + contract: v1beta1 + - major: 1 + minor: 5 + contract: v1beta1 + - major: 1 + minor: 4 + contract: v1beta1 + - major: 1 + minor: 3 + contract: v1beta1 + - major: 1 + minor: 2 + contract: v1beta1 + - major: 1 + minor: 1 + contract: v1beta1 + - major: 1 + minor: 0 + contract: v1beta1 + - major: 0 + minor: 4 + contract: v1alpha4 + - major: 0 + minor: 3 + contract: v1alpha3 +``` + +Note: `test/e2e/data/shared/v1.12/metadata.yaml` already exists on this branch (added in SP2) with `minor: 12` and `minor: 11` both `contract: v1beta2` — do not recreate it. + +- [ ] **Step 4: Replace `test/e2e/config/ionoscloud.yaml` with the merged version** + +Register the v1.8/v1.10/v1.12 core+bootstrap+control-plane provider versions (v1.12 as `v1beta2`, matching this branch's contract, not `e2e-suite-v1.12`'s stale `v1beta1`), add the `v0.6.3`/`v0.7.0` published infra-provider versions, and rename the local dev entry to `v0.8.99`: + +```yaml +images: + - name: ghcr.io/ionos-cloud/cluster-api-provider-ionoscloud:e2e + +providers: + - name: cluster-api + type: CoreProvider + versions: + - name: "v1.12.10" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.12.10/core-components.yaml" + type: url + contract: v1beta2 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.12/metadata.yaml" + - name: "v1.11.11" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/core-components.yaml" + type: url + contract: v1beta2 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.11/metadata.yaml" + - name: "v1.10.10" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/core-components.yaml" + type: url + contract: v1beta1 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.10/metadata.yaml" + - name: "v1.8.12" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.8.12/core-components.yaml" + type: url + contract: v1beta1 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.8/metadata.yaml" + - name: kubeadm + type: BootstrapProvider + versions: + - name: "v1.12.10" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.12.10/bootstrap-components.yaml" + type: url + contract: v1beta2 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.12/metadata.yaml" + - name: "v1.11.11" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/bootstrap-components.yaml" + type: url + contract: v1beta2 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.11/metadata.yaml" + - name: "v1.10.10" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/bootstrap-components.yaml" + type: url + contract: v1beta1 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.10/metadata.yaml" + - name: "v1.8.12" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.8.12/bootstrap-components.yaml" + type: url + contract: v1beta1 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.8/metadata.yaml" + - name: kubeadm + type: ControlPlaneProvider + versions: + - name: "v1.12.10" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.12.10/control-plane-components.yaml" + type: url + contract: v1beta2 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.12/metadata.yaml" + - name: "v1.11.11" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/control-plane-components.yaml" + type: url + contract: v1beta2 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.11/metadata.yaml" + - name: "v1.10.10" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/control-plane-components.yaml" + type: url + contract: v1beta1 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.10/metadata.yaml" + - name: "v1.8.12" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.8.12/control-plane-components.yaml" + type: url + contract: v1beta1 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.8/metadata.yaml" + - name: in-cluster + type: IPAMProvider + versions: + - name: v1.0.0 + # Use manifest from source files + value: "https://github.com/kubernetes-sigs/cluster-api-ipam-provider-in-cluster/releases/download/v1.0.0/ipam-components.yaml" + type: url + contract: v1beta1 + files: + - sourcePath: "../data/shared/capi-ipam/v1.0/metadata.yaml" + replacements: + - old: "imagePullPolicy: Always" + new: "imagePullPolicy: IfNotPresent" + - name: ionoscloud + type: InfrastructureProvider + versions: + - name: v0.6.3 + value: "https://github.com/ionos-cloud/cluster-api-provider-ionoscloud/releases/download/v0.6.3/infrastructure-components.yaml" + type: url + contract: v1beta1 + files: + - sourcePath: "../data/infrastructure-ionoscloud/cluster-template.yaml" + - name: v0.7.0 + value: "https://github.com/ionos-cloud/cluster-api-provider-ionoscloud/releases/download/v0.7.0/infrastructure-components.yaml" + type: url + contract: v1beta1 + files: + # The published v0.7.0 metadata.yaml is missing its own "0.7" releaseSeries + # entry, so clusterctl can't resolve the version's contract and init fails. + # Override it with the repo metadata.yaml, which declares 0.7 as v1beta1. + - sourcePath: "../../../metadata.yaml" + - sourcePath: "../data/infrastructure-ionoscloud/cluster-template.yaml" + - name: v0.8.99 + value: "../../../config/default" + replacements: + - old: ghcr.io/ionos-cloud/cluster-api-provider-ionoscloud:dev + new: ghcr.io/ionos-cloud/cluster-api-provider-ionoscloud:e2e + - old: "--leader-elect" + new: "--leader-elect\n - --insecure-diagnostics" + files: + - sourcePath: "../../../metadata.yaml" + - sourcePath: "../data/infrastructure-ionoscloud/cluster-template.yaml" + - sourcePath: "../data/infrastructure-ionoscloud/cluster-template-ipam.yaml" + - sourcePath: "../data/infrastructure-ionoscloud/cluster-template-image-selector.yaml" +variables: + # Default variables for the e2e test; those values could be overridden via env variables, thus + # allowing the same e2e config file to be reused in different Prow jobs e.g. each one with a K8s version permutation. + # The following Kubernetes versions should be the latest versions with already published kindest/node images. + # This avoids building node images in the default case which improves the test duration significantly. + KUBERNETES_VERSION: "v1.30.6" + IP_FAMILY: "ipv4" + CNI: "./data/cni/calico.yaml" + KUBETEST_CONFIGURATION: "./data/kubetest/conformance.yaml" + CLUSTER_NAME: "e2e-cluster-${RANDOM}" + # Enabling the feature flags by setting the env variables. + # Note: EXP_CLUSTER_RESOURCE_SET is enabled per default with CAPI v1.7.0. + # We still have to enable them here for clusterctl upgrade tests that use older versions. + EXP_CLUSTER_RESOURCE_SET: "true" + IONOSCLOUD_MACHINE_MEMORY_MB: "" + IONOSCLOUD_MACHINE_NUM_CORES: "" + IONOSCLOUD_MACHINE_SSH_KEYS: "" + IONOSCLOUD_DATACENTER_ID: "" + CONTROL_PLANE_ENDPOINT_IP: "" + CONTROL_PLANE_ENDPOINT_LOCATION: "" + IONOSCLOUD_MACHINE_IMAGE_ID: "" + +intervals: + default/wait-controllers: [ "30m", "10s" ] + default/wait-cluster: [ "30m", "10s" ] + default/wait-control-plane: [ "30m", "10s" ] + default/wait-worker-nodes: [ "30m", "10s" ] + default/wait-delete-cluster: [ "30m", "10s" ] + default/wait-nodes-ready: ["30m", "10s"] + scale/wait-cluster: ["30m", "10s"] + scale/wait-control-plane: ["30m", "10s"] + scale/wait-worker-nodes: ["30m", "10s"] +``` + +- [ ] **Step 5: Sanity-check the YAML parses and commit** + +Run: `python3 -c "import yaml; yaml.safe_load(open('test/e2e/config/ionoscloud.yaml'))"` +Expected: no output (valid YAML). If `python3`/`yaml` isn't available, skip — the e2e build step in Task 5 will catch a malformed file anyway. + +```bash +git add test/e2e/config/ionoscloud.yaml test/e2e/data/shared/v1.8/metadata.yaml test/e2e/data/shared/v1.10/metadata.yaml +git commit -m "test(e2e): register CAPI v1.8.12/v1.10.10/v1.12.10 and CAPIC v0.6.3/v0.7.0 for upgrade tests + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 2: Port the upgrade-assertion helpers, stripped of v1beta1 residue + +**Files:** +- Create: `test/e2e/helpers/conditions.go` +- Create: `test/e2e/helpers/clusterhealth.go` + +**Interfaces:** +- Produces: `helpers.AssertConditionsMigration(ctx context.Context) func(framework.ClusterProxy, string, string)`, `helpers.AssertCAPIV1Beta2Migration(ctx context.Context) func(framework.ClusterProxy, string, string)`, `helpers.AssertV1Beta1ClusterAndMachinesHealthy(ctx context.Context) func(framework.ClusterProxy, string, string)` — all consumed by Task 3's `upgrade_test.go`. + +- [ ] **Step 1: Create `test/e2e/helpers/conditions.go`** + +This is `e2e-suite-v1.12`'s version of the file with the `deprecatedReady`/`Status.Deprecated.V1Beta1` assertion removed — that field no longer exists after SP1's clean cut, and there is nothing left to migrate *to* it, so the check is dropped rather than adapted: + +```go +//go:build e2e + +/* +Copyright 2024 IONOS Cloud. + +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 helpers + +import ( + "context" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clusterctlcluster "sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster" + "sigs.k8s.io/cluster-api/test/framework" + runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" + + infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" + + . "github.com/onsi/gomega" +) + +// AssertConditionsMigration returns a PostUpgrade hook that verifies CAPIC resources +// carry the v1beta2 status fields after upgrading from v0.6.x to v0.8+. +func AssertConditionsMigration(ctx context.Context) func(framework.ClusterProxy, string, string) { + return func(proxy framework.ClusterProxy, namespace, _ string) { + c := proxy.GetClient() + + clusters := &infrav1.IonosCloudClusterList{} + Expect(c.List(ctx, clusters, runtimeclient.InNamespace(namespace))).To(Succeed()) + Expect(clusters.Items).NotTo(BeEmpty(), + "expected at least one IonosCloudCluster in namespace %s", namespace) + + for i := range clusters.Items { + assertClusterConditionsMigrated(&clusters.Items[i]) + } + + machines := &infrav1.IonosCloudMachineList{} + Expect(c.List(ctx, machines, runtimeclient.InNamespace(namespace))).To(Succeed()) + Expect(machines.Items).NotTo(BeEmpty(), + "expected at least one IonosCloudMachine in namespace %s", namespace) + + for i := range machines.Items { + assertMachineConditionsMigrated(&machines.Items[i]) + } + } +} + +// AssertCAPIV1Beta2Migration returns a PostUpgrade hook that verifies CAPI core +// v1beta2 types are active and the Cluster is available after upgrading to v1.11+. +func AssertCAPIV1Beta2Migration(ctx context.Context) func(framework.ClusterProxy, string, string) { + return func(proxy framework.ClusterProxy, namespace, clusterName string) { + c := proxy.GetClient() + + // Assert the clusters.cluster.x-k8s.io CRD storage migration to v1beta2 has completed + // (status.storedVersions == [v1beta2], not just spec.versions[].storage). + framework.ValidateCRDMigration(ctx, proxy, namespace, clusterName, + func(crd apiextensionsv1.CustomResourceDefinition) bool { + return crd.Name == "clusters.cluster.x-k8s.io" + }, + clusterctlcluster.FilterClusterObjectsWithNameFilter(clusterName)) + + // Assert Cluster has Available=True. + framework.VerifyClusterAvailable(ctx, framework.VerifyClusterAvailableInput{ + Getter: proxy.GetClient(), + Name: clusterName, + Namespace: namespace, + }) + + // Assert no continuous reconcile loop (resource versions stable). + framework.ValidateResourceVersionStable(ctx, framework.ValidateResourceVersionStableInput{ + ClusterProxy: proxy, + Namespace: namespace, + OwnerGraphFilterFunction: clusterctlcluster.FilterClusterObjectsWithNameFilter(clusterName), + }) + + // Assert CAPIC resources also carry the v1beta2 fields (upgrade included CAPIC too). + clusters := &infrav1.IonosCloudClusterList{} + Expect(c.List(ctx, clusters, runtimeclient.InNamespace(namespace))).To(Succeed()) + for i := range clusters.Items { + assertClusterConditionsMigrated(&clusters.Items[i]) + } + + machines := &infrav1.IonosCloudMachineList{} + Expect(c.List(ctx, machines, runtimeclient.InNamespace(namespace))).To(Succeed()) + for i := range machines.Items { + assertMachineConditionsMigrated(&machines.Items[i]) + } + } +} + +func assertClusterConditionsMigrated(cluster *infrav1.IonosCloudCluster) { + assertConditionsMigrated("IonosCloudCluster", cluster.Name, + cluster.Status.Initialization.Provisioned, cluster.Status.Conditions) +} + +func assertMachineConditionsMigrated(machine *infrav1.IonosCloudMachine) { + assertConditionsMigrated("IonosCloudMachine", machine.Name, + machine.Status.Initialization.Provisioned, machine.Status.Conditions) +} + +// assertConditionsMigrated verifies the v1beta2 status shape shared by IonosCloudCluster +// and IonosCloudMachine after upgrading from v0.6.x/v0.7.x to v0.8+, including that any +// condition carried over from the old v1beta1 conditions API (empty Reason) was backfilled +// by api/v1alpha1.BackfillLegacyConditionReasons rather than rejected by the new schema. +func assertConditionsMigrated(kind, name string, provisioned *bool, conditions []metav1.Condition) { + // status.initialization.provisioned must be set and true. + Expect(provisioned).NotTo(BeNil(), + "%s %s: status.initialization.provisioned must be set after upgrade", kind, name) + Expect(*provisioned).To(BeTrue(), + "%s %s: status.initialization.provisioned must be true after upgrade", kind, name) + + // status.conditions (metav1.Condition) must be non-empty. + Expect(conditions).NotTo(BeEmpty(), + "%s %s: status.conditions must be non-empty after upgrade", kind, name) + hasTrue := false + for _, cond := range conditions { + if cond.Status == metav1.ConditionTrue { + hasTrue = true + } + Expect(cond.Reason).NotTo(BeEmpty(), + "%s %s: condition %q must carry a non-empty Reason after upgrade "+ + "(legacy v1beta1 conditions must be backfilled, see api/v1alpha1.BackfillLegacyConditionReasons)", + kind, name, cond.Type) + } + Expect(hasTrue).To(BeTrue(), + "%s %s: at least one metav1.Condition must have Status=True", kind, name) +} +``` + +- [ ] **Step 2: Create `test/e2e/helpers/clusterhealth.go` verbatim** + +This file only reads the CAPI-core `clusterv1beta1.Cluster`/`MachineList` `.Status.Phase` — nothing here touches our provider's removed fields, so it ports unchanged: + +```go +//go:build e2e + +/* +Copyright 2024 IONOS Cloud. + +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 helpers + +import ( + "context" + + clusterv1beta1 "sigs.k8s.io/cluster-api/api/core/v1beta1" //nolint:staticcheck // only contract CAPI v1.10.x serves + "sigs.k8s.io/cluster-api/test/framework" + runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" + + . "github.com/onsi/gomega" +) + +// AssertV1Beta1ClusterAndMachinesHealthy is a PostUpgrade hook checking Cluster/Machine +// Status.Phase only — safe to use when the upgrade target doesn't serve v1beta2 yet. +func AssertV1Beta1ClusterAndMachinesHealthy(ctx context.Context) func(framework.ClusterProxy, string, string) { + return func(proxy framework.ClusterProxy, namespace, clusterName string) { + c := proxy.GetClient() + + cluster := &clusterv1beta1.Cluster{} + Expect(c.Get(ctx, runtimeclient.ObjectKey{Namespace: namespace, Name: clusterName}, cluster)).To(Succeed()) + Expect(cluster.Status.Phase).To(Equal(string(clusterv1beta1.ClusterPhaseProvisioned)), + "Cluster %s should be in the Provisioned phase", clusterName) + + machines := &clusterv1beta1.MachineList{} + Expect(c.List(ctx, machines, runtimeclient.InNamespace(namespace), + runtimeclient.MatchingLabels{clusterv1beta1.ClusterNameLabel: clusterName})).To(Succeed()) + Expect(machines.Items).NotTo(BeEmpty(), "expected at least one Machine for cluster %s", clusterName) + + for _, m := range machines.Items { + Expect(m.Status.Phase).To(Equal(string(clusterv1beta1.MachinePhaseRunning)), + "Machine %s should be in the Running phase", m.Name) + } + } +} +``` + +- [ ] **Step 3: Compile-check and commit** + +Run: `go build -tags e2e ./test/e2e/...` +Expected: builds clean (both helpers only reference types that already exist on this branch: `infrav1.IonosCloudClusterList/IonosCloudMachineList`, `Status.Initialization.Provisioned`, `Status.Conditions` — none of the removed `Status.Deprecated` fields). + +```bash +git add test/e2e/helpers/conditions.go test/e2e/helpers/clusterhealth.go +git commit -m "test(e2e): add upgrade PostUpgrade assertion helpers for conditions migration + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 3: Port and restage `upgrade_test.go` + +**Files:** +- Create: `test/e2e/upgrade_test.go` + +**Interfaces:** +- Consumes: `helpers.AssertConditionsMigration`, `helpers.AssertCAPIV1Beta2Migration`, `helpers.AssertV1Beta1ClusterAndMachinesHealthy` (Task 2); `e2eConfig`, `clusterctlConfigPath`, `bootstrapClusterProxy`, `artifactFolder`, `skipCleanup`, `cloudEnv.createCredentialsSecretPNC`, `KubernetesVersion` (all pre-existing in `test/e2e/suite_test.go`/`common.go`, unchanged by this plan). + +- [ ] **Step 1: Create `test/e2e/upgrade_test.go`** + +Three blocks, ported from `e2e-suite-v1.12`'s `upgrade_test.go` with two changes: every `v0.6.99` local-dev infra-provider reference becomes `v0.8.99`, and the first ("single-hop") block is restaged to go through the published `v0.7.0` release instead of jumping straight from `v0.6.3` to the local build — the direct jump would otherwise silently skip proving the `v0.6.3 → v0.7.0` hop that `BackfillLegacyConditionReasons` (`api/v1alpha1/conditions_migration.go`) exists to protect: + +```go +//go:build e2e + +/* +Copyright 2024 IONOS Cloud. + +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 e2e + +import ( + capie2e "sigs.k8s.io/cluster-api/test/e2e" + + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/test/e2e/helpers" + + . "github.com/onsi/ginkgo/v2" +) + +// upgradeSpecBaseInput returns the ClusterctlUpgradeSpecInput fields shared by all +// upgrade test blocks. Each block overrides init/upgrade versions and PostUpgrade. +func upgradeSpecBaseInput() capie2e.ClusterctlUpgradeSpecInput { + return capie2e.ClusterctlUpgradeSpecInput{ + E2EConfig: e2eConfig, + ClusterctlConfigPath: clusterctlConfigPath, + BootstrapClusterProxy: bootstrapClusterProxy, + ArtifactFolder: artifactFolder, + SkipCleanup: skipCleanup, + UseKindForManagementCluster: true, + PostNamespaceCreated: cloudEnv.createCredentialsSecretPNC, + // InitWithKubernetesVersion is required by ClusterctlUpgradeSpec: it is the + // Kubernetes version of the secondary (kind) management cluster the old + // providers are installed into. Reuse the suite's configured version. + InitWithKubernetesVersion: e2eConfig.MustGetVariable(KubernetesVersion), + ControlPlaneMachineCount: new(int64(1)), + } +} + +var _ = Describe("Should migrate CAPIC conditions from v1beta1 to metav1.Condition on the staged v0.6.3 -> v0.7.0 -> v0.8 provider upgrade", Label("upgrade", "single-hop"), func() { + capie2e.ClusterctlUpgradeSpec(ctx, func() capie2e.ClusterctlUpgradeSpecInput { + in := upgradeSpecBaseInput() + in.InitWithBinary = "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.8.12/clusterctl-{OS}-{ARCH}" + in.InitWithCoreProvider = "cluster-api:v1.8.12" + // Pin bootstrap/control-plane to the same version as core. Without this they + // fall back to the latest matching the "*" contract (v1.12.10), which would + // mismatch the v1.8.12 core provider and fail clusterctl init. + in.InitWithBootstrapProviders = []string{"kubeadm:v1.8.12"} + in.InitWithControlPlaneProviders = []string{"kubeadm:v1.8.12"} + in.InitWithInfrastructureProviders = []string{"ionoscloud:v0.6.3"} + in.Upgrades = []capie2e.ClusterctlUpgradeSpecInputUpgrade{ + { + // Real released hop: proves v0.6.3 (v1.8.12, v1beta1) -> v0.7.0 (v1.10.10, + // still pure v1beta1 — see docs/capi-upgrade-plan.md) survives before the + // pure-v1beta2 v0.8 cut is applied. + WithBinary: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/clusterctl-{OS}-{ARCH}", + CoreProvider: "cluster-api:v1.10.10", + InfrastructureProviders: []string{"ionoscloud:v0.7.0"}, + PostUpgrade: helpers.AssertV1Beta1ClusterAndMachinesHealthy(ctx), + }, + { + // This hop lands the pure-v1beta2 v0.8 provider (local build) on a + // v0.7.0-created cluster and proves the legacy v1beta1 conditions + // (empty Reason) are backfilled, not rejected by the new schema. + CoreProvider: "cluster-api:v1.11.11", + InfrastructureProviders: []string{"ionoscloud:v0.8.99"}, + PostUpgrade: helpers.AssertConditionsMigration(ctx), + }, + { + // Extra hop bumping CAPI core to v1.12.10. The v0.8.99 provider is + // already built against v1.12.10, so this exercises the v1.11 -> v1.12 + // core upgrade and confirms the migrated conditions stay intact. + CoreProvider: "cluster-api:v1.12.10", + InfrastructureProviders: []string{"ionoscloud:v0.8.99"}, + PostUpgrade: helpers.AssertCAPIV1Beta2Migration(ctx), + }, + } + return in + }) +}) + +var _ = Describe("Should keep a v1.8-created cluster reconcilable after staging the upgrade through v1.10 and v1.11 before reaching v1.12", Label("upgrade", "multi-hop"), func() { + capie2e.ClusterctlUpgradeSpec(ctx, func() capie2e.ClusterctlUpgradeSpecInput { + in := upgradeSpecBaseInput() + in.InitWithBinary = "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.8.12/clusterctl-{OS}-{ARCH}" + in.InitWithCoreProvider = "cluster-api:v1.8.12" + in.InitWithBootstrapProviders = []string{"kubeadm:v1.8.12"} + in.InitWithControlPlaneProviders = []string{"kubeadm:v1.8.12"} + in.InitWithInfrastructureProviders = []string{"ionoscloud:v0.6.3"} + in.Upgrades = []capie2e.ClusterctlUpgradeSpecInputUpgrade{ + { + // Landing point this block validates. Must not be the last entry (v1.10 has + // no v1beta2 API). WithBinary is required too: the in-process v1.12.10 + // clusterctl client refuses to move the core provider anywhere but v1beta2. + WithBinary: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/clusterctl-{OS}-{ARCH}", + CoreProvider: "cluster-api:v1.10.10", + InfrastructureProviders: []string{"ionoscloud:v0.7.0"}, + PostUpgrade: helpers.AssertV1Beta1ClusterAndMachinesHealthy(ctx), + }, + { + // Intermediate v1beta2 landing at v1.11 before the final v1.12 hop. + CoreProvider: "cluster-api:v1.11.11", + InfrastructureProviders: []string{"ionoscloud:v0.8.99"}, + PostUpgrade: helpers.AssertCAPIV1Beta2Migration(ctx), + }, + { + // Final hop bumping CAPI core to v1.12.10 (the version v0.8.99 is built + // against). Also satisfies the framework's mandatory last-step v1beta2 checks. + CoreProvider: "cluster-api:v1.12.10", + InfrastructureProviders: []string{"ionoscloud:v0.8.99"}, + PostUpgrade: helpers.AssertCAPIV1Beta2Migration(ctx), + }, + } + return in + }) +}) + +// Overlaps with the staged block's second hop on purpose: starting fresh at v1.10.10 +// isolates a real v1.10->v1.11 bug from one caused by the staged block's carried-over state. +var _ = Describe("Should handle CAPI core v1beta2 type migration when upgrading from v1.10 through v1.11 to v1.12", Label("upgrade", "single-hop"), func() { + capie2e.ClusterctlUpgradeSpec(ctx, func() capie2e.ClusterctlUpgradeSpecInput { + in := upgradeSpecBaseInput() + in.InitWithBinary = "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/clusterctl-{OS}-{ARCH}" + in.InitWithCoreProvider = "cluster-api:v1.10.10" + // Pin bootstrap/control-plane to match the v1.10.10 core provider (see block above). + in.InitWithBootstrapProviders = []string{"kubeadm:v1.10.10"} + in.InitWithControlPlaneProviders = []string{"kubeadm:v1.10.10"} + in.InitWithInfrastructureProviders = []string{"ionoscloud:v0.7.0"} + in.Upgrades = []capie2e.ClusterctlUpgradeSpecInputUpgrade{ + { + CoreProvider: "cluster-api:v1.11.11", + InfrastructureProviders: []string{"ionoscloud:v0.8.99"}, + PostUpgrade: helpers.AssertCAPIV1Beta2Migration(ctx), + }, + { + // Extra hop bumping CAPI core to v1.12.10 (the version v0.8.99 is built + // against) to validate the v1.11 -> v1.12 core upgrade. + CoreProvider: "cluster-api:v1.12.10", + InfrastructureProviders: []string{"ionoscloud:v0.8.99"}, + PostUpgrade: helpers.AssertCAPIV1Beta2Migration(ctx), + }, + } + return in + }) +}) +``` + +- [ ] **Step 2: Compile-check and commit** + +Run: `go build -tags e2e ./test/e2e/...` and `go vet -tags e2e ./test/e2e/...` +Expected: both clean. + +```bash +git add test/e2e/upgrade_test.go +git commit -m "test(e2e): add staged upgrade suite for conditions schema and CAPI v1beta2 migration + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 4: CI wiring — Makefile target and scheduled workflow + +**Files:** +- Modify: `Makefile` +- Create: `.github/workflows/e2e-upgrade.yaml` + +**Interfaces:** none (build/CI plumbing only). + +- [ ] **Step 1: Add the `test-e2e-upgrade` Makefile target and exclude `upgrade` from the default e2e run** + +In `Makefile`, find: + +```makefile +GINKGO_LABEL ?= "!Conformance" +``` + +Replace with: + +```makefile +# Exclude the "upgrade" label by default: the provider-upgrade suite is heavy +# (spins up its own kind management cluster and installs old providers per block) +# and is run separately via `make test-e2e-upgrade`. +GINKGO_LABEL ?= "!Conformance && !upgrade" +``` + +Then, immediately after the existing `test-e2e:` target's recipe (the block ending in `-e2e.skip-resource-cleanup=$(SKIP_RESOURCE_CLEANUP) -e2e.use-existing-cluster=$(USE_EXISTING_CLUSTER)`), add: + +```makefile +.PHONY: test-e2e-upgrade +test-e2e-upgrade: docker-build-e2e ## Run only the provider-upgrade e2e tests (Label "upgrade") + CGO_ENABLED=1 go run github.com/onsi/ginkgo/v2/ginkgo -v --trace \ + -poll-progress-after=$(GINKGO_POLL_PROGRESS_AFTER) \ + -poll-progress-interval=$(GINKGO_POLL_PROGRESS_INTERVAL) --tags=e2e --fail-fast \ + --nodes=1 --label-filter="upgrade" --timeout=$(GINKGO_TIMEOUT) --no-color=$(GINKGO_NOCOLOR) \ + --output-dir="$(ARTIFACTS)" --junit-report="junit.e2e_upgrade.1.xml" $(GINKGO_ARGS) $(ROOT_DIR)/$(TEST_DIR)/e2e -- \ + -e2e.artifacts-folder="$(ARTIFACTS)" -e2e.config="$(E2E_CONF_FILE)" \ + -e2e.skip-resource-cleanup=$(SKIP_RESOURCE_CLEANUP) -e2e.use-existing-cluster=$(USE_EXISTING_CLUSTER) +``` + +- [ ] **Step 2: Create the scheduled upgrade-e2e workflow** + +Create `.github/workflows/e2e-upgrade.yaml`: + +```yaml +name: End-to-end upgrade tests +on: + schedule: + # Weekly, matching the conformance suite's cadence. Offset to Tuesday to + # avoid contending for IONOS Cloud resources with conformance (Mondays). + - cron: "0 5 * * 2" + workflow_dispatch: {} +jobs: + e2e-upgrade: + runs-on: ubuntu-latest + environment: e2e + env: + IONOS_TOKEN: ${{ secrets.IONOS_TOKEN }} + IONOSCLOUD_MACHINE_IMAGE_ID: ${{ vars.IONOSCLOUD_MACHINE_IMAGE_ID }} + CONTROL_PLANE_ENDPOINT_LOCATION: ${{ vars.CONTROL_PLANE_ENDPOINT_LOCATION }} + IONOSCLOUD_CUBE_TEMPLATE_ID: ${{ vars.IONOSCLOUD_CUBE_TEMPLATE_ID }} + steps: + - uses: actions/checkout@v4 + + - name: Setup Go environment + uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + + - name: Run e2e upgrade tests + run: make test-e2e-upgrade + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + if: success() || failure() + with: + name: logs-upgrade + path: _artifacts + retention-days: 7 +``` + +- [ ] **Step 3: Verify the Makefile parses and commit** + +Run: `make -n test-e2e-upgrade | head -5` +Expected: prints the planned `ginkgo` command line without executing it (no error about an unknown target). + +```bash +git add Makefile .github/workflows/e2e-upgrade.yaml +git commit -m "ci(e2e): add scheduled upgrade e2e workflow and test-e2e-upgrade target + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 5: Full verification gate + +**Files:** none created; verification only. + +**Interfaces:** none. + +- [ ] **Step 1: Confirm no v1beta1 production-code residue was reintroduced** + +Run: `grep -rniE "SetV1Beta1Ready|GetV1Beta1Conditions|SetV1Beta1Conditions|WithOwnedV1Beta1Conditions|Status.Deprecated" --include="*.go" internal/ scope/ api/ | grep -v zz_generated` +Expected: no matches. If any appear, the wrong hunks from `e2e-suite-v1.12` were ported — undo them, keeping only the `test/e2e/`, `Makefile`, and `.github/` changes from this plan. + +- [ ] **Step 2: Build everything, including the e2e build tag** + +Run: `go build ./... && go build -tags e2e ./...` +Expected: both succeed with no output. + +- [ ] **Step 3: Vet everything, including the e2e build tag** + +Run: `go vet ./... && go vet -tags e2e ./...` +Expected: both succeed with no output. + +- [ ] **Step 4: Run the full non-e2e test suite** + +Run: `make unit-test` +Expected: all packages `ok`, race detector clean. + +- [ ] **Step 5: Verify generated artifacts and go.mod/go.sum are still up to date** + +Run: `make verify` +Expected: `verify-gen` and `verify-tidy` pass (no diff). This task doesn't touch generated code or dependencies, so it should be a no-op; if it reports a diff, something in Task 1-4 accidentally changed a non-e2e file. + +- [ ] **Step 6: Confirm the new branch sits cleanly on #380** + +Run: `git log --oneline feat/upgrade-cluster-api-v1.12.10..feat/upgrade-e2e-suite-v0.8` +Expected: exactly 4 commits (Tasks 1-4, in order), all authored on top of `b1e7fc4`. + +--- + +## Self-Review + +**Spec coverage** (against `docs/superpowers/specs/2026-07-17-drop-v1beta1-pure-v1beta2-design.md` §6): +- "Rebase `feat/upgrade-e2e-suite` onto #380" → done via fresh branch off `feat/upgrade-cluster-api-v1.12.10`, porting only e2e-scoped files (Tasks 1-4), explicitly excluding the v1beta1-bridge production-code hunks. ✅ +- "Replace the direct `v0.6.3 → v0.6.99(local)` block with the staged path `v0.6.3 → v0.7.0 → v0.8(local build)`" → Task 3, first `Describe` block. ✅ +- "Bump the local dev provider version tag... to `v0.8.99`" → Task 1 Step 4 (config) and Task 3 (test file), every reference renamed. ✅ +- "Reconcile e2e helpers that reference v1beta1" → Task 2: `clusterhealth.go` ports unchanged (only touches CAPI-core v1beta1 types, which still exist and are legitimately asserted mid-upgrade); `conditions.go` drops the `Status.Deprecated.V1Beta1` assertion, replaced with a Reason-non-empty check that documents *why* (backfill, not removal-without-replacement). ✅ +- "Prerequisite satisfied: v0.6.3 and v0.7.0 are both published" → verified as an assumption carried from the existing suite design (`docs/superpowers/specs/2026-06-16-upgrade-e2e-suite-design.md`), not re-derived here. + +**Deviation from the source branch, called out explicitly:** `e2e-suite-v1.12`'s `test/e2e/config/ionoscloud.yaml` declares `v1.11.11`/`v1.12.10` core as `contract: v1beta1` — that branch predates the 2026-07-17 pure-v1beta2 decision. This plan keeps them `v1beta2`, matching the already-committed fix on `feat/upgrade-cluster-api-v1.11.11` (commit `b5db700`) and `feat/upgrade-cluster-api-v1.12.10` (commit `b1e7fc4`). + +**Placeholder scan:** No TBD/TODO; every file-creation step shows complete file content; every command has an expected result. ✅ + +**Type consistency:** `helpers.AssertConditionsMigration` / `AssertCAPIV1Beta2Migration` / `AssertV1Beta1ClusterAndMachinesHealthy` signatures in Task 2 match their call sites in Task 3 exactly (`func(context.Context) func(framework.ClusterProxy, string, string)`). Infra-provider version string `v0.8.99` is consistent across Task 1's config and Task 3's test file. ✅ From c2685256ee3a8684ea79dd5f34464512e353c70b Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 31 Jul 2026 11:49:57 +0000 Subject: [PATCH 32/94] test(e2e): register CAPI v1.8.12/v1.10.10/v1.12.10 and CAPIC v0.6.3/v0.7.0 for upgrade tests Co-Authored-By: Claude Opus 4.8 --- test/e2e/config/ionoscloud.yaml | 100 +++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/test/e2e/config/ionoscloud.yaml b/test/e2e/config/ionoscloud.yaml index be0e9008..bb565f9d 100644 --- a/test/e2e/config/ionoscloud.yaml +++ b/test/e2e/config/ionoscloud.yaml @@ -14,6 +14,33 @@ providers: new: --metrics-addr=:8443 files: - sourcePath: "../data/shared/v1.12/metadata.yaml" + - name: "v1.11.11" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/core-components.yaml" + type: url + contract: v1beta2 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.11/metadata.yaml" + - name: "v1.10.10" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/core-components.yaml" + type: url + contract: v1beta1 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.10/metadata.yaml" + - name: "v1.8.12" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.8.12/core-components.yaml" + type: url + contract: v1beta1 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.8/metadata.yaml" - name: kubeadm type: BootstrapProvider versions: @@ -26,6 +53,33 @@ providers: new: --metrics-addr=:8443 files: - sourcePath: "../data/shared/v1.12/metadata.yaml" + - name: "v1.11.11" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/bootstrap-components.yaml" + type: url + contract: v1beta2 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.11/metadata.yaml" + - name: "v1.10.10" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/bootstrap-components.yaml" + type: url + contract: v1beta1 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.10/metadata.yaml" + - name: "v1.8.12" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.8.12/bootstrap-components.yaml" + type: url + contract: v1beta1 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.8/metadata.yaml" - name: kubeadm type: ControlPlaneProvider versions: @@ -38,6 +92,33 @@ providers: new: --metrics-addr=:8443 files: - sourcePath: "../data/shared/v1.12/metadata.yaml" + - name: "v1.11.11" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/control-plane-components.yaml" + type: url + contract: v1beta2 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.11/metadata.yaml" + - name: "v1.10.10" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/control-plane-components.yaml" + type: url + contract: v1beta1 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.10/metadata.yaml" + - name: "v1.8.12" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.8.12/control-plane-components.yaml" + type: url + contract: v1beta1 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.8/metadata.yaml" - name: in-cluster type: IPAMProvider versions: @@ -54,7 +135,23 @@ providers: - name: ionoscloud type: InfrastructureProvider versions: - - name: v0.6.99 + - name: v0.6.3 + value: "https://github.com/ionos-cloud/cluster-api-provider-ionoscloud/releases/download/v0.6.3/infrastructure-components.yaml" + type: url + contract: v1beta1 + files: + - sourcePath: "../data/infrastructure-ionoscloud/cluster-template.yaml" + - name: v0.7.0 + value: "https://github.com/ionos-cloud/cluster-api-provider-ionoscloud/releases/download/v0.7.0/infrastructure-components.yaml" + type: url + contract: v1beta1 + files: + # The published v0.7.0 metadata.yaml is missing its own "0.7" releaseSeries + # entry, so clusterctl can't resolve the version's contract and init fails. + # Override it with the repo metadata.yaml, which declares 0.7 as v1beta1. + - sourcePath: "../../../metadata.yaml" + - sourcePath: "../data/infrastructure-ionoscloud/cluster-template.yaml" + - name: v0.8.99 value: "../../../config/default" replacements: - old: ghcr.io/ionos-cloud/cluster-api-provider-ionoscloud:dev @@ -72,6 +169,7 @@ variables: # The following Kubernetes versions should be the latest versions with already published kindest/node images. # This avoids building node images in the default case which improves the test duration significantly. KUBERNETES_VERSION: "v1.30.6" + IP_FAMILY: "ipv4" CNI: "./data/cni/calico.yaml" KUBETEST_CONFIGURATION: "./data/kubetest/conformance.yaml" CLUSTER_NAME: "e2e-cluster-${RANDOM}" From 5f5a4995d90c99845098993c079461e1c66cab37 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 31 Jul 2026 11:53:10 +0000 Subject: [PATCH 33/94] test(e2e): add upgrade PostUpgrade assertion helpers for conditions migration Co-Authored-By: Claude Opus 4.8 --- test/e2e/helpers/clusterhealth.go | 52 +++++++++++ test/e2e/helpers/conditions.go | 140 ++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 test/e2e/helpers/clusterhealth.go create mode 100644 test/e2e/helpers/conditions.go diff --git a/test/e2e/helpers/clusterhealth.go b/test/e2e/helpers/clusterhealth.go new file mode 100644 index 00000000..934a526c --- /dev/null +++ b/test/e2e/helpers/clusterhealth.go @@ -0,0 +1,52 @@ +//go:build e2e + +/* +Copyright 2024 IONOS Cloud. + +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 helpers + +import ( + "context" + + clusterv1beta1 "sigs.k8s.io/cluster-api/api/core/v1beta1" //nolint:staticcheck // only contract CAPI v1.10.x serves + "sigs.k8s.io/cluster-api/test/framework" + runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" + + . "github.com/onsi/gomega" +) + +// AssertV1Beta1ClusterAndMachinesHealthy is a PostUpgrade hook checking Cluster/Machine +// Status.Phase only — safe to use when the upgrade target doesn't serve v1beta2 yet. +func AssertV1Beta1ClusterAndMachinesHealthy(ctx context.Context) func(framework.ClusterProxy, string, string) { + return func(proxy framework.ClusterProxy, namespace, clusterName string) { + c := proxy.GetClient() + + cluster := &clusterv1beta1.Cluster{} + Expect(c.Get(ctx, runtimeclient.ObjectKey{Namespace: namespace, Name: clusterName}, cluster)).To(Succeed()) + Expect(cluster.Status.Phase).To(Equal(string(clusterv1beta1.ClusterPhaseProvisioned)), + "Cluster %s should be in the Provisioned phase", clusterName) + + machines := &clusterv1beta1.MachineList{} + Expect(c.List(ctx, machines, runtimeclient.InNamespace(namespace), + runtimeclient.MatchingLabels{clusterv1beta1.ClusterNameLabel: clusterName})).To(Succeed()) + Expect(machines.Items).NotTo(BeEmpty(), "expected at least one Machine for cluster %s", clusterName) + + for _, m := range machines.Items { + Expect(m.Status.Phase).To(Equal(string(clusterv1beta1.MachinePhaseRunning)), + "Machine %s should be in the Running phase", m.Name) + } + } +} diff --git a/test/e2e/helpers/conditions.go b/test/e2e/helpers/conditions.go new file mode 100644 index 00000000..5a41839b --- /dev/null +++ b/test/e2e/helpers/conditions.go @@ -0,0 +1,140 @@ +//go:build e2e + +/* +Copyright 2024 IONOS Cloud. + +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 helpers + +import ( + "context" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clusterctlcluster "sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster" + "sigs.k8s.io/cluster-api/test/framework" + runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" + + infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" + + . "github.com/onsi/gomega" +) + +// AssertConditionsMigration returns a PostUpgrade hook that verifies CAPIC resources +// carry the v1beta2 status fields after upgrading from v0.6.x to v0.8+. +func AssertConditionsMigration(ctx context.Context) func(framework.ClusterProxy, string, string) { + return func(proxy framework.ClusterProxy, namespace, _ string) { + c := proxy.GetClient() + + clusters := &infrav1.IonosCloudClusterList{} + Expect(c.List(ctx, clusters, runtimeclient.InNamespace(namespace))).To(Succeed()) + Expect(clusters.Items).NotTo(BeEmpty(), + "expected at least one IonosCloudCluster in namespace %s", namespace) + + for i := range clusters.Items { + assertClusterConditionsMigrated(&clusters.Items[i]) + } + + machines := &infrav1.IonosCloudMachineList{} + Expect(c.List(ctx, machines, runtimeclient.InNamespace(namespace))).To(Succeed()) + Expect(machines.Items).NotTo(BeEmpty(), + "expected at least one IonosCloudMachine in namespace %s", namespace) + + for i := range machines.Items { + assertMachineConditionsMigrated(&machines.Items[i]) + } + } +} + +// AssertCAPIV1Beta2Migration returns a PostUpgrade hook that verifies CAPI core +// v1beta2 types are active and the Cluster is available after upgrading to v1.11+. +func AssertCAPIV1Beta2Migration(ctx context.Context) func(framework.ClusterProxy, string, string) { + return func(proxy framework.ClusterProxy, namespace, clusterName string) { + c := proxy.GetClient() + + // Assert the clusters.cluster.x-k8s.io CRD storage migration to v1beta2 has completed + // (status.storedVersions == [v1beta2], not just spec.versions[].storage). + framework.ValidateCRDMigration(ctx, proxy, namespace, clusterName, + func(crd apiextensionsv1.CustomResourceDefinition) bool { + return crd.Name == "clusters.cluster.x-k8s.io" + }, + clusterctlcluster.FilterClusterObjectsWithNameFilter(clusterName)) + + // Assert Cluster has Available=True. + framework.VerifyClusterAvailable(ctx, framework.VerifyClusterAvailableInput{ + Getter: proxy.GetClient(), + Name: clusterName, + Namespace: namespace, + }) + + // Assert no continuous reconcile loop (resource versions stable). + framework.ValidateResourceVersionStable(ctx, framework.ValidateResourceVersionStableInput{ + ClusterProxy: proxy, + Namespace: namespace, + OwnerGraphFilterFunction: clusterctlcluster.FilterClusterObjectsWithNameFilter(clusterName), + }) + + // Assert CAPIC resources also carry the v1beta2 fields (upgrade included CAPIC too). + clusters := &infrav1.IonosCloudClusterList{} + Expect(c.List(ctx, clusters, runtimeclient.InNamespace(namespace))).To(Succeed()) + for i := range clusters.Items { + assertClusterConditionsMigrated(&clusters.Items[i]) + } + + machines := &infrav1.IonosCloudMachineList{} + Expect(c.List(ctx, machines, runtimeclient.InNamespace(namespace))).To(Succeed()) + for i := range machines.Items { + assertMachineConditionsMigrated(&machines.Items[i]) + } + } +} + +func assertClusterConditionsMigrated(cluster *infrav1.IonosCloudCluster) { + assertConditionsMigrated("IonosCloudCluster", cluster.Name, + cluster.Status.Initialization.Provisioned, cluster.Status.Conditions) +} + +func assertMachineConditionsMigrated(machine *infrav1.IonosCloudMachine) { + assertConditionsMigrated("IonosCloudMachine", machine.Name, + machine.Status.Initialization.Provisioned, machine.Status.Conditions) +} + +// assertConditionsMigrated verifies the v1beta2 status shape shared by IonosCloudCluster +// and IonosCloudMachine after upgrading from v0.6.x/v0.7.x to v0.8+, including that any +// condition carried over from the old v1beta1 conditions API (empty Reason) was backfilled +// by api/v1alpha1.BackfillLegacyConditionReasons rather than rejected by the new schema. +func assertConditionsMigrated(kind, name string, provisioned *bool, conditions []metav1.Condition) { + // status.initialization.provisioned must be set and true. + Expect(provisioned).NotTo(BeNil(), + "%s %s: status.initialization.provisioned must be set after upgrade", kind, name) + Expect(*provisioned).To(BeTrue(), + "%s %s: status.initialization.provisioned must be true after upgrade", kind, name) + + // status.conditions (metav1.Condition) must be non-empty. + Expect(conditions).NotTo(BeEmpty(), + "%s %s: status.conditions must be non-empty after upgrade", kind, name) + hasTrue := false + for _, cond := range conditions { + if cond.Status == metav1.ConditionTrue { + hasTrue = true + } + Expect(cond.Reason).NotTo(BeEmpty(), + "%s %s: condition %q must carry a non-empty Reason after upgrade "+ + "(legacy v1beta1 conditions must be backfilled, see api/v1alpha1.BackfillLegacyConditionReasons)", + kind, name, cond.Type) + } + Expect(hasTrue).To(BeTrue(), + "%s %s: at least one metav1.Condition must have Status=True", kind, name) +} From 10116267eb25efd64768c82147af7fc76c75be4c Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 31 Jul 2026 11:56:05 +0000 Subject: [PATCH 34/94] test(e2e): add staged upgrade suite for conditions schema and CAPI v1beta2 migration Co-Authored-By: Claude Opus 4.8 --- test/e2e/upgrade_test.go | 153 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 test/e2e/upgrade_test.go diff --git a/test/e2e/upgrade_test.go b/test/e2e/upgrade_test.go new file mode 100644 index 00000000..434ed328 --- /dev/null +++ b/test/e2e/upgrade_test.go @@ -0,0 +1,153 @@ +//go:build e2e + +/* +Copyright 2024 IONOS Cloud. + +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 e2e + +import ( + capie2e "sigs.k8s.io/cluster-api/test/e2e" + + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/test/e2e/helpers" + + . "github.com/onsi/ginkgo/v2" +) + +// upgradeSpecBaseInput returns the ClusterctlUpgradeSpecInput fields shared by all +// upgrade test blocks. Each block overrides init/upgrade versions and PostUpgrade. +func upgradeSpecBaseInput() capie2e.ClusterctlUpgradeSpecInput { + return capie2e.ClusterctlUpgradeSpecInput{ + E2EConfig: e2eConfig, + ClusterctlConfigPath: clusterctlConfigPath, + BootstrapClusterProxy: bootstrapClusterProxy, + ArtifactFolder: artifactFolder, + SkipCleanup: skipCleanup, + UseKindForManagementCluster: true, + PostNamespaceCreated: cloudEnv.createCredentialsSecretPNC, + // InitWithKubernetesVersion is required by ClusterctlUpgradeSpec: it is the + // Kubernetes version of the secondary (kind) management cluster the old + // providers are installed into. Reuse the suite's configured version. + InitWithKubernetesVersion: e2eConfig.MustGetVariable(KubernetesVersion), + ControlPlaneMachineCount: new(int64(1)), + } +} + +var _ = Describe("Should migrate CAPIC conditions from v1beta1 to metav1.Condition on the staged v0.6.3 -> v0.7.0 -> v0.8 provider upgrade", Label("upgrade", "single-hop"), func() { + capie2e.ClusterctlUpgradeSpec(ctx, func() capie2e.ClusterctlUpgradeSpecInput { + in := upgradeSpecBaseInput() + in.InitWithBinary = "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.8.12/clusterctl-{OS}-{ARCH}" + in.InitWithCoreProvider = "cluster-api:v1.8.12" + // Pin bootstrap/control-plane to the same version as core. Without this they + // fall back to the latest matching the "*" contract (v1.12.10), which would + // mismatch the v1.8.12 core provider and fail clusterctl init. + in.InitWithBootstrapProviders = []string{"kubeadm:v1.8.12"} + in.InitWithControlPlaneProviders = []string{"kubeadm:v1.8.12"} + in.InitWithInfrastructureProviders = []string{"ionoscloud:v0.6.3"} + in.Upgrades = []capie2e.ClusterctlUpgradeSpecInputUpgrade{ + { + // Real released hop: proves v0.6.3 (v1.8.12, v1beta1) -> v0.7.0 (v1.10.10, + // still pure v1beta1 — see docs/capi-upgrade-plan.md) survives before the + // pure-v1beta2 v0.8 cut is applied. + WithBinary: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/clusterctl-{OS}-{ARCH}", + CoreProvider: "cluster-api:v1.10.10", + InfrastructureProviders: []string{"ionoscloud:v0.7.0"}, + PostUpgrade: helpers.AssertV1Beta1ClusterAndMachinesHealthy(ctx), + }, + { + // This hop lands the pure-v1beta2 v0.8 provider (local build) on a + // v0.7.0-created cluster and proves the legacy v1beta1 conditions + // (empty Reason) are backfilled, not rejected by the new schema. + CoreProvider: "cluster-api:v1.11.11", + InfrastructureProviders: []string{"ionoscloud:v0.8.99"}, + PostUpgrade: helpers.AssertConditionsMigration(ctx), + }, + { + // Extra hop bumping CAPI core to v1.12.10. The v0.8.99 provider is + // already built against v1.12.10, so this exercises the v1.11 -> v1.12 + // core upgrade and confirms the migrated conditions stay intact. + CoreProvider: "cluster-api:v1.12.10", + InfrastructureProviders: []string{"ionoscloud:v0.8.99"}, + PostUpgrade: helpers.AssertCAPIV1Beta2Migration(ctx), + }, + } + return in + }) +}) + +var _ = Describe("Should keep a v1.8-created cluster reconcilable after staging the upgrade through v1.10 and v1.11 before reaching v1.12", Label("upgrade", "multi-hop"), func() { + capie2e.ClusterctlUpgradeSpec(ctx, func() capie2e.ClusterctlUpgradeSpecInput { + in := upgradeSpecBaseInput() + in.InitWithBinary = "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.8.12/clusterctl-{OS}-{ARCH}" + in.InitWithCoreProvider = "cluster-api:v1.8.12" + in.InitWithBootstrapProviders = []string{"kubeadm:v1.8.12"} + in.InitWithControlPlaneProviders = []string{"kubeadm:v1.8.12"} + in.InitWithInfrastructureProviders = []string{"ionoscloud:v0.6.3"} + in.Upgrades = []capie2e.ClusterctlUpgradeSpecInputUpgrade{ + { + // Landing point this block validates. Must not be the last entry (v1.10 has + // no v1beta2 API). WithBinary is required too: the in-process v1.12.10 + // clusterctl client refuses to move the core provider anywhere but v1beta2. + WithBinary: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/clusterctl-{OS}-{ARCH}", + CoreProvider: "cluster-api:v1.10.10", + InfrastructureProviders: []string{"ionoscloud:v0.7.0"}, + PostUpgrade: helpers.AssertV1Beta1ClusterAndMachinesHealthy(ctx), + }, + { + // Intermediate v1beta2 landing at v1.11 before the final v1.12 hop. + CoreProvider: "cluster-api:v1.11.11", + InfrastructureProviders: []string{"ionoscloud:v0.8.99"}, + PostUpgrade: helpers.AssertCAPIV1Beta2Migration(ctx), + }, + { + // Final hop bumping CAPI core to v1.12.10 (the version v0.8.99 is built + // against). Also satisfies the framework's mandatory last-step v1beta2 checks. + CoreProvider: "cluster-api:v1.12.10", + InfrastructureProviders: []string{"ionoscloud:v0.8.99"}, + PostUpgrade: helpers.AssertCAPIV1Beta2Migration(ctx), + }, + } + return in + }) +}) + +// Overlaps with the staged block's second hop on purpose: starting fresh at v1.10.10 +// isolates a real v1.10->v1.11 bug from one caused by the staged block's carried-over state. +var _ = Describe("Should handle CAPI core v1beta2 type migration when upgrading from v1.10 through v1.11 to v1.12", Label("upgrade", "single-hop"), func() { + capie2e.ClusterctlUpgradeSpec(ctx, func() capie2e.ClusterctlUpgradeSpecInput { + in := upgradeSpecBaseInput() + in.InitWithBinary = "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/clusterctl-{OS}-{ARCH}" + in.InitWithCoreProvider = "cluster-api:v1.10.10" + // Pin bootstrap/control-plane to match the v1.10.10 core provider (see block above). + in.InitWithBootstrapProviders = []string{"kubeadm:v1.10.10"} + in.InitWithControlPlaneProviders = []string{"kubeadm:v1.10.10"} + in.InitWithInfrastructureProviders = []string{"ionoscloud:v0.7.0"} + in.Upgrades = []capie2e.ClusterctlUpgradeSpecInputUpgrade{ + { + CoreProvider: "cluster-api:v1.11.11", + InfrastructureProviders: []string{"ionoscloud:v0.8.99"}, + PostUpgrade: helpers.AssertCAPIV1Beta2Migration(ctx), + }, + { + // Extra hop bumping CAPI core to v1.12.10 (the version v0.8.99 is built + // against) to validate the v1.11 -> v1.12 core upgrade. + CoreProvider: "cluster-api:v1.12.10", + InfrastructureProviders: []string{"ionoscloud:v0.8.99"}, + PostUpgrade: helpers.AssertCAPIV1Beta2Migration(ctx), + }, + } + return in + }) +}) From 64690fc187e0a26480955f00ef9f928fe5d08e1a Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 31 Jul 2026 11:58:53 +0000 Subject: [PATCH 35/94] ci(e2e): add scheduled upgrade e2e workflow and test-e2e-upgrade target Co-Authored-By: Claude Opus 4.8 --- .github/workflows/e2e-upgrade.yaml | 34 ++++++++++++++++++++++++++++++ Makefile | 15 ++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/e2e-upgrade.yaml diff --git a/.github/workflows/e2e-upgrade.yaml b/.github/workflows/e2e-upgrade.yaml new file mode 100644 index 00000000..9c97f493 --- /dev/null +++ b/.github/workflows/e2e-upgrade.yaml @@ -0,0 +1,34 @@ +name: End-to-end upgrade tests +on: + schedule: + # Weekly, matching the conformance suite's cadence. Offset to Tuesday to + # avoid contending for IONOS Cloud resources with conformance (Mondays). + - cron: "0 5 * * 2" + workflow_dispatch: {} +jobs: + e2e-upgrade: + runs-on: ubuntu-latest + environment: e2e + env: + IONOS_TOKEN: ${{ secrets.IONOS_TOKEN }} + IONOSCLOUD_MACHINE_IMAGE_ID: ${{ vars.IONOSCLOUD_MACHINE_IMAGE_ID }} + CONTROL_PLANE_ENDPOINT_LOCATION: ${{ vars.CONTROL_PLANE_ENDPOINT_LOCATION }} + IONOSCLOUD_CUBE_TEMPLATE_ID: ${{ vars.IONOSCLOUD_CUBE_TEMPLATE_ID }} + steps: + - uses: actions/checkout@v4 + + - name: Setup Go environment + uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + + - name: Run e2e upgrade tests + run: make test-e2e-upgrade + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + if: success() || failure() + with: + name: logs-upgrade + path: _artifacts + retention-days: 7 diff --git a/Makefile b/Makefile index 4b4cc4ef..31eb2d9d 100644 --- a/Makefile +++ b/Makefile @@ -283,7 +283,10 @@ GINKGO_NODES ?= 1 GINKGO_FOCUS ?= GINKGO_TIMEOUT ?= 2h GINKGO_NOCOLOR ?= false -GINKGO_LABEL ?= "!Conformance" +# Exclude the "upgrade" label by default: the provider-upgrade suite is heavy +# (spins up its own kind management cluster and installs old providers per block) +# and is run separately via `make test-e2e-upgrade`. +GINKGO_LABEL ?= "!Conformance && !upgrade" GINKGO_SKIP ?= # To set multiple ginkgo skip flags, if any @@ -305,6 +308,16 @@ test-e2e: docker-build-e2e ## Run the end-to-end tests -e2e.artifacts-folder="$(ARTIFACTS)" -e2e.config="$(E2E_CONF_FILE)" \ -e2e.skip-resource-cleanup=$(SKIP_RESOURCE_CLEANUP) -e2e.use-existing-cluster=$(USE_EXISTING_CLUSTER) +.PHONY: test-e2e-upgrade +test-e2e-upgrade: docker-build-e2e ## Run only the provider-upgrade e2e tests (Label "upgrade") + CGO_ENABLED=1 go run github.com/onsi/ginkgo/v2/ginkgo -v --trace \ + -poll-progress-after=$(GINKGO_POLL_PROGRESS_AFTER) \ + -poll-progress-interval=$(GINKGO_POLL_PROGRESS_INTERVAL) --tags=e2e --fail-fast \ + --nodes=1 --label-filter="upgrade" --timeout=$(GINKGO_TIMEOUT) --no-color=$(GINKGO_NOCOLOR) \ + --output-dir="$(ARTIFACTS)" --junit-report="junit.e2e_upgrade.1.xml" $(GINKGO_ARGS) $(ROOT_DIR)/$(TEST_DIR)/e2e -- \ + -e2e.artifacts-folder="$(ARTIFACTS)" -e2e.config="$(E2E_CONF_FILE)" \ + -e2e.skip-resource-cleanup=$(SKIP_RESOURCE_CLEANUP) -e2e.use-existing-cluster=$(USE_EXISTING_CLUSTER) + .PHONY: ionosctl ionosctl: $(LOCALBIN) ## Download the latest release of ionosctl and add to the bin folder @if [ ! -f $(LOCALBIN)/ionosctl ]; then \ From e662368c9763843579a725d6b4d4a8a65297d0a9 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 31 Jul 2026 12:12:45 +0000 Subject: [PATCH 36/94] fix(e2e): address final-review findings (scheme, provider versions, dedup blocks, timeout) - Set KindManagementClusterNewClusterProxyFunc to the repo's own initScheme() so PostUpgrade hooks can list infrav1 types against the kind-based secondary management cluster; the framework's default initScheme() only registers upstream CAPI schemes. - Add matching BootstrapProviders/ControlPlaneProviders to every upgrade hop so the kubeadm bootstrap/control-plane providers are actually upgraded alongside core/infrastructure, instead of staying pinned. - Merge the now-duplicate "single-hop" and "multi-hop" Describe blocks into one staged block that runs both the conditions-migration and v1beta2-migration assertions at the v1.11 hop, avoiding a redundant multi-hour cloud run. Relabel the remaining two-hop block from "single-hop" to "isolated-hop" to match what it actually does. - Set GINKGO_TIMEOUT=4h in the scheduled e2e-upgrade workflow's env, since the Makefile's 2h default is unlikely to cover multiple real cluster provisioning hops. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/e2e-upgrade.yaml | 3 ++ test/e2e/upgrade_test.go | 71 ++++++++++++------------------ 2 files changed, 32 insertions(+), 42 deletions(-) diff --git a/.github/workflows/e2e-upgrade.yaml b/.github/workflows/e2e-upgrade.yaml index 9c97f493..92346f81 100644 --- a/.github/workflows/e2e-upgrade.yaml +++ b/.github/workflows/e2e-upgrade.yaml @@ -14,6 +14,9 @@ jobs: IONOSCLOUD_MACHINE_IMAGE_ID: ${{ vars.IONOSCLOUD_MACHINE_IMAGE_ID }} CONTROL_PLANE_ENDPOINT_LOCATION: ${{ vars.CONTROL_PLANE_ENDPOINT_LOCATION }} IONOSCLOUD_CUBE_TEMPLATE_ID: ${{ vars.IONOSCLOUD_CUBE_TEMPLATE_ID }} + # Real IONOS Cloud cluster provisioning across multiple upgrade hops is very + # likely to exceed the Makefile's 2h default; override for this scheduled run. + GINKGO_TIMEOUT: "4h" steps: - uses: actions/checkout@v4 diff --git a/test/e2e/upgrade_test.go b/test/e2e/upgrade_test.go index 434ed328..a0e8323b 100644 --- a/test/e2e/upgrade_test.go +++ b/test/e2e/upgrade_test.go @@ -19,6 +19,8 @@ limitations under the License. package e2e import ( + "sigs.k8s.io/cluster-api/test/framework" + capie2e "sigs.k8s.io/cluster-api/test/e2e" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/test/e2e/helpers" @@ -36,7 +38,14 @@ func upgradeSpecBaseInput() capie2e.ClusterctlUpgradeSpecInput { ArtifactFolder: artifactFolder, SkipCleanup: skipCleanup, UseKindForManagementCluster: true, - PostNamespaceCreated: cloudEnv.createCredentialsSecretPNC, + // Without this, the framework defaults to its own package-private initScheme(), + // which registers only the default CAPI schemes and not our infrav1 types — every + // PostUpgrade hook that lists IonosCloudCluster/IonosCloudMachine objects against + // this proxy would fail with "no kind registered for type v1alpha1...". + KindManagementClusterNewClusterProxyFunc: func(name, kubeconfigPath string) framework.ClusterProxy { + return framework.NewClusterProxy(name, kubeconfigPath, initScheme(), framework.WithMachineLogCollector(framework.DockerLogCollector{})) + }, + PostNamespaceCreated: cloudEnv.createCredentialsSecretPNC, // InitWithKubernetesVersion is required by ClusterctlUpgradeSpec: it is the // Kubernetes version of the secondary (kind) management cluster the old // providers are installed into. Reuse the suite's configured version. @@ -45,7 +54,7 @@ func upgradeSpecBaseInput() capie2e.ClusterctlUpgradeSpecInput { } } -var _ = Describe("Should migrate CAPIC conditions from v1beta1 to metav1.Condition on the staged v0.6.3 -> v0.7.0 -> v0.8 provider upgrade", Label("upgrade", "single-hop"), func() { +var _ = Describe("Should stage a v1.8-created cluster through v1.10 (real v0.7.0 release), v1.11 (v1beta1-conditions migration) and v1.12 (full v1beta2 migration)", Label("upgrade", "staged"), func() { capie2e.ClusterctlUpgradeSpec(ctx, func() capie2e.ClusterctlUpgradeSpecInput { in := upgradeSpecBaseInput() in.InitWithBinary = "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.8.12/clusterctl-{OS}-{ARCH}" @@ -63,58 +72,32 @@ var _ = Describe("Should migrate CAPIC conditions from v1beta1 to metav1.Conditi // pure-v1beta2 v0.8 cut is applied. WithBinary: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/clusterctl-{OS}-{ARCH}", CoreProvider: "cluster-api:v1.10.10", + BootstrapProviders: []string{"kubeadm:v1.10.10"}, + ControlPlaneProviders: []string{"kubeadm:v1.10.10"}, InfrastructureProviders: []string{"ionoscloud:v0.7.0"}, PostUpgrade: helpers.AssertV1Beta1ClusterAndMachinesHealthy(ctx), }, { // This hop lands the pure-v1beta2 v0.8 provider (local build) on a - // v0.7.0-created cluster and proves the legacy v1beta1 conditions - // (empty Reason) are backfilled, not rejected by the new schema. + // v0.7.0-created cluster and proves both: the legacy v1beta1 conditions + // (empty Reason) are backfilled, not rejected by the new schema, and the + // CAPI core v1beta2 type migration itself completes cleanly. CoreProvider: "cluster-api:v1.11.11", + BootstrapProviders: []string{"kubeadm:v1.11.11"}, + ControlPlaneProviders: []string{"kubeadm:v1.11.11"}, InfrastructureProviders: []string{"ionoscloud:v0.8.99"}, - PostUpgrade: helpers.AssertConditionsMigration(ctx), + PostUpgrade: func(proxy framework.ClusterProxy, namespace, clusterName string) { + helpers.AssertConditionsMigration(ctx)(proxy, namespace, clusterName) + helpers.AssertCAPIV1Beta2Migration(ctx)(proxy, namespace, clusterName) + }, }, { // Extra hop bumping CAPI core to v1.12.10. The v0.8.99 provider is // already built against v1.12.10, so this exercises the v1.11 -> v1.12 // core upgrade and confirms the migrated conditions stay intact. CoreProvider: "cluster-api:v1.12.10", - InfrastructureProviders: []string{"ionoscloud:v0.8.99"}, - PostUpgrade: helpers.AssertCAPIV1Beta2Migration(ctx), - }, - } - return in - }) -}) - -var _ = Describe("Should keep a v1.8-created cluster reconcilable after staging the upgrade through v1.10 and v1.11 before reaching v1.12", Label("upgrade", "multi-hop"), func() { - capie2e.ClusterctlUpgradeSpec(ctx, func() capie2e.ClusterctlUpgradeSpecInput { - in := upgradeSpecBaseInput() - in.InitWithBinary = "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.8.12/clusterctl-{OS}-{ARCH}" - in.InitWithCoreProvider = "cluster-api:v1.8.12" - in.InitWithBootstrapProviders = []string{"kubeadm:v1.8.12"} - in.InitWithControlPlaneProviders = []string{"kubeadm:v1.8.12"} - in.InitWithInfrastructureProviders = []string{"ionoscloud:v0.6.3"} - in.Upgrades = []capie2e.ClusterctlUpgradeSpecInputUpgrade{ - { - // Landing point this block validates. Must not be the last entry (v1.10 has - // no v1beta2 API). WithBinary is required too: the in-process v1.12.10 - // clusterctl client refuses to move the core provider anywhere but v1beta2. - WithBinary: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/clusterctl-{OS}-{ARCH}", - CoreProvider: "cluster-api:v1.10.10", - InfrastructureProviders: []string{"ionoscloud:v0.7.0"}, - PostUpgrade: helpers.AssertV1Beta1ClusterAndMachinesHealthy(ctx), - }, - { - // Intermediate v1beta2 landing at v1.11 before the final v1.12 hop. - CoreProvider: "cluster-api:v1.11.11", - InfrastructureProviders: []string{"ionoscloud:v0.8.99"}, - PostUpgrade: helpers.AssertCAPIV1Beta2Migration(ctx), - }, - { - // Final hop bumping CAPI core to v1.12.10 (the version v0.8.99 is built - // against). Also satisfies the framework's mandatory last-step v1beta2 checks. - CoreProvider: "cluster-api:v1.12.10", + BootstrapProviders: []string{"kubeadm:v1.12.10"}, + ControlPlaneProviders: []string{"kubeadm:v1.12.10"}, InfrastructureProviders: []string{"ionoscloud:v0.8.99"}, PostUpgrade: helpers.AssertCAPIV1Beta2Migration(ctx), }, @@ -125,7 +108,7 @@ var _ = Describe("Should keep a v1.8-created cluster reconcilable after staging // Overlaps with the staged block's second hop on purpose: starting fresh at v1.10.10 // isolates a real v1.10->v1.11 bug from one caused by the staged block's carried-over state. -var _ = Describe("Should handle CAPI core v1beta2 type migration when upgrading from v1.10 through v1.11 to v1.12", Label("upgrade", "single-hop"), func() { +var _ = Describe("Should handle CAPI core v1beta2 type migration when upgrading from v1.10 through v1.11 to v1.12", Label("upgrade", "isolated-hop"), func() { capie2e.ClusterctlUpgradeSpec(ctx, func() capie2e.ClusterctlUpgradeSpecInput { in := upgradeSpecBaseInput() in.InitWithBinary = "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/clusterctl-{OS}-{ARCH}" @@ -137,6 +120,8 @@ var _ = Describe("Should handle CAPI core v1beta2 type migration when upgrading in.Upgrades = []capie2e.ClusterctlUpgradeSpecInputUpgrade{ { CoreProvider: "cluster-api:v1.11.11", + BootstrapProviders: []string{"kubeadm:v1.11.11"}, + ControlPlaneProviders: []string{"kubeadm:v1.11.11"}, InfrastructureProviders: []string{"ionoscloud:v0.8.99"}, PostUpgrade: helpers.AssertCAPIV1Beta2Migration(ctx), }, @@ -144,6 +129,8 @@ var _ = Describe("Should handle CAPI core v1beta2 type migration when upgrading // Extra hop bumping CAPI core to v1.12.10 (the version v0.8.99 is built // against) to validate the v1.11 -> v1.12 core upgrade. CoreProvider: "cluster-api:v1.12.10", + BootstrapProviders: []string{"kubeadm:v1.12.10"}, + ControlPlaneProviders: []string{"kubeadm:v1.12.10"}, InfrastructureProviders: []string{"ionoscloud:v0.8.99"}, PostUpgrade: helpers.AssertCAPIV1Beta2Migration(ctx), }, From 9cb6bb518039d969c20853cbf4ef34b2e9c91a8e Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 31 Jul 2026 12:13:48 +0000 Subject: [PATCH 37/94] style(e2e): fix gci import ordering in upgrade_test.go Co-Authored-By: Claude Opus 4.8 --- test/e2e/upgrade_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/e2e/upgrade_test.go b/test/e2e/upgrade_test.go index a0e8323b..af7d4bd7 100644 --- a/test/e2e/upgrade_test.go +++ b/test/e2e/upgrade_test.go @@ -19,9 +19,8 @@ limitations under the License. package e2e import ( - "sigs.k8s.io/cluster-api/test/framework" - capie2e "sigs.k8s.io/cluster-api/test/e2e" + "sigs.k8s.io/cluster-api/test/framework" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/test/e2e/helpers" From d89c54413a23a9e7c224b78f7df8f14d7e08f8c5 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 12 Jun 2026 13:19:21 +0000 Subject: [PATCH 38/94] feat: upgrade cluster-api to v1.11.11 with v1beta2 types and contract - Bump sigs.k8s.io/cluster-api and cluster-api/test to v1.11.11, controller-runtime to v0.21.0, k8s.io/* to v0.33.3 - Move core CAPI imports to api/core/v1beta2, IPAM to api/ipam/v1beta2, addons/bootstrap/controlplane test imports to their v1beta2 packages - Adopt v1beta2 contract Stage 2 status layout on IonosCloudCluster and IonosCloudMachine: status.initialization.provisioned (*bool, omitzero, MinProperties=1 per contract conventions), top-level metav1.Conditions, deprecated v1beta1 fields under status.deprecated.v1beta1 - Add cluster.x-k8s.io/v1beta2 contract label to CRDs - Wire CRDMigrator with UseStatusForStorageVersionMigration and concurrency 1; update patch helper owned-conditions options - Update importas aliases for the new CAPI package layout - Regenerate deepcopy, CRD manifests and mocks Co-Authored-By: Claude Fable 5 --- .golangci.yml | 14 +- api/v1alpha1/ionoscloudcluster_types.go | 82 +++++- api/v1alpha1/ionoscloudcluster_types_test.go | 24 +- api/v1alpha1/ionoscloudmachine_types.go | 82 +++++- api/v1alpha1/ionoscloudmachine_types_test.go | 10 +- .../ionoscloudmachinetemplate_types.go | 2 +- api/v1alpha1/suite_test.go | 2 +- api/v1alpha1/zz_generated.deepcopy.go | 143 +++++++++- cmd/main.go | 75 +++-- ...e.cluster.x-k8s.io_ionoscloudclusters.yaml | 159 ++++++++--- ...r.x-k8s.io_ionoscloudclustertemplates.yaml | 7 +- ...e.cluster.x-k8s.io_ionoscloudmachines.yaml | 152 ++++++++-- ...r.x-k8s.io_ionoscloudmachinetemplates.yaml | 1 + config/crd/kustomization.yaml | 1 + go.mod | 101 ++++--- go.sum | 261 +++++++----------- .../ionoscloudcluster_controller.go | 26 +- .../ionoscloudmachine_controller.go | 28 +- internal/service/cloud/image.go | 2 +- internal/service/cloud/image_test.go | 6 +- internal/service/cloud/ipblock.go | 2 +- internal/service/cloud/ipblock_test.go | 2 +- internal/service/cloud/network.go | 14 +- internal/service/cloud/network_test.go | 2 +- internal/service/cloud/server.go | 19 +- internal/service/cloud/suite_test.go | 8 +- internal/service/k8s/ipam.go | 14 +- internal/service/k8s/ipam_test.go | 31 ++- scope/cluster.go | 30 +- scope/cluster_test.go | 2 +- scope/machine.go | 24 +- scope/machine_test.go | 2 +- test/e2e/capic_test.go | 6 +- test/e2e/helpers/finalizers.go | 2 +- test/e2e/helpers/ownerreference.go | 8 +- test/e2e/suite_test.go | 4 +- 36 files changed, 911 insertions(+), 437 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 7871cfc3..b168f76b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -84,10 +84,16 @@ linters-settings: - pkg: sigs.k8s.io/controller-runtime alias: ctrl # Cluster API - - pkg: sigs.k8s.io/cluster-api/api/v1beta1 + - pkg: sigs.k8s.io/cluster-api/api/core/v1beta2 alias: clusterv1 - pkg: "sigs.k8s.io/cluster-api/test/e2e" alias: capie2e + - pkg: "sigs.k8s.io/cluster-api/util/conditions" + alias: conditions + - pkg: "sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1" + alias: deprecatedv1beta1conditions + - pkg: "sigs.k8s.io/cluster-api/api/ipam/v1beta2" + alias: ipamv1 # Own module - pkg: github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1 alias: infrav1 @@ -120,6 +126,8 @@ linters-settings: disabled: true - name: range-val-address disabled: true # Obsolete since Go 1.22 + - name: redundant-import-alias + disabled: true # Conflicts with importas which enforces explicit aliases - name: struct-tag arguments: - json,inline @@ -146,7 +154,7 @@ issues: - linters: - revive text: "comment-spacings: no space between comment delimiter and comment text" - source: "//(\\+(kubebuilder|optional|required)|#nosec)" + source: "//(\\+(kubebuilder|optional|required|list)|#nosec)" - linters: - revive text: "exported: exported method .*\\.(Reconcile|SetupWithManager|SetupWebhookWithManager) should have comment or be unexported" @@ -205,4 +213,4 @@ run: - e2e timeout: 10m allow-parallel-runners: true - go: "1.22" + go: "1.25" diff --git a/api/v1alpha1/ionoscloudcluster_types.go b/api/v1alpha1/ionoscloudcluster_types.go index 22dcd654..afa450b7 100644 --- a/api/v1alpha1/ionoscloudcluster_types.go +++ b/api/v1alpha1/ionoscloudcluster_types.go @@ -19,7 +19,7 @@ package v1alpha1 import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" ) const ( @@ -64,13 +64,19 @@ type IonosCloudClusterSpec struct { // IonosCloudClusterStatus defines the observed state of IonosCloudCluster. type IonosCloudClusterStatus struct { - // Ready indicates that the cluster is ready. + // Initialization provides observations of the IonosCloudCluster initialization process. + // NOTE: Fields in this struct are part of the Cluster API contract and are used to orchestrate initial + // cluster provisioning. The value of these fields is never updated after initial provisioning is completed. + // Use conditions to monitor the operational state of the cluster's infrastructure. //+optional - Ready bool `json:"ready,omitempty"` + Initialization IonosCloudClusterInitializationStatus `json:"initialization,omitempty,omitzero"` - // Conditions defines current service state of the IonosCloudCluster. + // Conditions represents the observations of the current state of the IonosCloudCluster. //+optional - Conditions clusterv1.Conditions `json:"conditions,omitempty"` + //+listType=map + //+listMapKey=type + //+kubebuilder:validation:MaxItems=32 + Conditions []metav1.Condition `json:"conditions,omitempty"` // CurrentRequestByDatacenter maps data center IDs to a pending provisioning request made during reconciliation. //+optional @@ -83,13 +89,50 @@ type IonosCloudClusterStatus struct { // ControlPlaneEndpointIPBlockID is the IONOS Cloud UUID for the control plane endpoint IP block. //+optional ControlPlaneEndpointIPBlockID string `json:"controlPlaneEndpointIPBlockID,omitempty"` + + // Deprecated groups all status fields deprecated and scheduled for removal when v1beta1 contract support is dropped. + //+optional + Deprecated *IonosCloudClusterDeprecatedStatus `json:"deprecated,omitempty"` +} + +// IonosCloudClusterInitializationStatus provides observations of the IonosCloudCluster initialization process. +// +kubebuilder:validation:MinProperties=1 +type IonosCloudClusterInitializationStatus struct { + // Provisioned is true when the infrastructure cluster is fully provisioned. + // NOTE: this field is part of the Cluster API contract and is used to orchestrate provisioning. + // The value of this field is never updated after initial provisioning is completed. + //+optional + Provisioned *bool `json:"provisioned,omitempty"` +} + +// IonosCloudClusterDeprecatedStatus groups all status fields deprecated and scheduled for removal when +// v1beta1 contract support is dropped. +type IonosCloudClusterDeprecatedStatus struct { + // V1Beta1 groups all v1beta1 status fields that are deprecated and scheduled for removal. + //+optional + V1Beta1 *IonosCloudClusterV1Beta1DeprecatedStatus `json:"v1beta1,omitempty"` +} + +// IonosCloudClusterV1Beta1DeprecatedStatus contains deprecated v1beta1 fields. +type IonosCloudClusterV1Beta1DeprecatedStatus struct { + // Ready indicates that the cluster is ready. + // + // Deprecated: Use Initialization.Provisioned instead. + //+optional + Ready bool `json:"ready,omitempty"` + + // Conditions defines current service state of the IonosCloudCluster using the deprecated v1beta1 condition type. + // + // Deprecated: Use the top-level conditions field instead. + //+optional + Conditions clusterv1.Conditions `json:"conditions,omitempty"` } //+kubebuilder:object:root=true //+kubebuilder:subresource:status //+kubebuilder:resource:path=ionoscloudclusters,scope=Namespaced,categories=cluster-api;ionoscloud,shortName=icc //+kubebuilder:printcolumn:name="Cluster",type="string",JSONPath=".metadata.labels['cluster\\.x-k8s\\.io/cluster-name']",description="Cluster" -//+kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.ready",description="Cluster infrastructure is ready" +//+kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.initialization.provisioned",description="Cluster infrastructure is ready" //+kubebuilder:printcolumn:name="Endpoint",type="string",JSONPath=".spec.controlPlaneEndpoint",description="API Endpoint" // IonosCloudCluster is the Schema for the ionoscloudclusters API. @@ -114,13 +157,32 @@ func init() { objectTypes = append(objectTypes, &IonosCloudCluster{}, &IonosCloudClusterList{}) } -// GetConditions returns the conditions from the status. -func (i *IonosCloudCluster) GetConditions() clusterv1.Conditions { +// GetV1Beta1Conditions returns the deprecated v1beta1 conditions from status.deprecated.v1beta1.conditions. +func (i *IonosCloudCluster) GetV1Beta1Conditions() clusterv1.Conditions { + if i.Status.Deprecated == nil || i.Status.Deprecated.V1Beta1 == nil { + return nil + } + return i.Status.Deprecated.V1Beta1.Conditions +} + +// SetV1Beta1Conditions sets the deprecated v1beta1 conditions in status.deprecated.v1beta1.conditions. +func (i *IonosCloudCluster) SetV1Beta1Conditions(conditions clusterv1.Conditions) { + if i.Status.Deprecated == nil { + i.Status.Deprecated = &IonosCloudClusterDeprecatedStatus{} + } + if i.Status.Deprecated.V1Beta1 == nil { + i.Status.Deprecated.V1Beta1 = &IonosCloudClusterV1Beta1DeprecatedStatus{} + } + i.Status.Deprecated.V1Beta1.Conditions = conditions +} + +// GetConditions returns the v1beta2 conditions from status.conditions. +func (i *IonosCloudCluster) GetConditions() []metav1.Condition { return i.Status.Conditions } -// SetConditions sets the conditions in the status. -func (i *IonosCloudCluster) SetConditions(conditions clusterv1.Conditions) { +// SetConditions sets the v1beta2 conditions in status.conditions. +func (i *IonosCloudCluster) SetConditions(conditions []metav1.Condition) { i.Status.Conditions = conditions } diff --git a/api/v1alpha1/ionoscloudcluster_types_test.go b/api/v1alpha1/ionoscloudcluster_types_test.go index d28fcd1e..8247160c 100644 --- a/api/v1alpha1/ionoscloudcluster_types_test.go +++ b/api/v1alpha1/ionoscloudcluster_types_test.go @@ -23,10 +23,12 @@ import ( "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - "sigs.k8s.io/cluster-api/util/conditions" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" + deprecatedv1beta1conditions "sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -41,8 +43,8 @@ func TestIonosCloudCluster_Conditions(t *testing.T) { conds := clusterv1.Conditions{{Type: "type"}} cluster := &IonosCloudCluster{} - cluster.SetConditions(conds) - require.Equal(t, conds, cluster.GetConditions()) + cluster.SetV1Beta1Conditions(conds) + require.Equal(t, conds, cluster.GetV1Beta1Conditions()) } func defaultCluster() *IonosCloudCluster { @@ -149,12 +151,12 @@ var _ = Describe("IonosCloudCluster", func() { key := client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name} fetched := &IonosCloudCluster{} Expect(k8sClient.Get(context.Background(), key, fetched)).To(Succeed()) - Expect(fetched.Status.Ready).To(BeFalse()) + Expect(fetched.Status.Initialization.Provisioned).To(BeNil()) Expect(fetched.Status.CurrentRequestByDatacenter).To(BeEmpty()) - Expect(fetched.Status.Conditions).To(BeEmpty()) + Expect(fetched.GetV1Beta1Conditions()).To(BeEmpty()) By("retrieving the cluster and setting the status") - fetched.Status.Ready = true + fetched.Status.Initialization = IonosCloudClusterInitializationStatus{Provisioned: ptr.To(true)} wantProvisionRequest := ProvisioningRequest{ Method: "POST", RequestPath: "/path/to/resource", @@ -163,17 +165,17 @@ var _ = Describe("IonosCloudCluster", func() { fetched.Status.CurrentRequestByDatacenter = map[string]ProvisioningRequest{ "123": wantProvisionRequest, } - conditions.MarkTrue(fetched, clusterv1.ReadyCondition) + deprecatedv1beta1conditions.MarkTrue(fetched, clusterv1.ReadyCondition) By("updating the cluster status") Expect(k8sClient.Status().Update(context.Background(), fetched)).To(Succeed()) Expect(k8sClient.Get(context.Background(), key, fetched)).To(Succeed()) - Expect(fetched.Status.Ready).To(BeTrue()) + Expect(fetched.Status.Initialization.Provisioned).To(HaveValue(BeTrue())) Expect(fetched.Status.CurrentRequestByDatacenter).To(HaveLen(1)) Expect(fetched.Status.CurrentRequestByDatacenter["123"]).To(Equal(wantProvisionRequest)) - Expect(fetched.Status.Conditions).To(HaveLen(1)) - Expect(conditions.IsTrue(fetched, clusterv1.ReadyCondition)).To(BeTrue()) + Expect(fetched.GetV1Beta1Conditions()).To(HaveLen(1)) + Expect(deprecatedv1beta1conditions.IsTrue(fetched, clusterv1.ReadyCondition)).To(BeTrue()) By("Removing the entry from the status again") delete(fetched.Status.CurrentRequestByDatacenter, "123") diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index b81efce5..02fa15bc 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -20,7 +20,7 @@ import ( "strings" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" ) @@ -287,17 +287,23 @@ type ImageSelector struct { // IonosCloudMachineStatus defines the observed state of IonosCloudMachine. type IonosCloudMachineStatus struct { - // Ready indicates the VM has been provisioned and is ready. + // Initialization provides observations of the IonosCloudMachine initialization process. + // NOTE: Fields in this struct are part of the Cluster API contract and are used to orchestrate initial + // machine provisioning. The value of these fields is never updated after initial provisioning is completed. + // Use conditions to monitor the operational state of the machine's infrastructure. //+optional - Ready bool `json:"ready"` + Initialization IonosCloudMachineInitializationStatus `json:"initialization,omitempty,omitzero"` // MachineNetworkInfo contains information about the network configuration of the VM. //+optional MachineNetworkInfo *MachineNetworkInfo `json:"machineNetworkInfo,omitempty"` - // Conditions defines current service state of the IonosCloudMachine. + // Conditions represents the observations of the current state of the IonosCloudMachine. //+optional - Conditions clusterv1.Conditions `json:"conditions,omitempty"` + //+listType=map + //+listMapKey=type + //+kubebuilder:validation:MaxItems=32 + Conditions []metav1.Condition `json:"conditions,omitempty"` // CurrentRequest shows the current provisioning request for any // cloud resource that is being provisioned. @@ -307,6 +313,43 @@ type IonosCloudMachineStatus struct { // Location is the location of the datacenter the VM is provisioned in. //+optional Location string `json:"location"` + + // Deprecated groups all status fields deprecated and scheduled for removal when v1beta1 contract support is dropped. + //+optional + Deprecated *IonosCloudMachineDeprecatedStatus `json:"deprecated,omitempty"` +} + +// IonosCloudMachineInitializationStatus provides observations of the IonosCloudMachine initialization process. +// +kubebuilder:validation:MinProperties=1 +type IonosCloudMachineInitializationStatus struct { + // Provisioned is true when the machine infrastructure is fully provisioned. + // NOTE: this field is part of the Cluster API contract and is used to orchestrate provisioning. + // The value of this field is never updated after initial provisioning is completed. + //+optional + Provisioned *bool `json:"provisioned,omitempty"` +} + +// IonosCloudMachineDeprecatedStatus groups all status fields deprecated and scheduled for removal when +// v1beta1 contract support is dropped. +type IonosCloudMachineDeprecatedStatus struct { + // V1Beta1 groups all v1beta1 status fields that are deprecated and scheduled for removal. + //+optional + V1Beta1 *IonosCloudMachineV1Beta1DeprecatedStatus `json:"v1beta1,omitempty"` +} + +// IonosCloudMachineV1Beta1DeprecatedStatus contains deprecated v1beta1 fields. +type IonosCloudMachineV1Beta1DeprecatedStatus struct { + // Ready indicates the VM has been provisioned and is ready. + // + // Deprecated: Use Initialization.Provisioned instead. + //+optional + Ready bool `json:"ready,omitempty"` + + // Conditions defines current service state of the IonosCloudMachine using the deprecated v1beta1 condition type. + // + // Deprecated: Use the top-level conditions field instead. + //+optional + Conditions clusterv1.Conditions `json:"conditions,omitempty"` } // MachineNetworkInfo contains information about the network configuration of the VM. @@ -343,7 +386,7 @@ type NICInfo struct { //+kubebuilder:subresource:status //+kubebuilder:resource:path=ionoscloudmachines,scope=Namespaced,categories=cluster-api;ionoscloud,shortName=icm //+kubebuilder:printcolumn:name="Cluster",type="string",JSONPath=".metadata.labels['cluster\\.x-k8s\\.io/cluster-name']",description="Cluster" -//+kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.ready",description="Machine is ready" +//+kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.initialization.provisioned",description="Machine is ready" //+kubebuilder:printcolumn:name="IPv4 Addresses",type="string",JSONPath=".status.machineNetworkInfo.nicInfo[*].ipv4Addresses" //+kubebuilder:printcolumn:name="Machine Connected Networks",type="string",JSONPath=".status.machineNetworkInfo.nicInfo[*].networkID" //+kubebuilder:printcolumn:name="IPv6 Addresses",type="string",JSONPath=".status.machineNetworkInfo.nicInfo[*].ipv6Addresses",priority=1 @@ -367,13 +410,32 @@ type IonosCloudMachineList struct { Items []IonosCloudMachine `json:"items"` } -// GetConditions returns the observations of the operational state of the IonosCloudMachine resource. -func (m *IonosCloudMachine) GetConditions() clusterv1.Conditions { +// GetV1Beta1Conditions returns the deprecated v1beta1 conditions from status.deprecated.v1beta1.conditions. +func (m *IonosCloudMachine) GetV1Beta1Conditions() clusterv1.Conditions { + if m.Status.Deprecated == nil || m.Status.Deprecated.V1Beta1 == nil { + return nil + } + return m.Status.Deprecated.V1Beta1.Conditions +} + +// SetV1Beta1Conditions sets the deprecated v1beta1 conditions in status.deprecated.v1beta1.conditions. +func (m *IonosCloudMachine) SetV1Beta1Conditions(conditions clusterv1.Conditions) { + if m.Status.Deprecated == nil { + m.Status.Deprecated = &IonosCloudMachineDeprecatedStatus{} + } + if m.Status.Deprecated.V1Beta1 == nil { + m.Status.Deprecated.V1Beta1 = &IonosCloudMachineV1Beta1DeprecatedStatus{} + } + m.Status.Deprecated.V1Beta1.Conditions = conditions +} + +// GetConditions returns the v1beta2 conditions from status.conditions. +func (m *IonosCloudMachine) GetConditions() []metav1.Condition { return m.Status.Conditions } -// SetConditions sets the underlying service state of the IonosCloudMachine to the predescribed clusterv1.Conditions. -func (m *IonosCloudMachine) SetConditions(conditions clusterv1.Conditions) { +// SetConditions sets the v1beta2 conditions in status.conditions. +func (m *IonosCloudMachine) SetConditions(conditions []metav1.Condition) { m.Status.Conditions = conditions } diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index 5d11c27a..b2100671 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -23,7 +23,7 @@ import ( sdk "github.com/ionos-cloud/sdk-go/v6" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/cluster-api/util/conditions" + deprecatedv1beta1conditions "sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" @@ -535,13 +535,13 @@ var _ = Describe("IonosCloudMachine Tests", func() { context.Background(), client.ObjectKey{Name: m.Name, Namespace: m.Namespace}, m)).To(Succeed()) // Calls SetConditions with required fields - conditions.MarkTrue(m, MachineProvisionedCondition) + deprecatedv1beta1conditions.MarkTrue(m, MachineProvisionedCondition) Expect(k8sClient.Status().Update(context.Background(), m)).To(Succeed()) Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: m.Name, Namespace: m.Namespace}, m)).To(Succeed()) - machineConditions := m.GetConditions() + machineConditions := m.GetV1Beta1Conditions() Expect(machineConditions).To(HaveLen(1)) Expect(machineConditions[0].Type).To(Equal(MachineProvisionedCondition)) Expect(machineConditions[0].Status).To(Equal(corev1.ConditionTrue)) @@ -554,8 +554,8 @@ var _ = Describe("IonosCloudMachine Tests", func() { Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: m.Name, Namespace: m.Namespace}, m)).To(Succeed()) - m.Status.Ready = true - conditions.MarkTrue(m, MachineProvisionedCondition) + m.Status.Initialization = IonosCloudMachineInitializationStatus{Provisioned: ptr.To(true)} + deprecatedv1beta1conditions.MarkTrue(m, MachineProvisionedCondition) m.Status.CurrentRequest = &ProvisioningRequest{ Method: "GET", RequestPath: "path/to/resource", diff --git a/api/v1alpha1/ionoscloudmachinetemplate_types.go b/api/v1alpha1/ionoscloudmachinetemplate_types.go index 26118a72..b934601f 100644 --- a/api/v1alpha1/ionoscloudmachinetemplate_types.go +++ b/api/v1alpha1/ionoscloudmachinetemplate_types.go @@ -18,7 +18,7 @@ package v1alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" ) // IonosCloudMachineTemplateSpec defines the desired state of IonosCloudMachineTemplate. diff --git a/api/v1alpha1/suite_test.go b/api/v1alpha1/suite_test.go index f90453bf..7994073a 100644 --- a/api/v1alpha1/suite_test.go +++ b/api/v1alpha1/suite_test.go @@ -21,7 +21,7 @@ import ( "testing" "k8s.io/apimachinery/pkg/runtime" - ipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1" + ipamv1 "sigs.k8s.io/cluster-api/api/ipam/v1beta2" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" logf "sigs.k8s.io/controller-runtime/pkg/log" diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 1adb78a3..086fdf57 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -22,8 +22,9 @@ package v1alpha1 import ( "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/cluster-api/api/core/v1beta2" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -125,6 +126,46 @@ func (in *IonosCloudCluster) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IonosCloudClusterDeprecatedStatus) DeepCopyInto(out *IonosCloudClusterDeprecatedStatus) { + *out = *in + if in.V1Beta1 != nil { + in, out := &in.V1Beta1, &out.V1Beta1 + *out = new(IonosCloudClusterV1Beta1DeprecatedStatus) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudClusterDeprecatedStatus. +func (in *IonosCloudClusterDeprecatedStatus) DeepCopy() *IonosCloudClusterDeprecatedStatus { + if in == nil { + return nil + } + out := new(IonosCloudClusterDeprecatedStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IonosCloudClusterInitializationStatus) DeepCopyInto(out *IonosCloudClusterInitializationStatus) { + *out = *in + if in.Provisioned != nil { + in, out := &in.Provisioned, &out.Provisioned + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudClusterInitializationStatus. +func (in *IonosCloudClusterInitializationStatus) DeepCopy() *IonosCloudClusterInitializationStatus { + if in == nil { + return nil + } + out := new(IonosCloudClusterInitializationStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IonosCloudClusterList) DeepCopyInto(out *IonosCloudClusterList) { *out = *in @@ -182,9 +223,10 @@ func (in *IonosCloudClusterSpec) DeepCopy() *IonosCloudClusterSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IonosCloudClusterStatus) DeepCopyInto(out *IonosCloudClusterStatus) { *out = *in + in.Initialization.DeepCopyInto(&out.Initialization) if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make(v1beta1.Conditions, len(*in)) + *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -201,6 +243,11 @@ func (in *IonosCloudClusterStatus) DeepCopyInto(out *IonosCloudClusterStatus) { *out = new(ProvisioningRequest) **out = **in } + if in.Deprecated != nil { + in, out := &in.Deprecated, &out.Deprecated + *out = new(IonosCloudClusterDeprecatedStatus) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudClusterStatus. @@ -303,6 +350,28 @@ func (in *IonosCloudClusterTemplateSpec) DeepCopy() *IonosCloudClusterTemplateSp return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IonosCloudClusterV1Beta1DeprecatedStatus) DeepCopyInto(out *IonosCloudClusterV1Beta1DeprecatedStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make(v1beta2.Conditions, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudClusterV1Beta1DeprecatedStatus. +func (in *IonosCloudClusterV1Beta1DeprecatedStatus) DeepCopy() *IonosCloudClusterV1Beta1DeprecatedStatus { + if in == nil { + return nil + } + out := new(IonosCloudClusterV1Beta1DeprecatedStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IonosCloudMachine) DeepCopyInto(out *IonosCloudMachine) { *out = *in @@ -330,6 +399,46 @@ func (in *IonosCloudMachine) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IonosCloudMachineDeprecatedStatus) DeepCopyInto(out *IonosCloudMachineDeprecatedStatus) { + *out = *in + if in.V1Beta1 != nil { + in, out := &in.V1Beta1, &out.V1Beta1 + *out = new(IonosCloudMachineV1Beta1DeprecatedStatus) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudMachineDeprecatedStatus. +func (in *IonosCloudMachineDeprecatedStatus) DeepCopy() *IonosCloudMachineDeprecatedStatus { + if in == nil { + return nil + } + out := new(IonosCloudMachineDeprecatedStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IonosCloudMachineInitializationStatus) DeepCopyInto(out *IonosCloudMachineInitializationStatus) { + *out = *in + if in.Provisioned != nil { + in, out := &in.Provisioned, &out.Provisioned + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudMachineInitializationStatus. +func (in *IonosCloudMachineInitializationStatus) DeepCopy() *IonosCloudMachineInitializationStatus { + if in == nil { + return nil + } + out := new(IonosCloudMachineInitializationStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IonosCloudMachineList) DeepCopyInto(out *IonosCloudMachineList) { *out = *in @@ -413,6 +522,7 @@ func (in *IonosCloudMachineSpec) DeepCopy() *IonosCloudMachineSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IonosCloudMachineStatus) DeepCopyInto(out *IonosCloudMachineStatus) { *out = *in + in.Initialization.DeepCopyInto(&out.Initialization) if in.MachineNetworkInfo != nil { in, out := &in.MachineNetworkInfo, &out.MachineNetworkInfo *out = new(MachineNetworkInfo) @@ -420,7 +530,7 @@ func (in *IonosCloudMachineStatus) DeepCopyInto(out *IonosCloudMachineStatus) { } if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make(v1beta1.Conditions, len(*in)) + *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -430,6 +540,11 @@ func (in *IonosCloudMachineStatus) DeepCopyInto(out *IonosCloudMachineStatus) { *out = new(ProvisioningRequest) **out = **in } + if in.Deprecated != nil { + in, out := &in.Deprecated, &out.Deprecated + *out = new(IonosCloudMachineDeprecatedStatus) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudMachineStatus. @@ -533,6 +648,28 @@ func (in *IonosCloudMachineTemplateSpec) DeepCopy() *IonosCloudMachineTemplateSp return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IonosCloudMachineV1Beta1DeprecatedStatus) DeepCopyInto(out *IonosCloudMachineV1Beta1DeprecatedStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make(v1beta2.Conditions, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudMachineV1Beta1DeprecatedStatus. +func (in *IonosCloudMachineV1Beta1DeprecatedStatus) DeepCopy() *IonosCloudMachineV1Beta1DeprecatedStatus { + if in == nil { + return nil + } + out := new(IonosCloudMachineV1Beta1DeprecatedStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MachineNetworkInfo) DeepCopyInto(out *MachineNetworkInfo) { *out = *in diff --git a/cmd/main.go b/cmd/main.go index eed412a3..e770a43e 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -21,7 +21,6 @@ package main import ( "context" "flag" - "fmt" "os" "github.com/spf13/pflag" @@ -30,9 +29,9 @@ import ( utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/klog/v2" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" + ipamv1 "sigs.k8s.io/cluster-api/api/ipam/v1beta2" "sigs.k8s.io/cluster-api/controllers/crdmigrator" - ipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1" "sigs.k8s.io/cluster-api/util/flags" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -57,8 +56,8 @@ var ( func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) - utilruntime.Must(apiextensionsv1.AddToScheme(scheme)) + utilruntime.Must(apiextensionsv1.AddToScheme(scheme)) utilruntime.Must(clusterv1.AddToScheme(scheme)) utilruntime.Must(infrav1.AddToScheme(scheme)) utilruntime.Must(ipamv1.AddToScheme(scheme)) @@ -70,11 +69,11 @@ func init() { // +kubebuilder:rbac:groups=authentication.k8s.io,resources=tokenreviews,verbs=create // +kubebuilder:rbac:groups=authorization.k8s.io,resources=subjectaccessreviews,verbs=create -// Add CRD RBAC for CRD Migrator. +// Add RBAC for CRDMigrator controller. // +kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions,verbs=get;list;watch // +kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions;customresourcedefinitions/status,verbs=update;patch,resourceNames=ionoscloudclusters.infrastructure.cluster.x-k8s.io;ionoscloudclustertemplates.infrastructure.cluster.x-k8s.io;ionoscloudmachines.infrastructure.cluster.x-k8s.io;ionoscloudmachinetemplates.infrastructure.cluster.x-k8s.io -// Add CR RBAC for CRD Migrator. +// Add RBAC for template CRs (needed by CRDMigrator for storage version migration). // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=ionoscloudclustertemplates;ionoscloudmachinetemplates,verbs=get;list;watch;patch;update func main() { @@ -113,8 +112,23 @@ func main() { ctx := ctrl.SetupSignalHandler() - if err = setUpControllers(ctx, mgr); err != nil { - setupLog.Error(err, "unable to set up controllers") + if err = iccontroller.NewIonosCloudClusterReconciler(mgr).SetupWithManager( + ctx, + mgr, + controller.Options{MaxConcurrentReconciles: icClusterConcurrency}, + ); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "IonosCloudCluster") + os.Exit(1) + } + if err = iccontroller.NewIonosCloudMachineReconciler(mgr).SetupWithManager( + mgr, + controller.Options{MaxConcurrentReconciles: icMachineConcurrency}, + ); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "IonosCloudMachine") + os.Exit(1) + } + if err := setupCRDMigrator(ctx, mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "CRDMigrator") os.Exit(1) } //+kubebuilder:scaffold:builder @@ -135,39 +149,24 @@ func main() { } } -func setUpControllers(ctx context.Context, mgr ctrl.Manager) error { - skipPhases := make([]crdmigrator.Phase, 0, len(skipCRDMigrationPhases)) +func setupCRDMigrator(ctx context.Context, mgr ctrl.Manager) error { + crdMigratorConfig := map[client.Object]crdmigrator.ByObjectConfig{ + &infrav1.IonosCloudCluster{}: {UseCache: true, UseStatusForStorageVersionMigration: true}, + &infrav1.IonosCloudMachine{}: {UseCache: true, UseStatusForStorageVersionMigration: true}, + &infrav1.IonosCloudClusterTemplate{}: {UseCache: false}, + &infrav1.IonosCloudMachineTemplate{}: {UseCache: false}, + } + crdMigratorSkipPhases := make([]crdmigrator.Phase, 0, len(skipCRDMigrationPhases)) for _, p := range skipCRDMigrationPhases { - skipPhases = append(skipPhases, crdmigrator.Phase(p)) + crdMigratorSkipPhases = append(crdMigratorSkipPhases, crdmigrator.Phase(p)) } - if err := (&crdmigrator.CRDMigrator{ + return (&crdmigrator.CRDMigrator{ Client: mgr.GetClient(), APIReader: mgr.GetAPIReader(), - SkipCRDMigrationPhases: skipPhases, - Config: map[client.Object]crdmigrator.ByObjectConfig{ - &infrav1.IonosCloudCluster{}: {UseCache: true}, - &infrav1.IonosCloudClusterTemplate{}: {UseCache: false}, - &infrav1.IonosCloudMachine{}: {UseCache: true}, - &infrav1.IonosCloudMachineTemplate{}: {UseCache: false}, - }, - }).SetupWithManager(ctx, mgr, controller.Options{}); err != nil { - return fmt.Errorf("unable to create CRDMigrator controller: %w", err) - } - - if err := iccontroller.NewIonosCloudClusterReconciler(mgr).SetupWithManager( - ctx, - mgr, - controller.Options{MaxConcurrentReconciles: icClusterConcurrency}, - ); err != nil { - return fmt.Errorf("unable to create IonosCloudCluster controller: %w", err) - } - if err := iccontroller.NewIonosCloudMachineReconciler(mgr).SetupWithManager( - mgr, - controller.Options{MaxConcurrentReconciles: icMachineConcurrency}, - ); err != nil { - return fmt.Errorf("unable to create IonosCloudMachine controller: %w", err) - } - return nil + SkipCRDMigrationPhases: crdMigratorSkipPhases, + Config: crdMigratorConfig, + // Run with concurrency 1 to avoid overwhelming the apiserver. + }).SetupWithManager(ctx, mgr, controller.Options{MaxConcurrentReconciles: 1}) } // initFlags parses the command line flags. @@ -181,7 +180,7 @@ func initFlags() { "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") pflag.StringArrayVar(&skipCRDMigrationPhases, "skip-crd-migration-phases", []string{}, - "CRD migration phases to skip. Valid values are: StorageVersionMigration, CleanupManagedFields.") + "CRD migration phases to skip. Use when the CAPI CRDMigrator handles migration.") pflag.IntVar(&icClusterConcurrency, "ionoscloudcluster-concurrency", 1, "Number of IonosCloudClusters to process simultaneously") pflag.IntVar(&icMachineConcurrency, "ionoscloudmachine-concurrency", 1, diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml index c0ee9a3c..a19b0ed3 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml @@ -25,7 +25,7 @@ spec: name: Cluster type: string - description: Cluster infrastructure is ready - jsonPath: .status.ready + jsonPath: .status.initialization.provisioned name: Ready type: string - description: API Endpoint @@ -62,18 +62,19 @@ spec: ControlPlaneEndpoint represents the endpoint used to communicate with the control plane. provider-provided block IP into the kube-vip manifest. + minProperties: 1 properties: host: description: host is the hostname on which the API server is serving. maxLength: 512 + minLength: 1 type: string port: description: port is the port on which the API server is serving. format: int32 + maximum: 65535 + minimum: 1 type: integer - required: - - host - - port type: object x-kubernetes-validations: - message: port must be within 1-65535 @@ -132,57 +133,67 @@ spec: description: IonosCloudClusterStatus defines the observed state of IonosCloudCluster. properties: conditions: - description: Conditions defines current service state of the IonosCloudCluster. + description: Conditions represents the observations of the current + state of the IonosCloudCluster. items: - description: Condition defines an observation of a Cluster API resource - operational state. + description: Condition contains details for one aspect of the current + state of this API Resource. properties: lastTransitionTime: description: |- lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when - the API field changed is acceptable. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. format: date-time type: string message: description: |- message is a human readable message indicating details about the transition. - This field may be empty. - maxLength: 10240 - minLength: 1 + This may be an empty string. + maxLength: 32768 type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer reason: description: |- - reason is the reason for the condition's last transition in CamelCase. - The specific API may choose whether or not this field is considered a guaranteed API. - This field may be empty. - maxLength: 256 + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 minLength: 1 - type: string - severity: - description: |- - severity provides an explicit classification of Reason code, so the users or machines can immediately - understand the current situation and act accordingly. - The Severity field MUST be set only when Status=False. - maxLength: 32 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown type: string type: - description: |- - type of condition in CamelCase or in foo.example.com/CamelCase. - Many .condition.type values are consistent across resources like Available, but because arbitrary conditions - can be useful (see .node.status.conditions), the ability to deconflict is important. - maxLength: 256 - minLength: 1 + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime + - message + - reason - status - type type: object + maxItems: 32 type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map controlPlaneEndpointIPBlockID: description: ControlPlaneEndpointIPBlockID is the IONOS Cloud UUID for the control plane endpoint IP block. @@ -238,9 +249,93 @@ spec: description: CurrentRequestByDatacenter maps data center IDs to a pending provisioning request made during reconciliation. type: object - ready: - description: Ready indicates that the cluster is ready. - type: boolean + deprecated: + description: Deprecated groups all status fields deprecated and scheduled + for removal when v1beta1 contract support is dropped. + properties: + v1beta1: + description: V1Beta1 groups all v1beta1 status fields that are + deprecated and scheduled for removal. + properties: + conditions: + description: |- + Conditions defines current service state of the IonosCloudCluster using the deprecated v1beta1 condition type. + + Deprecated: Use the top-level conditions field instead. + items: + description: Condition defines an observation of a Cluster + API resource operational state. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when + the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This field may be empty. + maxLength: 10240 + minLength: 1 + type: string + reason: + description: |- + reason is the reason for the condition's last transition in CamelCase. + The specific API may choose whether or not this field is considered a guaranteed API. + This field may be empty. + maxLength: 256 + minLength: 1 + type: string + severity: + description: |- + severity provides an explicit classification of Reason code, so the users or machines can immediately + understand the current situation and act accordingly. + The Severity field MUST be set only when Status=False. + maxLength: 32 + type: string + status: + description: status of the condition, one of True, False, + Unknown. + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability to deconflict is important. + maxLength: 256 + minLength: 1 + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + ready: + description: |- + Ready indicates that the cluster is ready. + + Deprecated: Use Initialization.Provisioned instead. + type: boolean + type: object + type: object + initialization: + description: |- + Initialization provides observations of the IonosCloudCluster initialization process. + NOTE: Fields in this struct are part of the Cluster API contract and are used to orchestrate initial + cluster provisioning. The value of these fields is never updated after initial provisioning is completed. + Use conditions to monitor the operational state of the cluster's infrastructure. + minProperties: 1 + properties: + provisioned: + description: |- + Provisioned is true when the infrastructure cluster is fully provisioned. + NOTE: this field is part of the Cluster API contract and is used to orchestrate provisioning. + The value of this field is never updated after initial provisioning is completed. + type: boolean + type: object type: object type: object served: true diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclustertemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclustertemplates.yaml index be41b149..107fdf76 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclustertemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclustertemplates.yaml @@ -54,20 +54,21 @@ spec: ControlPlaneEndpoint represents the endpoint used to communicate with the control plane. provider-provided block IP into the kube-vip manifest. + minProperties: 1 properties: host: description: host is the hostname on which the API server is serving. maxLength: 512 + minLength: 1 type: string port: description: port is the port on which the API server is serving. format: int32 + maximum: 65535 + minimum: 1 type: integer - required: - - host - - port type: object x-kubernetes-validations: - message: port must be within 1-65535 diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml index e4901016..fd494980 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml @@ -25,7 +25,7 @@ spec: name: Cluster type: string - description: Machine is ready - jsonPath: .status.ready + jsonPath: .status.initialization.provisioned name: Ready type: string - jsonPath: .status.machineNetworkInfo.nicInfo[*].ipv4Addresses @@ -376,57 +376,67 @@ spec: description: IonosCloudMachineStatus defines the observed state of IonosCloudMachine. properties: conditions: - description: Conditions defines current service state of the IonosCloudMachine. + description: Conditions represents the observations of the current + state of the IonosCloudMachine. items: - description: Condition defines an observation of a Cluster API resource - operational state. + description: Condition contains details for one aspect of the current + state of this API Resource. properties: lastTransitionTime: description: |- lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when - the API field changed is acceptable. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. format: date-time type: string message: description: |- message is a human readable message indicating details about the transition. - This field may be empty. - maxLength: 10240 - minLength: 1 + This may be an empty string. + maxLength: 32768 type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer reason: description: |- - reason is the reason for the condition's last transition in CamelCase. - The specific API may choose whether or not this field is considered a guaranteed API. - This field may be empty. - maxLength: 256 + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 minLength: 1 - type: string - severity: - description: |- - severity provides an explicit classification of Reason code, so the users or machines can immediately - understand the current situation and act accordingly. - The Severity field MUST be set only when Status=False. - maxLength: 32 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown type: string type: - description: |- - type of condition in CamelCase or in foo.example.com/CamelCase. - Many .condition.type values are consistent across resources like Available, but because arbitrary conditions - can be useful (see .node.status.conditions), the ability to deconflict is important. - maxLength: 256 - minLength: 1 + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime + - message + - reason - status - type type: object + maxItems: 32 type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map currentRequest: description: |- CurrentRequest shows the current provisioning request for any @@ -451,6 +461,93 @@ spec: - method - requestPath type: object + deprecated: + description: Deprecated groups all status fields deprecated and scheduled + for removal when v1beta1 contract support is dropped. + properties: + v1beta1: + description: V1Beta1 groups all v1beta1 status fields that are + deprecated and scheduled for removal. + properties: + conditions: + description: |- + Conditions defines current service state of the IonosCloudMachine using the deprecated v1beta1 condition type. + + Deprecated: Use the top-level conditions field instead. + items: + description: Condition defines an observation of a Cluster + API resource operational state. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when + the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This field may be empty. + maxLength: 10240 + minLength: 1 + type: string + reason: + description: |- + reason is the reason for the condition's last transition in CamelCase. + The specific API may choose whether or not this field is considered a guaranteed API. + This field may be empty. + maxLength: 256 + minLength: 1 + type: string + severity: + description: |- + severity provides an explicit classification of Reason code, so the users or machines can immediately + understand the current situation and act accordingly. + The Severity field MUST be set only when Status=False. + maxLength: 32 + type: string + status: + description: status of the condition, one of True, False, + Unknown. + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability to deconflict is important. + maxLength: 256 + minLength: 1 + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + ready: + description: |- + Ready indicates the VM has been provisioned and is ready. + + Deprecated: Use Initialization.Provisioned instead. + type: boolean + type: object + type: object + initialization: + description: |- + Initialization provides observations of the IonosCloudMachine initialization process. + NOTE: Fields in this struct are part of the Cluster API contract and are used to orchestrate initial + machine provisioning. The value of these fields is never updated after initial provisioning is completed. + Use conditions to monitor the operational state of the machine's infrastructure. + minProperties: 1 + properties: + provisioned: + description: |- + Provisioned is true when the machine infrastructure is fully provisioned. + NOTE: this field is part of the Cluster API contract and is used to orchestrate provisioning. + The value of this field is never updated after initial provisioning is completed. + type: boolean + type: object location: description: Location is the location of the datacenter the VM is provisioned in. @@ -497,9 +594,6 @@ spec: type: object type: array type: object - ready: - description: Ready indicates the VM has been provisioned and is ready. - type: boolean type: object type: object served: true diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachinetemplates.yaml index 0652beb4..27e52db6 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachinetemplates.yaml @@ -49,6 +49,7 @@ spec: description: |- Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + minProperties: 1 properties: annotations: additionalProperties: diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 59e9227f..e7cdddd5 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -10,6 +10,7 @@ resources: commonLabels: cluster.x-k8s.io/v1beta1: v1alpha1 #https://cluster-api.sigs.k8s.io/developer/providers/contracts.html#api-version-labels + cluster.x-k8s.io/v1beta2: v1alpha1 patches: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. diff --git a/go.mod b/go.mod index fab20c06..9e1b781c 100644 --- a/go.mod +++ b/go.mod @@ -12,43 +12,40 @@ require ( github.com/onsi/gomega v1.42.1 github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 - k8s.io/api v0.32.13 - k8s.io/apiextensions-apiserver v0.32.13 - k8s.io/apimachinery v0.32.13 - k8s.io/client-go v0.32.13 + k8s.io/api v0.33.3 + k8s.io/apiextensions-apiserver v0.33.3 + k8s.io/apimachinery v0.33.3 + k8s.io/client-go v0.33.3 k8s.io/klog/v2 v2.130.1 - sigs.k8s.io/cluster-api v1.10.10 - sigs.k8s.io/cluster-api/test v1.10.10 - sigs.k8s.io/controller-runtime v0.20.4 + sigs.k8s.io/cluster-api v1.11.11 + sigs.k8s.io/cluster-api/test v1.11.11 + sigs.k8s.io/controller-runtime v0.21.0 ) require ( al.essio.dev/pkg/shellescape v1.5.1 // indirect cel.dev/expr v0.25.1 // indirect - dario.cat/mergo v1.0.1 // indirect github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect - github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect - github.com/Masterminds/sprig/v3 v3.3.0 // indirect - github.com/Microsoft/go-winio v0.5.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect github.com/adrg/xdg v0.5.3 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect - github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cloudflare/circl v1.6.1 // indirect + github.com/cloudflare/circl v1.6.3 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/docker/docker v28.0.2+incompatible // indirect - github.com/docker/go-connections v0.5.0 // indirect - github.com/docker/go-units v0.4.0 // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46 // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -63,94 +60,90 @@ require ( github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/gobuffalo/flect v1.0.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/cel-go v0.22.0 // indirect - github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/cel-go v0.23.2 // indirect + github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-github/v53 v53.2.0 // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect - github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect - github.com/huandu/xstrings v1.5.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect - github.com/mitchellh/copystructure v1.2.0 // indirect - github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/moby/api v1.54.1 // indirect + github.com/moby/moby/client v0.4.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.19.1 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/rivo/uniseg v0.4.2 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect - github.com/shopspring/decimal v1.4.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.12.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/spf13/cobra v1.9.1 // indirect - github.com/spf13/viper v1.20.0 // indirect + github.com/spf13/viper v1.20.1 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/valyala/fastjson v1.6.4 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect go.opentelemetry.io/otel v1.43.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect go.opentelemetry.io/otel/sdk v1.43.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect - go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.53.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect - golang.org/x/mod v0.37.0 // indirect + golang.org/x/mod v0.36.0 // indirect golang.org/x/net v0.56.0 // indirect - golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect golang.org/x/sync v0.21.0 // indirect golang.org/x/sys v0.46.0 // indirect golang.org/x/term v0.44.0 // indirect - golang.org/x/text v0.39.0 // indirect - golang.org/x/time v0.10.0 // indirect - golang.org/x/tools v0.47.0 // indirect + golang.org/x/text v0.38.0 // indirect + golang.org/x/time v0.11.0 // indirect + golang.org/x/tools v0.45.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect - google.golang.org/grpc v1.82.1 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/grpc v1.79.3 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiserver v0.32.13 // indirect - k8s.io/cluster-bootstrap v0.32.3 // indirect - k8s.io/component-base v0.32.13 // indirect - k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect + k8s.io/apiserver v0.33.3 // indirect + k8s.io/cluster-bootstrap v0.33.3 // indirect + k8s.io/component-base v0.33.3 // indirect + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect - sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect - sigs.k8s.io/kind v0.27.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect + sigs.k8s.io/kind v0.30.0 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index 27f933b1..8b36a509 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,6 @@ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= @@ -16,8 +14,8 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1 github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= -github.com/Microsoft/go-winio v0.5.0 h1:Elr9Wn+sGKPlkaBvwu4mTrxtmOp3F3yV9qhaHbXGjwU= -github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA= @@ -26,31 +24,26 @@ github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= -github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= -github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= -github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= -github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/coredns/caddy v1.1.1 h1:2eYKZT7i6yxIfGP3qLJoJ7HAsDJqYB+X68g4NYjSrE0= github.com/coredns/caddy v1.1.1/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4= -github.com/coredns/corefile-migration v1.0.29 h1:g4cPYMXXDDs9uLE2gFYrJaPBuUAR07eEMGyh9JBE13w= -github.com/coredns/corefile-migration v1.0.29/go.mod h1:56DPqONc3njpVPsdilEnfijCwNGC3/kTJLl7i7SPavY= -github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= -github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= -github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coredns/corefile-migration v1.0.30 h1:ljZNPGgna+4yKv81gfkvkgLEWdtz0NjBR1glaiPI140= +github.com/coredns/corefile-migration v1.0.30/go.mod h1:56DPqONc3njpVPsdilEnfijCwNGC3/kTJLl7i7SPavY= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -59,18 +52,14 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v28.0.2+incompatible h1:9BILleFwug5FSSqWBgVevgL3ewDJfWWWyZVqlDMttE8= -github.com/docker/docker v28.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= -github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46 h1:7QPwrLT79GlD5sizHf27aoY2RTvw62mO6x7mxkScNk0= github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46/go.mod h1:esf2rsHFNlZlxsqsZDojNBcnNs5REqIvRrWRHqX0vEU= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= @@ -116,16 +105,14 @@ github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= -github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.22.0 h1:b3FJZxpiv1vTMo2/5RDUqAHPxkT8mmMfJIrq1llbf7g= -github.com/google/cel-go v0.22.0/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8= -github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= -github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/cel-go v0.23.2 h1:UdEe3CvQh3Nv+E/j9r1Y//WO0K0cSyD7/y0bzyLIMI4= +github.com/google/cel-go v0.23.2/go.mod h1:52Pb6QsDbC5kvgxvZhiL9QX1oZEkcUF/ZqaPx1J5Wwo= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -140,22 +127,12 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg= github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= -github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 h1:SJ+NtwL6QaZ21U+IrK7d0gGgpjGGvd2kz+FzTHVzdqI= -github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2/go.mod h1:Tv1PlzqC9t8wNnpPdctvtSUOPUUg4SHeE6vR1Ir2hmg= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= -github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -164,8 +141,6 @@ github.com/ionos-cloud/sdk-go/v6 v6.3.11 h1:m4mX03vflm631znIPutojJxLZvxD5Mt3xv/s github.com/ionos-cloud/sdk-go/v6 v6.3.11/go.mod h1:nUGHP4kZHAZngCVr4v6C8nuargFrtvt7GrzH/hqn7c4= github.com/jarcoal/httpmock v1.4.2 h1:dKwiP/9zITCPfBLsDn3kchbSOu16JrnxtVEmL0fPRcI= github.com/jarcoal/httpmock v1.4.2/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0= -github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= -github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= @@ -174,6 +149,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -181,6 +158,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= @@ -203,15 +182,15 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4= +github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw= +github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= @@ -222,8 +201,8 @@ github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I= github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= -github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= @@ -233,12 +212,12 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= -github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= -github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -251,11 +230,6 @@ github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsF github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= -github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= @@ -267,8 +241,8 @@ github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wx github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY= -github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= +github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -276,7 +250,6 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -293,46 +266,20 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= -github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= -github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= -github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk= -github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= -go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= -go.etcd.io/etcd/api/v3 v3.5.20 h1:aKfz3nPZECWoZJXMSH9y6h2adXjtOHaHTGEVCuCmaz0= -go.etcd.io/etcd/api/v3 v3.5.20/go.mod h1:QqKGViq4KTgOG43dr/uH0vmGWIaoJY3ggFi6ZH0TH/U= -go.etcd.io/etcd/client/pkg/v3 v3.5.20 h1:sZIAtra+xCo56gdf6BR62to/hiie5Bwl7hQIqMzVTEM= -go.etcd.io/etcd/client/pkg/v3 v3.5.20/go.mod h1:qaOi1k4ZA9lVLejXNvyPABrVEe7VymMF2433yyRQ7O0= -go.etcd.io/etcd/client/v2 v2.305.16 h1:kQrn9o5czVNaukf2A2At43cE9ZtWauOtf9vRZuiKXow= -go.etcd.io/etcd/client/v2 v2.305.16/go.mod h1:h9YxWCzcdvZENbfzBTFCnoNumr2ax3F19sKMqHFmXHE= -go.etcd.io/etcd/client/v3 v3.5.20 h1:jMT2MwQEhyvhQg49Cec+1ZHJzfUf6ZgcmV0GjPv0tIQ= -go.etcd.io/etcd/client/v3 v3.5.20/go.mod h1:J5lbzYRMUR20YolS5UjlqqMcu3/wdEvG5VNBhzyo3m0= -go.etcd.io/etcd/pkg/v3 v3.5.16 h1:cnavs5WSPWeK4TYwPYfmcr3Joz9BH+TZ6qoUtz6/+mc= -go.etcd.io/etcd/pkg/v3 v3.5.16/go.mod h1:+lutCZHG5MBBFI/U4eYT5yL7sJfnexsoM20Y0t2uNuY= -go.etcd.io/etcd/raft/v3 v3.5.16 h1:zBXA3ZUpYs1AwiLGPafYAKKl/CORn/uaxYDwlNwndAk= -go.etcd.io/etcd/raft/v3 v3.5.16/go.mod h1:P4UP14AxofMJ/54boWilabqqWoW9eLodl6I5GdGzazI= -go.etcd.io/etcd/server/v3 v3.5.16 h1:d0/SAdJ3vVsZvF8IFVb1k8zqMZ+heGcNfft71ul9GWE= -go.etcd.io/etcd/server/v3 v3.5.16/go.mod h1:ynhyZZpdDp1Gq49jkUg5mfkDWZwXnn3eIqCqtJnrD/s= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCyI9jVEfqhUh2MoSkmolPjfh5fp2hnV0b0irxH4Q= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= @@ -341,14 +288,16 @@ go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfC go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= -go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= -go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -361,8 +310,8 @@ golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWB golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -370,8 +319,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= -golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= -golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -379,10 +328,8 @@ golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -394,32 +341,30 @@ golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= -golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= -golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= -golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= -golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= -gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= -google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc= -google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= -google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= -google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -429,46 +374,50 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= -gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= -gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= -k8s.io/api v0.32.13 h1:CAtHUTtSau6UhSGcrypjKXc2365TncaxUtrIfnjUPGE= -k8s.io/api v0.32.13/go.mod h1:PXqm+/G56aRPUJWUb8nGwBDovaXcqQ+e3o6+ZJIITPY= -k8s.io/apiextensions-apiserver v0.32.13 h1:3M8y1UNKgAp/I+T4TDEeqXiR1wj8nrqzTqgMHRKsO8A= -k8s.io/apiextensions-apiserver v0.32.13/go.mod h1:IMU3eme+2CoGzlmiHVcl6v08u3dRzebcBTnop09cwpY= -k8s.io/apimachinery v0.32.13 h1:OQ1djPkMwU8F9BQwZUW314DdYsalB8hRvBgLRqimJdo= -k8s.io/apimachinery v0.32.13/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= -k8s.io/apiserver v0.32.13 h1:oakvP13+5KyJSKo01uV6AD9n5oXOsQghSrpayYfeHeg= -k8s.io/apiserver v0.32.13/go.mod h1:3FGYjbrMtFOUx0p37vn4cEUWWO/a90fNTe/SIn2gaaQ= -k8s.io/client-go v0.32.13 h1:FxVdGzgrWW8QBprX/xJjoxs9tE06UJIbuy8IfNoxn0c= -k8s.io/client-go v0.32.13/go.mod h1:XhErcCmtSRUns7g0fXYjV8NAXvJWHQCT9EaYkf4dbyw= -k8s.io/cluster-bootstrap v0.32.3 h1:AqIpsUhB6MUeaAsl1WvaUw54AHRd2hfZrESlKChtd8s= -k8s.io/cluster-bootstrap v0.32.3/go.mod h1:CHbBwgOb6liDV6JFUTkx5t85T2xidy0sChBDoyYw344= -k8s.io/component-base v0.32.13 h1:QTroT4xOtYXc8ySp7Wvj5llxDNxz16YoG5Pw3zJBMds= -k8s.io/component-base v0.32.13/go.mod h1:hfuVb9GlAuoIXRimoph+0e862qEwxRA7h+6oOIFelCE= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +k8s.io/api v0.33.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8= +k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE= +k8s.io/apiextensions-apiserver v0.33.3 h1:qmOcAHN6DjfD0v9kxL5udB27SRP6SG/MTopmge3MwEs= +k8s.io/apiextensions-apiserver v0.33.3/go.mod h1:oROuctgo27mUsyp9+Obahos6CWcMISSAPzQ77CAQGz8= +k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA= +k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/apiserver v0.33.3 h1:Wv0hGc+QFdMJB4ZSiHrCgN3zL3QRatu56+rpccKC3J4= +k8s.io/apiserver v0.33.3/go.mod h1:05632ifFEe6TxwjdAIrwINHWE2hLwyADFk5mBsQa15E= +k8s.io/client-go v0.33.3 h1:M5AfDnKfYmVJif92ngN532gFqakcGi6RvaOF16efrpA= +k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg= +k8s.io/cluster-bootstrap v0.33.3 h1:u2NTxJ5CFSBFXaDxLQoOWMly8eni31psVso+caq6uwI= +k8s.io/cluster-bootstrap v0.33.3/go.mod h1:p970f8u8jf273zyQ5raD8WUu2XyAl0SAWOY82o7i/ds= +k8s.io/component-base v0.33.3 h1:mlAuyJqyPlKZM7FyaoM/LcunZaaY353RXiOd2+B5tGA= +k8s.io/component-base v0.33.3/go.mod h1:ktBVsBzkI3imDuxYXmVxZ2zxJnYTZ4HAsVj9iF09qp4= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= -k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 h1:CPT0ExVicCzcpeN4baWEV2ko2Z/AsiZgEdwgcfwLgMo= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= -sigs.k8s.io/cluster-api v1.10.10 h1:GF9B04puY1IrK9l8nc7xZaDITvKEkPtFAzccfgN4Gp0= -sigs.k8s.io/cluster-api v1.10.10/go.mod h1:IHnO3HR8ta0yK1S2SyI2qKpcrZNsyOdArwRjUfnJwnc= -sigs.k8s.io/cluster-api/test v1.10.10 h1:himbsoB2Yu1bY7il/qSwXzAVN2zWHGef+nxJw4eqqQI= -sigs.k8s.io/cluster-api/test v1.10.10/go.mod h1:ehnxLYJn73nn/0NM0x6dJgdYa04ZWwqn7T5xcvwRwYg= -sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= -sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= +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/cluster-api v1.11.11 h1:MvUjPlUYk72ow4HhI9lJlt0C1iY8JsyPtx5YFV8pLJA= +sigs.k8s.io/cluster-api v1.11.11/go.mod h1:wplBEE7AErSt8bN93qrqXeXQ8uEq69RfJBPPYoHWLAE= +sigs.k8s.io/cluster-api/test v1.11.11 h1:KZXBo1j9zfnysVemyX1N61K3S1PqqSvrlhJVQVNDWMk= +sigs.k8s.io/cluster-api/test v1.11.11/go.mod h1:UeTeX/FwCHTBKFylV8640zgjIDBZ3Z+c8jcDB+BfrTM= +sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= +sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= -sigs.k8s.io/kind v0.27.0 h1:PQ3f0iAWNIj66LYkZ1ivhEg/+Zb6UPMbO+qVei/INZA= -sigs.k8s.io/kind v0.27.0/go.mod h1:RZVFmy6qcwlSWwp6xeIUv7kXCPF3i8MXsEXxW/J+gJY= -sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= -sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/kind v0.30.0 h1:2Xi1KFEfSMm0XDcvKnUt15ZfgRPCT0OnCBbpgh8DztY= +sigs.k8s.io/kind v0.30.0/go.mod h1:FSqriGaoTPruiXWfRnUXNykF8r2t+fHtK0P0m1AbGF8= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/controller/ionoscloudcluster_controller.go b/internal/controller/ionoscloudcluster_controller.go index 449369f9..25e09a89 100644 --- a/internal/controller/ionoscloudcluster_controller.go +++ b/internal/controller/ionoscloudcluster_controller.go @@ -25,11 +25,12 @@ import ( "fmt" apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" "sigs.k8s.io/cluster-api/util" "sigs.k8s.io/cluster-api/util/annotations" - "sigs.k8s.io/cluster-api/util/conditions" + conditions "sigs.k8s.io/cluster-api/util/conditions" "sigs.k8s.io/cluster-api/util/predicates" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" @@ -42,6 +43,7 @@ import ( infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/service/cloud" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/locker" + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/scope" ) @@ -172,8 +174,20 @@ func (r *IonosCloudClusterReconciler) reconcileNormal( } } - conditions.MarkTrue(clusterScope.IonosCluster, infrav1.IonosCloudClusterReady) - clusterScope.IonosCluster.Status.Ready = true + conditions.Set(clusterScope.IonosCluster, metav1.Condition{ + Type: string(infrav1.IonosCloudClusterReady), + Status: metav1.ConditionTrue, + Reason: string(infrav1.IonosCloudClusterReady), + }) + clusterScope.IonosCluster.Status.Initialization.Provisioned = ptr.To(true) + // Set deprecated v1beta1 ready field for backwards compatibility. + if clusterScope.IonosCluster.Status.Deprecated == nil { + clusterScope.IonosCluster.Status.Deprecated = &infrav1.IonosCloudClusterDeprecatedStatus{} + } + if clusterScope.IonosCluster.Status.Deprecated.V1Beta1 == nil { + clusterScope.IonosCluster.Status.Deprecated.V1Beta1 = &infrav1.IonosCloudClusterV1Beta1DeprecatedStatus{} + } + clusterScope.IonosCluster.Status.Deprecated.V1Beta1.Ready = true //nolint:staticcheck // Intentionally setting deprecated field for v1beta1 backwards compatibility. return ctrl.Result{}, nil } @@ -261,7 +275,7 @@ func (r *IonosCloudClusterReconciler) SetupWithManager( return ctrl.NewControllerManagedBy(mgr). WithOptions(options). For(&infrav1.IonosCloudCluster{}). - WithEventFilter(predicates.ResourceNotPaused(mgr.GetScheme(), ctrl.LoggerFrom(ctx))). + WithEventFilter(predicates.ResourceNotPaused(r.scheme, ctrl.LoggerFrom(ctx))). Watches(&clusterv1.Cluster{}, handler.EnqueueRequestsFromMapFunc( util.ClusterToInfrastructureMapFunc( @@ -270,7 +284,7 @@ func (r *IonosCloudClusterReconciler) SetupWithManager( r.Client, &infrav1.IonosCloudCluster{}, ), ), - builder.WithPredicates(predicates.ClusterUnpaused(mgr.GetScheme(), ctrl.LoggerFrom(ctx))), + builder.WithPredicates(predicates.ClusterUnpaused(r.scheme, ctrl.LoggerFrom(ctx))), ). Complete(reconcile.AsReconciler[*infrav1.IonosCloudCluster](r.Client, r)) } diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index e6fa98a2..f8ca9160 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -22,11 +22,12 @@ import ( "fmt" apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" "sigs.k8s.io/cluster-api/util" "sigs.k8s.io/cluster-api/util/annotations" - "sigs.k8s.io/cluster-api/util/conditions" + conditions "sigs.k8s.io/cluster-api/util/conditions" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" @@ -299,13 +300,13 @@ func (*IonosCloudMachineReconciler) checkRequestStates( func (*IonosCloudMachineReconciler) isInfrastructureReady(ctx context.Context, ms *scope.Machine) bool { log := ctrl.LoggerFrom(ctx) // Make sure the infrastructure is ready. - if !ms.ClusterScope.Cluster.Status.InfrastructureReady { + if ms.ClusterScope.Cluster.Status.Initialization.InfrastructureProvisioned == nil || !*ms.ClusterScope.Cluster.Status.Initialization.InfrastructureProvisioned { log.Info("Cluster infrastructure is not ready yet") - conditions.MarkFalse( - ms.IonosMachine, - infrav1.MachineProvisionedCondition, - infrav1.WaitingForClusterInfrastructureReason, - clusterv1.ConditionSeverityInfo, "") + conditions.Set(ms.IonosMachine, metav1.Condition{ + Type: string(infrav1.MachineProvisionedCondition), + Status: metav1.ConditionFalse, + Reason: infrav1.WaitingForClusterInfrastructureReason, + }) return false } @@ -313,12 +314,11 @@ func (*IonosCloudMachineReconciler) isInfrastructureReady(ctx context.Context, m // Make sure to wait until the data secret was created if ms.Machine.Spec.Bootstrap.DataSecretName == nil { log.Info("Bootstrap data secret is not available yet") - conditions.MarkFalse( - ms.IonosMachine, - infrav1.MachineProvisionedCondition, - infrav1.WaitingForBootstrapDataReason, - clusterv1.ConditionSeverityInfo, "", - ) + conditions.Set(ms.IonosMachine, metav1.Condition{ + Type: string(infrav1.MachineProvisionedCondition), + Status: metav1.ConditionFalse, + Reason: infrav1.WaitingForBootstrapDataReason, + }) return false } diff --git a/internal/service/cloud/image.go b/internal/service/cloud/image.go index 04ba59b8..686f04d6 100644 --- a/internal/service/cloud/image.go +++ b/internal/service/cloud/image.go @@ -63,7 +63,7 @@ func (s *Service) lookupImageID(ctx context.Context, ms *scope.Machine) (string, } if ptr.Deref(imageSpec.Selector.UseMachineVersion, true) { - version := ptr.Deref(ms.Machine.Spec.Version, "") + version := ms.Machine.Spec.Version if version == "" { return "", errMissingMachineVersion } diff --git a/internal/service/cloud/image_test.go b/internal/service/cloud/image_test.go index 267e5aeb..2d85ab5f 100644 --- a/internal/service/cloud/image_test.go +++ b/internal/service/cloud/image_test.go @@ -99,7 +99,7 @@ func (s *imageTestSuite) TestLookupImageMissingMachineVersion() { s.ionosClient.EXPECT().GetImage(s.ctx, "image-1").Return(s.makeTestImage("image-1", "test", "loc"), nil).Once() s.ionosClient.EXPECT().GetDatacenterLocationByID(s.ctx, s.infraMachine.Spec.DatacenterID).Return("loc", nil).Once() - s.capiMachine.Spec.Version = ptr.To("") + s.capiMachine.Spec.Version = "" _, err := s.service.lookupImageID(s.ctx, s.machineScope) s.ErrorIs(err, errMissingMachineVersion) @@ -115,7 +115,7 @@ func (s *imageTestSuite) TestLookupImageIgnoreMissingMachineVersion() { s.ionosClient.EXPECT().GetDatacenterLocationByID(s.ctx, s.infraMachine.Spec.DatacenterID).Return("loc", nil).Once() s.infraMachine.Spec.Disk.Image.Selector.UseMachineVersion = ptr.To(false) - s.capiMachine.Spec.Version = ptr.To("") + s.capiMachine.Spec.Version = "" imageID, err := s.service.lookupImageID(s.ctx, s.machineScope) s.NoError(err) @@ -158,7 +158,7 @@ func (s *imageTestSuite) TestLookupImageNewestOK() { } func (s *imageTestSuite) makeTestImage(id, namePrefix, location string) *sdk.Image { - return makeTestImage(id, namePrefix+*s.capiMachine.Spec.Version, location) + return makeTestImage(id, namePrefix+s.capiMachine.Spec.Version, location) } func (s *imageTestSuite) makeTestImageWithDate(id, namePrefix, location string, createdDate time.Time) *sdk.Image { diff --git a/internal/service/cloud/ipblock.go b/internal/service/cloud/ipblock.go index a8df3e7e..6329cd87 100644 --- a/internal/service/cloud/ipblock.go +++ b/internal/service/cloud/ipblock.go @@ -27,7 +27,7 @@ import ( "github.com/go-logr/logr" sdk "github.com/ionos-cloud/sdk-go/v6" "k8s.io/apimachinery/pkg/util/sets" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" diff --git a/internal/service/cloud/ipblock_test.go b/internal/service/cloud/ipblock_test.go index 28dbcce4..9f9645b8 100644 --- a/internal/service/cloud/ipblock_test.go +++ b/internal/service/cloud/ipblock_test.go @@ -22,7 +22,7 @@ import ( sdk "github.com/ionos-cloud/sdk-go/v6" "github.com/stretchr/testify/suite" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud/clienttest" diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index a4af1100..e8b82af6 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -25,8 +25,7 @@ import ( "slices" sdk "github.com/ionos-cloud/sdk-go/v6" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - "sigs.k8s.io/cluster-api/util" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" "sigs.k8s.io/controller-runtime/pkg/client" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" @@ -308,7 +307,7 @@ func (s *Service) retrieveFailoverIPForMachine( ) (requeue bool, failoverIP string, err error) { log := s.logger.WithName("retrieveFailoverIPForMachine") - if util.IsControlPlaneMachine(ms.Machine) { + if isControlPlaneMachine(ms.Machine) { ip, err := ms.ClusterScope.GetControlPlaneEndpointIP(ctx) return false, ip, err } @@ -380,7 +379,7 @@ func (s *Service) ReconcileIPFailoverDeletion( matchLabels := client.MatchingLabels{} - if util.IsControlPlaneMachine(ms.Machine) { + if isControlPlaneMachine(ms.Machine) { matchLabels[clusterv1.MachineControlPlaneLabel] = "" } else { matchLabels[clusterv1.MachineDeploymentNameLabel] = ms.IonosMachine.Labels[clusterv1.MachineDeploymentNameLabel] @@ -657,5 +656,10 @@ func (s *Service) patchLAN(ctx context.Context, ms *scope.Machine, lanID string, } func failoverRequired(ms *scope.Machine) bool { - return util.IsControlPlaneMachine(ms.Machine) || ms.IonosMachine.Spec.FailoverIP != nil + return isControlPlaneMachine(ms.Machine) || ms.IonosMachine.Spec.FailoverIP != nil +} + +func isControlPlaneMachine(machine *clusterv1.Machine) bool { + _, ok := machine.Labels[clusterv1.MachineControlPlaneLabel] + return ok } diff --git a/internal/service/cloud/network_test.go b/internal/service/cloud/network_test.go index 4425e7c8..61b590e4 100644 --- a/internal/service/cloud/network_test.go +++ b/internal/service/cloud/network_test.go @@ -26,7 +26,7 @@ import ( sdk "github.com/ionos-cloud/sdk-go/v6" "github.com/stretchr/testify/suite" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" "sigs.k8s.io/controller-runtime/pkg/client" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" diff --git a/internal/service/cloud/server.go b/internal/service/cloud/server.go index 7bbda6c5..24335abb 100644 --- a/internal/service/cloud/server.go +++ b/internal/service/cloud/server.go @@ -29,7 +29,8 @@ import ( sdk "github.com/ionos-cloud/sdk-go/v6" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" - "sigs.k8s.io/cluster-api/util/conditions" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + conditions "sigs.k8s.io/cluster-api/util/conditions" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" @@ -144,8 +145,20 @@ func (s *Service) ReconcileServerDeletion(ctx context.Context, ms *scope.Machine // FinalizeMachineProvisioning marks the machine as provisioned. func (*Service) FinalizeMachineProvisioning(_ context.Context, ms *scope.Machine) (bool, error) { - ms.IonosMachine.Status.Ready = true - conditions.MarkTrue(ms.IonosMachine, infrav1.MachineProvisionedCondition) + ms.IonosMachine.Status.Initialization.Provisioned = ptr.To(true) + // Set deprecated v1beta1 ready field for backwards compatibility. + if ms.IonosMachine.Status.Deprecated == nil { + ms.IonosMachine.Status.Deprecated = &infrav1.IonosCloudMachineDeprecatedStatus{} + } + if ms.IonosMachine.Status.Deprecated.V1Beta1 == nil { + ms.IonosMachine.Status.Deprecated.V1Beta1 = &infrav1.IonosCloudMachineV1Beta1DeprecatedStatus{} + } + ms.IonosMachine.Status.Deprecated.V1Beta1.Ready = true //nolint:staticcheck // Intentionally setting deprecated field for v1beta1 backwards compatibility. + conditions.Set(ms.IonosMachine, metav1.Condition{ + Type: string(infrav1.MachineProvisionedCondition), + Status: metav1.ConditionTrue, + Reason: string(infrav1.MachineProvisionedCondition), + }) return false, nil } diff --git a/internal/service/cloud/suite_test.go b/internal/service/cloud/suite_test.go index c6463d9b..4ab44891 100644 --- a/internal/service/cloud/suite_test.go +++ b/internal/service/cloud/suite_test.go @@ -31,8 +31,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - ipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" + ipamv1 "sigs.k8s.io/cluster-api/api/ipam/v1beta2" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" @@ -138,8 +138,8 @@ func (s *ServiceTestSuite) SetupTest() { }, Spec: clusterv1.MachineSpec{ ClusterName: s.capiCluster.Name, - Version: ptr.To("v1.26.12"), - ProviderID: ptr.To("ionos://" + exampleServerID), + Version: "v1.26.12", + ProviderID: "ionos://" + exampleServerID, }, } s.infraMachine = &infrav1.IonosCloudMachine{ diff --git a/internal/service/k8s/ipam.go b/internal/service/k8s/ipam.go index e0b6207f..bceb8fa3 100644 --- a/internal/service/k8s/ipam.go +++ b/internal/service/k8s/ipam.go @@ -27,8 +27,8 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - ipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" + ipamv1 "sigs.k8s.io/cluster-api/api/ipam/v1beta2" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -268,6 +268,14 @@ func (h *Helper) CreateIPAddressClaim(ctx context.Context, owner client.Object, return nil } + ipPoolRef := ipamv1.IPPoolReference{ + Name: poolRef.Name, + Kind: poolRef.Kind, + } + if poolRef.APIGroup != nil { + ipPoolRef.APIGroup = *poolRef.APIGroup + } + desired := &ipamv1.IPAddressClaim{ ObjectMeta: metav1.ObjectMeta{ Name: claimRef.Name, @@ -275,7 +283,7 @@ func (h *Helper) CreateIPAddressClaim(ctx context.Context, owner client.Object, Labels: map[string]string{clusterv1.ClusterNameLabel: cluster}, }, Spec: ipamv1.IPAddressClaimSpec{ - PoolRef: *poolRef, + PoolRef: ipPoolRef, }, } _, err = controllerutil.CreateOrUpdate(ctx, h.client, desired, func() error { diff --git a/internal/service/k8s/ipam_test.go b/internal/service/k8s/ipam_test.go index 68e0506f..615c7f6c 100644 --- a/internal/service/k8s/ipam_test.go +++ b/internal/service/k8s/ipam_test.go @@ -27,8 +27,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - ipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" + ipamv1 "sigs.k8s.io/cluster-api/api/ipam/v1beta2" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" @@ -98,8 +98,8 @@ func (s *IpamTestSuite) SetupTest() { }, Spec: clusterv1.MachineSpec{ ClusterName: s.capiCluster.Name, - Version: ptr.To("v1.26.12"), - ProviderID: ptr.To("ionos://dd426c63-cd1d-4c02-aca3-13b4a27c2ebf"), + Version: "v1.26.12", + ProviderID: "ionos://dd426c63-cd1d-4c02-aca3-13b4a27c2ebf", }, } s.infraMachine = &infrav1.IonosCloudMachine{ @@ -320,10 +320,10 @@ func defaultIPv4Address(claim *ipamv1.IPAddressClaim, poolRef *corev1.TypedLocal Namespace: "default", }, Spec: ipamv1.IPAddressSpec{ - ClaimRef: *localRef(claim), - PoolRef: *poolRef, + ClaimRef: ipamv1.IPAddressClaimReference{Name: claim.GetName()}, + PoolRef: toIPPoolReference(poolRef), Address: "10.0.0.2", - Prefix: 16, + Prefix: ptr.To[int32](16), }, } } @@ -336,10 +336,10 @@ func defaultIPv6Address(claim *ipamv1.IPAddressClaim, poolRef *corev1.TypedLocal Namespace: "default", }, Spec: ipamv1.IPAddressSpec{ - ClaimRef: *localRef(claim), - PoolRef: *poolRef, + ClaimRef: ipamv1.IPAddressClaimReference{Name: claim.GetName()}, + PoolRef: toIPPoolReference(poolRef), Address: "2001:db8::", - Prefix: 42, + Prefix: ptr.To[int32](42), }, } } @@ -398,8 +398,13 @@ func defaultPrimaryIPv6Claim() *ipamv1.IPAddressClaim { } } -func localRef(obj client.Object) *corev1.LocalObjectReference { - return &corev1.LocalObjectReference{ - Name: obj.GetName(), +func toIPPoolReference(ref *corev1.TypedLocalObjectReference) ipamv1.IPPoolReference { + r := ipamv1.IPPoolReference{ + Name: ref.Name, + Kind: ref.Kind, } + if ref.APIGroup != nil { + r.APIGroup = *ref.APIGroup + } + return r } diff --git a/scope/cluster.go b/scope/cluster.go index bffb01d4..50814b15 100644 --- a/scope/cluster.go +++ b/scope/cluster.go @@ -27,8 +27,8 @@ import ( "time" "k8s.io/client-go/util/retry" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - "sigs.k8s.io/cluster-api/util/conditions" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" + conditions "sigs.k8s.io/cluster-api/util/conditions" "sigs.k8s.io/cluster-api/util/patch" "sigs.k8s.io/controller-runtime/pkg/client" @@ -202,9 +202,15 @@ func (c *Cluster) DeleteCurrentRequestByDatacenter(datacenterID string) { // PatchObject will apply all changes from the IonosCloudCluster. // It will also make sure to patch the status subresource. func (c *Cluster) PatchObject() error { - // always set the ready condition - conditions.SetSummary(c.IonosCluster, - conditions.WithConditions(infrav1.IonosCloudClusterReady)) + // always set the ready condition summary + if err := conditions.SetSummaryCondition( + c.IonosCluster, + c.IonosCluster, + string(clusterv1.ReadyCondition), + conditions.ForConditionTypes{string(infrav1.IonosCloudClusterReady)}, + ); err != nil { + return err + } // NOTE(piepmatz): We don't accept and forward a context here. This is on purpose: Even if a reconciliation is // aborted, we want to make sure that the final patch is applied. Reusing the context from the reconciliation @@ -212,11 +218,17 @@ func (c *Cluster) PatchObject() error { timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() - return c.patchHelper.Patch(timeoutCtx, c.IonosCluster, patch.WithOwnedConditions{ - Conditions: []clusterv1.ConditionType{ - clusterv1.ReadyCondition, + return c.patchHelper.Patch(timeoutCtx, c.IonosCluster, + patch.WithOwnedV1Beta1Conditions{ + Conditions: []clusterv1.ConditionType{clusterv1.ReadyCondition}, + }, + patch.WithOwnedConditions{ + Conditions: []string{ + string(clusterv1.ReadyCondition), + string(infrav1.IonosCloudClusterReady), + }, }, - }) + ) } // Finalize will make sure to apply a patch to the current IonosCloudCluster. diff --git a/scope/cluster_test.go b/scope/cluster_test.go index ad54485f..866facb2 100644 --- a/scope/cluster_test.go +++ b/scope/cluster_test.go @@ -29,7 +29,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/sets" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" diff --git a/scope/machine.go b/scope/machine.go index 0b6e2cd2..17398005 100644 --- a/scope/machine.go +++ b/scope/machine.go @@ -25,8 +25,8 @@ import ( "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" "k8s.io/client-go/util/retry" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - "sigs.k8s.io/cluster-api/util/conditions" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" + conditions "sigs.k8s.io/cluster-api/util/conditions" "sigs.k8s.io/cluster-api/util/patch" "sigs.k8s.io/controller-runtime/pkg/client" @@ -171,9 +171,14 @@ func (m *Machine) FindLatestMachine( // PatchObject will apply all changes from the IonosMachine. // It will also make sure to patch the status subresource. func (m *Machine) PatchObject() error { - conditions.SetSummary(m.IonosMachine, - conditions.WithConditions( - infrav1.MachineProvisionedCondition)) + if err := conditions.SetSummaryCondition( + m.IonosMachine, + m.IonosMachine, + string(clusterv1.ReadyCondition), + conditions.ForConditionTypes{string(infrav1.MachineProvisionedCondition)}, + ); err != nil { + return err + } timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() @@ -184,10 +189,15 @@ func (m *Machine) PatchObject() error { return m.patchHelper.Patch( timeoutCtx, m.IonosMachine, - patch.WithOwnedConditions{Conditions: []clusterv1.ConditionType{ + patch.WithOwnedV1Beta1Conditions{Conditions: []clusterv1.ConditionType{ clusterv1.ReadyCondition, infrav1.MachineProvisionedCondition, - }}) + }}, + patch.WithOwnedConditions{Conditions: []string{ + string(clusterv1.ReadyCondition), + string(infrav1.MachineProvisionedCondition), + }}, + ) } // Finalize will make sure to apply a patch to the current IonosCloudMachine. diff --git a/scope/machine_test.go b/scope/machine_test.go index ee63b01e..952a9804 100644 --- a/scope/machine_test.go +++ b/scope/machine_test.go @@ -23,7 +23,7 @@ import ( "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/scheme" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" diff --git a/test/e2e/capic_test.go b/test/e2e/capic_test.go index 4e249137..bd64f0eb 100644 --- a/test/e2e/capic_test.go +++ b/test/e2e/capic_test.go @@ -79,7 +79,11 @@ var _ = Describe("Quickstart: Should be able to create a cluster with 3 control- // This check ensures that the resourceVersions are stable, i.e. it verifies there are no // continuous reconciles when everything should be stable. - framework.ValidateResourceVersionStable(ctx, proxy, namespace, clusterctlcluster.FilterClusterObjectsWithNameFilter(clusterName)) + framework.ValidateResourceVersionStable(ctx, framework.ValidateResourceVersionStableInput{ + ClusterProxy: proxy, + Namespace: namespace, + OwnerGraphFilterFunction: clusterctlcluster.FilterClusterObjectsWithNameFilter(clusterName), + }) }, } }) diff --git a/test/e2e/helpers/finalizers.go b/test/e2e/helpers/finalizers.go index 23c93974..ffe1a87c 100644 --- a/test/e2e/helpers/finalizers.go +++ b/test/e2e/helpers/finalizers.go @@ -23,7 +23,7 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" - addonsv1 "sigs.k8s.io/cluster-api/api/addons/v1beta1" + addonsv1 "sigs.k8s.io/cluster-api/api/addons/v1beta2" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" ) diff --git a/test/e2e/helpers/ownerreference.go b/test/e2e/helpers/ownerreference.go index 6acfed06..95a76510 100644 --- a/test/e2e/helpers/ownerreference.go +++ b/test/e2e/helpers/ownerreference.go @@ -21,10 +21,10 @@ package helpers import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - addonsv1 "sigs.k8s.io/cluster-api/api/addons/v1beta1" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1" - controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1" + addonsv1 "sigs.k8s.io/cluster-api/api/addons/v1beta2" + bootstrapv1 "sigs.k8s.io/cluster-api/api/bootstrap/kubeadm/v1beta2" + controlplanev1 "sigs.k8s.io/cluster-api/api/controlplane/kubeadm/v1beta2" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" "sigs.k8s.io/cluster-api/test/framework" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" diff --git a/test/e2e/suite_test.go b/test/e2e/suite_test.go index 1a569b64..e089be93 100644 --- a/test/e2e/suite_test.go +++ b/test/e2e/suite_test.go @@ -30,8 +30,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/klog/v2" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - ipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" + ipamv1 "sigs.k8s.io/cluster-api/api/ipam/v1beta2" "sigs.k8s.io/cluster-api/test/framework" "sigs.k8s.io/cluster-api/test/framework/bootstrap" "sigs.k8s.io/cluster-api/test/framework/clusterctl" From b9ea15e6fab9464f8a46b07f73ba14d9ddbce476 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 12 Jun 2026 13:20:00 +0000 Subject: [PATCH 39/94] chore: update e2e config, release metadata and compatibility matrix for CAPI v1.11 - Point e2e core/bootstrap/control-plane components at CAPI v1.11.11 and add the v1.11 shared clusterctl metadata - Add 0.7 release series (contract v1beta1, matching upstream CAPI v1.11 metadata) for the release carrying this upgrade - Extend the README compatibility matrix: v0.7 requires CAPI v1.11 (the v1beta2 Stage 2 status layout drops top-level status.ready, which CAPI <= v1.10 relies on) Co-Authored-By: Claude Fable 5 --- README.md | 12 +++---- test/e2e/config/ionoscloud.yaml | 18 +++++----- test/e2e/data/shared/v1.11/metadata.yaml | 45 ++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 15 deletions(-) create mode 100644 test/e2e/data/shared/v1.11/metadata.yaml diff --git a/README.md b/README.md index 928e04ee..4bab34fb 100644 --- a/README.md +++ b/README.md @@ -47,12 +47,12 @@ This provider requires **Go 1.25 or newer**. The exact version is specified in ` This provider's versions are compatible with the following versions of Cluster API: -| | Cluster API v1beta1 (v1.8) | Cluster API v1beta1 (v1.9) | Cluster API v1beta1 (v1.10) | -|-----------------------|:--------------------------:|:--------------------------:|:---------------------------:| -| CAPIC v1alpha1 (v0.4) | ✓ | ☓ | ☓ | -| CAPIC v1alpha1 (v0.5) | ✓ | ☓ | ☓ | -| CAPIC v1alpha1 (v0.6) | ✓ | ✓ | ✓ | -| CAPIC v1alpha1 (v0.7) | ✓ | ✓ | ✓ | +| | Cluster API v1beta1 (v1.8) | Cluster API v1beta1 (v1.9) | Cluster API v1beta1 (v1.10) | Cluster API v1beta1/v1beta2 (v1.11) | +|-----------------------|:--------------------------:|:--------------------------:|:---------------------------:|:-----------------------------------:| +| CAPIC v1alpha1 (v0.4) | ✓ | ☓ | ☓ | ☓ | +| CAPIC v1alpha1 (v0.5) | ✓ | ☓ | ☓ | ☓ | +| CAPIC v1alpha1 (v0.6) | ✓ | ✓ | ✓ | ✓ | +| CAPIC v1alpha1 (v0.7) | ☓ | ☓ | ☓ | ✓ | ### Kubernetes Versions diff --git a/test/e2e/config/ionoscloud.yaml b/test/e2e/config/ionoscloud.yaml index 56f232bf..e8ff9524 100644 --- a/test/e2e/config/ionoscloud.yaml +++ b/test/e2e/config/ionoscloud.yaml @@ -5,39 +5,39 @@ providers: - name: cluster-api type: CoreProvider versions: - - name: "v1.10.10" - value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/core-components.yaml" + - name: "v1.11.11" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/core-components.yaml" type: url contract: v1beta1 replacements: - old: --metrics-addr=127.0.0.1:8080 new: --metrics-addr=:8443 files: - - sourcePath: "../data/shared/v1.10/metadata.yaml" + - sourcePath: "../data/shared/v1.11/metadata.yaml" - name: kubeadm type: BootstrapProvider versions: - - name: "v1.10.10" - value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/bootstrap-components.yaml" + - name: "v1.11.11" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/bootstrap-components.yaml" type: url contract: v1beta1 replacements: - old: --metrics-addr=127.0.0.1:8080 new: --metrics-addr=:8443 files: - - sourcePath: "../data/shared/v1.10/metadata.yaml" + - sourcePath: "../data/shared/v1.11/metadata.yaml" - name: kubeadm type: ControlPlaneProvider versions: - - name: "v1.10.10" - value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/control-plane-components.yaml" + - name: "v1.11.11" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/control-plane-components.yaml" type: url contract: v1beta1 replacements: - old: --metrics-addr=127.0.0.1:8080 new: --metrics-addr=:8443 files: - - sourcePath: "../data/shared/v1.10/metadata.yaml" + - sourcePath: "../data/shared/v1.11/metadata.yaml" - name: in-cluster type: IPAMProvider versions: diff --git a/test/e2e/data/shared/v1.11/metadata.yaml b/test/e2e/data/shared/v1.11/metadata.yaml new file mode 100644 index 00000000..8755a669 --- /dev/null +++ b/test/e2e/data/shared/v1.11/metadata.yaml @@ -0,0 +1,45 @@ +apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3 +kind: Metadata +releaseSeries: + - major: 1 + minor: 11 + contract: v1beta1 + - major: 1 + minor: 10 + contract: v1beta1 + - major: 1 + minor: 9 + contract: v1beta1 + - major: 1 + minor: 8 + contract: v1beta1 + - major: 1 + minor: 7 + contract: v1beta1 + - major: 1 + minor: 6 + contract: v1beta1 + - major: 1 + minor: 5 + contract: v1beta1 + - major: 1 + minor: 4 + contract: v1beta1 + - major: 1 + minor: 3 + contract: v1beta1 + - major: 1 + minor: 2 + contract: v1beta1 + - major: 1 + minor: 1 + contract: v1beta1 + - major: 1 + minor: 0 + contract: v1beta1 + - major: 0 + minor: 4 + contract: v1alpha4 + - major: 0 + minor: 3 + contract: v1alpha3 From 14dfe625c5af6bc76bb32aad1c85116976f45669 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 12 Jun 2026 13:25:32 +0000 Subject: [PATCH 40/94] chore: upgrade to Go 1.26 and modernize code - Raise the go directive to 1.26.0; bump builder image to golang:1.26.1 and lint/docs Go version references - Replace all ptr.To(...) call sites with the Go 1.26 extended new(expr) builtin and drop the now-unused ptr.To helper (ptr.Deref/ptr.Equal stay) - Apply gopls modernize fixes: errors.As -> errors.AsType (Go 1.26), strings.Split(...)[0] -> strings.Cut, redundant type arguments - Upgrade golangci-lint v1.64.8 -> v2.12.2 (v1 analyzers cannot parse new(expr) and reported false positives); migrate .golangci.yml to the v2 format, simplify lint-fix via the v2 fmt command, disable the new revive enforce-switch-style rule and exclude goconst in tests - Fix findings surfaced by v2's newer linters: preallocate slices, remove embedded-field selectors, fmt.Fprintf over WriteString(Sprintf), extract kind-name constants in e2e helpers Co-Authored-By: Claude Fable 5 --- .golangci.yml | 345 +++++++++--------- Dockerfile | 2 +- Makefile | 5 +- README.md | 2 +- api/v1alpha1/ionoscloudcluster_types_test.go | 4 +- api/v1alpha1/ionoscloudmachine_types_test.go | 48 ++- go.mod | 14 +- go.sum | 28 +- .../ionoscloudcluster_controller.go | 3 +- .../ionoscloudmachine_controller.go | 4 +- internal/service/cloud/image_test.go | 5 +- internal/service/cloud/ipblock_test.go | 45 ++- internal/service/cloud/network.go | 6 +- internal/service/cloud/network_test.go | 119 +++--- internal/service/cloud/request.go | 4 +- internal/service/cloud/request_test.go | 37 +- internal/service/cloud/server.go | 28 +- internal/service/cloud/server_test.go | 104 +++--- internal/service/cloud/service.go | 3 +- internal/service/cloud/suite_test.go | 47 ++- internal/service/k8s/ipam.go | 3 +- internal/service/k8s/ipam_test.go | 13 +- internal/util/ptr/ptr.go | 5 - internal/util/ptr/ptr_test.go | 11 +- scope/machine.go | 2 +- test/e2e/capic_test.go | 11 +- test/e2e/env_test.go | 2 +- test/e2e/helpers/finalizers.go | 8 +- test/e2e/helpers/ownerreference.go | 23 +- 29 files changed, 456 insertions(+), 475 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index b168f76b..3cce430c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,5 +1,11 @@ +version: "2" +run: + go: "1.26" + build-tags: + - e2e + allow-parallel-runners: true linters: - disable-all: true + default: none enable: - asciicheck - bidichk @@ -13,14 +19,11 @@ linters: - errname - errorlint - exhaustive - - gci - ginkgolinter - goconst - gocritic - godot - - gofumpt - gosec - - gosimple - govet - importas - inamedparam @@ -34,183 +37,181 @@ linters: - nilerr - nolintlint - nosprintfhostport - - prealloc - perfsprint - - revive + - prealloc - reassign + - revive - staticcheck - - stylecheck - tagalign - testifylint - - typecheck - - usetesting - unconvert - unparam - unused + - usetesting - whitespace - -linters-settings: - depguard: - rules: - main: - deny: - - pkg: "github.com/pkg/errors" - desc: Should be replaced by stdlib errors package - gci: - sections: - - standard - - default - - prefix(github.com/ionos-cloud/cluster-api-provider-ionoscloud) - - blank - - dot - gosec: - excludes: - - G601 # Implicit memory aliasing of items from a range statement: Obsolete since Go 1.22 - importas: - no-unaliased: true - alias: - # Kubernetes - - pkg: k8s.io/api/(\w+)/(v[\w\d]+) - alias: $1$2 - - pkg: k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1 - alias: apiextensionsv1 - - pkg: k8s.io/apimachinery/pkg/apis/meta/v1 - alias: metav1 - - pkg: k8s.io/apimachinery/pkg/api/errors - alias: apierrors - - pkg: k8s.io/apimachinery/pkg/util/errors - alias: kerrors - # Controller Runtime - - pkg: sigs.k8s.io/controller-runtime - alias: ctrl - # Cluster API - - pkg: sigs.k8s.io/cluster-api/api/core/v1beta2 - alias: clusterv1 - - pkg: "sigs.k8s.io/cluster-api/test/e2e" - alias: capie2e - - pkg: "sigs.k8s.io/cluster-api/util/conditions" - alias: conditions - - pkg: "sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1" - alias: deprecatedv1beta1conditions - - pkg: "sigs.k8s.io/cluster-api/api/ipam/v1beta2" - alias: ipamv1 - # Own module - - pkg: github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1 - alias: infrav1 - - pkg: github.com/ionos-cloud/sdk-go/v6 - alias: sdk - ireturn: - allow: - - error - - generic - - ionoscloud.Client - - bootstrap.ClusterProvider - - framework.ClusterProxy - loggercheck: - require-string-key: true - no-printf-like: true - revive: - enable-all-rules: true + settings: + depguard: + rules: + main: + deny: + - pkg: github.com/pkg/errors + desc: Should be replaced by stdlib errors package + gosec: + excludes: + - G601 + importas: + alias: + - pkg: k8s.io/api/(\w+)/(v[\w\d]+) + alias: $1$2 + - pkg: k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1 + alias: apiextensionsv1 + - pkg: k8s.io/apimachinery/pkg/apis/meta/v1 + alias: metav1 + - pkg: k8s.io/apimachinery/pkg/api/errors + alias: apierrors + - pkg: k8s.io/apimachinery/pkg/util/errors + alias: kerrors + - pkg: sigs.k8s.io/controller-runtime + alias: ctrl + - pkg: sigs.k8s.io/cluster-api/api/core/v1beta2 + alias: clusterv1 + - pkg: sigs.k8s.io/cluster-api/test/e2e + alias: capie2e + - pkg: sigs.k8s.io/cluster-api/util/conditions + alias: conditions + - pkg: sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1 + alias: deprecatedv1beta1conditions + - pkg: sigs.k8s.io/cluster-api/api/ipam/v1beta2 + alias: ipamv1 + - pkg: github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1 + alias: infrav1 + - pkg: github.com/ionos-cloud/sdk-go/v6 + alias: sdk + no-unaliased: true + ireturn: + allow: + - error + - generic + - ionoscloud.Client + - bootstrap.ClusterProvider + - framework.ClusterProxy + loggercheck: + require-string-key: true + no-printf-like: true + revive: + enable-all-rules: true + rules: + - name: add-constant + disabled: true + - name: cognitive-complexity + arguments: + - 25 + - name: cyclomatic + arguments: + - 15 + - name: enforce-switch-style + disabled: true + - name: line-length-limit + disabled: true + - name: max-public-structs + disabled: true + - name: range-val-address + disabled: true + - name: redundant-import-alias + disabled: true + - name: struct-tag + arguments: + - json,inline + - name: var-naming + arguments: + - [] + - - LAN + - IP + - NIC + testifylint: + enable-all: true + exclusions: + generated: lax rules: - - name: add-constant - disabled: true - - name: cognitive-complexity - arguments: - - 25 - - name: cyclomatic - arguments: - - 15 - - name: line-length-limit - disabled: true - - name: max-public-structs - disabled: true - - name: range-val-address - disabled: true # Obsolete since Go 1.22 - - name: redundant-import-alias - disabled: true # Conflicts with importas which enforces explicit aliases - - name: struct-tag - arguments: - - json,inline - - name: var-naming - arguments: - - [] # first list is an allow list. All initialisms placed here will be ignored - - [LAN, IP, NIC] # second list is a deny list. These rules will be enforced - testifylint: - enable-all: true - + - linters: + - containedctx + - goconst + path: (.+)_test\.go + - linters: + - gocritic + text: 'appendAssign: append result not assigned to the same slice' + - linters: + - revive + text: 'comment-spacings: no space between comment delimiter and comment text' + source: //(\+(kubebuilder|optional|required|list)|#nosec) + - linters: + - revive + text: 'exported: exported method .*\.(Reconcile|SetupWithManager|SetupWebhookWithManager) should have comment or be unexported' + - linters: + - revive + path: suite_test.go + text: maximum number of lines per function exceeded + - linters: + - revive + text: exported (method|function|type|const) (.+) should have comment or be unexported + source: (func|type).*Fake.* + - linters: + - revive + path: fake_\.go + text: exported (method|function|type|const) (.+) should have comment or be unexported + - linters: + - revive + path: (framework|e2e|infrastructure/docker)/.*.go + text: exported (method|function|type|const) (.+) should have comment or be unexported + - linters: + - revive + text: line is \d+ characters, out of limit \d+ + source: \s*//\+kubebuilder:.* + - linters: + - revive + text: line is \d+ characters, out of limit \d+ + source: \s*//.*[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?$ + - linters: + - revive + path: _test\.go + text: 'function-length: maximum number of lines per function exceeded' + - linters: + - unparam + text: always receives + - path: _test\.go + text: should not use dot imports + - path: _test\.go + text: cyclomatic complexity + - path: (framework|e2e)/.*.go + text: should not use dot imports + paths: + - zz_generated.*\.go$ + - .*conversion.*\.go$ + - mock* + - third_party$ + - builtin$ + - examples$ issues: - max-same-issues: 0 max-issues-per-linter: 0 - # We are disabling default golangci exclusions because we want to help reviewers to focus on reviewing the most relevant - # changes in PRs and avoid nitpicking. - exclude-use-default: false - exclude-rules: - - linters: - - containedctx - path: '(.+)_test\.go' - - linters: - - gocritic - text: "appendAssign: append result not assigned to the same slice" - - linters: - - revive - text: "comment-spacings: no space between comment delimiter and comment text" - source: "//(\\+(kubebuilder|optional|required|list)|#nosec)" - - linters: - - revive - text: "exported: exported method .*\\.(Reconcile|SetupWithManager|SetupWebhookWithManager) should have comment or be unexported" - - linters: - - revive - text: maximum number of lines per function exceeded - path: suite_test.go - # Exclude some packages or code to require comments, for example test code, or fake clients. - - linters: - - revive - text: exported (method|function|type|const) (.+) should have comment or be unexported - source: (func|type).*Fake.* - - linters: - - revive - text: exported (method|function|type|const) (.+) should have comment or be unexported - path: fake_\.go - - linters: - - revive - text: exported (method|function|type|const) (.+) should have comment or be unexported - path: "(framework|e2e|infrastructure/docker)/.*.go" - - linters: - - revive - text: "line is \\d+ characters, out of limit \\d+" - source: "\\s*//\\+kubebuilder:.*" - - linters: - - revive - text: "line is \\d+ characters, out of limit \\d+" - # If a line comment ends with something that looks like a URL, we ignore the line length limit. - source: "\\s*//.*[-a-zA-Z0-9@:%_\\+.~#?&//=]{2,256}\\.[a-z]{2,4}\\b(\\/[-a-zA-Z0-9@:%_\\+.~#?&//=]*)?$" - - linters: - - revive - text: "function-length: maximum number of lines per function exceeded" - path: _test\.go - # Disable unparam "always receives" which might not be really - # useful when building libraries. - - linters: - - unparam - text: always receives - # Dot imports for gomega or ginkgo are allowed - # within test files. - - path: _test\.go - text: should not use dot imports - - path: _test\.go - text: cyclomatic complexity - # Append should be able to assign to a different var/slice. - - path: (framework|e2e)/.*.go - text: should not use dot imports - exclude-files: - - "zz_generated.*\\.go$" - - ".*conversion.*\\.go$" - exclude-dirs: - - mock* - -run: - build-tags: - - e2e - timeout: 10m - allow-parallel-runners: true - go: "1.25" + max-same-issues: 0 +formatters: + enable: + - gci + - gofumpt + settings: + gci: + sections: + - standard + - default + - prefix(github.com/ionos-cloud/cluster-api-provider-ionoscloud) + - blank + - dot + exclusions: + generated: lax + paths: + - zz_generated.*\.go$ + - .*conversion.*\.go$ + - mock* + - third_party$ + - builtin$ + - examples$ diff --git a/Dockerfile b/Dockerfile index c565ec37..93552296 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.25.12 AS builder +FROM golang:1.26.4 AS builder ARG TARGETOS ARG TARGETARCH diff --git a/Makefile b/Makefile index 1cf93f20..e13ab382 100644 --- a/Makefile +++ b/Makefile @@ -64,8 +64,7 @@ lint: ## Run lint. .PHONY: lint-fix lint-fix: ## Fix linter problems. - # gci collides with gofumpt. But if we run gci before gofumpt, this will solve the issue. - $(GOLANGCI) run --timeout 5m -c .golangci.yml --enable-only gci --fix + $(GOLANGCI) fmt -c .golangci.yml $(GOLANGCI) run --timeout 5m -c .golangci.yml --fix .PHONY: vet @@ -175,7 +174,7 @@ KUBECTL ?= kubectl KUSTOMIZE ?= $(LOCALBIN)/kustomize CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen ENVTEST ?= $(LOCALBIN)/setup-envtest -GOLANGCI ?= go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.8 +GOLANGCI ?= go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 MOCKERY ?= go run github.com/vektra/mockery/v2@v2.53.5 ## Tool Versions diff --git a/README.md b/README.md index 4bab34fb..e6439498 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ If you need help with CAPIC, please visit the [#cluster-api-ionoscloud][slack] c ### Go Version -This provider requires **Go 1.25 or newer**. The exact version is specified in `go.mod`. +This provider requires **Go 1.26 or newer**. The exact version is specified in `go.mod`. ### Cluster API Versions diff --git a/api/v1alpha1/ionoscloudcluster_types_test.go b/api/v1alpha1/ionoscloudcluster_types_test.go index 8247160c..58ef83cb 100644 --- a/api/v1alpha1/ionoscloudcluster_types_test.go +++ b/api/v1alpha1/ionoscloudcluster_types_test.go @@ -27,8 +27,6 @@ import ( deprecatedv1beta1conditions "sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -156,7 +154,7 @@ var _ = Describe("IonosCloudCluster", func() { Expect(fetched.GetV1Beta1Conditions()).To(BeEmpty()) By("retrieving the cluster and setting the status") - fetched.Status.Initialization = IonosCloudClusterInitializationStatus{Provisioned: ptr.To(true)} + fetched.Status.Initialization = IonosCloudClusterInitializationStatus{Provisioned: new(true)} wantProvisionRequest := ProvisioningRequest{ Method: "POST", RequestPath: "/path/to/resource", diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index b2100671..3102cfb3 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -26,8 +26,6 @@ import ( deprecatedv1beta1conditions "sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -39,12 +37,12 @@ func defaultMachine() *IonosCloudMachine { Namespace: metav1.NamespaceDefault, }, Spec: IonosCloudMachineSpec{ - ProviderID: ptr.To("ionos://ee090ff2-1eef-48ec-a246-a51a33aa4f3a"), + ProviderID: new("ionos://ee090ff2-1eef-48ec-a246-a51a33aa4f3a"), DatacenterID: "ee090ff2-1eef-48ec-a246-a51a33aa4f3a", NumCores: 1, AvailabilityZone: AvailabilityZoneTwo, MemoryMB: 2048, - CPUFamily: ptr.To("AMD_OPTERON"), + CPUFamily: new("AMD_OPTERON"), Disk: &Volume{ Name: "disk", DiskType: VolumeDiskTypeSSDStandard, @@ -65,7 +63,7 @@ func defaultMachine() *IonosCloudMachine { func setInvalidPoolRef(m *IonosCloudMachine, poolType string, kind, apiGroup, name string) { ref := &corev1.TypedLocalObjectReference{ - APIGroup: ptr.To(apiGroup), + APIGroup: new(apiGroup), Kind: kind, Name: name, } @@ -380,7 +378,7 @@ var _ = Describe("IonosCloudMachine Tests", func() { DescribeTable("should allow IPv4PoolRef.Kind GlobalInClusterIPPool and InClusterIPPool", func(kind string) { m := defaultMachine() m.Spec.AdditionalNetworks[0].IPv4PoolRef = &corev1.TypedLocalObjectReference{ - APIGroup: ptr.To("ipam.cluster.x-k8s.io"), + APIGroup: new("ipam.cluster.x-k8s.io"), Kind: kind, Name: "ipv4-pool", } @@ -392,7 +390,7 @@ var _ = Describe("IonosCloudMachine Tests", func() { DescribeTable("should allow IPv6PoolRef.Kind GlobalInClusterIPPool and InClusterIPPool", func(kind string) { m := defaultMachine() m.Spec.AdditionalNetworks[0].IPv6PoolRef = &corev1.TypedLocalObjectReference{ - APIGroup: ptr.To("ipam.cluster.x-k8s.io"), + APIGroup: new("ipam.cluster.x-k8s.io"), Kind: kind, Name: "ipv6-pool", } @@ -424,15 +422,15 @@ var _ = Describe("IonosCloudMachine Tests", func() { Context("FailoverIP", func() { It("should allow setting AUTO as the value", func() { m := defaultMachine() - m.Spec.FailoverIP = ptr.To(CloudResourceConfigAuto) + m.Spec.FailoverIP = new(CloudResourceConfigAuto) Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(m.Spec.FailoverIP).To(Equal(ptr.To(CloudResourceConfigAuto))) + Expect(m.Spec.FailoverIP).To(Equal(new(CloudResourceConfigAuto))) }) It("should allow setting a valid IPv4 address", func() { m := defaultMachine() - m.Spec.FailoverIP = ptr.To("203.0.113.1") + m.Spec.FailoverIP = new("203.0.113.1") Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(m.Spec.FailoverIP).To(Equal(ptr.To("203.0.113.1"))) + Expect(m.Spec.FailoverIP).To(Equal(new("203.0.113.1"))) }) It("should allow setting null", func() { m := defaultMachine() @@ -452,26 +450,26 @@ var _ = Describe("IonosCloudMachine Tests", func() { ) It("should require AUTO to be in capital letters", func() { m := defaultMachine() - m.Spec.FailoverIP = ptr.To("Auto") + m.Spec.FailoverIP = new("Auto") Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }) It("should be immutable", func() { m := defaultMachine() - m.Spec.FailoverIP = ptr.To(CloudResourceConfigAuto) + m.Spec.FailoverIP = new(CloudResourceConfigAuto) Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(m.Spec.FailoverIP).To(Equal(ptr.To(CloudResourceConfigAuto))) - m.Spec.FailoverIP = ptr.To("127.0.0.1") + Expect(m.Spec.FailoverIP).To(Equal(new(CloudResourceConfigAuto))) + m.Spec.FailoverIP = new("127.0.0.1") Expect(k8sClient.Update(context.Background(), m)).ToNot(Succeed()) - m.Spec.FailoverIP = ptr.To("") + m.Spec.FailoverIP = new("") Expect(k8sClient.Update(context.Background(), m)).ToNot(Succeed()) }) }) Context("NetworkID", func() { It("should allow setting an existing NetworkID in the spec", func() { m := defaultMachine() - m.Spec.NetworkID = ptr.To("1") + m.Spec.NetworkID = new("1") Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(m.Spec.NetworkID).To(Equal(ptr.To("1"))) + Expect(m.Spec.NetworkID).To(Equal(new("1"))) }) It("should allow setting null", func() { m := defaultMachine() @@ -480,17 +478,17 @@ var _ = Describe("IonosCloudMachine Tests", func() { }) It("should not allow setting empty NetworkID", func() { m := defaultMachine() - m.Spec.NetworkID = ptr.To("") + m.Spec.NetworkID = new("") Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }) It("should be immutable", func() { m := defaultMachine() - m.Spec.NetworkID = ptr.To("1") + m.Spec.NetworkID = new("1") Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(m.Spec.NetworkID).To(Equal(ptr.To("1"))) - m.Spec.NetworkID = ptr.To("2") + Expect(m.Spec.NetworkID).To(Equal(new("1"))) + m.Spec.NetworkID = new("2") Expect(k8sClient.Update(context.Background(), m)).ToNot(Succeed()) - m.Spec.NetworkID = ptr.To("") + m.Spec.NetworkID = new("") Expect(k8sClient.Update(context.Background(), m)).ToNot(Succeed()) m.Spec.NetworkID = nil Expect(k8sClient.Update(context.Background(), m)).ToNot(Succeed()) @@ -511,7 +509,7 @@ var _ = Describe("IonosCloudMachine Tests", func() { }) It("should fail if cpuFamily is set and type is VCPU", func() { m := defaultMachine() - m.Spec.CPUFamily = ptr.To("some-cpu-family") + m.Spec.CPUFamily = new("some-cpu-family") m.Spec.Type = ServerTypeVCPU Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }) @@ -554,7 +552,7 @@ var _ = Describe("IonosCloudMachine Tests", func() { Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: m.Name, Namespace: m.Namespace}, m)).To(Succeed()) - m.Status.Initialization = IonosCloudMachineInitializationStatus{Provisioned: ptr.To(true)} + m.Status.Initialization = IonosCloudMachineInitializationStatus{Provisioned: new(true)} deprecatedv1beta1conditions.MarkTrue(m, MachineProvisionedCondition) m.Status.CurrentRequest = &ProvisioningRequest{ Method: "GET", diff --git a/go.mod b/go.mod index 9e1b781c..d6784240 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/ionos-cloud/cluster-api-provider-ionoscloud -go 1.25.0 +go 1.26.0 require ( github.com/go-logr/logr v1.4.4 @@ -49,7 +49,7 @@ require ( github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/fsnotify/fsnotify v1.10.1 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect @@ -113,14 +113,14 @@ require ( go.opentelemetry.io/otel/trace v1.43.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect + go.uber.org/zap v1.28.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.53.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect golang.org/x/mod v0.36.0 // indirect golang.org/x/net v0.56.0 // indirect - golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.21.0 // indirect golang.org/x/sys v0.46.0 // indirect golang.org/x/term v0.44.0 // indirect @@ -128,9 +128,9 @@ require ( golang.org/x/time v0.11.0 // indirect golang.org/x/tools v0.45.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect - google.golang.org/grpc v1.79.3 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/grpc v1.81.1 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index 8b36a509..371dc7eb 100644 --- a/go.sum +++ b/go.sum @@ -70,8 +70,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= -github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= @@ -294,8 +294,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -319,8 +319,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= -golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= -golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -357,14 +357,14 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= -google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= -google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4= +google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= +google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/controller/ionoscloudcluster_controller.go b/internal/controller/ionoscloudcluster_controller.go index 25e09a89..d2762cef 100644 --- a/internal/controller/ionoscloudcluster_controller.go +++ b/internal/controller/ionoscloudcluster_controller.go @@ -43,7 +43,6 @@ import ( infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/service/cloud" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/locker" - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/scope" ) @@ -179,7 +178,7 @@ func (r *IonosCloudClusterReconciler) reconcileNormal( Status: metav1.ConditionTrue, Reason: string(infrav1.IonosCloudClusterReady), }) - clusterScope.IonosCluster.Status.Initialization.Provisioned = ptr.To(true) + clusterScope.IonosCluster.Status.Initialization.Provisioned = new(true) // Set deprecated v1beta1 ready field for backwards compatibility. if clusterScope.IonosCluster.Status.Deprecated == nil { clusterScope.IonosCluster.Status.Deprecated = &infrav1.IonosCloudClusterDeprecatedStatus{} diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index f8ca9160..36d3a0e0 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -135,7 +135,7 @@ func (r *IonosCloudMachineReconciler) Reconcile( return ctrl.Result{}, fmt.Errorf("failed to create ionos client: %w", err) } - if !ionosCloudMachine.ObjectMeta.DeletionTimestamp.IsZero() { + if !ionosCloudMachine.DeletionTimestamp.IsZero() { return r.reconcileDelete(ctx, machineScope, cloudService) } @@ -351,7 +351,7 @@ func (r *IonosCloudMachineReconciler) getClusterScope( Name: cluster.Spec.InfrastructureRef.Name, } - if err := r.Client.Get(ctx, infraClusterName, ionosCloudCluster); err != nil { + if err := r.Get(ctx, infraClusterName, ionosCloudCluster); err != nil { if apierrors.IsNotFound(err) { // Cluster has not yet been created return nil, nil diff --git a/internal/service/cloud/image_test.go b/internal/service/cloud/image_test.go index 2d85ab5f..9cba8e84 100644 --- a/internal/service/cloud/image_test.go +++ b/internal/service/cloud/image_test.go @@ -29,7 +29,6 @@ import ( infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud/clienttest" - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" ) type imageTestSuite struct { @@ -114,7 +113,7 @@ func (s *imageTestSuite) TestLookupImageIgnoreMissingMachineVersion() { s.ionosClient.EXPECT().GetImage(s.ctx, "image-1").Return(s.makeTestImage("image-1", "test", "loc"), nil).Once() s.ionosClient.EXPECT().GetDatacenterLocationByID(s.ctx, s.infraMachine.Spec.DatacenterID).Return("loc", nil).Once() - s.infraMachine.Spec.Disk.Image.Selector.UseMachineVersion = ptr.To(false) + s.infraMachine.Spec.Disk.Image.Selector.UseMachineVersion = new(false) s.capiMachine.Spec.Version = "" imageID, err := s.service.lookupImageID(s.ctx, s.machineScope) @@ -233,7 +232,7 @@ func makeTestImage(id, name, location string) *sdk.Image { func makeTestLabel(typ, id, key, value string) sdk.Label { return sdk.Label{ - Id: ptr.To(fmt.Sprintf("urn:label:%s:%s:%s", typ, id, key)), + Id: new(fmt.Sprintf("urn:label:%s:%s:%s", typ, id, key)), Properties: &sdk.LabelProperties{ Key: &key, Value: &value, diff --git a/internal/service/cloud/ipblock_test.go b/internal/service/cloud/ipblock_test.go index 9f9645b8..14861ff5 100644 --- a/internal/service/cloud/ipblock_test.go +++ b/internal/service/cloud/ipblock_test.go @@ -26,7 +26,6 @@ import ( infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud/clienttest" - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" ) type ipBlockTestSuite struct { @@ -59,8 +58,8 @@ func (s *ipBlockTestSuite) TestGetControlPlaneEndpointIPBlockSingleMatch() { *exampleIPBlock(), { Properties: &sdk.IpBlockProperties{ - Name: ptr.To(exampleIPBlockName), - Location: ptr.To("es/vit"), + Name: new(exampleIPBlockName), + Location: new("es/vit"), }, }, }, @@ -75,14 +74,14 @@ func (s *ipBlockTestSuite) TestGetControlPlaneEndpointIPBlockSingleMatch() { func (s *ipBlockTestSuite) TestGetControlPlaneEndpointIPBlockUserSetIP() { s.clusterScope.IonosCluster.Spec.ControlPlaneEndpoint.Host = exampleEndpointIP - name := ptr.To("random name") - location := ptr.To(exampleLocation) + name := new("random name") + location := new(exampleLocation) s.mockListIPBlocksCall().Return(&sdk.IpBlocks{ Items: &[]sdk.IpBlock{ { - Id: ptr.To(exampleIPBlockID), + Id: new(exampleIPBlockID), Metadata: &sdk.DatacenterElementMetadata{ - State: ptr.To(sdk.Available), + State: new(sdk.Available), }, Properties: &sdk.IpBlockProperties{ Name: name, @@ -110,7 +109,7 @@ func (s *ipBlockTestSuite) TestGetControlPlaneEndpointIPBlockUserSetIP() { func (s *ipBlockTestSuite) TestGetControlPlaneEndpointIPBlockPreviouslySetID() { s.clusterScope.IonosCluster.Status.ControlPlaneEndpointIPBlockID = exampleIPBlockID s.mockGetIPBlockByIDCall(exampleIPBlockID).Return(&sdk.IpBlock{ - Id: ptr.To(exampleIPBlockID), + Id: new(exampleIPBlockID), }, nil).Once() block, err := s.service.getControlPlaneEndpointIPBlock(s.ctx, s.clusterScope) @@ -137,8 +136,8 @@ func (s *ipBlockTestSuite) TestGetControlPlaneEndpointIPBlockNoMatch() { Items: &[]sdk.IpBlock{ { Properties: &sdk.IpBlockProperties{ - Name: ptr.To(exampleIPBlockName), - Location: ptr.To("de/fra"), + Name: new(exampleIPBlockName), + Location: new("de/fra"), }, }, }, @@ -249,7 +248,7 @@ func (s *ipBlockTestSuite) TestReconcileControlPlaneEndpointUnavailable() { func (s *ipBlockTestSuite) TestReconcileControlPlaneEndpointUserSetIP() { block := exampleIPBlock() - block.Properties.Name = ptr.To("asdf") + block.Properties.Name = new("asdf") block.Properties.Ips = &[]string{ "another IP", exampleEndpointIP, @@ -324,7 +323,7 @@ func (s *ipBlockTestSuite) TestReconcileControlPlaneEndpointDeletionCreationPend func (s *ipBlockTestSuite) TestReconcileControlPlaneEndpointDeletionUserSetIPWithIPBlockID() { s.clusterScope.IonosCluster.Status.ControlPlaneEndpointIPBlockID = exampleIPBlockID s.mockGetIPBlockByIDCall(exampleIPBlockID).Return(&sdk.IpBlock{ - Id: ptr.To(exampleIPBlockID), + Id: new(exampleIPBlockID), }, nil).Once() requeue, err := s.service.ReconcileControlPlaneEndpointDeletion(s.ctx, s.clusterScope) s.False(requeue) @@ -333,7 +332,7 @@ func (s *ipBlockTestSuite) TestReconcileControlPlaneEndpointDeletionUserSetIPWit func (s *ipBlockTestSuite) TestReconcileControlPlaneEndpointDeletionUserSetIPWithoutIPBlockID() { block := exampleIPBlock() - block.Properties.Name = ptr.To("aaaa") + block.Properties.Name = new("aaaa") block.Properties.Ips = &[]string{ "an IP", exampleEndpointIP, @@ -398,7 +397,7 @@ func (s *ipBlockTestSuite) TestReconcileControlPlaneEndpointDeletionErrorOnDupli } func (s *ipBlockTestSuite) TestReconcileFailoverIPBlockDeletion() { - s.infraMachine.Spec.FailoverIP = ptr.To(infrav1.CloudResourceConfigAuto) + s.infraMachine.Spec.FailoverIP = new(infrav1.CloudResourceConfigAuto) ipBlock := exampleIPBlockWithName(s.service.failoverIPBlockName(s.machineScope)) s.mockGetDatacenterLocationByIDCall(exampleDatacenterID).Return(exampleLocation, nil).Once() @@ -413,11 +412,11 @@ func (s *ipBlockTestSuite) TestReconcileFailoverIPBlockDeletion() { } func (s *ipBlockTestSuite) TestReconcileFailoverIPBlockDeletionSkipped() { - s.infraMachine.Spec.FailoverIP = ptr.To(infrav1.CloudResourceConfigAuto) + s.infraMachine.Spec.FailoverIP = new(infrav1.CloudResourceConfigAuto) ipBlock := exampleIPBlockWithName(s.service.failoverIPBlockName(s.machineScope)) lan := s.exampleLAN() lan.Properties.IpFailover = &[]sdk.IPFailover{{ - Ip: ptr.To(exampleEndpointIP), + Ip: new(exampleEndpointIP), NicUuid: nil, }} @@ -432,7 +431,7 @@ func (s *ipBlockTestSuite) TestReconcileFailoverIPBlockDeletionSkipped() { } func (s *ipBlockTestSuite) TestReconcileFailoverIPBlockDeletionPendingCreation() { - s.infraMachine.Spec.FailoverIP = ptr.To(infrav1.CloudResourceConfigAuto) + s.infraMachine.Spec.FailoverIP = new(infrav1.CloudResourceConfigAuto) s.mockGetDatacenterLocationByIDCall(exampleDatacenterID).Return(exampleLocation, nil).Once() s.mockListIPBlocksCall().Return(nil, nil).Once() @@ -448,7 +447,7 @@ func (s *ipBlockTestSuite) TestReconcileFailoverIPBlockDeletionPendingCreation() } func (s *ipBlockTestSuite) TestReconcileFailoverIPBlockDeletionPendingDeletion() { - s.infraMachine.Spec.FailoverIP = ptr.To(infrav1.CloudResourceConfigAuto) + s.infraMachine.Spec.FailoverIP = new(infrav1.CloudResourceConfigAuto) ipBlock := exampleIPBlockWithName(s.service.failoverIPBlockName(s.machineScope)) s.mockGetDatacenterLocationByIDCall(exampleDatacenterID).Return(exampleLocation, nil).Once() @@ -476,7 +475,7 @@ func (s *ipBlockTestSuite) TestReconcileFailoverIPBlockDeletionPendingDeletion() } func (s *ipBlockTestSuite) TestReconcileFailoverIPBlockDeletionDeletionFinished() { - s.infraMachine.Spec.FailoverIP = ptr.To(infrav1.CloudResourceConfigAuto) + s.infraMachine.Spec.FailoverIP = new(infrav1.CloudResourceConfigAuto) ipBlock := exampleIPBlockWithName(s.service.failoverIPBlockName(s.machineScope)) s.mockGetDatacenterLocationByIDCall(exampleDatacenterID).Return(exampleLocation, nil).Once() @@ -517,13 +516,13 @@ func exampleIPBlock() *sdk.IpBlock { func exampleIPBlockWithName(name string) *sdk.IpBlock { return &sdk.IpBlock{ - Id: ptr.To(exampleIPBlockID), + Id: new(exampleIPBlockID), Metadata: &sdk.DatacenterElementMetadata{ - State: ptr.To(sdk.Available), + State: new(sdk.Available), }, Properties: &sdk.IpBlockProperties{ - Name: ptr.To(name), - Location: ptr.To(exampleLocation), + Name: new(name), + Location: new(exampleLocation), Ips: &[]string{ exampleEndpointIP, }, diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index e8b82af6..e7d29004 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -190,9 +190,9 @@ func (s *Service) createLAN(ctx context.Context, ms *scope.Machine) error { log := s.logger.WithName("createLAN") lanProperties := sdk.LanProperties{ - Name: ptr.To(s.lanName(ms.ClusterScope.Cluster)), - Public: ptr.To(true), - Ipv6CidrBlock: ptr.To("AUTO"), // IPv6 is enabled by default. + Name: new(s.lanName(ms.ClusterScope.Cluster)), + Public: new(true), + Ipv6CidrBlock: new("AUTO"), // IPv6 is enabled by default. } requestPath, err := s.ionosClient.CreateLAN(ctx, ms.DatacenterID(), lanProperties) diff --git a/internal/service/cloud/network_test.go b/internal/service/cloud/network_test.go index 61b590e4..f46dd1ac 100644 --- a/internal/service/cloud/network_test.go +++ b/internal/service/cloud/network_test.go @@ -31,7 +31,6 @@ import ( infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud/clienttest" - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" ) const ( @@ -103,7 +102,7 @@ func (s *lanSuite) TestNetworkGetLAN_ExistingLAN() { lan := s.exampleLAN() s.mockListLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{lan}}, nil).Once() - s.machineScope.IonosMachine.Spec.NetworkID = ptr.To("42") + s.machineScope.IonosMachine.Spec.NetworkID = new("42") foundLAN, err := s.service.getLAN(s.ctx, s.machineScope) s.NoError(err) s.NotNil(foundLAN) @@ -114,7 +113,7 @@ func (s *lanSuite) TestNetworkGetLAN_LANIDNotFound() { lan := s.exampleLAN() s.mockListLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{lan}}, nil).Once() - s.machineScope.IonosMachine.Spec.NetworkID = ptr.To("2") + s.machineScope.IonosMachine.Spec.NetworkID = new("2") foundLAN, err := s.service.getLAN(s.ctx, s.machineScope) s.EqualError(err, "LAN with ID 2 not found") s.Nil(foundLAN) @@ -165,7 +164,7 @@ func (s *lanSuite) TestNetworkReconcileLANExistingLAN() { func (s *lanSuite) TestNetworkReconcileLANExistingLANUnavailable() { lan := s.exampleLAN() - lan.Metadata.State = ptr.To("BUSY") + lan.Metadata.State = new("BUSY") s.mockListLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{lan}}, nil).Once() requeue, err := s.service.ReconcileLAN(s.ctx, s.machineScope) s.NoError(err) @@ -183,7 +182,7 @@ func (s *lanSuite) TestNetworkReconcileLANDeleteLANExistsNoPendingRequestsNoOthe func (s *lanSuite) TestNetworkReconcileLANDeleteLANExistsNoPendingRequestsHasOtherUsersNoDelete() { lan := s.exampleLAN() - lan.Entities.Nics.Items = &[]sdk.Nic{{Id: ptr.To("1")}} + lan.Entities.Nics.Items = &[]sdk.Nic{{Id: new("1")}} s.mockListLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{lan}}, nil).Once() s.mockGetLANDeletionRequestsCall().Return([]sdk.Request{}, nil).Once() requeue, err := s.service.ReconcileLANDeletion(s.ctx, s.machineScope) @@ -232,8 +231,8 @@ func (s *lanSuite) TestReconcileIPFailoverNICNotInFailoverGroup() { props := sdk.LanProperties{ IpFailover: &[]sdk.IPFailover{{ - Ip: ptr.To(exampleEndpointIP), - NicUuid: ptr.To(exampleNICID), + Ip: new(exampleEndpointIP), + NicUuid: new(exampleNICID), }}, } @@ -249,8 +248,8 @@ func (s *lanSuite) TestReconcileIPFailoverNICAlreadyInFailoverGroup() { testServer := s.defaultServer(s.infraMachine, exampleDHCPIP, exampleEndpointIP) testLAN := s.exampleLAN() testLAN.Properties.IpFailover = &[]sdk.IPFailover{{ - Ip: ptr.To(exampleEndpointIP), - NicUuid: ptr.To(exampleNICID), + Ip: new(exampleEndpointIP), + NicUuid: new(exampleNICID), }} s.mockGetServerCall(exampleServerID).Return(testServer, nil).Once() @@ -266,7 +265,7 @@ func (s *lanSuite) TestReconcileIPFailoverNICAlreadyInFailoverGroup() { func (s *lanSuite) TestReconcileIPFailoverForWorkerWithAUTOSettings() { const deploymentLabel = "test-deployment" s.infraMachine.SetLabels(map[string]string{clusterv1.MachineDeploymentNameLabel: deploymentLabel}) - s.infraMachine.Spec.FailoverIP = ptr.To(infrav1.CloudResourceConfigAuto) + s.infraMachine.Spec.FailoverIP = new(infrav1.CloudResourceConfigAuto) testServer := s.defaultServer(s.infraMachine, exampleDHCPIP, exampleWorkerFailoverIP) testLAN := s.exampleLAN() s.mockGetDatacenterLocationByIDCall(exampleDatacenterID).Return(exampleLocation, nil) @@ -293,8 +292,8 @@ func (s *lanSuite) TestReconcileIPFailoverForWorkerWithAUTOSettings() { s.mockGetLANPatchRequestCall().Return([]sdk.Request{patchRequest}, nil).Once() s.mockPatchLANCall(sdk.LanProperties{ IpFailover: &[]sdk.IPFailover{{ - NicUuid: ptr.To(exampleNICID), - Ip: ptr.To(exampleWorkerFailoverIP), + NicUuid: new(exampleNICID), + Ip: new(exampleWorkerFailoverIP), }}, }).Return(*patchRequest.GetMetadata().GetRequestStatus().GetHref(), nil).Once() s.mockWaitForRequestCall(*patchRequest.GetMetadata().GetRequestStatus().GetHref()).Return(nil).Once() @@ -305,7 +304,7 @@ func (s *lanSuite) TestReconcileIPFailoverForWorkerWithAUTOSettings() { func (s *lanSuite) TestReconcileIPFailoverReserveIPBlock() { const deploymentLabel = "test-deployment" s.infraMachine.SetLabels(map[string]string{clusterv1.MachineDeploymentNameLabel: deploymentLabel}) - s.infraMachine.Spec.FailoverIP = ptr.To(infrav1.CloudResourceConfigAuto) + s.infraMachine.Spec.FailoverIP = new(infrav1.CloudResourceConfigAuto) s.mockGetDatacenterLocationByIDCall(exampleDatacenterID).Return(exampleLocation, nil).Once() s.mockListIPBlocksCall().Return(nil, nil).Once() @@ -327,12 +326,12 @@ func (s *lanSuite) TestReconcileIPFailoverNICHasWrongIPInFailoverGroup() { testLAN.Properties.IpFailover = &[]sdk.IPFailover{{ // we expect that the first entry will also be included in the Patch request. - Ip: ptr.To(exampleArbitraryIP), - NicUuid: ptr.To(arbitraryNICID), + Ip: new(exampleArbitraryIP), + NicUuid: new(arbitraryNICID), }, { // LAN contains the NIC but has an incorrect IP - Ip: ptr.To(exampleUnexpectedIP), - NicUuid: ptr.To(exampleNICID), + Ip: new(exampleUnexpectedIP), + NicUuid: new(exampleNICID), }} s.mockGetServerCall(exampleServerID).Return(testServer, nil).Once() @@ -342,11 +341,11 @@ func (s *lanSuite) TestReconcileIPFailoverNICHasWrongIPInFailoverGroup() { // expect a patch request to update the IP props := sdk.LanProperties{ IpFailover: &[]sdk.IPFailover{{ - Ip: ptr.To(exampleArbitraryIP), - NicUuid: ptr.To(arbitraryNICID), + Ip: new(exampleArbitraryIP), + NicUuid: new(arbitraryNICID), }, { - Ip: ptr.To(exampleEndpointIP), - NicUuid: ptr.To(exampleNICID), + Ip: new(exampleEndpointIP), + NicUuid: new(exampleNICID), }}, } @@ -369,12 +368,12 @@ func (s *lanSuite) TestReconcileIPFailoverAnotherNICInFailoverGroup() { testLAN := s.exampleLAN() testLAN.Properties.IpFailover = &[]sdk.IPFailover{{ - Ip: ptr.To(exampleArbitraryIP), - NicUuid: ptr.To(arbitraryNICID), + Ip: new(exampleArbitraryIP), + NicUuid: new(arbitraryNICID), }, { - Ip: ptr.To(exampleEndpointIP), + Ip: new(exampleEndpointIP), // arbitrary NIC ID with the correct endpoint IP - NicUuid: ptr.To("f3b3f8e4-1f3d-11ec-82a8-0242ac130003"), + NicUuid: new("f3b3f8e4-1f3d-11ec-82a8-0242ac130003"), }} s.mockGetServerCall(exampleServerID).Return(testServer, nil).Once() @@ -401,8 +400,8 @@ func (s *lanSuite) reconcileIPFailoverDeletion(testServer *sdk.Server, testLAN s props := sdk.LanProperties{ IpFailover: &[]sdk.IPFailover{{ - Ip: ptr.To(exampleArbitraryIP), - NicUuid: ptr.To(arbitraryNICID), + Ip: new(exampleArbitraryIP), + NicUuid: new(arbitraryNICID), }}, } @@ -416,18 +415,18 @@ func (s *lanSuite) TestReconcileIPFailoverDeletionWorker() { labels[clusterv1.MachineDeploymentNameLabel] = deploymentLabel s.infraMachine.SetLabels(labels) - s.infraMachine.Spec.FailoverIP = ptr.To(infrav1.CloudResourceConfigAuto) + s.infraMachine.Spec.FailoverIP = new(infrav1.CloudResourceConfigAuto) testServer := s.defaultServer(s.infraMachine, exampleDHCPIP, exampleWorkerFailoverIP) s.NoError(s.k8sClient.Update(s.ctx, s.infraMachine)) testLAN := s.exampleLAN() testLAN.Properties.IpFailover = &[]sdk.IPFailover{{ - Ip: ptr.To(exampleArbitraryIP), - NicUuid: ptr.To(arbitraryNICID), + Ip: new(exampleArbitraryIP), + NicUuid: new(arbitraryNICID), }, { - Ip: ptr.To(exampleWorkerFailoverIP), - NicUuid: ptr.To(exampleNICID), + Ip: new(exampleWorkerFailoverIP), + NicUuid: new(exampleNICID), }} s.assertSuccessfulDeletion(s.reconcileIPFailoverDeletion(testServer, testLAN)) @@ -444,11 +443,11 @@ func (s *lanSuite) TestReconcileIPFailoverDeletionControlPlane() { testLAN := s.exampleLAN() testLAN.Properties.IpFailover = &[]sdk.IPFailover{{ - Ip: ptr.To(exampleArbitraryIP), - NicUuid: ptr.To(arbitraryNICID), + Ip: new(exampleArbitraryIP), + NicUuid: new(arbitraryNICID), }, { - Ip: ptr.To(exampleEndpointIP), - NicUuid: ptr.To(exampleNICID), + Ip: new(exampleEndpointIP), + NicUuid: new(exampleNICID), }} s.assertSuccessfulDeletion(s.reconcileIPFailoverDeletion(testServer, testLAN)) @@ -465,23 +464,23 @@ func (s *lanSuite) TestReconcileIPFailoverDeletionControlPlaneSwitchNIC() { newIonosMachine.SetName("test-machine-2") newIonosMachine.SetResourceVersion("") newIonosMachine.SetCreationTimestamp(metav1.NewTime(time.Now())) - newIonosMachine.Spec.ProviderID = ptr.To("ionos://" + exampleSecondaryServerID) + newIonosMachine.Spec.ProviderID = new("ionos://" + exampleSecondaryServerID) err = s.k8sClient.Create(s.ctx, newIonosMachine) s.NoError(err) testLAN := s.exampleLAN() testLAN.Properties.IpFailover = &[]sdk.IPFailover{{ - Ip: ptr.To(exampleArbitraryIP), - NicUuid: ptr.To(arbitraryNICID), + Ip: new(exampleArbitraryIP), + NicUuid: new(arbitraryNICID), }, { - Ip: ptr.To(exampleEndpointIP), - NicUuid: ptr.To(exampleNICID), + Ip: new(exampleEndpointIP), + NicUuid: new(exampleNICID), }} testServer := s.defaultServer(s.infraMachine, exampleDHCPIP, exampleEndpointIP) testSecondaryServer := s.defaultServer(newIonosMachine, exampleSecondaryDHCPIP, exampleEndpointIP) - testSecondaryServer.Id = ptr.To(exampleSecondaryServerID) - (*testSecondaryServer.Entities.Nics.Items)[0].Id = ptr.To(exampleSecondaryNICID) + testSecondaryServer.Id = new(exampleSecondaryServerID) + (*testSecondaryServer.Entities.Nics.Items)[0].Id = new(exampleSecondaryNICID) s.mockGetServerCall(exampleServerID).Return(testServer, nil).Once() s.mockGetServerCall(exampleSecondaryServerID).Return(testSecondaryServer, nil).Once() @@ -491,11 +490,11 @@ func (s *lanSuite) TestReconcileIPFailoverDeletionControlPlaneSwitchNIC() { props := sdk.LanProperties{ IpFailover: &[]sdk.IPFailover{{ - Ip: ptr.To(exampleArbitraryIP), - NicUuid: ptr.To(arbitraryNICID), + Ip: new(exampleArbitraryIP), + NicUuid: new(arbitraryNICID), }, { - Ip: ptr.To(exampleEndpointIP), - NicUuid: ptr.To(exampleSecondaryNICID), + Ip: new(exampleEndpointIP), + NicUuid: new(exampleSecondaryNICID), }}, } @@ -511,7 +510,7 @@ func (s *lanSuite) TestReconcileFailoverDeletionWorkerNoSwap() { labels[clusterv1.MachineDeploymentNameLabel] = deploymentLabel s.infraMachine.SetLabels(labels) - s.infraMachine.Spec.FailoverIP = ptr.To(exampleWorkerFailoverIP) + s.infraMachine.Spec.FailoverIP = new(exampleWorkerFailoverIP) secondaryMachine := s.infraMachine.DeepCopy() secondaryMachine.SetName("test-machine-2") @@ -524,11 +523,11 @@ func (s *lanSuite) TestReconcileFailoverDeletionWorkerNoSwap() { testLAN := s.exampleLAN() testLAN.Properties.IpFailover = &[]sdk.IPFailover{{ - Ip: ptr.To(exampleArbitraryIP), - NicUuid: ptr.To(arbitraryNICID), + Ip: new(exampleArbitraryIP), + NicUuid: new(arbitraryNICID), }, { - Ip: ptr.To(exampleWorkerFailoverIP), - NicUuid: ptr.To(exampleSecondaryNICID), + Ip: new(exampleWorkerFailoverIP), + NicUuid: new(exampleSecondaryNICID), }} s.mockGetServerCall(exampleServerID).Return(testServer, nil).Once() @@ -580,7 +579,7 @@ func (s *lanSuite) TestReconcileIPFailoverDeletionPrimaryNICNotFound() { s.NoError(err) testServer := &sdk.Server{ - Id: ptr.To(exampleServerID), + Id: new(exampleServerID), } s.mockGetServerCall(exampleServerID).Return(testServer, nil).Once() @@ -600,14 +599,14 @@ func (s *lanSuite) TestReconcileIPFailoverDeletionOnWorker() { func (s *lanSuite) exampleIPBlock() sdk.IpBlock { return sdk.IpBlock{ - Id: ptr.To(exampleIPBlockID), + Id: new(exampleIPBlockID), Properties: &sdk.IpBlockProperties{ Ips: &[]string{exampleWorkerFailoverIP}, - Location: ptr.To(s.clusterScope.Location()), - Name: ptr.To(s.service.failoverIPBlockName(s.machineScope)), + Location: new(s.clusterScope.Location()), + Name: new(s.service.failoverIPBlockName(s.machineScope)), }, Metadata: &sdk.DatacenterElementMetadata{ - State: ptr.To(sdk.Available), + State: new(sdk.Available), }, } } @@ -651,9 +650,9 @@ func (s *lanSuite) examplePatchRequest(status string) sdk.Request { func (s *lanSuite) mockCreateLANCall() *clienttest.MockClient_CreateLAN_Call { return s.ionosClient.EXPECT().CreateLAN(s.ctx, s.machineScope.DatacenterID(), sdk.LanProperties{ - Name: ptr.To(s.service.lanName(s.clusterScope.Cluster)), - Public: ptr.To(true), - Ipv6CidrBlock: ptr.To("AUTO"), + Name: new(s.service.lanName(s.clusterScope.Cluster)), + Public: new(true), + Ipv6CidrBlock: new("AUTO"), }) } diff --git a/internal/service/cloud/request.go b/internal/service/cloud/request.go index 54a2751a..855599f8 100644 --- a/internal/service/cloud/request.go +++ b/internal/service/cloud/request.go @@ -120,7 +120,7 @@ func getMatchingRequest[T any]( } // As we later on ignore query parameters in found requests, we need to do the same here for consistency. - urlWithoutQueryParams := strings.Split(url, "?")[0] + urlWithoutQueryParams, _, _ := strings.Cut(url, "?") requests, err := s.ionosClient.GetRequests(ctx, method, urlWithoutQueryParams) if err != nil { @@ -142,7 +142,7 @@ requestLoop: // The reason for comparing here at all is that the received requests can contain some that only contain the // desired URL as substring, e.g. a request for /resource/123/action will also be returned when looking // for /resource/123. We want to ignore those. - trimmedRequestURL := strings.Split(*req.GetProperties().GetUrl(), "?")[0] + trimmedRequestURL, _, _ := strings.Cut(*req.GetProperties().GetUrl(), "?") trimmedRequestURL = strings.TrimSuffix(trimmedRequestURL, "/") trimmedURL := strings.TrimSuffix(urlWithoutQueryParams, "/") if !strings.HasSuffix(trimmedRequestURL, trimmedURL) { diff --git a/internal/service/cloud/request_test.go b/internal/service/cloud/request_test.go index ee726ee3..6e5bfed0 100644 --- a/internal/service/cloud/request_test.go +++ b/internal/service/cloud/request_test.go @@ -28,7 +28,6 @@ import ( "github.com/stretchr/testify/suite" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud/clienttest" - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" ) const ( @@ -41,11 +40,11 @@ func TestMatcher_MatchByName(t *testing.T) { require.False(t, matchByNameFunc(&sdk.Server{}, sdk.Request{})) testServer := sdk.Server{ Properties: &sdk.ServerProperties{ - Name: ptr.To("test"), + Name: new("test"), }, } require.True(t, matchByNameFunc(&testServer, sdk.Request{})) - testServer.Properties.Name = ptr.To("wrong") + testServer.Properties.Name = new("wrong") require.False(t, matchByNameFunc(&testServer, sdk.Request{})) // l := (&sdk.Info{}).GetName() @@ -63,8 +62,8 @@ func TestGetRequestStatusTestSuite(t *testing.T) { func (s *getRequestStatusSuite) TestGetRequestStatusMissingMetadata() { s.mockCheckRequestStatusCall(baseTestURL).Return(&sdk.RequestStatus{ - Href: ptr.To(baseTestURL), - Id: ptr.To("12345"), + Href: new(baseTestURL), + Id: new("12345"), Metadata: nil, }, nil).Once() @@ -85,11 +84,11 @@ func (s *getRequestStatusSuite) TestGetRequestStatusMissingMetadata() { func (s *getRequestStatusSuite) TestGetRequestStatus() { s.mockCheckRequestStatusCall(baseTestURL).Return(&sdk.RequestStatus{ - Href: ptr.To(baseTestURL), - Id: ptr.To("12345"), + Href: new(baseTestURL), + Id: new("12345"), Metadata: &sdk.RequestStatusMetadata{ - Status: ptr.To(sdk.RequestStatusFailed), - Message: ptr.To("Failed to do foo and bar"), + Status: new(sdk.RequestStatusFailed), + Message: new("Failed to do foo and bar"), }, }, nil).Once() @@ -99,10 +98,10 @@ func (s *getRequestStatusSuite) TestGetRequestStatus() { s.Equal("Failed to do foo and bar", message, "message should be 'Failed to do foo and bar'") s.mockCheckRequestStatusCall(baseTestURL).Return(&sdk.RequestStatus{ - Href: ptr.To(baseTestURL), - Id: ptr.To("12345"), + Href: new(baseTestURL), + Id: new("12345"), Metadata: &sdk.RequestStatusMetadata{ - Status: ptr.To(sdk.RequestStatusQueued), + Status: new(sdk.RequestStatusQueued), Message: nil, }, }, nil).Once() @@ -155,7 +154,7 @@ func (s *getMatchingRequestSuite) TestUnsupportedResourceType() { func (s *getMatchingRequestSuite) TestMatching() { // req1 has a mismatch in its target type req1 := s.examplePostRequest("req1", sdk.RequestStatusQueued) - (*req1.Metadata.RequestStatus.Metadata.Targets)[0].Target.Type = ptr.To(sdk.SERVER) + (*req1.Metadata.RequestStatus.Metadata.Targets)[0].Target.Type = new(sdk.SERVER) // req2 has a mismatch in its URL req2 := s.examplePostRequest("req2", sdk.RequestStatusQueued) @@ -205,12 +204,12 @@ func TestHasRequestTargetType(t *testing.T) { req.Metadata.RequestStatus.Metadata.Targets = &[]sdk.RequestTarget{ { - Target: &sdk.ResourceReference{Type: ptr.To(sdk.SERVER)}, + Target: &sdk.ResourceReference{Type: new(sdk.SERVER)}, }, } require.False(t, hasRequestTargetType(req, sdk.LAN)) - (*req.Metadata.RequestStatus.Metadata.Targets)[0].Target.Type = ptr.To(sdk.LAN) + (*req.Metadata.RequestStatus.Metadata.Targets)[0].Target.Type = new(sdk.LAN) require.True(t, hasRequestTargetType(req, sdk.LAN)) } @@ -225,7 +224,7 @@ func TestFindResourceTestSuite(t *testing.T) { func (s *findResourceSuite) TestListingIsEnough() { resource, request, err := findResource( s.ctx, - func(_ context.Context) (*int, error) { return ptr.To(42), nil }, + func(_ context.Context) (*int, error) { return new(42), nil }, func(_ context.Context) (*requestInfo, error) { panic("don't call me") }, ) s.NoError(err) @@ -257,7 +256,7 @@ func (s *findResourceSuite) TestFoundOnSecondListing() { if listCalls == 1 { return nil, nil } - return ptr.To(42), nil + return new(42), nil }, func(_ context.Context) (*requestInfo, error) { return &requestInfo{status: sdk.RequestStatusDone}, nil }, ) @@ -322,8 +321,8 @@ func TestRequestInfo(t *testing.T) { } func TestMetadataHolder(t *testing.T) { - lan1 := &sdk.Lan{Metadata: &sdk.DatacenterElementMetadata{State: ptr.To("BUSY")}} - lan2 := &sdk.Lan{Metadata: &sdk.DatacenterElementMetadata{State: ptr.To(sdk.Available)}} + lan1 := &sdk.Lan{Metadata: &sdk.DatacenterElementMetadata{State: new("BUSY")}} + lan2 := &sdk.Lan{Metadata: &sdk.DatacenterElementMetadata{State: new(sdk.Available)}} require.False(t, isAvailable(getState(lan1))) require.True(t, isAvailable(getState(lan2))) diff --git a/internal/service/cloud/server.go b/internal/service/cloud/server.go index 24335abb..3c08ce8d 100644 --- a/internal/service/cloud/server.go +++ b/internal/service/cloud/server.go @@ -145,7 +145,7 @@ func (s *Service) ReconcileServerDeletion(ctx context.Context, ms *scope.Machine // FinalizeMachineProvisioning marks the machine as provisioned. func (*Service) FinalizeMachineProvisioning(_ context.Context, ms *scope.Machine) (bool, error) { - ms.IonosMachine.Status.Initialization.Provisioned = ptr.To(true) + ms.IonosMachine.Status.Initialization.Provisioned = new(true) // Set deprecated v1beta1 ready field for backwards compatibility. if ms.IonosMachine.Status.Deprecated == nil { ms.IonosMachine.Status.Deprecated = &infrav1.IonosCloudMachineDeprecatedStatus{} @@ -386,12 +386,12 @@ func (*Service) buildServerProperties( ms *scope.Machine, machineSpec *infrav1.IonosCloudMachineSpec, ) sdk.ServerProperties { props := sdk.ServerProperties{ - AvailabilityZone: ptr.To(machineSpec.AvailabilityZone.String()), + AvailabilityZone: new(machineSpec.AvailabilityZone.String()), Cores: &machineSpec.NumCores, - Name: ptr.To(ms.IonosMachine.Name), + Name: new(ms.IonosMachine.Name), Ram: &machineSpec.MemoryMB, CpuFamily: machineSpec.CPUFamily, - Type: ptr.To(machineSpec.Type.String()), + Type: new(machineSpec.Type.String()), } return props @@ -409,10 +409,10 @@ func (s *Service) buildServerEntities(ms *scope.Machine, params serverEntityPara machineSpec := params.machineSpec bootVolume := sdk.Volume{ Properties: &sdk.VolumeProperties{ - AvailabilityZone: ptr.To(machineSpec.Disk.AvailabilityZone.String()), - Name: ptr.To(s.volumeName(ms.IonosMachine)), - Size: ptr.To(float32(machineSpec.Disk.SizeGB)), - Type: ptr.To(machineSpec.Disk.DiskType.String()), + AvailabilityZone: new(machineSpec.Disk.AvailabilityZone.String()), + Name: new(s.volumeName(ms.IonosMachine)), + Size: new(float32(machineSpec.Disk.SizeGB)), + Type: new(machineSpec.Disk.DiskType.String()), UserData: ¶ms.boostrapData, }, } @@ -428,15 +428,15 @@ func (s *Service) buildServerEntities(ms *scope.Machine, params serverEntityPara primaryNIC := sdk.Nic{ Properties: &sdk.NicProperties{ Lan: ¶ms.lanID, - Name: ptr.To(s.nicName(ms.IonosMachine)), - Dhcp: ptr.To(true), + Name: new(s.nicName(ms.IonosMachine)), + Dhcp: new(true), }, } if ms.IonosMachine.Status.MachineNetworkInfo != nil { nicInfo := ms.IonosMachine.Status.MachineNetworkInfo.NICInfo[0] - primaryNIC.Properties.Ips = ptr.To(nicInfo.IPv4Addresses) - primaryNIC.Properties.Ipv6Ips = ptr.To(nicInfo.IPv6Addresses) + primaryNIC.Properties.Ips = new(nicInfo.IPv4Addresses) + primaryNIC.Properties.Ipv6Ips = new(nicInfo.IPv6Addresses) } // In case we want to retrieve a public IP from the DHCP, we need to @@ -462,8 +462,8 @@ func (s *Service) buildServerEntities(ms *scope.Machine, params serverEntityPara if ms.IonosMachine.Status.MachineNetworkInfo != nil { nicInfo := ms.IonosMachine.Status.MachineNetworkInfo.NICInfo[i+1] - nic.Properties.Ips = ptr.To(nicInfo.IPv4Addresses) - nic.Properties.Ipv6Ips = ptr.To(nicInfo.IPv6Addresses) + nic.Properties.Ips = new(nicInfo.IPv4Addresses) + nic.Properties.Ipv6Ips = new(nicInfo.IPv6Addresses) } items = append(items, nic) diff --git a/internal/service/cloud/server_test.go b/internal/service/cloud/server_test.go index 7799e4c7..a118db4c 100644 --- a/internal/service/cloud/server_test.go +++ b/internal/service/cloud/server_test.go @@ -53,7 +53,7 @@ func (s *serverSuite) TestReconcileServerNoBootstrapSecret() { s.True(requeue) s.Error(err) - s.machineScope.Machine.Spec.Bootstrap.DataSecretName = ptr.To("test") + s.machineScope.Machine.Spec.Bootstrap.DataSecretName = new("test") requeue, err = s.service.ReconcileServer(s.ctx, s.machineScope) s.False(requeue) s.NoError(err) @@ -74,10 +74,10 @@ func (s *serverSuite) TestReconcileServerRequestDoneStateBusy() { s.mockListServersCall().Return(&sdk.Servers{Items: &[]sdk.Server{ { Metadata: &sdk.DatacenterElementMetadata{ - State: ptr.To(sdk.Busy), + State: new(sdk.Busy), }, Properties: &sdk.ServerProperties{ - Name: ptr.To(s.infraMachine.Name), + Name: new(s.infraMachine.Name), }, }, }}, nil).Once() @@ -93,22 +93,22 @@ func (s *serverSuite) TestReconcileServerRequestDoneStateAvailable() { s.mockListServersCall().Return(&sdk.Servers{Items: &[]sdk.Server{ { Metadata: &sdk.DatacenterElementMetadata{ - State: ptr.To(sdk.Available), + State: new(sdk.Available), }, Properties: &sdk.ServerProperties{ - Name: ptr.To(s.infraMachine.Name), - VmState: ptr.To("RUNNING"), + Name: new(s.infraMachine.Name), + VmState: new("RUNNING"), }, Entities: &sdk.ServerEntities{ Nics: &sdk.Nics{ Items: &[]sdk.Nic{{ Properties: &sdk.NicProperties{ - Name: ptr.To(s.service.nicName(s.infraMachine)), - Dhcp: ptr.To(true), - Lan: ptr.To(int32(1)), - Ips: ptr.To([]string{"198.51.100.10"}), - Ipv6CidrBlock: ptr.To("2001:db8:2c0:301::/64"), - Ipv6Ips: ptr.To([]string{"2001:db8:2c0:301::1"}), + Name: new(s.service.nicName(s.infraMachine)), + Dhcp: new(true), + Lan: new(int32(1)), + Ips: new([]string{"198.51.100.10"}), + Ipv6CidrBlock: new("2001:db8:2c0:301::/64"), + Ipv6Ips: new([]string{"2001:db8:2c0:301::1"}), }, }}, }, @@ -132,13 +132,13 @@ func (s *serverSuite) TestReconcileServerRequestDoneStateAvailableTurnedOff() { s.mockGetServerCreationRequestCall().Return([]sdk.Request{s.examplePostRequest(sdk.RequestStatusDone)}, nil) s.mockListServersCall().Return(&sdk.Servers{Items: &[]sdk.Server{ { - Id: ptr.To(exampleServerID), + Id: new(exampleServerID), Metadata: &sdk.DatacenterElementMetadata{ - State: ptr.To(sdk.Available), + State: new(sdk.Available), }, Properties: &sdk.ServerProperties{ - Name: ptr.To(s.infraMachine.Name), - VmState: ptr.To("SHUTOFF"), + Name: new(s.infraMachine.Name), + VmState: new("SHUTOFF"), }, }, }}, nil).Once() @@ -153,35 +153,35 @@ func (s *serverSuite) TestReconcileServerRequestDoneStateAvailableTurnedOff() { func (s *serverSuite) TestReconcileServerAdditionalNetworks() { s.machineScope.IonosMachine.Spec.AdditionalNetworks = []infrav1.Network{{ NetworkID: 43, - DHCP: ptr.To(false), + DHCP: new(false), }, { NetworkID: 44, - DHCP: ptr.To(true), + DHCP: new(true), }} s.prepareReconcileServerRequestTest() s.mockGetServerCreationRequestCall().Return([]sdk.Request{}, nil) s.mockListLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{{ - Id: ptr.To(exampleLANID), + Id: new(exampleLANID), Properties: &sdk.LanProperties{ - Name: ptr.To(s.service.lanName(s.clusterScope.Cluster)), - Public: ptr.To(true), + Name: new(s.service.lanName(s.clusterScope.Cluster)), + Public: new(true), }, }}}, nil) properties, entities := s.defaultServerComponents() *entities.Nics.Items = append(*entities.Nics.Items, sdk.Nic{ Properties: &sdk.NicProperties{ - Lan: ptr.To[int32](43), - Dhcp: ptr.To(false), + Lan: new(int32(43)), + Dhcp: new(false), }, }, sdk.Nic{ Properties: &sdk.NicProperties{ - Lan: ptr.To[int32](44), - Dhcp: ptr.To(true), + Lan: new(int32(44)), + Dhcp: new(true), }, }) - s.mockCreateServerCall(properties, entities).Return(&sdk.Server{Id: ptr.To("12345")}, "location/to/server", nil) + s.mockCreateServerCall(properties, entities).Return(&sdk.Server{Id: new("12345")}, "location/to/server", nil) requeue, err := s.service.ReconcileServer(s.ctx, s.machineScope) s.Equal("ionos://12345", ptr.Deref(s.machineScope.IonosMachine.Spec.ProviderID, "")) @@ -192,12 +192,12 @@ func (s *serverSuite) TestReconcileServerAdditionalNetworks() { func (s *serverSuite) TestReconcileEnterpriseServerNoRequest() { s.prepareReconcileServerRequestTest() s.mockGetServerCreationRequestCall().Return([]sdk.Request{}, nil) - s.mockCreateServerCall(s.defaultServerComponents()).Return(&sdk.Server{Id: ptr.To("12345")}, "location/to/server", nil) + s.mockCreateServerCall(s.defaultServerComponents()).Return(&sdk.Server{Id: new("12345")}, "location/to/server", nil) s.mockListLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{{ - Id: ptr.To(exampleLANID), + Id: new(exampleLANID), Properties: &sdk.LanProperties{ - Name: ptr.To(s.service.lanName(s.clusterScope.Cluster)), - Public: ptr.To(true), + Name: new(s.service.lanName(s.clusterScope.Cluster)), + Public: new(true), }, }}}, nil) @@ -212,13 +212,13 @@ func (s *serverSuite) TestReconcileVCPUServerNoRequest() { s.mockGetServerCreationRequestCall().Return([]sdk.Request{}, nil) props, entities := s.defaultServerComponents() - props.Type = ptr.To(infrav1.ServerTypeVCPU.String()) - s.mockCreateServerCall(props, entities).Return(&sdk.Server{Id: ptr.To("12345")}, "location/to/server", nil) + props.Type = new(infrav1.ServerTypeVCPU.String()) + s.mockCreateServerCall(props, entities).Return(&sdk.Server{Id: new("12345")}, "location/to/server", nil) s.mockListLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{{ - Id: ptr.To(exampleLANID), + Id: new(exampleLANID), Properties: &sdk.LanProperties{ - Name: ptr.To(s.service.lanName(s.clusterScope.Cluster)), - Public: ptr.To(true), + Name: new(s.service.lanName(s.clusterScope.Cluster)), + Public: new(true), }, }}}, nil) @@ -243,14 +243,14 @@ func (s *serverSuite) prepareReconcileServerRequestTest() { s.NoError(s.k8sClient.Create(s.ctx, bootstrapSecret)) - s.machineScope.Machine.Spec.Bootstrap.DataSecretName = ptr.To("test") + s.machineScope.Machine.Spec.Bootstrap.DataSecretName = new("test") s.machineScope.IonosMachine.Spec.ProviderID = nil s.mockListServersCall().Return(&sdk.Servers{Items: &[]sdk.Server{}}, nil).Once() } func (s *serverSuite) TestReconcileServerDeletion() { s.mockGetServerCall(exampleServerID).Return(&sdk.Server{ - Id: ptr.To(exampleServerID), + Id: new(exampleServerID), }, nil) reqLocation := "delete/location" @@ -264,16 +264,16 @@ func (s *serverSuite) TestReconcileServerDeletion() { func (s *serverSuite) TestReconcileServerDeletionDeleteBootVolume() { s.mockGetServerCall(exampleServerID).Return(&sdk.Server{ - Id: ptr.To(exampleServerID), + Id: new(exampleServerID), Properties: &sdk.ServerProperties{ BootVolume: &sdk.ResourceReference{ - Id: ptr.To(exampleBootVolumeID), + Id: new(exampleBootVolumeID), }, }, }, nil).Once() s.mockGetServerCall(exampleServerID).Return(&sdk.Server{ - Id: ptr.To(exampleServerID), + Id: new(exampleServerID), }, nil).Once() reqLocationVolume := "delete/location/volume" @@ -292,12 +292,12 @@ func (s *serverSuite) TestReconcileServerDeletionDeleteBootVolume() { } func (s *serverSuite) TestReconcileServerDeletionDeleteAllVolumes() { - s.clusterScope.Cluster.DeletionTimestamp = ptr.To(metav1.Now()) + s.clusterScope.Cluster.DeletionTimestamp = new(metav1.Now()) s.mockGetServerCall(exampleServerID).Return(&sdk.Server{ - Id: ptr.To(exampleServerID), + Id: new(exampleServerID), Properties: &sdk.ServerProperties{ BootVolume: &sdk.ResourceReference{ - Id: ptr.To(exampleBootVolumeID), + Id: new(exampleBootVolumeID), }, }, }, nil).Once() @@ -358,7 +358,7 @@ func (s *serverSuite) TestReconcileServerDeletionCreateRequestPending() { func (s *serverSuite) TestReconcileServerDeletionRequestPending() { s.mockGetServerCall(exampleServerID).Return(&sdk.Server{ - Id: ptr.To(exampleServerID), + Id: new(exampleServerID), }, nil) exampleRequest := s.exampleDeleteRequest(sdk.RequestStatusQueued, exampleServerID) @@ -376,7 +376,7 @@ func (s *serverSuite) TestReconcileServerDeletionRequestPending() { func (s *serverSuite) TestReconcileServerDeletionRequestDone() { s.mockGetServerCall(exampleServerID).Return(&sdk.Server{ - Id: ptr.To(exampleServerID), + Id: new(exampleServerID), }, nil) exampleRequest := s.exampleDeleteRequest(sdk.RequestStatusDone, exampleServerID) @@ -392,7 +392,7 @@ func (s *serverSuite) TestReconcileServerDeletionRequestDone() { func (s *serverSuite) TestReconcileServerDeletionRequestFailed() { s.mockGetServerCall(exampleServerID).Return(&sdk.Server{ - Id: ptr.To(exampleServerID), + Id: new(exampleServerID), }, nil) exampleRequest := s.exampleDeleteRequest(sdk.RequestStatusFailed, exampleServerID) @@ -436,7 +436,7 @@ func (s *serverSuite) TestGetServerWithoutProviderIDFoundInList() { s.mockListServersCall().Return(&sdk.Servers{Items: &[]sdk.Server{ { Properties: &sdk.ServerProperties{ - Name: ptr.To(s.infraMachine.Name), + Name: new(s.infraMachine.Name), }, }, }}, nil) @@ -449,15 +449,15 @@ func (s *serverSuite) TestGetServerWithoutProviderIDFoundInList() { //nolint:unused func (*serverSuite) exampleServer() sdk.Server { return sdk.Server{ - Id: ptr.To("1"), + Id: new("1"), Metadata: &sdk.DatacenterElementMetadata{ - State: ptr.To(sdk.Available), + State: new(sdk.Available), }, Properties: &sdk.ServerProperties{ - AvailabilityZone: ptr.To("AUTO"), + AvailabilityZone: new("AUTO"), BootVolume: &sdk.ResourceReference{ - Id: ptr.To("1"), - Type: ptr.To(sdk.VOLUME), + Id: new("1"), + Type: new(sdk.VOLUME), }, Name: nil, VmState: nil, diff --git a/internal/service/cloud/service.go b/internal/service/cloud/service.go index 4f408315..fe0862f4 100644 --- a/internal/service/cloud/service.go +++ b/internal/service/cloud/service.go @@ -62,8 +62,7 @@ func isNotFound(err error) bool { return false } - var target sdk.GenericOpenAPIError - if errors.As(err, &target) { + if target, ok := errors.AsType[sdk.GenericOpenAPIError](err); ok { return target.StatusCode() == http.StatusNotFound } diff --git a/internal/service/cloud/suite_test.go b/internal/service/cloud/suite_test.go index 4ab44891..3de47d00 100644 --- a/internal/service/cloud/suite_test.go +++ b/internal/service/cloud/suite_test.go @@ -39,7 +39,6 @@ import ( infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud/clienttest" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/locker" - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/scope" ) @@ -152,12 +151,12 @@ func (s *ServiceTestSuite) SetupTest() { }, }, Spec: infrav1.IonosCloudMachineSpec{ - ProviderID: ptr.To("ionos://" + exampleServerID), + ProviderID: new("ionos://" + exampleServerID), DatacenterID: "ccf27092-34e8-499e-a2f5-2bdee9d34a12", NumCores: 2, AvailabilityZone: infrav1.AvailabilityZoneAuto, MemoryMB: 4096, - CPUFamily: ptr.To("AMD_OPTERON"), + CPUFamily: new("AMD_OPTERON"), Disk: &infrav1.Volume{ Name: "test-machine-hdd", DiskType: infrav1.VolumeDiskTypeHDD, @@ -225,7 +224,7 @@ func (*ServiceTestSuite) exampleRequest(opts requestBuildOptions) sdk.Request { Href: &opts.href, Metadata: &sdk.RequestStatusMetadata{ Status: &opts.status, - Message: ptr.To("test"), + Message: new("test"), }, }, }, @@ -252,14 +251,14 @@ func (*ServiceTestSuite) exampleRequest(opts requestBuildOptions) sdk.Request { func (s *ServiceTestSuite) defaultServer(m *infrav1.IonosCloudMachine, ips ...string) *sdk.Server { return &sdk.Server{ - Id: ptr.To(exampleServerID), + Id: new(exampleServerID), Entities: &sdk.ServerEntities{ Nics: &sdk.Nics{ Items: &[]sdk.Nic{{ - Id: ptr.To(exampleNICID), + Id: new(exampleNICID), Properties: &sdk.NicProperties{ - Dhcp: ptr.To(true), - Name: ptr.To(s.service.nicName(m)), + Dhcp: new(true), + Name: new(s.service.nicName(m)), Ips: &ips, }, }}, @@ -289,12 +288,12 @@ func (s *ServiceTestSuite) buildIPBlockRequestWithName(name, status, method, id func (s *ServiceTestSuite) exampleLAN() sdk.Lan { return sdk.Lan{ - Id: ptr.To(exampleLANID), + Id: new(exampleLANID), Properties: &sdk.LanProperties{ - Name: ptr.To(s.service.lanName(s.clusterScope.Cluster)), + Name: new(s.service.lanName(s.clusterScope.Cluster)), }, Metadata: &sdk.DatacenterElementMetadata{ - State: ptr.To(sdk.Available), + State: new(sdk.Available), }, Entities: &sdk.LanEntities{ Nics: &sdk.LanNics{ @@ -307,12 +306,12 @@ func (s *ServiceTestSuite) exampleLAN() sdk.Lan { func (s *ServiceTestSuite) defaultServerComponents() (sdk.ServerProperties, sdk.ServerEntities) { m := s.machineScope.IonosMachine props := sdk.ServerProperties{ - AvailabilityZone: ptr.To(m.Spec.AvailabilityZone.String()), - Cores: ptr.To(m.Spec.NumCores), + AvailabilityZone: new(m.Spec.AvailabilityZone.String()), + Cores: new(m.Spec.NumCores), CpuFamily: m.Spec.CPUFamily, - Name: ptr.To(m.Name), - Ram: ptr.To(m.Spec.MemoryMB), - Type: ptr.To(m.Spec.Type.String()), + Name: new(m.Name), + Ram: new(m.Spec.MemoryMB), + Type: new(m.Spec.Type.String()), } lanID, _ := strconv.ParseInt(exampleLANID, 10, 32) @@ -320,19 +319,19 @@ func (s *ServiceTestSuite) defaultServerComponents() (sdk.ServerProperties, sdk. entities := sdk.ServerEntities{ Nics: &sdk.Nics{Items: &[]sdk.Nic{{ Properties: &sdk.NicProperties{ - Lan: ptr.To(int32(lanID)), - Name: ptr.To(s.service.nicName(m)), - Dhcp: ptr.To(true), + Lan: new(int32(lanID)), + Name: new(s.service.nicName(m)), + Dhcp: new(true), }, }}}, Volumes: &sdk.AttachedVolumes{ Items: &[]sdk.Volume{{ Properties: &sdk.VolumeProperties{ - AvailabilityZone: ptr.To(m.Spec.Disk.AvailabilityZone.String()), - Image: ptr.To(m.Spec.Disk.Image.ID), - Name: ptr.To(s.service.volumeName(m)), - Type: ptr.To(m.Spec.Disk.DiskType.String()), - Size: ptr.To(float32(m.Spec.Disk.SizeGB)), + AvailabilityZone: new(m.Spec.Disk.AvailabilityZone.String()), + Image: new(m.Spec.Disk.Image.ID), + Name: new(s.service.volumeName(m)), + Type: new(m.Spec.Disk.DiskType.String()), + Size: new(float32(m.Spec.Disk.SizeGB)), UserData: nil, }, }}, diff --git a/internal/service/k8s/ipam.go b/internal/service/k8s/ipam.go index bceb8fa3..9527d924 100644 --- a/internal/service/k8s/ipam.go +++ b/internal/service/k8s/ipam.go @@ -102,7 +102,8 @@ func (h *Helper) ReconcileIPAddressClaimsDeletion(ctx context.Context, machineSc log.V(4).Info("removing finalizers from IPAddressClaims.") formats := []string{ipV4Format, ipV6Format} - nicNames := []string{fmt.Sprintf(primaryNICFormat, machineScope.IonosMachine.Name)} + nicNames := make([]string, 0, 1+len(machineScope.IonosMachine.Spec.AdditionalNetworks)) + nicNames = append(nicNames, fmt.Sprintf(primaryNICFormat, machineScope.IonosMachine.Name)) for _, network := range machineScope.IonosMachine.Spec.AdditionalNetworks { nicName := fmt.Sprintf(additionalNICFormat, machineScope.IonosMachine.Name, network.NetworkID) diff --git a/internal/service/k8s/ipam_test.go b/internal/service/k8s/ipam_test.go index 615c7f6c..418da924 100644 --- a/internal/service/k8s/ipam_test.go +++ b/internal/service/k8s/ipam_test.go @@ -36,7 +36,6 @@ import ( "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud/clienttest" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/service/cloud" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/locker" - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/scope" ) @@ -112,12 +111,12 @@ func (s *IpamTestSuite) SetupTest() { }, }, Spec: infrav1.IonosCloudMachineSpec{ - ProviderID: ptr.To("ionos://dd426c63-cd1d-4c02-aca3-13b4a27c2ebf"), + ProviderID: new("ionos://dd426c63-cd1d-4c02-aca3-13b4a27c2ebf"), DatacenterID: "ccf27092-34e8-499e-a2f5-2bdee9d34a12", NumCores: 2, AvailabilityZone: infrav1.AvailabilityZoneAuto, MemoryMB: 4096, - CPUFamily: ptr.To("AMD_OPTERON"), + CPUFamily: new("AMD_OPTERON"), Disk: &infrav1.Volume{ Name: "test-machine-hdd", DiskType: infrav1.VolumeDiskTypeHDD, @@ -298,7 +297,7 @@ func (s *IpamTestSuite) TestReconcileIPAddressesAdditionalIpv6GetIPFromClaim() { func defaultInClusterIPv4PoolRef() *corev1.TypedLocalObjectReference { return &corev1.TypedLocalObjectReference{ - APIGroup: ptr.To("ipam.cluster.x-k8s.io"), + APIGroup: new("ipam.cluster.x-k8s.io"), Kind: "InClusterIPPool", Name: "incluster-ipv4-pool", } @@ -306,7 +305,7 @@ func defaultInClusterIPv4PoolRef() *corev1.TypedLocalObjectReference { func defaultInClusterIPv6PoolRef() *corev1.TypedLocalObjectReference { return &corev1.TypedLocalObjectReference{ - APIGroup: ptr.To("ipam.cluster.x-k8s.io"), + APIGroup: new("ipam.cluster.x-k8s.io"), Kind: "InClusterIPPool", Name: "incluster-ipv6-pool", } @@ -323,7 +322,7 @@ func defaultIPv4Address(claim *ipamv1.IPAddressClaim, poolRef *corev1.TypedLocal ClaimRef: ipamv1.IPAddressClaimReference{Name: claim.GetName()}, PoolRef: toIPPoolReference(poolRef), Address: "10.0.0.2", - Prefix: ptr.To[int32](16), + Prefix: new(int32(16)), }, } } @@ -339,7 +338,7 @@ func defaultIPv6Address(claim *ipamv1.IPAddressClaim, poolRef *corev1.TypedLocal ClaimRef: ipamv1.IPAddressClaimReference{Name: claim.GetName()}, PoolRef: toIPPoolReference(poolRef), Address: "2001:db8::", - Prefix: ptr.To[int32](42), + Prefix: new(int32(42)), }, } } diff --git a/internal/util/ptr/ptr.go b/internal/util/ptr/ptr.go index 0af076f7..a9fe3a28 100644 --- a/internal/util/ptr/ptr.go +++ b/internal/util/ptr/ptr.go @@ -21,11 +21,6 @@ limitations under the License. // Dependabot to create a new PR for a version update. package ptr -// To returns a pointer to the given value. -func To[T any](v T) *T { - return &v -} - // Deref attempts to dereference a pointer and return the value. // If the pointer is nil, the provided default value will be returned instead. func Deref[T any](ptr *T, def T) T { diff --git a/internal/util/ptr/ptr_test.go b/internal/util/ptr/ptr_test.go index 91371fd0..52f23298 100644 --- a/internal/util/ptr/ptr_test.go +++ b/internal/util/ptr/ptr_test.go @@ -22,13 +22,6 @@ import ( "github.com/stretchr/testify/require" ) -func Test_PtrTo(t *testing.T) { - type testType struct{} - - require.IsType(t, (*testType)(nil), To(testType{})) - require.IsType(t, (**testType)(nil), To((*testType)(nil))) -} - func Test_PtrDeref(t *testing.T) { type testType struct{} @@ -38,6 +31,6 @@ func Test_PtrDeref(t *testing.T) { require.IsType(t, &testType{}, Deref(&testTypeInstance, &testType{})) // validate that deref returns default when passing a nil value var nilTestType *testType - require.Equal(t, testType{}, Deref[testType](nilTestType, testType{})) - require.Equal(t, testType{}, Deref[testType](nil, testType{})) + require.Equal(t, testType{}, Deref(nilTestType, testType{})) + require.Equal(t, testType{}, Deref(nil, testType{})) } diff --git a/scope/machine.go b/scope/machine.go index 17398005..216a558c 100644 --- a/scope/machine.go +++ b/scope/machine.go @@ -119,7 +119,7 @@ func (m *Machine) DatacenterID() string { // SetProviderID sets the provider ID for the IonosCloudMachine. func (m *Machine) SetProviderID(id string) { - m.IonosMachine.Spec.ProviderID = ptr.To("ionos://" + id) + m.IonosMachine.Spec.ProviderID = new("ionos://" + id) } // CountMachines returns the number of existing IonosCloudMachines in the same namespace diff --git a/test/e2e/capic_test.go b/test/e2e/capic_test.go index bd64f0eb..2b724a10 100644 --- a/test/e2e/capic_test.go +++ b/test/e2e/capic_test.go @@ -27,7 +27,6 @@ import ( runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/test/e2e/helpers" . "github.com/onsi/ginkgo/v2" @@ -42,8 +41,8 @@ var _ = Describe("Quickstart: Should be able to create a cluster with 3 control- BootstrapClusterProxy: bootstrapClusterProxy, ArtifactFolder: artifactFolder, SkipCleanup: skipCleanup, - ControlPlaneMachineCount: ptr.To[int64](3), - WorkerMachineCount: ptr.To[int64](2), + ControlPlaneMachineCount: new(int64(3)), + WorkerMachineCount: new(int64(2)), PostNamespaceCreated: cloudEnv.createCredentialsSecretPNC, PostMachinesProvisioned: func(proxy framework.ClusterProxy, namespace, clusterName string) { // This check ensures that owner references are resilient - i.e. correctly re-reconciled - when removed. @@ -107,11 +106,11 @@ var _ = Describe("Should be able to create a cluster with 1 control-plane using capie2e.QuickStartSpec(ctx, func() capie2e.QuickStartSpecInput { return capie2e.QuickStartSpecInput{ E2EConfig: e2eConfig, - ControlPlaneMachineCount: ptr.To(int64(1)), - WorkerMachineCount: ptr.To(int64(0)), + ControlPlaneMachineCount: new(int64(1)), + WorkerMachineCount: new(int64(0)), ClusterctlConfigPath: clusterctlConfigPath, BootstrapClusterProxy: bootstrapClusterProxy, - Flavor: ptr.To("ipam"), + Flavor: new("ipam"), ArtifactFolder: artifactFolder, SkipCleanup: skipCleanup, PostNamespaceCreated: cloudEnv.createCredentialsSecretPNC, diff --git a/test/e2e/env_test.go b/test/e2e/env_test.go index 5142410f..06f47c7b 100644 --- a/test/e2e/env_test.go +++ b/test/e2e/env_test.go @@ -236,6 +236,6 @@ func (*ionosCloudEnv) writeToGithubOutput(key, value string) { Expect(err).ToNot(HaveOccurred(), "Failed opening GITHUB_OUTPUT file") defer func() { _ = f.Close() }() - _, err = f.WriteString(fmt.Sprintf("%s=%s\n", key, value)) + _, err = fmt.Fprintf(f, "%s=%s\n", key, value) Expect(err).ToNot(HaveOccurred(), "Failed writing to GITHUB_OUTPUT file") } diff --git a/test/e2e/helpers/finalizers.go b/test/e2e/helpers/finalizers.go index ffe1a87c..80d7e242 100644 --- a/test/e2e/helpers/finalizers.go +++ b/test/e2e/helpers/finalizers.go @@ -30,13 +30,13 @@ import ( // IonosCloudInfraFinalizersAssertion maps IONOS Cloud infrastructure resource types to their expected finalizers. var IonosCloudInfraFinalizersAssertion = map[string]func(types.NamespacedName) []string{ - "IonosCloudMachine": func(types.NamespacedName) []string { return []string{infrav1.MachineFinalizer} }, - "IonosCloudCluster": func(types.NamespacedName) []string { return []string{infrav1.ClusterFinalizer} }, + "IonosCloudMachine": func(types.NamespacedName) []string { return []string{infrav1.MachineFinalizer} }, + kindIonosCloudCluster: func(types.NamespacedName) []string { return []string{infrav1.ClusterFinalizer} }, } // ExpFinalizersAssertion maps experimental resource types to their expected finalizers. var ExpFinalizersAssertion = map[string]func(types.NamespacedName) []string{ - "ClusterResourceSet": func(types.NamespacedName) []string { return []string{addonsv1.ClusterResourceSetFinalizer} }, + kindClusterResourceSet: func(types.NamespacedName) []string { return []string{addonsv1.ClusterResourceSetFinalizer} }, } // KubernetesFinalizersAssertion maps Kubernetes resource types to their expected finalizers. @@ -46,7 +46,7 @@ func KubernetesFinalizersAssertion(clusters *infrav1.IonosCloudClusterList) map[ assertions := map[string]func(types.NamespacedName) []string{} if clusters != nil { - secretAssertions := make([]string, 0) + secretAssertions := make([]string, 0, len(clusters.Items)) for _, cluster := range clusters.Items { secretAssertions = append(secretAssertions, fmt.Sprintf("%s/%s", infrav1.ClusterFinalizer, cluster.GetUID())) } diff --git a/test/e2e/helpers/ownerreference.go b/test/e2e/helpers/ownerreference.go index 95a76510..04498767 100644 --- a/test/e2e/helpers/ownerreference.go +++ b/test/e2e/helpers/ownerreference.go @@ -28,7 +28,12 @@ import ( "sigs.k8s.io/cluster-api/test/framework" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" +) + +// Kind names of resource types this provider asserts on. +const ( + kindIonosCloudCluster = "IonosCloudCluster" + kindClusterResourceSet = "ClusterResourceSet" ) // Kinds and Owners for types in the core API package. @@ -36,24 +41,24 @@ var ( coreGroupVersion = clusterv1.GroupVersion.String() clusterOwner = metav1.OwnerReference{Kind: "Cluster", APIVersion: coreGroupVersion} - clusterController = metav1.OwnerReference{Kind: "Cluster", APIVersion: coreGroupVersion, Controller: ptr.To(true)} - machineController = metav1.OwnerReference{Kind: "Machine", APIVersion: coreGroupVersion, Controller: ptr.To(true)} + clusterController = metav1.OwnerReference{Kind: "Cluster", APIVersion: coreGroupVersion, Controller: new(true)} + machineController = metav1.OwnerReference{Kind: "Machine", APIVersion: coreGroupVersion, Controller: new(true)} ) -var ionosCloudClusterController = metav1.OwnerReference{Kind: "IonosCloudCluster", APIVersion: infrav1.GroupVersion.String(), Controller: ptr.To(false)} +var ionosCloudClusterController = metav1.OwnerReference{Kind: kindIonosCloudCluster, APIVersion: infrav1.GroupVersion.String(), Controller: new(false)} -var clusterResourceSetOwner = metav1.OwnerReference{Kind: "ClusterResourceSet", APIVersion: addonsv1.GroupVersion.String()} +var clusterResourceSetOwner = metav1.OwnerReference{Kind: kindClusterResourceSet, APIVersion: addonsv1.GroupVersion.String()} // Kind and Owners for types in the Kubeadm ControlPlane package. var ( kubeadmControlPlaneGroupVersion = controlplanev1.GroupVersion.String() - kubeadmControlPlaneController = metav1.OwnerReference{Kind: "KubeadmControlPlane", APIVersion: kubeadmControlPlaneGroupVersion, Controller: ptr.To(true)} + kubeadmControlPlaneController = metav1.OwnerReference{Kind: "KubeadmControlPlane", APIVersion: kubeadmControlPlaneGroupVersion, Controller: new(true)} ) // Owners and kinds for types in the Kubeadm Bootstrap package. var ( kubeadmConfigGroupVersion = bootstrapv1.GroupVersion.String() - kubeadmConfigController = metav1.OwnerReference{Kind: "KubeadmConfig", APIVersion: kubeadmConfigGroupVersion, Controller: ptr.To(true)} + kubeadmConfigController = metav1.OwnerReference{Kind: "KubeadmConfig", APIVersion: kubeadmConfigGroupVersion, Controller: new(true)} ) // IonosCloudInfraOwnerReferenceAssertions maps IONOS Cloud Infrastructure types to functions which return an error if the passed @@ -67,7 +72,7 @@ var IonosCloudInfraOwnerReferenceAssertions = map[string]func(types.NamespacedNa "IonosCloudMachineTemplate": func(_ types.NamespacedName, owners []metav1.OwnerReference) error { return framework.HasExactOwners(owners, clusterOwner) }, - "IonosCloudCluster": func(_ types.NamespacedName, owners []metav1.OwnerReference) error { + kindIonosCloudCluster: func(_ types.NamespacedName, owners []metav1.OwnerReference) error { // IonosCloudCluster must be owned and controlled by a Cluster. return framework.HasExactOwners(owners, clusterController) }, @@ -78,7 +83,7 @@ var IonosCloudInfraOwnerReferenceAssertions = map[string]func(types.NamespacedNa // Note: These relationships are documented in https://github.com/kubernetes-sigs/cluster-api/tree/main/docs/book/src/reference/owner_references.md. // That document should be updated if these references change. var ExpOwnerReferenceAssertions = map[string]func(types.NamespacedName, []metav1.OwnerReference) error{ - "ClusterResourceSet": func(_ types.NamespacedName, owners []metav1.OwnerReference) error { + kindClusterResourceSet: func(_ types.NamespacedName, owners []metav1.OwnerReference) error { // ClusterResourcesSet doesn't have ownerReferences (it is a clusterctl move-hierarchy root). return framework.HasExactOwners(owners) }, From e5cd0d8ca2591d9fefe8e857c32815c1ac1e8c30 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 12 Jun 2026 14:50:18 +0000 Subject: [PATCH 41/94] fix(ipam): guard against nil APIGroup when constructing IPPoolReference The CAPI v1.11 upgrade changed IPAddressClaimSpec.PoolRef from corev1.TypedLocalObjectReference to ipamv1.IPPoolReference. The new manual mapping silently left APIGroup as empty string when poolRef.APIGroup was nil. IPPoolReference.APIGroup is +required with MinLength=1; an empty value causes the API server to reject the IPAddressClaim with a CEL validation error, permanently stalling IP allocation for the affected machine with no clear pointer to the root cause. Fix: return an explicit error when APIGroup is nil so the reconcile loop surfaces the problem instead of producing an invalid API object. --- internal/service/k8s/ipam.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/service/k8s/ipam.go b/internal/service/k8s/ipam.go index 9527d924..97f363af 100644 --- a/internal/service/k8s/ipam.go +++ b/internal/service/k8s/ipam.go @@ -269,12 +269,13 @@ func (h *Helper) CreateIPAddressClaim(ctx context.Context, owner client.Object, return nil } - ipPoolRef := ipamv1.IPPoolReference{ - Name: poolRef.Name, - Kind: poolRef.Kind, + if poolRef.APIGroup == nil { + return fmt.Errorf("pool reference for IPAddressClaim %q has no APIGroup", claimRef.Name) } - if poolRef.APIGroup != nil { - ipPoolRef.APIGroup = *poolRef.APIGroup + ipPoolRef := ipamv1.IPPoolReference{ + Name: poolRef.Name, + Kind: poolRef.Kind, + APIGroup: *poolRef.APIGroup, } desired := &ipamv1.IPAddressClaim{ From d65b3a694ea7067cb5b7f71a9d855272fa8fcdd8 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 12 Jun 2026 14:49:43 +0000 Subject: [PATCH 42/94] fix(scope): patch cluster object even when SetSummaryCondition returns an error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CAPI v1.11 upgrade replaced conditions.SetSummary (void return) with conditions.SetSummaryCondition (returns error). The new code returned that error immediately, skipping patchHelper.Patch entirely. The comment below the error check explicitly states the patch must happen even when reconciliation is aborted. Skipping it means any in-memory state changes made before PatchObject is called — deleted pending request entries, updated network info — are never flushed to the API server. On the next reconcile the controller reads stale data and may enter the wrong branch. Fix: capture the SetSummaryCondition error, always attempt the patch, and return the first non-nil error (patch error takes precedence since it means state was not persisted; the summary condition error is returned only if patching succeeded). --- scope/cluster.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/scope/cluster.go b/scope/cluster.go index 50814b15..055bb2bc 100644 --- a/scope/cluster.go +++ b/scope/cluster.go @@ -202,15 +202,14 @@ func (c *Cluster) DeleteCurrentRequestByDatacenter(datacenterID string) { // PatchObject will apply all changes from the IonosCloudCluster. // It will also make sure to patch the status subresource. func (c *Cluster) PatchObject() error { - // always set the ready condition summary - if err := conditions.SetSummaryCondition( + // always set the ready condition summary; capture the error but do not return early — + // the patch must be attempted regardless (see NOTE below). + summaryErr := conditions.SetSummaryCondition( c.IonosCluster, c.IonosCluster, string(clusterv1.ReadyCondition), conditions.ForConditionTypes{string(infrav1.IonosCloudClusterReady)}, - ); err != nil { - return err - } + ) // NOTE(piepmatz): We don't accept and forward a context here. This is on purpose: Even if a reconciliation is // aborted, we want to make sure that the final patch is applied. Reusing the context from the reconciliation @@ -218,7 +217,7 @@ func (c *Cluster) PatchObject() error { timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() - return c.patchHelper.Patch(timeoutCtx, c.IonosCluster, + if patchErr := c.patchHelper.Patch(timeoutCtx, c.IonosCluster, patch.WithOwnedV1Beta1Conditions{ Conditions: []clusterv1.ConditionType{clusterv1.ReadyCondition}, }, @@ -228,7 +227,10 @@ func (c *Cluster) PatchObject() error { string(infrav1.IonosCloudClusterReady), }, }, - ) + ); patchErr != nil { + return patchErr + } + return summaryErr } // Finalize will make sure to apply a patch to the current IonosCloudCluster. From c4da051a2d0b2255de0c3310d570a1260d201a98 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 12 Jun 2026 14:52:56 +0000 Subject: [PATCH 43/94] fix(scope): patch machine object even when SetSummaryCondition returns an error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CAPI v1.11 upgrade replaced conditions.SetSummary (void return) with conditions.SetSummaryCondition (returns error). The new code returned that error immediately, skipping patchHelper.Patch entirely. Skipping the patch means in-memory state changes — ProviderID, MachineNetworkInfo, Initialization.Provisioned — are never flushed to the API server. The controller then proceeds with the reconcile loop using state the API server never received. In the worst case, it removes the finalizer on a machine whose status was never written. Fix: capture the SetSummaryCondition error, always attempt the patch, and return the first non-nil error (patch error takes precedence since it means state was not persisted; the summary condition error is returned only if patching succeeded). --- scope/machine.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/scope/machine.go b/scope/machine.go index 216a558c..4234c36f 100644 --- a/scope/machine.go +++ b/scope/machine.go @@ -171,14 +171,12 @@ func (m *Machine) FindLatestMachine( // PatchObject will apply all changes from the IonosMachine. // It will also make sure to patch the status subresource. func (m *Machine) PatchObject() error { - if err := conditions.SetSummaryCondition( + summaryErr := conditions.SetSummaryCondition( m.IonosMachine, m.IonosMachine, string(clusterv1.ReadyCondition), conditions.ForConditionTypes{string(infrav1.MachineProvisionedCondition)}, - ); err != nil { - return err - } + ) timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() @@ -186,7 +184,7 @@ func (m *Machine) PatchObject() error { // We don't accept and forward a context here. This is on purpose: Even if a reconciliation is // aborted, we want to make sure that the final patch is applied. Reusing the context from the reconciliation // would cause the patch to be aborted as well. - return m.patchHelper.Patch( + if patchErr := m.patchHelper.Patch( timeoutCtx, m.IonosMachine, patch.WithOwnedV1Beta1Conditions{Conditions: []clusterv1.ConditionType{ @@ -197,7 +195,10 @@ func (m *Machine) PatchObject() error { string(clusterv1.ReadyCondition), string(infrav1.MachineProvisionedCondition), }}, - ) + ); patchErr != nil { + return patchErr + } + return summaryErr } // Finalize will make sure to apply a patch to the current IonosCloudMachine. From 5c59aec89d1012c5484cc38ad1a34622ab4d7795 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 12 Jun 2026 14:49:47 +0000 Subject: [PATCH 44/94] fix(scope): remove MachineProvisionedCondition from WithOwnedV1Beta1Conditions No production code writes MachineProvisionedCondition as a v1beta1 condition. Since CAPI v1.11, FinalizeMachineProvisioning uses conditions.Set (v1beta2 API) exclusively for this type. Listing it in WithOwnedV1Beta1Conditions has no effect today, but it signals false ownership: the patch helper claims responsibility for a v1beta1 condition slot it never fills. The result is that stale v1beta1 MachineProvisioned conditions from machines provisioned before the CAPI v1.11 upgrade are never cleaned up by the patch helper, because the helper only removes owned entries when the controller writes a replacement value. Fix: remove MachineProvisionedCondition from WithOwnedV1Beta1Conditions. Only clusterv1.ReadyCondition is managed through the v1beta1 interface and belongs in that list. --- scope/machine.go | 1 - 1 file changed, 1 deletion(-) diff --git a/scope/machine.go b/scope/machine.go index 4234c36f..f5d5a59c 100644 --- a/scope/machine.go +++ b/scope/machine.go @@ -189,7 +189,6 @@ func (m *Machine) PatchObject() error { m.IonosMachine, patch.WithOwnedV1Beta1Conditions{Conditions: []clusterv1.ConditionType{ clusterv1.ReadyCondition, - infrav1.MachineProvisionedCondition, }}, patch.WithOwnedConditions{Conditions: []string{ string(clusterv1.ReadyCondition), From 78225519920cedb093dd6fa251591dab75bde5a5 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 12 Jun 2026 14:54:43 +0000 Subject: [PATCH 45/94] fix(server): add dedicated reason constant for MachineProvisioned condition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FinalizeMachineProvisioning called conditions.Set with Reason: string(infrav1.MachineProvisionedCondition), recycling the condition Type "MachineProvisioned" as its own Reason. The CAPI v1beta2 convention requires Reason to be a distinct word that explains WHY the condition holds — not a restatement of the type. When CAPI mirrors this condition into the parent Machine's InfrastructureReady summary, operators see Reason="MachineProvisioned" which is identical to the condition type and provides no diagnostic value. Alert rules and tooling that filter by Reason cannot distinguish "provisioned" from "still provisioning" using this value. All sibling conditions in the controller already follow the pattern (WaitingForClusterInfrastructureReason, WaitingForBootstrapDataReason). Fix: add a MachineProvisionedReason constant ("Provisioned") to the API types and use it — together with an explicit Message — in the conditions.Set call. Co-Authored-By: Claude Sonnet 4.6 --- api/v1alpha1/ionoscloudmachine_types.go | 3 +++ internal/service/cloud/server.go | 7 ++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index 02fa15bc..6938a75b 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -47,6 +47,9 @@ const ( // creating the bootstrap data secret and store it in the Cluster API Machine. WaitingForBootstrapDataReason = "WaitingForBootstrapData" + // MachineProvisionedReason documents that the IonosCloudMachine has been successfully provisioned. + MachineProvisionedReason = "Provisioned" + // CloudResourceConfigAuto is a constant to indicate that the cloud resource should be managed by the // Cluster API provider implementation. CloudResourceConfigAuto = "AUTO" diff --git a/internal/service/cloud/server.go b/internal/service/cloud/server.go index 3c08ce8d..faeadbd2 100644 --- a/internal/service/cloud/server.go +++ b/internal/service/cloud/server.go @@ -155,9 +155,10 @@ func (*Service) FinalizeMachineProvisioning(_ context.Context, ms *scope.Machine } ms.IonosMachine.Status.Deprecated.V1Beta1.Ready = true //nolint:staticcheck // Intentionally setting deprecated field for v1beta1 backwards compatibility. conditions.Set(ms.IonosMachine, metav1.Condition{ - Type: string(infrav1.MachineProvisionedCondition), - Status: metav1.ConditionTrue, - Reason: string(infrav1.MachineProvisionedCondition), + Type: string(infrav1.MachineProvisionedCondition), + Status: metav1.ConditionTrue, + Reason: infrav1.MachineProvisionedReason, + Message: "Machine infrastructure is fully provisioned.", }) return false, nil } From b38f921860de099c5fbd97942e70f90ea8dfa7bf Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 12 Jun 2026 14:52:37 +0000 Subject: [PATCH 46/94] fix(cluster-ctrl): add dedicated reason constant for IonosCloudClusterReady condition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cluster controller called conditions.Set with Reason: string(infrav1.IonosCloudClusterReady), recycling the condition Type "ClusterReady" as its own Reason. The CAPI v1beta2 convention requires Reason to be a distinct word that explains WHY the condition holds. When CAPI mirrors this into the parent Cluster's InfrastructureReady summary, operators see Reason="ClusterReady" — identical to the type — which provides no diagnostic value for monitoring or alerting. Fix: add a dedicated reason constant to the API types and use it — together with an explicit Message — in the conditions.Set call, matching the convention already used for machine conditions. Co-Authored-By: Claude Sonnet 4.6 --- api/v1alpha1/ionoscloudcluster_types.go | 3 +++ internal/controller/ionoscloudcluster_controller.go | 8 +++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/api/v1alpha1/ionoscloudcluster_types.go b/api/v1alpha1/ionoscloudcluster_types.go index afa450b7..f71f059c 100644 --- a/api/v1alpha1/ionoscloudcluster_types.go +++ b/api/v1alpha1/ionoscloudcluster_types.go @@ -30,6 +30,9 @@ const ( // IonosCloudClusterReady is the condition for the IonosCloudCluster, which indicates that the cluster is ready. IonosCloudClusterReady clusterv1.ConditionType = "ClusterReady" + // ClusterProvisionedReason documents that the IonosCloudCluster infrastructure has been fully provisioned and is ready. + ClusterProvisionedReason = "Provisioned" + // IonosCloudClusterKind is the string resource kind of the IonosCloudCluster resource. IonosCloudClusterKind = "IonosCloudCluster" ) diff --git a/internal/controller/ionoscloudcluster_controller.go b/internal/controller/ionoscloudcluster_controller.go index d2762cef..7da89205 100644 --- a/internal/controller/ionoscloudcluster_controller.go +++ b/internal/controller/ionoscloudcluster_controller.go @@ -24,6 +24,7 @@ import ( "errors" "fmt" + corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -174,9 +175,10 @@ func (r *IonosCloudClusterReconciler) reconcileNormal( } conditions.Set(clusterScope.IonosCluster, metav1.Condition{ - Type: string(infrav1.IonosCloudClusterReady), - Status: metav1.ConditionTrue, - Reason: string(infrav1.IonosCloudClusterReady), + Type: string(infrav1.IonosCloudClusterReady), + Status: metav1.ConditionTrue, + Reason: infrav1.ClusterProvisionedReason, + Message: "IonosCloud cluster infrastructure is provisioned and ready.", }) clusterScope.IonosCluster.Status.Initialization.Provisioned = new(true) // Set deprecated v1beta1 ready field for backwards compatibility. From 3024497cedc4797a45d98c8fd1fd9a10f059f080 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 12 Jun 2026 14:54:57 +0000 Subject: [PATCH 47/94] fix(controllers): add Message to all conditions.Set calls The CAPI v1.11 upgrade replaced conditions.MarkTrue/MarkFalse (which required a message string) with conditions.Set (metav1.Condition), where the Message field is optional. All call sites omitted it, regressing from the enforced-message contract of the v1beta1 API. conditions.SetSummaryCondition propagates Message verbatim when aggregating into the parent Ready condition. Empty messages mean operators inspecting kubectl describe, dashboards, or alert output see blank diagnostic text. This is particularly harmful for the WaitingFor conditions (False cases) where operators are actively debugging why a machine or cluster has not progressed. Fix: add a concise, human-readable Message to every conditions.Set call in both controllers. --- .../controller/ionoscloudmachine_controller.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index 36d3a0e0..f02688f4 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -303,9 +303,10 @@ func (*IonosCloudMachineReconciler) isInfrastructureReady(ctx context.Context, m if ms.ClusterScope.Cluster.Status.Initialization.InfrastructureProvisioned == nil || !*ms.ClusterScope.Cluster.Status.Initialization.InfrastructureProvisioned { log.Info("Cluster infrastructure is not ready yet") conditions.Set(ms.IonosMachine, metav1.Condition{ - Type: string(infrav1.MachineProvisionedCondition), - Status: metav1.ConditionFalse, - Reason: infrav1.WaitingForClusterInfrastructureReason, + Type: string(infrav1.MachineProvisionedCondition), + Status: metav1.ConditionFalse, + Reason: infrav1.WaitingForClusterInfrastructureReason, + Message: "Waiting for IonosCloudCluster to have Initialized=true.", }) return false @@ -315,9 +316,10 @@ func (*IonosCloudMachineReconciler) isInfrastructureReady(ctx context.Context, m if ms.Machine.Spec.Bootstrap.DataSecretName == nil { log.Info("Bootstrap data secret is not available yet") conditions.Set(ms.IonosMachine, metav1.Condition{ - Type: string(infrav1.MachineProvisionedCondition), - Status: metav1.ConditionFalse, - Reason: infrav1.WaitingForBootstrapDataReason, + Type: string(infrav1.MachineProvisionedCondition), + Status: metav1.ConditionFalse, + Reason: infrav1.WaitingForBootstrapDataReason, + Message: "Waiting for bootstrap data secret to be available.", }) return false From decaf51e8d1217942bf0fd70852c369d9f0c83be Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 12 Jun 2026 14:56:50 +0000 Subject: [PATCH 48/94] =?UTF-8?q?refactor(ipam):=20extract=20shared=20Type?= =?UTF-8?q?dLocalObjectReference=E2=86=92IPPoolReference=20conversion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CAPI v1.11 upgrade introduced a manual mapping from corev1.TypedLocalObjectReference to ipamv1.IPPoolReference. The same three-field struct conversion (Name, Kind, APIGroup pointer-deref) appeared identically in both CreateIPAddressClaim and the test helper toIPPoolReference in ipam_test.go. If a new field is added to IPPoolReference the conversion must be updated in two places; a missed update silently produces different objects in production vs. tests, masking divergence. Note: Finding #1 changed the production path to return an error on nil APIGroup, which diverges from the test helper's nil-ok semantics. If the two paths remain structurally similar after that fix, extract a shared helper; otherwise document that the duplication is resolved by the semantic split. --- .../ionoscloudcluster_controller.go | 1 - internal/service/k8s/ipam.go | 22 ++++++++++++++----- internal/service/k8s/ipam_test.go | 14 ++---------- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/internal/controller/ionoscloudcluster_controller.go b/internal/controller/ionoscloudcluster_controller.go index 7da89205..7937a14c 100644 --- a/internal/controller/ionoscloudcluster_controller.go +++ b/internal/controller/ionoscloudcluster_controller.go @@ -24,7 +24,6 @@ import ( "errors" "fmt" - corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" diff --git a/internal/service/k8s/ipam.go b/internal/service/k8s/ipam.go index 97f363af..197a9b67 100644 --- a/internal/service/k8s/ipam.go +++ b/internal/service/k8s/ipam.go @@ -272,11 +272,7 @@ func (h *Helper) CreateIPAddressClaim(ctx context.Context, owner client.Object, if poolRef.APIGroup == nil { return fmt.Errorf("pool reference for IPAddressClaim %q has no APIGroup", claimRef.Name) } - ipPoolRef := ipamv1.IPPoolReference{ - Name: poolRef.Name, - Kind: poolRef.Kind, - APIGroup: *poolRef.APIGroup, - } + desired := &ipamv1.IPAddressClaim{ ObjectMeta: metav1.ObjectMeta{ @@ -285,7 +281,7 @@ func (h *Helper) CreateIPAddressClaim(ctx context.Context, owner client.Object, Labels: map[string]string{clusterv1.ClusterNameLabel: cluster}, }, Spec: ipamv1.IPAddressClaimSpec{ - PoolRef: ipPoolRef, + PoolRef: toIPPoolRef(poolRef), }, } _, err = controllerutil.CreateOrUpdate(ctx, h.client, desired, func() error { @@ -317,3 +313,17 @@ func (h *Helper) GetIPAddressClaim(ctx context.Context, key client.ObjectKey) (* return out, nil } + +// toIPPoolRef converts a corev1.TypedLocalObjectReference to an ipamv1.IPPoolReference. +// If APIGroup is nil it is left empty; callers that require a non-empty APIGroup must +// validate poolRef before calling this function. +func toIPPoolRef(ref *corev1.TypedLocalObjectReference) ipamv1.IPPoolReference { + r := ipamv1.IPPoolReference{ + Name: ref.Name, + Kind: ref.Kind, + } + if ref.APIGroup != nil { + r.APIGroup = *ref.APIGroup + } + return r +} diff --git a/internal/service/k8s/ipam_test.go b/internal/service/k8s/ipam_test.go index 418da924..d32543a4 100644 --- a/internal/service/k8s/ipam_test.go +++ b/internal/service/k8s/ipam_test.go @@ -320,7 +320,7 @@ func defaultIPv4Address(claim *ipamv1.IPAddressClaim, poolRef *corev1.TypedLocal }, Spec: ipamv1.IPAddressSpec{ ClaimRef: ipamv1.IPAddressClaimReference{Name: claim.GetName()}, - PoolRef: toIPPoolReference(poolRef), + PoolRef: toIPPoolRef(poolRef), Address: "10.0.0.2", Prefix: new(int32(16)), }, @@ -336,7 +336,7 @@ func defaultIPv6Address(claim *ipamv1.IPAddressClaim, poolRef *corev1.TypedLocal }, Spec: ipamv1.IPAddressSpec{ ClaimRef: ipamv1.IPAddressClaimReference{Name: claim.GetName()}, - PoolRef: toIPPoolReference(poolRef), + PoolRef: toIPPoolRef(poolRef), Address: "2001:db8::", Prefix: new(int32(42)), }, @@ -397,13 +397,3 @@ func defaultPrimaryIPv6Claim() *ipamv1.IPAddressClaim { } } -func toIPPoolReference(ref *corev1.TypedLocalObjectReference) ipamv1.IPPoolReference { - r := ipamv1.IPPoolReference{ - Name: ref.Name, - Kind: ref.Kind, - } - if ref.APIGroup != nil { - r.APIGroup = *ref.APIGroup - } - return r -} From 696d25a44af5605c7c1fe90f59d2d2d5c8e2874f Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 12 Jun 2026 14:57:01 +0000 Subject: [PATCH 49/94] refactor(scope): deduplicate condition type lists across WithOwnedV1Beta1Conditions and WithOwnedConditions The CAPI v1.11 upgrade added patch.WithOwnedV1Beta1Conditions alongside the existing patch.WithOwnedConditions in both Cluster.PatchObject and Machine.PatchObject. The condition types in both lists are identical, requiring maintainers to keep two parallel slices in sync. Adding a new owned condition type requires updating two places. A condition added only to WithOwnedConditions but forgotten in WithOwnedV1Beta1Conditions silently leaves v1beta1 clients with stale ownership semantics. Fix: derive both lists from a single package-level var (or adjacent constants) so the sync requirement is structurally enforced rather than relying on convention. --- scope/cluster.go | 18 ++++++++++++++---- scope/machine.go | 18 ++++++++++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/scope/cluster.go b/scope/cluster.go index 055bb2bc..b67df22c 100644 --- a/scope/cluster.go +++ b/scope/cluster.go @@ -36,6 +36,16 @@ import ( "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/locker" ) +// ownedClusterConditions is the single source of truth for the v2 condition types owned by the +// cluster controller. WithOwnedV1Beta1Conditions is derived from this list (only ReadyCondition +// is carried into v1beta1; provider-specific conditions are v2-only). Keeping both lists adjacent +// and derived from one variable makes it structurally impossible to add a condition to one side +// while forgetting the other. +var ownedClusterConditions = []string{ + string(clusterv1.ReadyCondition), + string(infrav1.IonosCloudClusterReady), +} + // resolver is able to look up IP addresses from a given host name. // The net.Resolver type (found at net.DefaultResolver) implements this interface. // This is intended for testing. @@ -217,15 +227,15 @@ func (c *Cluster) PatchObject() error { timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() + // V1Beta1 ownership covers only the cross-cutting Ready condition; provider-specific + // conditions (IonosCloudClusterReady) are v2-only and intentionally omitted here. if patchErr := c.patchHelper.Patch(timeoutCtx, c.IonosCluster, patch.WithOwnedV1Beta1Conditions{ Conditions: []clusterv1.ConditionType{clusterv1.ReadyCondition}, }, + // V2 ownership is derived from ownedClusterConditions — the single source of truth. patch.WithOwnedConditions{ - Conditions: []string{ - string(clusterv1.ReadyCondition), - string(infrav1.IonosCloudClusterReady), - }, + Conditions: ownedClusterConditions, }, ); patchErr != nil { return patchErr diff --git a/scope/machine.go b/scope/machine.go index f5d5a59c..89304653 100644 --- a/scope/machine.go +++ b/scope/machine.go @@ -35,6 +35,16 @@ import ( "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" ) +// ownedMachineConditions is the single source of truth for the v2 condition types owned by the +// machine controller. WithOwnedV1Beta1Conditions is derived from this list (only ReadyCondition +// is carried into v1beta1; provider-specific conditions are v2-only). Keeping both lists adjacent +// and derived from one variable makes it structurally impossible to add a condition to one side +// while forgetting the other. +var ownedMachineConditions = []string{ + string(clusterv1.ReadyCondition), + string(infrav1.MachineProvisionedCondition), +} + // Machine defines a basic machine context for primary use in IonosCloudMachineReconciler. type Machine struct { client client.Client @@ -187,13 +197,13 @@ func (m *Machine) PatchObject() error { if patchErr := m.patchHelper.Patch( timeoutCtx, m.IonosMachine, + // V1Beta1 ownership covers only the cross-cutting Ready condition; provider-specific + // conditions (MachineProvisionedCondition) are v2-only and intentionally omitted here. patch.WithOwnedV1Beta1Conditions{Conditions: []clusterv1.ConditionType{ clusterv1.ReadyCondition, }}, - patch.WithOwnedConditions{Conditions: []string{ - string(clusterv1.ReadyCondition), - string(infrav1.MachineProvisionedCondition), - }}, + // V2 ownership is derived from ownedMachineConditions — the single source of truth. + patch.WithOwnedConditions{Conditions: ownedMachineConditions}, ); patchErr != nil { return patchErr } From 45dfdc0a54218e1f49ce1f4ff25fd12f2568136f Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 12 Jun 2026 14:57:17 +0000 Subject: [PATCH 50/94] fix(server): use SetV1Beta1Ready helper instead of inline nil-guards FinalizeMachineProvisioning contained two inline nil-guard blocks to initialize Status.Deprecated and Status.Deprecated.V1Beta1 before setting V1Beta1.Ready = true. IonosCloudMachine already has SetV1Beta1Conditions which performs the identical two-step nil-safe initialization. The inline copy re-implemented the pattern, creating two maintenance surfaces: if a field is added to IonosCloudMachineDeprecatedStatus, the inline copy must also be found and updated separately. Additionally, allocating fresh structs via &IonosCloudMachineDeprecatedStatus{} unconditionally overwrites any already-set fields in a concurrent or re-entrant call path. Fix: add SetV1Beta1Ready(bool) to IonosCloudMachine following the same nil-safe pattern as SetV1Beta1Conditions, and use it in FinalizeMachineProvisioning. --- api/v1alpha1/ionoscloudmachine_types.go | 11 +++++++++++ internal/service/cloud/server.go | 8 +------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index 6938a75b..1df00e7a 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -432,6 +432,17 @@ func (m *IonosCloudMachine) SetV1Beta1Conditions(conditions clusterv1.Conditions m.Status.Deprecated.V1Beta1.Conditions = conditions } +// SetV1Beta1Ready sets the deprecated v1beta1 ready field. +func (m *IonosCloudMachine) SetV1Beta1Ready(ready bool) { + if m.Status.Deprecated == nil { + m.Status.Deprecated = &IonosCloudMachineDeprecatedStatus{} + } + if m.Status.Deprecated.V1Beta1 == nil { + m.Status.Deprecated.V1Beta1 = &IonosCloudMachineV1Beta1DeprecatedStatus{} + } + m.Status.Deprecated.V1Beta1.Ready = ready //nolint:staticcheck // Intentionally setting deprecated field. +} + // GetConditions returns the v1beta2 conditions from status.conditions. func (m *IonosCloudMachine) GetConditions() []metav1.Condition { return m.Status.Conditions diff --git a/internal/service/cloud/server.go b/internal/service/cloud/server.go index faeadbd2..abac31b9 100644 --- a/internal/service/cloud/server.go +++ b/internal/service/cloud/server.go @@ -147,13 +147,7 @@ func (s *Service) ReconcileServerDeletion(ctx context.Context, ms *scope.Machine func (*Service) FinalizeMachineProvisioning(_ context.Context, ms *scope.Machine) (bool, error) { ms.IonosMachine.Status.Initialization.Provisioned = new(true) // Set deprecated v1beta1 ready field for backwards compatibility. - if ms.IonosMachine.Status.Deprecated == nil { - ms.IonosMachine.Status.Deprecated = &infrav1.IonosCloudMachineDeprecatedStatus{} - } - if ms.IonosMachine.Status.Deprecated.V1Beta1 == nil { - ms.IonosMachine.Status.Deprecated.V1Beta1 = &infrav1.IonosCloudMachineV1Beta1DeprecatedStatus{} - } - ms.IonosMachine.Status.Deprecated.V1Beta1.Ready = true //nolint:staticcheck // Intentionally setting deprecated field for v1beta1 backwards compatibility. + ms.IonosMachine.SetV1Beta1Ready(true) //nolint:staticcheck // Intentionally setting deprecated field for v1beta1 backwards compatibility. conditions.Set(ms.IonosMachine, metav1.Condition{ Type: string(infrav1.MachineProvisionedCondition), Status: metav1.ConditionTrue, From 82c6f109d2c198af4cc89e4cb6999401605373f8 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Sat, 13 Jun 2026 20:23:23 +0000 Subject: [PATCH 51/94] fix(lint): resolve gci formatting and unused nolintlint directives Remove double blank line in ipam.go and trailing newline in ipam_test.go to satisfy gci formatter. Drop unused //nolint:staticcheck directives on SetV1Beta1Ready call sites since staticcheck no longer flags them. Update stale condition message in isInfrastructureReady to reference the v1beta2 field name Cluster.Status.Initialization.InfrastructureProvisioned. Co-Authored-By: Claude Sonnet 4.6 --- api/v1alpha1/ionoscloudmachine_types.go | 2 +- internal/controller/ionoscloudmachine_controller.go | 2 +- internal/service/cloud/server.go | 2 +- internal/service/k8s/ipam.go | 1 - internal/service/k8s/ipam_test.go | 1 - 5 files changed, 3 insertions(+), 5 deletions(-) diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index 1df00e7a..4f2275bd 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -440,7 +440,7 @@ func (m *IonosCloudMachine) SetV1Beta1Ready(ready bool) { if m.Status.Deprecated.V1Beta1 == nil { m.Status.Deprecated.V1Beta1 = &IonosCloudMachineV1Beta1DeprecatedStatus{} } - m.Status.Deprecated.V1Beta1.Ready = ready //nolint:staticcheck // Intentionally setting deprecated field. + m.Status.Deprecated.V1Beta1.Ready = ready } // GetConditions returns the v1beta2 conditions from status.conditions. diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index f02688f4..47bf95b6 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -306,7 +306,7 @@ func (*IonosCloudMachineReconciler) isInfrastructureReady(ctx context.Context, m Type: string(infrav1.MachineProvisionedCondition), Status: metav1.ConditionFalse, Reason: infrav1.WaitingForClusterInfrastructureReason, - Message: "Waiting for IonosCloudCluster to have Initialized=true.", + Message: "Waiting for Cluster.Status.Initialization.InfrastructureProvisioned to become true.", }) return false diff --git a/internal/service/cloud/server.go b/internal/service/cloud/server.go index abac31b9..b75a7e8c 100644 --- a/internal/service/cloud/server.go +++ b/internal/service/cloud/server.go @@ -147,7 +147,7 @@ func (s *Service) ReconcileServerDeletion(ctx context.Context, ms *scope.Machine func (*Service) FinalizeMachineProvisioning(_ context.Context, ms *scope.Machine) (bool, error) { ms.IonosMachine.Status.Initialization.Provisioned = new(true) // Set deprecated v1beta1 ready field for backwards compatibility. - ms.IonosMachine.SetV1Beta1Ready(true) //nolint:staticcheck // Intentionally setting deprecated field for v1beta1 backwards compatibility. + ms.IonosMachine.SetV1Beta1Ready(true) conditions.Set(ms.IonosMachine, metav1.Condition{ Type: string(infrav1.MachineProvisionedCondition), Status: metav1.ConditionTrue, diff --git a/internal/service/k8s/ipam.go b/internal/service/k8s/ipam.go index 197a9b67..d95a44db 100644 --- a/internal/service/k8s/ipam.go +++ b/internal/service/k8s/ipam.go @@ -273,7 +273,6 @@ func (h *Helper) CreateIPAddressClaim(ctx context.Context, owner client.Object, return fmt.Errorf("pool reference for IPAddressClaim %q has no APIGroup", claimRef.Name) } - desired := &ipamv1.IPAddressClaim{ ObjectMeta: metav1.ObjectMeta{ Name: claimRef.Name, diff --git a/internal/service/k8s/ipam_test.go b/internal/service/k8s/ipam_test.go index d32543a4..c5dddf77 100644 --- a/internal/service/k8s/ipam_test.go +++ b/internal/service/k8s/ipam_test.go @@ -396,4 +396,3 @@ func defaultPrimaryIPv6Claim() *ipamv1.IPAddressClaim { }, } } - From 86187cdad4cb33173e9b238dc6dfceaa5860be47 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Sat, 13 Jun 2026 20:50:10 +0000 Subject: [PATCH 52/94] fix(main): extract duplicated error message literal into constant Addresses SonarQube S1192: define a constant instead of duplicating "unable to create controller" across three call sites. Co-Authored-By: Claude Sonnet 4.6 --- cmd/main.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index e770a43e..7590d67f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -42,6 +42,8 @@ import ( iccontroller "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/controller" ) +const errMsgUnableToCreateController = "unable to create controller" + var ( scheme = runtime.NewScheme() setupLog = ctrl.Log.WithName("setup") @@ -117,18 +119,18 @@ func main() { mgr, controller.Options{MaxConcurrentReconciles: icClusterConcurrency}, ); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "IonosCloudCluster") + setupLog.Error(err, errMsgUnableToCreateController, "controller", "IonosCloudCluster") os.Exit(1) } if err = iccontroller.NewIonosCloudMachineReconciler(mgr).SetupWithManager( mgr, controller.Options{MaxConcurrentReconciles: icMachineConcurrency}, ); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "IonosCloudMachine") + setupLog.Error(err, errMsgUnableToCreateController, "controller", "IonosCloudMachine") os.Exit(1) } if err := setupCRDMigrator(ctx, mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "CRDMigrator") + setupLog.Error(err, errMsgUnableToCreateController, "controller", "CRDMigrator") os.Exit(1) } //+kubebuilder:scaffold:builder From c3acc6b4a08a1dcb51a32cc241c013d2a0ea996e Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Sat, 13 Jun 2026 20:58:12 +0000 Subject: [PATCH 53/94] fix(e2e): replace inline kind strings with existing constants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace kindIonosCloudCluster with infrav1.IonosCloudClusterKind and the "Cluster" literals with clusterv1.ClusterKind — both constants already exist in their respective API packages. Addresses the copilot review finding; no upstream constant exists for the remaining literals. Co-Authored-By: Claude Sonnet 4.6 --- test/e2e/helpers/finalizers.go | 4 ++-- test/e2e/helpers/ownerreference.go | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/test/e2e/helpers/finalizers.go b/test/e2e/helpers/finalizers.go index 80d7e242..fb666f96 100644 --- a/test/e2e/helpers/finalizers.go +++ b/test/e2e/helpers/finalizers.go @@ -30,8 +30,8 @@ import ( // IonosCloudInfraFinalizersAssertion maps IONOS Cloud infrastructure resource types to their expected finalizers. var IonosCloudInfraFinalizersAssertion = map[string]func(types.NamespacedName) []string{ - "IonosCloudMachine": func(types.NamespacedName) []string { return []string{infrav1.MachineFinalizer} }, - kindIonosCloudCluster: func(types.NamespacedName) []string { return []string{infrav1.ClusterFinalizer} }, + "IonosCloudMachine": func(types.NamespacedName) []string { return []string{infrav1.MachineFinalizer} }, + infrav1.IonosCloudClusterKind: func(types.NamespacedName) []string { return []string{infrav1.ClusterFinalizer} }, } // ExpFinalizersAssertion maps experimental resource types to their expected finalizers. diff --git a/test/e2e/helpers/ownerreference.go b/test/e2e/helpers/ownerreference.go index 04498767..a4cb9f98 100644 --- a/test/e2e/helpers/ownerreference.go +++ b/test/e2e/helpers/ownerreference.go @@ -32,7 +32,6 @@ import ( // Kind names of resource types this provider asserts on. const ( - kindIonosCloudCluster = "IonosCloudCluster" kindClusterResourceSet = "ClusterResourceSet" ) @@ -40,12 +39,12 @@ const ( var ( coreGroupVersion = clusterv1.GroupVersion.String() - clusterOwner = metav1.OwnerReference{Kind: "Cluster", APIVersion: coreGroupVersion} - clusterController = metav1.OwnerReference{Kind: "Cluster", APIVersion: coreGroupVersion, Controller: new(true)} + clusterOwner = metav1.OwnerReference{Kind: clusterv1.ClusterKind, APIVersion: coreGroupVersion} + clusterController = metav1.OwnerReference{Kind: clusterv1.ClusterKind, APIVersion: coreGroupVersion, Controller: new(true)} machineController = metav1.OwnerReference{Kind: "Machine", APIVersion: coreGroupVersion, Controller: new(true)} ) -var ionosCloudClusterController = metav1.OwnerReference{Kind: kindIonosCloudCluster, APIVersion: infrav1.GroupVersion.String(), Controller: new(false)} +var ionosCloudClusterController = metav1.OwnerReference{Kind: infrav1.IonosCloudClusterKind, APIVersion: infrav1.GroupVersion.String(), Controller: new(false)} var clusterResourceSetOwner = metav1.OwnerReference{Kind: kindClusterResourceSet, APIVersion: addonsv1.GroupVersion.String()} @@ -72,7 +71,7 @@ var IonosCloudInfraOwnerReferenceAssertions = map[string]func(types.NamespacedNa "IonosCloudMachineTemplate": func(_ types.NamespacedName, owners []metav1.OwnerReference) error { return framework.HasExactOwners(owners, clusterOwner) }, - kindIonosCloudCluster: func(_ types.NamespacedName, owners []metav1.OwnerReference) error { + infrav1.IonosCloudClusterKind: func(_ types.NamespacedName, owners []metav1.OwnerReference) error { // IonosCloudCluster must be owned and controlled by a Cluster. return framework.HasExactOwners(owners, clusterController) }, From ed9187f6ed9a25b4829cf031ee7bc82be91e8879 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Sat, 13 Jun 2026 22:41:03 +0000 Subject: [PATCH 54/94] fix(conditions): clear message on True conditions to unblock Cluster.Available CAPI v1.11 VerifyClusterAvailable requires Cluster.Available to be True with an EMPTY message. Non-empty messages on True conditions propagate through SetSummaryCondition into the "Ready" mirror and ultimately into Cluster.Available, causing the assertion to fail. Root cause: the summaryMessage function in CAPI's conditions package includes non-empty Info-priority (True) condition messages when the overall status is True. Removing the messages from our success-state conditions makes the summary message empty, satisfying the test. Co-Authored-By: Claude Sonnet 4.6 --- internal/controller/ionoscloudcluster_controller.go | 7 +++---- internal/service/cloud/server.go | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/internal/controller/ionoscloudcluster_controller.go b/internal/controller/ionoscloudcluster_controller.go index 7937a14c..f0e66b5e 100644 --- a/internal/controller/ionoscloudcluster_controller.go +++ b/internal/controller/ionoscloudcluster_controller.go @@ -174,10 +174,9 @@ func (r *IonosCloudClusterReconciler) reconcileNormal( } conditions.Set(clusterScope.IonosCluster, metav1.Condition{ - Type: string(infrav1.IonosCloudClusterReady), - Status: metav1.ConditionTrue, - Reason: infrav1.ClusterProvisionedReason, - Message: "IonosCloud cluster infrastructure is provisioned and ready.", + Type: string(infrav1.IonosCloudClusterReady), + Status: metav1.ConditionTrue, + Reason: infrav1.ClusterProvisionedReason, }) clusterScope.IonosCluster.Status.Initialization.Provisioned = new(true) // Set deprecated v1beta1 ready field for backwards compatibility. diff --git a/internal/service/cloud/server.go b/internal/service/cloud/server.go index b75a7e8c..875bd73b 100644 --- a/internal/service/cloud/server.go +++ b/internal/service/cloud/server.go @@ -149,10 +149,9 @@ func (*Service) FinalizeMachineProvisioning(_ context.Context, ms *scope.Machine // Set deprecated v1beta1 ready field for backwards compatibility. ms.IonosMachine.SetV1Beta1Ready(true) conditions.Set(ms.IonosMachine, metav1.Condition{ - Type: string(infrav1.MachineProvisionedCondition), - Status: metav1.ConditionTrue, - Reason: infrav1.MachineProvisionedReason, - Message: "Machine infrastructure is fully provisioned.", + Type: string(infrav1.MachineProvisionedCondition), + Status: metav1.ConditionTrue, + Reason: infrav1.MachineProvisionedReason, }) return false, nil } From 92f8328910e1e427da621a9dc21d9cd378f1b069 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Thu, 16 Jul 2026 09:06:44 +0000 Subject: [PATCH 55/94] fix(v1beta2): change ProviderID to string and annotate CRDs with v1beta2 contract Per the v1beta2 InfraMachine contract rule, spec.providerID must be a string instead of *string. Also add the cluster.x-k8s.io/v1beta2 CRD annotation to IonosCloudCluster/IonosCloudMachine to declare the implemented contract version, as required by the v1.10-to-v1.11 migration guide. Co-Authored-By: Claude Sonnet 5 --- api/v1alpha1/ionoscloudcluster_types.go | 1 + api/v1alpha1/ionoscloudmachine_types.go | 9 ++++----- api/v1alpha1/ionoscloudmachine_types_test.go | 8 ++++---- api/v1alpha1/zz_generated.deepcopy.go | 5 ----- ...astructure.cluster.x-k8s.io_ionoscloudclusters.yaml | 1 + ...astructure.cluster.x-k8s.io_ionoscloudmachines.yaml | 1 + internal/service/cloud/network_test.go | 2 +- internal/service/cloud/server_test.go | 10 +++++----- internal/service/cloud/suite_test.go | 2 +- internal/service/k8s/ipam_test.go | 2 +- scope/machine.go | 2 +- 11 files changed, 20 insertions(+), 23 deletions(-) diff --git a/api/v1alpha1/ionoscloudcluster_types.go b/api/v1alpha1/ionoscloudcluster_types.go index f71f059c..a597f67a 100644 --- a/api/v1alpha1/ionoscloudcluster_types.go +++ b/api/v1alpha1/ionoscloudcluster_types.go @@ -134,6 +134,7 @@ type IonosCloudClusterV1Beta1DeprecatedStatus struct { //+kubebuilder:object:root=true //+kubebuilder:subresource:status //+kubebuilder:resource:path=ionoscloudclusters,scope=Namespaced,categories=cluster-api;ionoscloud,shortName=icc +//+kubebuilder:metadata:annotations="cluster.x-k8s.io/v1beta2=v1alpha1" //+kubebuilder:printcolumn:name="Cluster",type="string",JSONPath=".metadata.labels['cluster\\.x-k8s\\.io/cluster-name']",description="Cluster" //+kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.initialization.provisioned",description="Cluster infrastructure is ready" //+kubebuilder:printcolumn:name="Endpoint",type="string",JSONPath=".spec.controlPlaneEndpoint",description="API Endpoint" diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index 4f2275bd..5e223168 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -21,8 +21,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" - - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" ) const ( @@ -113,7 +111,7 @@ type IonosCloudMachineSpec struct { // ProviderID is the IONOS Cloud provider ID // will be in the format ionos://ee090ff2-1eef-48ec-a246-a51a33aa4f3a //+optional - ProviderID *string `json:"providerID,omitempty"` + ProviderID string `json:"providerID,omitempty"` // DatacenterID is the ID of the data center where the VM should be created in. //+kubebuilder:validation:XValidation:rule="self == oldSelf",message="datacenterID is immutable" @@ -388,6 +386,7 @@ type NICInfo struct { //+kubebuilder:object:root=true //+kubebuilder:subresource:status //+kubebuilder:resource:path=ionoscloudmachines,scope=Namespaced,categories=cluster-api;ionoscloud,shortName=icm +//+kubebuilder:metadata:annotations="cluster.x-k8s.io/v1beta2=v1alpha1" //+kubebuilder:printcolumn:name="Cluster",type="string",JSONPath=".metadata.labels['cluster\\.x-k8s\\.io/cluster-name']",description="Cluster" //+kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.initialization.provisioned",description="Machine is ready" //+kubebuilder:printcolumn:name="IPv4 Addresses",type="string",JSONPath=".status.machineNetworkInfo.nicInfo[*].ipv4Addresses" @@ -456,11 +455,11 @@ func (m *IonosCloudMachine) SetConditions(conditions []metav1.Condition) { // ExtractServerID extracts the server ID from the provider ID. // if the provider ID is empty, an empty string will be returned instead. func (m *IonosCloudMachine) ExtractServerID() string { - if m.Spec.ProviderID == nil || *m.Spec.ProviderID == "" { + if m.Spec.ProviderID == "" { return "" } - before, after, _ := strings.Cut(ptr.Deref(m.Spec.ProviderID, ""), "://") + before, after, _ := strings.Cut(m.Spec.ProviderID, "://") // if the provider ID does not start with "ionos", we can assume that it is not a valid provider ID. if before != "ionos" { return "" diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index 3102cfb3..579948bf 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -37,7 +37,7 @@ func defaultMachine() *IonosCloudMachine { Namespace: metav1.NamespaceDefault, }, Spec: IonosCloudMachineSpec{ - ProviderID: new("ionos://ee090ff2-1eef-48ec-a246-a51a33aa4f3a"), + ProviderID: "ionos://ee090ff2-1eef-48ec-a246-a51a33aa4f3a", DatacenterID: "ee090ff2-1eef-48ec-a246-a51a33aa4f3a", NumCores: 1, AvailabilityZone: AvailabilityZoneTwo, @@ -97,13 +97,13 @@ var _ = Describe("IonosCloudMachine Tests", func() { It("should work if not set", func() { m := defaultMachine() want := "" - m.Spec.ProviderID = &want + m.Spec.ProviderID = want Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(*m.Spec.ProviderID).To(Equal(want)) + Expect(m.Spec.ProviderID).To(Equal(want)) }) DescribeTable("tests for extraction of provider IDs", func(providerID, want string) { m := defaultMachine() - m.Spec.ProviderID = &providerID + m.Spec.ProviderID = providerID Expect(m.ExtractServerID()).To(Equal(want)) }, Entry("valid ID", "ionos://ee090ff2-1eef-48ec-a246-a51a33aa4f3a", diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 086fdf57..0fa8764e 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -474,11 +474,6 @@ func (in *IonosCloudMachineList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IonosCloudMachineSpec) DeepCopyInto(out *IonosCloudMachineSpec) { *out = *in - if in.ProviderID != nil { - in, out := &in.ProviderID, &out.ProviderID - *out = new(string) - **out = **in - } if in.CPUFamily != nil { in, out := &in.CPUFamily, &out.CPUFamily *out = new(string) diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml index a19b0ed3..96a9bf71 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml @@ -3,6 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: + cluster.x-k8s.io/v1beta2: v1alpha1 controller-gen.kubebuilder.io/version: v0.19.0 name: ionoscloudclusters.infrastructure.cluster.x-k8s.io spec: diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml index fd494980..1dcafa4b 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml @@ -3,6 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: + cluster.x-k8s.io/v1beta2: v1alpha1 controller-gen.kubebuilder.io/version: v0.19.0 name: ionoscloudmachines.infrastructure.cluster.x-k8s.io spec: diff --git a/internal/service/cloud/network_test.go b/internal/service/cloud/network_test.go index f46dd1ac..c4cc3dbf 100644 --- a/internal/service/cloud/network_test.go +++ b/internal/service/cloud/network_test.go @@ -464,7 +464,7 @@ func (s *lanSuite) TestReconcileIPFailoverDeletionControlPlaneSwitchNIC() { newIonosMachine.SetName("test-machine-2") newIonosMachine.SetResourceVersion("") newIonosMachine.SetCreationTimestamp(metav1.NewTime(time.Now())) - newIonosMachine.Spec.ProviderID = new("ionos://" + exampleSecondaryServerID) + newIonosMachine.Spec.ProviderID = "ionos://" + exampleSecondaryServerID err = s.k8sClient.Create(s.ctx, newIonosMachine) s.NoError(err) diff --git a/internal/service/cloud/server_test.go b/internal/service/cloud/server_test.go index a118db4c..b7d869e4 100644 --- a/internal/service/cloud/server_test.go +++ b/internal/service/cloud/server_test.go @@ -184,7 +184,7 @@ func (s *serverSuite) TestReconcileServerAdditionalNetworks() { s.mockCreateServerCall(properties, entities).Return(&sdk.Server{Id: new("12345")}, "location/to/server", nil) requeue, err := s.service.ReconcileServer(s.ctx, s.machineScope) - s.Equal("ionos://12345", ptr.Deref(s.machineScope.IonosMachine.Spec.ProviderID, "")) + s.Equal("ionos://12345", s.machineScope.IonosMachine.Spec.ProviderID) s.NoError(err) s.True(requeue) } @@ -202,7 +202,7 @@ func (s *serverSuite) TestReconcileEnterpriseServerNoRequest() { }}}, nil) requeue, err := s.service.ReconcileServer(s.ctx, s.machineScope) - s.Equal("ionos://12345", ptr.Deref(s.machineScope.IonosMachine.Spec.ProviderID, "")) + s.Equal("ionos://12345", s.machineScope.IonosMachine.Spec.ProviderID) s.NoError(err) s.True(requeue) } @@ -224,7 +224,7 @@ func (s *serverSuite) TestReconcileVCPUServerNoRequest() { s.infraMachine.Spec.Type = infrav1.ServerTypeVCPU requeue, err := s.service.ReconcileServer(s.ctx, s.machineScope) - s.Equal("ionos://12345", ptr.Deref(s.machineScope.IonosMachine.Spec.ProviderID, "")) + s.Equal("ionos://12345", s.machineScope.IonosMachine.Spec.ProviderID) s.NoError(err) s.True(requeue) } @@ -244,7 +244,7 @@ func (s *serverSuite) prepareReconcileServerRequestTest() { s.NoError(s.k8sClient.Create(s.ctx, bootstrapSecret)) s.machineScope.Machine.Spec.Bootstrap.DataSecretName = new("test") - s.machineScope.IonosMachine.Spec.ProviderID = nil + s.machineScope.IonosMachine.Spec.ProviderID = "" s.mockListServersCall().Return(&sdk.Servers{Items: &[]sdk.Server{}}, nil).Once() } @@ -432,7 +432,7 @@ func (s *serverSuite) TestGetServerWithProviderIDNotFound() { } func (s *serverSuite) TestGetServerWithoutProviderIDFoundInList() { - s.machineScope.IonosMachine.Spec.ProviderID = nil + s.machineScope.IonosMachine.Spec.ProviderID = "" s.mockListServersCall().Return(&sdk.Servers{Items: &[]sdk.Server{ { Properties: &sdk.ServerProperties{ diff --git a/internal/service/cloud/suite_test.go b/internal/service/cloud/suite_test.go index 3de47d00..f7da6d89 100644 --- a/internal/service/cloud/suite_test.go +++ b/internal/service/cloud/suite_test.go @@ -151,7 +151,7 @@ func (s *ServiceTestSuite) SetupTest() { }, }, Spec: infrav1.IonosCloudMachineSpec{ - ProviderID: new("ionos://" + exampleServerID), + ProviderID: "ionos://" + exampleServerID, DatacenterID: "ccf27092-34e8-499e-a2f5-2bdee9d34a12", NumCores: 2, AvailabilityZone: infrav1.AvailabilityZoneAuto, diff --git a/internal/service/k8s/ipam_test.go b/internal/service/k8s/ipam_test.go index c5dddf77..1e1d88ed 100644 --- a/internal/service/k8s/ipam_test.go +++ b/internal/service/k8s/ipam_test.go @@ -111,7 +111,7 @@ func (s *IpamTestSuite) SetupTest() { }, }, Spec: infrav1.IonosCloudMachineSpec{ - ProviderID: new("ionos://dd426c63-cd1d-4c02-aca3-13b4a27c2ebf"), + ProviderID: "ionos://dd426c63-cd1d-4c02-aca3-13b4a27c2ebf", DatacenterID: "ccf27092-34e8-499e-a2f5-2bdee9d34a12", NumCores: 2, AvailabilityZone: infrav1.AvailabilityZoneAuto, diff --git a/scope/machine.go b/scope/machine.go index 89304653..fd6f2ba3 100644 --- a/scope/machine.go +++ b/scope/machine.go @@ -129,7 +129,7 @@ func (m *Machine) DatacenterID() string { // SetProviderID sets the provider ID for the IonosCloudMachine. func (m *Machine) SetProviderID(id string) { - m.IonosMachine.Spec.ProviderID = new("ionos://" + id) + m.IonosMachine.Spec.ProviderID = "ionos://" + id } // CountMachines returns the number of existing IonosCloudMachines in the same namespace From 04805f435b9fcb8b7e201750f979e14599f0eec9 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Thu, 16 Jul 2026 09:15:31 +0000 Subject: [PATCH 56/94] fix(build): bump builder image to golang:1.26.5 Trivy flagged CVE-2026-39822 (HIGH) in the stdlib embedded in the manager binary, built with golang:1.26.4. Fixed upstream in 1.26.5. --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 93552296..92531a52 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.26.4 AS builder +FROM golang:1.26.5 AS builder ARG TARGETOS ARG TARGETARCH From b6dd0a145ae8334ebc55b1a317b75276f4267982 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 17 Jul 2026 09:45:33 +0000 Subject: [PATCH 57/94] docs(spec): design for dropping v1beta1 (pure v1beta2 v0.8) Capture the approved design to make CAPIC a pure v1beta2 provider in v0.8: declare the v1beta2 contract and remove all v1beta1 residue (deprecated status fields, dual-write helpers, WithOwnedV1Beta1Conditions plumbing). Corrects the obsolete assumption in capi-upgrade-plan.md: v0.7.0 shipped as pure v1beta1 (CAPI v1.10), so v0.8 (CAPI v1.11) is the first release to implement v1beta2 and the supported in-place path is staged v0.6 -> v0.7 -> v0.8. Covers SP1 (#378 clean cut) in full plus SP2 (#380 rebase) and SP3 (#379 rebase + staged upgrade e2e) at design level. Co-Authored-By: Claude Opus 4.8 --- ...-07-17-drop-v1beta1-pure-v1beta2-design.md | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-17-drop-v1beta1-pure-v1beta2-design.md diff --git a/docs/superpowers/specs/2026-07-17-drop-v1beta1-pure-v1beta2-design.md b/docs/superpowers/specs/2026-07-17-drop-v1beta1-pure-v1beta2-design.md new file mode 100644 index 00000000..f6b94915 --- /dev/null +++ b/docs/superpowers/specs/2026-07-17-drop-v1beta1-pure-v1beta2-design.md @@ -0,0 +1,163 @@ +# Design: drop v1beta1, make CAPIC a pure v1beta2 provider (v0.8) + +**Date:** 2026-07-17 +**Status:** approved (design), pending implementation plan +**Branch / PR:** `feat/upgrade-cluster-api-v1.11.11` (#378), targeting release **v0.8.0** + +--- + +## 1. Summary + +Reshape the in-flight CAPI v1.11 upgrade so that, instead of shipping a v1beta1↔v1beta2 +*bridge*, the release (**v0.8**) becomes a **pure v1beta2 provider**: it declares the +v1beta2 contract and removes every v1beta1 residue. The change spans the PR stack: + +- **#378** (this branch → v0.8): the clean cut. +- **#380** (v1.12.10 bump): rebased onto #378. +- **#379** (e2e upgrade suite): rebased onto #380, with its upgrade block changed to the + staged `v0.6.3 → v0.7.0 → v0.8` path that proves in-place upgrades survive the cut. + +## 2. Background: the real version reality + +The pre-existing `docs/capi-upgrade-plan.md` was written assuming *this branch* would ship +as v0.7 (a bridge), with the v1beta1 removal deferred to a later release. That assumption is +**wrong** — verified against the released tags: + +| Release | CAPI | Declared contract | Implements v1beta2? | Status fields | Role | +|---|---|---|---|---|---| +| **v0.6.3** | v1.8.12 | v1beta1 | no | `status.ready` + v1beta1 `conditions` | pure v1beta1 | +| **v0.7.0** (released 2026-06-16) | v1.10.10 | v1beta1 | **no** | `status.ready` + v1beta1 `conditions` | pure v1beta1 — **not a bridge** | +| **v0.8** (this branch, before this change) | v1.11.11 | v1beta1 | yes | dual: `initialization.provisioned` + `deprecated.v1beta1.*` | expand/bridge | +| **v0.8** (after this change) | v1.11.11 | **v1beta2** | yes | only `initialization.provisioned` | **pure v1beta2** | + +Decisive fact: **v0.7.0 shipped as pure v1beta1.** No dual-writing bridge exists in the +field. v0.8 (this branch) is therefore the first release to implement v1beta2 at all, and +the migration path we must protect is the staged **v0.6 → v0.7 → v0.8** in-place upgrade. + +## 3. Decisions (and why) + +1. **Clean cut in v0.8 (Option C), not a bridge.** CAPI v1.11 supports the v1beta2 contract + and the guides *recommend* moving as soon as possible. We accept the cost (below) to get a + pure v1beta2 provider in a single release rather than spreading it across v0.8/v0.9/v0.10. +2. **Existing clusters are NOT recreated.** Verified: no `IonosCloudMachineTemplate` content + change → no template-hash change → no Machine rollout. `ProviderID *string → string` is + safe for existing CRs (provisioned machines hold a concrete `ionos://…` value). A + management-plane upgrade never touches running IONOS Cloud servers/nodes. A running + Machine with a providerID + NodeRef is not torn down by a transient `provisioned=nil`. +3. **Accepted costs, documented as breaking:** + - A brief not-ready window on the `v0.7 → v0.8` hop until the v0.8 controller reconciles + each CR (self-heals on controller startup; strict MHCs could react during the window). + - Removal of `.status.ready` / v1beta1 conditions is a **breaking change** for any + external tooling reading them. No v1beta1 downconversion (minor — CAPIC CRDs are + `v1alpha1`-only, so there is no v1beta1 API version to convert to). + - Requires upgrading CAPI core to v1.11+ alongside CAPIC v0.8 (clusterctl contract gating). +4. **No hard e2e gate on #378.** #378 merges on `make test/verify/lint`. The empirical + upgrade proof lives downstream in #379 (its natural home), not as a blocker on #378. +5. **Additive history on #378.** Add "declare v1beta2 / drop v1beta1" commits on top of the + existing bridge commits (net tree = pure v1beta2). No interactive rewrite. +6. **Linear stack preserved.** `main ← #378 ← #380 ← #379`; rebase downstream onto the + rewritten #378 in order. + +## 4. SP1 — #378: the clean cut (primary deliverable) + +Verified, complete change surface. + +### 4.1 API types (`api/v1alpha1/`) +- `ionoscloudcluster_types.go`: remove `Status.Deprecated` field; remove + `IonosCloudClusterDeprecatedStatus` and `IonosCloudClusterV1Beta1DeprecatedStatus`; remove + `GetV1Beta1Conditions` / `SetV1Beta1Conditions`. +- `ionoscloudmachine_types.go`: same, plus remove `SetV1Beta1Ready`. +- **Keep** the condition-type constants (`IonosCloudClusterReady`, `MachineProvisionedCondition`) + and the top-level `Conditions []metav1.Condition`. `clusterv1` + (`sigs.k8s.io/cluster-api/api/core/v1beta2`) stays imported — still used by those constants + (`clusterv1.ConditionType`). *Optional tidy:* retype the two constants to `string` (only + ever stringified); do it only if it keeps lint/`ireturn` quiet — not required. + +### 4.2 Controllers / services +- `internal/controller/ionoscloudcluster_controller.go`: delete the "set deprecated v1beta1 + ready" block (incl. its `//nolint:staticcheck`). Keep `Status.Initialization.Provisioned = new(true)`. +- `internal/service/cloud/server.go`: delete `ms.IonosMachine.SetV1Beta1Ready(true)`. + +### 4.3 Scope plumbing (`scope/cluster.go`, `scope/machine.go`) +- Remove the `patch.WithOwnedV1Beta1Conditions{…}` option from both `PatchObject` calls; keep + `patch.WithOwnedConditions{…}`. +- Simplify the `owned*Conditions` doc comments that reference the derived v1beta1 list. +- `clusterv1` remains used (`clusterv1.ReadyCondition` in `WithOwnedConditions` / + `SetSummaryCondition`), so the import stays. + +### 4.4 Generated (via `make manifests generate`) +- Both CRD YAMLs lose `status.deprecated`. +- `api/v1alpha1/zz_generated.deepcopy.go` loses the deprecated-type deepcopy funcs. + +### 4.5 Metadata / docs +- `metadata.yaml`: add release series `0.8` with `contract: v1beta2` (0.7 and below stay + `v1beta1`). This makes the *declared* contract match the CRD `cluster.x-k8s.io/v1beta2` + annotation already present. +- `README.md`: add a v0.8 row to the compatibility matrix (v1beta2; CAPI **v1.11+** only, + since a v1beta2-declared provider needs a v1beta2-capable core) and note v1beta1 is dropped. +- `docs/capi-upgrade-plan.md`: **rewrite** to the correct timeline (v0.6 v1.8/v1beta1 → + v0.7 v1.10/v1beta1 → **v0.8 v1.11 pure v1beta2 clean cut**), replacing the obsolete + bridge/Part-A/Part-B narrative. + +### 4.6 Tests +- `api/v1alpha1/ionoscloudcluster_types_test.go` (≈ lines 41–45, 154, 175) and + `ionoscloudmachine_types_test.go` (≈ line 542): remove the `Get/SetV1Beta1*` assertions. + +### 4.7 What deliberately stays +- The v1beta2 implementation already on the branch (`Status.Initialization.Provisioned`, + `[]metav1.Condition`, `conditions.Set`, CRD `cluster.x-k8s.io/v1beta2` annotation). +- No conversion webhook is added; CRDs remain `v1alpha1`. +- No CAPI version change beyond the v1.11.11 already on the branch. + +### 4.8 Verification (SP1 gate) +- `make test` (unit + envtest integration), `make verify` (gen/tidy up to date), `make lint`. +- Inspection check: generated CRD YAMLs contain no `status.deprecated`; no remaining + references to the removed symbols (`grep`). +- In-place upgrade behavior is validated by analysis here and empirically in SP3 — not gated + on #378. + +## 5. SP2 — #380: rebase the v1.12.10 bump + +Rebase `feat/upgrade-cluster-api-v1.12.10` onto the rewritten #378. It is a pure dependency +bump with no v1beta1 code, so conflicts should be limited to `go.mod`/`go.sum` and any +regenerated artifacts. Re-run `make verify` after rebase. Detailed steps deferred to SP2's own +plan. + +## 6. SP3 — #379: rebase + staged upgrade e2e + +Rebase `feat/upgrade-e2e-suite` onto #380. Then change `test/e2e/upgrade_test.go`: +- **Replace** the existing direct `v0.6.3 → v0.6.99(local)` block (which, with the clean cut, + silently became a direct v0.6 → v0.8 jump) with the **staged path**: + `v0.6.3 → v0.7.0 → v0.8(local build)`. Because #379 rebases onto #380, the local v0.8 + provider is built against CAPI **v1.12.10**, so the final hop lands the core on v1.12.10 + (mirroring the existing suite design). This proves the supported in-place upgrade survives + the pure-v1beta2 cut. +- Bump the local dev provider version tag (currently `v0.6.99`) to reflect v0.8 (e.g. + `v0.8.99`), and update `metadata.yaml` / e2e config references accordingly. +- Reconcile e2e helpers that reference v1beta1 (`test/e2e/helpers/clusterhealth.go`, + `conditions.go`): these legitimately assert *old* clusters during upgrade and stay, but + their version comments/labels are updated for the staged path. +- Prerequisite satisfied: v0.6.3 and v0.7.0 are both published, so the block can `init`. + +Detailed block layout deferred to SP3's own plan. + +## 7. Risks & mitigations + +| Risk | Likelihood | Mitigation | +|---|---|---| +| Transient not-ready window trips a strict MHC on `v0.7→v0.8` | low | Controller reconciles on startup; documented; SP3 e2e asserts cluster health post-upgrade | +| External tooling reads `.status.ready` and breaks | medium | Documented as a breaking change in README + release notes | +| Downstream rebase (#380/#379) conflicts | medium | Additive #378 history keeps diffs legible; rebase in order, re-run `make verify` | +| Removed symbol still referenced somewhere unseen | low | `grep` sweep in SP1 verification; `make build` fails fast otherwise | + +## 8. Non-goals +- No conversion webhook; no CRD version bump. +- No v0.9 follow-up — the drop happens in v0.8. +- No CAPI bump beyond v1.11.11 on #378 (v1.12.10 is #380's job). +- No new e2e suite beyond changing the existing upgrade block in #379. + +## 9. References +- CAPI v1.10→v1.11 provider migration guide +- CAPI v1.11→v1.12 provider migration guide: https://cluster-api.sigs.k8s.io/developer/providers/migrations/v1.11-to-v1.12 +- Compatibility matrix: `README.md` +- Superseded planning doc (to be rewritten): `docs/capi-upgrade-plan.md` From 745f71871580b75de0e4cf1d2c16a01e5ccfb8d6 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 17 Jul 2026 10:28:08 +0000 Subject: [PATCH 58/94] docs(plan): SP1 implementation plan for pure-v1beta2 clean cut Task-by-task plan to drop all v1beta1 residue on #378 and declare the v1beta2 contract for v0.8: remove deprecated status fields/helpers, drop v1beta1 condition ownership, regenerate CRDs/deepcopy, update metadata, README and the upgrade-plan doc. Co-Authored-By: Claude Opus 4.8 --- .../2026-07-17-sp1-drop-v1beta1-clean-cut.md | 830 ++++++++++++++++++ 1 file changed, 830 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-17-sp1-drop-v1beta1-clean-cut.md diff --git a/docs/superpowers/plans/2026-07-17-sp1-drop-v1beta1-clean-cut.md b/docs/superpowers/plans/2026-07-17-sp1-drop-v1beta1-clean-cut.md new file mode 100644 index 00000000..8df7f076 --- /dev/null +++ b/docs/superpowers/plans/2026-07-17-sp1-drop-v1beta1-clean-cut.md @@ -0,0 +1,830 @@ +# SP1 — Drop v1beta1 (pure v1beta2 v0.8) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make CAPIC a pure v1beta2 provider in v0.8 by declaring the v1beta2 contract and removing every v1beta1 residue from `#378` (branch `feat/upgrade-cluster-api-v1.11.11`). + +**Architecture:** This is a removal-and-declaration change on top of the existing v1beta2 implementation already on the branch. We delete the dual-write plumbing (deprecated `status.deprecated.v1beta1.*` fields, `Get/SetV1Beta1*` helpers, v1beta1 condition ownership in patch calls), flip the declared contract in `metadata.yaml`, regenerate CRDs/deepcopy, and update docs. Commits are additive (no history rewrite). + +**Tech Stack:** Go 1.26, kubebuilder v4, controller-gen (`make generate`/`make manifests`), Cluster API v1.11.11 (`sigs.k8s.io/cluster-api/api/core/v1beta2`), Ginkgo + envtest, testify. + +## Global Constraints + +- Go **1.26+**; do not change `go.mod` CAPI version (stays `sigs.k8s.io/cluster-api v1.11.11` — the v1.12 bump is PR #380, out of scope here). +- Never hand-edit generated files (`api/v1alpha1/zz_generated.deepcopy.go`, `config/crd/bases/*.yaml`); regenerate via `make generate` / `make manifests`. +- Linting is strict (`golangci.yml`, `default: none`). Run `make lint-fix` before considering a task done; unused imports are compile errors — remove them. +- Every commit message ends with the trailer line: `Co-Authored-By: Claude Opus 4.8 `. +- This is removal-heavy work: "tests" are compilation (`go build`/`go vet`), the existing suite still passing, and `grep` assertions that removed symbols are gone. Behavior that must be preserved (`Status.Initialization.Provisioned = true`) is already covered by existing controller/server tests — do not add new tests for it. +- Keep the CRD annotation `cluster.x-k8s.io/v1beta2=v1alpha1` and all v1beta2 status fields. Do NOT add a conversion webhook. Do NOT change the CRD version (`v1alpha1`). + +--- + +### Task 1: Stop writing the deprecated v1beta1 `ready` field + +**Files:** +- Modify: `internal/controller/ionoscloudcluster_controller.go` (~lines 181–189) +- Modify: `internal/service/cloud/server.go` (~lines 148–150) + +**Interfaces:** +- Consumes: nothing new. +- Produces: nothing new. After this task the deprecated helpers/fields still exist but are no longer written from the reconcile paths. + +- [ ] **Step 1: Remove the deprecated dual-write in the cluster controller** + +In `internal/controller/ionoscloudcluster_controller.go`, replace this block: + +```go + clusterScope.IonosCluster.Status.Initialization.Provisioned = new(true) + // Set deprecated v1beta1 ready field for backwards compatibility. + if clusterScope.IonosCluster.Status.Deprecated == nil { + clusterScope.IonosCluster.Status.Deprecated = &infrav1.IonosCloudClusterDeprecatedStatus{} + } + if clusterScope.IonosCluster.Status.Deprecated.V1Beta1 == nil { + clusterScope.IonosCluster.Status.Deprecated.V1Beta1 = &infrav1.IonosCloudClusterV1Beta1DeprecatedStatus{} + } + clusterScope.IonosCluster.Status.Deprecated.V1Beta1.Ready = true //nolint:staticcheck // Intentionally setting deprecated field for v1beta1 backwards compatibility. + return ctrl.Result{}, nil +``` + +with: + +```go + clusterScope.IonosCluster.Status.Initialization.Provisioned = new(true) + return ctrl.Result{}, nil +``` + +- [ ] **Step 2: Remove the deprecated dual-write in the server service** + +In `internal/service/cloud/server.go`, replace: + +```go + ms.IonosMachine.Status.Initialization.Provisioned = new(true) + // Set deprecated v1beta1 ready field for backwards compatibility. + ms.IonosMachine.SetV1Beta1Ready(true) + conditions.Set(ms.IonosMachine, metav1.Condition{ +``` + +with: + +```go + ms.IonosMachine.Status.Initialization.Provisioned = new(true) + conditions.Set(ms.IonosMachine, metav1.Condition{ +``` + +- [ ] **Step 3: Verify it compiles and vets** + +Run: `go build ./... && go vet ./internal/...` +Expected: no output, exit 0. + +- [ ] **Step 4: Run the affected unit tests** + +Run: `go test ./internal/controller/... ./internal/service/cloud/... -short` +Expected: `ok` for both packages (no test asserts the deprecated `ready` write). + +- [ ] **Step 5: Commit** + +```bash +git add internal/controller/ionoscloudcluster_controller.go internal/service/cloud/server.go +git commit -m "refactor: stop writing deprecated v1beta1 ready field + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 2: Drop v1beta1 condition ownership from the scope patch calls + +**Files:** +- Modify: `scope/cluster.go` (~lines 39–47 and ~230–242) +- Modify: `scope/machine.go` (~lines 38–46 and ~197–209) + +**Interfaces:** +- Consumes: `patch.WithOwnedConditions` (unchanged; kept). +- Produces: `PatchObject` on both scopes now declares only v1beta2 condition ownership. + +- [ ] **Step 1: Simplify the `ownedClusterConditions` doc comment** + +In `scope/cluster.go`, replace: + +```go +// ownedClusterConditions is the single source of truth for the v2 condition types owned by the +// cluster controller. WithOwnedV1Beta1Conditions is derived from this list (only ReadyCondition +// is carried into v1beta1; provider-specific conditions are v2-only). Keeping both lists adjacent +// and derived from one variable makes it structurally impossible to add a condition to one side +// while forgetting the other. +var ownedClusterConditions = []string{ +``` + +with: + +```go +// ownedClusterConditions is the single source of truth for the condition types owned by the +// cluster controller. +var ownedClusterConditions = []string{ +``` + +- [ ] **Step 2: Remove the v1beta1 ownership option from the cluster patch call** + +In `scope/cluster.go`, replace: + +```go + // V1Beta1 ownership covers only the cross-cutting Ready condition; provider-specific + // conditions (IonosCloudClusterReady) are v2-only and intentionally omitted here. + if patchErr := c.patchHelper.Patch(timeoutCtx, c.IonosCluster, + patch.WithOwnedV1Beta1Conditions{ + Conditions: []clusterv1.ConditionType{clusterv1.ReadyCondition}, + }, + // V2 ownership is derived from ownedClusterConditions — the single source of truth. + patch.WithOwnedConditions{ + Conditions: ownedClusterConditions, + }, + ); patchErr != nil { + return patchErr + } +``` + +with: + +```go + // Ownership is derived from ownedClusterConditions — the single source of truth. + if patchErr := c.patchHelper.Patch(timeoutCtx, c.IonosCluster, + patch.WithOwnedConditions{ + Conditions: ownedClusterConditions, + }, + ); patchErr != nil { + return patchErr + } +``` + +- [ ] **Step 3: Simplify the `ownedMachineConditions` doc comment** + +In `scope/machine.go`, replace: + +```go +// ownedMachineConditions is the single source of truth for the v2 condition types owned by the +// machine controller. WithOwnedV1Beta1Conditions is derived from this list (only ReadyCondition +// is carried into v1beta1; provider-specific conditions are v2-only). Keeping both lists adjacent +// and derived from one variable makes it structurally impossible to add a condition to one side +// while forgetting the other. +var ownedMachineConditions = []string{ +``` + +with: + +```go +// ownedMachineConditions is the single source of truth for the condition types owned by the +// machine controller. +var ownedMachineConditions = []string{ +``` + +- [ ] **Step 4: Remove the v1beta1 ownership option from the machine patch call** + +In `scope/machine.go`, replace: + +```go + if patchErr := m.patchHelper.Patch( + timeoutCtx, + m.IonosMachine, + // V1Beta1 ownership covers only the cross-cutting Ready condition; provider-specific + // conditions (MachineProvisionedCondition) are v2-only and intentionally omitted here. + patch.WithOwnedV1Beta1Conditions{Conditions: []clusterv1.ConditionType{ + clusterv1.ReadyCondition, + }}, + // V2 ownership is derived from ownedMachineConditions — the single source of truth. + patch.WithOwnedConditions{Conditions: ownedMachineConditions}, + ); patchErr != nil { + return patchErr + } +``` + +with: + +```go + if patchErr := m.patchHelper.Patch( + timeoutCtx, + m.IonosMachine, + // Ownership is derived from ownedMachineConditions — the single source of truth. + patch.WithOwnedConditions{Conditions: ownedMachineConditions}, + ); patchErr != nil { + return patchErr + } +``` + +- [ ] **Step 5: Verify build/vet (confirms `clusterv1` still used via `ReadyCondition`)** + +Run: `go build ./scope/... && go vet ./scope/...` +Expected: no output, exit 0. (If `clusterv1` reports as unused, something else changed — it must remain, used by `string(clusterv1.ReadyCondition)` in both `owned*Conditions` and `SetSummaryCondition`.) + +- [ ] **Step 6: Run scope tests** + +Run: `go test ./scope/... -short` +Expected: `ok github.com/ionos-cloud/cluster-api-provider-ionoscloud/scope`. + +- [ ] **Step 7: Commit** + +```bash +git add scope/cluster.go scope/machine.go +git commit -m "refactor(scope): drop v1beta1 condition ownership from patch calls + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 3: Remove the deprecated v1beta1 condition tests + +**Files:** +- Modify: `api/v1alpha1/ionoscloudcluster_types_test.go` +- Modify: `api/v1alpha1/ionoscloudmachine_types_test.go` + +**Interfaces:** +- Consumes: nothing new. +- Produces: the api test package no longer references `Get/SetV1Beta1Conditions` or `deprecatedv1beta1conditions`, so Task 4 can remove those helpers without breaking tests. + +- [ ] **Step 1: Delete the standalone `TestIonosCloudCluster_Conditions` unit test** + +In `api/v1alpha1/ionoscloudcluster_types_test.go`, delete this function entirely: + +```go +func TestIonosCloudCluster_Conditions(t *testing.T) { + conds := clusterv1.Conditions{{Type: "type"}} + cluster := &IonosCloudCluster{} + + cluster.SetV1Beta1Conditions(conds) + require.Equal(t, conds, cluster.GetV1Beta1Conditions()) +} +``` + +- [ ] **Step 2: Remove the deprecated assertions from the cluster "Status" spec** + +In the same file, inside `Context("Status", ...)`, delete these three lines (leaving the surrounding `Provisioned`/`CurrentRequestByDatacenter` assertions intact): + +- `Expect(fetched.GetV1Beta1Conditions()).To(BeEmpty())` +- `deprecatedv1beta1conditions.MarkTrue(fetched, clusterv1.ReadyCondition)` +- `Expect(fetched.GetV1Beta1Conditions()).To(HaveLen(1))` +- `Expect(deprecatedv1beta1conditions.IsTrue(fetched, clusterv1.ReadyCondition)).To(BeTrue())` + +After the edit that spec block reads: + +```go + Context("Status", func() { + It("should correctly get and set the status", func() { + By("initially having an empty status") + + cluster := defaultCluster() + Expect(k8sClient.Create(context.Background(), cluster)).To(Succeed()) + + key := client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name} + fetched := &IonosCloudCluster{} + Expect(k8sClient.Get(context.Background(), key, fetched)).To(Succeed()) + Expect(fetched.Status.Initialization.Provisioned).To(BeNil()) + Expect(fetched.Status.CurrentRequestByDatacenter).To(BeEmpty()) + + By("retrieving the cluster and setting the status") + fetched.Status.Initialization = IonosCloudClusterInitializationStatus{Provisioned: new(true)} + wantProvisionRequest := ProvisioningRequest{ + Method: "POST", + RequestPath: "/path/to/resource", + State: "QUEUED", + } + fetched.Status.CurrentRequestByDatacenter = map[string]ProvisioningRequest{ + "123": wantProvisionRequest, + } + + By("updating the cluster status") + Expect(k8sClient.Status().Update(context.Background(), fetched)).To(Succeed()) + + Expect(k8sClient.Get(context.Background(), key, fetched)).To(Succeed()) + Expect(fetched.Status.Initialization.Provisioned).To(HaveValue(BeTrue())) + Expect(fetched.Status.CurrentRequestByDatacenter).To(HaveLen(1)) + Expect(fetched.Status.CurrentRequestByDatacenter["123"]).To(Equal(wantProvisionRequest)) + + By("Removing the entry from the status again") + delete(fetched.Status.CurrentRequestByDatacenter, "123") + Expect(k8sClient.Status().Update(context.Background(), fetched)).To(Succeed()) + + Expect(k8sClient.Get(context.Background(), key, fetched)).To(Succeed()) + Expect(fetched.Status.CurrentRequestByDatacenter).To(BeEmpty()) + }) + }) +``` + +- [ ] **Step 3: Fix the cluster test imports (remove now-unused ones)** + +In `api/v1alpha1/ionoscloudcluster_types_test.go`, replace the import block: + +```go +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" + deprecatedv1beta1conditions "sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) +``` + +with (drop `testing`, `require`, `deprecatedv1beta1conditions`; keep `clusterv1` — still used by `clusterv1.APIEndpoint` in `defaultCluster`): + +```go +import ( + "context" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" + "sigs.k8s.io/controller-runtime/pkg/client" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) +``` + +- [ ] **Step 4: Delete the machine `Context("Conditions", ...)` spec** + +In `api/v1alpha1/ionoscloudmachine_types_test.go`, delete this entire block: + +```go + Context("Conditions", func() { + It("should correctly set and get the conditions", func() { + m := defaultMachine() + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(k8sClient.Get( + context.Background(), client.ObjectKey{Name: m.Name, Namespace: m.Namespace}, m)).To(Succeed()) + + // Calls SetConditions with required fields + deprecatedv1beta1conditions.MarkTrue(m, MachineProvisionedCondition) + + Expect(k8sClient.Status().Update(context.Background(), m)).To(Succeed()) + Expect(k8sClient.Get(context.Background(), + client.ObjectKey{Name: m.Name, Namespace: m.Namespace}, m)).To(Succeed()) + + machineConditions := m.GetV1Beta1Conditions() + Expect(machineConditions).To(HaveLen(1)) + Expect(machineConditions[0].Type).To(Equal(MachineProvisionedCondition)) + Expect(machineConditions[0].Status).To(Equal(corev1.ConditionTrue)) + }) + }) +``` + +- [ ] **Step 5: Remove the deprecated condition set-up from the machine "Status" spec** + +In the same file, inside `Context("Status", ...)`, delete the single line: + +```go + deprecatedv1beta1conditions.MarkTrue(m, MachineProvisionedCondition) +``` + +(The `want := *m.DeepCopy()` / `cmp.Diff(want.Status, m.Status)` round-trip below stays and still passes — it now compares a status without the deprecated subtree.) + +- [ ] **Step 6: Fix the machine test imports (remove `deprecatedv1beta1conditions`)** + +In `api/v1alpha1/ionoscloudmachine_types_test.go`, replace the import block: + +```go +import ( + "context" + + "github.com/google/go-cmp/cmp" + sdk "github.com/ionos-cloud/sdk-go/v6" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + deprecatedv1beta1conditions "sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) +``` + +with (drop `deprecatedv1beta1conditions`; keep `corev1` — still used by `corev1.TypedLocalObjectReference` elsewhere): + +```go +import ( + "context" + + "github.com/google/go-cmp/cmp" + sdk "github.com/ionos-cloud/sdk-go/v6" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) +``` + +- [ ] **Step 7: Verify the api test package still builds and vets** + +Run: `go vet ./api/...` +Expected: no output, exit 0. (Compilation covers the test files; a leftover unused import or missing symbol fails here.) + +- [ ] **Step 8: Commit** + +```bash +git add api/v1alpha1/ionoscloudcluster_types_test.go api/v1alpha1/ionoscloudmachine_types_test.go +git commit -m "test(api): remove deprecated v1beta1 condition tests + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 4: Remove deprecated v1beta1 status fields/helpers and regenerate + +**Files:** +- Modify: `api/v1alpha1/ionoscloudcluster_types.go` +- Modify: `api/v1alpha1/ionoscloudmachine_types.go` +- Regenerate: `api/v1alpha1/zz_generated.deepcopy.go`, `config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml`, `config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml` + +**Interfaces:** +- Consumes: nothing new. +- Produces: `IonosCloudClusterStatus` / `IonosCloudMachineStatus` no longer have a `Deprecated` field; the `Deprecated*`/`V1Beta1*` types and `Get/SetV1Beta1Conditions`/`SetV1Beta1Ready` methods no longer exist. `clusterv1` stays imported in both types files (used by the `clusterv1.ConditionType` constants). + +- [ ] **Step 1: Remove the `Deprecated` field from `IonosCloudClusterStatus`** + +In `api/v1alpha1/ionoscloudcluster_types.go`, delete these three lines (the last member of the status struct): + +```go + // Deprecated groups all status fields deprecated and scheduled for removal when v1beta1 contract support is dropped. + //+optional + Deprecated *IonosCloudClusterDeprecatedStatus `json:"deprecated,omitempty"` +``` + +- [ ] **Step 2: Remove the cluster deprecated status types** + +In the same file, delete both type declarations: + +```go +// IonosCloudClusterDeprecatedStatus groups all status fields deprecated and scheduled for removal when +// v1beta1 contract support is dropped. +type IonosCloudClusterDeprecatedStatus struct { + // V1Beta1 groups all v1beta1 status fields that are deprecated and scheduled for removal. + //+optional + V1Beta1 *IonosCloudClusterV1Beta1DeprecatedStatus `json:"v1beta1,omitempty"` +} + +// IonosCloudClusterV1Beta1DeprecatedStatus contains deprecated v1beta1 fields. +type IonosCloudClusterV1Beta1DeprecatedStatus struct { + // Ready indicates that the cluster is ready. + // + // Deprecated: Use Initialization.Provisioned instead. + //+optional + Ready bool `json:"ready,omitempty"` + + // Conditions defines current service state of the IonosCloudCluster using the deprecated v1beta1 condition type. + // + // Deprecated: Use the top-level conditions field instead. + //+optional + Conditions clusterv1.Conditions `json:"conditions,omitempty"` +} +``` + +- [ ] **Step 3: Remove the cluster deprecated helper methods** + +In the same file, delete both methods: + +```go +// GetV1Beta1Conditions returns the deprecated v1beta1 conditions from status.deprecated.v1beta1.conditions. +func (i *IonosCloudCluster) GetV1Beta1Conditions() clusterv1.Conditions { + if i.Status.Deprecated == nil || i.Status.Deprecated.V1Beta1 == nil { + return nil + } + return i.Status.Deprecated.V1Beta1.Conditions +} + +// SetV1Beta1Conditions sets the deprecated v1beta1 conditions in status.deprecated.v1beta1.conditions. +func (i *IonosCloudCluster) SetV1Beta1Conditions(conditions clusterv1.Conditions) { + if i.Status.Deprecated == nil { + i.Status.Deprecated = &IonosCloudClusterDeprecatedStatus{} + } + if i.Status.Deprecated.V1Beta1 == nil { + i.Status.Deprecated.V1Beta1 = &IonosCloudClusterV1Beta1DeprecatedStatus{} + } + i.Status.Deprecated.V1Beta1.Conditions = conditions +} +``` + +- [ ] **Step 4: Remove the `Deprecated` field from `IonosCloudMachineStatus`** + +In `api/v1alpha1/ionoscloudmachine_types.go`, delete these three lines: + +```go + // Deprecated groups all status fields deprecated and scheduled for removal when v1beta1 contract support is dropped. + //+optional + Deprecated *IonosCloudMachineDeprecatedStatus `json:"deprecated,omitempty"` +``` + +- [ ] **Step 5: Remove the machine deprecated status types** + +In the same file, delete both type declarations: + +```go +// IonosCloudMachineDeprecatedStatus groups all status fields deprecated and scheduled for removal when +// v1beta1 contract support is dropped. +type IonosCloudMachineDeprecatedStatus struct { + // V1Beta1 groups all v1beta1 status fields that are deprecated and scheduled for removal. + //+optional + V1Beta1 *IonosCloudMachineV1Beta1DeprecatedStatus `json:"v1beta1,omitempty"` +} + +// IonosCloudMachineV1Beta1DeprecatedStatus contains deprecated v1beta1 fields. +type IonosCloudMachineV1Beta1DeprecatedStatus struct { + // Ready indicates the VM has been provisioned and is ready. + // + // Deprecated: Use Initialization.Provisioned instead. + //+optional + Ready bool `json:"ready,omitempty"` + + // Conditions defines current service state of the IonosCloudMachine using the deprecated v1beta1 condition type. + // + // Deprecated: Use the top-level conditions field instead. + //+optional + Conditions clusterv1.Conditions `json:"conditions,omitempty"` +} +``` + +- [ ] **Step 6: Remove the machine deprecated helper methods** + +In the same file, delete all three methods: + +```go +// GetV1Beta1Conditions returns the deprecated v1beta1 conditions from status.deprecated.v1beta1.conditions. +func (m *IonosCloudMachine) GetV1Beta1Conditions() clusterv1.Conditions { + if m.Status.Deprecated == nil || m.Status.Deprecated.V1Beta1 == nil { + return nil + } + return m.Status.Deprecated.V1Beta1.Conditions +} + +// SetV1Beta1Conditions sets the deprecated v1beta1 conditions in status.deprecated.v1beta1.conditions. +func (m *IonosCloudMachine) SetV1Beta1Conditions(conditions clusterv1.Conditions) { + if m.Status.Deprecated == nil { + m.Status.Deprecated = &IonosCloudMachineDeprecatedStatus{} + } + if m.Status.Deprecated.V1Beta1 == nil { + m.Status.Deprecated.V1Beta1 = &IonosCloudMachineV1Beta1DeprecatedStatus{} + } + m.Status.Deprecated.V1Beta1.Conditions = conditions +} + +// SetV1Beta1Ready sets the deprecated v1beta1 ready field. +func (m *IonosCloudMachine) SetV1Beta1Ready(ready bool) { + if m.Status.Deprecated == nil { + m.Status.Deprecated = &IonosCloudMachineDeprecatedStatus{} + } + if m.Status.Deprecated.V1Beta1 == nil { + m.Status.Deprecated.V1Beta1 = &IonosCloudMachineV1Beta1DeprecatedStatus{} + } + m.Status.Deprecated.V1Beta1.Ready = ready +} +``` + +- [ ] **Step 7: Regenerate deepcopy and CRDs** + +Run: `make generate manifests` +Expected: succeeds. `git status` shows modifications to `api/v1alpha1/zz_generated.deepcopy.go` and the two CRD YAMLs under `config/crd/bases/`. + +- [ ] **Step 8: Confirm the generated artifacts no longer contain the deprecated subtree** + +Run: `grep -rniE "deprecated|v1beta1" config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml api/v1alpha1/zz_generated.deepcopy.go` +Expected: no matches (empty output). + +- [ ] **Step 9: Build and vet the whole module** + +Run: `go build ./... && go vet ./...` +Expected: no output, exit 0. (If `clusterv1` reports unused in a types file, verify the `IonosCloudClusterReady` / `MachineProvisionedCondition` constants still reference `clusterv1.ConditionType` — they must remain.) + +- [ ] **Step 10: Commit** + +```bash +git add api/v1alpha1/ionoscloudcluster_types.go api/v1alpha1/ionoscloudmachine_types.go api/v1alpha1/zz_generated.deepcopy.go config/crd/bases/ +git commit -m "feat(api)!: remove deprecated v1beta1 status fields and helpers + +BREAKING CHANGE: status.ready and the v1beta1 status.conditions are removed from +IonosCloudCluster/IonosCloudMachine. Consumers must read status.initialization.provisioned +and status.conditions (metav1.Condition). + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 5: Declare the v1beta2 contract (metadata + README) + +**Files:** +- Modify: `metadata.yaml` +- Modify: `README.md` (compatibility matrix, ~lines 50–55) + +**Interfaces:** +- Consumes: nothing. +- Produces: clusterctl sees the `0.8` release series as `contract: v1beta2`, matching the CRD annotation. + +- [ ] **Step 1: Add the v0.8 release series as v1beta2** + +In `metadata.yaml`, replace: + +```yaml +releaseSeries: +- major: 0 + minor: 7 + contract: v1beta1 +``` + +with: + +```yaml +releaseSeries: +- major: 0 + minor: 8 + contract: v1beta2 +- major: 0 + minor: 7 + contract: v1beta1 +``` + +- [ ] **Step 2: Add the v0.8 row to the README compatibility matrix** + +In `README.md`, replace the v0.7 matrix row: + +``` +| CAPIC v1alpha1 (v0.7) | ☓ | ☓ | ☓ | ✓ | +``` + +with the v0.7 row followed by a new v0.8 row: + +``` +| CAPIC v1alpha1 (v0.7) | ☓ | ☓ | ☓ | ✓ | +| CAPIC v1alpha1 (v0.8) | ☓ | ☓ | ☓ | ✓ | +``` + +- [ ] **Step 3: Add a contract note under the matrix** + +In `README.md`, immediately after the compatibility table (before the `### Kubernetes Versions` heading), insert: + +```markdown + +> **v0.8 declares the Cluster API `v1beta2` contract and drops `v1beta1`.** It requires a +> `v1beta2`-capable core (CAPI v1.11+). The deprecated `status.ready` and v1beta1 +> `status.conditions` fields are removed — read `status.initialization.provisioned` and +> `status.conditions` instead. Upgrade existing clusters via the staged path +> `v0.6 → v0.7 → v0.8`. +``` + +- [ ] **Step 4: Sanity-check YAML validity** + +Run: `go run sigs.k8s.io/cluster-api/cmd/clusterctl version >/dev/null 2>&1; grep -nE "minor:|contract:" metadata.yaml` +Expected: the `grep` output lists `minor: 8` / `contract: v1beta2` at the top, then `minor: 7` / `contract: v1beta1`, etc. (The `clusterctl` invocation is optional; if unavailable, just eyeball the YAML indentation matches the existing entries.) + +- [ ] **Step 5: Commit** + +```bash +git add metadata.yaml README.md +git commit -m "feat(metadata): declare v1beta2 contract for v0.8 + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 6: Rewrite the CAPI upgrade plan doc + +**Files:** +- Overwrite: `docs/capi-upgrade-plan.md` + +**Interfaces:** +- Consumes: nothing. +- Produces: an accurate migration doc reflecting the real v0.6/v0.7/v0.8 timeline and the clean cut. + +- [ ] **Step 1: Replace the entire contents of `docs/capi-upgrade-plan.md`** + +Write this exact content: + +````markdown +# Cluster API upgrade & v1beta2 migration + +How CAPIC moves to the Cluster API **v1beta2** contract, and how the release series line up. + +## Release timeline (actual) + +| CAPIC | CAPI | Declared contract | Implements v1beta2 | Notes | +|-------|------|-------------------|--------------------|-------| +| v0.6.x | v1.8 | v1beta1 | no | pure v1beta1 | +| v0.7.0 | v1.10 | v1beta1 | no | pure v1beta1 (CAPI dependency bump only) | +| **v0.8** | **v1.11** | **v1beta2** | yes | **pure v1beta2 — v1beta1 support dropped** | + +There is no dual-writing "bridge" release: v0.7.0 shipped as pure v1beta1, so v0.8 is the +first release to implement v1beta2, and it does so as a clean cut. + +## What v0.8 changes + +- Declares the **v1beta2** contract (`metadata.yaml`), matching the CRD + `cluster.x-k8s.io/v1beta2` annotation. +- Implements the v1beta2 status contract: `status.initialization.provisioned` and + `status.conditions` (`[]metav1.Condition`). +- **Removes** all v1beta1 residue: the `status.deprecated.v1beta1.*` fields, the + `Get/SetV1Beta1Conditions` / `SetV1Beta1Ready` helpers, and the v1beta1 condition + ownership in the patch helpers. + +## Supported upgrade path + +Staged, in place: **v0.6 → v0.7 → v0.8** (upgrade the CAPI core to v1.11+ alongside v0.8). + +- Existing clusters keep running; **no recreation** is required (no `IonosCloudMachineTemplate` + content change → no Machine rollout; running Machines are not torn down by a transient + `provisioned=nil`). +- On the `v0.7 → v0.8` hop there is a brief window where existing objects report infra + not-provisioned until the v0.8 controller reconciles them (self-heals on controller startup). +- **Breaking:** `.status.ready` and the v1beta1 `status.conditions` are removed. Any external + tooling reading them must move to `status.initialization.provisioned` / `status.conditions`. + +Empirical validation lives in the e2e upgrade suite (PR #379), which exercises the staged +`v0.6.3 → v0.7.0 → v0.8` path. + +## References + +- CAPI v1.11 → v1.12 provider migration guide: + https://cluster-api.sigs.k8s.io/developer/providers/migrations/v1.11-to-v1.12 +- Design spec: `docs/superpowers/specs/2026-07-17-drop-v1beta1-pure-v1beta2-design.md` +- Compatibility matrix: `README.md` +```` + +- [ ] **Step 2: Commit** + +```bash +git add docs/capi-upgrade-plan.md +git commit -m "docs: rewrite capi upgrade plan for pure-v1beta2 v0.8 + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 7: Full verification gate + +**Files:** none created; may regenerate/format. + +**Interfaces:** none. + +- [ ] **Step 1: Confirm no v1beta1 residue remains in production Go code** + +Run: `grep -rniE "v1beta1|V1Beta1" --include="*.go" api/ scope/ internal/ cmd/ | grep -v zz_generated | grep -viE "_test.go"` +Expected: no matches (empty output). The only `clusterv1` imports point at `.../api/core/v1beta2` (string `v1beta2`), so they do not match. + +- [ ] **Step 2: Confirm the deprecated symbols are fully gone** + +Run: `grep -rniE "SetV1Beta1Ready|GetV1Beta1Conditions|SetV1Beta1Conditions|WithOwnedV1Beta1Conditions|DeprecatedStatus" --include="*.go" . | grep -v zz_generated` +Expected: no matches (empty output). + +- [ ] **Step 3: Run linters** + +Run: `make lint` +Expected: `0 issues`. If `make lint-fix` changes formatting, re-stage and amend the most relevant commit or add a small `chore: lint-fix` commit. + +- [ ] **Step 4: Run the full test suite** + +Run: `make test` +Expected: unit tests, vet, and the api envtest integration suite all pass (`ok`/green). This exercises the retained v1beta2 status round-trip and the controller/service provisioning behavior. + +- [ ] **Step 5: Verify generated artifacts are up to date** + +Run: `make verify` +Expected: `verify-gen` and `verify-tidy` pass (no diff). If it reports a diff, run `make generate manifests` / `go mod tidy`, commit the result, and re-run. + +- [ ] **Step 6: Final commit (only if steps 3/5 produced changes)** + +```bash +git add -A +git commit -m "chore: regenerate and lint-fix after dropping v1beta1 + +Co-Authored-By: Claude Opus 4.8 " +``` + +If nothing changed, skip this step. + +--- + +## Self-Review + +**Spec coverage** (against `docs/superpowers/specs/2026-07-17-drop-v1beta1-pure-v1beta2-design.md` §4): +- §4.1 API types (fields/types/helpers) → Task 4. ✅ +- §4.2 controllers/services dual-write → Task 1. ✅ +- §4.3 scope plumbing → Task 2. ✅ +- §4.4 generated (deepcopy/CRDs) → Task 4 steps 7–8. ✅ +- §4.5 metadata + README + capi-upgrade-plan rewrite → Tasks 5 & 6. ✅ +- §4.6 tests → Task 3. ✅ +- §4.7 what stays (v1beta2 impl, CRD annotation, condition constants) → preserved by construction; asserted in Task 4 step 9 and Task 7 step 1. ✅ +- §4.8 verification (make test/verify/lint + grep + no `status.deprecated`) → Task 7 + Task 4 step 8. ✅ + +SP2 (#380 rebase) and SP3 (#379 rebase + staged e2e) are separate plans, per the spec's decomposition — intentionally out of scope here. + +**Placeholder scan:** No TBD/TODO; every code step shows the exact before/after content and every command has an expected result. ✅ + +**Type consistency:** `WithOwnedConditions` / `ownedClusterConditions` / `ownedMachineConditions` used consistently; `clusterv1` = `sigs.k8s.io/cluster-api/api/core/v1beta2` throughout; helper names (`GetV1Beta1Conditions`, `SetV1Beta1Conditions`, `SetV1Beta1Ready`) match the removals in Tasks 1/3/4. ✅ +```` From 924dd5c52c4fc38c057bc5a3a929437e564c0d30 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 17 Jul 2026 10:30:26 +0000 Subject: [PATCH 59/94] refactor: stop writing deprecated v1beta1 ready field Co-Authored-By: Claude Opus 4.8 --- internal/controller/ionoscloudcluster_controller.go | 8 -------- internal/service/cloud/server.go | 2 -- 2 files changed, 10 deletions(-) diff --git a/internal/controller/ionoscloudcluster_controller.go b/internal/controller/ionoscloudcluster_controller.go index f0e66b5e..ebdbedec 100644 --- a/internal/controller/ionoscloudcluster_controller.go +++ b/internal/controller/ionoscloudcluster_controller.go @@ -179,14 +179,6 @@ func (r *IonosCloudClusterReconciler) reconcileNormal( Reason: infrav1.ClusterProvisionedReason, }) clusterScope.IonosCluster.Status.Initialization.Provisioned = new(true) - // Set deprecated v1beta1 ready field for backwards compatibility. - if clusterScope.IonosCluster.Status.Deprecated == nil { - clusterScope.IonosCluster.Status.Deprecated = &infrav1.IonosCloudClusterDeprecatedStatus{} - } - if clusterScope.IonosCluster.Status.Deprecated.V1Beta1 == nil { - clusterScope.IonosCluster.Status.Deprecated.V1Beta1 = &infrav1.IonosCloudClusterV1Beta1DeprecatedStatus{} - } - clusterScope.IonosCluster.Status.Deprecated.V1Beta1.Ready = true //nolint:staticcheck // Intentionally setting deprecated field for v1beta1 backwards compatibility. return ctrl.Result{}, nil } diff --git a/internal/service/cloud/server.go b/internal/service/cloud/server.go index 875bd73b..02b5b085 100644 --- a/internal/service/cloud/server.go +++ b/internal/service/cloud/server.go @@ -146,8 +146,6 @@ func (s *Service) ReconcileServerDeletion(ctx context.Context, ms *scope.Machine // FinalizeMachineProvisioning marks the machine as provisioned. func (*Service) FinalizeMachineProvisioning(_ context.Context, ms *scope.Machine) (bool, error) { ms.IonosMachine.Status.Initialization.Provisioned = new(true) - // Set deprecated v1beta1 ready field for backwards compatibility. - ms.IonosMachine.SetV1Beta1Ready(true) conditions.Set(ms.IonosMachine, metav1.Condition{ Type: string(infrav1.MachineProvisionedCondition), Status: metav1.ConditionTrue, From 7051468f5639bb5478b6d3fc51640c3ab6914f5d Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 17 Jul 2026 10:33:07 +0000 Subject: [PATCH 60/94] refactor(scope): drop v1beta1 condition ownership from patch calls Co-Authored-By: Claude Opus 4.8 --- scope/cluster.go | 14 +++----------- scope/machine.go | 14 +++----------- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/scope/cluster.go b/scope/cluster.go index b67df22c..556c0d52 100644 --- a/scope/cluster.go +++ b/scope/cluster.go @@ -36,11 +36,8 @@ import ( "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/locker" ) -// ownedClusterConditions is the single source of truth for the v2 condition types owned by the -// cluster controller. WithOwnedV1Beta1Conditions is derived from this list (only ReadyCondition -// is carried into v1beta1; provider-specific conditions are v2-only). Keeping both lists adjacent -// and derived from one variable makes it structurally impossible to add a condition to one side -// while forgetting the other. +// ownedClusterConditions is the single source of truth for the condition types owned by the +// cluster controller. var ownedClusterConditions = []string{ string(clusterv1.ReadyCondition), string(infrav1.IonosCloudClusterReady), @@ -227,13 +224,8 @@ func (c *Cluster) PatchObject() error { timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() - // V1Beta1 ownership covers only the cross-cutting Ready condition; provider-specific - // conditions (IonosCloudClusterReady) are v2-only and intentionally omitted here. + // Ownership is derived from ownedClusterConditions — the single source of truth. if patchErr := c.patchHelper.Patch(timeoutCtx, c.IonosCluster, - patch.WithOwnedV1Beta1Conditions{ - Conditions: []clusterv1.ConditionType{clusterv1.ReadyCondition}, - }, - // V2 ownership is derived from ownedClusterConditions — the single source of truth. patch.WithOwnedConditions{ Conditions: ownedClusterConditions, }, diff --git a/scope/machine.go b/scope/machine.go index fd6f2ba3..fc7c26b2 100644 --- a/scope/machine.go +++ b/scope/machine.go @@ -35,11 +35,8 @@ import ( "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" ) -// ownedMachineConditions is the single source of truth for the v2 condition types owned by the -// machine controller. WithOwnedV1Beta1Conditions is derived from this list (only ReadyCondition -// is carried into v1beta1; provider-specific conditions are v2-only). Keeping both lists adjacent -// and derived from one variable makes it structurally impossible to add a condition to one side -// while forgetting the other. +// ownedMachineConditions is the single source of truth for the condition types owned by the +// machine controller. var ownedMachineConditions = []string{ string(clusterv1.ReadyCondition), string(infrav1.MachineProvisionedCondition), @@ -197,12 +194,7 @@ func (m *Machine) PatchObject() error { if patchErr := m.patchHelper.Patch( timeoutCtx, m.IonosMachine, - // V1Beta1 ownership covers only the cross-cutting Ready condition; provider-specific - // conditions (MachineProvisionedCondition) are v2-only and intentionally omitted here. - patch.WithOwnedV1Beta1Conditions{Conditions: []clusterv1.ConditionType{ - clusterv1.ReadyCondition, - }}, - // V2 ownership is derived from ownedMachineConditions — the single source of truth. + // Ownership is derived from ownedMachineConditions — the single source of truth. patch.WithOwnedConditions{Conditions: ownedMachineConditions}, ); patchErr != nil { return patchErr From c8bd0f05d9f7e8014867e7bf0aa6e98619f42c38 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 17 Jul 2026 10:36:17 +0000 Subject: [PATCH 61/94] test(api): remove deprecated v1beta1 condition tests Co-Authored-By: Claude Opus 4.8 --- api/v1alpha1/ionoscloudcluster_types_test.go | 15 ------------- api/v1alpha1/ionoscloudmachine_types_test.go | 22 -------------------- 2 files changed, 37 deletions(-) diff --git a/api/v1alpha1/ionoscloudcluster_types_test.go b/api/v1alpha1/ionoscloudcluster_types_test.go index 58ef83cb..464c9c56 100644 --- a/api/v1alpha1/ionoscloudcluster_types_test.go +++ b/api/v1alpha1/ionoscloudcluster_types_test.go @@ -18,13 +18,10 @@ package v1alpha1 import ( "context" - "testing" - "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" - deprecatedv1beta1conditions "sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" . "github.com/onsi/ginkgo/v2" @@ -37,14 +34,6 @@ const ( exampleIP = "198.51.100.1" ) -func TestIonosCloudCluster_Conditions(t *testing.T) { - conds := clusterv1.Conditions{{Type: "type"}} - cluster := &IonosCloudCluster{} - - cluster.SetV1Beta1Conditions(conds) - require.Equal(t, conds, cluster.GetV1Beta1Conditions()) -} - func defaultCluster() *IonosCloudCluster { return &IonosCloudCluster{ ObjectMeta: metav1.ObjectMeta{ @@ -151,7 +140,6 @@ var _ = Describe("IonosCloudCluster", func() { Expect(k8sClient.Get(context.Background(), key, fetched)).To(Succeed()) Expect(fetched.Status.Initialization.Provisioned).To(BeNil()) Expect(fetched.Status.CurrentRequestByDatacenter).To(BeEmpty()) - Expect(fetched.GetV1Beta1Conditions()).To(BeEmpty()) By("retrieving the cluster and setting the status") fetched.Status.Initialization = IonosCloudClusterInitializationStatus{Provisioned: new(true)} @@ -163,7 +151,6 @@ var _ = Describe("IonosCloudCluster", func() { fetched.Status.CurrentRequestByDatacenter = map[string]ProvisioningRequest{ "123": wantProvisionRequest, } - deprecatedv1beta1conditions.MarkTrue(fetched, clusterv1.ReadyCondition) By("updating the cluster status") Expect(k8sClient.Status().Update(context.Background(), fetched)).To(Succeed()) @@ -172,8 +159,6 @@ var _ = Describe("IonosCloudCluster", func() { Expect(fetched.Status.Initialization.Provisioned).To(HaveValue(BeTrue())) Expect(fetched.Status.CurrentRequestByDatacenter).To(HaveLen(1)) Expect(fetched.Status.CurrentRequestByDatacenter["123"]).To(Equal(wantProvisionRequest)) - Expect(fetched.GetV1Beta1Conditions()).To(HaveLen(1)) - Expect(deprecatedv1beta1conditions.IsTrue(fetched, clusterv1.ReadyCondition)).To(BeTrue()) By("Removing the entry from the status again") delete(fetched.Status.CurrentRequestByDatacenter, "123") diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index 579948bf..fbc00d5b 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -23,7 +23,6 @@ import ( sdk "github.com/ionos-cloud/sdk-go/v6" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - deprecatedv1beta1conditions "sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" . "github.com/onsi/ginkgo/v2" @@ -525,26 +524,6 @@ var _ = Describe("IonosCloudMachine Tests", func() { Entry("VCPU", ServerTypeVCPU), ) }) - Context("Conditions", func() { - It("should correctly set and get the conditions", func() { - m := defaultMachine() - Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(k8sClient.Get( - context.Background(), client.ObjectKey{Name: m.Name, Namespace: m.Namespace}, m)).To(Succeed()) - - // Calls SetConditions with required fields - deprecatedv1beta1conditions.MarkTrue(m, MachineProvisionedCondition) - - Expect(k8sClient.Status().Update(context.Background(), m)).To(Succeed()) - Expect(k8sClient.Get(context.Background(), - client.ObjectKey{Name: m.Name, Namespace: m.Namespace}, m)).To(Succeed()) - - machineConditions := m.GetV1Beta1Conditions() - Expect(machineConditions).To(HaveLen(1)) - Expect(machineConditions[0].Type).To(Equal(MachineProvisionedCondition)) - Expect(machineConditions[0].Status).To(Equal(corev1.ConditionTrue)) - }) - }) Context("Status", func() { It("should correctly set and get the status", func() { m := defaultMachine() @@ -553,7 +532,6 @@ var _ = Describe("IonosCloudMachine Tests", func() { client.ObjectKey{Name: m.Name, Namespace: m.Namespace}, m)).To(Succeed()) m.Status.Initialization = IonosCloudMachineInitializationStatus{Provisioned: new(true)} - deprecatedv1beta1conditions.MarkTrue(m, MachineProvisionedCondition) m.Status.CurrentRequest = &ProvisioningRequest{ Method: "GET", RequestPath: "path/to/resource", From c80dcb9eecfed40651cd9ab380944234f2891a05 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 17 Jul 2026 10:42:09 +0000 Subject: [PATCH 62/94] feat(api)!: remove deprecated v1beta1 status fields and helpers BREAKING CHANGE: status.ready and the v1beta1 status.conditions are removed from IonosCloudCluster/IonosCloudMachine. Consumers must read status.initialization.provisioned and status.conditions (metav1.Condition). Co-Authored-By: Claude Opus 4.8 --- api/v1alpha1/ionoscloudcluster_types.go | 46 --------- api/v1alpha1/ionoscloudmachine_types.go | 57 ----------- api/v1alpha1/zz_generated.deepcopy.go | 95 ------------------- ...e.cluster.x-k8s.io_ionoscloudclusters.yaml | 72 -------------- ...e.cluster.x-k8s.io_ionoscloudmachines.yaml | 72 -------------- 5 files changed, 342 deletions(-) diff --git a/api/v1alpha1/ionoscloudcluster_types.go b/api/v1alpha1/ionoscloudcluster_types.go index a597f67a..4b15f702 100644 --- a/api/v1alpha1/ionoscloudcluster_types.go +++ b/api/v1alpha1/ionoscloudcluster_types.go @@ -92,10 +92,6 @@ type IonosCloudClusterStatus struct { // ControlPlaneEndpointIPBlockID is the IONOS Cloud UUID for the control plane endpoint IP block. //+optional ControlPlaneEndpointIPBlockID string `json:"controlPlaneEndpointIPBlockID,omitempty"` - - // Deprecated groups all status fields deprecated and scheduled for removal when v1beta1 contract support is dropped. - //+optional - Deprecated *IonosCloudClusterDeprecatedStatus `json:"deprecated,omitempty"` } // IonosCloudClusterInitializationStatus provides observations of the IonosCloudCluster initialization process. @@ -108,29 +104,6 @@ type IonosCloudClusterInitializationStatus struct { Provisioned *bool `json:"provisioned,omitempty"` } -// IonosCloudClusterDeprecatedStatus groups all status fields deprecated and scheduled for removal when -// v1beta1 contract support is dropped. -type IonosCloudClusterDeprecatedStatus struct { - // V1Beta1 groups all v1beta1 status fields that are deprecated and scheduled for removal. - //+optional - V1Beta1 *IonosCloudClusterV1Beta1DeprecatedStatus `json:"v1beta1,omitempty"` -} - -// IonosCloudClusterV1Beta1DeprecatedStatus contains deprecated v1beta1 fields. -type IonosCloudClusterV1Beta1DeprecatedStatus struct { - // Ready indicates that the cluster is ready. - // - // Deprecated: Use Initialization.Provisioned instead. - //+optional - Ready bool `json:"ready,omitempty"` - - // Conditions defines current service state of the IonosCloudCluster using the deprecated v1beta1 condition type. - // - // Deprecated: Use the top-level conditions field instead. - //+optional - Conditions clusterv1.Conditions `json:"conditions,omitempty"` -} - //+kubebuilder:object:root=true //+kubebuilder:subresource:status //+kubebuilder:resource:path=ionoscloudclusters,scope=Namespaced,categories=cluster-api;ionoscloud,shortName=icc @@ -161,25 +134,6 @@ func init() { objectTypes = append(objectTypes, &IonosCloudCluster{}, &IonosCloudClusterList{}) } -// GetV1Beta1Conditions returns the deprecated v1beta1 conditions from status.deprecated.v1beta1.conditions. -func (i *IonosCloudCluster) GetV1Beta1Conditions() clusterv1.Conditions { - if i.Status.Deprecated == nil || i.Status.Deprecated.V1Beta1 == nil { - return nil - } - return i.Status.Deprecated.V1Beta1.Conditions -} - -// SetV1Beta1Conditions sets the deprecated v1beta1 conditions in status.deprecated.v1beta1.conditions. -func (i *IonosCloudCluster) SetV1Beta1Conditions(conditions clusterv1.Conditions) { - if i.Status.Deprecated == nil { - i.Status.Deprecated = &IonosCloudClusterDeprecatedStatus{} - } - if i.Status.Deprecated.V1Beta1 == nil { - i.Status.Deprecated.V1Beta1 = &IonosCloudClusterV1Beta1DeprecatedStatus{} - } - i.Status.Deprecated.V1Beta1.Conditions = conditions -} - // GetConditions returns the v1beta2 conditions from status.conditions. func (i *IonosCloudCluster) GetConditions() []metav1.Condition { return i.Status.Conditions diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index 5e223168..b1e2f9a1 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -314,10 +314,6 @@ type IonosCloudMachineStatus struct { // Location is the location of the datacenter the VM is provisioned in. //+optional Location string `json:"location"` - - // Deprecated groups all status fields deprecated and scheduled for removal when v1beta1 contract support is dropped. - //+optional - Deprecated *IonosCloudMachineDeprecatedStatus `json:"deprecated,omitempty"` } // IonosCloudMachineInitializationStatus provides observations of the IonosCloudMachine initialization process. @@ -330,29 +326,6 @@ type IonosCloudMachineInitializationStatus struct { Provisioned *bool `json:"provisioned,omitempty"` } -// IonosCloudMachineDeprecatedStatus groups all status fields deprecated and scheduled for removal when -// v1beta1 contract support is dropped. -type IonosCloudMachineDeprecatedStatus struct { - // V1Beta1 groups all v1beta1 status fields that are deprecated and scheduled for removal. - //+optional - V1Beta1 *IonosCloudMachineV1Beta1DeprecatedStatus `json:"v1beta1,omitempty"` -} - -// IonosCloudMachineV1Beta1DeprecatedStatus contains deprecated v1beta1 fields. -type IonosCloudMachineV1Beta1DeprecatedStatus struct { - // Ready indicates the VM has been provisioned and is ready. - // - // Deprecated: Use Initialization.Provisioned instead. - //+optional - Ready bool `json:"ready,omitempty"` - - // Conditions defines current service state of the IonosCloudMachine using the deprecated v1beta1 condition type. - // - // Deprecated: Use the top-level conditions field instead. - //+optional - Conditions clusterv1.Conditions `json:"conditions,omitempty"` -} - // MachineNetworkInfo contains information about the network configuration of the VM. // Before the provisioning MachineNetworkInfo may contain IP addresses to be used for provisioning. // After provisioning this information is available completely. @@ -412,36 +385,6 @@ type IonosCloudMachineList struct { Items []IonosCloudMachine `json:"items"` } -// GetV1Beta1Conditions returns the deprecated v1beta1 conditions from status.deprecated.v1beta1.conditions. -func (m *IonosCloudMachine) GetV1Beta1Conditions() clusterv1.Conditions { - if m.Status.Deprecated == nil || m.Status.Deprecated.V1Beta1 == nil { - return nil - } - return m.Status.Deprecated.V1Beta1.Conditions -} - -// SetV1Beta1Conditions sets the deprecated v1beta1 conditions in status.deprecated.v1beta1.conditions. -func (m *IonosCloudMachine) SetV1Beta1Conditions(conditions clusterv1.Conditions) { - if m.Status.Deprecated == nil { - m.Status.Deprecated = &IonosCloudMachineDeprecatedStatus{} - } - if m.Status.Deprecated.V1Beta1 == nil { - m.Status.Deprecated.V1Beta1 = &IonosCloudMachineV1Beta1DeprecatedStatus{} - } - m.Status.Deprecated.V1Beta1.Conditions = conditions -} - -// SetV1Beta1Ready sets the deprecated v1beta1 ready field. -func (m *IonosCloudMachine) SetV1Beta1Ready(ready bool) { - if m.Status.Deprecated == nil { - m.Status.Deprecated = &IonosCloudMachineDeprecatedStatus{} - } - if m.Status.Deprecated.V1Beta1 == nil { - m.Status.Deprecated.V1Beta1 = &IonosCloudMachineV1Beta1DeprecatedStatus{} - } - m.Status.Deprecated.V1Beta1.Ready = ready -} - // GetConditions returns the v1beta2 conditions from status.conditions. func (m *IonosCloudMachine) GetConditions() []metav1.Condition { return m.Status.Conditions diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 0fa8764e..b31bac1d 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -24,7 +24,6 @@ import ( "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "sigs.k8s.io/cluster-api/api/core/v1beta2" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -126,26 +125,6 @@ func (in *IonosCloudCluster) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IonosCloudClusterDeprecatedStatus) DeepCopyInto(out *IonosCloudClusterDeprecatedStatus) { - *out = *in - if in.V1Beta1 != nil { - in, out := &in.V1Beta1, &out.V1Beta1 - *out = new(IonosCloudClusterV1Beta1DeprecatedStatus) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudClusterDeprecatedStatus. -func (in *IonosCloudClusterDeprecatedStatus) DeepCopy() *IonosCloudClusterDeprecatedStatus { - if in == nil { - return nil - } - out := new(IonosCloudClusterDeprecatedStatus) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IonosCloudClusterInitializationStatus) DeepCopyInto(out *IonosCloudClusterInitializationStatus) { *out = *in @@ -243,11 +222,6 @@ func (in *IonosCloudClusterStatus) DeepCopyInto(out *IonosCloudClusterStatus) { *out = new(ProvisioningRequest) **out = **in } - if in.Deprecated != nil { - in, out := &in.Deprecated, &out.Deprecated - *out = new(IonosCloudClusterDeprecatedStatus) - (*in).DeepCopyInto(*out) - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudClusterStatus. @@ -350,28 +324,6 @@ func (in *IonosCloudClusterTemplateSpec) DeepCopy() *IonosCloudClusterTemplateSp return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IonosCloudClusterV1Beta1DeprecatedStatus) DeepCopyInto(out *IonosCloudClusterV1Beta1DeprecatedStatus) { - *out = *in - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make(v1beta2.Conditions, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudClusterV1Beta1DeprecatedStatus. -func (in *IonosCloudClusterV1Beta1DeprecatedStatus) DeepCopy() *IonosCloudClusterV1Beta1DeprecatedStatus { - if in == nil { - return nil - } - out := new(IonosCloudClusterV1Beta1DeprecatedStatus) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IonosCloudMachine) DeepCopyInto(out *IonosCloudMachine) { *out = *in @@ -399,26 +351,6 @@ func (in *IonosCloudMachine) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IonosCloudMachineDeprecatedStatus) DeepCopyInto(out *IonosCloudMachineDeprecatedStatus) { - *out = *in - if in.V1Beta1 != nil { - in, out := &in.V1Beta1, &out.V1Beta1 - *out = new(IonosCloudMachineV1Beta1DeprecatedStatus) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudMachineDeprecatedStatus. -func (in *IonosCloudMachineDeprecatedStatus) DeepCopy() *IonosCloudMachineDeprecatedStatus { - if in == nil { - return nil - } - out := new(IonosCloudMachineDeprecatedStatus) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IonosCloudMachineInitializationStatus) DeepCopyInto(out *IonosCloudMachineInitializationStatus) { *out = *in @@ -535,11 +467,6 @@ func (in *IonosCloudMachineStatus) DeepCopyInto(out *IonosCloudMachineStatus) { *out = new(ProvisioningRequest) **out = **in } - if in.Deprecated != nil { - in, out := &in.Deprecated, &out.Deprecated - *out = new(IonosCloudMachineDeprecatedStatus) - (*in).DeepCopyInto(*out) - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudMachineStatus. @@ -643,28 +570,6 @@ func (in *IonosCloudMachineTemplateSpec) DeepCopy() *IonosCloudMachineTemplateSp return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IonosCloudMachineV1Beta1DeprecatedStatus) DeepCopyInto(out *IonosCloudMachineV1Beta1DeprecatedStatus) { - *out = *in - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make(v1beta2.Conditions, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudMachineV1Beta1DeprecatedStatus. -func (in *IonosCloudMachineV1Beta1DeprecatedStatus) DeepCopy() *IonosCloudMachineV1Beta1DeprecatedStatus { - if in == nil { - return nil - } - out := new(IonosCloudMachineV1Beta1DeprecatedStatus) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MachineNetworkInfo) DeepCopyInto(out *MachineNetworkInfo) { *out = *in diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml index 96a9bf71..eb2515ce 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml @@ -250,78 +250,6 @@ spec: description: CurrentRequestByDatacenter maps data center IDs to a pending provisioning request made during reconciliation. type: object - deprecated: - description: Deprecated groups all status fields deprecated and scheduled - for removal when v1beta1 contract support is dropped. - properties: - v1beta1: - description: V1Beta1 groups all v1beta1 status fields that are - deprecated and scheduled for removal. - properties: - conditions: - description: |- - Conditions defines current service state of the IonosCloudCluster using the deprecated v1beta1 condition type. - - Deprecated: Use the top-level conditions field instead. - items: - description: Condition defines an observation of a Cluster - API resource operational state. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when - the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This field may be empty. - maxLength: 10240 - minLength: 1 - type: string - reason: - description: |- - reason is the reason for the condition's last transition in CamelCase. - The specific API may choose whether or not this field is considered a guaranteed API. - This field may be empty. - maxLength: 256 - minLength: 1 - type: string - severity: - description: |- - severity provides an explicit classification of Reason code, so the users or machines can immediately - understand the current situation and act accordingly. - The Severity field MUST be set only when Status=False. - maxLength: 32 - type: string - status: - description: status of the condition, one of True, False, - Unknown. - type: string - type: - description: |- - type of condition in CamelCase or in foo.example.com/CamelCase. - Many .condition.type values are consistent across resources like Available, but because arbitrary conditions - can be useful (see .node.status.conditions), the ability to deconflict is important. - maxLength: 256 - minLength: 1 - type: string - required: - - lastTransitionTime - - status - - type - type: object - type: array - ready: - description: |- - Ready indicates that the cluster is ready. - - Deprecated: Use Initialization.Provisioned instead. - type: boolean - type: object - type: object initialization: description: |- Initialization provides observations of the IonosCloudCluster initialization process. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml index 1dcafa4b..7d7170cb 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml @@ -462,78 +462,6 @@ spec: - method - requestPath type: object - deprecated: - description: Deprecated groups all status fields deprecated and scheduled - for removal when v1beta1 contract support is dropped. - properties: - v1beta1: - description: V1Beta1 groups all v1beta1 status fields that are - deprecated and scheduled for removal. - properties: - conditions: - description: |- - Conditions defines current service state of the IonosCloudMachine using the deprecated v1beta1 condition type. - - Deprecated: Use the top-level conditions field instead. - items: - description: Condition defines an observation of a Cluster - API resource operational state. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when - the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This field may be empty. - maxLength: 10240 - minLength: 1 - type: string - reason: - description: |- - reason is the reason for the condition's last transition in CamelCase. - The specific API may choose whether or not this field is considered a guaranteed API. - This field may be empty. - maxLength: 256 - minLength: 1 - type: string - severity: - description: |- - severity provides an explicit classification of Reason code, so the users or machines can immediately - understand the current situation and act accordingly. - The Severity field MUST be set only when Status=False. - maxLength: 32 - type: string - status: - description: status of the condition, one of True, False, - Unknown. - type: string - type: - description: |- - type of condition in CamelCase or in foo.example.com/CamelCase. - Many .condition.type values are consistent across resources like Available, but because arbitrary conditions - can be useful (see .node.status.conditions), the ability to deconflict is important. - maxLength: 256 - minLength: 1 - type: string - required: - - lastTransitionTime - - status - - type - type: object - type: array - ready: - description: |- - Ready indicates the VM has been provisioned and is ready. - - Deprecated: Use Initialization.Provisioned instead. - type: boolean - type: object - type: object initialization: description: |- Initialization provides observations of the IonosCloudMachine initialization process. From f18c283246304272181bdf8455ab971ddde9c9be Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 17 Jul 2026 10:45:11 +0000 Subject: [PATCH 63/94] feat(metadata): declare v1beta2 contract for v0.8 Co-Authored-By: Claude Opus 4.8 --- README.md | 7 +++++++ metadata.yaml | 3 +++ 2 files changed, 10 insertions(+) diff --git a/README.md b/README.md index e6439498..27af55c7 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,13 @@ This provider's versions are compatible with the following versions of Cluster A | CAPIC v1alpha1 (v0.5) | ✓ | ☓ | ☓ | ☓ | | CAPIC v1alpha1 (v0.6) | ✓ | ✓ | ✓ | ✓ | | CAPIC v1alpha1 (v0.7) | ☓ | ☓ | ☓ | ✓ | +| CAPIC v1alpha1 (v0.8) | ☓ | ☓ | ☓ | ✓ | + +> **v0.8 declares the Cluster API `v1beta2` contract and drops `v1beta1`.** It requires a +> `v1beta2`-capable core (CAPI v1.11+). The deprecated `status.ready` and v1beta1 +> `status.conditions` fields are removed — read `status.initialization.provisioned` and +> `status.conditions` instead. Upgrade existing clusters via the staged path +> `v0.6 → v0.7 → v0.8`. ### Kubernetes Versions diff --git a/metadata.yaml b/metadata.yaml index 9be90add..93072ee4 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -2,6 +2,9 @@ apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3 kind: Metadata releaseSeries: +- major: 0 + minor: 8 + contract: v1beta2 - major: 0 minor: 7 contract: v1beta1 From 6e59e1df1a83040925bcc7f290b1217c637695f6 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 17 Jul 2026 10:48:43 +0000 Subject: [PATCH 64/94] docs: rewrite capi upgrade plan for pure-v1beta2 v0.8 Co-Authored-By: Claude Opus 4.8 --- docs/capi-upgrade-plan.md | 46 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 docs/capi-upgrade-plan.md diff --git a/docs/capi-upgrade-plan.md b/docs/capi-upgrade-plan.md new file mode 100644 index 00000000..df809ca8 --- /dev/null +++ b/docs/capi-upgrade-plan.md @@ -0,0 +1,46 @@ +# Cluster API upgrade & v1beta2 migration + +How CAPIC moves to the Cluster API **v1beta2** contract, and how the release series line up. + +## Release timeline (actual) + +| CAPIC | CAPI | Declared contract | Implements v1beta2 | Notes | +|-------|------|-------------------|--------------------|-------| +| v0.6.x | v1.8 | v1beta1 | no | pure v1beta1 | +| v0.7.0 | v1.10 | v1beta1 | no | pure v1beta1 (CAPI dependency bump only) | +| **v0.8** | **v1.11** | **v1beta2** | yes | **pure v1beta2 — v1beta1 support dropped** | + +There is no dual-writing "bridge" release: v0.7.0 shipped as pure v1beta1, so v0.8 is the +first release to implement v1beta2, and it does so as a clean cut. + +## What v0.8 changes + +- Declares the **v1beta2** contract (`metadata.yaml`), matching the CRD + `cluster.x-k8s.io/v1beta2` annotation. +- Implements the v1beta2 status contract: `status.initialization.provisioned` and + `status.conditions` (`[]metav1.Condition`). +- **Removes** all v1beta1 residue: the `status.deprecated.v1beta1.*` fields, the + `Get/SetV1Beta1Conditions` / `SetV1Beta1Ready` helpers, and the v1beta1 condition + ownership in the patch helpers. + +## Supported upgrade path + +Staged, in place: **v0.6 → v0.7 → v0.8** (upgrade the CAPI core to v1.11+ alongside v0.8). + +- Existing clusters keep running; **no recreation** is required (no `IonosCloudMachineTemplate` + content change → no Machine rollout; running Machines are not torn down by a transient + `provisioned=nil`). +- On the `v0.7 → v0.8` hop there is a brief window where existing objects report infra + not-provisioned until the v0.8 controller reconciles them (self-heals on controller startup). +- **Breaking:** `.status.ready` and the v1beta1 `status.conditions` are removed. Any external + tooling reading them must move to `status.initialization.provisioned` / `status.conditions`. + +Empirical validation lives in the e2e upgrade suite (PR #379), which exercises the staged +`v0.6.3 → v0.7.0 → v0.8` path. + +## References + +- CAPI v1.11 → v1.12 provider migration guide: + https://cluster-api.sigs.k8s.io/developer/providers/migrations/v1.11-to-v1.12 +- Design spec: `docs/superpowers/specs/2026-07-17-drop-v1beta1-pure-v1beta2-design.md` +- Compatibility matrix: `README.md` From 1d25d7f056dc5982da33e8458994fc0692c4a609 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 17 Jul 2026 11:05:22 +0000 Subject: [PATCH 65/94] chore: address final review findings (README v0.7 row, dead lint alias, upgrade-plan reference) Fix three minor findings from the SP1 pure-v1beta2 whole-branch review: correct the CAPIC v0.7 compatibility row to match the v1.10.10 base documented in the upgrade plan, remove the now-unused v1beta1 conditions importas alias, and link the v1.10-to-v1.11 migration guide that introduced the v1beta2 contract. Co-Authored-By: Claude Opus 4.8 --- .golangci.yml | 2 -- README.md | 2 +- docs/capi-upgrade-plan.md | 2 ++ 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 3cce430c..40f0fdfd 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -79,8 +79,6 @@ linters: alias: capie2e - pkg: sigs.k8s.io/cluster-api/util/conditions alias: conditions - - pkg: sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1 - alias: deprecatedv1beta1conditions - pkg: sigs.k8s.io/cluster-api/api/ipam/v1beta2 alias: ipamv1 - pkg: github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1 diff --git a/README.md b/README.md index 27af55c7..fbe7c5e5 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ This provider's versions are compatible with the following versions of Cluster A | CAPIC v1alpha1 (v0.4) | ✓ | ☓ | ☓ | ☓ | | CAPIC v1alpha1 (v0.5) | ✓ | ☓ | ☓ | ☓ | | CAPIC v1alpha1 (v0.6) | ✓ | ✓ | ✓ | ✓ | -| CAPIC v1alpha1 (v0.7) | ☓ | ☓ | ☓ | ✓ | +| CAPIC v1alpha1 (v0.7) | ☓ | ☓ | ✓ | ✓ | | CAPIC v1alpha1 (v0.8) | ☓ | ☓ | ☓ | ✓ | > **v0.8 declares the Cluster API `v1beta2` contract and drops `v1beta1`.** It requires a diff --git a/docs/capi-upgrade-plan.md b/docs/capi-upgrade-plan.md index df809ca8..133cd5af 100644 --- a/docs/capi-upgrade-plan.md +++ b/docs/capi-upgrade-plan.md @@ -40,6 +40,8 @@ Empirical validation lives in the e2e upgrade suite (PR #379), which exercises t ## References +- CAPI v1.10 → v1.11 provider migration guide: + https://cluster-api.sigs.k8s.io/developer/providers/migrations/v1.10-to-v1.11 - CAPI v1.11 → v1.12 provider migration guide: https://cluster-api.sigs.k8s.io/developer/providers/migrations/v1.11-to-v1.12 - Design spec: `docs/superpowers/specs/2026-07-17-drop-v1beta1-pure-v1beta2-design.md` From 6805dcbfa3e5ec4c738eaadf63a353c1e769f804 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 31 Jul 2026 11:34:40 +0000 Subject: [PATCH 66/94] fix(api): declare v1beta2 contract and backfill legacy condition reasons Resolve the open question from the SP1 design spec: the e2e config and release metadata now declare v1.11 as contract v1beta2, matching the CRD kustomize label (the Go +kubebuilder annotation is inert for clusterctl's contract resolution, per docs/capi-upgrade-plan.md). Add api/v1alpha1/conditions_migration.go to backfill an empty condition Reason on objects written by CAPIC <= v0.7 under the old v1beta1 conditions API. The v1beta2 metav1.Condition CRD schema requires a non-empty Reason, and verified against a real apiserver (envtest, CRD swapped in place), a fresh Create of the legacy zero-value shape (e.g. clusterctl move) is rejected without this backfill. Wire it into scope.NewCluster/NewMachine before the patch helper's diff is computed, and add omitzero to ControlPlaneEndpoint/ObjectMeta so v0.8 controller writes never re-emit the legacy zero-value shape that trips the new schema. Co-Authored-By: Claude Opus 4.8 --- api/v1alpha1/conditions_migration.go | 40 +++++++++++++++++++ api/v1alpha1/conditions_migration_test.go | 38 ++++++++++++++++++ api/v1alpha1/ionoscloudcluster_types.go | 2 +- .../ionoscloudmachinetemplate_types.go | 2 +- docs/capi-upgrade-plan.md | 21 ++++++++-- scope/cluster.go | 5 +++ scope/machine.go | 6 +++ test/e2e/config/ionoscloud.yaml | 6 +-- test/e2e/data/shared/v1.11/metadata.yaml | 2 +- 9 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 api/v1alpha1/conditions_migration.go create mode 100644 api/v1alpha1/conditions_migration_test.go diff --git a/api/v1alpha1/conditions_migration.go b/api/v1alpha1/conditions_migration.go new file mode 100644 index 00000000..dd06a4a2 --- /dev/null +++ b/api/v1alpha1/conditions_migration.go @@ -0,0 +1,40 @@ +/* +Copyright 2024 IONOS Cloud. + +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 v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// LegacyConditionMigratedReason is backfilled onto conditions written by CAPIC <= v0.7, which used +// the v1beta1 conditions API and left Reason unset. The v1beta2 metav1.Condition CRD schema requires +// a non-empty, CamelCase Reason; without backfilling it, the first status patch issued against a +// pre-upgrade object after upgrading to v0.8 fails, because the patch helper resends the whole +// status.conditions array, including untouched legacy entries. +const LegacyConditionMigratedReason = "MigratedFromV1Beta1" + +// BackfillLegacyConditionReasons sets LegacyConditionMigratedReason on any condition with an empty +// Reason. It mutates and returns the given slice, so it must be called before a patch helper is +// asked to compute a diff, on the same object the helper's "before" snapshot was taken from +// (otherwise the backfill would look like a no-op change to the helper and the invalid legacy +// value would still be resent verbatim). +func BackfillLegacyConditionReasons(conditions []metav1.Condition) []metav1.Condition { + for i := range conditions { + if conditions[i].Reason == "" { + conditions[i].Reason = LegacyConditionMigratedReason + } + } + return conditions +} diff --git a/api/v1alpha1/conditions_migration_test.go b/api/v1alpha1/conditions_migration_test.go new file mode 100644 index 00000000..05e0dda5 --- /dev/null +++ b/api/v1alpha1/conditions_migration_test.go @@ -0,0 +1,38 @@ +/* +Copyright 2024 IONOS Cloud. + +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 v1alpha1 + +import ( + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func Test_BackfillLegacyConditionReasons(t *testing.T) { + conditions := []metav1.Condition{ + {Type: "Ready", Status: metav1.ConditionTrue, Reason: ""}, + {Type: "MachineProvisioned", Status: metav1.ConditionTrue, Reason: "Provisioned"}, + } + + got := BackfillLegacyConditionReasons(conditions) + + require.Equal(t, LegacyConditionMigratedReason, got[0].Reason, + "a condition written by CAPIC <= v0.7 (empty Reason) must be backfilled") + require.Equal(t, "Provisioned", got[1].Reason, + "a condition that already carries a Reason must be left untouched") +} diff --git a/api/v1alpha1/ionoscloudcluster_types.go b/api/v1alpha1/ionoscloudcluster_types.go index 4b15f702..123f44fc 100644 --- a/api/v1alpha1/ionoscloudcluster_types.go +++ b/api/v1alpha1/ionoscloudcluster_types.go @@ -46,7 +46,7 @@ type IonosCloudClusterSpec struct { // // TODO(gfariasalves): as of now, IP must be provided by the user as we still don't insert the // provider-provided block IP into the kube-vip manifest. - ControlPlaneEndpoint clusterv1.APIEndpoint `json:"controlPlaneEndpoint,omitempty"` + ControlPlaneEndpoint clusterv1.APIEndpoint `json:"controlPlaneEndpoint,omitempty,omitzero"` // Location is the location where the data centers should be located. // diff --git a/api/v1alpha1/ionoscloudmachinetemplate_types.go b/api/v1alpha1/ionoscloudmachinetemplate_types.go index b934601f..0a1d0937 100644 --- a/api/v1alpha1/ionoscloudmachinetemplate_types.go +++ b/api/v1alpha1/ionoscloudmachinetemplate_types.go @@ -51,7 +51,7 @@ type IonosCloudMachineTemplateResource struct { // Standard object's metadata. // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata //+optional - ObjectMeta clusterv1.ObjectMeta `json:"metadata,omitempty"` + ObjectMeta clusterv1.ObjectMeta `json:"metadata,omitempty,omitzero"` // Spec is the IonosCloudMachineSpec for the IonosCloudMachineTemplate. Spec IonosCloudMachineSpec `json:"spec"` } diff --git a/docs/capi-upgrade-plan.md b/docs/capi-upgrade-plan.md index 133cd5af..f36a5f36 100644 --- a/docs/capi-upgrade-plan.md +++ b/docs/capi-upgrade-plan.md @@ -16,12 +16,13 @@ first release to implement v1beta2, and it does so as a clean cut. ## What v0.8 changes - Declares the **v1beta2** contract (`metadata.yaml`), matching the CRD - `cluster.x-k8s.io/v1beta2` annotation. + `cluster.x-k8s.io/v1beta2` label set via `config/crd/kustomization.yaml`'s `commonLabels`. + (The `+kubebuilder:metadata:annotations` marker on the Go types produces an *annotation*, + which CAPI's contract resolution does not consult — the label is what's load-bearing.) - Implements the v1beta2 status contract: `status.initialization.provisioned` and `status.conditions` (`[]metav1.Condition`). -- **Removes** all v1beta1 residue: the `status.deprecated.v1beta1.*` fields, the - `Get/SetV1Beta1Conditions` / `SetV1Beta1Ready` helpers, and the v1beta1 condition - ownership in the patch helpers. +- **Removes** `status.ready` and the v1beta1 `status.conditions` shape in place, on the same + `v1alpha1` CRD apiVersion (no new stored version, no conversion webhook). ## Supported upgrade path @@ -34,6 +35,18 @@ Staged, in place: **v0.6 → v0.7 → v0.8** (upgrade the CAPI core to v1.11+ al not-provisioned until the v0.8 controller reconciles them (self-heals on controller startup). - **Breaking:** `.status.ready` and the v1beta1 `status.conditions` are removed. Any external tooling reading them must move to `status.initialization.provisioned` / `status.conditions`. +- **Residual risk of the in-place schema change:** objects written by v0.6/v0.7 can have + `spec.controlPlaneEndpoint: {host: "", port: 0}` and conditions with an empty `reason` — both + valid under the old schema, both rejected by the new one. The v0.8 controller backfills + condition reasons and omits a zero `controlPlaneEndpoint` on its own writes + (`api/v1alpha1/conditions_migration.go`, the `omitzero` tag on `ControlPlaneEndpoint`), and an + in-place `Update` of an untouched legacy endpoint keeps working (Kubernetes CRD validation + ratcheting allows an unchanged, already-invalid field to persist across an update). What still + breaks: a fresh `Create` of the legacy zero-value shape — e.g. `clusterctl move`, which reads + the object on the source cluster and re-creates it on the target, where there is no stored + value to ratchet against. Verified empirically against a real apiserver (envtest, CRD swapped + in place): a status-only patch of a legacy object round-trips cleanly, an in-place spec `Update` + of the untouched endpoint succeeds, but a `Create` carrying the same legacy shape is rejected. Empirical validation lives in the e2e upgrade suite (PR #379), which exercises the staged `v0.6.3 → v0.7.0 → v0.8` path. diff --git a/scope/cluster.go b/scope/cluster.go index 556c0d52..2b35ff40 100644 --- a/scope/cluster.go +++ b/scope/cluster.go @@ -92,6 +92,11 @@ func NewCluster(params ClusterParams) (*Cluster, error) { return nil, fmt.Errorf("failed to init patch helper: %w", err) } + // Backfill any condition left over from CAPIC <= v0.7 (empty Reason under the old v1beta1 + // conditions API) now that the helper's "before" snapshot has already been captured, so the + // backfill shows up as a genuine, patchable change instead of being resent verbatim. + params.IonosCluster.Status.Conditions = infrav1.BackfillLegacyConditionReasons(params.IonosCluster.Status.Conditions) + clusterScope := &Cluster{ client: params.Client, Cluster: params.Cluster, diff --git a/scope/machine.go b/scope/machine.go index fc7c26b2..b98039df 100644 --- a/scope/machine.go +++ b/scope/machine.go @@ -85,6 +85,12 @@ func NewMachine(params MachineParams) (*Machine, error) { if err != nil { return nil, fmt.Errorf("failed to init patch helper: %w", err) } + + // Backfill any condition left over from CAPIC <= v0.7 (empty Reason under the old v1beta1 + // conditions API) now that the helper's "before" snapshot has already been captured, so the + // backfill shows up as a genuine, patchable change instead of being resent verbatim. + params.IonosMachine.Status.Conditions = infrav1.BackfillLegacyConditionReasons(params.IonosMachine.Status.Conditions) + return &Machine{ client: params.Client, patchHelper: helper, diff --git a/test/e2e/config/ionoscloud.yaml b/test/e2e/config/ionoscloud.yaml index e8ff9524..f13732f9 100644 --- a/test/e2e/config/ionoscloud.yaml +++ b/test/e2e/config/ionoscloud.yaml @@ -8,7 +8,7 @@ providers: - name: "v1.11.11" value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/core-components.yaml" type: url - contract: v1beta1 + contract: v1beta2 replacements: - old: --metrics-addr=127.0.0.1:8080 new: --metrics-addr=:8443 @@ -20,7 +20,7 @@ providers: - name: "v1.11.11" value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/bootstrap-components.yaml" type: url - contract: v1beta1 + contract: v1beta2 replacements: - old: --metrics-addr=127.0.0.1:8080 new: --metrics-addr=:8443 @@ -32,7 +32,7 @@ providers: - name: "v1.11.11" value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/control-plane-components.yaml" type: url - contract: v1beta1 + contract: v1beta2 replacements: - old: --metrics-addr=127.0.0.1:8080 new: --metrics-addr=:8443 diff --git a/test/e2e/data/shared/v1.11/metadata.yaml b/test/e2e/data/shared/v1.11/metadata.yaml index 8755a669..0249344b 100644 --- a/test/e2e/data/shared/v1.11/metadata.yaml +++ b/test/e2e/data/shared/v1.11/metadata.yaml @@ -3,7 +3,7 @@ kind: Metadata releaseSeries: - major: 1 minor: 11 - contract: v1beta1 + contract: v1beta2 - major: 1 minor: 10 contract: v1beta1 From f2864f6a859c0aa832f0b609df650037a0092bf3 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 31 Jul 2026 13:25:07 +0000 Subject: [PATCH 67/94] fix(deps): bump golang.org/x/text and google.golang.org/grpc Trivy flags two HIGH vulnerabilities in the built image: CVE-2026-56852 (x/text norm.Iter infinite loop) and GHSA-hrxh-6v49-42gf (grpc xDS RBAC/ HTTP2). Both have fixed releases upstream; bump to pick them up. Co-Authored-By: Claude Sonnet 5 --- go.mod | 10 +++++----- go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index d6784240..3d5734f3 100644 --- a/go.mod +++ b/go.mod @@ -124,13 +124,13 @@ require ( golang.org/x/sync v0.21.0 // indirect golang.org/x/sys v0.46.0 // indirect golang.org/x/term v0.44.0 // indirect - golang.org/x/text v0.38.0 // indirect + golang.org/x/text v0.39.0 // indirect golang.org/x/time v0.11.0 // indirect - golang.org/x/tools v0.45.0 // indirect + golang.org/x/tools v0.47.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect - google.golang.org/grpc v1.81.1 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect + google.golang.org/grpc v1.82.1 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index 371dc7eb..157a58de 100644 --- a/go.sum +++ b/go.sum @@ -341,16 +341,16 @@ golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= -golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -359,12 +359,12 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4= -google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= -google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From ccd762a6e286c5b4ef290498dd6cfedcc6b59975 Mon Sep 17 00:00:00 2001 From: Stefan Scheglmann Date: Fri, 31 Jul 2026 13:25:14 +0000 Subject: [PATCH 68/94] fix(e2e): bump ionoscloud provider version to v0.8.99 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit metadata.yaml now declares a {major: 0, minor: 8, contract: v1beta2} release series, but the e2e config still built the local repository under the stale v0.6.99 tag. clusterctl's local-repository "latest" resolution cross-checks the metadata against CAPI core's own contract (v1beta2) and, on a major/minor mismatch, searches for a v0.8.x directory that never gets built — failing clusterctl init with "failed to find releases tagged with a valid semantic version number". Follow the repo's established per-release convention (v0.5.99->v0.6.99 at the v0.6.0 prep commit, etc.) and bump to v0.8.99 to match. Co-Authored-By: Claude Sonnet 5 --- go.mod | 2 +- go.sum | 4 ++-- test/e2e/config/ionoscloud.yaml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 3d5734f3..736bc39d 100644 --- a/go.mod +++ b/go.mod @@ -118,7 +118,7 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.53.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect - golang.org/x/mod v0.36.0 // indirect + golang.org/x/mod v0.37.0 // indirect golang.org/x/net v0.56.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.21.0 // indirect diff --git a/go.sum b/go.sum index 157a58de..8f939eb2 100644 --- a/go.sum +++ b/go.sum @@ -310,8 +310,8 @@ golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWB golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= -golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= diff --git a/test/e2e/config/ionoscloud.yaml b/test/e2e/config/ionoscloud.yaml index f13732f9..dba90b7b 100644 --- a/test/e2e/config/ionoscloud.yaml +++ b/test/e2e/config/ionoscloud.yaml @@ -54,7 +54,7 @@ providers: - name: ionoscloud type: InfrastructureProvider versions: - - name: v0.7.99 + - name: v0.8.99 value: "../../../config/default" replacements: - old: ghcr.io/ionos-cloud/cluster-api-provider-ionoscloud:dev From 075a0d3c73421c8239a144e9e88114e79a9b77c5 Mon Sep 17 00:00:00 2001 From: Max Neverov Date: Wed, 12 Aug 2026 14:54:26 +0200 Subject: [PATCH 69/94] fix: increase wait period for IONOS resources to become stable. Only applies to e2e tests. --- test/e2e/config/ionoscloud.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/test/e2e/config/ionoscloud.yaml b/test/e2e/config/ionoscloud.yaml index dba90b7b..1d44dbea 100644 --- a/test/e2e/config/ionoscloud.yaml +++ b/test/e2e/config/ionoscloud.yaml @@ -94,6 +94,7 @@ intervals: default/wait-worker-nodes: [ "30m", "10s" ] default/wait-delete-cluster: [ "30m", "10s" ] default/wait-nodes-ready: ["30m", "10s"] + default/wait-resource-versions-become-stable: ["5m", "10s"] scale/wait-cluster: ["30m", "10s"] scale/wait-control-plane: ["30m", "10s"] scale/wait-worker-nodes: ["30m", "10s"] From 0934f94bb6b1d8b35b92a8d3132e7129e37f6fd6 Mon Sep 17 00:00:00 2001 From: Max Neverov Date: Wed, 12 Aug 2026 17:39:23 +0200 Subject: [PATCH 70/94] fix: go mod tidy --- go.mod | 4 +++- go.sum | 44 ++++++++++++++++++++++---------------------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/go.mod b/go.mod index 80b731ca..6bdf4a50 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( require ( al.essio.dev/pkg/shellescape v1.5.1 // indirect cel.dev/expr v0.25.1 // indirect + dario.cat/mergo v1.0.1 // indirect github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect @@ -67,7 +68,7 @@ require ( github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-github/v53 v53.2.0 // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 // indirect + github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -111,6 +112,7 @@ require ( github.com/stoewer/go-strcase v1.3.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/valyala/fastjson v1.6.4 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect diff --git a/go.sum b/go.sum index 6b8ef4a1..9da0feca 100644 --- a/go.sum +++ b/go.sum @@ -129,8 +129,8 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY= -github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -143,10 +143,10 @@ github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/ionos-cloud/sdk-go/v6 v6.3.8 h1:CUZzrNciLM2IlmZtnclIznjST29tAYQbtQ8epiX5RUo= -github.com/ionos-cloud/sdk-go/v6 v6.3.8/go.mod h1:nUGHP4kZHAZngCVr4v6C8nuargFrtvt7GrzH/hqn7c4= -github.com/jarcoal/httpmock v1.4.0 h1:BvhqnH0JAYbNudL2GMJKgOHe2CtKlzJ/5rWKyp+hc2k= -github.com/jarcoal/httpmock v1.4.0/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0= +github.com/ionos-cloud/sdk-go/v6 v6.3.11 h1:m4mX03vflm631znIPutojJxLZvxD5Mt3xv/soy0kLIc= +github.com/ionos-cloud/sdk-go/v6 v6.3.11/go.mod h1:nUGHP4kZHAZngCVr4v6C8nuargFrtvt7GrzH/hqn7c4= +github.com/jarcoal/httpmock v1.4.2 h1:dKwiP/9zITCPfBLsDn3kchbSOu16JrnxtVEmL0fPRcI= +github.com/jarcoal/httpmock v1.4.2/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= @@ -206,10 +206,10 @@ github.com/olekukonko/ll v0.1.1 h1:9Dfeed5/Mgaxb9lHRAftLK9pVfYETvHn+If6lywVhJc= github.com/olekukonko/ll v0.1.1/go.mod h1:2dJo+hYZcJMLMbKwHEWvxCUbAOLc/CXWS9noET22Mdo= github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8= github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= -github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= -github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= -github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= -github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +github.com/onsi/ginkgo/v2 v2.32.0 h1:Hw7s2pVrQo/8Yz5N77qdnpHaoc+c6cC9WIV1Jce+J6E= +github.com/onsi/ginkgo/v2 v2.32.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= +github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I= +github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -331,8 +331,8 @@ golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWB golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= -golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -361,16 +361,16 @@ golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= -golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -379,12 +379,12 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4= -google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= -google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 3ef274bed5c6214a8dc52d10cd9b4d176275c458 Mon Sep 17 00:00:00 2001 From: Max Neverov Date: Wed, 12 Aug 2026 18:37:54 +0200 Subject: [PATCH 71/94] fix: drop AI instructions + superpowers --- docs/capi-upgrade-plan.md | 61 -- .../2026-07-17-sp1-drop-v1beta1-clean-cut.md | 830 ------------------ ...-07-17-drop-v1beta1-pure-v1beta2-design.md | 163 ---- 3 files changed, 1054 deletions(-) delete mode 100644 docs/capi-upgrade-plan.md delete mode 100644 docs/superpowers/plans/2026-07-17-sp1-drop-v1beta1-clean-cut.md delete mode 100644 docs/superpowers/specs/2026-07-17-drop-v1beta1-pure-v1beta2-design.md diff --git a/docs/capi-upgrade-plan.md b/docs/capi-upgrade-plan.md deleted file mode 100644 index f36a5f36..00000000 --- a/docs/capi-upgrade-plan.md +++ /dev/null @@ -1,61 +0,0 @@ -# Cluster API upgrade & v1beta2 migration - -How CAPIC moves to the Cluster API **v1beta2** contract, and how the release series line up. - -## Release timeline (actual) - -| CAPIC | CAPI | Declared contract | Implements v1beta2 | Notes | -|-------|------|-------------------|--------------------|-------| -| v0.6.x | v1.8 | v1beta1 | no | pure v1beta1 | -| v0.7.0 | v1.10 | v1beta1 | no | pure v1beta1 (CAPI dependency bump only) | -| **v0.8** | **v1.11** | **v1beta2** | yes | **pure v1beta2 — v1beta1 support dropped** | - -There is no dual-writing "bridge" release: v0.7.0 shipped as pure v1beta1, so v0.8 is the -first release to implement v1beta2, and it does so as a clean cut. - -## What v0.8 changes - -- Declares the **v1beta2** contract (`metadata.yaml`), matching the CRD - `cluster.x-k8s.io/v1beta2` label set via `config/crd/kustomization.yaml`'s `commonLabels`. - (The `+kubebuilder:metadata:annotations` marker on the Go types produces an *annotation*, - which CAPI's contract resolution does not consult — the label is what's load-bearing.) -- Implements the v1beta2 status contract: `status.initialization.provisioned` and - `status.conditions` (`[]metav1.Condition`). -- **Removes** `status.ready` and the v1beta1 `status.conditions` shape in place, on the same - `v1alpha1` CRD apiVersion (no new stored version, no conversion webhook). - -## Supported upgrade path - -Staged, in place: **v0.6 → v0.7 → v0.8** (upgrade the CAPI core to v1.11+ alongside v0.8). - -- Existing clusters keep running; **no recreation** is required (no `IonosCloudMachineTemplate` - content change → no Machine rollout; running Machines are not torn down by a transient - `provisioned=nil`). -- On the `v0.7 → v0.8` hop there is a brief window where existing objects report infra - not-provisioned until the v0.8 controller reconciles them (self-heals on controller startup). -- **Breaking:** `.status.ready` and the v1beta1 `status.conditions` are removed. Any external - tooling reading them must move to `status.initialization.provisioned` / `status.conditions`. -- **Residual risk of the in-place schema change:** objects written by v0.6/v0.7 can have - `spec.controlPlaneEndpoint: {host: "", port: 0}` and conditions with an empty `reason` — both - valid under the old schema, both rejected by the new one. The v0.8 controller backfills - condition reasons and omits a zero `controlPlaneEndpoint` on its own writes - (`api/v1alpha1/conditions_migration.go`, the `omitzero` tag on `ControlPlaneEndpoint`), and an - in-place `Update` of an untouched legacy endpoint keeps working (Kubernetes CRD validation - ratcheting allows an unchanged, already-invalid field to persist across an update). What still - breaks: a fresh `Create` of the legacy zero-value shape — e.g. `clusterctl move`, which reads - the object on the source cluster and re-creates it on the target, where there is no stored - value to ratchet against. Verified empirically against a real apiserver (envtest, CRD swapped - in place): a status-only patch of a legacy object round-trips cleanly, an in-place spec `Update` - of the untouched endpoint succeeds, but a `Create` carrying the same legacy shape is rejected. - -Empirical validation lives in the e2e upgrade suite (PR #379), which exercises the staged -`v0.6.3 → v0.7.0 → v0.8` path. - -## References - -- CAPI v1.10 → v1.11 provider migration guide: - https://cluster-api.sigs.k8s.io/developer/providers/migrations/v1.10-to-v1.11 -- CAPI v1.11 → v1.12 provider migration guide: - https://cluster-api.sigs.k8s.io/developer/providers/migrations/v1.11-to-v1.12 -- Design spec: `docs/superpowers/specs/2026-07-17-drop-v1beta1-pure-v1beta2-design.md` -- Compatibility matrix: `README.md` diff --git a/docs/superpowers/plans/2026-07-17-sp1-drop-v1beta1-clean-cut.md b/docs/superpowers/plans/2026-07-17-sp1-drop-v1beta1-clean-cut.md deleted file mode 100644 index 8df7f076..00000000 --- a/docs/superpowers/plans/2026-07-17-sp1-drop-v1beta1-clean-cut.md +++ /dev/null @@ -1,830 +0,0 @@ -# SP1 — Drop v1beta1 (pure v1beta2 v0.8) Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Make CAPIC a pure v1beta2 provider in v0.8 by declaring the v1beta2 contract and removing every v1beta1 residue from `#378` (branch `feat/upgrade-cluster-api-v1.11.11`). - -**Architecture:** This is a removal-and-declaration change on top of the existing v1beta2 implementation already on the branch. We delete the dual-write plumbing (deprecated `status.deprecated.v1beta1.*` fields, `Get/SetV1Beta1*` helpers, v1beta1 condition ownership in patch calls), flip the declared contract in `metadata.yaml`, regenerate CRDs/deepcopy, and update docs. Commits are additive (no history rewrite). - -**Tech Stack:** Go 1.26, kubebuilder v4, controller-gen (`make generate`/`make manifests`), Cluster API v1.11.11 (`sigs.k8s.io/cluster-api/api/core/v1beta2`), Ginkgo + envtest, testify. - -## Global Constraints - -- Go **1.26+**; do not change `go.mod` CAPI version (stays `sigs.k8s.io/cluster-api v1.11.11` — the v1.12 bump is PR #380, out of scope here). -- Never hand-edit generated files (`api/v1alpha1/zz_generated.deepcopy.go`, `config/crd/bases/*.yaml`); regenerate via `make generate` / `make manifests`. -- Linting is strict (`golangci.yml`, `default: none`). Run `make lint-fix` before considering a task done; unused imports are compile errors — remove them. -- Every commit message ends with the trailer line: `Co-Authored-By: Claude Opus 4.8 `. -- This is removal-heavy work: "tests" are compilation (`go build`/`go vet`), the existing suite still passing, and `grep` assertions that removed symbols are gone. Behavior that must be preserved (`Status.Initialization.Provisioned = true`) is already covered by existing controller/server tests — do not add new tests for it. -- Keep the CRD annotation `cluster.x-k8s.io/v1beta2=v1alpha1` and all v1beta2 status fields. Do NOT add a conversion webhook. Do NOT change the CRD version (`v1alpha1`). - ---- - -### Task 1: Stop writing the deprecated v1beta1 `ready` field - -**Files:** -- Modify: `internal/controller/ionoscloudcluster_controller.go` (~lines 181–189) -- Modify: `internal/service/cloud/server.go` (~lines 148–150) - -**Interfaces:** -- Consumes: nothing new. -- Produces: nothing new. After this task the deprecated helpers/fields still exist but are no longer written from the reconcile paths. - -- [ ] **Step 1: Remove the deprecated dual-write in the cluster controller** - -In `internal/controller/ionoscloudcluster_controller.go`, replace this block: - -```go - clusterScope.IonosCluster.Status.Initialization.Provisioned = new(true) - // Set deprecated v1beta1 ready field for backwards compatibility. - if clusterScope.IonosCluster.Status.Deprecated == nil { - clusterScope.IonosCluster.Status.Deprecated = &infrav1.IonosCloudClusterDeprecatedStatus{} - } - if clusterScope.IonosCluster.Status.Deprecated.V1Beta1 == nil { - clusterScope.IonosCluster.Status.Deprecated.V1Beta1 = &infrav1.IonosCloudClusterV1Beta1DeprecatedStatus{} - } - clusterScope.IonosCluster.Status.Deprecated.V1Beta1.Ready = true //nolint:staticcheck // Intentionally setting deprecated field for v1beta1 backwards compatibility. - return ctrl.Result{}, nil -``` - -with: - -```go - clusterScope.IonosCluster.Status.Initialization.Provisioned = new(true) - return ctrl.Result{}, nil -``` - -- [ ] **Step 2: Remove the deprecated dual-write in the server service** - -In `internal/service/cloud/server.go`, replace: - -```go - ms.IonosMachine.Status.Initialization.Provisioned = new(true) - // Set deprecated v1beta1 ready field for backwards compatibility. - ms.IonosMachine.SetV1Beta1Ready(true) - conditions.Set(ms.IonosMachine, metav1.Condition{ -``` - -with: - -```go - ms.IonosMachine.Status.Initialization.Provisioned = new(true) - conditions.Set(ms.IonosMachine, metav1.Condition{ -``` - -- [ ] **Step 3: Verify it compiles and vets** - -Run: `go build ./... && go vet ./internal/...` -Expected: no output, exit 0. - -- [ ] **Step 4: Run the affected unit tests** - -Run: `go test ./internal/controller/... ./internal/service/cloud/... -short` -Expected: `ok` for both packages (no test asserts the deprecated `ready` write). - -- [ ] **Step 5: Commit** - -```bash -git add internal/controller/ionoscloudcluster_controller.go internal/service/cloud/server.go -git commit -m "refactor: stop writing deprecated v1beta1 ready field - -Co-Authored-By: Claude Opus 4.8 " -``` - ---- - -### Task 2: Drop v1beta1 condition ownership from the scope patch calls - -**Files:** -- Modify: `scope/cluster.go` (~lines 39–47 and ~230–242) -- Modify: `scope/machine.go` (~lines 38–46 and ~197–209) - -**Interfaces:** -- Consumes: `patch.WithOwnedConditions` (unchanged; kept). -- Produces: `PatchObject` on both scopes now declares only v1beta2 condition ownership. - -- [ ] **Step 1: Simplify the `ownedClusterConditions` doc comment** - -In `scope/cluster.go`, replace: - -```go -// ownedClusterConditions is the single source of truth for the v2 condition types owned by the -// cluster controller. WithOwnedV1Beta1Conditions is derived from this list (only ReadyCondition -// is carried into v1beta1; provider-specific conditions are v2-only). Keeping both lists adjacent -// and derived from one variable makes it structurally impossible to add a condition to one side -// while forgetting the other. -var ownedClusterConditions = []string{ -``` - -with: - -```go -// ownedClusterConditions is the single source of truth for the condition types owned by the -// cluster controller. -var ownedClusterConditions = []string{ -``` - -- [ ] **Step 2: Remove the v1beta1 ownership option from the cluster patch call** - -In `scope/cluster.go`, replace: - -```go - // V1Beta1 ownership covers only the cross-cutting Ready condition; provider-specific - // conditions (IonosCloudClusterReady) are v2-only and intentionally omitted here. - if patchErr := c.patchHelper.Patch(timeoutCtx, c.IonosCluster, - patch.WithOwnedV1Beta1Conditions{ - Conditions: []clusterv1.ConditionType{clusterv1.ReadyCondition}, - }, - // V2 ownership is derived from ownedClusterConditions — the single source of truth. - patch.WithOwnedConditions{ - Conditions: ownedClusterConditions, - }, - ); patchErr != nil { - return patchErr - } -``` - -with: - -```go - // Ownership is derived from ownedClusterConditions — the single source of truth. - if patchErr := c.patchHelper.Patch(timeoutCtx, c.IonosCluster, - patch.WithOwnedConditions{ - Conditions: ownedClusterConditions, - }, - ); patchErr != nil { - return patchErr - } -``` - -- [ ] **Step 3: Simplify the `ownedMachineConditions` doc comment** - -In `scope/machine.go`, replace: - -```go -// ownedMachineConditions is the single source of truth for the v2 condition types owned by the -// machine controller. WithOwnedV1Beta1Conditions is derived from this list (only ReadyCondition -// is carried into v1beta1; provider-specific conditions are v2-only). Keeping both lists adjacent -// and derived from one variable makes it structurally impossible to add a condition to one side -// while forgetting the other. -var ownedMachineConditions = []string{ -``` - -with: - -```go -// ownedMachineConditions is the single source of truth for the condition types owned by the -// machine controller. -var ownedMachineConditions = []string{ -``` - -- [ ] **Step 4: Remove the v1beta1 ownership option from the machine patch call** - -In `scope/machine.go`, replace: - -```go - if patchErr := m.patchHelper.Patch( - timeoutCtx, - m.IonosMachine, - // V1Beta1 ownership covers only the cross-cutting Ready condition; provider-specific - // conditions (MachineProvisionedCondition) are v2-only and intentionally omitted here. - patch.WithOwnedV1Beta1Conditions{Conditions: []clusterv1.ConditionType{ - clusterv1.ReadyCondition, - }}, - // V2 ownership is derived from ownedMachineConditions — the single source of truth. - patch.WithOwnedConditions{Conditions: ownedMachineConditions}, - ); patchErr != nil { - return patchErr - } -``` - -with: - -```go - if patchErr := m.patchHelper.Patch( - timeoutCtx, - m.IonosMachine, - // Ownership is derived from ownedMachineConditions — the single source of truth. - patch.WithOwnedConditions{Conditions: ownedMachineConditions}, - ); patchErr != nil { - return patchErr - } -``` - -- [ ] **Step 5: Verify build/vet (confirms `clusterv1` still used via `ReadyCondition`)** - -Run: `go build ./scope/... && go vet ./scope/...` -Expected: no output, exit 0. (If `clusterv1` reports as unused, something else changed — it must remain, used by `string(clusterv1.ReadyCondition)` in both `owned*Conditions` and `SetSummaryCondition`.) - -- [ ] **Step 6: Run scope tests** - -Run: `go test ./scope/... -short` -Expected: `ok github.com/ionos-cloud/cluster-api-provider-ionoscloud/scope`. - -- [ ] **Step 7: Commit** - -```bash -git add scope/cluster.go scope/machine.go -git commit -m "refactor(scope): drop v1beta1 condition ownership from patch calls - -Co-Authored-By: Claude Opus 4.8 " -``` - ---- - -### Task 3: Remove the deprecated v1beta1 condition tests - -**Files:** -- Modify: `api/v1alpha1/ionoscloudcluster_types_test.go` -- Modify: `api/v1alpha1/ionoscloudmachine_types_test.go` - -**Interfaces:** -- Consumes: nothing new. -- Produces: the api test package no longer references `Get/SetV1Beta1Conditions` or `deprecatedv1beta1conditions`, so Task 4 can remove those helpers without breaking tests. - -- [ ] **Step 1: Delete the standalone `TestIonosCloudCluster_Conditions` unit test** - -In `api/v1alpha1/ionoscloudcluster_types_test.go`, delete this function entirely: - -```go -func TestIonosCloudCluster_Conditions(t *testing.T) { - conds := clusterv1.Conditions{{Type: "type"}} - cluster := &IonosCloudCluster{} - - cluster.SetV1Beta1Conditions(conds) - require.Equal(t, conds, cluster.GetV1Beta1Conditions()) -} -``` - -- [ ] **Step 2: Remove the deprecated assertions from the cluster "Status" spec** - -In the same file, inside `Context("Status", ...)`, delete these three lines (leaving the surrounding `Provisioned`/`CurrentRequestByDatacenter` assertions intact): - -- `Expect(fetched.GetV1Beta1Conditions()).To(BeEmpty())` -- `deprecatedv1beta1conditions.MarkTrue(fetched, clusterv1.ReadyCondition)` -- `Expect(fetched.GetV1Beta1Conditions()).To(HaveLen(1))` -- `Expect(deprecatedv1beta1conditions.IsTrue(fetched, clusterv1.ReadyCondition)).To(BeTrue())` - -After the edit that spec block reads: - -```go - Context("Status", func() { - It("should correctly get and set the status", func() { - By("initially having an empty status") - - cluster := defaultCluster() - Expect(k8sClient.Create(context.Background(), cluster)).To(Succeed()) - - key := client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name} - fetched := &IonosCloudCluster{} - Expect(k8sClient.Get(context.Background(), key, fetched)).To(Succeed()) - Expect(fetched.Status.Initialization.Provisioned).To(BeNil()) - Expect(fetched.Status.CurrentRequestByDatacenter).To(BeEmpty()) - - By("retrieving the cluster and setting the status") - fetched.Status.Initialization = IonosCloudClusterInitializationStatus{Provisioned: new(true)} - wantProvisionRequest := ProvisioningRequest{ - Method: "POST", - RequestPath: "/path/to/resource", - State: "QUEUED", - } - fetched.Status.CurrentRequestByDatacenter = map[string]ProvisioningRequest{ - "123": wantProvisionRequest, - } - - By("updating the cluster status") - Expect(k8sClient.Status().Update(context.Background(), fetched)).To(Succeed()) - - Expect(k8sClient.Get(context.Background(), key, fetched)).To(Succeed()) - Expect(fetched.Status.Initialization.Provisioned).To(HaveValue(BeTrue())) - Expect(fetched.Status.CurrentRequestByDatacenter).To(HaveLen(1)) - Expect(fetched.Status.CurrentRequestByDatacenter["123"]).To(Equal(wantProvisionRequest)) - - By("Removing the entry from the status again") - delete(fetched.Status.CurrentRequestByDatacenter, "123") - Expect(k8sClient.Status().Update(context.Background(), fetched)).To(Succeed()) - - Expect(k8sClient.Get(context.Background(), key, fetched)).To(Succeed()) - Expect(fetched.Status.CurrentRequestByDatacenter).To(BeEmpty()) - }) - }) -``` - -- [ ] **Step 3: Fix the cluster test imports (remove now-unused ones)** - -In `api/v1alpha1/ionoscloudcluster_types_test.go`, replace the import block: - -```go -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" - deprecatedv1beta1conditions "sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1" - "sigs.k8s.io/controller-runtime/pkg/client" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) -``` - -with (drop `testing`, `require`, `deprecatedv1beta1conditions`; keep `clusterv1` — still used by `clusterv1.APIEndpoint` in `defaultCluster`): - -```go -import ( - "context" - - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" - "sigs.k8s.io/controller-runtime/pkg/client" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) -``` - -- [ ] **Step 4: Delete the machine `Context("Conditions", ...)` spec** - -In `api/v1alpha1/ionoscloudmachine_types_test.go`, delete this entire block: - -```go - Context("Conditions", func() { - It("should correctly set and get the conditions", func() { - m := defaultMachine() - Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(k8sClient.Get( - context.Background(), client.ObjectKey{Name: m.Name, Namespace: m.Namespace}, m)).To(Succeed()) - - // Calls SetConditions with required fields - deprecatedv1beta1conditions.MarkTrue(m, MachineProvisionedCondition) - - Expect(k8sClient.Status().Update(context.Background(), m)).To(Succeed()) - Expect(k8sClient.Get(context.Background(), - client.ObjectKey{Name: m.Name, Namespace: m.Namespace}, m)).To(Succeed()) - - machineConditions := m.GetV1Beta1Conditions() - Expect(machineConditions).To(HaveLen(1)) - Expect(machineConditions[0].Type).To(Equal(MachineProvisionedCondition)) - Expect(machineConditions[0].Status).To(Equal(corev1.ConditionTrue)) - }) - }) -``` - -- [ ] **Step 5: Remove the deprecated condition set-up from the machine "Status" spec** - -In the same file, inside `Context("Status", ...)`, delete the single line: - -```go - deprecatedv1beta1conditions.MarkTrue(m, MachineProvisionedCondition) -``` - -(The `want := *m.DeepCopy()` / `cmp.Diff(want.Status, m.Status)` round-trip below stays and still passes — it now compares a status without the deprecated subtree.) - -- [ ] **Step 6: Fix the machine test imports (remove `deprecatedv1beta1conditions`)** - -In `api/v1alpha1/ionoscloudmachine_types_test.go`, replace the import block: - -```go -import ( - "context" - - "github.com/google/go-cmp/cmp" - sdk "github.com/ionos-cloud/sdk-go/v6" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - deprecatedv1beta1conditions "sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1" - "sigs.k8s.io/controller-runtime/pkg/client" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) -``` - -with (drop `deprecatedv1beta1conditions`; keep `corev1` — still used by `corev1.TypedLocalObjectReference` elsewhere): - -```go -import ( - "context" - - "github.com/google/go-cmp/cmp" - sdk "github.com/ionos-cloud/sdk-go/v6" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) -``` - -- [ ] **Step 7: Verify the api test package still builds and vets** - -Run: `go vet ./api/...` -Expected: no output, exit 0. (Compilation covers the test files; a leftover unused import or missing symbol fails here.) - -- [ ] **Step 8: Commit** - -```bash -git add api/v1alpha1/ionoscloudcluster_types_test.go api/v1alpha1/ionoscloudmachine_types_test.go -git commit -m "test(api): remove deprecated v1beta1 condition tests - -Co-Authored-By: Claude Opus 4.8 " -``` - ---- - -### Task 4: Remove deprecated v1beta1 status fields/helpers and regenerate - -**Files:** -- Modify: `api/v1alpha1/ionoscloudcluster_types.go` -- Modify: `api/v1alpha1/ionoscloudmachine_types.go` -- Regenerate: `api/v1alpha1/zz_generated.deepcopy.go`, `config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml`, `config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml` - -**Interfaces:** -- Consumes: nothing new. -- Produces: `IonosCloudClusterStatus` / `IonosCloudMachineStatus` no longer have a `Deprecated` field; the `Deprecated*`/`V1Beta1*` types and `Get/SetV1Beta1Conditions`/`SetV1Beta1Ready` methods no longer exist. `clusterv1` stays imported in both types files (used by the `clusterv1.ConditionType` constants). - -- [ ] **Step 1: Remove the `Deprecated` field from `IonosCloudClusterStatus`** - -In `api/v1alpha1/ionoscloudcluster_types.go`, delete these three lines (the last member of the status struct): - -```go - // Deprecated groups all status fields deprecated and scheduled for removal when v1beta1 contract support is dropped. - //+optional - Deprecated *IonosCloudClusterDeprecatedStatus `json:"deprecated,omitempty"` -``` - -- [ ] **Step 2: Remove the cluster deprecated status types** - -In the same file, delete both type declarations: - -```go -// IonosCloudClusterDeprecatedStatus groups all status fields deprecated and scheduled for removal when -// v1beta1 contract support is dropped. -type IonosCloudClusterDeprecatedStatus struct { - // V1Beta1 groups all v1beta1 status fields that are deprecated and scheduled for removal. - //+optional - V1Beta1 *IonosCloudClusterV1Beta1DeprecatedStatus `json:"v1beta1,omitempty"` -} - -// IonosCloudClusterV1Beta1DeprecatedStatus contains deprecated v1beta1 fields. -type IonosCloudClusterV1Beta1DeprecatedStatus struct { - // Ready indicates that the cluster is ready. - // - // Deprecated: Use Initialization.Provisioned instead. - //+optional - Ready bool `json:"ready,omitempty"` - - // Conditions defines current service state of the IonosCloudCluster using the deprecated v1beta1 condition type. - // - // Deprecated: Use the top-level conditions field instead. - //+optional - Conditions clusterv1.Conditions `json:"conditions,omitempty"` -} -``` - -- [ ] **Step 3: Remove the cluster deprecated helper methods** - -In the same file, delete both methods: - -```go -// GetV1Beta1Conditions returns the deprecated v1beta1 conditions from status.deprecated.v1beta1.conditions. -func (i *IonosCloudCluster) GetV1Beta1Conditions() clusterv1.Conditions { - if i.Status.Deprecated == nil || i.Status.Deprecated.V1Beta1 == nil { - return nil - } - return i.Status.Deprecated.V1Beta1.Conditions -} - -// SetV1Beta1Conditions sets the deprecated v1beta1 conditions in status.deprecated.v1beta1.conditions. -func (i *IonosCloudCluster) SetV1Beta1Conditions(conditions clusterv1.Conditions) { - if i.Status.Deprecated == nil { - i.Status.Deprecated = &IonosCloudClusterDeprecatedStatus{} - } - if i.Status.Deprecated.V1Beta1 == nil { - i.Status.Deprecated.V1Beta1 = &IonosCloudClusterV1Beta1DeprecatedStatus{} - } - i.Status.Deprecated.V1Beta1.Conditions = conditions -} -``` - -- [ ] **Step 4: Remove the `Deprecated` field from `IonosCloudMachineStatus`** - -In `api/v1alpha1/ionoscloudmachine_types.go`, delete these three lines: - -```go - // Deprecated groups all status fields deprecated and scheduled for removal when v1beta1 contract support is dropped. - //+optional - Deprecated *IonosCloudMachineDeprecatedStatus `json:"deprecated,omitempty"` -``` - -- [ ] **Step 5: Remove the machine deprecated status types** - -In the same file, delete both type declarations: - -```go -// IonosCloudMachineDeprecatedStatus groups all status fields deprecated and scheduled for removal when -// v1beta1 contract support is dropped. -type IonosCloudMachineDeprecatedStatus struct { - // V1Beta1 groups all v1beta1 status fields that are deprecated and scheduled for removal. - //+optional - V1Beta1 *IonosCloudMachineV1Beta1DeprecatedStatus `json:"v1beta1,omitempty"` -} - -// IonosCloudMachineV1Beta1DeprecatedStatus contains deprecated v1beta1 fields. -type IonosCloudMachineV1Beta1DeprecatedStatus struct { - // Ready indicates the VM has been provisioned and is ready. - // - // Deprecated: Use Initialization.Provisioned instead. - //+optional - Ready bool `json:"ready,omitempty"` - - // Conditions defines current service state of the IonosCloudMachine using the deprecated v1beta1 condition type. - // - // Deprecated: Use the top-level conditions field instead. - //+optional - Conditions clusterv1.Conditions `json:"conditions,omitempty"` -} -``` - -- [ ] **Step 6: Remove the machine deprecated helper methods** - -In the same file, delete all three methods: - -```go -// GetV1Beta1Conditions returns the deprecated v1beta1 conditions from status.deprecated.v1beta1.conditions. -func (m *IonosCloudMachine) GetV1Beta1Conditions() clusterv1.Conditions { - if m.Status.Deprecated == nil || m.Status.Deprecated.V1Beta1 == nil { - return nil - } - return m.Status.Deprecated.V1Beta1.Conditions -} - -// SetV1Beta1Conditions sets the deprecated v1beta1 conditions in status.deprecated.v1beta1.conditions. -func (m *IonosCloudMachine) SetV1Beta1Conditions(conditions clusterv1.Conditions) { - if m.Status.Deprecated == nil { - m.Status.Deprecated = &IonosCloudMachineDeprecatedStatus{} - } - if m.Status.Deprecated.V1Beta1 == nil { - m.Status.Deprecated.V1Beta1 = &IonosCloudMachineV1Beta1DeprecatedStatus{} - } - m.Status.Deprecated.V1Beta1.Conditions = conditions -} - -// SetV1Beta1Ready sets the deprecated v1beta1 ready field. -func (m *IonosCloudMachine) SetV1Beta1Ready(ready bool) { - if m.Status.Deprecated == nil { - m.Status.Deprecated = &IonosCloudMachineDeprecatedStatus{} - } - if m.Status.Deprecated.V1Beta1 == nil { - m.Status.Deprecated.V1Beta1 = &IonosCloudMachineV1Beta1DeprecatedStatus{} - } - m.Status.Deprecated.V1Beta1.Ready = ready -} -``` - -- [ ] **Step 7: Regenerate deepcopy and CRDs** - -Run: `make generate manifests` -Expected: succeeds. `git status` shows modifications to `api/v1alpha1/zz_generated.deepcopy.go` and the two CRD YAMLs under `config/crd/bases/`. - -- [ ] **Step 8: Confirm the generated artifacts no longer contain the deprecated subtree** - -Run: `grep -rniE "deprecated|v1beta1" config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml api/v1alpha1/zz_generated.deepcopy.go` -Expected: no matches (empty output). - -- [ ] **Step 9: Build and vet the whole module** - -Run: `go build ./... && go vet ./...` -Expected: no output, exit 0. (If `clusterv1` reports unused in a types file, verify the `IonosCloudClusterReady` / `MachineProvisionedCondition` constants still reference `clusterv1.ConditionType` — they must remain.) - -- [ ] **Step 10: Commit** - -```bash -git add api/v1alpha1/ionoscloudcluster_types.go api/v1alpha1/ionoscloudmachine_types.go api/v1alpha1/zz_generated.deepcopy.go config/crd/bases/ -git commit -m "feat(api)!: remove deprecated v1beta1 status fields and helpers - -BREAKING CHANGE: status.ready and the v1beta1 status.conditions are removed from -IonosCloudCluster/IonosCloudMachine. Consumers must read status.initialization.provisioned -and status.conditions (metav1.Condition). - -Co-Authored-By: Claude Opus 4.8 " -``` - ---- - -### Task 5: Declare the v1beta2 contract (metadata + README) - -**Files:** -- Modify: `metadata.yaml` -- Modify: `README.md` (compatibility matrix, ~lines 50–55) - -**Interfaces:** -- Consumes: nothing. -- Produces: clusterctl sees the `0.8` release series as `contract: v1beta2`, matching the CRD annotation. - -- [ ] **Step 1: Add the v0.8 release series as v1beta2** - -In `metadata.yaml`, replace: - -```yaml -releaseSeries: -- major: 0 - minor: 7 - contract: v1beta1 -``` - -with: - -```yaml -releaseSeries: -- major: 0 - minor: 8 - contract: v1beta2 -- major: 0 - minor: 7 - contract: v1beta1 -``` - -- [ ] **Step 2: Add the v0.8 row to the README compatibility matrix** - -In `README.md`, replace the v0.7 matrix row: - -``` -| CAPIC v1alpha1 (v0.7) | ☓ | ☓ | ☓ | ✓ | -``` - -with the v0.7 row followed by a new v0.8 row: - -``` -| CAPIC v1alpha1 (v0.7) | ☓ | ☓ | ☓ | ✓ | -| CAPIC v1alpha1 (v0.8) | ☓ | ☓ | ☓ | ✓ | -``` - -- [ ] **Step 3: Add a contract note under the matrix** - -In `README.md`, immediately after the compatibility table (before the `### Kubernetes Versions` heading), insert: - -```markdown - -> **v0.8 declares the Cluster API `v1beta2` contract and drops `v1beta1`.** It requires a -> `v1beta2`-capable core (CAPI v1.11+). The deprecated `status.ready` and v1beta1 -> `status.conditions` fields are removed — read `status.initialization.provisioned` and -> `status.conditions` instead. Upgrade existing clusters via the staged path -> `v0.6 → v0.7 → v0.8`. -``` - -- [ ] **Step 4: Sanity-check YAML validity** - -Run: `go run sigs.k8s.io/cluster-api/cmd/clusterctl version >/dev/null 2>&1; grep -nE "minor:|contract:" metadata.yaml` -Expected: the `grep` output lists `minor: 8` / `contract: v1beta2` at the top, then `minor: 7` / `contract: v1beta1`, etc. (The `clusterctl` invocation is optional; if unavailable, just eyeball the YAML indentation matches the existing entries.) - -- [ ] **Step 5: Commit** - -```bash -git add metadata.yaml README.md -git commit -m "feat(metadata): declare v1beta2 contract for v0.8 - -Co-Authored-By: Claude Opus 4.8 " -``` - ---- - -### Task 6: Rewrite the CAPI upgrade plan doc - -**Files:** -- Overwrite: `docs/capi-upgrade-plan.md` - -**Interfaces:** -- Consumes: nothing. -- Produces: an accurate migration doc reflecting the real v0.6/v0.7/v0.8 timeline and the clean cut. - -- [ ] **Step 1: Replace the entire contents of `docs/capi-upgrade-plan.md`** - -Write this exact content: - -````markdown -# Cluster API upgrade & v1beta2 migration - -How CAPIC moves to the Cluster API **v1beta2** contract, and how the release series line up. - -## Release timeline (actual) - -| CAPIC | CAPI | Declared contract | Implements v1beta2 | Notes | -|-------|------|-------------------|--------------------|-------| -| v0.6.x | v1.8 | v1beta1 | no | pure v1beta1 | -| v0.7.0 | v1.10 | v1beta1 | no | pure v1beta1 (CAPI dependency bump only) | -| **v0.8** | **v1.11** | **v1beta2** | yes | **pure v1beta2 — v1beta1 support dropped** | - -There is no dual-writing "bridge" release: v0.7.0 shipped as pure v1beta1, so v0.8 is the -first release to implement v1beta2, and it does so as a clean cut. - -## What v0.8 changes - -- Declares the **v1beta2** contract (`metadata.yaml`), matching the CRD - `cluster.x-k8s.io/v1beta2` annotation. -- Implements the v1beta2 status contract: `status.initialization.provisioned` and - `status.conditions` (`[]metav1.Condition`). -- **Removes** all v1beta1 residue: the `status.deprecated.v1beta1.*` fields, the - `Get/SetV1Beta1Conditions` / `SetV1Beta1Ready` helpers, and the v1beta1 condition - ownership in the patch helpers. - -## Supported upgrade path - -Staged, in place: **v0.6 → v0.7 → v0.8** (upgrade the CAPI core to v1.11+ alongside v0.8). - -- Existing clusters keep running; **no recreation** is required (no `IonosCloudMachineTemplate` - content change → no Machine rollout; running Machines are not torn down by a transient - `provisioned=nil`). -- On the `v0.7 → v0.8` hop there is a brief window where existing objects report infra - not-provisioned until the v0.8 controller reconciles them (self-heals on controller startup). -- **Breaking:** `.status.ready` and the v1beta1 `status.conditions` are removed. Any external - tooling reading them must move to `status.initialization.provisioned` / `status.conditions`. - -Empirical validation lives in the e2e upgrade suite (PR #379), which exercises the staged -`v0.6.3 → v0.7.0 → v0.8` path. - -## References - -- CAPI v1.11 → v1.12 provider migration guide: - https://cluster-api.sigs.k8s.io/developer/providers/migrations/v1.11-to-v1.12 -- Design spec: `docs/superpowers/specs/2026-07-17-drop-v1beta1-pure-v1beta2-design.md` -- Compatibility matrix: `README.md` -```` - -- [ ] **Step 2: Commit** - -```bash -git add docs/capi-upgrade-plan.md -git commit -m "docs: rewrite capi upgrade plan for pure-v1beta2 v0.8 - -Co-Authored-By: Claude Opus 4.8 " -``` - ---- - -### Task 7: Full verification gate - -**Files:** none created; may regenerate/format. - -**Interfaces:** none. - -- [ ] **Step 1: Confirm no v1beta1 residue remains in production Go code** - -Run: `grep -rniE "v1beta1|V1Beta1" --include="*.go" api/ scope/ internal/ cmd/ | grep -v zz_generated | grep -viE "_test.go"` -Expected: no matches (empty output). The only `clusterv1` imports point at `.../api/core/v1beta2` (string `v1beta2`), so they do not match. - -- [ ] **Step 2: Confirm the deprecated symbols are fully gone** - -Run: `grep -rniE "SetV1Beta1Ready|GetV1Beta1Conditions|SetV1Beta1Conditions|WithOwnedV1Beta1Conditions|DeprecatedStatus" --include="*.go" . | grep -v zz_generated` -Expected: no matches (empty output). - -- [ ] **Step 3: Run linters** - -Run: `make lint` -Expected: `0 issues`. If `make lint-fix` changes formatting, re-stage and amend the most relevant commit or add a small `chore: lint-fix` commit. - -- [ ] **Step 4: Run the full test suite** - -Run: `make test` -Expected: unit tests, vet, and the api envtest integration suite all pass (`ok`/green). This exercises the retained v1beta2 status round-trip and the controller/service provisioning behavior. - -- [ ] **Step 5: Verify generated artifacts are up to date** - -Run: `make verify` -Expected: `verify-gen` and `verify-tidy` pass (no diff). If it reports a diff, run `make generate manifests` / `go mod tidy`, commit the result, and re-run. - -- [ ] **Step 6: Final commit (only if steps 3/5 produced changes)** - -```bash -git add -A -git commit -m "chore: regenerate and lint-fix after dropping v1beta1 - -Co-Authored-By: Claude Opus 4.8 " -``` - -If nothing changed, skip this step. - ---- - -## Self-Review - -**Spec coverage** (against `docs/superpowers/specs/2026-07-17-drop-v1beta1-pure-v1beta2-design.md` §4): -- §4.1 API types (fields/types/helpers) → Task 4. ✅ -- §4.2 controllers/services dual-write → Task 1. ✅ -- §4.3 scope plumbing → Task 2. ✅ -- §4.4 generated (deepcopy/CRDs) → Task 4 steps 7–8. ✅ -- §4.5 metadata + README + capi-upgrade-plan rewrite → Tasks 5 & 6. ✅ -- §4.6 tests → Task 3. ✅ -- §4.7 what stays (v1beta2 impl, CRD annotation, condition constants) → preserved by construction; asserted in Task 4 step 9 and Task 7 step 1. ✅ -- §4.8 verification (make test/verify/lint + grep + no `status.deprecated`) → Task 7 + Task 4 step 8. ✅ - -SP2 (#380 rebase) and SP3 (#379 rebase + staged e2e) are separate plans, per the spec's decomposition — intentionally out of scope here. - -**Placeholder scan:** No TBD/TODO; every code step shows the exact before/after content and every command has an expected result. ✅ - -**Type consistency:** `WithOwnedConditions` / `ownedClusterConditions` / `ownedMachineConditions` used consistently; `clusterv1` = `sigs.k8s.io/cluster-api/api/core/v1beta2` throughout; helper names (`GetV1Beta1Conditions`, `SetV1Beta1Conditions`, `SetV1Beta1Ready`) match the removals in Tasks 1/3/4. ✅ -```` diff --git a/docs/superpowers/specs/2026-07-17-drop-v1beta1-pure-v1beta2-design.md b/docs/superpowers/specs/2026-07-17-drop-v1beta1-pure-v1beta2-design.md deleted file mode 100644 index f6b94915..00000000 --- a/docs/superpowers/specs/2026-07-17-drop-v1beta1-pure-v1beta2-design.md +++ /dev/null @@ -1,163 +0,0 @@ -# Design: drop v1beta1, make CAPIC a pure v1beta2 provider (v0.8) - -**Date:** 2026-07-17 -**Status:** approved (design), pending implementation plan -**Branch / PR:** `feat/upgrade-cluster-api-v1.11.11` (#378), targeting release **v0.8.0** - ---- - -## 1. Summary - -Reshape the in-flight CAPI v1.11 upgrade so that, instead of shipping a v1beta1↔v1beta2 -*bridge*, the release (**v0.8**) becomes a **pure v1beta2 provider**: it declares the -v1beta2 contract and removes every v1beta1 residue. The change spans the PR stack: - -- **#378** (this branch → v0.8): the clean cut. -- **#380** (v1.12.10 bump): rebased onto #378. -- **#379** (e2e upgrade suite): rebased onto #380, with its upgrade block changed to the - staged `v0.6.3 → v0.7.0 → v0.8` path that proves in-place upgrades survive the cut. - -## 2. Background: the real version reality - -The pre-existing `docs/capi-upgrade-plan.md` was written assuming *this branch* would ship -as v0.7 (a bridge), with the v1beta1 removal deferred to a later release. That assumption is -**wrong** — verified against the released tags: - -| Release | CAPI | Declared contract | Implements v1beta2? | Status fields | Role | -|---|---|---|---|---|---| -| **v0.6.3** | v1.8.12 | v1beta1 | no | `status.ready` + v1beta1 `conditions` | pure v1beta1 | -| **v0.7.0** (released 2026-06-16) | v1.10.10 | v1beta1 | **no** | `status.ready` + v1beta1 `conditions` | pure v1beta1 — **not a bridge** | -| **v0.8** (this branch, before this change) | v1.11.11 | v1beta1 | yes | dual: `initialization.provisioned` + `deprecated.v1beta1.*` | expand/bridge | -| **v0.8** (after this change) | v1.11.11 | **v1beta2** | yes | only `initialization.provisioned` | **pure v1beta2** | - -Decisive fact: **v0.7.0 shipped as pure v1beta1.** No dual-writing bridge exists in the -field. v0.8 (this branch) is therefore the first release to implement v1beta2 at all, and -the migration path we must protect is the staged **v0.6 → v0.7 → v0.8** in-place upgrade. - -## 3. Decisions (and why) - -1. **Clean cut in v0.8 (Option C), not a bridge.** CAPI v1.11 supports the v1beta2 contract - and the guides *recommend* moving as soon as possible. We accept the cost (below) to get a - pure v1beta2 provider in a single release rather than spreading it across v0.8/v0.9/v0.10. -2. **Existing clusters are NOT recreated.** Verified: no `IonosCloudMachineTemplate` content - change → no template-hash change → no Machine rollout. `ProviderID *string → string` is - safe for existing CRs (provisioned machines hold a concrete `ionos://…` value). A - management-plane upgrade never touches running IONOS Cloud servers/nodes. A running - Machine with a providerID + NodeRef is not torn down by a transient `provisioned=nil`. -3. **Accepted costs, documented as breaking:** - - A brief not-ready window on the `v0.7 → v0.8` hop until the v0.8 controller reconciles - each CR (self-heals on controller startup; strict MHCs could react during the window). - - Removal of `.status.ready` / v1beta1 conditions is a **breaking change** for any - external tooling reading them. No v1beta1 downconversion (minor — CAPIC CRDs are - `v1alpha1`-only, so there is no v1beta1 API version to convert to). - - Requires upgrading CAPI core to v1.11+ alongside CAPIC v0.8 (clusterctl contract gating). -4. **No hard e2e gate on #378.** #378 merges on `make test/verify/lint`. The empirical - upgrade proof lives downstream in #379 (its natural home), not as a blocker on #378. -5. **Additive history on #378.** Add "declare v1beta2 / drop v1beta1" commits on top of the - existing bridge commits (net tree = pure v1beta2). No interactive rewrite. -6. **Linear stack preserved.** `main ← #378 ← #380 ← #379`; rebase downstream onto the - rewritten #378 in order. - -## 4. SP1 — #378: the clean cut (primary deliverable) - -Verified, complete change surface. - -### 4.1 API types (`api/v1alpha1/`) -- `ionoscloudcluster_types.go`: remove `Status.Deprecated` field; remove - `IonosCloudClusterDeprecatedStatus` and `IonosCloudClusterV1Beta1DeprecatedStatus`; remove - `GetV1Beta1Conditions` / `SetV1Beta1Conditions`. -- `ionoscloudmachine_types.go`: same, plus remove `SetV1Beta1Ready`. -- **Keep** the condition-type constants (`IonosCloudClusterReady`, `MachineProvisionedCondition`) - and the top-level `Conditions []metav1.Condition`. `clusterv1` - (`sigs.k8s.io/cluster-api/api/core/v1beta2`) stays imported — still used by those constants - (`clusterv1.ConditionType`). *Optional tidy:* retype the two constants to `string` (only - ever stringified); do it only if it keeps lint/`ireturn` quiet — not required. - -### 4.2 Controllers / services -- `internal/controller/ionoscloudcluster_controller.go`: delete the "set deprecated v1beta1 - ready" block (incl. its `//nolint:staticcheck`). Keep `Status.Initialization.Provisioned = new(true)`. -- `internal/service/cloud/server.go`: delete `ms.IonosMachine.SetV1Beta1Ready(true)`. - -### 4.3 Scope plumbing (`scope/cluster.go`, `scope/machine.go`) -- Remove the `patch.WithOwnedV1Beta1Conditions{…}` option from both `PatchObject` calls; keep - `patch.WithOwnedConditions{…}`. -- Simplify the `owned*Conditions` doc comments that reference the derived v1beta1 list. -- `clusterv1` remains used (`clusterv1.ReadyCondition` in `WithOwnedConditions` / - `SetSummaryCondition`), so the import stays. - -### 4.4 Generated (via `make manifests generate`) -- Both CRD YAMLs lose `status.deprecated`. -- `api/v1alpha1/zz_generated.deepcopy.go` loses the deprecated-type deepcopy funcs. - -### 4.5 Metadata / docs -- `metadata.yaml`: add release series `0.8` with `contract: v1beta2` (0.7 and below stay - `v1beta1`). This makes the *declared* contract match the CRD `cluster.x-k8s.io/v1beta2` - annotation already present. -- `README.md`: add a v0.8 row to the compatibility matrix (v1beta2; CAPI **v1.11+** only, - since a v1beta2-declared provider needs a v1beta2-capable core) and note v1beta1 is dropped. -- `docs/capi-upgrade-plan.md`: **rewrite** to the correct timeline (v0.6 v1.8/v1beta1 → - v0.7 v1.10/v1beta1 → **v0.8 v1.11 pure v1beta2 clean cut**), replacing the obsolete - bridge/Part-A/Part-B narrative. - -### 4.6 Tests -- `api/v1alpha1/ionoscloudcluster_types_test.go` (≈ lines 41–45, 154, 175) and - `ionoscloudmachine_types_test.go` (≈ line 542): remove the `Get/SetV1Beta1*` assertions. - -### 4.7 What deliberately stays -- The v1beta2 implementation already on the branch (`Status.Initialization.Provisioned`, - `[]metav1.Condition`, `conditions.Set`, CRD `cluster.x-k8s.io/v1beta2` annotation). -- No conversion webhook is added; CRDs remain `v1alpha1`. -- No CAPI version change beyond the v1.11.11 already on the branch. - -### 4.8 Verification (SP1 gate) -- `make test` (unit + envtest integration), `make verify` (gen/tidy up to date), `make lint`. -- Inspection check: generated CRD YAMLs contain no `status.deprecated`; no remaining - references to the removed symbols (`grep`). -- In-place upgrade behavior is validated by analysis here and empirically in SP3 — not gated - on #378. - -## 5. SP2 — #380: rebase the v1.12.10 bump - -Rebase `feat/upgrade-cluster-api-v1.12.10` onto the rewritten #378. It is a pure dependency -bump with no v1beta1 code, so conflicts should be limited to `go.mod`/`go.sum` and any -regenerated artifacts. Re-run `make verify` after rebase. Detailed steps deferred to SP2's own -plan. - -## 6. SP3 — #379: rebase + staged upgrade e2e - -Rebase `feat/upgrade-e2e-suite` onto #380. Then change `test/e2e/upgrade_test.go`: -- **Replace** the existing direct `v0.6.3 → v0.6.99(local)` block (which, with the clean cut, - silently became a direct v0.6 → v0.8 jump) with the **staged path**: - `v0.6.3 → v0.7.0 → v0.8(local build)`. Because #379 rebases onto #380, the local v0.8 - provider is built against CAPI **v1.12.10**, so the final hop lands the core on v1.12.10 - (mirroring the existing suite design). This proves the supported in-place upgrade survives - the pure-v1beta2 cut. -- Bump the local dev provider version tag (currently `v0.6.99`) to reflect v0.8 (e.g. - `v0.8.99`), and update `metadata.yaml` / e2e config references accordingly. -- Reconcile e2e helpers that reference v1beta1 (`test/e2e/helpers/clusterhealth.go`, - `conditions.go`): these legitimately assert *old* clusters during upgrade and stay, but - their version comments/labels are updated for the staged path. -- Prerequisite satisfied: v0.6.3 and v0.7.0 are both published, so the block can `init`. - -Detailed block layout deferred to SP3's own plan. - -## 7. Risks & mitigations - -| Risk | Likelihood | Mitigation | -|---|---|---| -| Transient not-ready window trips a strict MHC on `v0.7→v0.8` | low | Controller reconciles on startup; documented; SP3 e2e asserts cluster health post-upgrade | -| External tooling reads `.status.ready` and breaks | medium | Documented as a breaking change in README + release notes | -| Downstream rebase (#380/#379) conflicts | medium | Additive #378 history keeps diffs legible; rebase in order, re-run `make verify` | -| Removed symbol still referenced somewhere unseen | low | `grep` sweep in SP1 verification; `make build` fails fast otherwise | - -## 8. Non-goals -- No conversion webhook; no CRD version bump. -- No v0.9 follow-up — the drop happens in v0.8. -- No CAPI bump beyond v1.11.11 on #378 (v1.12.10 is #380's job). -- No new e2e suite beyond changing the existing upgrade block in #379. - -## 9. References -- CAPI v1.10→v1.11 provider migration guide -- CAPI v1.11→v1.12 provider migration guide: https://cluster-api.sigs.k8s.io/developer/providers/migrations/v1.11-to-v1.12 -- Compatibility matrix: `README.md` -- Superseded planning doc (to be rewritten): `docs/capi-upgrade-plan.md` From e130bd37365ce077869a6682b3796b4b49051349 Mon Sep 17 00:00:00 2001 From: Max Neverov Date: Wed, 12 Aug 2026 18:44:44 +0200 Subject: [PATCH 72/94] fix: reverted CRDMigrationPhases description --- cmd/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/main.go b/cmd/main.go index 7590d67f..1bfa1603 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -182,7 +182,7 @@ func initFlags() { "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") pflag.StringArrayVar(&skipCRDMigrationPhases, "skip-crd-migration-phases", []string{}, - "CRD migration phases to skip. Use when the CAPI CRDMigrator handles migration.") + "CRD migration phases to skip. Valid values are: All, StorageVersionMigration, CleanupManagedFields.") pflag.IntVar(&icClusterConcurrency, "ionoscloudcluster-concurrency", 1, "Number of IonosCloudClusters to process simultaneously") pflag.IntVar(&icMachineConcurrency, "ionoscloudmachine-concurrency", 1, From 0bbd0d2a3a150fcdfb7896ef675a4016aa23abb8 Mon Sep 17 00:00:00 2001 From: Max Neverov Date: Wed, 12 Aug 2026 18:55:13 +0200 Subject: [PATCH 73/94] fix: owner reference relationships link --- test/e2e/helpers/ownerreference.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/e2e/helpers/ownerreference.go b/test/e2e/helpers/ownerreference.go index a4cb9f98..775389cc 100644 --- a/test/e2e/helpers/ownerreference.go +++ b/test/e2e/helpers/ownerreference.go @@ -62,7 +62,7 @@ var ( // IonosCloudInfraOwnerReferenceAssertions maps IONOS Cloud Infrastructure types to functions which return an error if the passed // OwnerReferences aren't as expected. -// Note: These relationships are documented in https://github.com/kubernetes-sigs/cluster-api/tree/main/docs/book/src/reference/owner_references.md. +// Note: These relationships are documented in https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/book/src/reference/api/owner-references.md. // That document should be updated if these references change. var IonosCloudInfraOwnerReferenceAssertions = map[string]func(types.NamespacedName, []metav1.OwnerReference) error{ "IonosCloudMachine": func(_ types.NamespacedName, owners []metav1.OwnerReference) error { @@ -79,7 +79,7 @@ var IonosCloudInfraOwnerReferenceAssertions = map[string]func(types.NamespacedNa // ExpOwnerReferenceAssertions maps experimental types to functions which return an error if the passed OwnerReferences // aren't as expected. -// Note: These relationships are documented in https://github.com/kubernetes-sigs/cluster-api/tree/main/docs/book/src/reference/owner_references.md. +// Note: These relationships are documented in https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/book/src/reference/api/owner-references.md. // That document should be updated if these references change. var ExpOwnerReferenceAssertions = map[string]func(types.NamespacedName, []metav1.OwnerReference) error{ kindClusterResourceSet: func(_ types.NamespacedName, owners []metav1.OwnerReference) error { @@ -94,7 +94,7 @@ var ExpOwnerReferenceAssertions = map[string]func(types.NamespacedName, []metav1 // KubernetesReferenceAssertions maps Kubernetes types to functions which return an error if the passed OwnerReferences // aren't as expected. -// Note: These relationships are documented in https://github.com/kubernetes-sigs/cluster-api/tree/main/docs/book/src/reference/owner_references.md. +// Note: These relationships are documented in https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/book/src/reference/api/owner-references.md. // That document should be updated if these references change. var KubernetesReferenceAssertions = map[string]func(types.NamespacedName, []metav1.OwnerReference) error{ "Secret": func(_ types.NamespacedName, owners []metav1.OwnerReference) error { From 8b41ced3dd0106b9b6d2e721be8134ade7f0c014 Mon Sep 17 00:00:00 2001 From: Max Neverov Date: Wed, 12 Aug 2026 18:58:07 +0200 Subject: [PATCH 74/94] fix: use constants intstead of string --- test/e2e/helpers/finalizers.go | 2 +- test/e2e/helpers/ownerreference.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/helpers/finalizers.go b/test/e2e/helpers/finalizers.go index fb666f96..efc46cba 100644 --- a/test/e2e/helpers/finalizers.go +++ b/test/e2e/helpers/finalizers.go @@ -30,7 +30,7 @@ import ( // IonosCloudInfraFinalizersAssertion maps IONOS Cloud infrastructure resource types to their expected finalizers. var IonosCloudInfraFinalizersAssertion = map[string]func(types.NamespacedName) []string{ - "IonosCloudMachine": func(types.NamespacedName) []string { return []string{infrav1.MachineFinalizer} }, + infrav1.IonosCloudMachineType: func(types.NamespacedName) []string { return []string{infrav1.MachineFinalizer} }, infrav1.IonosCloudClusterKind: func(types.NamespacedName) []string { return []string{infrav1.ClusterFinalizer} }, } diff --git a/test/e2e/helpers/ownerreference.go b/test/e2e/helpers/ownerreference.go index 775389cc..dc69e04b 100644 --- a/test/e2e/helpers/ownerreference.go +++ b/test/e2e/helpers/ownerreference.go @@ -65,7 +65,7 @@ var ( // Note: These relationships are documented in https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/book/src/reference/api/owner-references.md. // That document should be updated if these references change. var IonosCloudInfraOwnerReferenceAssertions = map[string]func(types.NamespacedName, []metav1.OwnerReference) error{ - "IonosCloudMachine": func(_ types.NamespacedName, owners []metav1.OwnerReference) error { + infrav1.IonosCloudMachineType: func(_ types.NamespacedName, owners []metav1.OwnerReference) error { return framework.HasExactOwners(owners, machineController) }, "IonosCloudMachineTemplate": func(_ types.NamespacedName, owners []metav1.OwnerReference) error { From 79a6ebe3f92189b0cd5c34e7ab20f15b6b2c8daa Mon Sep 17 00:00:00 2001 From: Max Neverov Date: Wed, 12 Aug 2026 19:07:13 +0200 Subject: [PATCH 75/94] fix: add useful linter comments back --- .golangci.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 40f0fdfd..7b4efed6 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -58,7 +58,7 @@ linters: desc: Should be replaced by stdlib errors package gosec: excludes: - - G601 + - G601 # Implicit memory aliasing of items from a range statement: Obsolete since Go 1.22 importas: alias: - pkg: k8s.io/api/(\w+)/(v[\w\d]+) @@ -122,8 +122,8 @@ linters: - json,inline - name: var-naming arguments: - - [] - - - LAN + - [] # first list is an allow list. All initialisms placed here will be ignored + - - LAN # second list is a deny list. These rules will be enforced - IP - NIC testifylint: @@ -157,6 +157,7 @@ linters: - revive path: fake_\.go text: exported (method|function|type|const) (.+) should have comment or be unexported + # Exclude some packages or code to require comments, for example test code, or fake clients. - linters: - revive path: (framework|e2e|infrastructure/docker)/.*.go @@ -168,18 +169,24 @@ linters: - linters: - revive text: line is \d+ characters, out of limit \d+ + # If a line comment ends with something that looks like a URL, we ignore the line length limit. source: \s*//.*[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?$ - linters: - revive path: _test\.go text: 'function-length: maximum number of lines per function exceeded' + # Disable unparam "always receives" which might not be really + # useful when building libraries. - linters: - unparam text: always receives + # Dot imports for gomega or ginkgo are allowed + # within test files. - path: _test\.go text: should not use dot imports - path: _test\.go text: cyclomatic complexity + # Append should be able to assign to a different var/slice. - path: (framework|e2e)/.*.go text: should not use dot imports paths: From fa3cd6cf14e4b26e5bac76a5e23141a7b959ac4c Mon Sep 17 00:00:00 2001 From: Max Neverov Date: Thu, 13 Aug 2026 16:03:13 +0200 Subject: [PATCH 76/94] chore: bump nextVersion --- clusterctl-settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clusterctl-settings.json b/clusterctl-settings.json index f7ce2969..f1c6de67 100644 --- a/clusterctl-settings.json +++ b/clusterctl-settings.json @@ -2,6 +2,6 @@ "name": "infrastructure-ionoscloud", "config": { "componentsFile": "infrastructure-components.yaml", - "nextVersion": "v0.7.0" + "nextVersion": "v0.8.0" } } From 201cb11acb86af82fb544ed33f95888da1413cee Mon Sep 17 00:00:00 2001 From: Max Neverov Date: Thu, 20 Aug 2026 13:15:28 +0200 Subject: [PATCH 77/94] Revert "chore: bump nextVersion" This reverts commit fa3cd6cf14e4b26e5bac76a5e23141a7b959ac4c. --- clusterctl-settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clusterctl-settings.json b/clusterctl-settings.json index f1c6de67..f7ce2969 100644 --- a/clusterctl-settings.json +++ b/clusterctl-settings.json @@ -2,6 +2,6 @@ "name": "infrastructure-ionoscloud", "config": { "componentsFile": "infrastructure-components.yaml", - "nextVersion": "v0.8.0" + "nextVersion": "v0.7.0" } } From 8a115e4724cdd265d7969e863266bd42e82fe999 Mon Sep 17 00:00:00 2001 From: Max Neverov Date: Thu, 20 Aug 2026 14:06:32 +0200 Subject: [PATCH 78/94] chore: bump go version --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 736bc39d..e7bca781 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/ionos-cloud/cluster-api-provider-ionoscloud -go 1.26.0 +go 1.26.6 require ( github.com/go-logr/logr v1.4.4 From 3c2cf01623bf3deed4de29f5eb83f4fee38dcf2b Mon Sep 17 00:00:00 2001 From: Max Neverov Date: Thu, 20 Aug 2026 14:19:25 +0200 Subject: [PATCH 79/94] chore: bump go version in docker --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 92531a52..f3797616 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.26.5 AS builder +FROM golang:1.26.6 AS builder ARG TARGETOS ARG TARGETARCH From b65735a0811a09240005b79870ff48503ada2e10 Mon Sep 17 00:00:00 2001 From: Wassim Kallel Date: Mon, 21 Sep 2026 17:50:26 +0200 Subject: [PATCH 80/94] validate skipCRDMigrationphases --- cmd/main.go | 33 +++++++++++++- cmd/main_test.go | 81 +++++++++++++++++++++++++++++++++++ config/crd/kustomization.yaml | 7 ++- 3 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 cmd/main_test.go diff --git a/cmd/main.go b/cmd/main.go index 1bfa1603..51928216 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -21,7 +21,10 @@ package main import ( "context" "flag" + "fmt" "os" + "slices" + "strings" "github.com/spf13/pflag" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" @@ -44,6 +47,29 @@ import ( const errMsgUnableToCreateController = "unable to create controller" +// validSkipCRDMigrationPhases are the phases accepted by --skip-crd-migration-phases. It is +// derived from the CAPI constants that crdmigrator.setup() switches on, so the flag's help text +// and validation cannot drift from what the migrator actually accepts. +var validSkipCRDMigrationPhases = []string{ + string(crdmigrator.StorageVersionMigrationPhase), + string(crdmigrator.CleanupManagedFieldsPhase), +} + +// validateSkipCRDMigrationPhases rejects unknown --skip-crd-migration-phases values at startup. +// crdmigrator.setup() also rejects them, but only during controller setup, after the manager has +// been built, and it surfaces as a generic "unable to create controller" that does not say which +// value was wrong. +func validateSkipCRDMigrationPhases(phases []string) error { + for _, phase := range phases { + if !slices.Contains(validSkipCRDMigrationPhases, phase) { + return fmt.Errorf("invalid --skip-crd-migration-phases value %q: valid values are %s", + phase, strings.Join(validSkipCRDMigrationPhases, ", ")) + } + } + + return nil +} + var ( scheme = runtime.NewScheme() setupLog = ctrl.Log.WithName("setup") @@ -83,6 +109,11 @@ func main() { initFlags() pflag.Parse() + if err := validateSkipCRDMigrationPhases(skipCRDMigrationPhases); err != nil { + setupLog.Error(err, "invalid flag value") + os.Exit(1) + } + _, metricsOptions, err := flags.GetManagerOptions(managerOptions) if err != nil { setupLog.Error(err, "unable to get manager options") @@ -182,7 +213,7 @@ func initFlags() { "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") pflag.StringArrayVar(&skipCRDMigrationPhases, "skip-crd-migration-phases", []string{}, - "CRD migration phases to skip. Valid values are: All, StorageVersionMigration, CleanupManagedFields.") + "CRD migration phases to skip. Valid values are: "+strings.Join(validSkipCRDMigrationPhases, ", ")+".") pflag.IntVar(&icClusterConcurrency, "ionoscloudcluster-concurrency", 1, "Number of IonosCloudClusters to process simultaneously") pflag.IntVar(&icMachineConcurrency, "ionoscloudmachine-concurrency", 1, diff --git a/cmd/main_test.go b/cmd/main_test.go new file mode 100644 index 00000000..add0a679 --- /dev/null +++ b/cmd/main_test.go @@ -0,0 +1,81 @@ +/* +Copyright 2024 IONOS Cloud. + +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 main + +import ( + "testing" + + "github.com/stretchr/testify/require" + "sigs.k8s.io/cluster-api/controllers/crdmigrator" +) + +func TestValidateSkipCRDMigrationPhases(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + phases []string + wantErr string + }{ + {name: "no phases skipped", phases: nil}, + { + name: "both valid phases", + phases: []string{"StorageVersionMigration", "CleanupManagedFields"}, + }, + { + // "All" was advertised by the flag's help text but crdmigrator.setup() rejects it, + // which made the documented usage crash the manager at startup. + name: "All is not a phase", + phases: []string{"All"}, + wantErr: `invalid --skip-crd-migration-phases value "All"`, + }, + { + name: "unknown phase among valid ones", + phases: []string{"StorageVersionMigration", "Nope"}, + wantErr: `invalid --skip-crd-migration-phases value "Nope"`, + }, + { + name: "phases are case sensitive", + phases: []string{"storageversionmigration"}, + wantErr: `invalid --skip-crd-migration-phases value "storageversionmigration"`, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + err := validateSkipCRDMigrationPhases(test.phases) + if test.wantErr == "" { + require.NoError(t, err) + return + } + require.ErrorContains(t, err, test.wantErr) + }) + } +} + +// TestValidSkipCRDMigrationPhasesMatchesCRDMigrator guards against the accepted set drifting away +// from the phases crdmigrator.setup() actually switches on. +func TestValidSkipCRDMigrationPhasesMatchesCRDMigrator(t *testing.T) { + t.Parallel() + + require.ElementsMatch(t, []string{ + string(crdmigrator.StorageVersionMigrationPhase), + string(crdmigrator.CleanupManagedFieldsPhase), + }, validSkipCRDMigrationPhases) +} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index e7cdddd5..18fed15f 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -8,8 +8,13 @@ resources: - bases/infrastructure.cluster.x-k8s.io_ionoscloudmachinetemplates.yaml #+kubebuilder:scaffold:crdkustomizeresource +# https://cluster-api.sigs.k8s.io/developer/providers/contracts.html#api-version-labels +# Only v1beta2 is advertised: v0.8 dropped status.ready, which is the field the v1beta1 +# contract reads for provisioning (CAPI internal/contract/infrastructure_cluster.go +# Provisioned("v1beta1") -> status.ready). Keeping the v1beta1 label would make a +# v1beta1-only core accept this provider and then poll a field that no longer exists, +# hanging silently instead of failing with "no versions matching contract". commonLabels: - cluster.x-k8s.io/v1beta1: v1alpha1 #https://cluster-api.sigs.k8s.io/developer/providers/contracts.html#api-version-labels cluster.x-k8s.io/v1beta2: v1alpha1 patches: From ba2c3084315bcc2dfa4b2db6a0d177fd69906ccf Mon Sep 17 00:00:00 2001 From: Wassim Kallel Date: Mon, 21 Sep 2026 22:40:24 +0200 Subject: [PATCH 81/94] fix(deps): bump google.golang.org/grpc to v1.83.2 Trivy fails the "build image" job on two HIGH CVEs in the grpc version pulled in transitively via cluster-api/util/flags -> k8s.io/apiserver: CVE-2026-84304 fixed in 1.83.1 CVE-2026-84445 fixed in 1.82.2, 1.83.2 v1.83.2 is the lowest version that clears both. go mod tidy carries along x/crypto, x/net, x/text, x/tools, otel and genproto; all remain indirect. Verified with the same scanner and flags CI uses (trivy 0.69.3, --ignore-unfixed --severity HIGH,CRITICAL --exit-code 1): 0 findings for both the debian base and the manager gobinary, exit code 0. Co-Authored-By: Claude Opus 5 (1M context) --- go.mod | 32 +++++++++++++-------------- go.sum | 68 +++++++++++++++++++++++++++++----------------------------- 2 files changed, 50 insertions(+), 50 deletions(-) diff --git a/go.mod b/go.mod index e7bca781..1872d772 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( require ( al.essio.dev/pkg/shellescape v1.5.1 // indirect - cel.dev/expr v0.25.1 // indirect + cel.dev/expr v0.25.2 // indirect github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect @@ -105,32 +105,32 @@ require ( github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect - go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel v1.44.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect - go.opentelemetry.io/otel/metric v1.43.0 // indirect - go.opentelemetry.io/otel/sdk v1.43.0 // indirect - go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/otel/sdk v1.44.0 // indirect + go.opentelemetry.io/otel/trace v1.44.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.28.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.53.0 // indirect + golang.org/x/crypto v0.55.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect - golang.org/x/mod v0.37.0 // indirect - golang.org/x/net v0.56.0 // indirect + golang.org/x/mod v0.38.0 // indirect + golang.org/x/net v0.58.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.21.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/term v0.44.0 // indirect - golang.org/x/text v0.39.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/term v0.45.0 // indirect + golang.org/x/text v0.41.0 // indirect golang.org/x/time v0.11.0 // indirect - golang.org/x/tools v0.47.0 // indirect + golang.org/x/tools v0.48.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect - google.golang.org/grpc v1.82.1 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/grpc v1.83.2 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index 8f939eb2..b2b8dafb 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= -cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= -cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs= +cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= @@ -274,20 +274,20 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= -go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= -go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= -go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= -go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= -go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= -go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= -go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= -go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= -go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= -go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -304,28 +304,28 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= -golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -334,23 +334,23 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= -golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= -golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= -golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= +golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -359,12 +359,12 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= -google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= -google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From a7f007b7ed9762b7054bd0e64e800269303ba2d5 Mon Sep 17 00:00:00 2001 From: Wassim Kallel Date: Tue, 22 Sep 2026 11:16:34 +0200 Subject: [PATCH 82/94] refactor: use util.IsControlPlaneMachine instead of a local copy The CAPI v1.11 import migration dropped the sigs.k8s.io/cluster-api/util import and inlined IsControlPlaneMachine as a private helper. Nothing required that: util never moved to a versioned path, and the function is byte-identical and present in v1.11.11, v1.12.10 and v1.13.5. Keeping a private copy freezes upstream's definition of "control plane machine" at one point in time. failoverRequired() depends on it, so a future upstream change would silently alter which machines get failover IPs configured. Co-Authored-By: Claude Opus 5 --- internal/service/cloud/network.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index e7d29004..294c50ce 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -26,6 +26,7 @@ import ( sdk "github.com/ionos-cloud/sdk-go/v6" clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" + "sigs.k8s.io/cluster-api/util" "sigs.k8s.io/controller-runtime/pkg/client" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" @@ -307,7 +308,7 @@ func (s *Service) retrieveFailoverIPForMachine( ) (requeue bool, failoverIP string, err error) { log := s.logger.WithName("retrieveFailoverIPForMachine") - if isControlPlaneMachine(ms.Machine) { + if util.IsControlPlaneMachine(ms.Machine) { ip, err := ms.ClusterScope.GetControlPlaneEndpointIP(ctx) return false, ip, err } @@ -379,7 +380,7 @@ func (s *Service) ReconcileIPFailoverDeletion( matchLabels := client.MatchingLabels{} - if isControlPlaneMachine(ms.Machine) { + if util.IsControlPlaneMachine(ms.Machine) { matchLabels[clusterv1.MachineControlPlaneLabel] = "" } else { matchLabels[clusterv1.MachineDeploymentNameLabel] = ms.IonosMachine.Labels[clusterv1.MachineDeploymentNameLabel] @@ -656,10 +657,5 @@ func (s *Service) patchLAN(ctx context.Context, ms *scope.Machine, lanID string, } func failoverRequired(ms *scope.Machine) bool { - return isControlPlaneMachine(ms.Machine) || ms.IonosMachine.Spec.FailoverIP != nil -} - -func isControlPlaneMachine(machine *clusterv1.Machine) bool { - _, ok := machine.Labels[clusterv1.MachineControlPlaneLabel] - return ok + return util.IsControlPlaneMachine(ms.Machine) || ms.IonosMachine.Spec.FailoverIP != nil } From 1da553ea4bcfda7282c02e78dab5662e0cb7cfee Mon Sep 17 00:00:00 2001 From: Wassim Kallel Date: Tue, 22 Sep 2026 11:19:58 +0200 Subject: [PATCH 83/94] fix(api): drop the controlPlaneEndpoint CEL rule broken by v1beta2 v1beta2's APIEndpoint made host and port optional (they were required), so port can be absent and the rule dereferences it unconditionally: spec.controlPlaneEndpoint: Invalid value: "object": no such key: port evaluating rule: port must be within 1-65535 That rejects {host: "1.2.3.4"} with no port, which is the intended flow -- ipblock.go defaults the port to 6443. Reproduced against the installed CRDs. The rule is also redundant now: the generated schema carries minimum 1 / maximum 65535 on port and minLength 1 on host, which is exactly what the rule's message claims. Reported by @gfariasalves-ionos. Co-Authored-By: Claude Opus 5 --- api/v1alpha1/ionoscloudcluster_types.go | 1 - .../infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml | 4 ---- ...structure.cluster.x-k8s.io_ionoscloudclustertemplates.yaml | 4 ---- 3 files changed, 9 deletions(-) diff --git a/api/v1alpha1/ionoscloudcluster_types.go b/api/v1alpha1/ionoscloudcluster_types.go index 123f44fc..34e7b537 100644 --- a/api/v1alpha1/ionoscloudcluster_types.go +++ b/api/v1alpha1/ionoscloudcluster_types.go @@ -42,7 +42,6 @@ const ( // IonosCloudClusterSpec defines the desired state of IonosCloudCluster. type IonosCloudClusterSpec struct { // ControlPlaneEndpoint represents the endpoint used to communicate with the control plane. - // +kubebuilder:validation:XValidation:rule="size(self.host) == 0 && self.port == 0 || self.port > 0 && self.port < 65536",message="port must be within 1-65535" // // TODO(gfariasalves): as of now, IP must be provided by the user as we still don't insert the // provider-provided block IP into the kube-vip manifest. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml index eb2515ce..a3f76945 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml @@ -77,10 +77,6 @@ spec: minimum: 1 type: integer type: object - x-kubernetes-validations: - - message: port must be within 1-65535 - rule: size(self.host) == 0 && self.port == 0 || self.port > 0 && - self.port < 65536 credentialsRef: description: CredentialsRef is a reference to the secret containing the credentials to access the IONOS Cloud API. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclustertemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclustertemplates.yaml index 107fdf76..9a179868 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclustertemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclustertemplates.yaml @@ -70,10 +70,6 @@ spec: minimum: 1 type: integer type: object - x-kubernetes-validations: - - message: port must be within 1-65535 - rule: size(self.host) == 0 && self.port == 0 || self.port - > 0 && self.port < 65536 credentialsRef: description: CredentialsRef is a reference to the secret containing the credentials to access the IONOS Cloud API. From bdb9edb55fe4109bf097cf4bcfcaa6acbf5442dd Mon Sep 17 00:00:00 2001 From: Wassim Kallel Date: Tue, 22 Sep 2026 11:19:58 +0200 Subject: [PATCH 84/94] fix(e2e): actually use wait-resource-versions-become-stable The interval was added to the e2e config but never passed to ValidateResourceVersionStableInput, so the framework fell back to its hardcoded 2m/15s defaults and the effective timeout stayed 2m, not 5m. Reported by @gfariasalves-ionos. Co-Authored-By: Claude Opus 5 --- test/e2e/capic_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/test/e2e/capic_test.go b/test/e2e/capic_test.go index 2b724a10..0b08ecb3 100644 --- a/test/e2e/capic_test.go +++ b/test/e2e/capic_test.go @@ -82,6 +82,7 @@ var _ = Describe("Quickstart: Should be able to create a cluster with 3 control- ClusterProxy: proxy, Namespace: namespace, OwnerGraphFilterFunction: clusterctlcluster.FilterClusterObjectsWithNameFilter(clusterName), + WaitToBecomeStable: e2eConfig.GetIntervals("default", "wait-resource-versions-become-stable"), }) }, } From e46f0bd23ca45ea61416601f9952b6a4edd5a363 Mon Sep 17 00:00:00 2001 From: Wassim Kallel Date: Tue, 22 Sep 2026 11:44:38 +0200 Subject: [PATCH 85/94] test(api): update port validation test for v1beta2 semantics Dropping the controlPlaneEndpoint CEL rule (1da553e) broke this test, which asserted on the rule's message. It set Port = 0 and expected a rejection, but v1beta2 made APIEndpoint.Port optional, so 0 serialises as absent: minimum: 1 never applies and the update is legal. "Port 0" now means unset, which the provider defaults to 6443 in ipblock.go -- it is no longer invalid input. Assert on a genuinely out-of-range port (65536), which maximum: 65535 rejects, and add a case pinning that clearing the port is accepted. Co-Authored-By: Claude Opus 5 --- api/v1alpha1/ionoscloudcluster_types_test.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/api/v1alpha1/ionoscloudcluster_types_test.go b/api/v1alpha1/ionoscloudcluster_types_test.go index 464c9c56..6c968e43 100644 --- a/api/v1alpha1/ionoscloudcluster_types_test.go +++ b/api/v1alpha1/ionoscloudcluster_types_test.go @@ -100,13 +100,22 @@ var _ = Describe("IonosCloudCluster", func() { }) When("trying to update the control plane endpoint", func() { - It("should fail when attempting to set an invalid port number", func() { + It("should fail when attempting to set an out-of-range port number", func() { cluster := defaultCluster() Expect(k8sClient.Create(context.Background(), cluster)).To(Succeed()) - cluster.Spec.ControlPlaneEndpoint.Port = 0 + cluster.Spec.ControlPlaneEndpoint.Port = 65536 Expect(k8sClient.Update(context.Background(), cluster)). - Should(MatchError(ContainSubstring("port must be within 1-65535"))) + Should(MatchError(ContainSubstring("65535"))) + }) + It("should allow clearing the port, which the provider defaults to 6443", func() { + cluster := defaultCluster() + Expect(k8sClient.Create(context.Background(), cluster)).To(Succeed()) + + // v1beta2 made APIEndpoint.Port optional, so 0 serialises as absent and + // means "unset" rather than "invalid". + cluster.Spec.ControlPlaneEndpoint.Port = 0 + Expect(k8sClient.Update(context.Background(), cluster)).To(Succeed()) }) It("should not fail when updating the endpoint correctly", func() { cluster := defaultCluster() From f81c21aba4029ae778b443b3f3c4156c210de142 Mon Sep 17 00:00:00 2001 From: Wassim Kallel Date: Tue, 22 Sep 2026 12:12:30 +0200 Subject: [PATCH 86/94] fix(scope): log the summary condition error instead of returning it PatchObject returned summaryErr after a successful Patch, and Finalize retries on any error, so a persistent SetSummaryCondition failure would re-patch on every backoff step without ever resolving. The condition is observability only. CAPI mirrors it onto the Cluster's InfrastructureReady (cluster_controller_status.go) and the Machine's InfrastructureReady, but both use a FallbackCondition based on status.initialization.provisioned when it is absent, so a failure degrades the message rather than the provisioning. Reported by @gfariasalves-ionos. Co-Authored-By: Claude Opus 5 --- scope/cluster.go | 14 +++++++++++++- scope/machine.go | 11 ++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/scope/cluster.go b/scope/cluster.go index 2b35ff40..e379d7ea 100644 --- a/scope/cluster.go +++ b/scope/cluster.go @@ -30,6 +30,7 @@ import ( clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" conditions "sigs.k8s.io/cluster-api/util/conditions" "sigs.k8s.io/cluster-api/util/patch" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" @@ -237,7 +238,18 @@ func (c *Cluster) PatchObject() error { ); patchErr != nil { return patchErr } - return summaryErr + + // Logged, not returned: Finalize retries on any error, so a persistent summary failure + // would re-patch on every backoff step without ever resolving. The condition is + // observability only -- CAPI mirrors it onto Cluster InfrastructureReady but falls back + // to status.initialization.provisioned when it is absent. + if summaryErr != nil { + ctrl.Log.WithName("scope.Cluster").Error(summaryErr, + "failed to set the Ready summary condition", + "ionoscloudcluster", c.IonosCluster.Name, "namespace", c.IonosCluster.Namespace) + } + + return nil } // Finalize will make sure to apply a patch to the current IonosCloudCluster. diff --git a/scope/machine.go b/scope/machine.go index b98039df..794161b5 100644 --- a/scope/machine.go +++ b/scope/machine.go @@ -28,6 +28,7 @@ import ( clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" conditions "sigs.k8s.io/cluster-api/util/conditions" "sigs.k8s.io/cluster-api/util/patch" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" @@ -205,7 +206,15 @@ func (m *Machine) PatchObject() error { ); patchErr != nil { return patchErr } - return summaryErr + + // Logged, not returned: see the equivalent note in scope/cluster.go PatchObject. + if summaryErr != nil { + ctrl.Log.WithName("scope.Machine").Error(summaryErr, + "failed to set the Ready summary condition", + "ionoscloudmachine", m.IonosMachine.Name, "namespace", m.IonosMachine.Namespace) + } + + return nil } // Finalize will make sure to apply a patch to the current IonosCloudMachine. From a3d2a1e9b8e972946b9c114f9e65ef8ee52dafda Mon Sep 17 00:00:00 2001 From: Wassim Kallel Date: Tue, 22 Sep 2026 16:04:07 +0200 Subject: [PATCH 87/94] fix(deps): bump cel-go and otel to clear open Dependabot alerts GHSA-gcjh-h69q-9w9g MEDIUM github.com/google/cel-go <=0.28.1 -> 0.29.0 CVE-2026-81870 LOW otel/sdk <=1.44.0 -> 1.45.0 CVE-2026-81870 LOW otel/.../otlptrace <=1.44.0 -> 1.45.0 CVE-2026-81870 LOW otel/.../otlptracegrpc <=1.44.0 -> 1.45.0 All four are indirect. otel is reachable from cmd via cluster-api/util/flags -> k8s.io/apiserver, so it ships in the binary; cel-go is in the module graph but not needed by the main module. Neither is caught by the image scan gate, which filters to HIGH,CRITICAL. Trivy on the rebuilt image now reports 0 findings at every severity. Co-Authored-By: Claude Opus 5 --- go.mod | 25 ++++++++++++------------- go.sum | 54 ++++++++++++++++++++++++++---------------------------- 2 files changed, 38 insertions(+), 41 deletions(-) diff --git a/go.mod b/go.mod index ad4020c9..fdbc34c1 100644 --- a/go.mod +++ b/go.mod @@ -35,7 +35,7 @@ require ( github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect github.com/adrg/xdg v0.5.3 // indirect - github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect @@ -64,12 +64,12 @@ require ( github.com/gobuffalo/flect v1.0.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/cel-go v0.26.0 // indirect + github.com/google/cel-go v0.29.0 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-github/v53 v53.2.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -109,20 +109,19 @@ require ( github.com/spf13/cast v1.10.0 // indirect github.com/spf13/cobra v1.10.1 // indirect github.com/spf13/viper v1.21.0 // indirect - github.com/stoewer/go-strcase v1.3.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/valyala/fastjson v1.6.4 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect - go.opentelemetry.io/otel v1.44.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect - go.opentelemetry.io/otel/metric v1.44.0 // indirect - go.opentelemetry.io/otel/sdk v1.44.0 // indirect - go.opentelemetry.io/otel/trace v1.44.0 // indirect - go.opentelemetry.io/proto/otlp v1.9.0 // indirect + go.opentelemetry.io/otel v1.45.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0 // indirect + go.opentelemetry.io/otel/metric v1.45.0 // indirect + go.opentelemetry.io/otel/sdk v1.45.0 // indirect + go.opentelemetry.io/otel/trace v1.45.0 // indirect + go.opentelemetry.io/proto/otlp v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.28.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect @@ -139,8 +138,8 @@ require ( golang.org/x/time v0.11.0 // indirect golang.org/x/tools v0.48.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect google.golang.org/grpc v1.83.2 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect diff --git a/go.sum b/go.sum index f6342880..1da46e2d 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,8 @@ github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwF github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= -github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= -github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= @@ -114,8 +114,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= -github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +github.com/google/cel-go v0.29.0 h1:fEG+Ja3YRwNOqnQxTyJwoByAUAvTuxUGiro/jhrm4F4= +github.com/google/cel-go v0.29.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -137,8 +137,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -254,8 +254,6 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= -github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -295,22 +293,22 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0/go.mod h1:KDgtbWKTQs4bM+VPUr6WlL9m/WXcmkCcBlIzqxPGzmI= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= -go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= -go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= -go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= -go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= -go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= -go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= -go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= -go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= -go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= -go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= -go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= -go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU= +go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 h1:QRefszxJmfPdjXUUm3j6iDzY03mTPXMjqErFqQ67vUg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0/go.mod h1:Tiz03lTBVBrm7eWZBOidzEaYaJa8tjwGUGv6d8mlTyk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0 h1:fG5MCxGz8+2VtrN/WgqSpJFctVz24gpxj8CxkKmc8Ww= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0/go.mod h1:BmAYTn+3ysbRe+IU2msxmf5Rx3g6DHvex+tWI3LdhYI= +go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M= +go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s= +go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw= +go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA= +go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o= +go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA= +go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag= +go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc= +go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk= +go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -379,10 +377,10 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= -google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d h1:FarXi840EJWSHYTN3ERkADbPWjl307+FGrA22KAVjjc= +google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d/go.mod h1:K/+WGbmBY7aNW1HDw1fJnKYo10i0DkAX6pows00dLig= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d h1:IL4hdHzcUv2l/gcg98/Rj3FbtE6axwqslOW8SW0C+S0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= From 51c2bb79898e0b754e59ee30e8d21b52cb44b098 Mon Sep 17 00:00:00 2001 From: Wassim Kallel Date: Tue, 22 Sep 2026 16:09:52 +0200 Subject: [PATCH 88/94] ci: pin trivy-action to a commit SHA SonarQube security hotspot: a tag is a mutable ref, so aquasecurity/trivy-action@v0.35.0 can be repointed at different code without the workflow changing. Pin the full commit SHA instead; v0.35.0 resolves to 57a97c7 ("chore(deps): Update trivy to v0.69.3"), which matches the Trivy version CI reports. Matches the existing format used for codespell-project/actions-codespell in codespell.yml, the only other SHA-pinned action. Co-Authored-By: Claude Opus 5 --- .github/workflows/image.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/image.yaml b/.github/workflows/image.yaml index b412b75a..7a0a48b9 100644 --- a/.github/workflows/image.yaml +++ b/.github/workflows/image.yaml @@ -50,7 +50,7 @@ jobs: - name: Scan image if: github.event_name == 'pull_request' - uses: aquasecurity/trivy-action@v0.35.0 + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 with: scan-ref: ${{ steps.meta.outputs.tags }} exit-code: '1' From dc9469966c5106fd1c410eeb90732d318b860a81 Mon Sep 17 00:00:00 2001 From: Wassim Kallel Date: Tue, 22 Sep 2026 16:12:01 +0200 Subject: [PATCH 89/94] ci: bump trivy-action to v0.36.0 v0.36.0 is the latest release; ed142fd is its commit SHA. It bundles Trivy v0.70.0 (was v0.69.3), so the scanner and its vulnerability DB both move. Verified before bumping: scanning the current image with Trivy 0.70.0 under the same gate the workflow uses (--ignore-unfixed --severity HIGH,CRITICAL --exit-code 1) reports 0 findings and exits 0, so the newer DB surfaces nothing the previous scanner missed. Co-Authored-By: Claude Opus 5 --- .github/workflows/image.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/image.yaml b/.github/workflows/image.yaml index 7a0a48b9..e285ffc5 100644 --- a/.github/workflows/image.yaml +++ b/.github/workflows/image.yaml @@ -50,7 +50,7 @@ jobs: - name: Scan image if: github.event_name == 'pull_request' - uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: scan-ref: ${{ steps.meta.outputs.tags }} exit-code: '1' From bb41e7af26b2dcff1e76f3fd95766cb8467eac8e Mon Sep 17 00:00:00 2001 From: Wassim Kallel Date: Wed, 23 Sep 2026 01:24:22 +0200 Subject: [PATCH 90/94] fix(e2e): split the management cluster version from the workload version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit suite_test.go passed KUBERNETES_VERSION to CreateKindBootstrapClusterAndLoadImages, so one variable set both the kind management cluster and the workload clusters. That was fine under CAPI v1.11, but v1.12 lists v1.29/v1.30 as workload-only and supports v1.31-v1.34 for management, so this upgrade quietly moved the suite off the matrix. The two have different constraints and cannot share a value: the management version needs a pre-built kindest/node image (GetMapping requires an exact patch match and otherwise returns an unpinned fallback tag), while the workload version must match the IONOS node image from IONOSCLOUD_MACHINE_IMAGE_ID. Mirror upstream, which keeps them separate (test/e2e/common.go KubernetesVersionManagement, e2e_suite_test.go). Management moves to v1.33.7 -- pre-built with a pinned digest, and inside both v1.12's v1.31-v1.34 and v1.13's v1.31-v1.36 ranges, so #398 needs no further bump. The workload version is unchanged, so no IONOS image or repo variable has to move. Also note in the README that a "☓" in the compatibility matrix means not validated: metadata.yaml declares v0.6 and v0.7 both as contract v1beta1, and CAPI v1.11-v1.13 still accept it, so nothing technical separates those rows. Reported by @gfariasalves-ionos and Copilot. Co-Authored-By: Claude Opus 5 --- README.md | 3 +++ test/e2e/common.go | 11 +++++++++-- test/e2e/config/ionoscloud.yaml | 11 +++++++++++ test/e2e/suite_test.go | 2 +- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5ed1c920..c360a470 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,9 @@ This provider's versions are compatible with the following versions of Cluster A > `status.conditions` instead. Upgrade existing clusters via the staged path > `v0.6 → v0.7 → v0.8`. +> `☓` means not validated. CAPI v1.11–v1.13 still accept the `v1beta1` contract, so +> older CAPIC releases may keep working past their last `✓` column — just untested. + ### Kubernetes Versions The IONOS Cloud provider is able to install and manage the [versions of Kubernetes supported by the Cluster API (CAPI) project](https://cluster-api.sigs.k8s.io/reference/versions.html#supported-kubernetes-versions). diff --git a/test/e2e/common.go b/test/e2e/common.go index c7316de9..7d54eb76 100644 --- a/test/e2e/common.go +++ b/test/e2e/common.go @@ -27,9 +27,16 @@ import ( // Test suite constants for e2e config variables. const ( - CNIPath = "CNI" - CNIResources = "CNI_RESOURCES" + CNIPath = "CNI" + CNIResources = "CNI_RESOURCES" + // KubernetesVersion is the version of the workload clusters the specs create. KubernetesVersion = "KUBERNETES_VERSION" + // KubernetesVersionManagement is the version of the kind bootstrap cluster. It is kept + // separate from KubernetesVersion because the two have different constraints: the + // management cluster must be within the CAPI release's supported management range and + // have a pre-built kindest/node image, while the workload version must match the IONOS + // node image the machines boot. + KubernetesVersionManagement = "KUBERNETES_VERSION_MANAGEMENT" defaultCloudResourceNamePrefix = "capic-e2e-test-" ) diff --git a/test/e2e/config/ionoscloud.yaml b/test/e2e/config/ionoscloud.yaml index 0aff4eeb..85a1a506 100644 --- a/test/e2e/config/ionoscloud.yaml +++ b/test/e2e/config/ionoscloud.yaml @@ -71,7 +71,18 @@ variables: # allowing the same e2e config file to be reused in different Prow jobs e.g. each one with a K8s version permutation. # The following Kubernetes versions should be the latest versions with already published kindest/node images. # This avoids building node images in the default case which improves the test duration significantly. + # + # KUBERNETES_VERSION is the workload cluster version. It must match the IONOS node image + # the machines boot (IONOSCLOUD_MACHINE_IMAGE_ID), otherwise kubeadm and kubelet on the + # node disagree with the requested control plane version and the suite waits for nodes + # that never appear. v1.30 remains supported for workload clusters on CAPI v1.12. KUBERNETES_VERSION: "v1.30.6" + # KUBERNETES_VERSION_MANAGEMENT is the kind bootstrap cluster version (suite_test.go). + # It has different constraints: it must sit inside the CAPI release's supported + # management range -- v1.31 to v1.34 for v1.12, where v1.29/v1.30 are workload-only -- + # and have a pre-built kindest/node image in cluster-api/test's kind mapper, since + # GetMapping needs an exact patch match and otherwise falls back to an unpinned tag. + KUBERNETES_VERSION_MANAGEMENT: "v1.33.7" CNI: "./data/cni/calico.yaml" KUBETEST_CONFIGURATION: "./data/kubetest/conformance.yaml" CLUSTER_NAME: "e2e-cluster-${RANDOM}" diff --git a/test/e2e/suite_test.go b/test/e2e/suite_test.go index e089be93..cc4a5a1a 100644 --- a/test/e2e/suite_test.go +++ b/test/e2e/suite_test.go @@ -233,7 +233,7 @@ func setupBootstrapCluster(scheme *runtime.Scheme) (bootstrap.ClusterProvider, f kubeconfigPath := "" if !useExistingCluster { clusterProvider = bootstrap.CreateKindBootstrapClusterAndLoadImages(ctx, bootstrap.CreateKindBootstrapClusterAndLoadImagesInput{ - KubernetesVersion: e2eConfig.MustGetVariable(KubernetesVersion), + KubernetesVersion: e2eConfig.MustGetVariable(KubernetesVersionManagement), Name: e2eConfig.ManagementClusterName, Images: e2eConfig.Images, LogFolder: filepath.Join(artifactFolder, "kind"), From 1244ab5c72633764a50f20e20d8a28134c35fa9a Mon Sep 17 00:00:00 2001 From: Wassim Kallel Date: Wed, 23 Sep 2026 01:37:11 +0200 Subject: [PATCH 91/94] fix(e2e): add the upgrade intervals, align action versions, drop the plan doc Intervals: ClusterctlUpgradeSpec (this suite) reads wait-resource-versions-remain-stable, and cluster_upgrade.go / cluster_upgrade_runtimesdk.go / self_hosted.go read wait-control-plane-upgrade and wait-machine-deployment-upgrade. None were defined. GetIntervals returns nil for a missing key and Eventually treats that as Gomega's ~1s default, so the specs would have failed with near-instant timeouts that look like product bugs. Values match upstream's docker.yaml. Workflow: actions/checkout, setup-go and upload-artifact were at v4/v5/v4 while every other workflow in the repo is on v7 (bumped by #388 and #319). Aligned, so this does not reintroduce old majors and trigger fresh dependabot PRs. docs/superpowers: removed. The directory was deleted twice on purpose (3ef274b "drop AI instructions + superpowers", f4c2bc7 "remove agent-generated planning docs") and main carries none. The file is an agent working document that also references specs #378 already deleted, and nothing links to it. Co-Authored-By: Claude Opus 5 --- .github/workflows/e2e-upgrade.yaml | 6 +- .../2026-07-31-sp3-adopt-upgrade-e2e-suite.md | 904 ------------------ test/e2e/config/ionoscloud.yaml | 8 + 3 files changed, 11 insertions(+), 907 deletions(-) delete mode 100644 docs/superpowers/plans/2026-07-31-sp3-adopt-upgrade-e2e-suite.md diff --git a/.github/workflows/e2e-upgrade.yaml b/.github/workflows/e2e-upgrade.yaml index 92346f81..4e832f6d 100644 --- a/.github/workflows/e2e-upgrade.yaml +++ b/.github/workflows/e2e-upgrade.yaml @@ -18,10 +18,10 @@ jobs: # likely to exceed the Makefile's 2h default; override for this scheduled run. GINKGO_TIMEOUT: "4h" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup Go environment - uses: actions/setup-go@v5 + uses: actions/setup-go@v7 with: go-version-file: "go.mod" @@ -29,7 +29,7 @@ jobs: run: make test-e2e-upgrade - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: success() || failure() with: name: logs-upgrade diff --git a/docs/superpowers/plans/2026-07-31-sp3-adopt-upgrade-e2e-suite.md b/docs/superpowers/plans/2026-07-31-sp3-adopt-upgrade-e2e-suite.md deleted file mode 100644 index 3dcf4dfa..00000000 --- a/docs/superpowers/plans/2026-07-31-sp3-adopt-upgrade-e2e-suite.md +++ /dev/null @@ -1,904 +0,0 @@ -# SP3 — Adopt the staged upgrade e2e suite onto pure-v1beta2 #380 Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Port the staged-upgrade e2e suite (currently on `origin/feat/upgrade-e2e-suite`, mirrored locally as `e2e-suite-v1.12`, commit `a74c4eb`) onto the rewritten PR stack `main ← #378 (feat/upgrade-cluster-api-v1.11.11) ← #380 (feat/upgrade-cluster-api-v1.12.10)`, dropping the v1beta1-bridge production-code changes that branch predates the pure-v1beta2 clean cut (`docs/superpowers/specs/2026-07-17-drop-v1beta1-pure-v1beta2-design.md`) made obsolete, and restaging the "single-hop" upgrade block through the published `v0.7.0` release instead of jumping directly from `v0.6.3` to a local `v0.8` build. - -**Architecture:** Branch `feat/upgrade-e2e-suite-v0.8` off `feat/upgrade-cluster-api-v1.12.10` (current tip `b1e7fc4`). Cherry-pick/hand-port only the e2e-only files from `e2e-suite-v1.12` (test data, helpers, config, CI, Makefile) — never its `internal/controller/`, `scope/*.go`, or `metadata.yaml` changes, which re-add the v1beta1 deprecated-status bridge that #378 intentionally removed. Rename the local dev provider tag from `v0.6.99` to `v0.8.99` throughout (it now builds v0.8, not a v0.6.x patch). Restructure the "single-hop" `Describe` block in `upgrade_test.go` to stage through the real `v0.7.0` release, matching the already-staged "multi-hop" block. - -**Tech Stack:** Ginkgo/Gomega e2e suite (`test/e2e/`, build tag `e2e`), `sigs.k8s.io/cluster-api/test/framework` `ClusterctlUpgradeSpec`, clusterctl config (`test/e2e/config/ionoscloud.yaml`), kind-based secondary management cluster. - -## Global Constraints - -- Do not reintroduce `Status.Deprecated` / `SetV1Beta1Ready` / `WithOwnedV1Beta1Conditions` anywhere in production code (`internal/`, `scope/`, `api/v1alpha1/`) — that is the SP1 decision this branch must not undo. -- `test/e2e/config/ionoscloud.yaml`'s `v1.11.11` and `v1.12.10` core/bootstrap/control-plane entries must declare `contract: v1beta2` (not `v1beta1` as `e2e-suite-v1.12` has them — that branch predates the clean cut). `v1.10.10` and `v1.8.12` stay `contract: v1beta1` (real upstream fact, unrelated to our provider). -- The local dev infra-provider version name is `v0.8.99` (was `v0.6.99`), everywhere it's referenced: `test/e2e/config/ionoscloud.yaml` and `test/e2e/upgrade_test.go`. -- Every new/ported file must build under `go build -tags e2e ./...` and `go vet -tags e2e ./...`. -- Base branch for all work: `feat/upgrade-cluster-api-v1.12.10` at commit `b1e7fc4` (SP2, already rebased onto pure-v1beta2 #378). - ---- - -### Task 1: Branch setup and e2e data/config file ports - -**Files:** -- Create: `test/e2e/data/shared/v1.8/metadata.yaml` -- Create: `test/e2e/data/shared/v1.10/metadata.yaml` -- Modify: `test/e2e/config/ionoscloud.yaml` - -**Interfaces:** none (data/config only). - -- [ ] **Step 1: Create the branch** - -```bash -git checkout -b feat/upgrade-e2e-suite-v0.8 feat/upgrade-cluster-api-v1.12.10 -``` - -- [ ] **Step 2: Add the v1.8 CAPI core metadata override** - -Create `test/e2e/data/shared/v1.8/metadata.yaml`: - -```yaml -apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3 -kind: Metadata -releaseSeries: - - major: 1 - minor: 8 - contract: v1beta1 - - major: 1 - minor: 7 - contract: v1beta1 - - major: 1 - minor: 6 - contract: v1beta1 - - major: 1 - minor: 5 - contract: v1beta1 - - major: 1 - minor: 4 - contract: v1beta1 - - major: 1 - minor: 3 - contract: v1beta1 - - major: 1 - minor: 2 - contract: v1beta1 - - major: 1 - minor: 1 - contract: v1beta1 - - major: 1 - minor: 0 - contract: v1beta1 - - major: 0 - minor: 4 - contract: v1alpha4 - - major: 0 - minor: 3 - contract: v1alpha3 -``` - -- [ ] **Step 3: Add the v1.10 CAPI core metadata override** - -Create `test/e2e/data/shared/v1.10/metadata.yaml`: - -```yaml -apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3 -kind: Metadata -releaseSeries: - - major: 1 - minor: 10 - contract: v1beta1 - - major: 1 - minor: 9 - contract: v1beta1 - - major: 1 - minor: 8 - contract: v1beta1 - - major: 1 - minor: 7 - contract: v1beta1 - - major: 1 - minor: 6 - contract: v1beta1 - - major: 1 - minor: 5 - contract: v1beta1 - - major: 1 - minor: 4 - contract: v1beta1 - - major: 1 - minor: 3 - contract: v1beta1 - - major: 1 - minor: 2 - contract: v1beta1 - - major: 1 - minor: 1 - contract: v1beta1 - - major: 1 - minor: 0 - contract: v1beta1 - - major: 0 - minor: 4 - contract: v1alpha4 - - major: 0 - minor: 3 - contract: v1alpha3 -``` - -Note: `test/e2e/data/shared/v1.12/metadata.yaml` already exists on this branch (added in SP2) with `minor: 12` and `minor: 11` both `contract: v1beta2` — do not recreate it. - -- [ ] **Step 4: Replace `test/e2e/config/ionoscloud.yaml` with the merged version** - -Register the v1.8/v1.10/v1.12 core+bootstrap+control-plane provider versions (v1.12 as `v1beta2`, matching this branch's contract, not `e2e-suite-v1.12`'s stale `v1beta1`), add the `v0.6.3`/`v0.7.0` published infra-provider versions, and rename the local dev entry to `v0.8.99`: - -```yaml -images: - - name: ghcr.io/ionos-cloud/cluster-api-provider-ionoscloud:e2e - -providers: - - name: cluster-api - type: CoreProvider - versions: - - name: "v1.12.10" - value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.12.10/core-components.yaml" - type: url - contract: v1beta2 - replacements: - - old: --metrics-addr=127.0.0.1:8080 - new: --metrics-addr=:8443 - files: - - sourcePath: "../data/shared/v1.12/metadata.yaml" - - name: "v1.11.11" - value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/core-components.yaml" - type: url - contract: v1beta2 - replacements: - - old: --metrics-addr=127.0.0.1:8080 - new: --metrics-addr=:8443 - files: - - sourcePath: "../data/shared/v1.11/metadata.yaml" - - name: "v1.10.10" - value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/core-components.yaml" - type: url - contract: v1beta1 - replacements: - - old: --metrics-addr=127.0.0.1:8080 - new: --metrics-addr=:8443 - files: - - sourcePath: "../data/shared/v1.10/metadata.yaml" - - name: "v1.8.12" - value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.8.12/core-components.yaml" - type: url - contract: v1beta1 - replacements: - - old: --metrics-addr=127.0.0.1:8080 - new: --metrics-addr=:8443 - files: - - sourcePath: "../data/shared/v1.8/metadata.yaml" - - name: kubeadm - type: BootstrapProvider - versions: - - name: "v1.12.10" - value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.12.10/bootstrap-components.yaml" - type: url - contract: v1beta2 - replacements: - - old: --metrics-addr=127.0.0.1:8080 - new: --metrics-addr=:8443 - files: - - sourcePath: "../data/shared/v1.12/metadata.yaml" - - name: "v1.11.11" - value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/bootstrap-components.yaml" - type: url - contract: v1beta2 - replacements: - - old: --metrics-addr=127.0.0.1:8080 - new: --metrics-addr=:8443 - files: - - sourcePath: "../data/shared/v1.11/metadata.yaml" - - name: "v1.10.10" - value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/bootstrap-components.yaml" - type: url - contract: v1beta1 - replacements: - - old: --metrics-addr=127.0.0.1:8080 - new: --metrics-addr=:8443 - files: - - sourcePath: "../data/shared/v1.10/metadata.yaml" - - name: "v1.8.12" - value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.8.12/bootstrap-components.yaml" - type: url - contract: v1beta1 - replacements: - - old: --metrics-addr=127.0.0.1:8080 - new: --metrics-addr=:8443 - files: - - sourcePath: "../data/shared/v1.8/metadata.yaml" - - name: kubeadm - type: ControlPlaneProvider - versions: - - name: "v1.12.10" - value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.12.10/control-plane-components.yaml" - type: url - contract: v1beta2 - replacements: - - old: --metrics-addr=127.0.0.1:8080 - new: --metrics-addr=:8443 - files: - - sourcePath: "../data/shared/v1.12/metadata.yaml" - - name: "v1.11.11" - value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/control-plane-components.yaml" - type: url - contract: v1beta2 - replacements: - - old: --metrics-addr=127.0.0.1:8080 - new: --metrics-addr=:8443 - files: - - sourcePath: "../data/shared/v1.11/metadata.yaml" - - name: "v1.10.10" - value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/control-plane-components.yaml" - type: url - contract: v1beta1 - replacements: - - old: --metrics-addr=127.0.0.1:8080 - new: --metrics-addr=:8443 - files: - - sourcePath: "../data/shared/v1.10/metadata.yaml" - - name: "v1.8.12" - value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.8.12/control-plane-components.yaml" - type: url - contract: v1beta1 - replacements: - - old: --metrics-addr=127.0.0.1:8080 - new: --metrics-addr=:8443 - files: - - sourcePath: "../data/shared/v1.8/metadata.yaml" - - name: in-cluster - type: IPAMProvider - versions: - - name: v1.0.0 - # Use manifest from source files - value: "https://github.com/kubernetes-sigs/cluster-api-ipam-provider-in-cluster/releases/download/v1.0.0/ipam-components.yaml" - type: url - contract: v1beta1 - files: - - sourcePath: "../data/shared/capi-ipam/v1.0/metadata.yaml" - replacements: - - old: "imagePullPolicy: Always" - new: "imagePullPolicy: IfNotPresent" - - name: ionoscloud - type: InfrastructureProvider - versions: - - name: v0.6.3 - value: "https://github.com/ionos-cloud/cluster-api-provider-ionoscloud/releases/download/v0.6.3/infrastructure-components.yaml" - type: url - contract: v1beta1 - files: - - sourcePath: "../data/infrastructure-ionoscloud/cluster-template.yaml" - - name: v0.7.0 - value: "https://github.com/ionos-cloud/cluster-api-provider-ionoscloud/releases/download/v0.7.0/infrastructure-components.yaml" - type: url - contract: v1beta1 - files: - # The published v0.7.0 metadata.yaml is missing its own "0.7" releaseSeries - # entry, so clusterctl can't resolve the version's contract and init fails. - # Override it with the repo metadata.yaml, which declares 0.7 as v1beta1. - - sourcePath: "../../../metadata.yaml" - - sourcePath: "../data/infrastructure-ionoscloud/cluster-template.yaml" - - name: v0.8.99 - value: "../../../config/default" - replacements: - - old: ghcr.io/ionos-cloud/cluster-api-provider-ionoscloud:dev - new: ghcr.io/ionos-cloud/cluster-api-provider-ionoscloud:e2e - - old: "--leader-elect" - new: "--leader-elect\n - --insecure-diagnostics" - files: - - sourcePath: "../../../metadata.yaml" - - sourcePath: "../data/infrastructure-ionoscloud/cluster-template.yaml" - - sourcePath: "../data/infrastructure-ionoscloud/cluster-template-ipam.yaml" - - sourcePath: "../data/infrastructure-ionoscloud/cluster-template-image-selector.yaml" -variables: - # Default variables for the e2e test; those values could be overridden via env variables, thus - # allowing the same e2e config file to be reused in different Prow jobs e.g. each one with a K8s version permutation. - # The following Kubernetes versions should be the latest versions with already published kindest/node images. - # This avoids building node images in the default case which improves the test duration significantly. - KUBERNETES_VERSION: "v1.30.6" - IP_FAMILY: "ipv4" - CNI: "./data/cni/calico.yaml" - KUBETEST_CONFIGURATION: "./data/kubetest/conformance.yaml" - CLUSTER_NAME: "e2e-cluster-${RANDOM}" - # Enabling the feature flags by setting the env variables. - # Note: EXP_CLUSTER_RESOURCE_SET is enabled per default with CAPI v1.7.0. - # We still have to enable them here for clusterctl upgrade tests that use older versions. - EXP_CLUSTER_RESOURCE_SET: "true" - IONOSCLOUD_MACHINE_MEMORY_MB: "" - IONOSCLOUD_MACHINE_NUM_CORES: "" - IONOSCLOUD_MACHINE_SSH_KEYS: "" - IONOSCLOUD_DATACENTER_ID: "" - CONTROL_PLANE_ENDPOINT_IP: "" - CONTROL_PLANE_ENDPOINT_LOCATION: "" - IONOSCLOUD_MACHINE_IMAGE_ID: "" - -intervals: - default/wait-controllers: [ "30m", "10s" ] - default/wait-cluster: [ "30m", "10s" ] - default/wait-control-plane: [ "30m", "10s" ] - default/wait-worker-nodes: [ "30m", "10s" ] - default/wait-delete-cluster: [ "30m", "10s" ] - default/wait-nodes-ready: ["30m", "10s"] - scale/wait-cluster: ["30m", "10s"] - scale/wait-control-plane: ["30m", "10s"] - scale/wait-worker-nodes: ["30m", "10s"] -``` - -- [ ] **Step 5: Sanity-check the YAML parses and commit** - -Run: `python3 -c "import yaml; yaml.safe_load(open('test/e2e/config/ionoscloud.yaml'))"` -Expected: no output (valid YAML). If `python3`/`yaml` isn't available, skip — the e2e build step in Task 5 will catch a malformed file anyway. - -```bash -git add test/e2e/config/ionoscloud.yaml test/e2e/data/shared/v1.8/metadata.yaml test/e2e/data/shared/v1.10/metadata.yaml -git commit -m "test(e2e): register CAPI v1.8.12/v1.10.10/v1.12.10 and CAPIC v0.6.3/v0.7.0 for upgrade tests - -Co-Authored-By: Claude Opus 4.8 " -``` - ---- - -### Task 2: Port the upgrade-assertion helpers, stripped of v1beta1 residue - -**Files:** -- Create: `test/e2e/helpers/conditions.go` -- Create: `test/e2e/helpers/clusterhealth.go` - -**Interfaces:** -- Produces: `helpers.AssertConditionsMigration(ctx context.Context) func(framework.ClusterProxy, string, string)`, `helpers.AssertCAPIV1Beta2Migration(ctx context.Context) func(framework.ClusterProxy, string, string)`, `helpers.AssertV1Beta1ClusterAndMachinesHealthy(ctx context.Context) func(framework.ClusterProxy, string, string)` — all consumed by Task 3's `upgrade_test.go`. - -- [ ] **Step 1: Create `test/e2e/helpers/conditions.go`** - -This is `e2e-suite-v1.12`'s version of the file with the `deprecatedReady`/`Status.Deprecated.V1Beta1` assertion removed — that field no longer exists after SP1's clean cut, and there is nothing left to migrate *to* it, so the check is dropped rather than adapted: - -```go -//go:build e2e - -/* -Copyright 2024 IONOS Cloud. - -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 helpers - -import ( - "context" - - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - clusterctlcluster "sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster" - "sigs.k8s.io/cluster-api/test/framework" - runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" - - infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" - - . "github.com/onsi/gomega" -) - -// AssertConditionsMigration returns a PostUpgrade hook that verifies CAPIC resources -// carry the v1beta2 status fields after upgrading from v0.6.x to v0.8+. -func AssertConditionsMigration(ctx context.Context) func(framework.ClusterProxy, string, string) { - return func(proxy framework.ClusterProxy, namespace, _ string) { - c := proxy.GetClient() - - clusters := &infrav1.IonosCloudClusterList{} - Expect(c.List(ctx, clusters, runtimeclient.InNamespace(namespace))).To(Succeed()) - Expect(clusters.Items).NotTo(BeEmpty(), - "expected at least one IonosCloudCluster in namespace %s", namespace) - - for i := range clusters.Items { - assertClusterConditionsMigrated(&clusters.Items[i]) - } - - machines := &infrav1.IonosCloudMachineList{} - Expect(c.List(ctx, machines, runtimeclient.InNamespace(namespace))).To(Succeed()) - Expect(machines.Items).NotTo(BeEmpty(), - "expected at least one IonosCloudMachine in namespace %s", namespace) - - for i := range machines.Items { - assertMachineConditionsMigrated(&machines.Items[i]) - } - } -} - -// AssertCAPIV1Beta2Migration returns a PostUpgrade hook that verifies CAPI core -// v1beta2 types are active and the Cluster is available after upgrading to v1.11+. -func AssertCAPIV1Beta2Migration(ctx context.Context) func(framework.ClusterProxy, string, string) { - return func(proxy framework.ClusterProxy, namespace, clusterName string) { - c := proxy.GetClient() - - // Assert the clusters.cluster.x-k8s.io CRD storage migration to v1beta2 has completed - // (status.storedVersions == [v1beta2], not just spec.versions[].storage). - framework.ValidateCRDMigration(ctx, proxy, namespace, clusterName, - func(crd apiextensionsv1.CustomResourceDefinition) bool { - return crd.Name == "clusters.cluster.x-k8s.io" - }, - clusterctlcluster.FilterClusterObjectsWithNameFilter(clusterName)) - - // Assert Cluster has Available=True. - framework.VerifyClusterAvailable(ctx, framework.VerifyClusterAvailableInput{ - Getter: proxy.GetClient(), - Name: clusterName, - Namespace: namespace, - }) - - // Assert no continuous reconcile loop (resource versions stable). - framework.ValidateResourceVersionStable(ctx, framework.ValidateResourceVersionStableInput{ - ClusterProxy: proxy, - Namespace: namespace, - OwnerGraphFilterFunction: clusterctlcluster.FilterClusterObjectsWithNameFilter(clusterName), - }) - - // Assert CAPIC resources also carry the v1beta2 fields (upgrade included CAPIC too). - clusters := &infrav1.IonosCloudClusterList{} - Expect(c.List(ctx, clusters, runtimeclient.InNamespace(namespace))).To(Succeed()) - for i := range clusters.Items { - assertClusterConditionsMigrated(&clusters.Items[i]) - } - - machines := &infrav1.IonosCloudMachineList{} - Expect(c.List(ctx, machines, runtimeclient.InNamespace(namespace))).To(Succeed()) - for i := range machines.Items { - assertMachineConditionsMigrated(&machines.Items[i]) - } - } -} - -func assertClusterConditionsMigrated(cluster *infrav1.IonosCloudCluster) { - assertConditionsMigrated("IonosCloudCluster", cluster.Name, - cluster.Status.Initialization.Provisioned, cluster.Status.Conditions) -} - -func assertMachineConditionsMigrated(machine *infrav1.IonosCloudMachine) { - assertConditionsMigrated("IonosCloudMachine", machine.Name, - machine.Status.Initialization.Provisioned, machine.Status.Conditions) -} - -// assertConditionsMigrated verifies the v1beta2 status shape shared by IonosCloudCluster -// and IonosCloudMachine after upgrading from v0.6.x/v0.7.x to v0.8+, including that any -// condition carried over from the old v1beta1 conditions API (empty Reason) was backfilled -// by api/v1alpha1.BackfillLegacyConditionReasons rather than rejected by the new schema. -func assertConditionsMigrated(kind, name string, provisioned *bool, conditions []metav1.Condition) { - // status.initialization.provisioned must be set and true. - Expect(provisioned).NotTo(BeNil(), - "%s %s: status.initialization.provisioned must be set after upgrade", kind, name) - Expect(*provisioned).To(BeTrue(), - "%s %s: status.initialization.provisioned must be true after upgrade", kind, name) - - // status.conditions (metav1.Condition) must be non-empty. - Expect(conditions).NotTo(BeEmpty(), - "%s %s: status.conditions must be non-empty after upgrade", kind, name) - hasTrue := false - for _, cond := range conditions { - if cond.Status == metav1.ConditionTrue { - hasTrue = true - } - Expect(cond.Reason).NotTo(BeEmpty(), - "%s %s: condition %q must carry a non-empty Reason after upgrade "+ - "(legacy v1beta1 conditions must be backfilled, see api/v1alpha1.BackfillLegacyConditionReasons)", - kind, name, cond.Type) - } - Expect(hasTrue).To(BeTrue(), - "%s %s: at least one metav1.Condition must have Status=True", kind, name) -} -``` - -- [ ] **Step 2: Create `test/e2e/helpers/clusterhealth.go` verbatim** - -This file only reads the CAPI-core `clusterv1beta1.Cluster`/`MachineList` `.Status.Phase` — nothing here touches our provider's removed fields, so it ports unchanged: - -```go -//go:build e2e - -/* -Copyright 2024 IONOS Cloud. - -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 helpers - -import ( - "context" - - clusterv1beta1 "sigs.k8s.io/cluster-api/api/core/v1beta1" //nolint:staticcheck // only contract CAPI v1.10.x serves - "sigs.k8s.io/cluster-api/test/framework" - runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" - - . "github.com/onsi/gomega" -) - -// AssertV1Beta1ClusterAndMachinesHealthy is a PostUpgrade hook checking Cluster/Machine -// Status.Phase only — safe to use when the upgrade target doesn't serve v1beta2 yet. -func AssertV1Beta1ClusterAndMachinesHealthy(ctx context.Context) func(framework.ClusterProxy, string, string) { - return func(proxy framework.ClusterProxy, namespace, clusterName string) { - c := proxy.GetClient() - - cluster := &clusterv1beta1.Cluster{} - Expect(c.Get(ctx, runtimeclient.ObjectKey{Namespace: namespace, Name: clusterName}, cluster)).To(Succeed()) - Expect(cluster.Status.Phase).To(Equal(string(clusterv1beta1.ClusterPhaseProvisioned)), - "Cluster %s should be in the Provisioned phase", clusterName) - - machines := &clusterv1beta1.MachineList{} - Expect(c.List(ctx, machines, runtimeclient.InNamespace(namespace), - runtimeclient.MatchingLabels{clusterv1beta1.ClusterNameLabel: clusterName})).To(Succeed()) - Expect(machines.Items).NotTo(BeEmpty(), "expected at least one Machine for cluster %s", clusterName) - - for _, m := range machines.Items { - Expect(m.Status.Phase).To(Equal(string(clusterv1beta1.MachinePhaseRunning)), - "Machine %s should be in the Running phase", m.Name) - } - } -} -``` - -- [ ] **Step 3: Compile-check and commit** - -Run: `go build -tags e2e ./test/e2e/...` -Expected: builds clean (both helpers only reference types that already exist on this branch: `infrav1.IonosCloudClusterList/IonosCloudMachineList`, `Status.Initialization.Provisioned`, `Status.Conditions` — none of the removed `Status.Deprecated` fields). - -```bash -git add test/e2e/helpers/conditions.go test/e2e/helpers/clusterhealth.go -git commit -m "test(e2e): add upgrade PostUpgrade assertion helpers for conditions migration - -Co-Authored-By: Claude Opus 4.8 " -``` - ---- - -### Task 3: Port and restage `upgrade_test.go` - -**Files:** -- Create: `test/e2e/upgrade_test.go` - -**Interfaces:** -- Consumes: `helpers.AssertConditionsMigration`, `helpers.AssertCAPIV1Beta2Migration`, `helpers.AssertV1Beta1ClusterAndMachinesHealthy` (Task 2); `e2eConfig`, `clusterctlConfigPath`, `bootstrapClusterProxy`, `artifactFolder`, `skipCleanup`, `cloudEnv.createCredentialsSecretPNC`, `KubernetesVersion` (all pre-existing in `test/e2e/suite_test.go`/`common.go`, unchanged by this plan). - -- [ ] **Step 1: Create `test/e2e/upgrade_test.go`** - -Three blocks, ported from `e2e-suite-v1.12`'s `upgrade_test.go` with two changes: every `v0.6.99` local-dev infra-provider reference becomes `v0.8.99`, and the first ("single-hop") block is restaged to go through the published `v0.7.0` release instead of jumping straight from `v0.6.3` to the local build — the direct jump would otherwise silently skip proving the `v0.6.3 → v0.7.0` hop that `BackfillLegacyConditionReasons` (`api/v1alpha1/conditions_migration.go`) exists to protect: - -```go -//go:build e2e - -/* -Copyright 2024 IONOS Cloud. - -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 e2e - -import ( - capie2e "sigs.k8s.io/cluster-api/test/e2e" - - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/test/e2e/helpers" - - . "github.com/onsi/ginkgo/v2" -) - -// upgradeSpecBaseInput returns the ClusterctlUpgradeSpecInput fields shared by all -// upgrade test blocks. Each block overrides init/upgrade versions and PostUpgrade. -func upgradeSpecBaseInput() capie2e.ClusterctlUpgradeSpecInput { - return capie2e.ClusterctlUpgradeSpecInput{ - E2EConfig: e2eConfig, - ClusterctlConfigPath: clusterctlConfigPath, - BootstrapClusterProxy: bootstrapClusterProxy, - ArtifactFolder: artifactFolder, - SkipCleanup: skipCleanup, - UseKindForManagementCluster: true, - PostNamespaceCreated: cloudEnv.createCredentialsSecretPNC, - // InitWithKubernetesVersion is required by ClusterctlUpgradeSpec: it is the - // Kubernetes version of the secondary (kind) management cluster the old - // providers are installed into. Reuse the suite's configured version. - InitWithKubernetesVersion: e2eConfig.MustGetVariable(KubernetesVersion), - ControlPlaneMachineCount: new(int64(1)), - } -} - -var _ = Describe("Should migrate CAPIC conditions from v1beta1 to metav1.Condition on the staged v0.6.3 -> v0.7.0 -> v0.8 provider upgrade", Label("upgrade", "single-hop"), func() { - capie2e.ClusterctlUpgradeSpec(ctx, func() capie2e.ClusterctlUpgradeSpecInput { - in := upgradeSpecBaseInput() - in.InitWithBinary = "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.8.12/clusterctl-{OS}-{ARCH}" - in.InitWithCoreProvider = "cluster-api:v1.8.12" - // Pin bootstrap/control-plane to the same version as core. Without this they - // fall back to the latest matching the "*" contract (v1.12.10), which would - // mismatch the v1.8.12 core provider and fail clusterctl init. - in.InitWithBootstrapProviders = []string{"kubeadm:v1.8.12"} - in.InitWithControlPlaneProviders = []string{"kubeadm:v1.8.12"} - in.InitWithInfrastructureProviders = []string{"ionoscloud:v0.6.3"} - in.Upgrades = []capie2e.ClusterctlUpgradeSpecInputUpgrade{ - { - // Real released hop: proves v0.6.3 (v1.8.12, v1beta1) -> v0.7.0 (v1.10.10, - // still pure v1beta1 — see docs/capi-upgrade-plan.md) survives before the - // pure-v1beta2 v0.8 cut is applied. - WithBinary: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/clusterctl-{OS}-{ARCH}", - CoreProvider: "cluster-api:v1.10.10", - InfrastructureProviders: []string{"ionoscloud:v0.7.0"}, - PostUpgrade: helpers.AssertV1Beta1ClusterAndMachinesHealthy(ctx), - }, - { - // This hop lands the pure-v1beta2 v0.8 provider (local build) on a - // v0.7.0-created cluster and proves the legacy v1beta1 conditions - // (empty Reason) are backfilled, not rejected by the new schema. - CoreProvider: "cluster-api:v1.11.11", - InfrastructureProviders: []string{"ionoscloud:v0.8.99"}, - PostUpgrade: helpers.AssertConditionsMigration(ctx), - }, - { - // Extra hop bumping CAPI core to v1.12.10. The v0.8.99 provider is - // already built against v1.12.10, so this exercises the v1.11 -> v1.12 - // core upgrade and confirms the migrated conditions stay intact. - CoreProvider: "cluster-api:v1.12.10", - InfrastructureProviders: []string{"ionoscloud:v0.8.99"}, - PostUpgrade: helpers.AssertCAPIV1Beta2Migration(ctx), - }, - } - return in - }) -}) - -var _ = Describe("Should keep a v1.8-created cluster reconcilable after staging the upgrade through v1.10 and v1.11 before reaching v1.12", Label("upgrade", "multi-hop"), func() { - capie2e.ClusterctlUpgradeSpec(ctx, func() capie2e.ClusterctlUpgradeSpecInput { - in := upgradeSpecBaseInput() - in.InitWithBinary = "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.8.12/clusterctl-{OS}-{ARCH}" - in.InitWithCoreProvider = "cluster-api:v1.8.12" - in.InitWithBootstrapProviders = []string{"kubeadm:v1.8.12"} - in.InitWithControlPlaneProviders = []string{"kubeadm:v1.8.12"} - in.InitWithInfrastructureProviders = []string{"ionoscloud:v0.6.3"} - in.Upgrades = []capie2e.ClusterctlUpgradeSpecInputUpgrade{ - { - // Landing point this block validates. Must not be the last entry (v1.10 has - // no v1beta2 API). WithBinary is required too: the in-process v1.12.10 - // clusterctl client refuses to move the core provider anywhere but v1beta2. - WithBinary: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/clusterctl-{OS}-{ARCH}", - CoreProvider: "cluster-api:v1.10.10", - InfrastructureProviders: []string{"ionoscloud:v0.7.0"}, - PostUpgrade: helpers.AssertV1Beta1ClusterAndMachinesHealthy(ctx), - }, - { - // Intermediate v1beta2 landing at v1.11 before the final v1.12 hop. - CoreProvider: "cluster-api:v1.11.11", - InfrastructureProviders: []string{"ionoscloud:v0.8.99"}, - PostUpgrade: helpers.AssertCAPIV1Beta2Migration(ctx), - }, - { - // Final hop bumping CAPI core to v1.12.10 (the version v0.8.99 is built - // against). Also satisfies the framework's mandatory last-step v1beta2 checks. - CoreProvider: "cluster-api:v1.12.10", - InfrastructureProviders: []string{"ionoscloud:v0.8.99"}, - PostUpgrade: helpers.AssertCAPIV1Beta2Migration(ctx), - }, - } - return in - }) -}) - -// Overlaps with the staged block's second hop on purpose: starting fresh at v1.10.10 -// isolates a real v1.10->v1.11 bug from one caused by the staged block's carried-over state. -var _ = Describe("Should handle CAPI core v1beta2 type migration when upgrading from v1.10 through v1.11 to v1.12", Label("upgrade", "single-hop"), func() { - capie2e.ClusterctlUpgradeSpec(ctx, func() capie2e.ClusterctlUpgradeSpecInput { - in := upgradeSpecBaseInput() - in.InitWithBinary = "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/clusterctl-{OS}-{ARCH}" - in.InitWithCoreProvider = "cluster-api:v1.10.10" - // Pin bootstrap/control-plane to match the v1.10.10 core provider (see block above). - in.InitWithBootstrapProviders = []string{"kubeadm:v1.10.10"} - in.InitWithControlPlaneProviders = []string{"kubeadm:v1.10.10"} - in.InitWithInfrastructureProviders = []string{"ionoscloud:v0.7.0"} - in.Upgrades = []capie2e.ClusterctlUpgradeSpecInputUpgrade{ - { - CoreProvider: "cluster-api:v1.11.11", - InfrastructureProviders: []string{"ionoscloud:v0.8.99"}, - PostUpgrade: helpers.AssertCAPIV1Beta2Migration(ctx), - }, - { - // Extra hop bumping CAPI core to v1.12.10 (the version v0.8.99 is built - // against) to validate the v1.11 -> v1.12 core upgrade. - CoreProvider: "cluster-api:v1.12.10", - InfrastructureProviders: []string{"ionoscloud:v0.8.99"}, - PostUpgrade: helpers.AssertCAPIV1Beta2Migration(ctx), - }, - } - return in - }) -}) -``` - -- [ ] **Step 2: Compile-check and commit** - -Run: `go build -tags e2e ./test/e2e/...` and `go vet -tags e2e ./test/e2e/...` -Expected: both clean. - -```bash -git add test/e2e/upgrade_test.go -git commit -m "test(e2e): add staged upgrade suite for conditions schema and CAPI v1beta2 migration - -Co-Authored-By: Claude Opus 4.8 " -``` - ---- - -### Task 4: CI wiring — Makefile target and scheduled workflow - -**Files:** -- Modify: `Makefile` -- Create: `.github/workflows/e2e-upgrade.yaml` - -**Interfaces:** none (build/CI plumbing only). - -- [ ] **Step 1: Add the `test-e2e-upgrade` Makefile target and exclude `upgrade` from the default e2e run** - -In `Makefile`, find: - -```makefile -GINKGO_LABEL ?= "!Conformance" -``` - -Replace with: - -```makefile -# Exclude the "upgrade" label by default: the provider-upgrade suite is heavy -# (spins up its own kind management cluster and installs old providers per block) -# and is run separately via `make test-e2e-upgrade`. -GINKGO_LABEL ?= "!Conformance && !upgrade" -``` - -Then, immediately after the existing `test-e2e:` target's recipe (the block ending in `-e2e.skip-resource-cleanup=$(SKIP_RESOURCE_CLEANUP) -e2e.use-existing-cluster=$(USE_EXISTING_CLUSTER)`), add: - -```makefile -.PHONY: test-e2e-upgrade -test-e2e-upgrade: docker-build-e2e ## Run only the provider-upgrade e2e tests (Label "upgrade") - CGO_ENABLED=1 go run github.com/onsi/ginkgo/v2/ginkgo -v --trace \ - -poll-progress-after=$(GINKGO_POLL_PROGRESS_AFTER) \ - -poll-progress-interval=$(GINKGO_POLL_PROGRESS_INTERVAL) --tags=e2e --fail-fast \ - --nodes=1 --label-filter="upgrade" --timeout=$(GINKGO_TIMEOUT) --no-color=$(GINKGO_NOCOLOR) \ - --output-dir="$(ARTIFACTS)" --junit-report="junit.e2e_upgrade.1.xml" $(GINKGO_ARGS) $(ROOT_DIR)/$(TEST_DIR)/e2e -- \ - -e2e.artifacts-folder="$(ARTIFACTS)" -e2e.config="$(E2E_CONF_FILE)" \ - -e2e.skip-resource-cleanup=$(SKIP_RESOURCE_CLEANUP) -e2e.use-existing-cluster=$(USE_EXISTING_CLUSTER) -``` - -- [ ] **Step 2: Create the scheduled upgrade-e2e workflow** - -Create `.github/workflows/e2e-upgrade.yaml`: - -```yaml -name: End-to-end upgrade tests -on: - schedule: - # Weekly, matching the conformance suite's cadence. Offset to Tuesday to - # avoid contending for IONOS Cloud resources with conformance (Mondays). - - cron: "0 5 * * 2" - workflow_dispatch: {} -jobs: - e2e-upgrade: - runs-on: ubuntu-latest - environment: e2e - env: - IONOS_TOKEN: ${{ secrets.IONOS_TOKEN }} - IONOSCLOUD_MACHINE_IMAGE_ID: ${{ vars.IONOSCLOUD_MACHINE_IMAGE_ID }} - CONTROL_PLANE_ENDPOINT_LOCATION: ${{ vars.CONTROL_PLANE_ENDPOINT_LOCATION }} - IONOSCLOUD_CUBE_TEMPLATE_ID: ${{ vars.IONOSCLOUD_CUBE_TEMPLATE_ID }} - steps: - - uses: actions/checkout@v4 - - - name: Setup Go environment - uses: actions/setup-go@v5 - with: - go-version-file: "go.mod" - - - name: Run e2e upgrade tests - run: make test-e2e-upgrade - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - if: success() || failure() - with: - name: logs-upgrade - path: _artifacts - retention-days: 7 -``` - -- [ ] **Step 3: Verify the Makefile parses and commit** - -Run: `make -n test-e2e-upgrade | head -5` -Expected: prints the planned `ginkgo` command line without executing it (no error about an unknown target). - -```bash -git add Makefile .github/workflows/e2e-upgrade.yaml -git commit -m "ci(e2e): add scheduled upgrade e2e workflow and test-e2e-upgrade target - -Co-Authored-By: Claude Opus 4.8 " -``` - ---- - -### Task 5: Full verification gate - -**Files:** none created; verification only. - -**Interfaces:** none. - -- [ ] **Step 1: Confirm no v1beta1 production-code residue was reintroduced** - -Run: `grep -rniE "SetV1Beta1Ready|GetV1Beta1Conditions|SetV1Beta1Conditions|WithOwnedV1Beta1Conditions|Status.Deprecated" --include="*.go" internal/ scope/ api/ | grep -v zz_generated` -Expected: no matches. If any appear, the wrong hunks from `e2e-suite-v1.12` were ported — undo them, keeping only the `test/e2e/`, `Makefile`, and `.github/` changes from this plan. - -- [ ] **Step 2: Build everything, including the e2e build tag** - -Run: `go build ./... && go build -tags e2e ./...` -Expected: both succeed with no output. - -- [ ] **Step 3: Vet everything, including the e2e build tag** - -Run: `go vet ./... && go vet -tags e2e ./...` -Expected: both succeed with no output. - -- [ ] **Step 4: Run the full non-e2e test suite** - -Run: `make unit-test` -Expected: all packages `ok`, race detector clean. - -- [ ] **Step 5: Verify generated artifacts and go.mod/go.sum are still up to date** - -Run: `make verify` -Expected: `verify-gen` and `verify-tidy` pass (no diff). This task doesn't touch generated code or dependencies, so it should be a no-op; if it reports a diff, something in Task 1-4 accidentally changed a non-e2e file. - -- [ ] **Step 6: Confirm the new branch sits cleanly on #380** - -Run: `git log --oneline feat/upgrade-cluster-api-v1.12.10..feat/upgrade-e2e-suite-v0.8` -Expected: exactly 4 commits (Tasks 1-4, in order), all authored on top of `b1e7fc4`. - ---- - -## Self-Review - -**Spec coverage** (against `docs/superpowers/specs/2026-07-17-drop-v1beta1-pure-v1beta2-design.md` §6): -- "Rebase `feat/upgrade-e2e-suite` onto #380" → done via fresh branch off `feat/upgrade-cluster-api-v1.12.10`, porting only e2e-scoped files (Tasks 1-4), explicitly excluding the v1beta1-bridge production-code hunks. ✅ -- "Replace the direct `v0.6.3 → v0.6.99(local)` block with the staged path `v0.6.3 → v0.7.0 → v0.8(local build)`" → Task 3, first `Describe` block. ✅ -- "Bump the local dev provider version tag... to `v0.8.99`" → Task 1 Step 4 (config) and Task 3 (test file), every reference renamed. ✅ -- "Reconcile e2e helpers that reference v1beta1" → Task 2: `clusterhealth.go` ports unchanged (only touches CAPI-core v1beta1 types, which still exist and are legitimately asserted mid-upgrade); `conditions.go` drops the `Status.Deprecated.V1Beta1` assertion, replaced with a Reason-non-empty check that documents *why* (backfill, not removal-without-replacement). ✅ -- "Prerequisite satisfied: v0.6.3 and v0.7.0 are both published" → verified as an assumption carried from the existing suite design (`docs/superpowers/specs/2026-06-16-upgrade-e2e-suite-design.md`), not re-derived here. - -**Deviation from the source branch, called out explicitly:** `e2e-suite-v1.12`'s `test/e2e/config/ionoscloud.yaml` declares `v1.11.11`/`v1.12.10` core as `contract: v1beta1` — that branch predates the 2026-07-17 pure-v1beta2 decision. This plan keeps them `v1beta2`, matching the already-committed fix on `feat/upgrade-cluster-api-v1.11.11` (commit `b5db700`) and `feat/upgrade-cluster-api-v1.12.10` (commit `b1e7fc4`). - -**Placeholder scan:** No TBD/TODO; every file-creation step shows complete file content; every command has an expected result. ✅ - -**Type consistency:** `helpers.AssertConditionsMigration` / `AssertCAPIV1Beta2Migration` / `AssertV1Beta1ClusterAndMachinesHealthy` signatures in Task 2 match their call sites in Task 3 exactly (`func(context.Context) func(framework.ClusterProxy, string, string)`). Infra-provider version string `v0.8.99` is consistent across Task 1's config and Task 3's test file. ✅ diff --git a/test/e2e/config/ionoscloud.yaml b/test/e2e/config/ionoscloud.yaml index 1d1faa72..89e538ae 100644 --- a/test/e2e/config/ionoscloud.yaml +++ b/test/e2e/config/ionoscloud.yaml @@ -204,6 +204,14 @@ intervals: default/wait-delete-cluster: [ "30m", "10s" ] default/wait-nodes-ready: ["30m", "10s"] default/wait-resource-versions-become-stable: ["5m", "10s"] + # Read by ClusterctlUpgradeSpec. A missing key makes GetIntervals return nil, which + # Eventually silently treats as Gomega's ~1s default -- producing near-instant timeouts + # that look like product bugs rather than a config gap. + default/wait-resource-versions-remain-stable: ["2m", "5s"] + # Read by cluster_upgrade.go, cluster_upgrade_runtimesdk.go and self_hosted.go. Values + # match upstream's docker.yaml so the upgrade specs behave the same here. + default/wait-control-plane-upgrade: ["15m", "10s"] + default/wait-machine-deployment-upgrade: ["10m", "10s"] scale/wait-cluster: ["30m", "10s"] scale/wait-control-plane: ["30m", "10s"] scale/wait-worker-nodes: ["30m", "10s"] From 3dc0ab4ae557be4f0662e53e6171fdf7e256d53e Mon Sep 17 00:00:00 2001 From: Wassim Kallel Date: Wed, 23 Sep 2026 01:40:00 +0200 Subject: [PATCH 92/94] chore: trim overlong comments Condense the explanatory comments added across the review fixes to a line or two each. The reasoning belongs in the commit messages and PR threads, not inline. Co-Authored-By: Claude Opus 5 --- cmd/main.go | 11 ++++------- config/crd/kustomization.yaml | 6 +----- scope/cluster.go | 6 ++---- test/e2e/common.go | 8 ++------ test/e2e/config/ionoscloud.yaml | 12 ++---------- 5 files changed, 11 insertions(+), 32 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 51928216..64726043 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -47,18 +47,15 @@ import ( const errMsgUnableToCreateController = "unable to create controller" -// validSkipCRDMigrationPhases are the phases accepted by --skip-crd-migration-phases. It is -// derived from the CAPI constants that crdmigrator.setup() switches on, so the flag's help text -// and validation cannot drift from what the migrator actually accepts. +// validSkipCRDMigrationPhases are the phases crdmigrator.setup() accepts. Derived from its +// constants so the help text and validation cannot drift. var validSkipCRDMigrationPhases = []string{ string(crdmigrator.StorageVersionMigrationPhase), string(crdmigrator.CleanupManagedFieldsPhase), } -// validateSkipCRDMigrationPhases rejects unknown --skip-crd-migration-phases values at startup. -// crdmigrator.setup() also rejects them, but only during controller setup, after the manager has -// been built, and it surfaces as a generic "unable to create controller" that does not say which -// value was wrong. +// validateSkipCRDMigrationPhases rejects unknown values at startup, where the error can name the +// offending value, rather than during controller setup where crdmigrator reports it generically. func validateSkipCRDMigrationPhases(phases []string) error { for _, phase := range phases { if !slices.Contains(validSkipCRDMigrationPhases, phase) { diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 18fed15f..c9d23f8a 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -9,11 +9,7 @@ resources: #+kubebuilder:scaffold:crdkustomizeresource # https://cluster-api.sigs.k8s.io/developer/providers/contracts.html#api-version-labels -# Only v1beta2 is advertised: v0.8 dropped status.ready, which is the field the v1beta1 -# contract reads for provisioning (CAPI internal/contract/infrastructure_cluster.go -# Provisioned("v1beta1") -> status.ready). Keeping the v1beta1 label would make a -# v1beta1-only core accept this provider and then poll a field that no longer exists, -# hanging silently instead of failing with "no versions matching contract". +# Only v1beta2: v0.8 dropped status.ready, which the v1beta1 contract reads. commonLabels: cluster.x-k8s.io/v1beta2: v1alpha1 diff --git a/scope/cluster.go b/scope/cluster.go index e379d7ea..661d25ef 100644 --- a/scope/cluster.go +++ b/scope/cluster.go @@ -239,10 +239,8 @@ func (c *Cluster) PatchObject() error { return patchErr } - // Logged, not returned: Finalize retries on any error, so a persistent summary failure - // would re-patch on every backoff step without ever resolving. The condition is - // observability only -- CAPI mirrors it onto Cluster InfrastructureReady but falls back - // to status.initialization.provisioned when it is absent. + // Logged, not returned: Finalize retries on any error, and the condition is observability + // only -- CAPI falls back to status.initialization.provisioned when it is absent. if summaryErr != nil { ctrl.Log.WithName("scope.Cluster").Error(summaryErr, "failed to set the Ready summary condition", diff --git a/test/e2e/common.go b/test/e2e/common.go index 7d54eb76..ee856f9c 100644 --- a/test/e2e/common.go +++ b/test/e2e/common.go @@ -29,13 +29,9 @@ import ( const ( CNIPath = "CNI" CNIResources = "CNI_RESOURCES" - // KubernetesVersion is the version of the workload clusters the specs create. + // KubernetesVersion is the workload cluster version. KubernetesVersion = "KUBERNETES_VERSION" - // KubernetesVersionManagement is the version of the kind bootstrap cluster. It is kept - // separate from KubernetesVersion because the two have different constraints: the - // management cluster must be within the CAPI release's supported management range and - // have a pre-built kindest/node image, while the workload version must match the IONOS - // node image the machines boot. + // KubernetesVersionManagement is the kind bootstrap cluster version. KubernetesVersionManagement = "KUBERNETES_VERSION_MANAGEMENT" defaultCloudResourceNamePrefix = "capic-e2e-test-" diff --git a/test/e2e/config/ionoscloud.yaml b/test/e2e/config/ionoscloud.yaml index 85a1a506..7a51e89b 100644 --- a/test/e2e/config/ionoscloud.yaml +++ b/test/e2e/config/ionoscloud.yaml @@ -71,17 +71,9 @@ variables: # allowing the same e2e config file to be reused in different Prow jobs e.g. each one with a K8s version permutation. # The following Kubernetes versions should be the latest versions with already published kindest/node images. # This avoids building node images in the default case which improves the test duration significantly. - # - # KUBERNETES_VERSION is the workload cluster version. It must match the IONOS node image - # the machines boot (IONOSCLOUD_MACHINE_IMAGE_ID), otherwise kubeadm and kubelet on the - # node disagree with the requested control plane version and the suite waits for nodes - # that never appear. v1.30 remains supported for workload clusters on CAPI v1.12. + # Workload version: must match the IONOS node image (IONOSCLOUD_MACHINE_IMAGE_ID). KUBERNETES_VERSION: "v1.30.6" - # KUBERNETES_VERSION_MANAGEMENT is the kind bootstrap cluster version (suite_test.go). - # It has different constraints: it must sit inside the CAPI release's supported - # management range -- v1.31 to v1.34 for v1.12, where v1.29/v1.30 are workload-only -- - # and have a pre-built kindest/node image in cluster-api/test's kind mapper, since - # GetMapping needs an exact patch match and otherwise falls back to an unpinned tag. + # Management (kind) version: must be in CAPI's supported management range, v1.31-v1.34 for v1.12. KUBERNETES_VERSION_MANAGEMENT: "v1.33.7" CNI: "./data/cni/calico.yaml" KUBETEST_CONFIGURATION: "./data/kubetest/conformance.yaml" From 60bce0328b75fc6ba6b5531cdf1ebca75c90a186 Mon Sep 17 00:00:00 2001 From: Wassim Kallel Date: Wed, 23 Sep 2026 04:21:48 -0700 Subject: [PATCH 93/94] Update test/e2e/config/ionoscloud.yaml Co-authored-by: Jonas Riedel <138458199+jriedel-ionos@users.noreply.github.com> --- test/e2e/config/ionoscloud.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/config/ionoscloud.yaml b/test/e2e/config/ionoscloud.yaml index 7a51e89b..63021dc1 100644 --- a/test/e2e/config/ionoscloud.yaml +++ b/test/e2e/config/ionoscloud.yaml @@ -73,7 +73,7 @@ variables: # This avoids building node images in the default case which improves the test duration significantly. # Workload version: must match the IONOS node image (IONOSCLOUD_MACHINE_IMAGE_ID). KUBERNETES_VERSION: "v1.30.6" - # Management (kind) version: must be in CAPI's supported management range, v1.31-v1.34 for v1.12. + # Management (kind) version: must be in CAPI's supported management range, v1.31-v1.35 for v1.12. KUBERNETES_VERSION_MANAGEMENT: "v1.33.7" CNI: "./data/cni/calico.yaml" KUBETEST_CONFIGURATION: "./data/kubetest/conformance.yaml" From 72502e8b924209edc74b352818a10f0da51ecf52 Mon Sep 17 00:00:00 2001 From: Wassim Kallel Date: Wed, 23 Sep 2026 13:53:00 +0200 Subject: [PATCH 94/94] test(e2e): bump workload KUBERNETES_VERSION to v1.34.11 Matches the new IONOSCLOUD_MACHINE_IMAGE_ID node image. Co-Authored-By: Claude Opus 5 (1M context) --- test/e2e/config/ionoscloud.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/config/ionoscloud.yaml b/test/e2e/config/ionoscloud.yaml index 63021dc1..d197f27e 100644 --- a/test/e2e/config/ionoscloud.yaml +++ b/test/e2e/config/ionoscloud.yaml @@ -72,7 +72,7 @@ variables: # The following Kubernetes versions should be the latest versions with already published kindest/node images. # This avoids building node images in the default case which improves the test duration significantly. # Workload version: must match the IONOS node image (IONOSCLOUD_MACHINE_IMAGE_ID). - KUBERNETES_VERSION: "v1.30.6" + KUBERNETES_VERSION: "v1.34.11" # Management (kind) version: must be in CAPI's supported management range, v1.31-v1.35 for v1.12. KUBERNETES_VERSION_MANAGEMENT: "v1.33.7" CNI: "./data/cni/calico.yaml"