diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c69ddafc..9a773796 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,15 +17,14 @@ jobs: - name: Run lint run: "make lint" - # TODO(lubedacht) include later - # yamllint: - # name: yamllint - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v4 - # - uses: ibiqlik/action-yamllint@v3 - # with: - # format: github + yamllint: + name: yamllint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ibiqlik/action-yamllint@v3 + with: + format: github actionlint: name: actionlint diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 93b6cd37..a9ac3341 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,10 +11,3 @@ jobs: go-version-file: go.mod - name: Run tests run: "make test" - - # TODO(lubedacht) include later - # - name: SonarCloud Scan - # uses: SonarSource/sonarcloud-github-action@v2.0.2 - # env: - # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any - # SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.yamllint b/.yamllint new file mode 100644 index 00000000..e1c09826 --- /dev/null +++ b/.yamllint @@ -0,0 +1,34 @@ +--- +extends: default + +rules: + # the default of 80 is overly-restrictive, particularly when nested + line-length: + max: 120 + level: warning + # as this repository also contains generated yaml, we only enforce + # indentation consistency within a file + indentation: + spaces: consistent + indent-sequences: consistent + level: warning + comments: + min-spaces-from-content: 1 + # comments-indentation linting has unwanted edgecases: + # https://github.com/adrienverge/yamllint/issues/141 + comments-indentation: disable + +ignore: +# generated files +- config/crd +- config/certmanager +- config/prometheus +- config/rbac +- test/e2e +- out +- .*.yaml +- .*.yml +# these are clusterctl templates, not yaml +- templates +# github actions checked by actionlint +- .github/workflows diff --git a/api/v1alpha1/ionoscloudcluster_types.go b/api/v1alpha1/ionoscloudcluster_types.go index 3709855b..103118a1 100644 --- a/api/v1alpha1/ionoscloudcluster_types.go +++ b/api/v1alpha1/ionoscloudcluster_types.go @@ -27,6 +27,10 @@ const ( // associated with the IonosCloudCluster before removing it from the API server. ClusterFinalizer = "ionoscloudcluster.infrastructure.cluster.x-k8s.io" + // ClusterCredentialsFinalizer allows cleanup of resources, which are + // associated with the IonosCloudCluster credentials before removing it from the API server. + ClusterCredentialsFinalizer = ClusterFinalizer + "/credentials" + // IonosCloudClusterReady is the condition for the IonosCloudCluster, which indicates that the cluster is ready. IonosCloudClusterReady clusterv1.ConditionType = "ClusterReady" diff --git a/api/v1alpha1/ionoscloudcluster_types_test.go b/api/v1alpha1/ionoscloudcluster_types_test.go index 2382821e..9e3d5b3b 100644 --- a/api/v1alpha1/ionoscloudcluster_types_test.go +++ b/api/v1alpha1/ionoscloudcluster_types_test.go @@ -73,6 +73,11 @@ var _ = Describe("IonosCloudCluster", func() { It("should allow creating valid clusters", func() { Expect(k8sClient.Create(context.Background(), defaultCluster())).To(Succeed()) }) + It("should work with a FQDN controlplane endpoint", func() { + cluster := defaultCluster() + cluster.Spec.ControlPlaneEndpoint.Host = "example.org" + Expect(k8sClient.Create(context.Background(), cluster)).To(Succeed()) + }) It("should not allow creating clusters with empty credential secret", func() { cluster := defaultCluster() cluster.Spec.CredentialsRef.Name = "" diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index 63cba88b..083b6612 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -146,9 +146,9 @@ type IonosCloudMachineSpec struct { // // If the machine is a control plane machine, this field will not be taken into account. //+kubebuilder:validation:XValidation:rule="self == oldSelf",message="failoverIP is immutable" - //+kubebuilder:validation:XValidation:rule=`self == "" || self == "AUTO" || self.matches("((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$")`,message="failoverIP must be either 'AUTO' or a valid IPv4 address" + //+kubebuilder:validation:XValidation:rule=`self == "AUTO" || self.matches("((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$")`,message="failoverIP must be either 'AUTO' or a valid IPv4 address" //+optional - FailoverIP string `json:"failoverIP"` + FailoverIP *string `json:"failoverIP,omitempty"` } // Networks contains a list of additional LAN IDs diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index b72cd6ba..533025d4 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -342,24 +342,24 @@ var _ = Describe("IonosCloudMachine Tests", func() { Context("FailoverIP", func() { It("should allow setting AUTO as the value", func() { m := defaultMachine() - m.Spec.FailoverIP = CloudResourceConfigAuto + m.Spec.FailoverIP = ptr.To(CloudResourceConfigAuto) Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(m.Spec.FailoverIP).To(Equal(CloudResourceConfigAuto)) + Expect(m.Spec.FailoverIP).To(Equal(ptr.To(CloudResourceConfigAuto))) }) It("should allow setting a valid IPv4 address", func() { m := defaultMachine() - m.Spec.FailoverIP = "203.0.113.1" + m.Spec.FailoverIP = ptr.To("203.0.113.1") Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(m.Spec.FailoverIP).To(Equal("203.0.113.1")) + Expect(m.Spec.FailoverIP).To(Equal(ptr.To("203.0.113.1"))) }) - It("should allow setting empty string", func() { + It("should allow setting null", func() { m := defaultMachine() Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(m.Spec.FailoverIP).To(Equal("")) + Expect(m.Spec.FailoverIP).To(BeNil()) }) DescribeTable("should not allow setting invalid IPv4 addresses", func(ip string) { m := defaultMachine() - m.Spec.FailoverIP = ip + m.Spec.FailoverIP = &ip Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }, Entry("IPv4 out of range", "203.0.113.256"), @@ -370,17 +370,17 @@ var _ = Describe("IonosCloudMachine Tests", func() { ) It("should require AUTO to be in capital letters", func() { m := defaultMachine() - m.Spec.FailoverIP = "Auto" + m.Spec.FailoverIP = ptr.To("Auto") Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }) It("should be immutable", func() { m := defaultMachine() - m.Spec.FailoverIP = "AUTO" + m.Spec.FailoverIP = ptr.To(CloudResourceConfigAuto) Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(m.Spec.FailoverIP).To(Equal("AUTO")) - m.Spec.FailoverIP = "127.0.0.1" + Expect(m.Spec.FailoverIP).To(Equal(ptr.To(CloudResourceConfigAuto))) + m.Spec.FailoverIP = ptr.To("127.0.0.1") Expect(k8sClient.Update(context.Background(), m)).ToNot(Succeed()) - m.Spec.FailoverIP = "" + m.Spec.FailoverIP = ptr.To("") Expect(k8sClient.Update(context.Background(), m)).ToNot(Succeed()) }) }) diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 40606f5e..9aef4f4c 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -233,6 +233,11 @@ func (in *IonosCloudMachineSpec) DeepCopyInto(out *IonosCloudMachineSpec) { *out = make(Networks, len(*in)) copy(*out, *in) } + if in.FailoverIP != nil { + in, out := &in.FailoverIP, &out.FailoverIP + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudMachineSpec. 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 b1939417..13c837bf 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml @@ -158,7 +158,7 @@ spec: - message: failoverIP is immutable rule: self == oldSelf - message: failoverIP must be either 'AUTO' or a valid IPv4 address - rule: self == "" || self == "AUTO" || self.matches("((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$") + rule: self == "AUTO" || self.matches("((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$") memoryMB: default: 3072 description: |- 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 c507efea..882c020a 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachinetemplates.yaml @@ -176,7 +176,7 @@ spec: rule: self == oldSelf - message: failoverIP must be either 'AUTO' or a valid IPv4 address - rule: self == "" || self == "AUTO" || self.matches("((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$") + rule: self == "AUTO" || self.matches("((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$") memoryMB: default: 3072 description: |- diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index 79367f05..0017ee90 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -1,3 +1,4 @@ +--- # Adds namespace to all resources. namePrefix: capic- namespace: capic-system @@ -12,29 +13,28 @@ resources: - ../manager # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml -#- ../webhook +# - ../webhook # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. -#- ../certmanager +# - ../certmanager # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. -#- ../prometheus +# - ../prometheus patchesStrategicMerge: - manager_image_patch.yaml - # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml -#- manager_webhook_patch.yaml +# - manager_webhook_patch.yaml # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. # 'CERTMANAGER' needs to be enabled to use ca injection -#- webhookcainjection_patch.yaml +# - webhookcainjection_patch.yaml # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. # Uncomment the following replacements to add the cert-manager CA injection annotations -#replacements: +# replacements: # - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs # kind: Certificate # group: cert-manager.io diff --git a/config/default/manager_config_patch.yaml b/config/default/manager_config_patch.yaml index f6f58916..ec4ccc0b 100644 --- a/config/default/manager_config_patch.yaml +++ b/config/default/manager_config_patch.yaml @@ -1,3 +1,4 @@ +--- apiVersion: apps/v1 kind: Deployment metadata: diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index ad13e96b..28684b4e 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -1,3 +1,4 @@ +--- resources: - manager.yaml apiVersion: kustomize.config.k8s.io/v1beta1 diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 19e0249d..64cb465e 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -1,3 +1,4 @@ +--- apiVersion: v1 kind: Namespace metadata: @@ -73,14 +74,14 @@ spec: image: controller:latest name: manager ports: - - containerPort: 8443 - name: diagnostics - protocol: TCP + - containerPort: 8443 + name: diagnostics + protocol: TCP securityContext: allowPrivilegeEscalation: false capabilities: drop: - - "ALL" + - "ALL" livenessProbe: httpGet: path: /healthz @@ -122,9 +123,7 @@ spec: selector: control-plane: controller-manager ports: - - name: diagnostics-svc - protocol: TCP - port: 8443 - targetPort: diagnostics - - + - name: diagnostics-svc + protocol: TCP + port: 8443 + targetPort: diagnostics diff --git a/config/samples/infrastructure_v1alpha1_ionoscloudcluster.yaml b/config/samples/infrastructure_v1alpha1_ionoscloudcluster.yaml index 84befb1a..131aab82 100644 --- a/config/samples/infrastructure_v1alpha1_ionoscloudcluster.yaml +++ b/config/samples/infrastructure_v1alpha1_ionoscloudcluster.yaml @@ -1,3 +1,4 @@ +--- apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 kind: IonosCloudCluster metadata: diff --git a/config/samples/infrastructure_v1alpha1_ionoscloudmachine.yaml b/config/samples/infrastructure_v1alpha1_ionoscloudmachine.yaml index 04ab3475..2293710e 100644 --- a/config/samples/infrastructure_v1alpha1_ionoscloudmachine.yaml +++ b/config/samples/infrastructure_v1alpha1_ionoscloudmachine.yaml @@ -1,3 +1,4 @@ +--- apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 kind: IonosCloudMachine metadata: diff --git a/config/samples/infrastructure_v1alpha1_ionoscloudmachinetemplate.yaml b/config/samples/infrastructure_v1alpha1_ionoscloudmachinetemplate.yaml index 17716777..652dfcad 100644 --- a/config/samples/infrastructure_v1alpha1_ionoscloudmachinetemplate.yaml +++ b/config/samples/infrastructure_v1alpha1_ionoscloudmachinetemplate.yaml @@ -1,3 +1,4 @@ +--- apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 kind: IonosCloudMachineTemplate metadata: diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 995562cd..ddc5c7bd 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -1,6 +1,7 @@ +--- ## Append samples of your project ## resources: - infrastructure_v1alpha1_ionoscloudcluster.yaml - infrastructure_v1alpha1_ionoscloudmachine.yaml - infrastructure_v1alpha1_ionoscloudmachinetemplate.yaml -#+kubebuilder:scaffold:manifestskustomizesamples +# +kubebuilder:scaffold:manifestskustomizesamples diff --git a/docs/quickstart.md b/docs/quickstart.md index e290638b..c6ffb8d0 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -39,19 +39,23 @@ CAPIC requires several environment variables to be set in order to create a Kube They can be exported or saved inside the clusterctl config file at `~/.cluster-api/clusterctl.yaml` ```env -## -- Cloud specific environment variables -- ## +## -- Cloud-specific environment variables -- ## IONOS_TOKEN # The token of the IONOS Cloud account. -IONOS_API_URL # The API URL of the IONOS Cloud account. - # Defaults to https://api.ionos.com/cloudapi/v6 - -## -- Cluster API related environment variables -- ## -CONTROL_PLANE_ENDPOINT_IP # The IP address of the control plane endpoint. -CONTROL_PLANE_ENDPOINT_PORT # The port of the control plane endpoint. +IONOS_API_URL # The API URL of the IONOS Cloud account (optional). + # Defaults to https://api.ionos.com/cloudapi/v6. + +## -- Cluster API-related environment variables -- ## +CONTROL_PLANE_ENDPOINT_HOST # The control plane endpoint host (optional). + # If it's not an IP but an FQDN, the provider must be able to resolve it + # to the value for CONTROL_PLANE_ENDPOINT_IP. +CONTROL_PLANE_ENDPOINT_IP # The IPv4 address of the control plane endpoint. +CONTROL_PLANE_ENDPOINT_PORT # The port of the control plane endpoint (optional). + # Defaults to 6443. CONTROL_PLANE_ENDPOINT_LOCATION # The location of the control plane endpoint. CLUSTER_NAME # The name of the cluster. KUBERNETES_VERSION # The version of Kubernetes to be installed (can also be set via clusterctl). -## -- Kubernetes Cluster related environment variables -- ## +## -- Kubernetes Cluster-related environment variables -- ## IONOSCLOUD_CONTRACT_NUMBER # The contract number of the IONOS Cloud contract. IONOSCLOUD_DATACENTER_ID # The datacenter ID where the cluster should be created. IONOSCLOUD_MACHINE_NUM_CORES # The number of cores. diff --git a/envfile.example b/envfile.example index 33ec7eb2..83609fc9 100644 --- a/envfile.example +++ b/envfile.example @@ -6,6 +6,7 @@ export IONOS_API_URL="https://api.ionos.com/cloudapi/v6" # Cluster API related environment variables +export CONTROL_PLANE_ENDPOINT_HOST="example.org" export CONTROL_PLANE_ENDPOINT_IP="192.168.0.1" export CONTROL_PLANE_ENDPOINT_PORT=6443 export CONTROL_PLANE_ENDPOINT_LOCATION="de/txl" diff --git a/go.mod b/go.mod index eb94d54e..a2b5a9ac 100644 --- a/go.mod +++ b/go.mod @@ -9,14 +9,14 @@ require ( github.com/ionos-cloud/sdk-go/v6 v6.1.11 github.com/jarcoal/httpmock v1.3.1 github.com/onsi/ginkgo/v2 v2.17.2 - github.com/onsi/gomega v1.33.0 + github.com/onsi/gomega v1.33.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 k8s.io/api v0.29.4 k8s.io/apimachinery v0.29.4 k8s.io/client-go v0.29.4 - k8s.io/klog/v2 v2.110.1 - sigs.k8s.io/cluster-api v1.6.3 + k8s.io/klog/v2 v2.120.1 + sigs.k8s.io/cluster-api v1.7.1 sigs.k8s.io/controller-runtime v0.17.3 ) @@ -29,8 +29,8 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect - github.com/evanphx/json-patch v5.6.0+incompatible // indirect - github.com/evanphx/json-patch/v5 v5.8.0 // indirect + github.com/evanphx/json-patch v5.7.0+incompatible // indirect + github.com/evanphx/json-patch/v5 v5.9.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -78,25 +78,25 @@ require ( go.uber.org/zap v1.26.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/net v0.24.0 // indirect - golang.org/x/oauth2 v0.14.0 // indirect + golang.org/x/oauth2 v0.18.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.19.0 // indirect golang.org/x/term v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/time v0.3.0 // indirect + golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.20.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect google.golang.org/grpc v1.59.0 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.29.2 // indirect - k8s.io/apiserver v0.29.2 // indirect - k8s.io/component-base v0.29.2 // indirect + k8s.io/apiextensions-apiserver v0.29.3 // indirect + k8s.io/apiserver v0.29.3 // indirect + k8s.io/component-base v0.29.3 // indirect k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect k8s.io/utils v0.0.0-20240102154912-e7106e64919e // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.28.0 // indirect diff --git a/go.sum b/go.sum index 4bef3c1c..9228ebf1 100644 --- a/go.sum +++ b/go.sum @@ -41,16 +41,15 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= -github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch/v5 v5.8.0 h1:lRj6N9Nci7MvzrXuX6HFzU8XjmhPiXPlsKEy1u0KQro= -github.com/evanphx/json-patch/v5 v5.8.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +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.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= +github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -150,8 +149,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.17.2 h1:7eMhcy3GimbsA3hEnVKdw/PQM9XN9krpKVXsZdph0/g= github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc= -github.com/onsi/gomega v1.33.0 h1:snPCflnZrpMsy94p4lXVEkHo12lmPnc3vY5XBbreexE= -github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY= +github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= +github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= 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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -175,8 +174,8 @@ github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0 github.com/sirupsen/logrus v1.9.0/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/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= -github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -204,14 +203,14 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= -go.etcd.io/etcd/api/v3 v3.5.10 h1:szRajuUUbLyppkhs9K6BRtjY37l66XQQmw7oZRANE4k= -go.etcd.io/etcd/api/v3 v3.5.10/go.mod h1:TidfmT4Uycad3NM/o25fG3J07odo4GBB9hoxaodFCtI= -go.etcd.io/etcd/client/pkg/v3 v3.5.10 h1:kfYIdQftBnbAq8pUWFXfpuuxFSKzlmM5cSn76JByiT0= -go.etcd.io/etcd/client/pkg/v3 v3.5.10/go.mod h1:DYivfIviIuQ8+/lCq4vcxuseg2P2XbHygkKwFo9fc8U= +go.etcd.io/etcd/api/v3 v3.5.13 h1:8WXU2/NBge6AUF1K1gOexB6e07NgsN1hXK0rSTtgSp4= +go.etcd.io/etcd/api/v3 v3.5.13/go.mod h1:gBqlqkcMMZMVTMm4NDZloEVJzxQOQIls8splbqBDa0c= +go.etcd.io/etcd/client/pkg/v3 v3.5.13 h1:RVZSAnWWWiI5IrYAXjQorajncORbS0zI48LQlE2kQWg= +go.etcd.io/etcd/client/pkg/v3 v3.5.13/go.mod h1:XxHT4u1qU12E2+po+UVPrEeL94Um6zL58ppuJWXSAB8= go.etcd.io/etcd/client/v2 v2.305.10 h1:MrmRktzv/XF8CvtQt+P6wLUlURaNpSDJHFZhe//2QE4= go.etcd.io/etcd/client/v2 v2.305.10/go.mod h1:m3CKZi69HzilhVqtPDcjhSGp+kA1OmbNn0qamH80xjA= -go.etcd.io/etcd/client/v3 v3.5.10 h1:W9TXNZ+oB3MCd/8UjxHTWK5J9Nquw9fQBLJd5ne5/Ao= -go.etcd.io/etcd/client/v3 v3.5.10/go.mod h1:RVeBnDz2PUEZqTpgqwAtUd8nAPf5kjyFyND7P1VkOKc= +go.etcd.io/etcd/client/v3 v3.5.13 h1:o0fHTNJLeO0MyVbc7I3fsCf6nrOqn5d+diSarKnB2js= +go.etcd.io/etcd/client/v3 v3.5.13/go.mod h1:cqiAeY8b5DEEcpxvgWKsbLIWNM/8Wy2xJSDMtioMcoI= go.etcd.io/etcd/pkg/v3 v3.5.10 h1:WPR8K0e9kWl1gAhB5A7gEa5ZBTNkT9NdNWrR8Qpo1CM= go.etcd.io/etcd/pkg/v3 v3.5.10/go.mod h1:TKTuCKKcF1zxmfKWDkfz5qqYaE3JncKKZPFf8c1nFUs= go.etcd.io/etcd/raft/v3 v3.5.10 h1:cgNAYe7xrsrn/5kXMSaH8kM/Ky8mAdMqGOxyYwpP0LA= @@ -261,8 +260,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= -golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0= -golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= +golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= +golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= 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= @@ -288,8 +287,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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= @@ -305,12 +304,12 @@ gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb h1:XFBgcDwm7irdHTbz4Zk2h7Mh+eis4nfJEFQFYzJzuIA= -google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= -google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb h1:lK0oleSc7IQsUxO3U5TjL9DWlsxpEBemh+zpB7IqhWI= -google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 h1:N3bU/SQDCDyD6R528GJ/PwW9KjYcJA3dgyH+MovAkIM= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:KSqppvjFjtoCI+KGd4PELB0qLNxdJHRGqRI09mB6pQA= +google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ= +google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY= +google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo= +google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc= google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= @@ -334,28 +333,28 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.29.4 h1:WEnF/XdxuCxdG3ayHNRR8yH3cI1B/llkWBma6bq4R3w= k8s.io/api v0.29.4/go.mod h1:DetSv0t4FBTcEpfA84NJV3g9a7+rSzlUHk5ADAYHUv0= -k8s.io/apiextensions-apiserver v0.29.2 h1:UK3xB5lOWSnhaCk0RFZ0LUacPZz9RY4wi/yt2Iu+btg= -k8s.io/apiextensions-apiserver v0.29.2/go.mod h1:aLfYjpA5p3OwtqNXQFkhJ56TB+spV8Gc4wfMhUA3/b8= +k8s.io/apiextensions-apiserver v0.29.3 h1:9HF+EtZaVpFjStakF4yVufnXGPRppWFEQ87qnO91YeI= +k8s.io/apiextensions-apiserver v0.29.3/go.mod h1:po0XiY5scnpJfFizNGo6puNU6Fq6D70UJY2Cb2KwAVc= k8s.io/apimachinery v0.29.4 h1:RaFdJiDmuKs/8cm1M6Dh1Kvyh59YQFDcFuFTSmXes6Q= k8s.io/apimachinery v0.29.4/go.mod h1:i3FJVwhvSp/6n8Fl4K97PJEP8C+MM+aoDq4+ZJBf70Y= -k8s.io/apiserver v0.29.2 h1:+Z9S0dSNr+CjnVXQePG8TcBWHr3Q7BmAr7NraHvsMiQ= -k8s.io/apiserver v0.29.2/go.mod h1:B0LieKVoyU7ykQvPFm7XSdIHaCHSzCzQWPFa5bqbeMQ= +k8s.io/apiserver v0.29.3 h1:xR7ELlJ/BZSr2n4CnD3lfA4gzFivh0wwfNfz9L0WZcE= +k8s.io/apiserver v0.29.3/go.mod h1:hrvXlwfRulbMbBgmWRQlFru2b/JySDpmzvQwwk4GUOs= k8s.io/client-go v0.29.4 h1:79ytIedxVfyXV8rpH3jCBW0u+un0fxHDwX5F9K8dPR8= k8s.io/client-go v0.29.4/go.mod h1:kC1thZQ4zQWYwldsfI088BbK6RkxK+aF5ebV8y9Q4tk= -k8s.io/cluster-bootstrap v0.28.4 h1:4MKNy1Qd9QY7pl47rSMGIORF+tm3CUaqC1M8U9bjn4Q= -k8s.io/cluster-bootstrap v0.28.4/go.mod h1:/c4ro/R4yf4EtJgFgFtvnHkbDOHwubeKJXh5R1c89Bc= -k8s.io/component-base v0.29.2 h1:lpiLyuvPA9yV1aQwGLENYyK7n/8t6l3nn3zAtFTJYe8= -k8s.io/component-base v0.29.2/go.mod h1:BfB3SLrefbZXiBfbM+2H1dlat21Uewg/5qtKOl8degM= -k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= -k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/cluster-bootstrap v0.29.3 h1:DIMDZSN8gbFMy9CS2mAS2Iqq/fIUG783WN/1lqi5TF8= +k8s.io/cluster-bootstrap v0.29.3/go.mod h1:aPAg1VtXx3uRrx5qU2jTzR7p1rf18zLXWS+pGhiqPto= +k8s.io/component-base v0.29.3 h1:Oq9/nddUxlnrCuuR2K/jp6aflVvc0uDvxMzAWxnGzAo= +k8s.io/component-base v0.29.3/go.mod h1:Yuj33XXjuOk2BAaHsIGHhCKZQAgYKhqIxIjIr2UXYio= +k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= +k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= k8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCfRziVtos3ofG/sQ= k8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.28.0 h1:TgtAeesdhpm2SGwkQasmbeqDo8th5wOBA5h/AjTKA4I= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.28.0/go.mod h1:VHVDI/KrK4fjnV61bE2g3sA7tiETLn8sooImelsCx3Y= -sigs.k8s.io/cluster-api v1.6.3 h1:VOlPNg92PQLlhBVLc5pg+cbAuPvGOOBujeFLk9zgnoo= -sigs.k8s.io/cluster-api v1.6.3/go.mod h1:4FzfgPPiYaFq8X9F9j2SvmggH/4OOLEDgVJuWDqKLig= +sigs.k8s.io/cluster-api v1.7.1 h1:JkMAbAMzBM+WBHxXLTJXTiCisv1PAaHRzld/3qrmLYY= +sigs.k8s.io/cluster-api v1.7.1/go.mod h1:V9ZhKLvQtsDODwjXOKgbitjyCmC71yMBwDcMyNNIov0= sigs.k8s.io/controller-runtime v0.17.3 h1:65QmN7r3FWgTxDMz9fvGnO1kbf2nu+acg9p2R9oYYYk= sigs.k8s.io/controller-runtime v0.17.3/go.mod h1:N0jpP5Lo7lMTF9aL56Z/B2oWBJjey6StQM0jRbKQXtY= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= diff --git a/internal/controller/ionoscloudcluster_controller.go b/internal/controller/ionoscloudcluster_controller.go index 5f47387c..1d0c317e 100644 --- a/internal/controller/ionoscloudcluster_controller.go +++ b/internal/controller/ionoscloudcluster_controller.go @@ -171,8 +171,15 @@ func (r *IonosCloudClusterReconciler) reconcileDelete( return ctrl.Result{RequeueAfter: defaultReconcileDuration}, nil } - // TODO(lubedacht): check if there are any more machine CRs existing. - // If there are requeue with an offset. + machines, err := clusterScope.ListMachines(ctx, nil) + if err != nil { + return ctrl.Result{}, err + } + + if len(machines) > 0 { + log.Info("Waiting for all IonosCloudMachines to be deleted", "remaining", len(machines)) + return ctrl.Result{RequeueAfter: defaultReconcileDuration}, nil + } reconcileSequence := []serviceReconcileStep[scope.Cluster]{ {"ReconcileControlPlaneEndpointDeletion", cloudService.ReconcileControlPlaneEndpointDeletion}, @@ -186,6 +193,9 @@ func (r *IonosCloudClusterReconciler) reconcileDelete( return ctrl.Result{RequeueAfter: defaultReconcileDuration}, err } } + if err := removeCredentialsFinalizer(ctx, r.Client, clusterScope.IonosCluster); err != nil { + return ctrl.Result{}, err + } controllerutil.RemoveFinalizer(clusterScope.IonosCluster, infrav1.ClusterFinalizer) return ctrl.Result{}, nil } diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index e9dd5580..becf72b3 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -52,15 +52,6 @@ type IonosCloudMachineReconciler struct { //+kubebuilder:rbac:groups="",resources=secrets;,verbs=get;list;watch //+kubebuilder:rbac:groups="",resources=events,verbs=get;list;watch;create;update;patch -// Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by -// the IonosCloudMachine object against the actual cluster state, and then -// perform operations to make the cluster state reflect the state specified by -// the user. -// -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.16.0/pkg/reconcile func (r *IonosCloudMachineReconciler) Reconcile( ctx context.Context, ionosCloudMachine *infrav1.IonosCloudMachine, @@ -153,7 +144,7 @@ func (r *IonosCloudMachineReconciler) reconcileNormal( if controllerutil.AddFinalizer(machineScope.IonosMachine, infrav1.MachineFinalizer) { if err := machineScope.PatchObject(); err != nil { - log.Error(err, "unable to update finalizer on object") + err = fmt.Errorf("unable to update finalizer on object: %w", err) return ctrl.Result{}, err } } diff --git a/internal/controller/util.go b/internal/controller/util.go index 1c091d37..7a60a22e 100644 --- a/internal/controller/util.go +++ b/internal/controller/util.go @@ -25,6 +25,7 @@ import ( sdk "github.com/ionos-cloud/sdk-go/v6" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" icc "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud/client" @@ -80,6 +81,10 @@ func createServiceFromCluster( return nil, err } + if err := ensureSecretControlledByCluster(ctx, c, cluster, &authSecret); err != nil { + return nil, err + } + token := string(authSecret.Data["token"]) apiURL := string(authSecret.Data["apiURL"]) caBundle := authSecret.Data["caBundle"] @@ -91,3 +96,43 @@ func createServiceFromCluster( return cloud.NewService(ionosClient, log) } + +// ensureSecretControlledByCluster ensures that the secrets will contain a finalizer and a controller reference. +// The secret should only be deleted when there are no resources left in the IONOS Cloud environment. +func ensureSecretControlledByCluster( + ctx context.Context, c client.Client, + cluster *infrav1.IonosCloudCluster, + secret *corev1.Secret, +) error { + requireUpdate := controllerutil.AddFinalizer(secret, infrav1.ClusterCredentialsFinalizer) + + if !controllerutil.HasControllerReference(secret) { + if err := controllerutil.SetControllerReference(cluster, secret, c.Scheme()); err != nil { + return err + } + requireUpdate = true + } + + if requireUpdate { + return c.Update(ctx, secret) + } + + return nil +} + +// removeCredentialsFinalizer removes the finalizer from the credential secret. +func removeCredentialsFinalizer(ctx context.Context, c client.Client, cluster *infrav1.IonosCloudCluster) error { + secretKey := client.ObjectKey{ + Namespace: cluster.Namespace, + Name: cluster.Spec.CredentialsRef.Name, + } + var secret corev1.Secret + + if err := c.Get(ctx, secretKey, &secret); err != nil { + // If the secret does not exist anymore, there is nothing we can do. + return client.IgnoreNotFound(err) + } + + controllerutil.RemoveFinalizer(&secret, infrav1.ClusterCredentialsFinalizer) + return c.Update(ctx, &secret) +} diff --git a/internal/service/cloud/ipblock.go b/internal/service/cloud/ipblock.go index 88e72e9d..8e86b549 100644 --- a/internal/service/cloud/ipblock.go +++ b/internal/service/cloud/ipblock.go @@ -148,10 +148,8 @@ func (s *Service) ReconcileControlPlaneEndpointDeletion( // ReconcileFailoverIPBlockDeletion ensures that the IP block is deleted. func (s *Service) ReconcileFailoverIPBlockDeletion(ctx context.Context, ms *scope.Machine) (requeue bool, err error) { log := s.logger.WithName("ReconcileFailoverIPBlockDeletion") - if ms.IonosMachine.Spec.FailoverIP != infrav1.CloudResourceConfigAuto { - log.V(4).Info("Failover IP block is not managed by the provider, skipping deletion", - "failoverIP", ms.IonosMachine.Spec.FailoverIP, - ) + if foIP := ms.IonosMachine.Spec.FailoverIP; foIP == nil || *foIP != infrav1.CloudResourceConfigAuto { + log.V(4).Info("Failover IP block is not managed by the provider, skipping deletion", "failoverIP", foIP) return false, nil } @@ -253,11 +251,17 @@ func (s *Service) getControlPlaneEndpointIPBlock(ctx context.Context, cs *scope. if ipBlock != nil || ignoreNotFound(err) != nil { return ipBlock, err } + notFoundError := err s.logger.Info("IP block not found by ID, trying to find by listing IP blocks instead") - blocks, listErr := s.apiWithDepth(listIPBlocksDepth).ListIPBlocks(ctx) - if listErr != nil { - return nil, fmt.Errorf("failed to list IP blocks: %w", listErr) + blocks, err := s.apiWithDepth(listIPBlocksDepth).ListIPBlocks(ctx) + if err != nil { + return nil, fmt.Errorf("failed to list IP blocks: %w", err) + } + + controlPlaneEndpointIP, err := cs.GetControlPlaneEndpointIP(ctx) + if err != nil { + return nil, err } var ( @@ -277,7 +281,7 @@ func (s *Service) getControlPlaneEndpointIPBlock(ctx context.Context, cs *scope. if err != nil { return nil, err } - case s.checkIfUserSetBlock(cs, props): + case s.checkIfUserSetBlock(controlPlaneEndpointIP, props): // NOTE: this is for when customers set IPs for the control plane endpoint themselves. foundBlock, err = s.cloudAPIStateInconsistencyWorkaround(ctx, &block) if err != nil { @@ -287,11 +291,11 @@ func (s *Service) getControlPlaneEndpointIPBlock(ctx context.Context, cs *scope. } if count > 1 { return nil, fmt.Errorf( - "cannot determine IP block for Control Plane Endpoint as there are multiple IP blocks with the name %s", + "cannot determine IP block for Control Plane Endpoint, as there are multiple IP blocks with the name %s", expectedName) } } - if count == 0 && cs.GetControlPlaneEndpoint().Host != "" { + if count == 0 && controlPlaneEndpointIP != "" { return nil, errUserSetIPNotFound } if foundBlock != nil { @@ -299,13 +303,12 @@ func (s *Service) getControlPlaneEndpointIPBlock(ctx context.Context, cs *scope. } // if we still can't find an IP block we return the potential // initial not found error. - return nil, err + return nil, notFoundError } -func (*Service) checkIfUserSetBlock(cs *scope.Cluster, props *sdk.IpBlockProperties) bool { - ip := cs.GetControlPlaneEndpoint().Host +func (*Service) checkIfUserSetBlock(controlPlaneEndpointIP string, props *sdk.IpBlockProperties) bool { ips := ptr.Deref(props.GetIps(), nil) - return ip != "" && slices.Contains(ips, ip) + return controlPlaneEndpointIP != "" && slices.Contains(ips, controlPlaneEndpointIP) } // cloudAPIStateInconsistencyWorkaround is a workaround for a bug where the API returns different states for the same @@ -322,7 +325,7 @@ func (s *Service) cloudAPIStateInconsistencyWorkaround(ctx context.Context, bloc func (s *Service) getIPBlockByID(ctx context.Context, ipBlockID string) (*sdk.IpBlock, error) { if ipBlockID == "" { - s.logger.Info("Could not find any IP block by ID as the provider ID is not set.") + s.logger.Info("Could not find any IP block by ID, as the provider ID is not set.") return nil, nil } ipBlock, err := s.ionosClient.GetIPBlock(ctx, ipBlockID) @@ -444,11 +447,11 @@ func (s *Service) getLatestIPBlockDeletionRequest(ctx context.Context, ipBlockID // controlPlaneEndpointIPBlockName returns the name that should be used for cluster context resources. func (*Service) controlPlaneEndpointIPBlockName(cs *scope.Cluster) string { - return fmt.Sprintf("k8s-ipb-%s-%s", cs.Cluster.Namespace, cs.Cluster.Name) + return fmt.Sprintf("ipb-%s-%s", cs.Cluster.Namespace, cs.Cluster.Name) } func (*Service) failoverIPBlockName(ms *scope.Machine) string { - return fmt.Sprintf("k8s-fo-ipb-%s-%s", + return fmt.Sprintf("fo-ipb-%s-%s", ms.IonosMachine.Namespace, ms.IonosMachine.Labels[clusterv1.MachineDeploymentNameLabel], ) diff --git a/internal/service/cloud/ipblock_test.go b/internal/service/cloud/ipblock_test.go index 43ae3f9a..2a81525d 100644 --- a/internal/service/cloud/ipblock_test.go +++ b/internal/service/cloud/ipblock_test.go @@ -38,7 +38,7 @@ func TestIPBlockTestSuite(t *testing.T) { } const ( - exampleIPBlockName = "k8s-ipb-default-test-cluster" + exampleIPBlockName = "ipb-default-test-cluster" ) func (s *ipBlockTestSuite) TestGetControlPlaneEndpointIPBlockMultipleMatches() { @@ -387,7 +387,7 @@ func (s *ipBlockTestSuite) TestReconcileControlPlaneEndpointDeletionRequestNewDe } func (s *ipBlockTestSuite) TestReconcileFailoverIPBlockDeletion() { - s.infraMachine.Spec.FailoverIP = infrav1.CloudResourceConfigAuto + s.infraMachine.Spec.FailoverIP = ptr.To(infrav1.CloudResourceConfigAuto) ipBlock := exampleIPBlockWithName(s.service.failoverIPBlockName(s.machineScope)) s.mockListIPBlocksCall().Return(&sdk.IpBlocks{Items: &[]sdk.IpBlock{*ipBlock}}, nil).Once() @@ -402,7 +402,7 @@ func (s *ipBlockTestSuite) TestReconcileFailoverIPBlockDeletion() { } func (s *ipBlockTestSuite) TestReconcileFailoverIPBlockDeletionSkipped() { - s.infraMachine.Spec.FailoverIP = infrav1.CloudResourceConfigAuto + s.infraMachine.Spec.FailoverIP = ptr.To(infrav1.CloudResourceConfigAuto) ipBlock := exampleIPBlockWithName(s.service.failoverIPBlockName(s.machineScope)) lan := s.exampleLAN() lan.Properties.IpFailover = &[]sdk.IPFailover{{ @@ -421,7 +421,7 @@ func (s *ipBlockTestSuite) TestReconcileFailoverIPBlockDeletionSkipped() { } func (s *ipBlockTestSuite) TestReconcileFailoverIPBlockDeletionPendingCreation() { - s.infraMachine.Spec.FailoverIP = infrav1.CloudResourceConfigAuto + s.infraMachine.Spec.FailoverIP = ptr.To(infrav1.CloudResourceConfigAuto) s.mockListIPBlocksCall().Return(nil, nil).Once() s.mockGetIPBlocksRequestsPostCall().Return([]sdk.Request{ @@ -436,7 +436,7 @@ func (s *ipBlockTestSuite) TestReconcileFailoverIPBlockDeletionPendingCreation() } func (s *ipBlockTestSuite) TestReconcileFailoverIPBlockDeletionPendingDeletion() { - s.infraMachine.Spec.FailoverIP = infrav1.CloudResourceConfigAuto + s.infraMachine.Spec.FailoverIP = ptr.To(infrav1.CloudResourceConfigAuto) ipBlock := exampleIPBlockWithName(s.service.failoverIPBlockName(s.machineScope)) s.mockListIPBlocksCall().Return(&sdk.IpBlocks{Items: &[]sdk.IpBlock{*ipBlock}}, nil).Once() @@ -464,7 +464,7 @@ func (s *ipBlockTestSuite) TestReconcileFailoverIPBlockDeletionPendingDeletion() } func (s *ipBlockTestSuite) TestReconcileFailoverIPBlockDeletionDeletionFinished() { - s.infraMachine.Spec.FailoverIP = infrav1.CloudResourceConfigAuto + s.infraMachine.Spec.FailoverIP = ptr.To(infrav1.CloudResourceConfigAuto) ipBlock := exampleIPBlockWithName(s.service.failoverIPBlockName(s.machineScope)) s.mockListIPBlocksCall().Return(&sdk.IpBlocks{Items: &[]sdk.IpBlock{*ipBlock}}, nil).Once() diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index e9103423..e2e25ad7 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -37,7 +37,7 @@ import ( // lanName returns the name of the cluster LAN. func (*Service) lanName(c *clusterv1.Cluster) string { return fmt.Sprintf( - "k8s-lan-%s-%s", + "lan-%s-%s", c.Namespace, c.Name) } @@ -278,10 +278,15 @@ func (s *Service) retrieveFailoverIPForMachine( log := s.logger.WithName("retrieveFailoverIPForMachine") if util.IsControlPlaneMachine(ms.Machine) { - return false, ms.ClusterScope.GetControlPlaneEndpoint().Host, nil + ip, err := ms.ClusterScope.GetControlPlaneEndpointIP(ctx) + return false, ip, err } - failoverIP = ms.IonosMachine.Spec.FailoverIP + failoverIP = ptr.Deref(ms.IonosMachine.Spec.FailoverIP, "") + if failoverIP == "" { + const errorMessage = "failover IP contains an empty string. Provide either a valid IP address or 'AUTO'" + return false, "", errors.New(errorMessage) + } // AUTO means we have to reserve an IP address. if failoverIP == infrav1.CloudResourceConfigAuto { @@ -438,7 +443,7 @@ func (s *Service) getServerNICID(ctx context.Context, ms *scope.Machine) (string log.Info("Server was not found or already deleted.") return "", nil } - log.Error(err, "Unable to retrieve server") + err = fmt.Errorf("unable to retrieve server %w", err) return "", err } @@ -597,5 +602,5 @@ 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 != "" + return util.IsControlPlaneMachine(ms.Machine) || ms.IonosMachine.Spec.FailoverIP != nil } diff --git a/internal/service/cloud/network_test.go b/internal/service/cloud/network_test.go index 5f0a8105..1fe52035 100644 --- a/internal/service/cloud/network_test.go +++ b/internal/service/cloud/network_test.go @@ -47,7 +47,7 @@ func TestLANSuite(t *testing.T) { } func (s *lanSuite) TestNetworkLANName() { - s.Equal("k8s-lan-default-test-cluster", s.service.lanName(s.clusterScope.Cluster)) + s.Equal("lan-default-test-cluster", s.service.lanName(s.clusterScope.Cluster)) } func (s *lanSuite) TestLANURL() { @@ -246,7 +246,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 = infrav1.CloudResourceConfigAuto + s.infraMachine.Spec.FailoverIP = ptr.To(infrav1.CloudResourceConfigAuto) testServer := s.defaultServer(s.infraMachine, exampleDHCPIP, exampleWorkerFailoverIP) testLAN := s.exampleLAN() @@ -285,7 +285,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 = infrav1.CloudResourceConfigAuto + s.infraMachine.Spec.FailoverIP = ptr.To(infrav1.CloudResourceConfigAuto) s.mockListIPBlocksCall().Return(nil, nil).Once() s.mockGetIPBlocksRequestsPostCall().Return(nil, nil).Once() @@ -397,7 +397,7 @@ func (s *lanSuite) TestReconcileIPFailoverDeletionWorker() { labels[clusterv1.MachineDeploymentNameLabel] = deploymentLabel s.infraMachine.SetLabels(labels) - s.infraMachine.Spec.FailoverIP = infrav1.CloudResourceConfigAuto + s.infraMachine.Spec.FailoverIP = ptr.To(infrav1.CloudResourceConfigAuto) testServer := s.defaultServer(s.infraMachine, exampleDHCPIP, exampleWorkerFailoverIP) s.NoError(s.k8sClient.Update(s.ctx, s.infraMachine)) diff --git a/internal/service/cloud/nic.go b/internal/service/cloud/nic.go index f318c226..2b9aa9da 100644 --- a/internal/service/cloud/nic.go +++ b/internal/service/cloud/nic.go @@ -149,5 +149,5 @@ func nicHasIP(nic *sdk.Nic, expectedIP string) bool { } func (*Service) nicName(m *infrav1.IonosCloudMachine) string { - return fmt.Sprintf("k8s-nic-%s-%s", m.Namespace, m.Name) + return "nic-" + m.Name } diff --git a/internal/service/cloud/nic_test.go b/internal/service/cloud/nic_test.go index 8cdaab2b..b6ac51db 100644 --- a/internal/service/cloud/nic_test.go +++ b/internal/service/cloud/nic_test.go @@ -41,6 +41,12 @@ func TestNICSuite(t *testing.T) { suite.Run(t, new(nicSuite)) } +func (s *nicSuite) TestNICName() { + nicName := s.service.nicName(s.infraMachine) + expected := "nic-" + s.infraMachine.Name + s.Equal(expected, nicName) +} + func (s *nicSuite) TestReconcileNICConfig() { s.mockGetServerCall(exampleServerID).Return(s.defaultServer(s.infraMachine, exampleDHCPIP), nil).Once() diff --git a/internal/service/cloud/server.go b/internal/service/cloud/server.go index 256260ff..8b88ba33 100644 --- a/internal/service/cloud/server.go +++ b/internal/service/cloud/server.go @@ -62,43 +62,40 @@ func (s *Service) ReconcileServer(ctx context.Context, ms *scope.Machine) (reque return true, nil } - if server != nil { - // Server is available - - if !s.isServerAvailable(ms, server) { - // Server is still provisioning, checking again later - return true, nil + if server == nil { + // Server does not exist yet, create it + log.V(4).Info("No server was found. Creating new server") + if err := s.createServer(ctx, secret, ms); err != nil { + return false, err } + log.V(4).Info("Successfully initiated server creation") + // If we reach this point, we want to requeue as the request is not processed yet, + // and we will check for the status again later. + return true, nil + } - // Attach the IPs from all NICs of the server to the status - netInfo := &infrav1.MachineNetworkInfo{NICInfo: make([]infrav1.NICInfo, 0)} - - for _, nic := range ptr.Deref(server.GetEntities().GetNics().GetItems(), []sdk.Nic{}) { - netInfo.NICInfo = append(netInfo.NICInfo, infrav1.NICInfo{ - IPv4Addresses: ptr.Deref(nic.GetProperties().GetIps(), []string{}), - IPv6Addresses: ptr.Deref(nic.GetProperties().GetIpv6Ips(), []string{}), - NetworkID: ptr.Deref(nic.GetProperties().GetLan(), 0), - Primary: s.isPrimaryNIC(ms.IonosMachine, &nic), - }) - } + requeue, err = s.ensureServerAvailable(ctx, ms, server) + if requeue || err != nil { + return requeue, err + } - ms.IonosMachine.Status.MachineNetworkInfo = netInfo + // Attach the IPs from all NICs of the server to the status + netInfo := &infrav1.MachineNetworkInfo{NICInfo: make([]infrav1.NICInfo, 0)} - log.Info("Server is available", "serverID", ptr.Deref(server.GetId(), "")) - // server exists and is available. - return false, nil + for _, nic := range ptr.Deref(server.GetEntities().GetNics().GetItems(), []sdk.Nic{}) { + netInfo.NICInfo = append(netInfo.NICInfo, infrav1.NICInfo{ + IPv4Addresses: ptr.Deref(nic.GetProperties().GetIps(), []string{}), + IPv6Addresses: ptr.Deref(nic.GetProperties().GetIpv6Ips(), []string{}), + NetworkID: ptr.Deref(nic.GetProperties().GetLan(), 0), + Primary: s.isPrimaryNIC(ms.IonosMachine, &nic), + }) } - // server does not exist yet, create it - log.V(4).Info("No server was found. Creating new server") - if err := s.createServer(ctx, secret, ms); err != nil { - return false, err - } + ms.IonosMachine.Status.MachineNetworkInfo = netInfo - log.V(4).Info("successfully finished reconciling server") - // If we reach this point, we want to requeue as the request is not processed yet, - // and we will check for the status again later. - return true, nil + log.Info("Server is available", "serverID", ptr.Deref(server.GetId(), "")) + // server exists and is available. + return false, nil } // ReconcileServerDeletion ensures the server is deleted. @@ -152,23 +149,40 @@ func (*Service) FinalizeMachineProvisioning(_ context.Context, ms *scope.Machine return false, nil } -func (s *Service) isServerAvailable(ms *scope.Machine, server *sdk.Server) bool { +// isServerAvailable checks if the server is in state AVAILABLE. +func (s *Service) isServerAvailable(server *sdk.Server) bool { log := s.logger.WithName("isServerAvailable") if state := getState(server); !isAvailable(state) { log.Info("Server is not available yet", "state", state) return false } + return true +} + +// ensureServerAvailable checks the availability of the specified server. +func (s *Service) ensureServerAvailable(ctx context.Context, ms *scope.Machine, server *sdk.Server) (bool, error) { + log := s.logger.WithName("ensureServerAvailable") + + // Check if the server is available + if !s.isServerAvailable(server) { + // Server is still provisioning, checking again later + return true, nil + } + // Check the VM state; if not running, try to start it if vmState := getVMState(server); !isRunning(vmState) { - err := s.startServer(context.Background(), ms.DatacenterID(), *server.Id) + err := s.startServer(ctx, ms, *server.Id) if err != nil { log.Error(err, "Failed to start the server") - return false + return true, err } - return true + // If we reach this point, we want to requeue as the request is not processed yet, + // and we will check for the status again later. + return true, nil } - return true + // Default return path when no conditions are met (server is available and running) + return false, nil } // getServerByServerID checks if the IonosCloudMachine has a provider ID set. @@ -213,7 +227,7 @@ func (s *Service) getServer(ctx context.Context, ms *scope.Machine) (*sdk.Server items := ptr.Deref(serverList.Items, []sdk.Server{}) // find servers with the expected name for _, server := range items { - if server.HasProperties() && *server.Properties.Name == s.serverName(ms.IonosMachine) { + if server.HasProperties() && *server.Properties.Name == ms.IonosMachine.Name { // if the server was found, we set the provider ID and return it ms.SetProviderID(ptr.Deref(server.Id, "")) return &server, nil @@ -241,17 +255,18 @@ func (s *Service) deleteServer(ctx context.Context, ms *scope.Machine, serverID return nil } -func (s *Service) startServer(ctx context.Context, datacenterID, serverID string) error { +func (s *Service) startServer(ctx context.Context, ms *scope.Machine, serverID string) error { log := s.logger.WithName("startServer") log.V(4).Info("Starting server", "serverID", serverID) - requestLocation, err := s.ionosClient.StartServer(ctx, datacenterID, serverID) + requestLocation, err := s.ionosClient.StartServer(ctx, ms.DatacenterID(), serverID) if err != nil { return fmt.Errorf("failed to request server start: %w", err) } log.Info("Successfully requested for server start", "location", requestLocation) - log.V(4).Info("Done starting server") + ms.IonosMachine.SetCurrentRequest(http.MethodPost, sdk.RequestStatusQueued, requestLocation) + return nil } @@ -261,7 +276,7 @@ func (s *Service) getLatestServerCreationRequest(ctx context.Context, ms *scope. s, http.MethodPost, path.Join("datacenters", ms.DatacenterID(), "servers"), - matchByName[*sdk.Server, *sdk.ServerProperties](s.serverName(ms.IonosMachine)), + matchByName[*sdk.Server, *sdk.ServerProperties](ms.IonosMachine.Name), ) } @@ -328,13 +343,13 @@ func (s *Service) createServer(ctx context.Context, secret *corev1.Secret, ms *s } // buildServerProperties returns the server properties for the expected cloud server resource. -func (s *Service) buildServerProperties( +func (*Service) buildServerProperties( ms *scope.Machine, machineSpec *infrav1.IonosCloudMachineSpec, ) sdk.ServerProperties { props := sdk.ServerProperties{ AvailabilityZone: ptr.To(machineSpec.AvailabilityZone.String()), Cores: &machineSpec.NumCores, - Name: ptr.To(s.serverName(ms.IonosMachine)), + Name: ptr.To(ms.IonosMachine.Name), Ram: &machineSpec.MemoryMB, CpuFamily: machineSpec.CPUFamily, } @@ -400,15 +415,12 @@ func (s *Service) buildServerEntities(ms *scope.Machine, params serverEntityPara } } -func (s *Service) renderUserData(ms *scope.Machine, input string) string { - // TODO(lubedacht) update user data to include needed information - // VNC and hostname - +func (*Service) renderUserData(ms *scope.Machine, input string) string { const bootCmdFormat = `bootcmd: - echo %[1]s > /etc/hostname - hostname %[1]s ` - bootCmdString := fmt.Sprintf(bootCmdFormat, s.serverName(ms.IonosMachine)) + bootCmdString := fmt.Sprintf(bootCmdFormat, ms.IonosMachine.Name) input = fmt.Sprintf("%s\n%s", input, bootCmdString) return base64.StdEncoding.EncodeToString([]byte(input)) @@ -418,14 +430,6 @@ func (*Service) serversURL(datacenterID string) string { return path.Join("datacenters", datacenterID, "servers") } -// serverName returns a formatted name for the expected cloud server resource. -func (*Service) serverName(m *infrav1.IonosCloudMachine) string { - return fmt.Sprintf( - "k8s-%s-%s", - m.Namespace, - m.Name) -} - func (*Service) volumeName(m *infrav1.IonosCloudMachine) string { - return fmt.Sprintf("k8s-vol-%s-%s", m.Namespace, m.Name) + return "vol-" + m.Name } diff --git a/internal/service/cloud/server_test.go b/internal/service/cloud/server_test.go index 249d23e1..f0284ace 100644 --- a/internal/service/cloud/server_test.go +++ b/internal/service/cloud/server_test.go @@ -40,9 +40,10 @@ func TestServerSuite(t *testing.T) { suite.Run(t, new(serverSuite)) } -func (s *serverSuite) TestServerName() { - serverName := s.service.serverName(s.infraMachine) - s.Equal("k8s-default-test-machine", serverName) +func (s *serverSuite) TestVolumeName() { + volumeName := s.service.volumeName(s.infraMachine) + expected := "vol-" + s.infraMachine.Name + s.Equal(expected, volumeName) } func (s *serverSuite) TestReconcileServerNoBootstrapSecret() { @@ -74,7 +75,7 @@ func (s *serverSuite) TestReconcileServerRequestDoneStateBusy() { State: ptr.To(sdk.Busy), }, Properties: &sdk.ServerProperties{ - Name: ptr.To(s.service.serverName(s.infraMachine)), + Name: ptr.To(s.infraMachine.Name), }, }, }}, nil).Once() @@ -93,7 +94,7 @@ func (s *serverSuite) TestReconcileServerRequestDoneStateAvailable() { State: ptr.To(sdk.Available), }, Properties: &sdk.ServerProperties{ - Name: ptr.To(s.service.serverName(s.infraMachine)), + Name: ptr.To(s.infraMachine.Name), VmState: ptr.To("RUNNING"), }, Entities: &sdk.ServerEntities{ @@ -134,8 +135,8 @@ func (s *serverSuite) TestReconcileServerRequestDoneStateAvailableTurnedOff() { State: ptr.To(sdk.Available), }, Properties: &sdk.ServerProperties{ - Name: ptr.To(s.service.serverName(s.infraMachine)), - VmState: ptr.To(sdk.Available), + Name: ptr.To(s.infraMachine.Name), + VmState: ptr.To("SHUTOFF"), }, }, }}, nil).Once() @@ -144,7 +145,7 @@ func (s *serverSuite) TestReconcileServerRequestDoneStateAvailableTurnedOff() { requeue, err := s.service.ReconcileServer(s.ctx, s.machineScope) s.NoError(err) - s.False(requeue) + s.True(requeue) } func (s *serverSuite) TestReconcileServerNoRequest() { @@ -314,12 +315,11 @@ func (s *serverSuite) TestGetServerWithProviderIDNotFound() { } func (s *serverSuite) TestGetServerWithoutProviderIDFoundInList() { - serverName := s.service.serverName(s.infraMachine) s.machineScope.IonosMachine.Spec.ProviderID = nil s.mockListServersCall().Return(&sdk.Servers{Items: &[]sdk.Server{ { Properties: &sdk.ServerProperties{ - Name: ptr.To(serverName), + Name: ptr.To(s.infraMachine.Name), }, }, }}, nil) @@ -396,7 +396,7 @@ func (s *serverSuite) examplePostRequest(status string) sdk.Request { status: status, method: http.MethodPost, url: s.service.serversURL(s.machineScope.DatacenterID()), - body: fmt.Sprintf(`{"properties": {"name": "%s"}}`, s.service.serverName(s.infraMachine)), + body: fmt.Sprintf(`{"properties": {"name": "%s"}}`, s.infraMachine.Name), href: exampleRequestPath, targetID: exampleServerID, targetType: sdk.SERVER, diff --git a/scope/cluster.go b/scope/cluster.go index ff580fb8..daf6a16e 100644 --- a/scope/cluster.go +++ b/scope/cluster.go @@ -21,6 +21,9 @@ import ( "context" "errors" "fmt" + "net" + "net/netip" + "slices" "time" "k8s.io/client-go/util/retry" @@ -32,9 +35,18 @@ import ( infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" ) +// 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. +type resolver interface { + LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, error) +} + // Cluster defines a basic cluster context for primary use in IonosCloudClusterReconciler. type Cluster struct { + client client.Client patchHelper *patch.Helper + resolver resolver Cluster *clusterv1.Cluster IonosCluster *infrav1.IonosCloudCluster } @@ -67,9 +79,11 @@ func NewCluster(params ClusterParams) (*Cluster, error) { } clusterScope := &Cluster{ + client: params.Client, Cluster: params.Cluster, IonosCluster: params.IonosCluster, patchHelper: helper, + resolver: net.DefaultResolver, } return clusterScope, nil @@ -80,11 +94,55 @@ func (c *Cluster) GetControlPlaneEndpoint() clusterv1.APIEndpoint { return c.IonosCluster.Spec.ControlPlaneEndpoint } +// GetControlPlaneEndpointIP returns the endpoint IP for the IonosCloudCluster. +// If the endpoint host is unset (neither an IP nor an FQDN), it will return an empty string. +func (c *Cluster) GetControlPlaneEndpointIP(ctx context.Context) (string, error) { + host := c.GetControlPlaneEndpoint().Host + if host == "" { + return "", nil + } + + if ip, err := netip.ParseAddr(host); err == nil { + return ip.String(), nil + } + + // If the host is not an IP, try to resolve it. + ips, err := c.resolver.LookupNetIP(ctx, "ip4", host) + if err != nil { + return "", fmt.Errorf("failed to resolve control plane endpoint IP: %w", err) + } + + // Sort IPs to deal with random order intended for load balancing. + slices.SortFunc(ips, func(a, b netip.Addr) int { return a.Compare(b) }) + + return ips[0].String(), nil +} + // SetControlPlaneEndpointIPBlockID sets the IP block ID in the IonosCloudCluster status. func (c *Cluster) SetControlPlaneEndpointIPBlockID(id string) { c.IonosCluster.Status.ControlPlaneEndpointIPBlockID = id } +// ListMachines returns a list of IonosCloudMachines in the same namespace and with the same cluster label. +// With machineLabels, additional search labels can be provided. +func (c *Cluster) ListMachines( + ctx context.Context, + machineLabels client.MatchingLabels, +) ([]infrav1.IonosCloudMachine, error) { + if machineLabels == nil { + machineLabels = client.MatchingLabels{} + } + + machineLabels[clusterv1.ClusterNameLabel] = c.Cluster.Name + listOpts := []client.ListOption{client.InNamespace(c.Cluster.Namespace), machineLabels} + + machineList := &infrav1.IonosCloudMachineList{} + if err := c.client.List(ctx, machineList, listOpts...); err != nil { + return nil, err + } + return machineList.Items, nil +} + // Location is a shortcut for getting the location used by the IONOS Cloud cluster IP block. func (c *Cluster) Location() string { return c.IonosCluster.Spec.Location diff --git a/scope/cluster_test.go b/scope/cluster_test.go index d8165dfe..45699c32 100644 --- a/scope/cluster_test.go +++ b/scope/cluster_test.go @@ -17,11 +17,17 @@ limitations under the License. package scope import ( + "context" + "net" + "net/netip" "testing" "github.com/stretchr/testify/require" + 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" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" @@ -81,7 +87,185 @@ func TestNewClusterMissingParams(t *testing.T) { params, err := NewCluster(test.params) require.NoError(t, err) require.NotNil(t, params) + require.Equal(t, net.DefaultResolver, params.resolver) } }) } } + +type mockResolver struct { + addrs map[string][]netip.Addr +} + +func (m *mockResolver) LookupNetIP(_ context.Context, _, host string) ([]netip.Addr, error) { + return m.addrs[host], nil +} + +func resolvesTo(ips ...string) []netip.Addr { + res := make([]netip.Addr, 0, len(ips)) + for _, ip := range ips { + res = append(res, netip.MustParseAddr(ip)) + } + return res +} + +func TestCluster_GetControlPlaneEndpointIP(t *testing.T) { + tests := []struct { + name string + host string + resolver resolver + want string + }{ + { + name: "host empty", + host: "", + want: "", + }, + { + name: "host is IP", + host: "127.0.0.1", + want: "127.0.0.1", + }, + { + name: "host is FQDN with single IP", + host: "localhost", + resolver: &mockResolver{ + addrs: map[string][]netip.Addr{ + "localhost": resolvesTo("127.0.0.1"), + }, + }, + want: "127.0.0.1", + }, + { + name: "host is FQDN with multiple IPs", + host: "example.org", + resolver: &mockResolver{ + addrs: map[string][]netip.Addr{ + "example.org": resolvesTo("2.3.4.5", "1.2.3.4"), + }, + }, + want: "1.2.3.4", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Cluster{ + resolver: tt.resolver, + IonosCluster: &infrav1.IonosCloudCluster{ + Spec: infrav1.IonosCloudClusterSpec{ + ControlPlaneEndpoint: clusterv1.APIEndpoint{ + Host: tt.host, + }, + }, + }, + } + got, err := c.GetControlPlaneEndpointIP(context.Background()) + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} + +func TestClusterListMachines(t *testing.T) { + scheme := runtime.NewScheme() + require.NoError(t, infrav1.AddToScheme(scheme)) + + const clusterName = "test-cluster" + + makeLabels := func(clusterName string, additionalLabels map[string]string) map[string]string { + if additionalLabels == nil { + return map[string]string{clusterv1.ClusterNameLabel: clusterName} + } + + additionalLabels[clusterv1.ClusterNameLabel] = clusterName + return additionalLabels + } + + tests := []struct { + name string + initialObjects []client.Object + searchLabels client.MatchingLabels + expectedNames sets.Set[string] + }{{ + name: "List all machines for a cluster", + initialObjects: []client.Object{ + buildMachineWithLabel("machine-1", makeLabels(clusterName, nil)), + buildMachineWithLabel("machine-2", makeLabels(clusterName, nil)), + buildMachineWithLabel("machine-3", makeLabels(clusterName, nil)), + }, + searchLabels: client.MatchingLabels{}, + expectedNames: sets.New("machine-1", "machine-2", "machine-3"), + }, { + name: "List only machines with specific labels", + initialObjects: []client.Object{ + buildMachineWithLabel("machine-1", makeLabels(clusterName, map[string]string{"foo": "bar"})), + buildMachineWithLabel("machine-2", makeLabels(clusterName, map[string]string{"foo": "bar"})), + buildMachineWithLabel("machine-3", makeLabels(clusterName, nil)), + }, + searchLabels: client.MatchingLabels{ + "foo": "bar", + }, + expectedNames: sets.New("machine-1", "machine-2"), + }, { + name: "List no machines", + initialObjects: []client.Object{ + buildMachineWithLabel("machine-1", makeLabels(clusterName, map[string]string{"foo": "notbar"})), + buildMachineWithLabel("machine-2", makeLabels(clusterName, map[string]string{"foo": "notbar"})), + buildMachineWithLabel("machine-3", makeLabels(clusterName, map[string]string{"foo": "notbar"})), + }, + searchLabels: makeLabels(clusterName, map[string]string{"foo": "bar"}), + expectedNames: sets.New[string](), + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + params := ClusterParams{ + Cluster: &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterName, + Namespace: metav1.NamespaceDefault, + Labels: map[string]string{ + clusterv1.ClusterNameLabel: clusterName, + }, + }, + }, + IonosCluster: &infrav1.IonosCloudCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ionos-cluster", + Namespace: metav1.NamespaceDefault, + Labels: map[string]string{ + clusterv1.ClusterNameLabel: clusterName, + }, + }, + Status: infrav1.IonosCloudClusterStatus{}, + }, + } + + cl := fake.NewClientBuilder().WithScheme(scheme). + WithObjects(test.initialObjects...).Build() + + params.Client = cl + cs, err := NewCluster(params) + require.NoError(t, err) + require.NotNil(t, cs) + + machines, err := cs.ListMachines(context.Background(), test.searchLabels) + require.NoError(t, err) + require.Len(t, machines, len(test.expectedNames)) + + for _, m := range machines { + require.Contains(t, test.expectedNames, m.Name) + } + }) + } +} + +func buildMachineWithLabel(name string, labels map[string]string) *infrav1.IonosCloudMachine { + return &infrav1.IonosCloudMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: metav1.NamespaceDefault, + Labels: labels, + }, + } +} diff --git a/scope/machine.go b/scope/machine.go index f714460f..6c681295 100644 --- a/scope/machine.go +++ b/scope/machine.go @@ -122,24 +122,12 @@ func (m *Machine) CountMachines(ctx context.Context, machineLabels client.Matchi return len(machines), err } -// ListMachines returns a list of IonosCloudMachines in the same namespace and with the same cluster label. -// With machineLabels, additional search labels can be provided. +// ListMachines is a convenience wrapper function for the Cluster.ListMachines function. func (m *Machine) ListMachines( ctx context.Context, machineLabels client.MatchingLabels, ) ([]infrav1.IonosCloudMachine, error) { - if machineLabels == nil { - machineLabels = client.MatchingLabels{} - } - - machineLabels[clusterv1.ClusterNameLabel] = m.ClusterScope.Cluster.Name - listOpts := []client.ListOption{client.InNamespace(m.IonosMachine.Namespace), machineLabels} - - machineList := &infrav1.IonosCloudMachineList{} - if err := m.client.List(ctx, machineList, listOpts...); err != nil { - return nil, err - } - return machineList.Items, nil + return m.ClusterScope.ListMachines(ctx, machineLabels) } // FindLatestMachine returns the latest IonosCloudMachine in the same namespace @@ -151,23 +139,17 @@ func (m *Machine) FindLatestMachine( ctx context.Context, matchLabels client.MatchingLabels, ) (*infrav1.IonosCloudMachine, error) { - if matchLabels == nil { - matchLabels = client.MatchingLabels{} - } - - matchLabels[clusterv1.ClusterNameLabel] = m.ClusterScope.Cluster.Name - listOpts := []client.ListOption{client.InNamespace(m.IonosMachine.Namespace), matchLabels} - - machineList := &infrav1.IonosCloudMachineList{} - if err := m.client.List(ctx, machineList, listOpts...); err != nil { + machines, err := m.ClusterScope.ListMachines(ctx, matchLabels) + if err != nil { return nil, err } - if len(machineList.Items) <= 1 { + + if len(machines) <= 1 { return nil, nil } - latestMachine := machineList.Items[0] - for _, machine := range machineList.Items { + latestMachine := machines[0] + for _, machine := range machines { if !machine.CreationTimestamp.Before(&latestMachine.CreationTimestamp) && machine.Name != m.IonosMachine.Name { latestMachine = machine } diff --git a/scope/machine_test.go b/scope/machine_test.go index 47d0b2f2..68a59af1 100644 --- a/scope/machine_test.go +++ b/scope/machine_test.go @@ -37,10 +37,12 @@ func exampleParams(t *testing.T) MachineParams { if err := infrav1.AddToScheme(scheme.Scheme); err != nil { require.NoError(t, err, "could not construct params") } + cl := fake.NewClientBuilder().WithScheme(scheme.Scheme).Build() return MachineParams{ - Client: fake.NewClientBuilder().WithScheme(scheme.Scheme).Build(), + Client: cl, Machine: &clusterv1.Machine{}, ClusterScope: &Cluster{ + client: cl, Cluster: &clusterv1.Cluster{}, }, IonosMachine: &infrav1.IonosCloudMachine{}, @@ -96,7 +98,7 @@ func TestMachineHasFailedFailureMessage(t *testing.T) { func TestMachineHasFailedFailureReason(t *testing.T) { scope, err := NewMachine(exampleParams(t)) require.NoError(t, err) - scope.IonosMachine.Status.FailureReason = capierrors.MachineStatusErrorPtr("¯\\_(ツ)_/¯") + scope.IonosMachine.Status.FailureReason = (*capierrors.MachineStatusError)(ptr.To("¯\\_(ツ)_/¯")) require.True(t, scope.HasFailed()) } diff --git a/templates/cluster-template.yaml b/templates/cluster-template.yaml index 61045484..5960918f 100644 --- a/templates/cluster-template.yaml +++ b/templates/cluster-template.yaml @@ -31,7 +31,7 @@ metadata: name: "${CLUSTER_NAME}" spec: controlPlaneEndpoint: - host: ${CONTROL_PLANE_ENDPOINT_IP} + host: ${CONTROL_PLANE_ENDPOINT_HOST:-${CONTROL_PLANE_ENDPOINT_IP}} port: ${CONTROL_PLANE_ENDPOINT_PORT:-6443} location: ${CONTROL_PLANE_ENDPOINT_LOCATION} contractNumber: "${IONOSCLOUD_CONTRACT_NUMBER}"