From e1190f9acb12d7f36bf1b459c1879827fb65f1c3 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Wed, 3 Jan 2024 17:27:09 +0100 Subject: [PATCH 001/109] add api resources and tests --- api/v1alpha1/ionoscloudmachine_types.go | 122 ++++++++++++++++++ api/v1alpha1/ionoscloudmachine_types_test.go | 97 ++++++++++++++ api/v1alpha1/types.go | 37 ++++++ api/v1alpha1/zz_generated.deepcopy.go | 77 ++++++++++- ...e.cluster.x-k8s.io_ionoscloudmachines.yaml | 115 +++++++++++++++++ .../ionoscloudmachine_controller.go | 7 + 6 files changed, 454 insertions(+), 1 deletion(-) create mode 100644 api/v1alpha1/ionoscloudmachine_types_test.go create mode 100644 api/v1alpha1/types.go diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index dd674c53..a6e550b5 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -22,12 +22,120 @@ import ( "sigs.k8s.io/cluster-api/errors" ) +const ( + // IonosCloudMachineType is the named type for the API object. + IonosCloudMachineType = "IonosCloudMachine" +) + +// VolumeDiskType specifies the type of hard disk. +type VolumeDiskType string + +const ( + // VolumeDiskTypeHDD defines the disk type HDD. + VolumeDiskTypeHDD VolumeDiskType = "HDD" + // VolumeDiskTypeSSD defines the disk type SSD. + VolumeDiskTypeSSD VolumeDiskType = "SSD" +) + +// AvailabilityZone is the availability zone, where volumes are created. +type AvailabilityZone string + +const ( + // AvailabilityZoneAuto selected an automatic availability zone. + AvailabilityZoneAuto = "AUTO" + // AvailabilityZoneOne zone 1. + AvailabilityZoneOne = "ZONE_1" + // AvailabilityZoneTwo zone 2. + AvailabilityZoneTwo = "ZONE_2" + // AvailabilityZoneThree zone 3. + AvailabilityZoneThree = "ZONE_3" +) + // IonosCloudMachineSpec defines the desired state of IonosCloudMachine. 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"` + + // DatacenterID is the ID of the datacenter, where the machine should be created. + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="DatacenterID is immutable" + DatacenterID string `json:"datacenterID"` + + // ServerID is the unique identifier for a server in the IONOS Cloud context. + // The value will be set, once the server was created. + // +optional + ServerID string `json:"serverID,omitempty"` + + // Cores defines the total number of cores for the enterprise server. + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:default=1 + // +optional + NumCores int32 `json:"numCores,omitempty"` + + // MemoryMiB is the memory size for the enterprise server in MB. + // Size must be specified in multiples of 256 MB with a minimum of 1024 MB + // which is required as we are using hot-pluggable RAM by default. + // +kubebuilder:validation:MultipleOf=256 + // +kubebuilder:validation:Minimum=1024 + // +kubebuilder:default=1024 + // +optional + MemoryMiB int32 `json:"memoryMiB,omitempty"` + + // CPUFamily defines the CPU architecture, which will be used for this enterprise server. + // The not all CPU architectures are available in all datacenters. + // +kubebuilder:example=AMD_OPTERON + CPUFamily string `json:"cpuFamily"` + + // Disk defines the boot volume of the machine. + // +optional + Disk *Volume `json:"disk,omitempty"` + + // Network defines the network configuration for the enterprise server. + Network *Network `json:"network,omitempty"` +} + +// Network contains a network config. +type Network struct { + // IPs is an optional set of IP addresses, which have been + // reserved in the corresponding datacenter. + // +listType=set + // +optional + IPs []string `json:"ips,omitempty"` + + // UseDHCP sets whether dhcp should be used or not. + // NOTE(lubedacht) currently we do not support private clusters + // therefore dhcp must be set to true. + // +kubebuilder:default=true + // +optional + UseDHCP *bool `json:"useDhcp"` +} + +// Volume is the physical storage on the machine. +type Volume struct { + // Name is the name of the volume + // +optional + Name string `json:"name,omitempty"` + + // DiskType defines the type of the hard drive. + // +kubebuilder:validation:Enum=HDD;SSD + // +kubebuilder:default=HDD + // +optional + DiskType VolumeDiskType `json:"diskType,omitempty"` + + // SizeGB defines the size of the volume in GB + // +kubebuilder:validation:Minimum=5 + SizeGB int `json:"sizeGB"` + + // AvailabilityZone is the availabilityZone where the volume will be created. + // +kubebuilder:default=AUTO + // +optional + AvailabilityZone string `json:"availabilityZone,omitempty"` + + // SSHKeys contains a set of public ssh keys which will be added to the + // list of authorized keys. + // +listType=set + SSHKeys []string `json:"SSHKeys,omitempty"` } // IonosCloudMachineStatus defines the observed state of IonosCloudMachine. @@ -77,6 +185,10 @@ type IonosCloudMachineStatus struct { // Conditions defines current service state of the IonosCloudMachine. // +optional Conditions clusterv1.Conditions `json:"conditions,omitempty"` + + // CurrentRequest shows the current provisioning request for any + // cloud resource, that is being created. + CurrentRequest *ProvisioningRequest `json:"currentRequest,omitempty"` } //+kubebuilder:object:root=true @@ -100,6 +212,16 @@ type IonosCloudMachineList struct { Items []IonosCloudMachine `json:"items"` } +// GetConditions returns the observations of the operational state of the ProxmoxMachine resource. +func (m *IonosCloudMachine) GetConditions() clusterv1.Conditions { + return m.Status.Conditions +} + +// SetConditions sets the underlying service state of the ProxmoxMachine to the predescribed clusterv1.Conditions. +func (m *IonosCloudMachine) SetConditions(conditions clusterv1.Conditions) { + m.Status.Conditions = conditions +} + func init() { SchemeBuilder.Register(&IonosCloudMachine{}, &IonosCloudMachineList{}) } diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go new file mode 100644 index 00000000..1a5c43e0 --- /dev/null +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -0,0 +1,97 @@ +package v1alpha1 + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func defaultMachine() *IonosCloudMachine { + return &IonosCloudMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-machine", + Namespace: metav1.NamespaceDefault, + }, + Spec: IonosCloudMachineSpec{ + ProviderID: "ionos://ee090ff2-1eef-48ec-a246-a51a33aa4f3a", + ServerID: "11432411-cf83-4f62-b50f-798060493ea9", + Network: &Network{}, + }, + Status: IonosCloudMachineStatus{}, + } +} + +var _ = Describe("IonosCloudMachine Tests", func() { + AfterEach(func() { + err := k8sClient.Delete(context.Background(), defaultMachine()) + Expect(client.IgnoreNotFound(err)).To(Succeed()) + }) + + Context("Create", func() { + It("Should allow creation of valid machines", func() { + Expect(k8sClient.Create(context.Background(), defaultMachine())).To(Succeed()) + }) + }) + + Context("Validation", func() { + It("Should set defaults for Volumes", func() { + m := defaultMachine() + m.Spec.Disk = &Volume{ + Name: "test-volume", + SizeGB: 5, + } + + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(k8sClient.Get(context.Background(), client.ObjectKeyFromObject(m), m)).To(Succeed()) + + spec := m.Spec + Expect(spec.NumCores).To(Equal(int32(1))) + Expect(spec.MemoryMiB).To(Equal(int32(1024))) + + Expect(spec.Disk.DiskType).To(Equal(VolumeDiskTypeHDD)) + Expect(spec.Disk.AvailabilityZone).To(Equal("AUTO")) + Expect(spec.Disk.SizeGB).To(Equal(5)) + + Expect(spec.Network.UseDHCP).ToNot(BeNil()) + Expect(deref(spec.Network.UseDHCP, false)).To(BeTrue()) + }) + + It("Should fail if datacenterID would be updated", func() { + m := defaultMachine() + m.Spec.DatacenterID = "test" + + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(k8sClient.Get(context.Background(), client.ObjectKeyFromObject(m), m)).To(Succeed()) + + m.Spec.DatacenterID = "changed" + Expect(k8sClient.Update(context.Background(), m)).To(HaveOccurred()) + }) + + It("Should fail if size is less than 5", func() { + m := defaultMachine() + m.Spec.Disk = &Volume{ + Name: "test-volume", + SizeGB: 4, + } + + Expect(k8sClient.Create(context.Background(), m)).To(MatchError(ContainSubstring("should be greater than or equal to 5"))) + }) + + It("Should make sure to have unique ip addresses", func() { + m := defaultMachine() + + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + }) + }) +}) + +func deref[T any](ptr *T, def T) T { + if ptr != nil { + return *ptr + } + + return def +} diff --git a/api/v1alpha1/types.go b/api/v1alpha1/types.go new file mode 100644 index 00000000..f4a03bc2 --- /dev/null +++ b/api/v1alpha1/types.go @@ -0,0 +1,37 @@ +package v1alpha1 + +// RequestStatus shows the status of the current request. +type RequestStatus string + +const ( + // RequestStatusQueued indicates, that the request is queued and not yet being processed. + RequestStatusQueued RequestStatus = "QUEUED" + + // RequestStatusRunning indicates, that the request is currently being processed. + RequestStatusRunning RequestStatus = "RUNNING" + + // RequestStatusDone indicates, that the request has been successfully processed. + RequestStatusDone RequestStatus = "DONE" + + // RequestStatusFailed indicates, that the request has failed. + RequestStatusFailed RequestStatus = "FAILED" +) + +// ProvisioningRequest is a definition of a provisioning request +// in the IONOS Cloud. +type ProvisioningRequest struct { + // Method is the request method + Method string `json:"method"` + + // RequestPath is the sub path for the request URL + RequestPath string `json:"requestPath"` + + // RequestStatus is the status of the request in the queue. + // +kubebuilder:validation:Enum=QUEUED;RUNNING;DONE;FAILED + // +optional + State RequestStatus `json:"state"` + + // Message is the request message, which can also contain error information. + // +optional + Message string `json:"failureMessage,omitempty"` +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index ba900a61..b1080c08 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -128,7 +128,7 @@ func (in *IonosCloudMachine) DeepCopyInto(out *IonosCloudMachine) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -185,6 +185,16 @@ 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.Disk != nil { + in, out := &in.Disk, &out.Disk + *out = new(Volume) + (*in).DeepCopyInto(*out) + } + if in.Network != nil { + in, out := &in.Network, &out.Network + *out = new(Network) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudMachineSpec. @@ -217,6 +227,11 @@ func (in *IonosCloudMachineStatus) DeepCopyInto(out *IonosCloudMachineStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.CurrentRequest != nil { + in, out := &in.CurrentRequest, &out.CurrentRequest + *out = new(ProvisioningRequest) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudMachineStatus. @@ -228,3 +243,63 @@ func (in *IonosCloudMachineStatus) DeepCopy() *IonosCloudMachineStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Network) DeepCopyInto(out *Network) { + *out = *in + if in.IPs != nil { + in, out := &in.IPs, &out.IPs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.UseDHCP != nil { + in, out := &in.UseDHCP, &out.UseDHCP + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Network. +func (in *Network) DeepCopy() *Network { + if in == nil { + return nil + } + out := new(Network) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProvisioningRequest) DeepCopyInto(out *ProvisioningRequest) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProvisioningRequest. +func (in *ProvisioningRequest) DeepCopy() *ProvisioningRequest { + if in == nil { + return nil + } + out := new(ProvisioningRequest) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Volume) DeepCopyInto(out *Volume) { + *out = *in + if in.SSHKeys != nil { + in, out := &in.SSHKeys, &out.SSHKeys + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Volume. +func (in *Volume) DeepCopy() *Volume { + if in == nil { + return nil + } + out := new(Volume) + in.DeepCopyInto(out) + return out +} 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 c55cb380..ac87f258 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml @@ -34,10 +34,98 @@ spec: spec: description: IonosCloudMachineSpec defines the desired state of IonosCloudMachine. properties: + cpuFamily: + description: CPUFamily defines the CPU architecture, which will be + used for this enterprise server. The not all CPU architectures are + available in all datacenters. + example: AMD_OPTERON + type: string + datacenterID: + description: DatacenterID is the ID of the datacenter, where the machine + should be created. + type: string + x-kubernetes-validations: + - message: DatacenterID is immutable + rule: self == oldSelf + disk: + description: Disk defines the boot volume of the machine. + properties: + SSHKeys: + description: SSHKeys contains a set of public ssh keys which will + be added to the list of authorized keys. + items: + type: string + type: array + x-kubernetes-list-type: set + availabilityZone: + default: AUTO + description: AvailabilityZone is the availabilityZone where the + volume will be created. + type: string + diskType: + default: HDD + description: DiskType defines the type of the hard drive. + enum: + - HDD + - SSD + type: string + name: + description: Name is the name of the volume + type: string + sizeGB: + description: SizeGB defines the size of the volume in GB + minimum: 5 + type: integer + required: + - sizeGB + type: object + memoryMiB: + default: 1024 + description: MemoryMiB is the memory size for the enterprise server + in MB. Size must be specified in multiples of 256 MB with a minimum + of 1024 MB which is required as we are using hot-pluggable RAM by + default. + format: int32 + minimum: 1024 + multipleOf: 256 + type: integer + network: + description: Network defines the network configuration for the enterprise + server. + properties: + ips: + description: IPs is an optional set of IP addresses, which have + been reserved in the corresponding datacenter. + items: + type: string + type: array + x-kubernetes-list-type: set + useDhcp: + default: true + description: UseDHCP sets whether dhcp should be used or not. + NOTE(lubedacht) currently we do not support private clusters + therefore dhcp must be set to true. + type: boolean + type: object + numCores: + default: 1 + description: Cores defines the total number of cores for the enterprise + server. + format: int32 + minimum: 1 + type: integer providerId: description: ProviderID is the IONOS Cloud provider ID will be in the format ionos://ee090ff2-1eef-48ec-a246-a51a33aa4f3a type: string + serverID: + description: ServerID is the unique identifier for a server in the + IONOS Cloud context. The value will be set, once the server was + created. + type: string + required: + - cpuFamily + - datacenterID type: object status: description: IonosCloudMachineStatus defines the observed state of IonosCloudMachine. @@ -87,6 +175,33 @@ spec: - type type: object type: array + currentRequest: + description: CurrentRequest shows the current provisioning request + for any cloud resource, that is being created. + properties: + failureMessage: + description: Message is the request message, which can also contain + error information. + type: string + method: + description: Method is the request method + type: string + requestPath: + description: RequestPath is the sub path for the request URL + type: string + state: + description: RequestStatus is the status of the request in the + queue. + enum: + - QUEUED + - RUNNING + - DONE + - FAILED + type: string + required: + - method + - requestPath + type: object failureMessage: description: "FailureMessage will be set in the event that there is a terminal problem reconciling the Machine and will contain a more diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index 64892a14..30003e1b 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -19,6 +19,10 @@ package controller import ( "context" + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/cluster-api/util" + "sigs.k8s.io/controller-runtime/pkg/handler" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" @@ -66,5 +70,8 @@ func (r *IonosCloudMachineReconciler) Reconcile(ctx context.Context, req ctrl.Re func (r *IonosCloudMachineReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&infrav1.IonosCloudMachine{}). + Watches( + &clusterv1.Machine{}, + handler.EnqueueRequestsFromMapFunc(util.MachineToInfrastructureMapFunc(infrav1.GroupVersion.WithKind(infrav1.IonosCloudMachineType)))). Complete(r) } From b56a505eea19addd607bff04397bb88bf9e6fbf0 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 8 Jan 2024 10:45:14 +0100 Subject: [PATCH 002/109] adds test and license header --- api/v1alpha1/ionoscloudmachine_types.go | 2 +- api/v1alpha1/ionoscloudmachine_types_test.go | 21 ++++++++++++++++++-- api/v1alpha1/types.go | 16 +++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index a6e550b5..9321ffe5 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -187,7 +187,7 @@ type IonosCloudMachineStatus struct { Conditions clusterv1.Conditions `json:"conditions,omitempty"` // CurrentRequest shows the current provisioning request for any - // cloud resource, that is being created. + // cloud resource, that is being provisioned. CurrentRequest *ProvisioningRequest `json:"currentRequest,omitempty"` } diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index 1a5c43e0..da0ca080 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -1,3 +1,19 @@ +/* +Copyright 2023 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 ( @@ -80,10 +96,11 @@ var _ = Describe("IonosCloudMachine Tests", func() { Expect(k8sClient.Create(context.Background(), m)).To(MatchError(ContainSubstring("should be greater than or equal to 5"))) }) - It("Should make sure to have unique ip addresses", func() { + It("Should fail when providing duplicate ip address", func() { m := defaultMachine() - Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + m.Spec.Network.IPs = append(m.Spec.Network.IPs, "10.0.0.0", "10.0.0.1", "10.0.0.1") + Expect(k8sClient.Create(context.Background(), m)).To(MatchError(ContainSubstring("Duplicate value: \"10.0.0.1\""))) }) }) }) diff --git a/api/v1alpha1/types.go b/api/v1alpha1/types.go index f4a03bc2..a34be2dd 100644 --- a/api/v1alpha1/types.go +++ b/api/v1alpha1/types.go @@ -1,3 +1,19 @@ +/* +Copyright 2023 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 // RequestStatus shows the status of the current request. From 2b86e684e80bcc317fc7c57e57d71fb78cccf0b7 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 8 Jan 2024 12:14:21 +0100 Subject: [PATCH 003/109] satisfy sonarcloud --- api/v1alpha1/ionoscloudmachine_types_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index da0ca080..6e76108b 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -99,8 +99,8 @@ var _ = Describe("IonosCloudMachine Tests", func() { It("Should fail when providing duplicate ip address", func() { m := defaultMachine() - m.Spec.Network.IPs = append(m.Spec.Network.IPs, "10.0.0.0", "10.0.0.1", "10.0.0.1") - Expect(k8sClient.Create(context.Background(), m)).To(MatchError(ContainSubstring("Duplicate value: \"10.0.0.1\""))) + m.Spec.Network.IPs = append(m.Spec.Network.IPs, "192.0.2.0", "192.0.2.1", "192.0.2.1") + Expect(k8sClient.Create(context.Background(), m)).To(MatchError(ContainSubstring("Duplicate value: \"192.0.2.1\""))) }) }) }) From 2cbc010ab3e9cd277f8f02af5b6876e98347c267 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 8 Jan 2024 13:33:00 +0100 Subject: [PATCH 004/109] add tilt settings --- ...re.cluster.x-k8s.io_ionoscloudmachines.yaml | 2 +- config/default/kustomization.yaml | 18 +++++------------- config/default/manager_image_patch.yaml | 12 ++++++++++++ 3 files changed, 18 insertions(+), 14 deletions(-) create mode 100644 config/default/manager_image_patch.yaml 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 ac87f258..9f87b79e 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml @@ -177,7 +177,7 @@ spec: type: array currentRequest: description: CurrentRequest shows the current provisioning request - for any cloud resource, that is being created. + for any cloud resource, that is being provisioned. properties: failureMessage: description: Message is the request message, which can also contain diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index 57389b70..1870131b 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -1,18 +1,9 @@ # Adds namespace to all resources. -namespace: cluster-api-provider-ionoscloud-system +namePrefix: capic- +namespace: capic-system -# Value of this field is prepended to the -# names of all resources, e.g. a deployment named -# "wordpress" becomes "alices-wordpress". -# Note that it should also match with the prefix (text before '-') of the namespace -# field above. -namePrefix: cluster-api-provider-ionoscloud- - -# Labels to add to all resources and selectors. -#labels: -#- includeSelectors: true -# pairs: -# someName: someValue +commonLabels: + cluster.x-k8s.io/provider: infrastructure-ionoscloud resources: - ../crd @@ -31,6 +22,7 @@ patchesStrategicMerge: # If you want your controller-manager to expose the /metrics # endpoint w/o any authn/z, please comment the following line. - manager_auth_proxy_patch.yaml +- manager_image_patch.yaml diff --git a/config/default/manager_image_patch.yaml b/config/default/manager_image_patch.yaml new file mode 100644 index 00000000..c3eaa3ba --- /dev/null +++ b/config/default/manager_image_patch.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - image: ghcr.io/ionos-cloud/cluster-api-provider-ionoscloud:dev + name: manager From 2a60b295dc35023677d6a837299733a5c16fe872 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 8 Jan 2024 15:37:18 +0100 Subject: [PATCH 005/109] add type for enum values --- api/v1alpha1/ionoscloudmachine_types.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index 9321ffe5..bab1a32d 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -42,13 +42,13 @@ type AvailabilityZone string const ( // AvailabilityZoneAuto selected an automatic availability zone. - AvailabilityZoneAuto = "AUTO" + AvailabilityZoneAuto AvailabilityZone = "AUTO" // AvailabilityZoneOne zone 1. - AvailabilityZoneOne = "ZONE_1" + AvailabilityZoneOne AvailabilityZone = "ZONE_1" // AvailabilityZoneTwo zone 2. - AvailabilityZoneTwo = "ZONE_2" + AvailabilityZoneTwo AvailabilityZone = "ZONE_2" // AvailabilityZoneThree zone 3. - AvailabilityZoneThree = "ZONE_3" + AvailabilityZoneThree AvailabilityZone = "ZONE_3" ) // IonosCloudMachineSpec defines the desired state of IonosCloudMachine. From ddbe510cfc597d311540bf110a042347274f955c Mon Sep 17 00:00:00 2001 From: Gustavo Alves <112630064+gfariasalves-ionos@users.noreply.github.com> Date: Tue, 9 Jan 2024 11:12:52 +0100 Subject: [PATCH 006/109] Implement LAN provisioning (#22) --- .codespellignore | 2 + api/v1alpha1/ionoscloudcluster_types.go | 4 + api/v1alpha1/ionoscloudmachine_types_test.go | 2 +- api/v1alpha1/zz_generated.deepcopy.go | 23 +- ...e.cluster.x-k8s.io_ionoscloudclusters.yaml | 31 ++ hack/boilerplate.go.txt | 2 +- .../ionoscloudcluster_controller.go | 2 +- .../ionoscloudmachine_controller.go | 315 +++++++++++++++++- internal/ionoscloud/client.go | 11 +- internal/ionoscloud/client/client.go | 53 ++- internal/ionoscloud/client/client_test.go | 2 +- internal/ionoscloud/client/errors.go | 6 +- pkg/scope/machine.go | 91 +++++ 13 files changed, 507 insertions(+), 37 deletions(-) create mode 100644 pkg/scope/machine.go diff --git a/.codespellignore b/.codespellignore index ce223b56..202f388b 100644 --- a/.codespellignore +++ b/.codespellignore @@ -1,3 +1,5 @@ capi capic decorder +reterr +ionos \ No newline at end of file diff --git a/api/v1alpha1/ionoscloudcluster_types.go b/api/v1alpha1/ionoscloudcluster_types.go index 559ccdcc..a1e359dc 100644 --- a/api/v1alpha1/ionoscloudcluster_types.go +++ b/api/v1alpha1/ionoscloudcluster_types.go @@ -54,6 +54,10 @@ type IonosCloudClusterStatus struct { // Conditions defines current service state of the IonosCloudCluster. // +optional Conditions clusterv1.Conditions `json:"conditions,omitempty"` + + // PendingRequests is a map that maps data centers IDs with a pending provisioning request made during reconciliation. + // +optional + PendingRequests map[string]*ProvisioningRequest `json:"pendingRequests,omitempty"` } //+kubebuilder:object:root=true diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index 6e76108b..950910e6 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -1,5 +1,5 @@ /* -Copyright 2023 IONOS Cloud. +Copyright 2023-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. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index b1080c08..86dd37f4 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1,7 +1,7 @@ //go:build !ignore_autogenerated /* -Copyright 2023 IONOS Cloud. +Copyright 2023-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. @@ -111,6 +111,22 @@ func (in *IonosCloudClusterStatus) DeepCopyInto(out *IonosCloudClusterStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.PendingRequests != nil { + in, out := &in.PendingRequests, &out.PendingRequests + *out = make(map[string]*ProvisioningRequest, len(*in)) + for key, val := range *in { + var outVal *ProvisioningRequest + if val == nil { + (*out)[key] = nil + } else { + inVal := (*in)[key] + in, out := &inVal, &outVal + *out = new(ProvisioningRequest) + **out = **in + } + (*out)[key] = outVal + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudClusterStatus. @@ -227,11 +243,6 @@ func (in *IonosCloudMachineStatus) DeepCopyInto(out *IonosCloudMachineStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } - if in.CurrentRequest != nil { - in, out := &in.CurrentRequest, &out.CurrentRequest - *out = new(ProvisioningRequest) - **out = **in - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudMachineStatus. 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 cbfd3546..acf0f60e 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml @@ -120,6 +120,37 @@ spec: - type type: object type: array + pendingRequests: + additionalProperties: + description: ProvisioningRequest is a definition of a provisioning + request in the IONOS Cloud. + properties: + failureMessage: + description: Message is the request message, which can also + contain error information. + type: string + method: + description: Method is the request method + type: string + requestPath: + description: RequestPath is the sub path for the request URL + type: string + state: + description: RequestStatus is the status of the request in the + queue. + enum: + - QUEUED + - RUNNING + - DONE + - FAILED + type: string + required: + - method + - requestPath + type: object + description: PendingRequests is a map that maps data centers IDs with + a pending provisioning request made during reconciliation. + type: object ready: default: false description: Ready indicates that the cluster is ready. diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt index cc3f609d..7ecb9d36 100644 --- a/hack/boilerplate.go.txt +++ b/hack/boilerplate.go.txt @@ -1,5 +1,5 @@ /* -Copyright 2023 IONOS Cloud. +Copyright 2023-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. diff --git a/internal/controller/ionoscloudcluster_controller.go b/internal/controller/ionoscloudcluster_controller.go index f0029d2a..3eba04e4 100644 --- a/internal/controller/ionoscloudcluster_controller.go +++ b/internal/controller/ionoscloudcluster_controller.go @@ -1,5 +1,5 @@ /* -Copyright 2023 IONOS Cloud. +Copyright 2023-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. diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index 30003e1b..56a4e337 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -1,5 +1,5 @@ /* -Copyright 2023 IONOS Cloud. +Copyright 2023-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. @@ -18,18 +18,26 @@ package controller import ( "context" + "errors" + "fmt" + "net/http" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - "sigs.k8s.io/cluster-api/util" - "sigs.k8s.io/controller-runtime/pkg/handler" - + "github.com/go-logr/logr" + sdk "github.com/ionos-cloud/sdk-go/v6" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/klog/v2" + "k8s.io/utils/pointer" + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/cluster-api/util" + "sigs.k8s.io/cluster-api/util/annotations" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud" + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/pkg/scope" ) // IonosCloudMachineReconciler reconciles a IonosCloudMachine object. @@ -52,10 +60,9 @@ type IonosCloudMachineReconciler struct { // // 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, req ctrl.Request) (ctrl.Result, error) { - _ = ctrl.LoggerFrom(ctx) +func (r *IonosCloudMachineReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) { + logger := ctrl.LoggerFrom(ctx) - // TODO(user): your logic here ionosCloudMachine := &infrav1.IonosCloudMachine{} if err := r.Client.Get(ctx, req.NamespacedName, ionosCloudMachine); err != nil { if apierrors.IsNotFound(err) { @@ -63,6 +70,93 @@ func (r *IonosCloudMachineReconciler) Reconcile(ctx context.Context, req ctrl.Re } return ctrl.Result{}, err } + + // Fetch the Machine. + machine, err := util.GetOwnerMachine(ctx, r.Client, ionosCloudMachine.ObjectMeta) + if err != nil { + return ctrl.Result{}, err + } + if machine == nil { + logger.Info("machine controller has not yet set OwnerRef") + return ctrl.Result{}, nil + } + + logger = logger.WithValues("machine", klog.KObj(machine)) + + // Fetch the Cluster. + cluster, err := util.GetClusterFromMetadata(ctx, r.Client, machine.ObjectMeta) + if err != nil { + logger.Info("machine is missing cluster label or cluster does not exist") + return ctrl.Result{}, err + } + + if annotations.IsPaused(cluster, ionosCloudMachine) { + logger.Info("ionos cloud machine or linked cluster is marked as paused, not reconciling") + return ctrl.Result{}, nil + } + + logger = logger.WithValues("cluster", klog.KObj(cluster)) + + infraCluster, err := r.getInfraCluster(ctx, &logger, cluster, ionosCloudMachine) + if err != nil { + return ctrl.Result{}, fmt.Errorf("error getting infra provider cluster or control plane object: %w", err) + } + if infraCluster == nil { + logger.Info("ionos cloud machine is not ready yet") + return ctrl.Result{}, nil + } + + // Create the machine scope + machineScope, err := scope.NewMachineScope(scope.MachineScopeParams{ + Client: r.Client, + Cluster: cluster, + Machine: machine, + InfraCluster: infraCluster, + IonosCloudMachine: ionosCloudMachine, + Logger: &logger, + }) + if err != nil { + logger.Error(err, "failed to create scope") + return ctrl.Result{}, err + } + + //// Always close the scope when exiting this function, so we can persist any ProxmoxMachine changes. + // defer func() { + // if err := machineScope.Close(); err != nil && reterr == nil { + // reterr = err + // } + // }() + + if !ionosCloudMachine.ObjectMeta.DeletionTimestamp.IsZero() { + return r.reconcileDelete(ctx, machineScope) + } + + return r.reconcileNormal(ctx, machineScope) +} + +func (r *IonosCloudMachineReconciler) reconcileNormal( + ctx context.Context, machineScope *scope.MachineScope, +) (ctrl.Result, error) { + lan, err := r.reconcileLAN(ctx, machineScope) + if err != nil { + return ctrl.Result{}, fmt.Errorf("could not ensure lan: %w", err) + } + if lan == nil { + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, nil +} + +func (r *IonosCloudMachineReconciler) reconcileDelete( + ctx context.Context, machineScope *scope.MachineScope, +) (ctrl.Result, error) { + shouldProceed, err := r.reconcileLANDelete(ctx, machineScope) + if err != nil { + return ctrl.Result{}, fmt.Errorf("could not ensure lan: %w", err) + } + if err == nil && !shouldProceed { + return ctrl.Result{Requeue: true}, nil + } return ctrl.Result{}, nil } @@ -75,3 +169,208 @@ func (r *IonosCloudMachineReconciler) SetupWithManager(mgr ctrl.Manager) error { handler.EnqueueRequestsFromMapFunc(util.MachineToInfrastructureMapFunc(infrav1.GroupVersion.WithKind(infrav1.IonosCloudMachineType)))). Complete(r) } + +func (r *IonosCloudMachineReconciler) getInfraCluster( + ctx context.Context, logger *logr.Logger, cluster *clusterv1.Cluster, ionosCloudMachine *infrav1.IonosCloudMachine, +) (*scope.ClusterScope, error) { + var clusterScope *scope.ClusterScope + var err error + + ionosCloudCluster := &infrav1.IonosCloudCluster{} + + infraClusterName := client.ObjectKey{ + Namespace: ionosCloudMachine.Namespace, + Name: cluster.Spec.InfrastructureRef.Name, + } + + if err := r.Client.Get(ctx, infraClusterName, ionosCloudCluster); err != nil { + // IonosCloudCluster is not ready + return nil, nil //nolint:nilerr + } + + // Create the cluster scope + clusterScope, err = scope.NewClusterScope(scope.ClusterScopeParams{ + Client: r.Client, + Logger: logger, + Cluster: cluster, + IonosCluster: ionosCloudCluster, + IonosClient: r.IonosCloudClient, + }) + if err != nil { + return nil, fmt.Errorf("failed to creat cluster scope: %w", err) + } + + return clusterScope, nil +} + +const lanFormatString = "%s-k8s-lan" + +func (r *IonosCloudMachineReconciler) reconcileLAN( + ctx context.Context, machineScope *scope.MachineScope, +) (*sdk.Lan, error) { + logger := machineScope.Logger + dataCenterID := machineScope.IonosCloudMachine.Spec.DatacenterID + ionos := r.IonosCloudClient + clusterScope := machineScope.ClusterScope + clusterName := clusterScope.Cluster.Name + var err error + var lan *sdk.Lan + + // try to find available LAN + lan, err = r.findLANWithinDatacenterLANs(ctx, machineScope) + if err != nil { + return nil, fmt.Errorf("could not search for LAN within LAN list: %w", err) + } + if lan == nil { + // check if there is a provisioning request + reqStatus, err := r.checkProvisioningRequest(ctx, machineScope) + if err != nil && reqStatus == "" { + return nil, fmt.Errorf("could not check status of provisioning request: %w", err) + } + if reqStatus != "" { + req := clusterScope.IonosCluster.Status.PendingRequests[dataCenterID] + l := logger.WithValues( + "requestURL", req.RequestPath, + "requestMethod", req.Method, + "requestStatus", req.State) + switch reqStatus { + case string(infrav1.RequestStatusFailed): + delete(clusterScope.IonosCluster.Status.PendingRequests, dataCenterID) + return nil, fmt.Errorf("provisioning request has failed: %w", err) + case string(infrav1.RequestStatusQueued), string(infrav1.RequestStatusRunning): + l.Info("provisioning request hasn't finished yet. trying again later.") + return nil, nil + case string(infrav1.RequestStatusDone): + lan, err = r.findLANWithinDatacenterLANs(ctx, machineScope) + if err != nil { + return nil, fmt.Errorf("could not search for lan within lan list: %w", err) + } + if lan == nil { + l.Info("pending provisioning request has finished, but lan could not be found. trying again later.") + return nil, nil + } + } + } + } else { + return lan, nil + } + // request LAN creation + requestURL, err := ionos.CreateLAN(ctx, dataCenterID, sdk.LanPropertiesPost{ + Name: pointer.String(fmt.Sprintf(lanFormatString, clusterName)), + Public: pointer.Bool(true), + }) + if err != nil { + return nil, fmt.Errorf("could not create a new LAN: %w ", err) + } + clusterScope.IonosCluster.Status.PendingRequests[dataCenterID] = &infrav1.ProvisioningRequest{Method: requestURL} + logger.WithValues("requestURL", requestURL).Info("new LAN creation was requested") + + return nil, nil +} + +func (r *IonosCloudMachineReconciler) findLANWithinDatacenterLANs( + ctx context.Context, machineScope *scope.MachineScope, +) (lan *sdk.Lan, err error) { + dataCenterID := machineScope.IonosCloudMachine.Spec.DatacenterID + ionos := r.IonosCloudClient + clusterScope := machineScope.ClusterScope + clusterName := clusterScope.Cluster.Name + + lans, err := ionos.ListLANs(ctx, dataCenterID) + if err != nil { + return nil, fmt.Errorf("could not list lans: %w", err) + } + if lans.Items != nil { + for _, lan := range *(lans.Items) { + if name := lan.Properties.Name; name != nil && *name == fmt.Sprintf(lanFormatString, clusterName) { + return &lan, nil + } + } + } + return nil, nil +} + +func (r *IonosCloudMachineReconciler) checkProvisioningRequest( + ctx context.Context, machineScope *scope.MachineScope, +) (string, error) { + clusterScope := machineScope.ClusterScope + ionos := r.IonosCloudClient + dataCenterID := machineScope.IonosCloudMachine.Spec.DatacenterID + request, requestExists := clusterScope.IonosCluster.Status.PendingRequests[dataCenterID] + + if requestExists { + reqStatus, err := ionos.CheckRequestStatus(ctx, request.RequestPath) + if err != nil { + return "", fmt.Errorf("could not check status of provisioning request: %w", err) + } + clusterScope.IonosCluster.Status.PendingRequests[dataCenterID].State = infrav1.RequestStatus(*reqStatus.Metadata.Status) + clusterScope.IonosCluster.Status.PendingRequests[dataCenterID].Message = *reqStatus.Metadata.Message + if *reqStatus.Metadata.Status != sdk.RequestStatusDone { + if metadata := *reqStatus.Metadata; *metadata.Status == sdk.RequestStatusFailed { + return sdk.RequestStatusFailed, errors.New(*metadata.Message) + } + return *reqStatus.Metadata.Status, nil + } + } + return "", nil +} + +func (r *IonosCloudMachineReconciler) reconcileLANDelete(ctx context.Context, machineScope *scope.MachineScope) (bool, error) { + logger := machineScope.Logger + clusterScope := machineScope.ClusterScope + dataCenterID := machineScope.IonosCloudMachine.Spec.DatacenterID + lan, err := r.findLANWithinDatacenterLANs(ctx, machineScope) + if err != nil { + return false, fmt.Errorf("error while trying to find lan: %w", err) + } + // Check if there is a provisioning request going on + if lan != nil { + reqStatus, err := r.checkProvisioningRequest(ctx, machineScope) + if err != nil && reqStatus == "" { + return false, fmt.Errorf("could not check status of provisioning request: %w", err) + } + if reqStatus != "" { + req := clusterScope.IonosCluster.Status.PendingRequests[dataCenterID] + l := logger.WithValues( + "requestURL", req.RequestPath, + "requestMethod", req.Method, + "requestStatus", req.State) + switch reqStatus { + case string(infrav1.RequestStatusFailed): + delete(clusterScope.IonosCluster.Status.PendingRequests, dataCenterID) + return false, fmt.Errorf("provisioning request has failed: %w", err) + case string(infrav1.RequestStatusQueued), string(infrav1.RequestStatusRunning): + l.Info("provisioning request hasn't finished yet. trying again later.") + return false, nil + case string(infrav1.RequestStatusDone): + lan, err = r.findLANWithinDatacenterLANs(ctx, machineScope) + if err != nil { + return false, fmt.Errorf("could not search for lan within lan list: %w", err) + } + if lan != nil { + l.Info("pending provisioning request has finished, but lan could still be found. trying again later.") + return false, nil + } + } + } + } + if lan == nil { + logger.Info("lan seems to be deleted.") + return true, nil + } + if lan.Entities.HasNics() { + logger.Info("lan seems like it is still being used. let whoever still uses it delete it.") + // NOTE: the LAN isn't deleted, but we can use the bool to signalize that we can proceed with the machine deletion. + return true, nil + } + requestURL, err := r.IonosCloudClient.DestroyLAN(ctx, dataCenterID, *lan.Id) + if err != nil { + return false, fmt.Errorf("could not destroy lan: %w", err) + } + machineScope.ClusterScope.IonosCluster.Status.PendingRequests[dataCenterID] = &infrav1.ProvisioningRequest{ + Method: http.MethodDelete, + RequestPath: requestURL, + } + logger.WithValues("requestURL", requestURL).Info("requested LAN deletion") + return false, nil +} diff --git a/internal/ionoscloud/client.go b/internal/ionoscloud/client.go index 7fb08f05..aecd2858 100644 --- a/internal/ionoscloud/client.go +++ b/internal/ionoscloud/client.go @@ -1,5 +1,5 @@ /* -Copyright 2023 IONOS Cloud. +Copyright 2023-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. @@ -40,8 +40,7 @@ type Client interface { // DestroyServer deletes the server that matches the provided serverID in the specified data center. DestroyServer(ctx context.Context, dataCenterID, serverID string) error // CreateLAN creates a new LAN with the provided properties in the specified data center. - CreateLAN(ctx context.Context, dataCenterID string, properties ionoscloud.LanPropertiesPost) ( - *ionoscloud.LanPost, error) + CreateLAN(ctx context.Context, dataCenterID string, properties ionoscloud.LanPropertiesPost) (string, error) // UpdateLAN updates a LAN with the provided properties in the specified data center. UpdateLAN(ctx context.Context, dataCenterID string, lanID string, properties ionoscloud.LanProperties) ( *ionoscloud.Lan, error) @@ -53,11 +52,15 @@ type Client interface { // GetLAN returns the LAN that matches lanID in the specified data center. GetLAN(ctx context.Context, dataCenterID, lanID string) (*ionoscloud.Lan, error) // DestroyLAN deletes the LAN that matches the provided lanID in the specified data center. - DestroyLAN(ctx context.Context, dataCenterID, lanID string) error + DestroyLAN(ctx context.Context, dataCenterID, lanID string) (string, error) + // CheckRequestStatus checks the status of a provided request identified by requestID + CheckRequestStatus(ctx context.Context, requestID string) (*ionoscloud.RequestStatus, error) // ListVolumes returns a list of volumes in a specified data center. ListVolumes(ctx context.Context, dataCenterID string) (*ionoscloud.Volumes, error) // GetVolume returns the volume that matches volumeID in the specified data center. GetVolume(ctx context.Context, dataCenterID, volumeID string) (*ionoscloud.Volume, error) // DestroyVolume deletes the volume that matches volumeID in the specified data center. DestroyVolume(ctx context.Context, dataCenterID, volumeID string) error + // WaitForRequest waits for the completion of the provided request, return an error if it fails. + WaitForRequest(ctx context.Context, requestURL string) error } diff --git a/internal/ionoscloud/client/client.go b/internal/ionoscloud/client/client.go index eab70d40..5da2f5bb 100644 --- a/internal/ionoscloud/client/client.go +++ b/internal/ionoscloud/client/client.go @@ -1,5 +1,5 @@ /* -Copyright 2023 IONOS Cloud. +Copyright 2023-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. @@ -143,20 +143,23 @@ func (c *IonosCloudClient) DestroyServer(ctx context.Context, dataCenterID, serv return err } -// CreateLAN creates a new LAN with the provided properties in the specified data center. +// CreateLAN creates a new LAN with the provided properties in the specified data center, returning the request ID. func (c *IonosCloudClient) CreateLAN(ctx context.Context, dataCenterID string, properties sdk.LanPropertiesPost, -) (*sdk.LanPost, error) { +) (string, error) { if dataCenterID == "" { - return nil, errDataCenterIDIsEmpty + return "", errDataCenterIDIsEmpty } lanPost := sdk.LanPost{ Properties: &properties, } - lp, _, err := c.API.LANsApi.DatacentersLansPost(ctx, dataCenterID).Lan(lanPost).Execute() + _, req, err := c.API.LANsApi.DatacentersLansPost(ctx, dataCenterID).Lan(lanPost).Execute() if err != nil { - return nil, fmt.Errorf(apiCallErrWrapper, err) + return "", fmt.Errorf(apiCallErrWrapper, err) + } + if location := req.Header.Get("Location"); location != "" { + return location, nil } - return &lp, nil + return "", errors.New(apiNoLocationErrWrapper) } // UpdateLAN updates a LAN with the provided properties in the specified data center. @@ -221,18 +224,18 @@ func (c *IonosCloudClient) GetLAN(ctx context.Context, dataCenterID, lanID strin } // DestroyLAN deletes the LAN that matches the provided lanID in the specified data center. -func (c *IonosCloudClient) DestroyLAN(ctx context.Context, dataCenterID, lanID string) error { +func (c *IonosCloudClient) DestroyLAN(ctx context.Context, dataCenterID, lanID string) (string, error) { if dataCenterID == "" { - return errDataCenterIDIsEmpty + return "", errDataCenterIDIsEmpty } if lanID == "" { - return errLanIDIsEmpty + return "", errLanIDIsEmpty } - _, err := c.API.LANsApi.DatacentersLansDelete(ctx, dataCenterID, lanID).Execute() + req, err := c.API.LANsApi.DatacentersLansDelete(ctx, dataCenterID, lanID).Execute() if err != nil { - return fmt.Errorf(apiCallErrWrapper, err) + return "", fmt.Errorf(apiCallErrWrapper, err) } - return nil + return req.Header.Get("Location"), nil } // ListVolumes returns a list of volumes in the specified data center. @@ -278,3 +281,27 @@ func (c *IonosCloudClient) DestroyVolume(ctx context.Context, dataCenterID, volu } return nil } + +// CheckRequestStatus returns the status of a request and an error if checking for it fails. +func (c *IonosCloudClient) CheckRequestStatus(ctx context.Context, requestURL string) (*sdk.RequestStatus, error) { + if requestURL == "" { + return nil, errRequestURLIsEmpty + } + requestStatus, _, err := c.API.GetRequestStatus(ctx, requestURL) + if err != nil { + return nil, fmt.Errorf(apiCallErrWrapper, err) + } + return requestStatus, nil +} + +// WaitForRequest waits for the completion of the provided request, return an error if it fails. +func (c *IonosCloudClient) WaitForRequest(ctx context.Context, requestURL string) error { + if requestURL == "" { + return errRequestURLIsEmpty + } + _, err := c.API.WaitForRequest(ctx, requestURL) + if err != nil { + return fmt.Errorf(apiCallErrWrapper, err) + } + return nil +} diff --git a/internal/ionoscloud/client/client_test.go b/internal/ionoscloud/client/client_test.go index 802782ed..37dfe9e4 100644 --- a/internal/ionoscloud/client/client_test.go +++ b/internal/ionoscloud/client/client_test.go @@ -1,5 +1,5 @@ /* -Copyright 2023 IONOS Cloud. +Copyright 2023-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. diff --git a/internal/ionoscloud/client/errors.go b/internal/ionoscloud/client/errors.go index 6f7baf72..79964097 100644 --- a/internal/ionoscloud/client/errors.go +++ b/internal/ionoscloud/client/errors.go @@ -1,5 +1,5 @@ /* -Copyright 2023 IONOS Cloud. +Copyright 2023-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. @@ -23,8 +23,10 @@ var ( errServerIDIsEmpty = errors.New("error parsing server ID: value cannot be empty") errLanIDIsEmpty = errors.New("error parsing lan ID: value cannot be empty") errVolumeIDIsEmpty = errors.New("error parsing volume ID: value cannot be empty") + errRequestURLIsEmpty = errors.New("a request url is necessary for the operation") ) const ( - apiCallErrWrapper = "request to Cloud API has failed: %w" + apiCallErrWrapper = "request to Cloud API has failed: %w" + apiNoLocationErrWrapper = "request to Cloud API did not return the request url" ) diff --git a/pkg/scope/machine.go b/pkg/scope/machine.go new file mode 100644 index 00000000..24281e6c --- /dev/null +++ b/pkg/scope/machine.go @@ -0,0 +1,91 @@ +/* + * Copyright 2023-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 scope + +import ( + "context" + "errors" + "fmt" + + "github.com/go-logr/logr" + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/cluster-api/util/patch" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" +) + +// MachineScope defines a basic context for primary use in IonosCloudMachineReconciler. +type MachineScope struct { + *logr.Logger + + client client.Client + patchHelper *patch.Helper + Cluster *clusterv1.Cluster + Machine *clusterv1.Machine + + ClusterScope *ClusterScope + IonosCloudMachine *infrav1.IonosCloudMachine +} + +// MachineScopeParams is a struct that contains the params used to create a new MachineScope through NewMachineScope. +type MachineScopeParams struct { + Client client.Client + Logger *logr.Logger + Cluster *clusterv1.Cluster + Machine *clusterv1.Machine + InfraCluster *ClusterScope + IonosCloudMachine *infrav1.IonosCloudMachine +} + +// NewMachineScope creates a new MachineScope using the provided params. +func NewMachineScope(params MachineScopeParams) (*MachineScope, error) { + if params.Client == nil { + return nil, errors.New("machine scope params lack a client") + } + if params.Cluster == nil { + return nil, errors.New("machine scope params lack a cluster") + } + if params.Machine == nil { + return nil, errors.New("machine scope params lack a cluster api machine") + } + if params.IonosCloudMachine == nil { + return nil, errors.New("machine scope params lack a ionos cloud machine") + } + if params.InfraCluster == nil { + return nil, errors.New("machine scope params need a ionos cloud cluster scope") + } + if params.Logger == nil { + logger := log.FromContext(context.Background()) + params.Logger = &logger + } + helper, err := patch.NewHelper(params.IonosCloudMachine, params.Client) + if err != nil { + return nil, fmt.Errorf("failed to init patch helper: %w", err) + } + return &MachineScope{ + Logger: params.Logger, + client: params.Client, + patchHelper: helper, + Cluster: params.Cluster, + Machine: params.Machine, + ClusterScope: params.InfraCluster, + IonosCloudMachine: params.IonosCloudMachine, + }, nil +} From 0358f121718d6ad61beb3a75115fefd3755e258b Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Tue, 9 Jan 2024 12:08:20 +0100 Subject: [PATCH 007/109] update boilerplate license header and files --- README.md | 2 +- api/v1alpha1/groupversion_info.go | 2 +- api/v1alpha1/ionoscloudcluster_types.go | 2 +- api/v1alpha1/ionoscloudcluster_types_test.go | 2 +- api/v1alpha1/ionoscloudmachine_types.go | 2 +- api/v1alpha1/ionoscloudmachine_types_test.go | 2 +- api/v1alpha1/suite_test.go | 2 +- api/v1alpha1/types.go | 2 +- api/v1alpha1/zz_generated.deepcopy.go | 2 +- cmd/main.go | 2 +- hack/boilerplate.go.txt | 2 +- internal/controller/ionoscloudcluster_controller.go | 2 +- internal/controller/ionoscloudmachine_controller.go | 2 +- internal/controller/suite_test.go | 2 +- internal/ionoscloud/client.go | 2 +- internal/ionoscloud/client/client.go | 2 +- internal/ionoscloud/client/client_test.go | 2 +- internal/ionoscloud/client/errors.go | 2 +- internal/ionoscloud/clienttest/mock_client.go | 2 +- pkg/scope/cluster.go | 2 +- pkg/scope/cluster_test.go | 2 +- pkg/scope/machine.go | 2 +- tools/tools.go | 2 +- 23 files changed, 23 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 9455ee63..6cefd73a 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ To get started with developing, please see [our development docs](./docs/Develop ## License -Copyright 2023 IONOS Cloud. +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. diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go index 0cad5cdf..57a4ac33 100644 --- a/api/v1alpha1/groupversion_info.go +++ b/api/v1alpha1/groupversion_info.go @@ -1,5 +1,5 @@ /* -Copyright 2023 IONOS Cloud. +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. diff --git a/api/v1alpha1/ionoscloudcluster_types.go b/api/v1alpha1/ionoscloudcluster_types.go index a1e359dc..9f4fc65b 100644 --- a/api/v1alpha1/ionoscloudcluster_types.go +++ b/api/v1alpha1/ionoscloudcluster_types.go @@ -1,5 +1,5 @@ /* -Copyright 2023 IONOS Cloud. +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. diff --git a/api/v1alpha1/ionoscloudcluster_types_test.go b/api/v1alpha1/ionoscloudcluster_types_test.go index 8fdd3ae3..93ada629 100644 --- a/api/v1alpha1/ionoscloudcluster_types_test.go +++ b/api/v1alpha1/ionoscloudcluster_types_test.go @@ -1,5 +1,5 @@ /* -Copyright 2023 IONOS Cloud. +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. diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index bab1a32d..b95e9bb4 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -1,5 +1,5 @@ /* -Copyright 2023 IONOS Cloud. +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. diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index 950910e6..dcba1ad6 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -1,5 +1,5 @@ /* -Copyright 2023-2024 IONOS Cloud. +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. diff --git a/api/v1alpha1/suite_test.go b/api/v1alpha1/suite_test.go index 0ccdba33..efc2b39e 100644 --- a/api/v1alpha1/suite_test.go +++ b/api/v1alpha1/suite_test.go @@ -1,5 +1,5 @@ /* -Copyright 2023 IONOS Cloud. +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. diff --git a/api/v1alpha1/types.go b/api/v1alpha1/types.go index a34be2dd..4f637895 100644 --- a/api/v1alpha1/types.go +++ b/api/v1alpha1/types.go @@ -1,5 +1,5 @@ /* -Copyright 2023 IONOS Cloud. +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. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 86dd37f4..a0586a2e 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1,7 +1,7 @@ //go:build !ignore_autogenerated /* -Copyright 2023-2024 IONOS Cloud. +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. diff --git a/cmd/main.go b/cmd/main.go index 1d4b0d7a..bcb053b6 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,5 +1,5 @@ /* -Copyright 2023 IONOS Cloud. +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. diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt index 7ecb9d36..eb2d26db 100644 --- a/hack/boilerplate.go.txt +++ b/hack/boilerplate.go.txt @@ -1,5 +1,5 @@ /* -Copyright 2023-2024 IONOS Cloud. +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. diff --git a/internal/controller/ionoscloudcluster_controller.go b/internal/controller/ionoscloudcluster_controller.go index 3eba04e4..e8602a12 100644 --- a/internal/controller/ionoscloudcluster_controller.go +++ b/internal/controller/ionoscloudcluster_controller.go @@ -1,5 +1,5 @@ /* -Copyright 2023-2024 IONOS Cloud. +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. diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index 56a4e337..f9f8d235 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -1,5 +1,5 @@ /* -Copyright 2023-2024 IONOS Cloud. +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. diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index a782b3a7..32837925 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -1,5 +1,5 @@ /* -Copyright 2023 IONOS Cloud. +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. diff --git a/internal/ionoscloud/client.go b/internal/ionoscloud/client.go index aecd2858..8a6b84d8 100644 --- a/internal/ionoscloud/client.go +++ b/internal/ionoscloud/client.go @@ -1,5 +1,5 @@ /* -Copyright 2023-2024 IONOS Cloud. +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. diff --git a/internal/ionoscloud/client/client.go b/internal/ionoscloud/client/client.go index 5da2f5bb..12332293 100644 --- a/internal/ionoscloud/client/client.go +++ b/internal/ionoscloud/client/client.go @@ -1,5 +1,5 @@ /* -Copyright 2023-2024 IONOS Cloud. +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. diff --git a/internal/ionoscloud/client/client_test.go b/internal/ionoscloud/client/client_test.go index 37dfe9e4..c679f131 100644 --- a/internal/ionoscloud/client/client_test.go +++ b/internal/ionoscloud/client/client_test.go @@ -1,5 +1,5 @@ /* -Copyright 2023-2024 IONOS Cloud. +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. diff --git a/internal/ionoscloud/client/errors.go b/internal/ionoscloud/client/errors.go index 79964097..4e710efd 100644 --- a/internal/ionoscloud/client/errors.go +++ b/internal/ionoscloud/client/errors.go @@ -1,5 +1,5 @@ /* -Copyright 2023-2024 IONOS Cloud. +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. diff --git a/internal/ionoscloud/clienttest/mock_client.go b/internal/ionoscloud/clienttest/mock_client.go index 408eb113..58d98a4e 100644 --- a/internal/ionoscloud/clienttest/mock_client.go +++ b/internal/ionoscloud/clienttest/mock_client.go @@ -1,5 +1,5 @@ /* -Copyright 2023 IONOS Cloud. +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. diff --git a/pkg/scope/cluster.go b/pkg/scope/cluster.go index 123683cb..c4b035c4 100644 --- a/pkg/scope/cluster.go +++ b/pkg/scope/cluster.go @@ -1,5 +1,5 @@ /* -Copyright 2023 IONOS Cloud. +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. diff --git a/pkg/scope/cluster_test.go b/pkg/scope/cluster_test.go index c47a6be7..ff6cc474 100644 --- a/pkg/scope/cluster_test.go +++ b/pkg/scope/cluster_test.go @@ -1,5 +1,5 @@ /* -Copyright 2023 IONOS Cloud. +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. diff --git a/pkg/scope/machine.go b/pkg/scope/machine.go index 24281e6c..bb6ba382 100644 --- a/pkg/scope/machine.go +++ b/pkg/scope/machine.go @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 IONOS Cloud. + * 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. diff --git a/tools/tools.go b/tools/tools.go index e272479d..f902a4e7 100644 --- a/tools/tools.go +++ b/tools/tools.go @@ -2,7 +2,7 @@ // +build tools /* -Copyright 2023 IONOS Cloud. +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. From 9c14e767ee9ebe167229056c6cd2405e6d0c432b Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Tue, 9 Jan 2024 12:08:45 +0100 Subject: [PATCH 008/109] update manifests --- api/v1alpha1/zz_generated.deepcopy.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index a0586a2e..cde3fa4a 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -243,6 +243,11 @@ func (in *IonosCloudMachineStatus) DeepCopyInto(out *IonosCloudMachineStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.CurrentRequest != nil { + in, out := &in.CurrentRequest, &out.CurrentRequest + *out = new(ProvisioningRequest) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudMachineStatus. From 88d422e97c379cce58427770b449009fee530662 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Tue, 9 Jan 2024 12:10:35 +0100 Subject: [PATCH 009/109] fix license header in file --- pkg/scope/machine.go | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/pkg/scope/machine.go b/pkg/scope/machine.go index bb6ba382..4f42bdc9 100644 --- a/pkg/scope/machine.go +++ b/pkg/scope/machine.go @@ -1,19 +1,18 @@ /* - * 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. - * - */ +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 scope From 4ff0574a5a858be5cfa9ec9b5d47ad89adc651ff Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Tue, 9 Jan 2024 12:21:43 +0100 Subject: [PATCH 010/109] run tidy --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 507667f4..ec795e7f 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/stretchr/testify v1.8.4 k8s.io/apimachinery v0.28.4 k8s.io/client-go v0.28.4 + k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 k8s.io/klog/v2 v2.110.1 sigs.k8s.io/cluster-api v1.6.0 sigs.k8s.io/controller-runtime v0.16.3 @@ -74,7 +75,6 @@ require ( k8s.io/apiextensions-apiserver v0.28.4 // indirect k8s.io/component-base v0.28.4 // indirect k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect - k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/yaml v1.4.0 // indirect From 5c0e5f2de3c86f6577c877dbc0872bfa3d860c1d Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Tue, 9 Jan 2024 12:25:05 +0100 Subject: [PATCH 011/109] tidy after rebase --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index ec795e7f..be55218c 100644 --- a/go.mod +++ b/go.mod @@ -10,8 +10,8 @@ require ( github.com/stretchr/testify v1.8.4 k8s.io/apimachinery v0.28.4 k8s.io/client-go v0.28.4 - k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 k8s.io/klog/v2 v2.110.1 + k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 sigs.k8s.io/cluster-api v1.6.0 sigs.k8s.io/controller-runtime v0.16.3 ) From 9cfe0715c94503a11a4a239d416c9facb71da8be Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Tue, 9 Jan 2024 12:50:39 +0100 Subject: [PATCH 012/109] removed pkg folder --- internal/controller/ionoscloudcluster_controller.go | 2 +- internal/controller/ionoscloudmachine_controller.go | 2 +- {pkg/scope => scope}/cluster.go | 0 {pkg/scope => scope}/cluster_test.go | 0 {pkg/scope => scope}/machine.go | 0 5 files changed, 2 insertions(+), 2 deletions(-) rename {pkg/scope => scope}/cluster.go (100%) rename {pkg/scope => scope}/cluster_test.go (100%) rename {pkg/scope => scope}/machine.go (100%) diff --git a/internal/controller/ionoscloudcluster_controller.go b/internal/controller/ionoscloudcluster_controller.go index e8602a12..a943ebb7 100644 --- a/internal/controller/ionoscloudcluster_controller.go +++ b/internal/controller/ionoscloudcluster_controller.go @@ -40,7 +40,7 @@ import ( infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud" - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/pkg/scope" + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/scope" ) // IonosCloudClusterReconciler reconciles a IonosCloudCluster object. diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index f9f8d235..a6a596a9 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -37,7 +37,7 @@ import ( infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud" - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/pkg/scope" + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/scope" ) // IonosCloudMachineReconciler reconciles a IonosCloudMachine object. diff --git a/pkg/scope/cluster.go b/scope/cluster.go similarity index 100% rename from pkg/scope/cluster.go rename to scope/cluster.go diff --git a/pkg/scope/cluster_test.go b/scope/cluster_test.go similarity index 100% rename from pkg/scope/cluster_test.go rename to scope/cluster_test.go diff --git a/pkg/scope/machine.go b/scope/machine.go similarity index 100% rename from pkg/scope/machine.go rename to scope/machine.go From 0e924e5fe1fc59b0ba23e21594570a60851aaeee Mon Sep 17 00:00:00 2001 From: Gustavo Alves Date: Wed, 10 Jan 2024 03:17:00 +0100 Subject: [PATCH 013/109] Refactor LAN Provisioning --- .../ionoscloudmachine_controller.go | 205 ++---------------- internal/ionoscloud/client.go | 4 +- internal/ionoscloud/client/client.go | 29 ++- internal/service/datacenter.go | 22 ++ internal/service/network.go | 167 ++++++++++++++ internal/service/service.go | 48 ++++ 6 files changed, 284 insertions(+), 191 deletions(-) create mode 100644 internal/service/datacenter.go create mode 100644 internal/service/network.go create mode 100644 internal/service/service.go diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index a6a596a9..4bece503 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -18,16 +18,12 @@ package controller import ( "context" - "errors" "fmt" - "net/http" "github.com/go-logr/logr" - sdk "github.com/ionos-cloud/sdk-go/v6" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/klog/v2" - "k8s.io/utils/pointer" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" "sigs.k8s.io/cluster-api/util" "sigs.k8s.io/cluster-api/util/annotations" @@ -37,6 +33,7 @@ import ( infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud" + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/service" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/scope" ) @@ -127,36 +124,38 @@ func (r *IonosCloudMachineReconciler) Reconcile(ctx context.Context, req ctrl.Re // } // }() + machineService, err := service.NewMachineService(ctx, machineScope) + if err != nil { + return ctrl.Result{}, fmt.Errorf("could not create machine service") + } if !ionosCloudMachine.ObjectMeta.DeletionTimestamp.IsZero() { - return r.reconcileDelete(ctx, machineScope) + return r.reconcileDelete(machineService) } - return r.reconcileNormal(ctx, machineScope) + return r.reconcileNormal(machineService) } -func (r *IonosCloudMachineReconciler) reconcileNormal( - ctx context.Context, machineScope *scope.MachineScope, -) (ctrl.Result, error) { - lan, err := r.reconcileLAN(ctx, machineScope) +func (r *IonosCloudMachineReconciler) reconcileNormal(machineService *service.MachineService) (ctrl.Result, error) { + lan, err := machineService.GetLAN() if err != nil { - return ctrl.Result{}, fmt.Errorf("could not ensure lan: %w", err) + return ctrl.Result{}, fmt.Errorf("could not reconcile LAN: %w", err) } if lan == nil { return ctrl.Result{Requeue: true}, nil } + return ctrl.Result{}, nil } -func (r *IonosCloudMachineReconciler) reconcileDelete( - ctx context.Context, machineScope *scope.MachineScope, -) (ctrl.Result, error) { - shouldProceed, err := r.reconcileLANDelete(ctx, machineScope) +func (r *IonosCloudMachineReconciler) reconcileDelete(machineService *service.MachineService) (ctrl.Result, error) { + isLANGone, err := machineService.DeleteLAN("placeholder for LAN ID") if err != nil { - return ctrl.Result{}, fmt.Errorf("could not ensure lan: %w", err) + return ctrl.Result{}, fmt.Errorf("could not delete LAN: %w", err) } - if err == nil && !shouldProceed { + if err == nil && !isLANGone { return ctrl.Result{Requeue: true}, nil } + return ctrl.Result{}, nil } @@ -202,175 +201,3 @@ func (r *IonosCloudMachineReconciler) getInfraCluster( return clusterScope, nil } - -const lanFormatString = "%s-k8s-lan" - -func (r *IonosCloudMachineReconciler) reconcileLAN( - ctx context.Context, machineScope *scope.MachineScope, -) (*sdk.Lan, error) { - logger := machineScope.Logger - dataCenterID := machineScope.IonosCloudMachine.Spec.DatacenterID - ionos := r.IonosCloudClient - clusterScope := machineScope.ClusterScope - clusterName := clusterScope.Cluster.Name - var err error - var lan *sdk.Lan - - // try to find available LAN - lan, err = r.findLANWithinDatacenterLANs(ctx, machineScope) - if err != nil { - return nil, fmt.Errorf("could not search for LAN within LAN list: %w", err) - } - if lan == nil { - // check if there is a provisioning request - reqStatus, err := r.checkProvisioningRequest(ctx, machineScope) - if err != nil && reqStatus == "" { - return nil, fmt.Errorf("could not check status of provisioning request: %w", err) - } - if reqStatus != "" { - req := clusterScope.IonosCluster.Status.PendingRequests[dataCenterID] - l := logger.WithValues( - "requestURL", req.RequestPath, - "requestMethod", req.Method, - "requestStatus", req.State) - switch reqStatus { - case string(infrav1.RequestStatusFailed): - delete(clusterScope.IonosCluster.Status.PendingRequests, dataCenterID) - return nil, fmt.Errorf("provisioning request has failed: %w", err) - case string(infrav1.RequestStatusQueued), string(infrav1.RequestStatusRunning): - l.Info("provisioning request hasn't finished yet. trying again later.") - return nil, nil - case string(infrav1.RequestStatusDone): - lan, err = r.findLANWithinDatacenterLANs(ctx, machineScope) - if err != nil { - return nil, fmt.Errorf("could not search for lan within lan list: %w", err) - } - if lan == nil { - l.Info("pending provisioning request has finished, but lan could not be found. trying again later.") - return nil, nil - } - } - } - } else { - return lan, nil - } - // request LAN creation - requestURL, err := ionos.CreateLAN(ctx, dataCenterID, sdk.LanPropertiesPost{ - Name: pointer.String(fmt.Sprintf(lanFormatString, clusterName)), - Public: pointer.Bool(true), - }) - if err != nil { - return nil, fmt.Errorf("could not create a new LAN: %w ", err) - } - clusterScope.IonosCluster.Status.PendingRequests[dataCenterID] = &infrav1.ProvisioningRequest{Method: requestURL} - logger.WithValues("requestURL", requestURL).Info("new LAN creation was requested") - - return nil, nil -} - -func (r *IonosCloudMachineReconciler) findLANWithinDatacenterLANs( - ctx context.Context, machineScope *scope.MachineScope, -) (lan *sdk.Lan, err error) { - dataCenterID := machineScope.IonosCloudMachine.Spec.DatacenterID - ionos := r.IonosCloudClient - clusterScope := machineScope.ClusterScope - clusterName := clusterScope.Cluster.Name - - lans, err := ionos.ListLANs(ctx, dataCenterID) - if err != nil { - return nil, fmt.Errorf("could not list lans: %w", err) - } - if lans.Items != nil { - for _, lan := range *(lans.Items) { - if name := lan.Properties.Name; name != nil && *name == fmt.Sprintf(lanFormatString, clusterName) { - return &lan, nil - } - } - } - return nil, nil -} - -func (r *IonosCloudMachineReconciler) checkProvisioningRequest( - ctx context.Context, machineScope *scope.MachineScope, -) (string, error) { - clusterScope := machineScope.ClusterScope - ionos := r.IonosCloudClient - dataCenterID := machineScope.IonosCloudMachine.Spec.DatacenterID - request, requestExists := clusterScope.IonosCluster.Status.PendingRequests[dataCenterID] - - if requestExists { - reqStatus, err := ionos.CheckRequestStatus(ctx, request.RequestPath) - if err != nil { - return "", fmt.Errorf("could not check status of provisioning request: %w", err) - } - clusterScope.IonosCluster.Status.PendingRequests[dataCenterID].State = infrav1.RequestStatus(*reqStatus.Metadata.Status) - clusterScope.IonosCluster.Status.PendingRequests[dataCenterID].Message = *reqStatus.Metadata.Message - if *reqStatus.Metadata.Status != sdk.RequestStatusDone { - if metadata := *reqStatus.Metadata; *metadata.Status == sdk.RequestStatusFailed { - return sdk.RequestStatusFailed, errors.New(*metadata.Message) - } - return *reqStatus.Metadata.Status, nil - } - } - return "", nil -} - -func (r *IonosCloudMachineReconciler) reconcileLANDelete(ctx context.Context, machineScope *scope.MachineScope) (bool, error) { - logger := machineScope.Logger - clusterScope := machineScope.ClusterScope - dataCenterID := machineScope.IonosCloudMachine.Spec.DatacenterID - lan, err := r.findLANWithinDatacenterLANs(ctx, machineScope) - if err != nil { - return false, fmt.Errorf("error while trying to find lan: %w", err) - } - // Check if there is a provisioning request going on - if lan != nil { - reqStatus, err := r.checkProvisioningRequest(ctx, machineScope) - if err != nil && reqStatus == "" { - return false, fmt.Errorf("could not check status of provisioning request: %w", err) - } - if reqStatus != "" { - req := clusterScope.IonosCluster.Status.PendingRequests[dataCenterID] - l := logger.WithValues( - "requestURL", req.RequestPath, - "requestMethod", req.Method, - "requestStatus", req.State) - switch reqStatus { - case string(infrav1.RequestStatusFailed): - delete(clusterScope.IonosCluster.Status.PendingRequests, dataCenterID) - return false, fmt.Errorf("provisioning request has failed: %w", err) - case string(infrav1.RequestStatusQueued), string(infrav1.RequestStatusRunning): - l.Info("provisioning request hasn't finished yet. trying again later.") - return false, nil - case string(infrav1.RequestStatusDone): - lan, err = r.findLANWithinDatacenterLANs(ctx, machineScope) - if err != nil { - return false, fmt.Errorf("could not search for lan within lan list: %w", err) - } - if lan != nil { - l.Info("pending provisioning request has finished, but lan could still be found. trying again later.") - return false, nil - } - } - } - } - if lan == nil { - logger.Info("lan seems to be deleted.") - return true, nil - } - if lan.Entities.HasNics() { - logger.Info("lan seems like it is still being used. let whoever still uses it delete it.") - // NOTE: the LAN isn't deleted, but we can use the bool to signalize that we can proceed with the machine deletion. - return true, nil - } - requestURL, err := r.IonosCloudClient.DestroyLAN(ctx, dataCenterID, *lan.Id) - if err != nil { - return false, fmt.Errorf("could not destroy lan: %w", err) - } - machineScope.ClusterScope.IonosCluster.Status.PendingRequests[dataCenterID] = &infrav1.ProvisioningRequest{ - Method: http.MethodDelete, - RequestPath: requestURL, - } - logger.WithValues("requestURL", requestURL).Info("requested LAN deletion") - return false, nil -} diff --git a/internal/ionoscloud/client.go b/internal/ionoscloud/client.go index 8a6b84d8..63ab3bb8 100644 --- a/internal/ionoscloud/client.go +++ b/internal/ionoscloud/client.go @@ -39,7 +39,7 @@ type Client interface { GetServer(ctx context.Context, dataCenterID, serverID string) (*ionoscloud.Server, error) // DestroyServer deletes the server that matches the provided serverID in the specified data center. DestroyServer(ctx context.Context, dataCenterID, serverID string) error - // CreateLAN creates a new LAN with the provided properties in the specified data center. + // CreateLAN creates a new LAN with the provided properties in the specified data center, returning the request location. CreateLAN(ctx context.Context, dataCenterID string, properties ionoscloud.LanPropertiesPost) (string, error) // UpdateLAN updates a LAN with the provided properties in the specified data center. UpdateLAN(ctx context.Context, dataCenterID string, lanID string, properties ionoscloud.LanProperties) ( @@ -63,4 +63,6 @@ type Client interface { DestroyVolume(ctx context.Context, dataCenterID, volumeID string) error // WaitForRequest waits for the completion of the provided request, return an error if it fails. WaitForRequest(ctx context.Context, requestURL string) error + // GetRequests returns the requests made in the last 24 hours that match the provided method and path. + GetRequests(ctx context.Context, method, path string) (*[]ionoscloud.Request, error) } diff --git a/internal/ionoscloud/client/client.go b/internal/ionoscloud/client/client.go index 12332293..1e8b8f44 100644 --- a/internal/ionoscloud/client/client.go +++ b/internal/ionoscloud/client/client.go @@ -21,6 +21,8 @@ import ( "context" "errors" "fmt" + "slices" + "time" sdk "github.com/ionos-cloud/sdk-go/v6" @@ -143,7 +145,7 @@ func (c *IonosCloudClient) DestroyServer(ctx context.Context, dataCenterID, serv return err } -// CreateLAN creates a new LAN with the provided properties in the specified data center, returning the request ID. +// CreateLAN creates a new LAN with the provided properties in the specified data center, returning the request location. func (c *IonosCloudClient) CreateLAN(ctx context.Context, dataCenterID string, properties sdk.LanPropertiesPost, ) (string, error) { if dataCenterID == "" { @@ -294,6 +296,31 @@ func (c *IonosCloudClient) CheckRequestStatus(ctx context.Context, requestURL st return requestStatus, nil } +// GetRequests returns the requests made in the last 24 hours that match the provided method and path. +func (c *IonosCloudClient) GetRequests(ctx context.Context, method, path string) (*[]sdk.Request, error) { + if path == "" { + return nil, errors.New("path needs to be provided") + } + if method == "" { + return nil, errors.New("method needs to be provided") + } + yesterday := time.Now().Add(-24 * time.Hour).Format(time.DateTime) + reqs, _, err := c.API.RequestsApi.RequestsGet(ctx). + FilterMethod(method). + FilterUrl(path). + FilterCreatedAfter(yesterday). + Execute() + if err != nil { + return nil, fmt.Errorf("failed to get requests: %w", err) + } + items := *reqs.Items + slices.SortFunc(items, func(a, b sdk.Request) int { + // We invert the value to sort in descending order + return -a.Metadata.CreatedDate.Compare(b.Metadata.CreatedDate.Time) + }) + return &items, nil +} + // WaitForRequest waits for the completion of the provided request, return an error if it fails. func (c *IonosCloudClient) WaitForRequest(ctx context.Context, requestURL string) error { if requestURL == "" { diff --git a/internal/service/datacenter.go b/internal/service/datacenter.go new file mode 100644 index 00000000..276cc5dd --- /dev/null +++ b/internal/service/datacenter.go @@ -0,0 +1,22 @@ +/* +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 service + +// DataCenterID is a shortcut for getting the data center ID used by the IONOS Cloud machine. +func (s *MachineService) DataCenterID() string { + return s.scope.IonosCloudMachine.Spec.DatacenterID +} diff --git a/internal/service/network.go b/internal/service/network.go new file mode 100644 index 00000000..121bc9f5 --- /dev/null +++ b/internal/service/network.go @@ -0,0 +1,167 @@ +/* +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 service + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "strings" + + sdk "github.com/ionos-cloud/sdk-go/v6" + "k8s.io/utils/pointer" +) + +// LANName returns the name of the cluster LAN. +func (s *MachineService) LANName() string { + return fmt.Sprintf( + "k8s-lan-%s-%s", + s.scope.ClusterScope.Cluster.Namespace, + s.scope.ClusterScope.Cluster.Name) +} + +// GetLAN ensures a LAN is created and returns it if available. +func (s *MachineService) GetLAN() (*sdk.Lan, error) { + var err error + log := s.scope.Logger.WithName("GetLAN") + + // Check for LAN creation requests + requestExists, err := s.lanRequestExists(http.MethodPost, "") + if err != nil { + return nil, fmt.Errorf("could not check if a LAN request exists") + } + if requestExists { + return nil, nil + } + // Search for LAN + lans, err := s.API().ListLANs(s.ctx, s.DataCenterID()) + if err != nil { + return nil, fmt.Errorf("could not list LANs in data center") + } + for _, l := range *lans.Items { + if name := l.Properties.Name; name != nil && *name == s.LANName() { + return &l, nil + } + } + // Create LAN + log.Info("no LAN was found. requesting creation of a new one") + requestPath, err := s.API().CreateLAN(s.ctx, s.DataCenterID(), sdk.LanPropertiesPost{ + Name: pointer.String(s.LANName()), + Public: pointer.Bool(true), + }) + if err != nil { + return nil, fmt.Errorf("could not request LAN creation: %w", err) + } + log.WithValues("requestPath", requestPath).Info("Successfully requested for LAN creation") + return nil, nil +} + +// DeleteLAN deletes the lan used by the cluster. A bool indicates if the LAN still exists. +func (s *MachineService) DeleteLAN(lanID string) (bool, error) { + var err error + log := s.scope.Logger.WithName("DestroyLAN") + + // Check for LAN deletion requests + requestExists, err := s.lanRequestExists(http.MethodDelete, lanID) + if err != nil { + return false, fmt.Errorf("could not check if a LAN request exists: %w", err) + } + if requestExists { + log.Info("the latest deletion request has not finished yet, so let's try again later.") + return false, nil + } + // Search for LAN + lan, err := s.API().GetLAN(s.ctx, s.DataCenterID(), lanID) + if err != nil { + return false, fmt.Errorf("could not check if LAN exists: %w", err) + } + if lan != nil && len(*lan.Entities.Nics.Items) > 0 { + log.Info("the cluster still has more than node. skipping LAN deletion.") + return false, nil + } + if lan == nil { + log.Info("lan could not be found") + return true, nil + } + // Destroy LAN + log.Info("requesting deletion of LAN") + requestPath, err := s.API().DestroyLAN(s.ctx, s.DataCenterID(), lanID) + if err != nil { + return false, fmt.Errorf("could not request deletion of LAN: %w", err) + } + log.WithValues("requestPath", requestPath).Info("successfully requested lan deletion.") + return false, nil +} + +// lanRequestExists checks if there is a request for the creation or deletion of a LAN in the data center. +// For deletion requests, it is also necessary to provide the LAN ID (value will be ignored for creation). +func (s *MachineService) lanRequestExists(method string, lanID string) (bool, error) { + if method != http.MethodPost && method != http.MethodDelete { + return false, fmt.Errorf("invalid method %s (only POST and DELETE are valid)", method) + } + if method == http.MethodDelete && lanID == "" { + return false, fmt.Errorf("when method is DELETE, lanID cannot be empty") + } + + lanPath, err := url.JoinPath("datacenter", s.scope.IonosCloudMachine.Spec.DatacenterID, "lan") + if err != nil { + return false, fmt.Errorf("could not generate datacenter/{dataCenterID}/lan path: %w", err) + } + requests, err := s.API().GetRequests(s.ctx, method, lanPath) + if err != nil { + return false, fmt.Errorf("could not get requests: %w", err) + } + for _, r := range *requests { + if method == "POST" { + var lan sdk.Lan + err = json.Unmarshal([]byte(*r.Properties.Body), &lan) + if err != nil { + return false, fmt.Errorf("could not unmarshal request into LAN: %w", err) + } + if *lan.Properties.Name != s.LANName() { + continue + } + } else if method == "DELETE" { + u, err := url.Parse(*r.Properties.Url) + if err != nil { + return false, fmt.Errorf("could not format url: %w", err) + } + lanIDPath, err := url.JoinPath(lanPath, lanID) + if err != nil { + return false, fmt.Errorf("could not generate lanPath for lan resource: %w", err) + } + + if !strings.HasSuffix(u.Path, lanIDPath) { + continue + } + } + status := *r.Metadata.RequestStatus.Metadata.Status + if status == sdk.RequestStatusFailed { + message := r.Metadata.RequestStatus.Metadata.Message + s.scope.Logger.WithValues("requestID", r.Id, "requestStatus", status). + Error(errors.New(*message), "last request for LAN has failed. logging it for debugging purposes") + // We just log the error but not return it, so we can retry the request. + return false, nil + } + if status == sdk.RequestStatusQueued || status == sdk.RequestStatusRunning { + return true, nil + } + } + return false, nil +} diff --git a/internal/service/service.go b/internal/service/service.go new file mode 100644 index 00000000..44ae0e7e --- /dev/null +++ b/internal/service/service.go @@ -0,0 +1,48 @@ +/* +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 service offers infra resources services for IONOS Cloud machine reconciliation. +package service + +import ( + "context" + "errors" + + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud" + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/scope" +) + +// MachineService offers infra resources services for IONOS Cloud machine reconciliation. +type MachineService struct { + scope *scope.MachineScope + ctx context.Context +} + +// NewMachineService returns a new MachineService. +func NewMachineService(ctx context.Context, s *scope.MachineScope) (*MachineService, error) { + if s == nil { + return nil, errors.New("machine service cannot use a nil machine scope") + } + return &MachineService{ + scope: s, + ctx: ctx, + }, nil +} + +// API is a shortcut for the IONOS Cloud Client. +func (s *MachineService) API() ionoscloud.Client { + return s.scope.ClusterScope.IonosClient +} From 09be88b4d0eed6f22565cb57abd4cbee857afcbd Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Wed, 10 Jan 2024 17:56:21 +0100 Subject: [PATCH 014/109] updated lan creation logic --- api/v1alpha1/ionoscloudcluster_types.go | 2 +- api/v1alpha1/ionoscloudmachine_types.go | 18 ++ go.mod | 2 +- go.sum | 2 + .../ionoscloudcluster_controller.go | 8 +- .../ionoscloudmachine_controller.go | 106 ++++++-- internal/ionoscloud/client/client.go | 7 +- internal/service/{ => cloud}/datacenter.go | 4 +- internal/service/cloud/network.go | 226 ++++++++++++++++++ internal/service/cloud/request.go | 35 +++ internal/service/{ => cloud}/service.go | 18 +- internal/service/network.go | 167 ------------- scope/cluster.go | 6 +- scope/machine.go | 32 +++ 14 files changed, 420 insertions(+), 213 deletions(-) rename internal/service/{ => cloud}/datacenter.go (91%) create mode 100644 internal/service/cloud/network.go create mode 100644 internal/service/cloud/request.go rename internal/service/{ => cloud}/service.go (64%) delete mode 100644 internal/service/network.go diff --git a/api/v1alpha1/ionoscloudcluster_types.go b/api/v1alpha1/ionoscloudcluster_types.go index 9f4fc65b..35e6bd49 100644 --- a/api/v1alpha1/ionoscloudcluster_types.go +++ b/api/v1alpha1/ionoscloudcluster_types.go @@ -23,7 +23,7 @@ import ( const ( // ClusterFinalizer allows cleanup of resources, which are - // associated with the IonosCloudCluster before removing it from the apiserver. + // associated with the IonosCloudCluster before removing it from the API server. ClusterFinalizer = "ionoscloudcluster.infrastructure.cluster.x-k8s.io" // IonosCloudClusterReady is the condition for the IonosCloudCluster, which indicates that the cluster is ready. diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index b95e9bb4..37009e23 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -25,6 +25,24 @@ import ( const ( // IonosCloudMachineType is the named type for the API object. IonosCloudMachineType = "IonosCloudMachine" + + // MachineFinalizer is the finalizer for the IonosCloudMachine resources. + // It will prevent the deletion of the resource until it was removed by the controller + // to ensure that related cloud resources will be deleted before the IonosCloudMachine resource + // will be removed from the API server. + MachineFinalizer = "ionoscloudmachine.infrastructure.cluster.x-k8s.io" + + // MachineProvisionedCondition documents the status of the provisioning of a IonosCloudMachine and + // the underlying enterprise VM. + MachineProvisionedCondition clusterv1.ConditionType = "MachineProvisioned" + + // WaitingForClusterInfrastructureReason (Severity=Info) indicates, that the IonosCloudMachine is currently + // waiting for the cluster infrastructure to become ready. + WaitingForClusterInfrastructureReason = "WaitingForClusterInfrastructure" + + // WaitingForBootstrapDataReason (Severity=Info) indicates, that the bootstrap provider has not yet finished + // creating the bootstrap data secret and store it in the Cluster API Machine. + WaitingForBootstrapDataReason = "WaitingForBootstrapData" ) // VolumeDiskType specifies the type of hard disk. diff --git a/go.mod b/go.mod index be55218c..88dd44c5 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( k8s.io/apimachinery v0.28.4 k8s.io/client-go v0.28.4 k8s.io/klog/v2 v2.110.1 - k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 + k8s.io/utils v0.0.0-20240102154912-e7106e64919e sigs.k8s.io/cluster-api v1.6.0 sigs.k8s.io/controller-runtime v0.16.3 ) diff --git a/go.sum b/go.sum index 55af2adf..15b19b7e 100644 --- a/go.sum +++ b/go.sum @@ -291,6 +291,8 @@ k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5Ohx k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +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/cluster-api v1.6.0 h1:2bhVSnUbtWI8taCjd9lGiHExsRUpKf7Z1fXqi/IwYx4= sigs.k8s.io/cluster-api v1.6.0/go.mod h1:LB7u/WxiWj4/bbpHNOa1oQ8nq0MQ5iYlD0pGfRSBGLI= sigs.k8s.io/controller-runtime v0.16.3 h1:2TuvuokmfXvDUamSx1SuAOO3eTyye+47mJCigwG62c4= diff --git a/internal/controller/ionoscloudcluster_controller.go b/internal/controller/ionoscloudcluster_controller.go index a943ebb7..944ad60e 100644 --- a/internal/controller/ionoscloudcluster_controller.go +++ b/internal/controller/ionoscloudcluster_controller.go @@ -104,11 +104,7 @@ func (r *IonosCloudClusterReconciler) Reconcile(ctx context.Context, req ctrl.Re // Make sure to persist the changes to the cluster before exiting the function. defer func() { if err := clusterScope.Finalize(); err != nil { - if retErr != nil { - retErr = errors.Join(err, retErr) - return - } - retErr = err + retErr = errors.Join(err, retErr) } }() @@ -121,9 +117,7 @@ func (r *IonosCloudClusterReconciler) Reconcile(ctx context.Context, req ctrl.Re //nolint:unparam func (r *IonosCloudClusterReconciler) reconcileNormal(_ context.Context, clusterScope *scope.ClusterScope) (ctrl.Result, error) { - // TODO(lubedacht): setup cloud resources which are required before we create the machines controllerutil.AddFinalizer(clusterScope.IonosCluster, infrav1.ClusterFinalizer) - conditions.MarkTrue(clusterScope.IonosCluster, infrav1.IonosCloudClusterReady) clusterScope.IonosCluster.Status.Ready = true diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index 4bece503..9dde779b 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -18,7 +18,12 @@ package controller import ( "context" + "errors" "fmt" + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/service/cloud" + "sigs.k8s.io/cluster-api/util/conditions" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "time" "github.com/go-logr/logr" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -33,7 +38,6 @@ import ( infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud" - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/service" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/scope" ) @@ -94,11 +98,11 @@ func (r *IonosCloudMachineReconciler) Reconcile(ctx context.Context, req ctrl.Re logger = logger.WithValues("cluster", klog.KObj(cluster)) - infraCluster, err := r.getInfraCluster(ctx, &logger, cluster, ionosCloudMachine) + clusterScope, err := r.getClusterScope(ctx, &logger, cluster, ionosCloudMachine) if err != nil { return ctrl.Result{}, fmt.Errorf("error getting infra provider cluster or control plane object: %w", err) } - if infraCluster == nil { + if clusterScope == nil { logger.Info("ionos cloud machine is not ready yet") return ctrl.Result{}, nil } @@ -108,7 +112,7 @@ func (r *IonosCloudMachineReconciler) Reconcile(ctx context.Context, req ctrl.Re Client: r.Client, Cluster: cluster, Machine: machine, - InfraCluster: infraCluster, + InfraCluster: clusterScope, IonosCloudMachine: ionosCloudMachine, Logger: &logger, }) @@ -117,37 +121,97 @@ func (r *IonosCloudMachineReconciler) Reconcile(ctx context.Context, req ctrl.Re return ctrl.Result{}, err } - //// Always close the scope when exiting this function, so we can persist any ProxmoxMachine changes. - // defer func() { - // if err := machineScope.Close(); err != nil && reterr == nil { - // reterr = err - // } - // }() + defer func() { + if err := machineScope.Finalize(); err != nil { + reterr = errors.Join(err, reterr) + } + }() - machineService, err := service.NewMachineService(ctx, machineScope) + cloudService, err := cloud.NewService(ctx, machineScope) if err != nil { return ctrl.Result{}, fmt.Errorf("could not create machine service") } if !ionosCloudMachine.ObjectMeta.DeletionTimestamp.IsZero() { - return r.reconcileDelete(machineService) + return r.reconcileDelete(cloudService) } - return r.reconcileNormal(machineService) + return r.reconcileNormal(machineScope, clusterScope, cloudService) } -func (r *IonosCloudMachineReconciler) reconcileNormal(machineService *service.MachineService) (ctrl.Result, error) { - lan, err := machineService.GetLAN() - if err != nil { - return ctrl.Result{}, fmt.Errorf("could not reconcile LAN: %w", err) +func (r *IonosCloudMachineReconciler) isInfrastructureReady(machineScope *scope.MachineScope) bool { + // Make sure the infrastructure is ready. + if !machineScope.Cluster.Status.InfrastructureReady { + machineScope.Info("Cluster infrastructure is not ready yet") + conditions.MarkFalse( + machineScope.IonosCloudMachine, + infrav1.MachineProvisionedCondition, + infrav1.WaitingForClusterInfrastructureReason, + clusterv1.ConditionSeverityInfo, "") + + return false } - if lan == nil { - return ctrl.Result{Requeue: true}, nil + + // Make sure to wait until the data secret was created + if machineScope.Machine.Spec.Bootstrap.DataSecretName == nil { + machineScope.Info("Boostrap data secret is not available yet") + conditions.MarkFalse( + machineScope.IonosCloudMachine, + infrav1.MachineProvisionedCondition, + infrav1.WaitingForBootstrapDataReason, + clusterv1.ConditionSeverityInfo, "", + ) + + return false + } + + return true +} + +func (r *IonosCloudMachineReconciler) reconcileNormal(machineScope *scope.MachineScope, _ *scope.ClusterScope, cloudService *cloud.Service) (ctrl.Result, error) { + machineScope.V(4).Info("Reconciling IonosCloudMachine") + + if machineScope.HasFailed() { + machineScope.Info("Error state detected, skipping reconciliation") + return ctrl.Result{}, nil } + if !r.isInfrastructureReady(machineScope) { + return ctrl.Result{}, nil + } + + if controllerutil.AddFinalizer(machineScope.IonosCloudMachine, infrav1.MachineFinalizer) { + if err := machineScope.PatchObject(); err != nil { + machineScope.Error(err, "unable to update finalizer on object") + return ctrl.Result{}, err + } + } + + // TODO(lubedacht) Check before starting reconciliation if there is any pending request in the Ionos cluster or machine spec + // If there is, query for the request and check the status + // Status: + // * Done = Clear request from the status and continue reconciliation + // * Queued, Running => Requeue the current request + // * Failed => We need to discuss this, log error and continue (retry last request in the corresponding reconcile function) + + // Ensure that a lan is created in the datacenter + if requeue, err := cloudService.ReconcileLAN(); err != nil || requeue { + if requeue { + return ctrl.Result{RequeueAfter: time.Second * 30}, err + } + return ctrl.Result{}, fmt.Errorf("could not reconcile LAN %w", err) + } + + //if err != nil { + // return ctrl.Result{}, fmt.Errorf("could not reconcile LAN: %w", err) + //} + //if lan == nil { + // return ctrl.Result{Requeue: true}, nil + //} + return ctrl.Result{}, nil } -func (r *IonosCloudMachineReconciler) reconcileDelete(machineService *service.MachineService) (ctrl.Result, error) { +func (r *IonosCloudMachineReconciler) reconcileDelete(machineService *cloud.Service) (ctrl.Result, error) { isLANGone, err := machineService.DeleteLAN("placeholder for LAN ID") if err != nil { return ctrl.Result{}, fmt.Errorf("could not delete LAN: %w", err) @@ -169,7 +233,7 @@ func (r *IonosCloudMachineReconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } -func (r *IonosCloudMachineReconciler) getInfraCluster( +func (r *IonosCloudMachineReconciler) getClusterScope( ctx context.Context, logger *logr.Logger, cluster *clusterv1.Cluster, ionosCloudMachine *infrav1.IonosCloudMachine, ) (*scope.ClusterScope, error) { var clusterScope *scope.ClusterScope diff --git a/internal/ionoscloud/client/client.go b/internal/ionoscloud/client/client.go index 1e8b8f44..08d735df 100644 --- a/internal/ionoscloud/client/client.go +++ b/internal/ionoscloud/client/client.go @@ -304,11 +304,13 @@ func (c *IonosCloudClient) GetRequests(ctx context.Context, method, path string) if method == "" { return nil, errors.New("method needs to be provided") } - yesterday := time.Now().Add(-24 * time.Hour).Format(time.DateTime) + + lookback := time.Now().Add(-24 * time.Hour).Format(time.DateTime) reqs, _, err := c.API.RequestsApi.RequestsGet(ctx). + Depth(3). FilterMethod(method). FilterUrl(path). - FilterCreatedAfter(yesterday). + FilterCreatedAfter(lookback). Execute() if err != nil { return nil, fmt.Errorf("failed to get requests: %w", err) @@ -318,6 +320,7 @@ func (c *IonosCloudClient) GetRequests(ctx context.Context, method, path string) // We invert the value to sort in descending order return -a.Metadata.CreatedDate.Compare(b.Metadata.CreatedDate.Time) }) + return &items, nil } diff --git a/internal/service/datacenter.go b/internal/service/cloud/datacenter.go similarity index 91% rename from internal/service/datacenter.go rename to internal/service/cloud/datacenter.go index 276cc5dd..d986519d 100644 --- a/internal/service/datacenter.go +++ b/internal/service/cloud/datacenter.go @@ -14,9 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -package service +package cloud // DataCenterID is a shortcut for getting the data center ID used by the IONOS Cloud machine. -func (s *MachineService) DataCenterID() string { +func (s *Service) DataCenterID() string { return s.scope.IonosCloudMachine.Spec.DatacenterID } diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go new file mode 100644 index 00000000..f1efba20 --- /dev/null +++ b/internal/service/cloud/network.go @@ -0,0 +1,226 @@ +/* +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 cloud + +import ( + "errors" + "fmt" + infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" + "k8s.io/apimachinery/pkg/util/json" + "k8s.io/utils/ptr" + "net/http" + "path" + "strings" + + sdk "github.com/ionos-cloud/sdk-go/v6" +) + +// LANName returns the name of the cluster LAN. +func (s *Service) LANName() string { + return fmt.Sprintf( + "k8s-lan-%s-%s", + s.scope.ClusterScope.Cluster.Namespace, + s.scope.ClusterScope.Cluster.Name) +} + +func (s *Service) ReconcileLAN() (requeue bool, err error) { + log := s.scope.Logger.WithName("ReconcileLAN") + + // try to retrieve the cluster lan + clusterLan, err := s.GetLAN() + if clusterLan != nil || err != nil { + // If we found the LAN, we don't need to create one. + // TODO(lubedacht) check if patching is required => future task. + return false, err + + } + + // if we didn't find a lan, we check if a lan is already in creation + requestStatus, err := s.checkForPendingLanRequest(http.MethodPost, "") + if err != nil { + return false, fmt.Errorf("unable to list pending lan requests: %w", err) + } + + // We want to requeue and check again after some time + if requestStatus == sdk.RequestStatusRunning || requestStatus == sdk.RequestStatusQueued { + return true, nil + } + + // check again as the request might be done right after we checked + // to prevent duplicate creation + if requestStatus == sdk.RequestStatusDone { + clusterLan, err = s.GetLAN() + if clusterLan != nil || err != nil { + return false, err + } + + // If we still don't get a lan here even though we found request, which was done + // the lan was probably deleted before. + // Therefore, we will attempt to create the lan again. + // + // TODO(lubedacht) + // Another solution would be to query for a deletion request and check if the created time + // is bigger than the created time of the lan POST request. + } + + log.V(4).Info("No lan was found. Creating new lan") + if err := s.CreateLAN(); err != nil { + return false, err + } + + // after creating the lan, we want to requeue and let the request be finished + return true, nil +} + +// GetLAN tries to retrieve the cluster related lan in the datacenter. +func (s *Service) GetLAN() (*sdk.Lan, error) { + // check if the Lan exists + lans, err := s.API().ListLANs(s.ctx, s.DataCenterID()) + if err != nil { + return nil, fmt.Errorf("could not list lans in datacenter %s: %w", s.DataCenterID(), err) + } + + var foundLan *sdk.Lan + for _, l := range *lans.Items { + if name := l.Properties.Name; name != nil && *l.Properties.Name == s.LANName() { + foundLan = &l + break + } + } + + return foundLan, nil +} + +func (s *Service) CreateLAN() error { + log := s.scope.Logger.WithName("CreateLAN") + + requestPath, err := s.API().CreateLAN(s.ctx, s.DataCenterID(), sdk.LanPropertiesPost{ + Name: ptr.To(s.LANName()), + Public: ptr.To(true), + }) + + if err != nil { + return fmt.Errorf("unable to create lan in datacenter %s: %w", s.DataCenterID(), err) + } + + s.scope.ClusterScope.IonosCluster.Status.PendingRequests[s.DataCenterID()] = &infrav1.ProvisioningRequest{ + Method: http.MethodPost, + RequestPath: requestPath, + State: infrav1.RequestStatusQueued, + } + + err = s.scope.ClusterScope.PatchObject() + if err != nil { + return fmt.Errorf("unable to patch the cluster: %w", err) + } + + log.WithValues("requestPath", requestPath).Info("Successfully requested for LAN creation") + + return nil +} + +// DeleteLAN deletes the lan used by the cluster. A bool indicates if the LAN still exists. +//func (s *Service) DeleteLAN(lanID string) (bool, error) { +// var err error +// log := s.scope.Logger.WithName("DestroyLAN") +// +// // Check for LAN deletion requests +// requestExists, err := s.checkForPendingLanRequest(http.MethodDelete, lanID) +// if err != nil { +// return false, fmt.Errorf("could not check if a LAN request exists: %w", err) +// } +// if requestExists { +// log.Info("the latest deletion request has not finished yet, so let's try again later.") +// return false, nil +// } +// // Search for LAN +// lan, err := s.API().GetLAN(s.ctx, s.DataCenterID(), lanID) +// if err != nil { +// return false, fmt.Errorf("could not check if LAN exists: %w", err) +// } +// if lan != nil && len(*lan.Entities.Nics.Items) > 0 { +// log.Info("the cluster still has more than node. skipping LAN deletion.") +// return false, nil +// } +// if lan == nil { +// log.Info("lan could not be found") +// return true, nil +// } +// // Destroy LAN +// log.Info("requesting deletion of LAN") +// requestPath, err := s.API().DestroyLAN(s.ctx, s.DataCenterID(), lanID) +// if err != nil { +// return false, fmt.Errorf("could not request deletion of LAN: %w", err) +// } +// log.WithValues("requestPath", requestPath).Info("successfully requested lan deletion.") +// return false, nil +//} + +// checkForPendingLanRequest checks if there is a request for the creation, update or deletion of a LAN in the data center. +// For update and deletion requests, it is also necessary to provide the LAN ID (value will be ignored for creation). +func (s *Service) checkForPendingLanRequest(method string, lanID string) (status string, err error) { + switch method { + default: + return "", fmt.Errorf("unsupported method %s, allowed methods are %s", method, strings.Join( + []string{http.MethodPost, http.MethodDelete, http.MethodPatch}, + ",", + )) + case http.MethodDelete, http.MethodPatch: + if lanID == "" { + return "", errors.New("lanID cannot be empty for DELETE and PATCH requests") + } + break + case http.MethodPost: + break + } + + lanPath := path.Join("datacenters", s.DataCenterID(), "lan") + requests, err := s.getPendingRequests(method, lanPath) + if err != nil { + return "", err + } + + for _, r := range requests { + if method != http.MethodPost { + id := *(*r.Metadata.RequestStatus.Metadata.Targets)[0].Target.Id + if id != lanID { + continue + } + } else { + var lan sdk.Lan + err = json.Unmarshal([]byte(*r.Properties.Body), &lan) + if err != nil { + return "", fmt.Errorf("could not unmarshal request into LAN: %w", err) + } + if *lan.Properties.Name != s.LANName() { + continue + } + } + + status := *r.Metadata.RequestStatus.Metadata.Status + + if status == sdk.RequestStatusFailed { + // We just log the error but not return it, so we can retry the request. + message := r.Metadata.RequestStatus.Metadata.Message + s.scope.Logger.WithValues("requestID", r.Id, "requestStatus", status). + Error(errors.New(*message), "last request for LAN has failed. logging it for debugging purposes") + } + + return status, nil + } + return "", nil +} diff --git a/internal/service/cloud/request.go b/internal/service/cloud/request.go new file mode 100644 index 00000000..fcb7d868 --- /dev/null +++ b/internal/service/cloud/request.go @@ -0,0 +1,35 @@ +/* +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 cloud + +import ( + "fmt" + sdk "github.com/ionos-cloud/sdk-go/v6" +) + +func (s *Service) getPendingRequests(method, resourcePath string) ([]sdk.Request, error) { + requests, err := s.API().GetRequests(s.ctx, method, resourcePath) + if err != nil { + return nil, fmt.Errorf("could not get requests: %w", err) + } + + if requests == nil { + return nil, nil + } + + return *requests, nil +} diff --git a/internal/service/service.go b/internal/service/cloud/service.go similarity index 64% rename from internal/service/service.go rename to internal/service/cloud/service.go index 44ae0e7e..c72a43a5 100644 --- a/internal/service/service.go +++ b/internal/service/cloud/service.go @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package service offers infra resources services for IONOS Cloud machine reconciliation. -package service +// Package cloud offers infra resources services for IONOS Cloud machine reconciliation. +package cloud import ( "context" @@ -25,24 +25,24 @@ import ( "github.com/ionos-cloud/cluster-api-provider-ionoscloud/scope" ) -// MachineService offers infra resources services for IONOS Cloud machine reconciliation. -type MachineService struct { +// Service offers infra resources services for IONOS Cloud machine reconciliation. +type Service struct { scope *scope.MachineScope ctx context.Context } -// NewMachineService returns a new MachineService. -func NewMachineService(ctx context.Context, s *scope.MachineScope) (*MachineService, error) { +// NewService returns a new Service. +func NewService(ctx context.Context, s *scope.MachineScope) (*Service, error) { if s == nil { - return nil, errors.New("machine service cannot use a nil machine scope") + return nil, errors.New("cloud service cannot use a nil machine scope") } - return &MachineService{ + return &Service{ scope: s, ctx: ctx, }, nil } // API is a shortcut for the IONOS Cloud Client. -func (s *MachineService) API() ionoscloud.Client { +func (s *Service) API() ionoscloud.Client { return s.scope.ClusterScope.IonosClient } diff --git a/internal/service/network.go b/internal/service/network.go deleted file mode 100644 index 121bc9f5..00000000 --- a/internal/service/network.go +++ /dev/null @@ -1,167 +0,0 @@ -/* -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 service - -import ( - "encoding/json" - "errors" - "fmt" - "net/http" - "net/url" - "strings" - - sdk "github.com/ionos-cloud/sdk-go/v6" - "k8s.io/utils/pointer" -) - -// LANName returns the name of the cluster LAN. -func (s *MachineService) LANName() string { - return fmt.Sprintf( - "k8s-lan-%s-%s", - s.scope.ClusterScope.Cluster.Namespace, - s.scope.ClusterScope.Cluster.Name) -} - -// GetLAN ensures a LAN is created and returns it if available. -func (s *MachineService) GetLAN() (*sdk.Lan, error) { - var err error - log := s.scope.Logger.WithName("GetLAN") - - // Check for LAN creation requests - requestExists, err := s.lanRequestExists(http.MethodPost, "") - if err != nil { - return nil, fmt.Errorf("could not check if a LAN request exists") - } - if requestExists { - return nil, nil - } - // Search for LAN - lans, err := s.API().ListLANs(s.ctx, s.DataCenterID()) - if err != nil { - return nil, fmt.Errorf("could not list LANs in data center") - } - for _, l := range *lans.Items { - if name := l.Properties.Name; name != nil && *name == s.LANName() { - return &l, nil - } - } - // Create LAN - log.Info("no LAN was found. requesting creation of a new one") - requestPath, err := s.API().CreateLAN(s.ctx, s.DataCenterID(), sdk.LanPropertiesPost{ - Name: pointer.String(s.LANName()), - Public: pointer.Bool(true), - }) - if err != nil { - return nil, fmt.Errorf("could not request LAN creation: %w", err) - } - log.WithValues("requestPath", requestPath).Info("Successfully requested for LAN creation") - return nil, nil -} - -// DeleteLAN deletes the lan used by the cluster. A bool indicates if the LAN still exists. -func (s *MachineService) DeleteLAN(lanID string) (bool, error) { - var err error - log := s.scope.Logger.WithName("DestroyLAN") - - // Check for LAN deletion requests - requestExists, err := s.lanRequestExists(http.MethodDelete, lanID) - if err != nil { - return false, fmt.Errorf("could not check if a LAN request exists: %w", err) - } - if requestExists { - log.Info("the latest deletion request has not finished yet, so let's try again later.") - return false, nil - } - // Search for LAN - lan, err := s.API().GetLAN(s.ctx, s.DataCenterID(), lanID) - if err != nil { - return false, fmt.Errorf("could not check if LAN exists: %w", err) - } - if lan != nil && len(*lan.Entities.Nics.Items) > 0 { - log.Info("the cluster still has more than node. skipping LAN deletion.") - return false, nil - } - if lan == nil { - log.Info("lan could not be found") - return true, nil - } - // Destroy LAN - log.Info("requesting deletion of LAN") - requestPath, err := s.API().DestroyLAN(s.ctx, s.DataCenterID(), lanID) - if err != nil { - return false, fmt.Errorf("could not request deletion of LAN: %w", err) - } - log.WithValues("requestPath", requestPath).Info("successfully requested lan deletion.") - return false, nil -} - -// lanRequestExists checks if there is a request for the creation or deletion of a LAN in the data center. -// For deletion requests, it is also necessary to provide the LAN ID (value will be ignored for creation). -func (s *MachineService) lanRequestExists(method string, lanID string) (bool, error) { - if method != http.MethodPost && method != http.MethodDelete { - return false, fmt.Errorf("invalid method %s (only POST and DELETE are valid)", method) - } - if method == http.MethodDelete && lanID == "" { - return false, fmt.Errorf("when method is DELETE, lanID cannot be empty") - } - - lanPath, err := url.JoinPath("datacenter", s.scope.IonosCloudMachine.Spec.DatacenterID, "lan") - if err != nil { - return false, fmt.Errorf("could not generate datacenter/{dataCenterID}/lan path: %w", err) - } - requests, err := s.API().GetRequests(s.ctx, method, lanPath) - if err != nil { - return false, fmt.Errorf("could not get requests: %w", err) - } - for _, r := range *requests { - if method == "POST" { - var lan sdk.Lan - err = json.Unmarshal([]byte(*r.Properties.Body), &lan) - if err != nil { - return false, fmt.Errorf("could not unmarshal request into LAN: %w", err) - } - if *lan.Properties.Name != s.LANName() { - continue - } - } else if method == "DELETE" { - u, err := url.Parse(*r.Properties.Url) - if err != nil { - return false, fmt.Errorf("could not format url: %w", err) - } - lanIDPath, err := url.JoinPath(lanPath, lanID) - if err != nil { - return false, fmt.Errorf("could not generate lanPath for lan resource: %w", err) - } - - if !strings.HasSuffix(u.Path, lanIDPath) { - continue - } - } - status := *r.Metadata.RequestStatus.Metadata.Status - if status == sdk.RequestStatusFailed { - message := r.Metadata.RequestStatus.Metadata.Message - s.scope.Logger.WithValues("requestID", r.Id, "requestStatus", status). - Error(errors.New(*message), "last request for LAN has failed. logging it for debugging purposes") - // We just log the error but not return it, so we can retry the request. - return false, nil - } - if status == sdk.RequestStatusQueued || status == sdk.RequestStatusRunning { - return true, nil - } - } - return false, nil -} diff --git a/scope/cluster.go b/scope/cluster.go index c4b035c4..355289ac 100644 --- a/scope/cluster.go +++ b/scope/cluster.go @@ -98,9 +98,9 @@ func NewClusterScope(params ClusterScopeParams) (*ClusterScope, error) { return clusterScope, nil } -// patchObject will apply all changes from the IonosCloudCluster. +// PatchObject will apply all changes from the IonosCloudCluster. // It will also make sure to patch the status subresource. -func (c *ClusterScope) patchObject() error { +func (c *ClusterScope) PatchObject() error { // always set the ready condition conditions.SetSummary(c.IonosCluster, conditions.WithConditions(infrav1.IonosCloudClusterReady)) @@ -125,5 +125,5 @@ func (c *ClusterScope) Finalize() error { return retry.OnError( retry.DefaultBackoff, shouldRetry, - c.patchObject) + c.PatchObject) } diff --git a/scope/machine.go b/scope/machine.go index 4f42bdc9..dc84fe5c 100644 --- a/scope/machine.go +++ b/scope/machine.go @@ -20,6 +20,8 @@ import ( "context" "errors" "fmt" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/cluster-api/util/conditions" "github.com/go-logr/logr" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" @@ -88,3 +90,33 @@ func NewMachineScope(params MachineScopeParams) (*MachineScope, error) { IonosCloudMachine: params.IonosCloudMachine, }, nil } + +func (m *MachineScope) HasFailed() bool { + status := m.IonosCloudMachine.Status + return status.FailureReason != nil || status.FailureMessage != nil +} + +func (m *MachineScope) PatchObject() error { + conditions.SetSummary(m.IonosCloudMachine, + conditions.WithConditions( + infrav1.MachineProvisionedCondition)) + + return m.patchHelper.Patch( + context.TODO(), + m.IonosCloudMachine, + patch.WithOwnedConditions{Conditions: []clusterv1.ConditionType{ + clusterv1.ReadyCondition, + infrav1.MachineProvisionedCondition, + }}) +} + +func (m *MachineScope) Finalize() error { + // NOTE(lubedacht) retry is only a way to reduce the failure chance, + // but in general, the reconciliation logic must be resilient + // to handle an outdated resource from that API server. + shouldRetry := func(error) bool { return true } + return retry.OnError( + retry.DefaultBackoff, + shouldRetry, + m.PatchObject) +} From e2bdee6b1824ea85508809323c9ab73e46567fec Mon Sep 17 00:00:00 2001 From: Gustavo Alves Date: Thu, 11 Jan 2024 10:51:58 +0100 Subject: [PATCH 015/109] Refactor LAN deletion reconciliation --- .../ionoscloudmachine_controller.go | 14 +- internal/service/cloud/network.go | 125 ++++++++++++------ 2 files changed, 90 insertions(+), 49 deletions(-) diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index 9dde779b..3ad1ff3a 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -211,16 +211,14 @@ func (r *IonosCloudMachineReconciler) reconcileNormal(machineScope *scope.Machin return ctrl.Result{}, nil } -func (r *IonosCloudMachineReconciler) reconcileDelete(machineService *cloud.Service) (ctrl.Result, error) { - isLANGone, err := machineService.DeleteLAN("placeholder for LAN ID") +func (r *IonosCloudMachineReconciler) reconcileDelete(cloudService *cloud.Service) (ctrl.Result, error) { + requeue, err := cloudService.ReconcileLANDeletion() if err != nil { - return ctrl.Result{}, fmt.Errorf("could not delete LAN: %w", err) + return ctrl.Result{}, fmt.Errorf("could not reconcile LAN deletion: %w", err) } - if err == nil && !isLANGone { - return ctrl.Result{Requeue: true}, nil - } - - return ctrl.Result{}, nil + return ctrl.Result{ + Requeue: requeue, + }, nil } // SetupWithManager sets up the controller with the Manager. diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index f1efba20..b4cf0755 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -50,7 +50,7 @@ func (s *Service) ReconcileLAN() (requeue bool, err error) { } // if we didn't find a lan, we check if a lan is already in creation - requestStatus, err := s.checkForPendingLanRequest(http.MethodPost, "") + requestStatus, err := s.checkForPendingLANRequest(http.MethodPost, "") if err != nil { return false, fmt.Errorf("unable to list pending lan requests: %w", err) } @@ -105,6 +105,56 @@ func (s *Service) GetLAN() (*sdk.Lan, error) { return foundLan, nil } +func (s *Service) ReconcileLANDeletion() (requeue bool, err error) { + log := s.scope.Logger.WithName("ReconcileLANDeletion") + + // try to retrieve the cluster LAN + clusterLAN, err := s.GetLAN() + if clusterLAN == nil { + err = s.removeLANPendingRequestFromCluster() + return err != nil, err + } + if err != nil { + return false, err + } + + // if we found a LAN, we check if there is a deletion already in process + requestStatus, err := s.checkForPendingLANRequest(http.MethodDelete, *clusterLAN.Id) + if err != nil { + return false, fmt.Errorf("unable to list pending LAN requests: %w", err) + } + if requestStatus != "" { + // We want to requeue and check again after some time + if requestStatus == sdk.RequestStatusRunning || requestStatus == sdk.RequestStatusQueued { + return true, nil + } + + if requestStatus == sdk.RequestStatusDone { + // Here we can check if the LAN is indeed gone or there's some inconsistency in the last request or + // this request points to an old, far gone LAN with the same ID. + clusterLAN, err = s.GetLAN() + if clusterLAN == nil { + err = s.removeLANPendingRequestFromCluster() + return err != nil, err + } + if err != nil { + return false, err + } + } + } + + if clusterLAN != nil && len(*clusterLAN.Entities.Nics.Items) > 0 { + log.Info("the cluster LAN is still being used by another resource. skipping deletion") + return false, nil + } + // Request for LAN destruction + err = s.DeleteLAN(*clusterLAN.Id) + if err != nil { + return false, err + } + return true, nil +} + func (s *Service) CreateLAN() error { log := s.scope.Logger.WithName("CreateLAN") @@ -114,7 +164,7 @@ func (s *Service) CreateLAN() error { }) if err != nil { - return fmt.Errorf("unable to create lan in datacenter %s: %w", s.DataCenterID(), err) + return fmt.Errorf("unable to create lan in data center %s: %w", s.DataCenterID(), err) } s.scope.ClusterScope.IonosCluster.Status.PendingRequests[s.DataCenterID()] = &infrav1.ProvisioningRequest{ @@ -133,46 +183,31 @@ func (s *Service) CreateLAN() error { return nil } -// DeleteLAN deletes the lan used by the cluster. A bool indicates if the LAN still exists. -//func (s *Service) DeleteLAN(lanID string) (bool, error) { -// var err error -// log := s.scope.Logger.WithName("DestroyLAN") -// -// // Check for LAN deletion requests -// requestExists, err := s.checkForPendingLanRequest(http.MethodDelete, lanID) -// if err != nil { -// return false, fmt.Errorf("could not check if a LAN request exists: %w", err) -// } -// if requestExists { -// log.Info("the latest deletion request has not finished yet, so let's try again later.") -// return false, nil -// } -// // Search for LAN -// lan, err := s.API().GetLAN(s.ctx, s.DataCenterID(), lanID) -// if err != nil { -// return false, fmt.Errorf("could not check if LAN exists: %w", err) -// } -// if lan != nil && len(*lan.Entities.Nics.Items) > 0 { -// log.Info("the cluster still has more than node. skipping LAN deletion.") -// return false, nil -// } -// if lan == nil { -// log.Info("lan could not be found") -// return true, nil -// } -// // Destroy LAN -// log.Info("requesting deletion of LAN") -// requestPath, err := s.API().DestroyLAN(s.ctx, s.DataCenterID(), lanID) -// if err != nil { -// return false, fmt.Errorf("could not request deletion of LAN: %w", err) -// } -// log.WithValues("requestPath", requestPath).Info("successfully requested lan deletion.") -// return false, nil -//} - -// checkForPendingLanRequest checks if there is a request for the creation, update or deletion of a LAN in the data center. +func (s *Service) DeleteLAN(lanID string) error { + log := s.scope.Logger.WithName("DeleteLAN") + + requestPath, err := s.API().DestroyLAN(s.ctx, s.DataCenterID(), lanID) + if err != nil { + return fmt.Errorf("unable to request lan deletion in data center: %w", err) + } + + s.scope.ClusterScope.IonosCluster.Status.PendingRequests[s.DataCenterID()] = &infrav1.ProvisioningRequest{ + Method: http.MethodDelete, + RequestPath: requestPath, + State: infrav1.RequestStatusQueued, + } + + err = s.scope.ClusterScope.PatchObject() + if err != nil { + return fmt.Errorf("unable to patch cluster: %w", err) + } + log.WithValues("requestPath", requestPath).Info("Successfully requested for LAN deletion") + return nil +} + +// checkForPendingLANRequest checks if there is a request for the creation, update or deletion of a LAN in the data center. // For update and deletion requests, it is also necessary to provide the LAN ID (value will be ignored for creation). -func (s *Service) checkForPendingLanRequest(method string, lanID string) (status string, err error) { +func (s *Service) checkForPendingLANRequest(method string, lanID string) (status string, err error) { switch method { default: return "", fmt.Errorf("unsupported method %s, allowed methods are %s", method, strings.Join( @@ -224,3 +259,11 @@ func (s *Service) checkForPendingLanRequest(method string, lanID string) (status } return "", nil } + +func (s *Service) removeLANPendingRequestFromCluster() error { + s.scope.ClusterScope.IonosCluster.Status.PendingRequests[s.DataCenterID()] = nil + if err := s.scope.ClusterScope.PatchObject(); err != nil { + return fmt.Errorf("could not remove stale LAN pending request from cluster: %w", err) + } + return nil +} From e05cd012f706186c15e59fa9e08d2e3167314c3b Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Thu, 11 Jan 2024 11:47:46 +0100 Subject: [PATCH 016/109] remove comment --- internal/controller/ionoscloudmachine_controller.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index 3ad1ff3a..715191b0 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -201,13 +201,6 @@ func (r *IonosCloudMachineReconciler) reconcileNormal(machineScope *scope.Machin return ctrl.Result{}, fmt.Errorf("could not reconcile LAN %w", err) } - //if err != nil { - // return ctrl.Result{}, fmt.Errorf("could not reconcile LAN: %w", err) - //} - //if lan == nil { - // return ctrl.Result{Requeue: true}, nil - //} - return ctrl.Result{}, nil } From f5b6462f8b1c8f2b54f83609fa2233092cb32bba Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Thu, 11 Jan 2024 11:48:36 +0100 Subject: [PATCH 017/109] fix typo --- internal/controller/ionoscloudmachine_controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index 715191b0..fcd41995 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -153,7 +153,7 @@ func (r *IonosCloudMachineReconciler) isInfrastructureReady(machineScope *scope. // Make sure to wait until the data secret was created if machineScope.Machine.Spec.Bootstrap.DataSecretName == nil { - machineScope.Info("Boostrap data secret is not available yet") + machineScope.Info("Bootstrap data secret is not available yet") conditions.MarkFalse( machineScope.IonosCloudMachine, infrav1.MachineProvisionedCondition, From 274c3c4a1d9cd687b84cd0d1b701e98ee753e2bf Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Thu, 11 Jan 2024 12:04:26 +0100 Subject: [PATCH 018/109] adds linter fixes --- internal/controller/ionoscloudmachine_controller.go | 3 ++- scope/machine.go | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index fcd41995..253f9a92 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -20,10 +20,11 @@ import ( "context" "errors" "fmt" + "time" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/service/cloud" + "sigs.k8s.io/cluster-api/util/conditions" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "time" "github.com/go-logr/logr" apierrors "k8s.io/apimachinery/pkg/api/errors" diff --git a/scope/machine.go b/scope/machine.go index dc84fe5c..a465546a 100644 --- a/scope/machine.go +++ b/scope/machine.go @@ -20,6 +20,7 @@ import ( "context" "errors" "fmt" + "k8s.io/client-go/util/retry" "sigs.k8s.io/cluster-api/util/conditions" @@ -91,11 +92,14 @@ func NewMachineScope(params MachineScopeParams) (*MachineScope, error) { }, nil } +// HasFailed Checks if the IonosCloudMachine is in a failed state. func (m *MachineScope) HasFailed() bool { status := m.IonosCloudMachine.Status return status.FailureReason != nil || status.FailureMessage != nil } +// PatchObject will apply all changes from the IonosCloudMachine. +// It will also make sure to patch the status subresource. func (m *MachineScope) PatchObject() error { conditions.SetSummary(m.IonosCloudMachine, conditions.WithConditions( @@ -110,6 +114,9 @@ func (m *MachineScope) PatchObject() error { }}) } +// Finalize will make sure to apply a patch to the current IonosCloudMachine. +// It also implements a retry mechanism to increase the chance of success +// in case the patch operation was not successful. func (m *MachineScope) Finalize() error { // NOTE(lubedacht) retry is only a way to reduce the failure chance, // but in general, the reconciliation logic must be resilient From f4844bd0942bf6deacb8f34d173ec95a997687fd Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Thu, 11 Jan 2024 12:06:44 +0100 Subject: [PATCH 019/109] update go.sum --- go.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.sum b/go.sum index 15b19b7e..b586f8ad 100644 --- a/go.sum +++ b/go.sum @@ -289,8 +289,6 @@ 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/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ= k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= -k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= -k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 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/cluster-api v1.6.0 h1:2bhVSnUbtWI8taCjd9lGiHExsRUpKf7Z1fXqi/IwYx4= From 16f84e6db00698c2e1630ae7e97f51eab97bce1c Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Thu, 11 Jan 2024 12:15:01 +0100 Subject: [PATCH 020/109] additional linter fixes --- internal/controller/ionoscloudmachine_controller.go | 1 + internal/service/cloud/request.go | 1 + 2 files changed, 2 insertions(+) diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index 253f9a92..29815398 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "time" + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/service/cloud" "sigs.k8s.io/cluster-api/util/conditions" diff --git a/internal/service/cloud/request.go b/internal/service/cloud/request.go index fcb7d868..56248f60 100644 --- a/internal/service/cloud/request.go +++ b/internal/service/cloud/request.go @@ -18,6 +18,7 @@ package cloud import ( "fmt" + sdk "github.com/ionos-cloud/sdk-go/v6" ) From 1dd8e334643c28d341e806d0b139866cb4f5eb62 Mon Sep 17 00:00:00 2001 From: Gustavo Alves Date: Thu, 11 Jan 2024 13:28:09 +0100 Subject: [PATCH 021/109] Make the linter happy --- .../ionoscloudmachine_controller.go | 2 +- internal/ionoscloud/client/errors.go | 2 +- internal/service/cloud/network.go | 65 +++++++++---------- 3 files changed, 33 insertions(+), 36 deletions(-) diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index 29815398..1d7f9c8f 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -195,7 +195,7 @@ func (r *IonosCloudMachineReconciler) reconcileNormal(machineScope *scope.Machin // * Queued, Running => Requeue the current request // * Failed => We need to discuss this, log error and continue (retry last request in the corresponding reconcile function) - // Ensure that a lan is created in the datacenter + // Ensure that a LAN is created in the datacenter if requeue, err := cloudService.ReconcileLAN(); err != nil || requeue { if requeue { return ctrl.Result{RequeueAfter: time.Second * 30}, err diff --git a/internal/ionoscloud/client/errors.go b/internal/ionoscloud/client/errors.go index 4e710efd..0eeb1f17 100644 --- a/internal/ionoscloud/client/errors.go +++ b/internal/ionoscloud/client/errors.go @@ -21,7 +21,7 @@ import "errors" var ( errDataCenterIDIsEmpty = errors.New("error parsing data center ID: value cannot be empty") errServerIDIsEmpty = errors.New("error parsing server ID: value cannot be empty") - errLanIDIsEmpty = errors.New("error parsing lan ID: value cannot be empty") + errLanIDIsEmpty = errors.New("error parsing LAN ID: value cannot be empty") errVolumeIDIsEmpty = errors.New("error parsing volume ID: value cannot be empty") errRequestURLIsEmpty = errors.New("a request url is necessary for the operation") ) diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index b4cf0755..4481246d 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -19,14 +19,15 @@ package cloud import ( "errors" "fmt" - infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" - "k8s.io/apimachinery/pkg/util/json" - "k8s.io/utils/ptr" "net/http" "path" "strings" sdk "github.com/ionos-cloud/sdk-go/v6" + "k8s.io/apimachinery/pkg/util/json" + "k8s.io/utils/ptr" + + infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" ) // LANName returns the name of the cluster LAN. @@ -37,22 +38,22 @@ func (s *Service) LANName() string { s.scope.ClusterScope.Cluster.Name) } +// ReconcileLAN ensures the cluster LAN exist, creating one if it doesn't. func (s *Service) ReconcileLAN() (requeue bool, err error) { log := s.scope.Logger.WithName("ReconcileLAN") - // try to retrieve the cluster lan + // try to retrieve the cluster LAN clusterLan, err := s.GetLAN() if clusterLan != nil || err != nil { // If we found the LAN, we don't need to create one. // TODO(lubedacht) check if patching is required => future task. return false, err - } - // if we didn't find a lan, we check if a lan is already in creation + // if we didn't find a LAN, we check if a LAN is already in creation requestStatus, err := s.checkForPendingLANRequest(http.MethodPost, "") if err != nil { - return false, fmt.Errorf("unable to list pending lan requests: %w", err) + return false, fmt.Errorf("unable to list pending LAN requests: %w", err) } // We want to requeue and check again after some time @@ -68,43 +69,42 @@ func (s *Service) ReconcileLAN() (requeue bool, err error) { return false, err } - // If we still don't get a lan here even though we found request, which was done - // the lan was probably deleted before. - // Therefore, we will attempt to create the lan again. + // If we still don't get a LAN here even though we found request, which was done + // the LAN was probably deleted before. + // Therefore, we will attempt to create the LAN again. // // TODO(lubedacht) // Another solution would be to query for a deletion request and check if the created time - // is bigger than the created time of the lan POST request. + // is bigger than the created time of the LAN POST request. } - log.V(4).Info("No lan was found. Creating new lan") - if err := s.CreateLAN(); err != nil { + log.V(4).Info("No LAN was found. Creating new LAN") + if err := s.createLAN(); err != nil { return false, err } - // after creating the lan, we want to requeue and let the request be finished + // after creating the LAN, we want to requeue and let the request be finished return true, nil } -// GetLAN tries to retrieve the cluster related lan in the datacenter. +// GetLAN tries to retrieve the cluster related LAN in the datacenter. func (s *Service) GetLAN() (*sdk.Lan, error) { - // check if the Lan exists + // check if the LAN exists lans, err := s.API().ListLANs(s.ctx, s.DataCenterID()) if err != nil { return nil, fmt.Errorf("could not list lans in datacenter %s: %w", s.DataCenterID(), err) } - var foundLan *sdk.Lan for _, l := range *lans.Items { if name := l.Properties.Name; name != nil && *l.Properties.Name == s.LANName() { - foundLan = &l - break + return &l, nil } } - - return foundLan, nil + return nil, nil } +// ReconcileLANDeletion ensures there's no cluster LAN available, requesting for deletion (if no other resource +// uses it) otherwise. func (s *Service) ReconcileLANDeletion() (requeue bool, err error) { log := s.scope.Logger.WithName("ReconcileLANDeletion") @@ -148,23 +148,22 @@ func (s *Service) ReconcileLANDeletion() (requeue bool, err error) { return false, nil } // Request for LAN destruction - err = s.DeleteLAN(*clusterLAN.Id) + err = s.deleteLAN(*clusterLAN.Id) if err != nil { return false, err } return true, nil } -func (s *Service) CreateLAN() error { +func (s *Service) createLAN() error { log := s.scope.Logger.WithName("CreateLAN") requestPath, err := s.API().CreateLAN(s.ctx, s.DataCenterID(), sdk.LanPropertiesPost{ Name: ptr.To(s.LANName()), Public: ptr.To(true), }) - if err != nil { - return fmt.Errorf("unable to create lan in data center %s: %w", s.DataCenterID(), err) + return fmt.Errorf("unable to create LAN in data center %s: %w", s.DataCenterID(), err) } s.scope.ClusterScope.IonosCluster.Status.PendingRequests[s.DataCenterID()] = &infrav1.ProvisioningRequest{ @@ -183,12 +182,12 @@ func (s *Service) CreateLAN() error { return nil } -func (s *Service) DeleteLAN(lanID string) error { +func (s *Service) deleteLAN(lanID string) error { log := s.scope.Logger.WithName("DeleteLAN") requestPath, err := s.API().DestroyLAN(s.ctx, s.DataCenterID(), lanID) if err != nil { - return fmt.Errorf("unable to request lan deletion in data center: %w", err) + return fmt.Errorf("unable to request LAN deletion in data center: %w", err) } s.scope.ClusterScope.IonosCluster.Status.PendingRequests[s.DataCenterID()] = &infrav1.ProvisioningRequest{ @@ -209,18 +208,16 @@ func (s *Service) DeleteLAN(lanID string) error { // For update and deletion requests, it is also necessary to provide the LAN ID (value will be ignored for creation). func (s *Service) checkForPendingLANRequest(method string, lanID string) (status string, err error) { switch method { + case http.MethodPost: + case http.MethodDelete, http.MethodPatch: + if lanID == "" { + return "", errors.New("lanID cannot be empty for DELETE and PATCH requests") + } default: return "", fmt.Errorf("unsupported method %s, allowed methods are %s", method, strings.Join( []string{http.MethodPost, http.MethodDelete, http.MethodPatch}, ",", )) - case http.MethodDelete, http.MethodPatch: - if lanID == "" { - return "", errors.New("lanID cannot be empty for DELETE and PATCH requests") - } - break - case http.MethodPost: - break } lanPath := path.Join("datacenters", s.DataCenterID(), "lan") From f46c5c320bd59875b73f9a7a5d428bb6fcea06f2 Mon Sep 17 00:00:00 2001 From: Gustavo Alves Date: Thu, 11 Jan 2024 13:33:08 +0100 Subject: [PATCH 022/109] Make the method switch A E S T H E T I C --- internal/service/cloud/network.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index 4481246d..be957747 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -209,6 +209,7 @@ func (s *Service) deleteLAN(lanID string) error { func (s *Service) checkForPendingLANRequest(method string, lanID string) (status string, err error) { switch method { case http.MethodPost: + break case http.MethodDelete, http.MethodPatch: if lanID == "" { return "", errors.New("lanID cannot be empty for DELETE and PATCH requests") From 30b3210e7995ec2082edab60189b523475345a02 Mon Sep 17 00:00:00 2001 From: Gustavo Alves Date: Thu, 11 Jan 2024 13:41:44 +0100 Subject: [PATCH 023/109] Remove break from empty switch case --- internal/service/cloud/network.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index be957747..4481246d 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -209,7 +209,6 @@ func (s *Service) deleteLAN(lanID string) error { func (s *Service) checkForPendingLANRequest(method string, lanID string) (status string, err error) { switch method { case http.MethodPost: - break case http.MethodDelete, http.MethodPatch: if lanID == "" { return "", errors.New("lanID cannot be empty for DELETE and PATCH requests") From d9640bd96067035eeadaa38f710329e8e18a8f71 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Thu, 11 Jan 2024 15:32:41 +0100 Subject: [PATCH 024/109] add revive rule to enforce LAN as acronym --- .gitignore | 3 +++ .golangci.yml | 8 ++++++++ internal/ionoscloud/client/client.go | 8 ++++---- internal/ionoscloud/client/errors.go | 2 +- internal/service/cloud/network.go | 8 ++++---- 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 7f02333d..eea0dccd 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ Dockerfile.cross *.swp *.swo *~ + +# test path for local test programs +/cmd/test/** diff --git a/.golangci.yml b/.golangci.yml index 896af027..3c6baa4e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -34,6 +34,14 @@ linters: - whitespace linters-settings: + revive: + rules: + - name: "var-naming" + arguments: + - [] # first list is a whitelist. All initialisms placed here will be ignored + - ["LAN"] # second list is a black list. These rules will be enforced + severity: error + goimports: local-prefixes: github.com/ionos-cloud/cluster-api-provider-ionoscloud/ ifshort: diff --git a/internal/ionoscloud/client/client.go b/internal/ionoscloud/client/client.go index 08d735df..a810cb34 100644 --- a/internal/ionoscloud/client/client.go +++ b/internal/ionoscloud/client/client.go @@ -172,7 +172,7 @@ func (c *IonosCloudClient) UpdateLAN( return nil, errDataCenterIDIsEmpty } if lanID == "" { - return nil, errLanIDIsEmpty + return nil, errLANIDIsEmpty } l, _, err := c.API.LANsApi.DatacentersLansPatch(ctx, dataCenterID, lanID). Lan(properties).Execute() @@ -189,7 +189,7 @@ func (c *IonosCloudClient) AttachToLAN(ctx context.Context, dataCenterID, lanID return nil, errDataCenterIDIsEmpty } if lanID == "" { - return nil, errLanIDIsEmpty + return nil, errLANIDIsEmpty } n, _, err := c.API.LANsApi.DatacentersLansNicsPost(ctx, dataCenterID, lanID).Nic(nic).Execute() if err != nil { @@ -216,7 +216,7 @@ func (c *IonosCloudClient) GetLAN(ctx context.Context, dataCenterID, lanID strin return nil, errDataCenterIDIsEmpty } if lanID == "" { - return nil, errLanIDIsEmpty + return nil, errLANIDIsEmpty } lan, _, err := c.API.LANsApi.DatacentersLansFindById(ctx, dataCenterID, lanID).Execute() if err != nil { @@ -231,7 +231,7 @@ func (c *IonosCloudClient) DestroyLAN(ctx context.Context, dataCenterID, lanID s return "", errDataCenterIDIsEmpty } if lanID == "" { - return "", errLanIDIsEmpty + return "", errLANIDIsEmpty } req, err := c.API.LANsApi.DatacentersLansDelete(ctx, dataCenterID, lanID).Execute() if err != nil { diff --git a/internal/ionoscloud/client/errors.go b/internal/ionoscloud/client/errors.go index 0eeb1f17..9798ec35 100644 --- a/internal/ionoscloud/client/errors.go +++ b/internal/ionoscloud/client/errors.go @@ -21,7 +21,7 @@ import "errors" var ( errDataCenterIDIsEmpty = errors.New("error parsing data center ID: value cannot be empty") errServerIDIsEmpty = errors.New("error parsing server ID: value cannot be empty") - errLanIDIsEmpty = errors.New("error parsing LAN ID: value cannot be empty") + errLANIDIsEmpty = errors.New("error parsing LAN ID: value cannot be empty") errVolumeIDIsEmpty = errors.New("error parsing volume ID: value cannot be empty") errRequestURLIsEmpty = errors.New("a request url is necessary for the operation") ) diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index 4481246d..d0825b42 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -43,8 +43,8 @@ func (s *Service) ReconcileLAN() (requeue bool, err error) { log := s.scope.Logger.WithName("ReconcileLAN") // try to retrieve the cluster LAN - clusterLan, err := s.GetLAN() - if clusterLan != nil || err != nil { + clusterLAN, err := s.GetLAN() + if clusterLAN != nil || err != nil { // If we found the LAN, we don't need to create one. // TODO(lubedacht) check if patching is required => future task. return false, err @@ -64,8 +64,8 @@ func (s *Service) ReconcileLAN() (requeue bool, err error) { // check again as the request might be done right after we checked // to prevent duplicate creation if requestStatus == sdk.RequestStatusDone { - clusterLan, err = s.GetLAN() - if clusterLan != nil || err != nil { + clusterLAN, err = s.GetLAN() + if clusterLAN != nil || err != nil { return false, err } From a5fef56b2efe4e6b17e8c74349e441ee65c1e446 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Fri, 12 Jan 2024 11:16:11 +0100 Subject: [PATCH 025/109] Updates from PR --- .codespellignore | 3 +- .golangci.yml | 4 +- api/v1alpha1/ionoscloudcluster_types.go | 4 +- api/v1alpha1/ionoscloudmachine_types.go | 66 +++++++++------- api/v1alpha1/ionoscloudmachine_types_test.go | 2 +- api/v1alpha1/types.go | 12 +-- api/v1alpha1/zz_generated.deepcopy.go | 20 ++--- ...e.cluster.x-k8s.io_ionoscloudclusters.yaml | 6 +- ...e.cluster.x-k8s.io_ionoscloudmachines.yaml | 79 +++++++++++-------- .../ionoscloudmachine_controller.go | 4 +- internal/service/cloud/network.go | 6 +- 11 files changed, 111 insertions(+), 95 deletions(-) diff --git a/.codespellignore b/.codespellignore index 202f388b..b281223e 100644 --- a/.codespellignore +++ b/.codespellignore @@ -1,5 +1,4 @@ capi capic decorder -reterr -ionos \ No newline at end of file +ionos diff --git a/.golangci.yml b/.golangci.yml index 3c6baa4e..1fc43331 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -38,8 +38,8 @@ linters-settings: rules: - name: "var-naming" arguments: - - [] # first list is a whitelist. All initialisms placed here will be ignored - - ["LAN"] # second list is a black list. These rules will be enforced + - [] # 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 severity: error goimports: diff --git a/api/v1alpha1/ionoscloudcluster_types.go b/api/v1alpha1/ionoscloudcluster_types.go index 35e6bd49..dfe3a2e6 100644 --- a/api/v1alpha1/ionoscloudcluster_types.go +++ b/api/v1alpha1/ionoscloudcluster_types.go @@ -55,9 +55,9 @@ type IonosCloudClusterStatus struct { // +optional Conditions clusterv1.Conditions `json:"conditions,omitempty"` - // PendingRequests is a map that maps data centers IDs with a pending provisioning request made during reconciliation. + // PendingRequests maps data center IDs to a pending provisioning request made during reconciliation. // +optional - PendingRequests map[string]*ProvisioningRequest `json:"pendingRequests,omitempty"` + PendingRequests map[string]ProvisioningRequest `json:"pendingRequests,omitempty"` } //+kubebuilder:object:root=true diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index 37009e23..1771f9ce 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -33,7 +33,7 @@ const ( MachineFinalizer = "ionoscloudmachine.infrastructure.cluster.x-k8s.io" // MachineProvisionedCondition documents the status of the provisioning of a IonosCloudMachine and - // the underlying enterprise VM. + // the underlying VM. MachineProvisionedCondition clusterv1.ConditionType = "MachineProvisioned" // WaitingForClusterInfrastructureReason (Severity=Info) indicates, that the IonosCloudMachine is currently @@ -51,15 +51,18 @@ type VolumeDiskType string const ( // VolumeDiskTypeHDD defines the disk type HDD. VolumeDiskTypeHDD VolumeDiskType = "HDD" - // VolumeDiskTypeSSD defines the disk type SSD. - VolumeDiskTypeSSD VolumeDiskType = "SSD" + // VolumeDiskTypeSSDStandard defines the standard SSD disk type. + // This is the same as VolumeDiskTypeSSD. + VolumeDiskTypeSSDStandard VolumeDiskType = "SSD Standard" + // VolumeDiskTypeSSDPremium defines the premium SSD disk type. + VolumeDiskTypeSSDPremium VolumeDiskType = "SSD Premium" ) -// AvailabilityZone is the availability zone, where volumes are created. +// AvailabilityZone is the availability zone where volumes are created in. type AvailabilityZone string const ( - // AvailabilityZoneAuto selected an automatic availability zone. + // AvailabilityZoneAuto automatically selects an availability zone. AvailabilityZoneAuto AvailabilityZone = "AUTO" // AvailabilityZoneOne zone 1. AvailabilityZoneOne AvailabilityZone = "ZONE_1" @@ -76,40 +79,44 @@ type IonosCloudMachineSpec struct { // +optional ProviderID string `json:"providerId,omitempty"` - // DatacenterID is the ID of the datacenter, where the machine should be created. - // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="DatacenterID is immutable" - DatacenterID string `json:"datacenterID"` + // DatacenterID is the ID of the datacenter where the machine should be created in. + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="datacenterID is immutable" + DatacenterID string `json:"datacenterId"` - // ServerID is the unique identifier for a server in the IONOS Cloud context. - // The value will be set, once the server was created. + // ServerID is the ID for a VM in the IONOS Cloud context. + // The value will be set once the server was created. // +optional - ServerID string `json:"serverID,omitempty"` + ServerID string `json:"serverId,omitempty"` - // Cores defines the total number of cores for the enterprise server. + // NumCores defines the number of cores for the VM. // +kubebuilder:validation:Minimum=1 // +kubebuilder:default=1 - // +optional - NumCores int32 `json:"numCores,omitempty"` + NumCores int32 `json:"numCores"` + + // AvailabilityZone is the availability zone in which the VM should be provisioned. + // +kubebuilder:validation:Enum=AUTO;ZONE_1;ZONE_2;ZONE_3 + // +kubebuilder:default=AUTO + AvailabilityZone AvailabilityZone `json:"availabilityZone"` - // MemoryMiB is the memory size for the enterprise server in MB. + // MemoryMb is the memory size for the VM in MB. // Size must be specified in multiples of 256 MB with a minimum of 1024 MB // which is required as we are using hot-pluggable RAM by default. // +kubebuilder:validation:MultipleOf=256 // +kubebuilder:validation:Minimum=1024 // +kubebuilder:default=1024 // +optional - MemoryMiB int32 `json:"memoryMiB,omitempty"` + MemoryMb int32 `json:"memoryMb,omitempty"` - // CPUFamily defines the CPU architecture, which will be used for this enterprise server. + // CPUFamily defines the CPU architecture, which will be used for this VM. // The not all CPU architectures are available in all datacenters. // +kubebuilder:example=AMD_OPTERON CPUFamily string `json:"cpuFamily"` - // Disk defines the boot volume of the machine. + // Disk defines the boot volume of the VM. // +optional Disk *Volume `json:"disk,omitempty"` - // Network defines the network configuration for the enterprise server. + // Network defines the network configuration for the VM. Network *Network `json:"network,omitempty"` } @@ -121,7 +128,7 @@ type Network struct { // +optional IPs []string `json:"ips,omitempty"` - // UseDHCP sets whether dhcp should be used or not. + // UseDHCP sets whether DHCP should be used or not. // NOTE(lubedacht) currently we do not support private clusters // therefore dhcp must be set to true. // +kubebuilder:default=true @@ -138,22 +145,21 @@ type Volume struct { // DiskType defines the type of the hard drive. // +kubebuilder:validation:Enum=HDD;SSD // +kubebuilder:default=HDD - // +optional - DiskType VolumeDiskType `json:"diskType,omitempty"` + DiskType VolumeDiskType `json:"diskType"` // SizeGB defines the size of the volume in GB // +kubebuilder:validation:Minimum=5 SizeGB int `json:"sizeGB"` - // AvailabilityZone is the availabilityZone where the volume will be created. + // AvailabilityZone is the availability zone where the volume will be created. + // +kubebuilder:validation:Enum=AUTO;ZONE_1;ZONE_2;ZONE_3 // +kubebuilder:default=AUTO - // +optional - AvailabilityZone string `json:"availabilityZone,omitempty"` + AvailabilityZone AvailabilityZone `json:"availabilityZone"` - // SSHKeys contains a set of public ssh keys which will be added to the + // SSHKeys contains a set of public SSH keys which will be added to the // list of authorized keys. // +listType=set - SSHKeys []string `json:"SSHKeys,omitempty"` + SSHKeys []string `json:"sshKeys,omitempty"` } // IonosCloudMachineStatus defines the observed state of IonosCloudMachine. @@ -205,7 +211,7 @@ type IonosCloudMachineStatus struct { Conditions clusterv1.Conditions `json:"conditions,omitempty"` // CurrentRequest shows the current provisioning request for any - // cloud resource, that is being provisioned. + // cloud resource that is being provisioned. CurrentRequest *ProvisioningRequest `json:"currentRequest,omitempty"` } @@ -230,12 +236,12 @@ type IonosCloudMachineList struct { Items []IonosCloudMachine `json:"items"` } -// GetConditions returns the observations of the operational state of the ProxmoxMachine resource. +// GetConditions returns the observations of the operational state of the IonosCloudMachine resource. func (m *IonosCloudMachine) GetConditions() clusterv1.Conditions { return m.Status.Conditions } -// SetConditions sets the underlying service state of the ProxmoxMachine to the predescribed clusterv1.Conditions. +// SetConditions sets the underlying service state of the IonosCloudMachine to the predescribed clusterv1.Conditions. func (m *IonosCloudMachine) SetConditions(conditions clusterv1.Conditions) { m.Status.Conditions = conditions } diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index dcba1ad6..789d0ddf 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -65,7 +65,7 @@ var _ = Describe("IonosCloudMachine Tests", func() { spec := m.Spec Expect(spec.NumCores).To(Equal(int32(1))) - Expect(spec.MemoryMiB).To(Equal(int32(1024))) + Expect(spec.MemoryMb).To(Equal(int32(1024))) Expect(spec.Disk.DiskType).To(Equal(VolumeDiskTypeHDD)) Expect(spec.Disk.AvailabilityZone).To(Equal("AUTO")) diff --git a/api/v1alpha1/types.go b/api/v1alpha1/types.go index 4f637895..01355608 100644 --- a/api/v1alpha1/types.go +++ b/api/v1alpha1/types.go @@ -20,16 +20,16 @@ package v1alpha1 type RequestStatus string const ( - // RequestStatusQueued indicates, that the request is queued and not yet being processed. + // RequestStatusQueued indicates that the request is queued and not yet being processed. RequestStatusQueued RequestStatus = "QUEUED" - // RequestStatusRunning indicates, that the request is currently being processed. + // RequestStatusRunning indicates that the request is currently being processed. RequestStatusRunning RequestStatus = "RUNNING" - // RequestStatusDone indicates, that the request has been successfully processed. + // RequestStatusDone indicates that the request has been successfully processed. RequestStatusDone RequestStatus = "DONE" - // RequestStatusFailed indicates, that the request has failed. + // RequestStatusFailed indicates that the request has failed. RequestStatusFailed RequestStatus = "FAILED" ) @@ -45,9 +45,9 @@ type ProvisioningRequest struct { // RequestStatus is the status of the request in the queue. // +kubebuilder:validation:Enum=QUEUED;RUNNING;DONE;FAILED // +optional - State RequestStatus `json:"state"` + State RequestStatus `json:"state,omitempty"` // Message is the request message, which can also contain error information. // +optional - Message string `json:"failureMessage,omitempty"` + Message *string `json:"message,omitempty"` } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index cde3fa4a..c9d294bf 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -113,18 +113,9 @@ func (in *IonosCloudClusterStatus) DeepCopyInto(out *IonosCloudClusterStatus) { } if in.PendingRequests != nil { in, out := &in.PendingRequests, &out.PendingRequests - *out = make(map[string]*ProvisioningRequest, len(*in)) + *out = make(map[string]ProvisioningRequest, len(*in)) for key, val := range *in { - var outVal *ProvisioningRequest - if val == nil { - (*out)[key] = nil - } else { - inVal := (*in)[key] - in, out := &inVal, &outVal - *out = new(ProvisioningRequest) - **out = **in - } - (*out)[key] = outVal + (*out)[key] = *val.DeepCopy() } } } @@ -246,7 +237,7 @@ func (in *IonosCloudMachineStatus) DeepCopyInto(out *IonosCloudMachineStatus) { if in.CurrentRequest != nil { in, out := &in.CurrentRequest, &out.CurrentRequest *out = new(ProvisioningRequest) - **out = **in + (*in).DeepCopyInto(*out) } } @@ -288,6 +279,11 @@ func (in *Network) DeepCopy() *Network { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ProvisioningRequest) DeepCopyInto(out *ProvisioningRequest) { *out = *in + if in.Message != nil { + in, out := &in.Message, &out.Message + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProvisioningRequest. 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 acf0f60e..a0876eed 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml @@ -125,7 +125,7 @@ spec: description: ProvisioningRequest is a definition of a provisioning request in the IONOS Cloud. properties: - failureMessage: + message: description: Message is the request message, which can also contain error information. type: string @@ -148,8 +148,8 @@ spec: - method - requestPath type: object - description: PendingRequests is a map that maps data centers IDs with - a pending provisioning request made during reconciliation. + description: PendingRequests maps data center IDs to a pending provisioning + request made during reconciliation. type: object ready: default: false 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 9f87b79e..d4123247 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml @@ -34,33 +34,41 @@ spec: spec: description: IonosCloudMachineSpec defines the desired state of IonosCloudMachine. properties: + availabilityZone: + default: AUTO + description: AvailabilityZone is the availability zone in which the + VM should be provisioned. + enum: + - AUTO + - ZONE_1 + - ZONE_2 + - ZONE_3 + type: string cpuFamily: description: CPUFamily defines the CPU architecture, which will be - used for this enterprise server. The not all CPU architectures are - available in all datacenters. + used for this VM. The not all CPU architectures are available in + all datacenters. example: AMD_OPTERON type: string - datacenterID: - description: DatacenterID is the ID of the datacenter, where the machine - should be created. + datacenterId: + description: DatacenterID is the ID of the datacenter where the machine + should be created in. type: string x-kubernetes-validations: - - message: DatacenterID is immutable + - message: datacenterID is immutable rule: self == oldSelf disk: - description: Disk defines the boot volume of the machine. + description: Disk defines the boot volume of the VM. properties: - SSHKeys: - description: SSHKeys contains a set of public ssh keys which will - be added to the list of authorized keys. - items: - type: string - type: array - x-kubernetes-list-type: set availabilityZone: default: AUTO - description: AvailabilityZone is the availabilityZone where the + description: AvailabilityZone is the availability zone where the volume will be created. + enum: + - AUTO + - ZONE_1 + - ZONE_2 + - ZONE_3 type: string diskType: default: HDD @@ -76,22 +84,29 @@ spec: description: SizeGB defines the size of the volume in GB minimum: 5 type: integer + sshKeys: + description: SSHKeys contains a set of public SSH keys which will + be added to the list of authorized keys. + items: + type: string + type: array + x-kubernetes-list-type: set required: + - availabilityZone + - diskType - sizeGB type: object - memoryMiB: + memoryMb: default: 1024 - description: MemoryMiB is the memory size for the enterprise server - in MB. Size must be specified in multiples of 256 MB with a minimum - of 1024 MB which is required as we are using hot-pluggable RAM by - default. + description: MemoryMb is the memory size for the VM in MB. Size must + be specified in multiples of 256 MB with a minimum of 1024 MB which + is required as we are using hot-pluggable RAM by default. format: int32 minimum: 1024 multipleOf: 256 type: integer network: - description: Network defines the network configuration for the enterprise - server. + description: Network defines the network configuration for the VM. properties: ips: description: IPs is an optional set of IP addresses, which have @@ -102,15 +117,14 @@ spec: x-kubernetes-list-type: set useDhcp: default: true - description: UseDHCP sets whether dhcp should be used or not. + description: UseDHCP sets whether DHCP should be used or not. NOTE(lubedacht) currently we do not support private clusters therefore dhcp must be set to true. type: boolean type: object numCores: default: 1 - description: Cores defines the total number of cores for the enterprise - server. + description: NumCores defines the number of cores for the VM. format: int32 minimum: 1 type: integer @@ -118,14 +132,15 @@ spec: description: ProviderID is the IONOS Cloud provider ID will be in the format ionos://ee090ff2-1eef-48ec-a246-a51a33aa4f3a type: string - serverID: - description: ServerID is the unique identifier for a server in the - IONOS Cloud context. The value will be set, once the server was - created. + serverId: + description: ServerID is the ID for a VM in the IONOS Cloud context. + The value will be set once the server was created. type: string required: + - availabilityZone - cpuFamily - - datacenterID + - datacenterId + - numCores type: object status: description: IonosCloudMachineStatus defines the observed state of IonosCloudMachine. @@ -177,9 +192,9 @@ spec: type: array currentRequest: description: CurrentRequest shows the current provisioning request - for any cloud resource, that is being provisioned. + for any cloud resource that is being provisioned. properties: - failureMessage: + message: description: Message is the request message, which can also contain error information. type: string diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index 1d7f9c8f..e3eecfae 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -63,7 +63,7 @@ type IonosCloudMachineReconciler struct { // // 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, req ctrl.Request) (_ ctrl.Result, reterr error) { +func (r *IonosCloudMachineReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, retErr error) { logger := ctrl.LoggerFrom(ctx) ionosCloudMachine := &infrav1.IonosCloudMachine{} @@ -125,7 +125,7 @@ func (r *IonosCloudMachineReconciler) Reconcile(ctx context.Context, req ctrl.Re defer func() { if err := machineScope.Finalize(); err != nil { - reterr = errors.Join(err, reterr) + retErr = errors.Join(err, retErr) } }() diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index d0825b42..182a2efe 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -166,7 +166,7 @@ func (s *Service) createLAN() error { return fmt.Errorf("unable to create LAN in data center %s: %w", s.DataCenterID(), err) } - s.scope.ClusterScope.IonosCluster.Status.PendingRequests[s.DataCenterID()] = &infrav1.ProvisioningRequest{ + s.scope.ClusterScope.IonosCluster.Status.PendingRequests[s.DataCenterID()] = infrav1.ProvisioningRequest{ Method: http.MethodPost, RequestPath: requestPath, State: infrav1.RequestStatusQueued, @@ -190,7 +190,7 @@ func (s *Service) deleteLAN(lanID string) error { return fmt.Errorf("unable to request LAN deletion in data center: %w", err) } - s.scope.ClusterScope.IonosCluster.Status.PendingRequests[s.DataCenterID()] = &infrav1.ProvisioningRequest{ + s.scope.ClusterScope.IonosCluster.Status.PendingRequests[s.DataCenterID()] = infrav1.ProvisioningRequest{ Method: http.MethodDelete, RequestPath: requestPath, State: infrav1.RequestStatusQueued, @@ -258,7 +258,7 @@ func (s *Service) checkForPendingLANRequest(method string, lanID string) (status } func (s *Service) removeLANPendingRequestFromCluster() error { - s.scope.ClusterScope.IonosCluster.Status.PendingRequests[s.DataCenterID()] = nil + delete(s.scope.ClusterScope.IonosCluster.Status.PendingRequests, s.DataCenterID()) if err := s.scope.ClusterScope.PatchObject(); err != nil { return fmt.Errorf("could not remove stale LAN pending request from cluster: %w", err) } From 3f1d0565276a22e91b31b976bd7eb0eb34a0846a Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Fri, 12 Jan 2024 11:21:19 +0100 Subject: [PATCH 026/109] make boot disk required --- api/v1alpha1/ionoscloudmachine_types.go | 3 +-- api/v1alpha1/ionoscloudmachine_types_test.go | 4 ++-- api/v1alpha1/zz_generated.deepcopy.go | 6 +----- .../infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml | 1 + 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index 1771f9ce..36296cde 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -113,8 +113,7 @@ type IonosCloudMachineSpec struct { CPUFamily string `json:"cpuFamily"` // Disk defines the boot volume of the VM. - // +optional - Disk *Volume `json:"disk,omitempty"` + Disk Volume `json:"disk"` // Network defines the network configuration for the VM. Network *Network `json:"network,omitempty"` diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index 789d0ddf..044c276d 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -55,7 +55,7 @@ var _ = Describe("IonosCloudMachine Tests", func() { Context("Validation", func() { It("Should set defaults for Volumes", func() { m := defaultMachine() - m.Spec.Disk = &Volume{ + m.Spec.Disk = Volume{ Name: "test-volume", SizeGB: 5, } @@ -88,7 +88,7 @@ var _ = Describe("IonosCloudMachine Tests", func() { It("Should fail if size is less than 5", func() { m := defaultMachine() - m.Spec.Disk = &Volume{ + m.Spec.Disk = Volume{ Name: "test-volume", SizeGB: 4, } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index c9d294bf..005ff1b2 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -192,11 +192,7 @@ 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.Disk != nil { - in, out := &in.Disk, &out.Disk - *out = new(Volume) - (*in).DeepCopyInto(*out) - } + in.Disk.DeepCopyInto(&out.Disk) if in.Network != nil { in, out := &in.Network, &out.Network *out = new(Network) 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 d4123247..d0903d60 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml @@ -140,6 +140,7 @@ spec: - availabilityZone - cpuFamily - datacenterId + - disk - numCores type: object status: From 01cd5ca069a20ab6612b2567d64a7e139de91afb Mon Sep 17 00:00:00 2001 From: Gustavo Alves Date: Fri, 12 Jan 2024 16:56:17 +0100 Subject: [PATCH 027/109] tests, tests and more tests for MachineController types --- .codespellignore | 2 +- api/v1alpha1/ionoscloudcluster_types.go | 4 +- api/v1alpha1/ionoscloudmachine_types.go | 46 ++- api/v1alpha1/ionoscloudmachine_types_test.go | 343 +++++++++++++++--- api/v1alpha1/suite_test.go | 3 + api/v1alpha1/zz_generated.deepcopy.go | 4 +- ...e.cluster.x-k8s.io_ionoscloudclusters.yaml | 4 +- ...e.cluster.x-k8s.io_ionoscloudmachines.yaml | 25 +- internal/ionoscloud/clienttest/mock_client.go | 196 +++++++++- internal/service/cloud/datacenter.go | 2 +- 10 files changed, 516 insertions(+), 113 deletions(-) diff --git a/.codespellignore b/.codespellignore index b281223e..4a82a4bd 100644 --- a/.codespellignore +++ b/.codespellignore @@ -1,4 +1,4 @@ capi capic decorder -ionos +IONOS diff --git a/api/v1alpha1/ionoscloudcluster_types.go b/api/v1alpha1/ionoscloudcluster_types.go index dfe3a2e6..ae4887d3 100644 --- a/api/v1alpha1/ionoscloudcluster_types.go +++ b/api/v1alpha1/ionoscloudcluster_types.go @@ -55,9 +55,9 @@ type IonosCloudClusterStatus struct { // +optional Conditions clusterv1.Conditions `json:"conditions,omitempty"` - // PendingRequests maps data center IDs to a pending provisioning request made during reconciliation. + // CurrentRequest maps data center IDs to a pending provisioning request made during reconciliation. // +optional - PendingRequests map[string]ProvisioningRequest `json:"pendingRequests,omitempty"` + CurrentRequest map[string]ProvisioningRequest `json:"currentRequest,omitempty"` } //+kubebuilder:object:root=true diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index 36296cde..80048d47 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -79,33 +79,32 @@ type IonosCloudMachineSpec struct { // +optional ProviderID string `json:"providerId,omitempty"` - // DatacenterID is the ID of the datacenter where the machine should be created in. - // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="datacenterID is immutable" - DatacenterID string `json:"datacenterId"` - - // ServerID is the ID for a VM in the IONOS Cloud context. - // The value will be set once the server was created. - // +optional - ServerID string `json:"serverId,omitempty"` + // DataCenterID is the ID of the datacenter where the machine should be created in. + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="datacenterId is immutable" + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Format=uuid + DataCenterID string `json:"datacenterId"` // NumCores defines the number of cores for the VM. // +kubebuilder:validation:Minimum=1 // +kubebuilder:default=1 - NumCores int32 `json:"numCores"` + // +optional + NumCores int32 `json:"numCores,omitempty"` // AvailabilityZone is the availability zone in which the VM should be provisioned. // +kubebuilder:validation:Enum=AUTO;ZONE_1;ZONE_2;ZONE_3 // +kubebuilder:default=AUTO - AvailabilityZone AvailabilityZone `json:"availabilityZone"` + // +optional + AvailabilityZone AvailabilityZone `json:"availabilityZone,omitempty"` - // MemoryMb is the memory size for the VM in MB. + // MemoryMB is the memory size for the VM in MB. // Size must be specified in multiples of 256 MB with a minimum of 1024 MB // which is required as we are using hot-pluggable RAM by default. - // +kubebuilder:validation:MultipleOf=256 - // +kubebuilder:validation:Minimum=1024 - // +kubebuilder:default=1024 + // +kubebuilder:validation:MultipleOf=1024 + // +kubebuilder:validation:Minimum=2048 + // +kubebuilder:default=3072 // +optional - MemoryMb int32 `json:"memoryMb,omitempty"` + MemoryMB int32 `json:"memoryMb,omitempty"` // CPUFamily defines the CPU architecture, which will be used for this VM. // The not all CPU architectures are available in all datacenters. @@ -116,6 +115,7 @@ type IonosCloudMachineSpec struct { Disk Volume `json:"disk"` // Network defines the network configuration for the VM. + // +optional Network *Network `json:"network,omitempty"` } @@ -132,7 +132,7 @@ type Network struct { // therefore dhcp must be set to true. // +kubebuilder:default=true // +optional - UseDHCP *bool `json:"useDhcp"` + UseDHCP *bool `json:"useDhcp,omitempty"` } // Volume is the physical storage on the machine. @@ -142,22 +142,27 @@ type Volume struct { Name string `json:"name,omitempty"` // DiskType defines the type of the hard drive. - // +kubebuilder:validation:Enum=HDD;SSD + // +kubebuilder:validation:Enum=HDD;SSD Standard;SSD Premium // +kubebuilder:default=HDD - DiskType VolumeDiskType `json:"diskType"` + // +optional + DiskType VolumeDiskType `json:"diskType,omitempty"` // SizeGB defines the size of the volume in GB // +kubebuilder:validation:Minimum=5 - SizeGB int `json:"sizeGB"` + // +kubebuilder:default=5 + // +optional + SizeGB int `json:"sizeGB,omitempty"` // AvailabilityZone is the availability zone where the volume will be created. // +kubebuilder:validation:Enum=AUTO;ZONE_1;ZONE_2;ZONE_3 // +kubebuilder:default=AUTO - AvailabilityZone AvailabilityZone `json:"availabilityZone"` + // +optional + AvailabilityZone AvailabilityZone `json:"availabilityZone,omitempty"` // SSHKeys contains a set of public SSH keys which will be added to the // list of authorized keys. // +listType=set + // +optional SSHKeys []string `json:"sshKeys,omitempty"` } @@ -211,6 +216,7 @@ type IonosCloudMachineStatus struct { // CurrentRequest shows the current provisioning request for any // cloud resource that is being provisioned. + // +optional CurrentRequest *ProvisioningRequest `json:"currentRequest,omitempty"` } diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index 044c276d..2b9723bf 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -18,10 +18,11 @@ package v1alpha1 import ( "context" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + "net/http" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -33,82 +34,318 @@ func defaultMachine() *IonosCloudMachine { }, Spec: IonosCloudMachineSpec{ ProviderID: "ionos://ee090ff2-1eef-48ec-a246-a51a33aa4f3a", - ServerID: "11432411-cf83-4f62-b50f-798060493ea9", Network: &Network{}, }, Status: IonosCloudMachineStatus{}, } } +func completeMachine() *IonosCloudMachine { + return &IonosCloudMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-machine", + Namespace: metav1.NamespaceDefault, + }, + Spec: IonosCloudMachineSpec{ + ProviderID: "ionos://ee090ff2-1eef-48ec-a246-a51a33aa4f3a", + DataCenterID: "ee090ff2-1eef-48ec-a246-a51a33aa4f3a", + NumCores: 1, + AvailabilityZone: AvailabilityZoneTwo, + MemoryMB: 2048, + CPUFamily: "AMD_OPTERON", + Disk: Volume{ + Name: "disk", + DiskType: VolumeDiskTypeSSDStandard, + SizeGB: 23, + AvailabilityZone: AvailabilityZoneThree, + SSHKeys: []string{"public-key"}, + }, + Network: &Network{ + IPs: []string{"1.2.3.4"}, + UseDHCP: ptr.To(true), + }, + }, + } +} + +func completeStatus() *IonosCloudMachineStatus { + return &IonosCloudMachineStatus{ + Ready: false, + CurrentRequest: &ProvisioningRequest{ + Method: http.MethodPost, + RequestPath: "requestPath", + State: RequestStatusRunning, + Message: ptr.To("requestMessage"), + }, + } +} + var _ = Describe("IonosCloudMachine Tests", func() { AfterEach(func() { err := k8sClient.Delete(context.Background(), defaultMachine()) Expect(client.IgnoreNotFound(err)).To(Succeed()) }) - Context("Create", func() { - It("Should allow creation of valid machines", func() { - Expect(k8sClient.Create(context.Background(), defaultMachine())).To(Succeed()) - }) - }) - Context("Validation", func() { - It("Should set defaults for Volumes", func() { - m := defaultMachine() - m.Spec.Disk = Volume{ - Name: "test-volume", - SizeGB: 5, - } - + It("shouldn't fail if everything is seems to be properly set", func() { + m := completeMachine() Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(k8sClient.Get(context.Background(), client.ObjectKeyFromObject(m), m)).To(Succeed()) - - spec := m.Spec - Expect(spec.NumCores).To(Equal(int32(1))) - Expect(spec.MemoryMb).To(Equal(int32(1024))) - - Expect(spec.Disk.DiskType).To(Equal(VolumeDiskTypeHDD)) - Expect(spec.Disk.AvailabilityZone).To(Equal("AUTO")) - Expect(spec.Disk.SizeGB).To(Equal(5)) - - Expect(spec.Network.UseDHCP).ToNot(BeNil()) - Expect(deref(spec.Network.UseDHCP, false)).To(BeTrue()) }) - It("Should fail if datacenterID would be updated", func() { - m := defaultMachine() - m.Spec.DatacenterID = "test" - + It("should not fail if providerId is empty", func() { + m := completeMachine() + want := "" + m.Spec.ProviderID = want Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(k8sClient.Get(context.Background(), client.ObjectKeyFromObject(m), m)).To(Succeed()) + Expect(m.Spec.ProviderID).To(Equal(want)) + }) + + When("data center id", func() { + It("it should fail if dataCenterId is not a uuid", func() { + m := completeMachine() + want := "" + m.Spec.DataCenterID = want + Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) + Expect(m.Spec.DataCenterID).To(Equal(want)) + }) + It("should be immutable", func() { + m := completeMachine() + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(m.Spec.DataCenterID).To(Equal(completeMachine().Spec.DataCenterID)) + m.Spec.DataCenterID = "6ded8c5f-8df2-46ef-b4ce-61833daf0961" + Expect(k8sClient.Update(context.Background(), m)).ToNot(Succeed()) + }) + }) - m.Spec.DatacenterID = "changed" - Expect(k8sClient.Update(context.Background(), m)).To(HaveOccurred()) + When("the number of cores, ", func() { + It("is less than 1, it should fail", func() { + m := completeMachine() + m.Spec.NumCores = -1 + Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) + }) + It("should have a minimum value of 1", func() { + m := completeMachine() + want := int32(1) + m.Spec.NumCores = want + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(m.Spec.NumCores).To(Equal(want)) + }) + It("isn't set, it should work and default to 1", func() { + m := completeMachine() + // because NumCores is int32, setting the value as 0 is the same as not setting anything + m.Spec.NumCores = 0 + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(m.Spec.NumCores).To(Equal(int32(1))) + }) }) - It("Should fail if size is less than 5", func() { - m := defaultMachine() - m.Spec.Disk = Volume{ - Name: "test-volume", - SizeGB: 4, - } + When("the number of cores, ", func() { + It("is less than 1, it should fail", func() { + m := completeMachine() + m.Spec.NumCores = -1 + Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) + }) + It("should have a minimum value of 1", func() { + m := completeMachine() + want := int32(1) + m.Spec.NumCores = want + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(m.Spec.NumCores).To(Equal(want)) + }) + It("isn't set, it should work and default to 1", func() { + m := completeMachine() + m.Spec.NumCores = 0 + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(m.Spec.NumCores).To(Equal(int32(1))) + }) + }) - Expect(k8sClient.Create(context.Background(), m)).To(MatchError(ContainSubstring("should be greater than or equal to 5"))) + When("the machine availability zone", func() { + It("isn't set, should default to AUTO", func() { + m := completeMachine() + // because AvailabilityZone is a string, setting the value as "" is the same as not setting anything + m.Spec.AvailabilityZone = "" + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(m.Spec.AvailabilityZone).To(Equal(AvailabilityZoneAuto)) + }) + It("it not part of the enum it should not work", func() { + m := completeMachine() + m.Spec.AvailabilityZone = "this-should-not-work" + Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) + }) + DescribeTable("should work for these values", + func(zone AvailabilityZone) { + m := completeMachine() + m.Spec.AvailabilityZone = zone + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(m.Spec.AvailabilityZone).To(Equal(zone)) + }, + Entry("AUTO", AvailabilityZoneAuto), + Entry("ZONE_1", AvailabilityZoneOne), + Entry("ZONE_2", AvailabilityZoneTwo), + Entry("ZONE_3", AvailabilityZoneThree), + ) }) - It("Should fail when providing duplicate ip address", func() { - m := defaultMachine() + When("the machine memory size", func() { + It("isn't set, should default to 3072MB", func() { + m := completeMachine() + // because MemoryMB is an int32, setting the value as 0 is the same as not setting anything + m.Spec.MemoryMB = 0 + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(m.Spec.MemoryMB).To(Equal(int32(3072))) + }) + It("should be at least 2048, therefore less than it should not work", func() { + m := completeMachine() + m.Spec.MemoryMB = 1024 + Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) + }) + It("should be at least 2048, therefore 2048 should work", func() { + m := completeMachine() + want := int32(2048) + m.Spec.MemoryMB = want + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(m.Spec.MemoryMB).To(Equal(want)) + }) + It("it should be a multiple of 1024", func() { + m := completeMachine() + m.Spec.MemoryMB = 2100 + Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) + }) + It("should be at least 2048 and a multiple of 1024, therefore 4096 should work", func() { + m := completeMachine() + want := int32(4096) + m.Spec.MemoryMB = want + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(m.Spec.MemoryMB).To(Equal(want)) + }) + }) - m.Spec.Network.IPs = append(m.Spec.Network.IPs, "192.0.2.0", "192.0.2.1", "192.0.2.1") - Expect(k8sClient.Create(context.Background(), m)).To(MatchError(ContainSubstring("Duplicate value: \"192.0.2.1\""))) + Context("Volume", func() { + It("can have an optional name", func() { + m := completeMachine() + want := "" + m.Spec.Disk.Name = want + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(m.Spec.Disk.Name).To(Equal(want)) + }) + When("the disk availability zone", func() { + It("isn't set, should default to AUTO", func() { + m := completeMachine() + // because AvailabilityZone is a string, setting the value as "" is the same as not setting anything + m.Spec.Disk.AvailabilityZone = "" + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(m.Spec.Disk.AvailabilityZone).To(Equal(AvailabilityZoneAuto)) + }) + It("is not part of the enum it should not work", func() { + m := completeMachine() + m.Spec.Disk.AvailabilityZone = "this-should-not-work" + Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) + }) + DescribeTable("should work for these values", + func(zone AvailabilityZone) { + m := completeMachine() + m.Spec.Disk.AvailabilityZone = zone + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(m.Spec.Disk.AvailabilityZone).To(Equal(zone)) + }, + Entry("AUTO", AvailabilityZoneAuto), + Entry("ZONE_1", AvailabilityZoneOne), + Entry("ZONE_2", AvailabilityZoneTwo), + Entry("ZONE_3", AvailabilityZoneThree), + ) + }) + It("can be created without ssh keys", func() { + m := completeMachine() + var want []string + m.Spec.Disk.SSHKeys = want + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(m.Spec.Disk.SSHKeys).To(Equal(want)) + }) + When("the disk size (in GB)", func() { + It("is less than 5, it should fail", func() { + m := completeMachine() + m.Spec.Disk.SizeGB = 4 + Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) + }) + It("it is not set, it should default to 5", func() { + m := completeMachine() + // Because disk size is an int, setting it as 0 is the same as not setting anything + m.Spec.Disk.SizeGB = 0 + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(m.Spec.Disk.SizeGB).To(Equal(5)) + }) + It("It should be at least 5; therefore 5 should work", func() { + m := completeMachine() + want := 5 + m.Spec.Disk.SizeGB = want + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(m.Spec.Disk.SizeGB).To(Equal(want)) + }) + It("It should be at least 5; therefore 6 should work", func() { + m := completeMachine() + want := 6 + m.Spec.Disk.SizeGB = want + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(m.Spec.Disk.SizeGB).To(Equal(want)) + }) + }) + When("the disk type", func() { + It("isn't set, should default to HDD", func() { + m := completeMachine() + // because DiskType is a string, setting the value as "" is the same as not setting anything + m.Spec.Disk.DiskType = "" + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(m.Spec.Disk.DiskType).To(Equal(VolumeDiskTypeHDD)) + }) + It("is not part of the enum it should not work", func() { + m := completeMachine() + m.Spec.Disk.AvailabilityZone = "tape" + Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) + }) + DescribeTable("should work for these values", + func(diskType VolumeDiskType) { + m := completeMachine() + m.Spec.Disk.DiskType = diskType + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(m.Spec.Disk.DiskType).To(Equal(diskType)) + }, + Entry("HDD", VolumeDiskTypeHDD), + Entry("SSD Standard", VolumeDiskTypeSSDStandard), + Entry("SSD Premium", VolumeDiskTypeSSDPremium), + ) + }) + }) + Context("Network", func() { + It("network config should be optional", func() { + m := completeMachine() + m.Spec.Network = nil + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(m.Spec.Network).To(BeNil()) + }) + It("If UseDHCP is not set, it should default to true", func() { + m := completeMachine() + m.Spec.Network.UseDHCP = nil + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(m.Spec.Network.UseDHCP).ToNot(BeNil()) + Expect(*m.Spec.Network.UseDHCP).To(BeTrue()) + }) + DescribeTable("if set UseDHCP can be", + func(useDHCP *bool) { + m := completeMachine() + m.Spec.Network.UseDHCP = useDHCP + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(m.Spec.Network.UseDHCP).To(Equal(useDHCP)) + }, + Entry("true", ptr.To(true)), + Entry("false", ptr.To(false)), + ) + It("reserved ips should be optional", func() { + m := completeMachine() + m.Spec.Network.IPs = nil + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(m.Spec.Network.IPs).To(BeNil()) + }) }) }) }) - -func deref[T any](ptr *T, def T) T { - if ptr != nil { - return *ptr - } - - return def -} diff --git a/api/v1alpha1/suite_test.go b/api/v1alpha1/suite_test.go index efc2b39e..c12d4014 100644 --- a/api/v1alpha1/suite_test.go +++ b/api/v1alpha1/suite_test.go @@ -47,6 +47,9 @@ var _ = BeforeSuite(func() { CRDDirectoryPaths: []string{ filepath.Join("..", "..", "config", "crd", "bases"), }, + // NOTE(gfariasalves): To be removed after I finish the PR comments + //BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", "1.28.0-linux-amd64"), + ErrorIfCRDPathMissing: true, } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 005ff1b2..f786b125 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -111,8 +111,8 @@ func (in *IonosCloudClusterStatus) DeepCopyInto(out *IonosCloudClusterStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } - if in.PendingRequests != nil { - in, out := &in.PendingRequests, &out.PendingRequests + if in.CurrentRequest != nil { + in, out := &in.CurrentRequest, &out.CurrentRequest *out = make(map[string]ProvisioningRequest, len(*in)) for key, val := range *in { (*out)[key] = *val.DeepCopy() 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 a0876eed..6dae51f2 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml @@ -120,7 +120,7 @@ spec: - type type: object type: array - pendingRequests: + currentRequest: additionalProperties: description: ProvisioningRequest is a definition of a provisioning request in the IONOS Cloud. @@ -148,7 +148,7 @@ spec: - method - requestPath type: object - description: PendingRequests maps data center IDs to a pending provisioning + description: CurrentRequest maps data center IDs to a pending provisioning request made during reconciliation. type: object ready: 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 d0903d60..b51cadf4 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml @@ -51,11 +51,12 @@ spec: example: AMD_OPTERON type: string datacenterId: - description: DatacenterID is the ID of the datacenter where the machine + description: DataCenterID is the ID of the datacenter where the machine should be created in. + format: uuid type: string x-kubernetes-validations: - - message: datacenterID is immutable + - message: datacenterId is immutable rule: self == oldSelf disk: description: Disk defines the boot volume of the VM. @@ -75,12 +76,14 @@ spec: description: DiskType defines the type of the hard drive. enum: - HDD - - SSD + - SSD Standard + - SSD Premium type: string name: description: Name is the name of the volume type: string sizeGB: + default: 5 description: SizeGB defines the size of the volume in GB minimum: 5 type: integer @@ -92,18 +95,16 @@ spec: type: array x-kubernetes-list-type: set required: - - availabilityZone - - diskType - sizeGB type: object memoryMb: - default: 1024 - description: MemoryMb is the memory size for the VM in MB. Size must + default: 3072 + description: MemoryMB is the memory size for the VM in MB. Size must be specified in multiples of 256 MB with a minimum of 1024 MB which is required as we are using hot-pluggable RAM by default. format: int32 - minimum: 1024 - multipleOf: 256 + minimum: 2048 + multipleOf: 1024 type: integer network: description: Network defines the network configuration for the VM. @@ -132,16 +133,10 @@ spec: description: ProviderID is the IONOS Cloud provider ID will be in the format ionos://ee090ff2-1eef-48ec-a246-a51a33aa4f3a type: string - serverId: - description: ServerID is the ID for a VM in the IONOS Cloud context. - The value will be set once the server was created. - type: string required: - - availabilityZone - cpuFamily - datacenterId - disk - - numCores type: object status: description: IonosCloudMachineStatus defines the observed state of IonosCloudMachine. diff --git a/internal/ionoscloud/clienttest/mock_client.go b/internal/ionoscloud/clienttest/mock_client.go index 58d98a4e..1ad84518 100644 --- a/internal/ionoscloud/clienttest/mock_client.go +++ b/internal/ionoscloud/clienttest/mock_client.go @@ -96,6 +96,61 @@ func (_c *MockClient_AttachToLAN_Call) RunAndReturn(run func(context.Context, st return _c } +// CheckRequestStatus provides a mock function with given fields: ctx, requestID +func (_m *MockClient) CheckRequestStatus(ctx context.Context, requestID string) (*ionoscloud.RequestStatus, error) { + ret := _m.Called(ctx, requestID) + + var r0 *ionoscloud.RequestStatus + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*ionoscloud.RequestStatus, error)); ok { + return rf(ctx, requestID) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *ionoscloud.RequestStatus); ok { + r0 = rf(ctx, requestID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ionoscloud.RequestStatus) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, requestID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockClient_CheckRequestStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CheckRequestStatus' +type MockClient_CheckRequestStatus_Call struct { + *mock.Call +} + +// CheckRequestStatus is a helper method to define mock.On call +// - ctx context.Context +// - requestID string +func (_e *MockClient_Expecter) CheckRequestStatus(ctx interface{}, requestID interface{}) *MockClient_CheckRequestStatus_Call { + return &MockClient_CheckRequestStatus_Call{Call: _e.mock.On("CheckRequestStatus", ctx, requestID)} +} + +func (_c *MockClient_CheckRequestStatus_Call) Run(run func(ctx context.Context, requestID string)) *MockClient_CheckRequestStatus_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *MockClient_CheckRequestStatus_Call) Return(_a0 *ionoscloud.RequestStatus, _a1 error) *MockClient_CheckRequestStatus_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockClient_CheckRequestStatus_Call) RunAndReturn(run func(context.Context, string) (*ionoscloud.RequestStatus, error)) *MockClient_CheckRequestStatus_Call { + _c.Call.Return(run) + return _c +} + // CreateDataCenter provides a mock function with given fields: ctx, properties func (_m *MockClient) CreateDataCenter(ctx context.Context, properties ionoscloud.DatacenterProperties) (*ionoscloud.Datacenter, error) { ret := _m.Called(ctx, properties) @@ -152,20 +207,18 @@ func (_c *MockClient_CreateDataCenter_Call) RunAndReturn(run func(context.Contex } // CreateLAN provides a mock function with given fields: ctx, dataCenterID, properties -func (_m *MockClient) CreateLAN(ctx context.Context, dataCenterID string, properties ionoscloud.LanPropertiesPost) (*ionoscloud.LanPost, error) { +func (_m *MockClient) CreateLAN(ctx context.Context, dataCenterID string, properties ionoscloud.LanPropertiesPost) (string, error) { ret := _m.Called(ctx, dataCenterID, properties) - var r0 *ionoscloud.LanPost + var r0 string var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, ionoscloud.LanPropertiesPost) (*ionoscloud.LanPost, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, ionoscloud.LanPropertiesPost) (string, error)); ok { return rf(ctx, dataCenterID, properties) } - if rf, ok := ret.Get(0).(func(context.Context, string, ionoscloud.LanPropertiesPost) *ionoscloud.LanPost); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, ionoscloud.LanPropertiesPost) string); ok { r0 = rf(ctx, dataCenterID, properties) } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*ionoscloud.LanPost) - } + r0 = ret.Get(0).(string) } if rf, ok := ret.Get(1).(func(context.Context, string, ionoscloud.LanPropertiesPost) error); ok { @@ -197,12 +250,12 @@ func (_c *MockClient_CreateLAN_Call) Run(run func(ctx context.Context, dataCente return _c } -func (_c *MockClient_CreateLAN_Call) Return(_a0 *ionoscloud.LanPost, _a1 error) *MockClient_CreateLAN_Call { +func (_c *MockClient_CreateLAN_Call) Return(_a0 string, _a1 error) *MockClient_CreateLAN_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *MockClient_CreateLAN_Call) RunAndReturn(run func(context.Context, string, ionoscloud.LanPropertiesPost) (*ionoscloud.LanPost, error)) *MockClient_CreateLAN_Call { +func (_c *MockClient_CreateLAN_Call) RunAndReturn(run func(context.Context, string, ionoscloud.LanPropertiesPost) (string, error)) *MockClient_CreateLAN_Call { _c.Call.Return(run) return _c } @@ -264,17 +317,27 @@ func (_c *MockClient_CreateServer_Call) RunAndReturn(run func(context.Context, s } // DestroyLAN provides a mock function with given fields: ctx, dataCenterID, lanID -func (_m *MockClient) DestroyLAN(ctx context.Context, dataCenterID string, lanID string) error { +func (_m *MockClient) DestroyLAN(ctx context.Context, dataCenterID string, lanID string) (string, error) { ret := _m.Called(ctx, dataCenterID, lanID) - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (string, error)); ok { + return rf(ctx, dataCenterID, lanID) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) string); ok { r0 = rf(ctx, dataCenterID, lanID) } else { - r0 = ret.Error(0) + r0 = ret.Get(0).(string) } - return r0 + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, dataCenterID, lanID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } // MockClient_DestroyLAN_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DestroyLAN' @@ -297,12 +360,12 @@ func (_c *MockClient_DestroyLAN_Call) Run(run func(ctx context.Context, dataCent return _c } -func (_c *MockClient_DestroyLAN_Call) Return(_a0 error) *MockClient_DestroyLAN_Call { - _c.Call.Return(_a0) +func (_c *MockClient_DestroyLAN_Call) Return(_a0 string, _a1 error) *MockClient_DestroyLAN_Call { + _c.Call.Return(_a0, _a1) return _c } -func (_c *MockClient_DestroyLAN_Call) RunAndReturn(run func(context.Context, string, string) error) *MockClient_DestroyLAN_Call { +func (_c *MockClient_DestroyLAN_Call) RunAndReturn(run func(context.Context, string, string) (string, error)) *MockClient_DestroyLAN_Call { _c.Call.Return(run) return _c } @@ -506,6 +569,62 @@ func (_c *MockClient_GetLAN_Call) RunAndReturn(run func(context.Context, string, return _c } +// GetRequests provides a mock function with given fields: ctx, method, path +func (_m *MockClient) GetRequests(ctx context.Context, method string, path string) (*[]ionoscloud.Request, error) { + ret := _m.Called(ctx, method, path) + + var r0 *[]ionoscloud.Request + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*[]ionoscloud.Request, error)); ok { + return rf(ctx, method, path) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *[]ionoscloud.Request); ok { + r0 = rf(ctx, method, path) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*[]ionoscloud.Request) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, method, path) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockClient_GetRequests_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRequests' +type MockClient_GetRequests_Call struct { + *mock.Call +} + +// GetRequests is a helper method to define mock.On call +// - ctx context.Context +// - method string +// - path string +func (_e *MockClient_Expecter) GetRequests(ctx interface{}, method interface{}, path interface{}) *MockClient_GetRequests_Call { + return &MockClient_GetRequests_Call{Call: _e.mock.On("GetRequests", ctx, method, path)} +} + +func (_c *MockClient_GetRequests_Call) Run(run func(ctx context.Context, method string, path string)) *MockClient_GetRequests_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *MockClient_GetRequests_Call) Return(_a0 *[]ionoscloud.Request, _a1 error) *MockClient_GetRequests_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockClient_GetRequests_Call) RunAndReturn(run func(context.Context, string, string) (*[]ionoscloud.Request, error)) *MockClient_GetRequests_Call { + _c.Call.Return(run) + return _c +} + // GetServer provides a mock function with given fields: ctx, dataCenterID, serverID func (_m *MockClient) GetServer(ctx context.Context, dataCenterID string, serverID string) (*ionoscloud.Server, error) { ret := _m.Called(ctx, dataCenterID, serverID) @@ -840,6 +959,49 @@ func (_c *MockClient_UpdateLAN_Call) RunAndReturn(run func(context.Context, stri return _c } +// WaitForRequest provides a mock function with given fields: ctx, requestURL +func (_m *MockClient) WaitForRequest(ctx context.Context, requestURL string) error { + ret := _m.Called(ctx, requestURL) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, requestURL) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockClient_WaitForRequest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WaitForRequest' +type MockClient_WaitForRequest_Call struct { + *mock.Call +} + +// WaitForRequest is a helper method to define mock.On call +// - ctx context.Context +// - requestURL string +func (_e *MockClient_Expecter) WaitForRequest(ctx interface{}, requestURL interface{}) *MockClient_WaitForRequest_Call { + return &MockClient_WaitForRequest_Call{Call: _e.mock.On("WaitForRequest", ctx, requestURL)} +} + +func (_c *MockClient_WaitForRequest_Call) Run(run func(ctx context.Context, requestURL string)) *MockClient_WaitForRequest_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *MockClient_WaitForRequest_Call) Return(_a0 error) *MockClient_WaitForRequest_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockClient_WaitForRequest_Call) RunAndReturn(run func(context.Context, string) error) *MockClient_WaitForRequest_Call { + _c.Call.Return(run) + return _c +} + // NewMockClient creates a new instance of MockClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMockClient(t interface { diff --git a/internal/service/cloud/datacenter.go b/internal/service/cloud/datacenter.go index d986519d..00b87bbb 100644 --- a/internal/service/cloud/datacenter.go +++ b/internal/service/cloud/datacenter.go @@ -18,5 +18,5 @@ package cloud // DataCenterID is a shortcut for getting the data center ID used by the IONOS Cloud machine. func (s *Service) DataCenterID() string { - return s.scope.IonosCloudMachine.Spec.DatacenterID + return s.scope.IonosCloudMachine.Spec.DataCenterID } From 12625c9cd446f7df1b0f68d2f5d2b7873d4b6900 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 09:23:16 +0100 Subject: [PATCH 028/109] test SSH keys --- api/v1alpha1/ionoscloudmachine_types_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index 2b9723bf..585daa41 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -255,13 +255,18 @@ var _ = Describe("IonosCloudMachine Tests", func() { Entry("ZONE_3", AvailabilityZoneThree), ) }) - It("can be created without ssh keys", func() { + It("can be created without SSH keys", func() { m := completeMachine() var want []string m.Spec.Disk.SSHKeys = want Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.Disk.SSHKeys).To(Equal(want)) }) + It("should prevent setting identical SSH keys", func() { + m := completeMachine() + m.Spec.Disk.SSHKeys = []string{"Key1", "Key1", "Key2", "Key3"} + Expect(k8sClient.Create(context.Background(), m)).To(HaveOccurred()) + }) When("the disk size (in GB)", func() { It("is less than 5, it should fail", func() { m := completeMachine() From 8a0bfa75fd01b54147bd4963a7f8685b60e645f5 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 09:26:18 +0100 Subject: [PATCH 029/109] recreate manifests --- .../infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml | 2 -- 1 file changed, 2 deletions(-) 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 b51cadf4..9fd5095a 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml @@ -94,8 +94,6 @@ spec: type: string type: array x-kubernetes-list-type: set - required: - - sizeGB type: object memoryMb: default: 3072 From ded89ceaf54160efbf934181bd4c8103097d6ec9 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 09:45:35 +0100 Subject: [PATCH 030/109] Naming --- .../ionoscloudmachine_controller.go | 14 +++++----- scope/machine.go | 27 +++++++++---------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index e3eecfae..46043185 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -57,7 +57,7 @@ type IonosCloudMachineReconciler struct { // 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 +// the IonosMachine object against the actual cluster state, and then // perform operations to make the cluster state reflect the state specified by // the user. // @@ -111,12 +111,12 @@ func (r *IonosCloudMachineReconciler) Reconcile(ctx context.Context, req ctrl.Re // Create the machine scope machineScope, err := scope.NewMachineScope(scope.MachineScopeParams{ - Client: r.Client, - Cluster: cluster, - Machine: machine, - InfraCluster: clusterScope, - IonosCloudMachine: ionosCloudMachine, - Logger: &logger, + Client: r.Client, + Cluster: cluster, + Machine: machine, + InfraCluster: clusterScope, + IonosMachine: ionosCloudMachine, + Logger: &logger, }) if err != nil { logger.Error(err, "failed to create scope") diff --git a/scope/machine.go b/scope/machine.go index a465546a..fbe93c95 100644 --- a/scope/machine.go +++ b/scope/machine.go @@ -20,17 +20,16 @@ import ( "context" "errors" "fmt" + ctrl "sigs.k8s.io/controller-runtime" "k8s.io/client-go/util/retry" "sigs.k8s.io/cluster-api/util/conditions" "github.com/go-logr/logr" + infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" "sigs.k8s.io/cluster-api/util/patch" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" - - infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" ) // MachineScope defines a basic context for primary use in IonosCloudMachineReconciler. @@ -48,12 +47,12 @@ type MachineScope struct { // MachineScopeParams is a struct that contains the params used to create a new MachineScope through NewMachineScope. type MachineScopeParams struct { - Client client.Client - Logger *logr.Logger - Cluster *clusterv1.Cluster - Machine *clusterv1.Machine - InfraCluster *ClusterScope - IonosCloudMachine *infrav1.IonosCloudMachine + Client client.Client + Logger *logr.Logger + Cluster *clusterv1.Cluster + Machine *clusterv1.Machine + InfraCluster *ClusterScope + IonosMachine *infrav1.IonosCloudMachine } // NewMachineScope creates a new MachineScope using the provided params. @@ -65,19 +64,19 @@ func NewMachineScope(params MachineScopeParams) (*MachineScope, error) { return nil, errors.New("machine scope params lack a cluster") } if params.Machine == nil { - return nil, errors.New("machine scope params lack a cluster api machine") + return nil, errors.New("machine scope params lack a Cluster API machine") } - if params.IonosCloudMachine == nil { + if params.IonosMachine == nil { return nil, errors.New("machine scope params lack a ionos cloud machine") } if params.InfraCluster == nil { return nil, errors.New("machine scope params need a ionos cloud cluster scope") } if params.Logger == nil { - logger := log.FromContext(context.Background()) + logger := ctrl.Log params.Logger = &logger } - helper, err := patch.NewHelper(params.IonosCloudMachine, params.Client) + helper, err := patch.NewHelper(params.IonosMachine, params.Client) if err != nil { return nil, fmt.Errorf("failed to init patch helper: %w", err) } @@ -88,7 +87,7 @@ func NewMachineScope(params MachineScopeParams) (*MachineScope, error) { Cluster: params.Cluster, Machine: params.Machine, ClusterScope: params.InfraCluster, - IonosCloudMachine: params.IonosCloudMachine, + IonosCloudMachine: params.IonosMachine, }, nil } From b9d5f76a84048112994fc8b135cba021e26cb058 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 09:50:56 +0100 Subject: [PATCH 031/109] linting fixes and compiler error fix --- api/v1alpha1/ionoscloudmachine_types_test.go | 14 +------------- api/v1alpha1/suite_test.go | 2 +- internal/service/cloud/network.go | 6 +++--- scope/machine.go | 4 +++- 4 files changed, 8 insertions(+), 18 deletions(-) diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index 585daa41..c5e2cf68 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -22,7 +22,7 @@ import ( . "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" - "net/http" + "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -68,18 +68,6 @@ func completeMachine() *IonosCloudMachine { } } -func completeStatus() *IonosCloudMachineStatus { - return &IonosCloudMachineStatus{ - Ready: false, - CurrentRequest: &ProvisioningRequest{ - Method: http.MethodPost, - RequestPath: "requestPath", - State: RequestStatusRunning, - Message: ptr.To("requestMessage"), - }, - } -} - var _ = Describe("IonosCloudMachine Tests", func() { AfterEach(func() { err := k8sClient.Delete(context.Background(), defaultMachine()) diff --git a/api/v1alpha1/suite_test.go b/api/v1alpha1/suite_test.go index c12d4014..77ce231a 100644 --- a/api/v1alpha1/suite_test.go +++ b/api/v1alpha1/suite_test.go @@ -48,7 +48,7 @@ var _ = BeforeSuite(func() { filepath.Join("..", "..", "config", "crd", "bases"), }, // NOTE(gfariasalves): To be removed after I finish the PR comments - //BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", "1.28.0-linux-amd64"), + // BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", "1.28.0-linux-amd64"), ErrorIfCRDPathMissing: true, } diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index 182a2efe..4d26c314 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -166,7 +166,7 @@ func (s *Service) createLAN() error { return fmt.Errorf("unable to create LAN in data center %s: %w", s.DataCenterID(), err) } - s.scope.ClusterScope.IonosCluster.Status.PendingRequests[s.DataCenterID()] = infrav1.ProvisioningRequest{ + s.scope.ClusterScope.IonosCluster.Status.CurrentRequest[s.DataCenterID()] = infrav1.ProvisioningRequest{ Method: http.MethodPost, RequestPath: requestPath, State: infrav1.RequestStatusQueued, @@ -190,7 +190,7 @@ func (s *Service) deleteLAN(lanID string) error { return fmt.Errorf("unable to request LAN deletion in data center: %w", err) } - s.scope.ClusterScope.IonosCluster.Status.PendingRequests[s.DataCenterID()] = infrav1.ProvisioningRequest{ + s.scope.ClusterScope.IonosCluster.Status.CurrentRequest[s.DataCenterID()] = infrav1.ProvisioningRequest{ Method: http.MethodDelete, RequestPath: requestPath, State: infrav1.RequestStatusQueued, @@ -258,7 +258,7 @@ func (s *Service) checkForPendingLANRequest(method string, lanID string) (status } func (s *Service) removeLANPendingRequestFromCluster() error { - delete(s.scope.ClusterScope.IonosCluster.Status.PendingRequests, s.DataCenterID()) + delete(s.scope.ClusterScope.IonosCluster.Status.CurrentRequest, s.DataCenterID()) if err := s.scope.ClusterScope.PatchObject(); err != nil { return fmt.Errorf("could not remove stale LAN pending request from cluster: %w", err) } diff --git a/scope/machine.go b/scope/machine.go index fbe93c95..939a50cf 100644 --- a/scope/machine.go +++ b/scope/machine.go @@ -20,16 +20,18 @@ import ( "context" "errors" "fmt" + ctrl "sigs.k8s.io/controller-runtime" "k8s.io/client-go/util/retry" "sigs.k8s.io/cluster-api/util/conditions" "github.com/go-logr/logr" - infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" "sigs.k8s.io/cluster-api/util/patch" "sigs.k8s.io/controller-runtime/pkg/client" + + infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" ) // MachineScope defines a basic context for primary use in IonosCloudMachineReconciler. From 0663b4258550b1c1e26905e41ec82b96903349e3 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 09:52:38 +0100 Subject: [PATCH 032/109] fixed IONOS naming --- internal/controller/ionoscloudmachine_controller.go | 4 ++-- scope/machine.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index 46043185..0a6be47c 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -94,7 +94,7 @@ func (r *IonosCloudMachineReconciler) Reconcile(ctx context.Context, req ctrl.Re } if annotations.IsPaused(cluster, ionosCloudMachine) { - logger.Info("ionos cloud machine or linked cluster is marked as paused, not reconciling") + logger.Info("IONOS Cloud machine or linked cluster is marked as paused, not reconciling") return ctrl.Result{}, nil } @@ -105,7 +105,7 @@ func (r *IonosCloudMachineReconciler) Reconcile(ctx context.Context, req ctrl.Re return ctrl.Result{}, fmt.Errorf("error getting infra provider cluster or control plane object: %w", err) } if clusterScope == nil { - logger.Info("ionos cloud machine is not ready yet") + logger.Info("IONOS Cloud machine is not ready yet") return ctrl.Result{}, nil } diff --git a/scope/machine.go b/scope/machine.go index 939a50cf..b65ec614 100644 --- a/scope/machine.go +++ b/scope/machine.go @@ -69,10 +69,10 @@ func NewMachineScope(params MachineScopeParams) (*MachineScope, error) { return nil, errors.New("machine scope params lack a Cluster API machine") } if params.IonosMachine == nil { - return nil, errors.New("machine scope params lack a ionos cloud machine") + return nil, errors.New("machine scope params lack a IONOS Cloud machine") } if params.InfraCluster == nil { - return nil, errors.New("machine scope params need a ionos cloud cluster scope") + return nil, errors.New("machine scope params need a IONOS Cloud cluster scope") } if params.Logger == nil { logger := ctrl.Log From 5b3e358d8ac97a8d6b21525870ede8d908e8bb1a Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 09:53:00 +0100 Subject: [PATCH 033/109] update comment --- scope/machine.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scope/machine.go b/scope/machine.go index b65ec614..b63e84f8 100644 --- a/scope/machine.go +++ b/scope/machine.go @@ -93,7 +93,7 @@ func NewMachineScope(params MachineScopeParams) (*MachineScope, error) { }, nil } -// HasFailed Checks if the IonosCloudMachine is in a failed state. +// HasFailed checks if the IonosCloudMachine is in a failed state. func (m *MachineScope) HasFailed() bool { status := m.IonosCloudMachine.Status return status.FailureReason != nil || status.FailureMessage != nil From f3efe5c8989ed9718bbd227dbef1f313801227a8 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 10:02:42 +0100 Subject: [PATCH 034/109] add timeout context and comment --- api/v1alpha1/ionoscloudmachine_types_test.go | 1 + scope/machine.go | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index c5e2cf68..9e94a476 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -18,6 +18,7 @@ package v1alpha1 import ( "context" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/scope/machine.go b/scope/machine.go index b63e84f8..91f82e95 100644 --- a/scope/machine.go +++ b/scope/machine.go @@ -20,6 +20,7 @@ import ( "context" "errors" "fmt" + "time" ctrl "sigs.k8s.io/controller-runtime" @@ -106,8 +107,14 @@ func (m *MachineScope) PatchObject() error { conditions.WithConditions( infrav1.MachineProvisionedCondition)) + timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + // 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( - context.TODO(), + timeoutCtx, m.IonosCloudMachine, patch.WithOwnedConditions{Conditions: []clusterv1.ConditionType{ clusterv1.ReadyCondition, From 4045d51b291e7d6a911cc972b2e11d1d5663dc9f Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 10:05:01 +0100 Subject: [PATCH 035/109] fix url naming --- internal/ionoscloud/client/errors.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/ionoscloud/client/errors.go b/internal/ionoscloud/client/errors.go index 9798ec35..09686e28 100644 --- a/internal/ionoscloud/client/errors.go +++ b/internal/ionoscloud/client/errors.go @@ -23,10 +23,10 @@ var ( errServerIDIsEmpty = errors.New("error parsing server ID: value cannot be empty") errLANIDIsEmpty = errors.New("error parsing LAN ID: value cannot be empty") errVolumeIDIsEmpty = errors.New("error parsing volume ID: value cannot be empty") - errRequestURLIsEmpty = errors.New("a request url is necessary for the operation") + errRequestURLIsEmpty = errors.New("a request URL is necessary for the operation") ) const ( apiCallErrWrapper = "request to Cloud API has failed: %w" - apiNoLocationErrWrapper = "request to Cloud API did not return the request url" + apiNoLocationErrWrapper = "request to Cloud API did not return the request URL" ) From 78709a81cc040866e1449724c8ca22d862f78a31 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 10:09:36 +0100 Subject: [PATCH 036/109] constant naming --- internal/ionoscloud/client/client.go | 2 +- internal/ionoscloud/client/errors.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/ionoscloud/client/client.go b/internal/ionoscloud/client/client.go index a810cb34..1c1b0805 100644 --- a/internal/ionoscloud/client/client.go +++ b/internal/ionoscloud/client/client.go @@ -161,7 +161,7 @@ func (c *IonosCloudClient) CreateLAN(ctx context.Context, dataCenterID string, p if location := req.Header.Get("Location"); location != "" { return location, nil } - return "", errors.New(apiNoLocationErrWrapper) + return "", errors.New(apiNoLocationErrMessage) } // UpdateLAN updates a LAN with the provided properties in the specified data center. diff --git a/internal/ionoscloud/client/errors.go b/internal/ionoscloud/client/errors.go index 09686e28..4cdf721c 100644 --- a/internal/ionoscloud/client/errors.go +++ b/internal/ionoscloud/client/errors.go @@ -28,5 +28,5 @@ var ( const ( apiCallErrWrapper = "request to Cloud API has failed: %w" - apiNoLocationErrWrapper = "request to Cloud API did not return the request URL" + apiNoLocationErrMessage = "request to Cloud API did not return the request URL" ) From 816ba3a39d0328d35ea1a388bbe898d3d848c2cf Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 10:22:18 +0100 Subject: [PATCH 037/109] renamed API function --- internal/ionoscloud/client.go | 2 +- internal/ionoscloud/client/client.go | 4 ++-- internal/ionoscloud/clienttest/mock_client.go | 20 +++++++++---------- internal/service/cloud/network.go | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/internal/ionoscloud/client.go b/internal/ionoscloud/client.go index 63ab3bb8..9e65863c 100644 --- a/internal/ionoscloud/client.go +++ b/internal/ionoscloud/client.go @@ -52,7 +52,7 @@ type Client interface { // GetLAN returns the LAN that matches lanID in the specified data center. GetLAN(ctx context.Context, dataCenterID, lanID string) (*ionoscloud.Lan, error) // DestroyLAN deletes the LAN that matches the provided lanID in the specified data center. - DestroyLAN(ctx context.Context, dataCenterID, lanID string) (string, error) + DeleteLAN(ctx context.Context, dataCenterID, lanID string) (string, error) // CheckRequestStatus checks the status of a provided request identified by requestID CheckRequestStatus(ctx context.Context, requestID string) (*ionoscloud.RequestStatus, error) // ListVolumes returns a list of volumes in a specified data center. diff --git a/internal/ionoscloud/client/client.go b/internal/ionoscloud/client/client.go index 1c1b0805..aa12cef8 100644 --- a/internal/ionoscloud/client/client.go +++ b/internal/ionoscloud/client/client.go @@ -225,8 +225,8 @@ func (c *IonosCloudClient) GetLAN(ctx context.Context, dataCenterID, lanID strin return &lan, nil } -// DestroyLAN deletes the LAN that matches the provided lanID in the specified data center. -func (c *IonosCloudClient) DestroyLAN(ctx context.Context, dataCenterID, lanID string) (string, error) { +// DeleteLAN deletes the LAN that matches the provided lanID in the specified data center. +func (c *IonosCloudClient) DeleteLAN(ctx context.Context, dataCenterID, lanID string) (string, error) { if dataCenterID == "" { return "", errDataCenterIDIsEmpty } diff --git a/internal/ionoscloud/clienttest/mock_client.go b/internal/ionoscloud/clienttest/mock_client.go index 1ad84518..c8be0776 100644 --- a/internal/ionoscloud/clienttest/mock_client.go +++ b/internal/ionoscloud/clienttest/mock_client.go @@ -316,8 +316,8 @@ func (_c *MockClient_CreateServer_Call) RunAndReturn(run func(context.Context, s return _c } -// DestroyLAN provides a mock function with given fields: ctx, dataCenterID, lanID -func (_m *MockClient) DestroyLAN(ctx context.Context, dataCenterID string, lanID string) (string, error) { +// DeleteLAN provides a mock function with given fields: ctx, dataCenterID, lanID +func (_m *MockClient) DeleteLAN(ctx context.Context, dataCenterID string, lanID string) (string, error) { ret := _m.Called(ctx, dataCenterID, lanID) var r0 string @@ -340,32 +340,32 @@ func (_m *MockClient) DestroyLAN(ctx context.Context, dataCenterID string, lanID return r0, r1 } -// MockClient_DestroyLAN_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DestroyLAN' -type MockClient_DestroyLAN_Call struct { +// MockClient_DeleteLAN_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteLAN' +type MockClient_DeleteLAN_Call struct { *mock.Call } -// DestroyLAN is a helper method to define mock.On call +// DeleteLAN is a helper method to define mock.On call // - ctx context.Context // - dataCenterID string // - lanID string -func (_e *MockClient_Expecter) DestroyLAN(ctx interface{}, dataCenterID interface{}, lanID interface{}) *MockClient_DestroyLAN_Call { - return &MockClient_DestroyLAN_Call{Call: _e.mock.On("DestroyLAN", ctx, dataCenterID, lanID)} +func (_e *MockClient_Expecter) DeleteLAN(ctx interface{}, dataCenterID interface{}, lanID interface{}) *MockClient_DeleteLAN_Call { + return &MockClient_DeleteLAN_Call{Call: _e.mock.On("DeleteLAN", ctx, dataCenterID, lanID)} } -func (_c *MockClient_DestroyLAN_Call) Run(run func(ctx context.Context, dataCenterID string, lanID string)) *MockClient_DestroyLAN_Call { +func (_c *MockClient_DeleteLAN_Call) Run(run func(ctx context.Context, dataCenterID string, lanID string)) *MockClient_DeleteLAN_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string), args[2].(string)) }) return _c } -func (_c *MockClient_DestroyLAN_Call) Return(_a0 string, _a1 error) *MockClient_DestroyLAN_Call { +func (_c *MockClient_DeleteLAN_Call) Return(_a0 string, _a1 error) *MockClient_DeleteLAN_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *MockClient_DestroyLAN_Call) RunAndReturn(run func(context.Context, string, string) (string, error)) *MockClient_DestroyLAN_Call { +func (_c *MockClient_DeleteLAN_Call) RunAndReturn(run func(context.Context, string, string) (string, error)) *MockClient_DeleteLAN_Call { _c.Call.Return(run) return _c } diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index 4d26c314..12131240 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -185,7 +185,7 @@ func (s *Service) createLAN() error { func (s *Service) deleteLAN(lanID string) error { log := s.scope.Logger.WithName("DeleteLAN") - requestPath, err := s.API().DestroyLAN(s.ctx, s.DataCenterID(), lanID) + requestPath, err := s.API().DeleteLAN(s.ctx, s.DataCenterID(), lanID) if err != nil { return fmt.Errorf("unable to request LAN deletion in data center: %w", err) } From 643db98f06f0fdcad8980efeaf69b334d1694200 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 10:23:33 +0100 Subject: [PATCH 038/109] update comments --- internal/ionoscloud/client.go | 2 +- internal/ionoscloud/client/client.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/ionoscloud/client.go b/internal/ionoscloud/client.go index 9e65863c..cb43ee16 100644 --- a/internal/ionoscloud/client.go +++ b/internal/ionoscloud/client.go @@ -51,7 +51,7 @@ type Client interface { ListLANs(ctx context.Context, dataCenterID string) (*ionoscloud.Lans, error) // GetLAN returns the LAN that matches lanID in the specified data center. GetLAN(ctx context.Context, dataCenterID, lanID string) (*ionoscloud.Lan, error) - // DestroyLAN deletes the LAN that matches the provided lanID in the specified data center. + // DeleteLAN deletes the LAN that matches the provided lanID in the specified data center, returning the request location. DeleteLAN(ctx context.Context, dataCenterID, lanID string) (string, error) // CheckRequestStatus checks the status of a provided request identified by requestID CheckRequestStatus(ctx context.Context, requestID string) (*ionoscloud.RequestStatus, error) diff --git a/internal/ionoscloud/client/client.go b/internal/ionoscloud/client/client.go index aa12cef8..b21914d1 100644 --- a/internal/ionoscloud/client/client.go +++ b/internal/ionoscloud/client/client.go @@ -225,7 +225,7 @@ func (c *IonosCloudClient) GetLAN(ctx context.Context, dataCenterID, lanID strin return &lan, nil } -// DeleteLAN deletes the LAN that matches the provided lanID in the specified data center. +// DeleteLAN deletes the LAN that matches the provided lanID in the specified data center, returning the request location. func (c *IonosCloudClient) DeleteLAN(ctx context.Context, dataCenterID, lanID string) (string, error) { if dataCenterID == "" { return "", errDataCenterIDIsEmpty From 1ed92cb7662591c73f8b9a6c55f9cbef0459441b Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 10:25:27 +0100 Subject: [PATCH 039/109] unexport function --- internal/service/cloud/datacenter.go | 4 ++-- internal/service/cloud/network.go | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/internal/service/cloud/datacenter.go b/internal/service/cloud/datacenter.go index 00b87bbb..ee084944 100644 --- a/internal/service/cloud/datacenter.go +++ b/internal/service/cloud/datacenter.go @@ -16,7 +16,7 @@ limitations under the License. package cloud -// DataCenterID is a shortcut for getting the data center ID used by the IONOS Cloud machine. -func (s *Service) DataCenterID() string { +// dataCenterID is a shortcut for getting the data center ID used by the IONOS Cloud machine. +func (s *Service) dataCenterID() string { return s.scope.IonosCloudMachine.Spec.DataCenterID } diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index 12131240..c02a8f25 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -90,9 +90,9 @@ func (s *Service) ReconcileLAN() (requeue bool, err error) { // GetLAN tries to retrieve the cluster related LAN in the datacenter. func (s *Service) GetLAN() (*sdk.Lan, error) { // check if the LAN exists - lans, err := s.API().ListLANs(s.ctx, s.DataCenterID()) + lans, err := s.API().ListLANs(s.ctx, s.dataCenterID()) if err != nil { - return nil, fmt.Errorf("could not list lans in datacenter %s: %w", s.DataCenterID(), err) + return nil, fmt.Errorf("could not list lans in datacenter %s: %w", s.dataCenterID(), err) } for _, l := range *lans.Items { @@ -158,15 +158,15 @@ func (s *Service) ReconcileLANDeletion() (requeue bool, err error) { func (s *Service) createLAN() error { log := s.scope.Logger.WithName("CreateLAN") - requestPath, err := s.API().CreateLAN(s.ctx, s.DataCenterID(), sdk.LanPropertiesPost{ + requestPath, err := s.API().CreateLAN(s.ctx, s.dataCenterID(), sdk.LanPropertiesPost{ Name: ptr.To(s.LANName()), Public: ptr.To(true), }) if err != nil { - return fmt.Errorf("unable to create LAN in data center %s: %w", s.DataCenterID(), err) + return fmt.Errorf("unable to create LAN in data center %s: %w", s.dataCenterID(), err) } - s.scope.ClusterScope.IonosCluster.Status.CurrentRequest[s.DataCenterID()] = infrav1.ProvisioningRequest{ + s.scope.ClusterScope.IonosCluster.Status.CurrentRequest[s.dataCenterID()] = infrav1.ProvisioningRequest{ Method: http.MethodPost, RequestPath: requestPath, State: infrav1.RequestStatusQueued, @@ -185,12 +185,12 @@ func (s *Service) createLAN() error { func (s *Service) deleteLAN(lanID string) error { log := s.scope.Logger.WithName("DeleteLAN") - requestPath, err := s.API().DeleteLAN(s.ctx, s.DataCenterID(), lanID) + requestPath, err := s.API().DeleteLAN(s.ctx, s.dataCenterID(), lanID) if err != nil { return fmt.Errorf("unable to request LAN deletion in data center: %w", err) } - s.scope.ClusterScope.IonosCluster.Status.CurrentRequest[s.DataCenterID()] = infrav1.ProvisioningRequest{ + s.scope.ClusterScope.IonosCluster.Status.CurrentRequest[s.dataCenterID()] = infrav1.ProvisioningRequest{ Method: http.MethodDelete, RequestPath: requestPath, State: infrav1.RequestStatusQueued, @@ -220,7 +220,7 @@ func (s *Service) checkForPendingLANRequest(method string, lanID string) (status )) } - lanPath := path.Join("datacenters", s.DataCenterID(), "lan") + lanPath := path.Join("datacenters", s.dataCenterID(), "lan") requests, err := s.getPendingRequests(method, lanPath) if err != nil { return "", err @@ -258,7 +258,7 @@ func (s *Service) checkForPendingLANRequest(method string, lanID string) (status } func (s *Service) removeLANPendingRequestFromCluster() error { - delete(s.scope.ClusterScope.IonosCluster.Status.CurrentRequest, s.DataCenterID()) + delete(s.scope.ClusterScope.IonosCluster.Status.CurrentRequest, s.dataCenterID()) if err := s.scope.ClusterScope.PatchObject(); err != nil { return fmt.Errorf("could not remove stale LAN pending request from cluster: %w", err) } From 2fc84c30a65e0af4058e44ef80f704a8268c6201 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 10:28:54 +0100 Subject: [PATCH 040/109] unexport api function --- internal/service/cloud/network.go | 6 +++--- internal/service/cloud/request.go | 2 +- internal/service/cloud/service.go | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index c02a8f25..15c8a273 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -90,7 +90,7 @@ func (s *Service) ReconcileLAN() (requeue bool, err error) { // GetLAN tries to retrieve the cluster related LAN in the datacenter. func (s *Service) GetLAN() (*sdk.Lan, error) { // check if the LAN exists - lans, err := s.API().ListLANs(s.ctx, s.dataCenterID()) + lans, err := s.api().ListLANs(s.ctx, s.dataCenterID()) if err != nil { return nil, fmt.Errorf("could not list lans in datacenter %s: %w", s.dataCenterID(), err) } @@ -158,7 +158,7 @@ func (s *Service) ReconcileLANDeletion() (requeue bool, err error) { func (s *Service) createLAN() error { log := s.scope.Logger.WithName("CreateLAN") - requestPath, err := s.API().CreateLAN(s.ctx, s.dataCenterID(), sdk.LanPropertiesPost{ + requestPath, err := s.api().CreateLAN(s.ctx, s.dataCenterID(), sdk.LanPropertiesPost{ Name: ptr.To(s.LANName()), Public: ptr.To(true), }) @@ -185,7 +185,7 @@ func (s *Service) createLAN() error { func (s *Service) deleteLAN(lanID string) error { log := s.scope.Logger.WithName("DeleteLAN") - requestPath, err := s.API().DeleteLAN(s.ctx, s.dataCenterID(), lanID) + requestPath, err := s.api().DeleteLAN(s.ctx, s.dataCenterID(), lanID) if err != nil { return fmt.Errorf("unable to request LAN deletion in data center: %w", err) } diff --git a/internal/service/cloud/request.go b/internal/service/cloud/request.go index 56248f60..054314ef 100644 --- a/internal/service/cloud/request.go +++ b/internal/service/cloud/request.go @@ -23,7 +23,7 @@ import ( ) func (s *Service) getPendingRequests(method, resourcePath string) ([]sdk.Request, error) { - requests, err := s.API().GetRequests(s.ctx, method, resourcePath) + requests, err := s.api().GetRequests(s.ctx, method, resourcePath) if err != nil { return nil, fmt.Errorf("could not get requests: %w", err) } diff --git a/internal/service/cloud/service.go b/internal/service/cloud/service.go index c72a43a5..fce54c53 100644 --- a/internal/service/cloud/service.go +++ b/internal/service/cloud/service.go @@ -42,7 +42,7 @@ func NewService(ctx context.Context, s *scope.MachineScope) (*Service, error) { }, nil } -// API is a shortcut for the IONOS Cloud Client. -func (s *Service) API() ionoscloud.Client { +// api is a shortcut for the IONOS Cloud Client. +func (s *Service) api() ionoscloud.Client { return s.scope.ClusterScope.IonosClient } From d4ac419a8352d2cec94f7802b8b22c80e3e3d40b Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 10:36:11 +0100 Subject: [PATCH 041/109] lan naming --- internal/service/cloud/network.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index 15c8a273..65e41dd3 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -92,7 +92,7 @@ func (s *Service) GetLAN() (*sdk.Lan, error) { // check if the LAN exists lans, err := s.api().ListLANs(s.ctx, s.dataCenterID()) if err != nil { - return nil, fmt.Errorf("could not list lans in datacenter %s: %w", s.dataCenterID(), err) + return nil, fmt.Errorf("could not list LANs in datacenter %s: %w", s.dataCenterID(), err) } for _, l := range *lans.Items { From ce06e6150628661f7ce5949f79bd59feeddd3224 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 10:50:22 +0100 Subject: [PATCH 042/109] renamed function to better indicate the purpose --- internal/service/cloud/network.go | 2 +- internal/service/cloud/request.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index 65e41dd3..d04f72d9 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -221,7 +221,7 @@ func (s *Service) checkForPendingLANRequest(method string, lanID string) (status } lanPath := path.Join("datacenters", s.dataCenterID(), "lan") - requests, err := s.getPendingRequests(method, lanPath) + requests, err := s.findRelatedRequests(method, lanPath) if err != nil { return "", err } diff --git a/internal/service/cloud/request.go b/internal/service/cloud/request.go index 054314ef..11b4fc97 100644 --- a/internal/service/cloud/request.go +++ b/internal/service/cloud/request.go @@ -22,7 +22,7 @@ import ( sdk "github.com/ionos-cloud/sdk-go/v6" ) -func (s *Service) getPendingRequests(method, resourcePath string) ([]sdk.Request, error) { +func (s *Service) findRelatedRequests(method, resourcePath string) ([]sdk.Request, error) { requests, err := s.api().GetRequests(s.ctx, method, resourcePath) if err != nil { return nil, fmt.Errorf("could not get requests: %w", err) From 11f51a1be5152d9fd87807128dd8def70eb0995f Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 11:04:34 +0100 Subject: [PATCH 043/109] aded default requeue constant --- internal/controller/ionoscloudmachine_controller.go | 9 +++++++-- internal/controller/util.go | 7 +++++++ 2 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 internal/controller/util.go diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index 0a6be47c..f6a1ef54 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -198,7 +198,7 @@ func (r *IonosCloudMachineReconciler) reconcileNormal(machineScope *scope.Machin // Ensure that a LAN is created in the datacenter if requeue, err := cloudService.ReconcileLAN(); err != nil || requeue { if requeue { - return ctrl.Result{RequeueAfter: time.Second * 30}, err + return ctrl.Result{RequeueAfter: defaultReconcileDuration}, err } return ctrl.Result{}, fmt.Errorf("could not reconcile LAN %w", err) } @@ -211,8 +211,13 @@ func (r *IonosCloudMachineReconciler) reconcileDelete(cloudService *cloud.Servic if err != nil { return ctrl.Result{}, fmt.Errorf("could not reconcile LAN deletion: %w", err) } + + var after time.Duration + if requeue { + after = defaultReconcileDuration + } return ctrl.Result{ - Requeue: requeue, + RequeueAfter: after, }, nil } diff --git a/internal/controller/util.go b/internal/controller/util.go new file mode 100644 index 00000000..8e4ed7d2 --- /dev/null +++ b/internal/controller/util.go @@ -0,0 +1,7 @@ +package controller + +import "time" + +const ( + defaultReconcileDuration = time.Second * 20 +) From ae0ee97dc150d6ff993b7064bf0c372afe4aa170 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 11:11:01 +0100 Subject: [PATCH 044/109] renamed struct field --- .../ionoscloudmachine_controller.go | 6 ++--- internal/service/cloud/datacenter.go | 2 +- scope/machine.go | 24 +++++++++---------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index f6a1ef54..86ec93b1 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -145,7 +145,7 @@ func (r *IonosCloudMachineReconciler) isInfrastructureReady(machineScope *scope. if !machineScope.Cluster.Status.InfrastructureReady { machineScope.Info("Cluster infrastructure is not ready yet") conditions.MarkFalse( - machineScope.IonosCloudMachine, + machineScope.IonosMachine, infrav1.MachineProvisionedCondition, infrav1.WaitingForClusterInfrastructureReason, clusterv1.ConditionSeverityInfo, "") @@ -157,7 +157,7 @@ func (r *IonosCloudMachineReconciler) isInfrastructureReady(machineScope *scope. if machineScope.Machine.Spec.Bootstrap.DataSecretName == nil { machineScope.Info("Bootstrap data secret is not available yet") conditions.MarkFalse( - machineScope.IonosCloudMachine, + machineScope.IonosMachine, infrav1.MachineProvisionedCondition, infrav1.WaitingForBootstrapDataReason, clusterv1.ConditionSeverityInfo, "", @@ -181,7 +181,7 @@ func (r *IonosCloudMachineReconciler) reconcileNormal(machineScope *scope.Machin return ctrl.Result{}, nil } - if controllerutil.AddFinalizer(machineScope.IonosCloudMachine, infrav1.MachineFinalizer) { + if controllerutil.AddFinalizer(machineScope.IonosMachine, infrav1.MachineFinalizer) { if err := machineScope.PatchObject(); err != nil { machineScope.Error(err, "unable to update finalizer on object") return ctrl.Result{}, err diff --git a/internal/service/cloud/datacenter.go b/internal/service/cloud/datacenter.go index ee084944..55b42fbe 100644 --- a/internal/service/cloud/datacenter.go +++ b/internal/service/cloud/datacenter.go @@ -18,5 +18,5 @@ package cloud // dataCenterID is a shortcut for getting the data center ID used by the IONOS Cloud machine. func (s *Service) dataCenterID() string { - return s.scope.IonosCloudMachine.Spec.DataCenterID + return s.scope.IonosMachine.Spec.DataCenterID } diff --git a/scope/machine.go b/scope/machine.go index 91f82e95..e2ab1f4d 100644 --- a/scope/machine.go +++ b/scope/machine.go @@ -44,8 +44,8 @@ type MachineScope struct { Cluster *clusterv1.Cluster Machine *clusterv1.Machine - ClusterScope *ClusterScope - IonosCloudMachine *infrav1.IonosCloudMachine + ClusterScope *ClusterScope + IonosMachine *infrav1.IonosCloudMachine } // MachineScopeParams is a struct that contains the params used to create a new MachineScope through NewMachineScope. @@ -84,26 +84,26 @@ func NewMachineScope(params MachineScopeParams) (*MachineScope, error) { return nil, fmt.Errorf("failed to init patch helper: %w", err) } return &MachineScope{ - Logger: params.Logger, - client: params.Client, - patchHelper: helper, - Cluster: params.Cluster, - Machine: params.Machine, - ClusterScope: params.InfraCluster, - IonosCloudMachine: params.IonosMachine, + Logger: params.Logger, + client: params.Client, + patchHelper: helper, + Cluster: params.Cluster, + Machine: params.Machine, + ClusterScope: params.InfraCluster, + IonosMachine: params.IonosMachine, }, nil } // HasFailed checks if the IonosCloudMachine is in a failed state. func (m *MachineScope) HasFailed() bool { - status := m.IonosCloudMachine.Status + status := m.IonosMachine.Status return status.FailureReason != nil || status.FailureMessage != nil } // PatchObject will apply all changes from the IonosCloudMachine. // It will also make sure to patch the status subresource. func (m *MachineScope) PatchObject() error { - conditions.SetSummary(m.IonosCloudMachine, + conditions.SetSummary(m.IonosMachine, conditions.WithConditions( infrav1.MachineProvisionedCondition)) @@ -115,7 +115,7 @@ func (m *MachineScope) PatchObject() error { // would cause the patch to be aborted as well. return m.patchHelper.Patch( timeoutCtx, - m.IonosCloudMachine, + m.IonosMachine, patch.WithOwnedConditions{Conditions: []clusterv1.ConditionType{ clusterv1.ReadyCondition, infrav1.MachineProvisionedCondition, From 529c3030d61a9df05e2b379af230e362e26068f2 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 11:25:35 +0100 Subject: [PATCH 045/109] add license header --- internal/controller/util.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/internal/controller/util.go b/internal/controller/util.go index 8e4ed7d2..9f5e7a62 100644 --- a/internal/controller/util.go +++ b/internal/controller/util.go @@ -1,3 +1,19 @@ +/* +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 controller import "time" From d19605b64af8539cd173f047b8d7096ceaf8e505 Mon Sep 17 00:00:00 2001 From: Gustavo Alves Date: Mon, 15 Jan 2024 13:01:53 +0100 Subject: [PATCH 046/109] Implement Machine Scope tests --- api/v1alpha1/suite_test.go | 3 - .../ionoscloudmachine_controller.go | 2 +- scope/machine.go | 8 +- scope/machine_test.go | 98 +++++++++++++++++++ 4 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 scope/machine_test.go diff --git a/api/v1alpha1/suite_test.go b/api/v1alpha1/suite_test.go index 77ce231a..efc2b39e 100644 --- a/api/v1alpha1/suite_test.go +++ b/api/v1alpha1/suite_test.go @@ -47,9 +47,6 @@ var _ = BeforeSuite(func() { CRDDirectoryPaths: []string{ filepath.Join("..", "..", "config", "crd", "bases"), }, - // NOTE(gfariasalves): To be removed after I finish the PR comments - // BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", "1.28.0-linux-amd64"), - ErrorIfCRDPathMissing: true, } diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index 86ec93b1..628efaa5 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -114,7 +114,7 @@ func (r *IonosCloudMachineReconciler) Reconcile(ctx context.Context, req ctrl.Re Client: r.Client, Cluster: cluster, Machine: machine, - InfraCluster: clusterScope, + ClusterScope: clusterScope, IonosMachine: ionosCloudMachine, Logger: &logger, }) diff --git a/scope/machine.go b/scope/machine.go index e2ab1f4d..362e8d25 100644 --- a/scope/machine.go +++ b/scope/machine.go @@ -54,7 +54,7 @@ type MachineScopeParams struct { Logger *logr.Logger Cluster *clusterv1.Cluster Machine *clusterv1.Machine - InfraCluster *ClusterScope + ClusterScope *ClusterScope IonosMachine *infrav1.IonosCloudMachine } @@ -72,7 +72,7 @@ func NewMachineScope(params MachineScopeParams) (*MachineScope, error) { if params.IonosMachine == nil { return nil, errors.New("machine scope params lack a IONOS Cloud machine") } - if params.InfraCluster == nil { + if params.ClusterScope == nil { return nil, errors.New("machine scope params need a IONOS Cloud cluster scope") } if params.Logger == nil { @@ -89,7 +89,7 @@ func NewMachineScope(params MachineScopeParams) (*MachineScope, error) { patchHelper: helper, Cluster: params.Cluster, Machine: params.Machine, - ClusterScope: params.InfraCluster, + ClusterScope: params.ClusterScope, IonosMachine: params.IonosMachine, }, nil } @@ -100,7 +100,7 @@ func (m *MachineScope) HasFailed() bool { return status.FailureReason != nil || status.FailureMessage != nil } -// PatchObject will apply all changes from the IonosCloudMachine. +// PatchObject will apply all changes from the IonosMachine. // It will also make sure to patch the status subresource. func (m *MachineScope) PatchObject() error { conditions.SetSummary(m.IonosMachine, diff --git a/scope/machine_test.go b/scope/machine_test.go new file mode 100644 index 00000000..59a0f592 --- /dev/null +++ b/scope/machine_test.go @@ -0,0 +1,98 @@ +package scope + +import ( + "github.com/go-logr/logr" + infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" + "github.com/stretchr/testify/require" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/utils/ptr" + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + capierrors "sigs.k8s.io/cluster-api/errors" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "testing" +) + +func exampleParams(t *testing.T) MachineScopeParams { + if err := infrav1.AddToScheme(scheme.Scheme); err != nil { + require.NoError(t, err, "could not construct params") + } + logger := logr.Discard() + return MachineScopeParams{ + Client: fake.NewClientBuilder().WithScheme(scheme.Scheme).Build(), + Logger: &logger, + Cluster: &clusterv1.Cluster{}, + Machine: &clusterv1.Machine{}, + ClusterScope: &ClusterScope{}, + IonosMachine: &infrav1.IonosCloudMachine{}, + } +} + +func TestNewMachineScope_OK(t *testing.T) { + scope, err := NewMachineScope(exampleParams(t)) + require.NotNil(t, scope, "returned machine scope should not be nil") + require.NoError(t, err) + require.NotNil(t, scope.patchHelper, "returned scope should have a non-nil patchHelper") +} + +func TestMachineScopeParams_NilClientShouldFail(t *testing.T) { + params := exampleParams(t) + params.Client = nil + scope, err := NewMachineScope(params) + require.Nil(t, scope, "returned machine scope should be nil") + require.Error(t, err) +} + +func TestMachineScopeParams_NilLoggerShouldWork(t *testing.T) { + params := exampleParams(t) + params.Logger = nil + scope, err := NewMachineScope(params) + require.NotNil(t, scope, "returned machine scope shouldn't be nil") + require.NoError(t, err) + require.NotNil(t, scope.Logger, "logger should not be nil") +} + +func TestMachineScopeParams_NilClusterShouldFail(t *testing.T) { + params := exampleParams(t) + params.Cluster = nil + scope, err := NewMachineScope(params) + require.Nil(t, scope, "returned machine scope should be nil") + require.Error(t, err) +} + +func TestMachineScopeParams_NilMachineShouldFail(t *testing.T) { + params := exampleParams(t) + params.Machine = nil + scope, err := NewMachineScope(params) + require.Nil(t, scope, "returned machine scope should be nil") + require.Error(t, err) +} + +func TestMachineScopeParams_NilIonosMachineShouldFail(t *testing.T) { + params := exampleParams(t) + params.IonosMachine = nil + scope, err := NewMachineScope(params) + require.Nil(t, scope, "returned machine scope should be nil") + require.Error(t, err) +} + +func TestMachineScopeParams_NilClusterScopeShouldFail(t *testing.T) { + params := exampleParams(t) + params.ClusterScope = nil + scope, err := NewMachineScope(params) + require.Nil(t, scope, "returned machine scope should be nil") + require.Error(t, err) +} + +func TestMachineScope_HasFailed_FailureMessage(t *testing.T) { + scope, err := NewMachineScope(exampleParams(t)) + require.NoError(t, err) + scope.IonosMachine.Status.FailureMessage = ptr.To("¯\\_(ツ)_/¯") + require.True(t, scope.HasFailed()) +} + +func TestMachineScope_HasFailed_FailureReason(t *testing.T) { + scope, err := NewMachineScope(exampleParams(t)) + require.NoError(t, err) + scope.IonosMachine.Status.FailureReason = capierrors.MachineStatusErrorPtr("¯\\_(ツ)_/¯") + require.True(t, scope.HasFailed()) +} From 87e6767fef0bc19e16b1fef1af53c02e5f3f5e5f Mon Sep 17 00:00:00 2001 From: Gustavo Alves Date: Mon, 15 Jan 2024 13:05:01 +0100 Subject: [PATCH 047/109] lint-fix machine_test.go --- scope/machine_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scope/machine_test.go b/scope/machine_test.go index 59a0f592..7923d479 100644 --- a/scope/machine_test.go +++ b/scope/machine_test.go @@ -1,15 +1,17 @@ package scope import ( + "testing" + "github.com/go-logr/logr" - infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/stretchr/testify/require" "k8s.io/client-go/kubernetes/scheme" "k8s.io/utils/ptr" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" capierrors "sigs.k8s.io/cluster-api/errors" "sigs.k8s.io/controller-runtime/pkg/client/fake" - "testing" + + infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" ) func exampleParams(t *testing.T) MachineScopeParams { From b14ecde211367f550091cd0b75458bee806be577 Mon Sep 17 00:00:00 2001 From: Gustavo Alves Date: Mon, 15 Jan 2024 13:09:50 +0100 Subject: [PATCH 048/109] add copyright to machine_test.go --- scope/machine_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/scope/machine_test.go b/scope/machine_test.go index 7923d479..5c415167 100644 --- a/scope/machine_test.go +++ b/scope/machine_test.go @@ -1,3 +1,18 @@ +/* +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 scope import ( From 182923b6db8369d830a6cb86f79b8a5fd852e4f1 Mon Sep 17 00:00:00 2001 From: Gustavo Alves Date: Mon, 15 Jan 2024 13:13:25 +0100 Subject: [PATCH 049/109] add copyright to machine_test.go (2) --- scope/machine_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/scope/machine_test.go b/scope/machine_test.go index 5c415167..ee6f826f 100644 --- a/scope/machine_test.go +++ b/scope/machine_test.go @@ -13,6 +13,7 @@ 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 scope import ( From 6bf54b276bcdb9004a1bf563feaf49eba081ac6e Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 13:31:17 +0100 Subject: [PATCH 050/109] fix license --- scope/machine_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scope/machine_test.go b/scope/machine_test.go index ee6f826f..38c42e92 100644 --- a/scope/machine_test.go +++ b/scope/machine_test.go @@ -5,7 +5,7 @@ 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 + 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, From 92dab43c2e1f73658ecc47870c29c09a767f9b3a Mon Sep 17 00:00:00 2001 From: Gustavo Alves Date: Mon, 15 Jan 2024 13:47:47 +0100 Subject: [PATCH 051/109] update some tests --- api/v1alpha1/ionoscloudmachine_types_test.go | 107 +++++++++---------- api/v1alpha1/suite_test.go | 3 + 2 files changed, 52 insertions(+), 58 deletions(-) diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index 9e94a476..08668c2e 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -28,20 +28,6 @@ import ( ) func defaultMachine() *IonosCloudMachine { - return &IonosCloudMachine{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-machine", - Namespace: metav1.NamespaceDefault, - }, - Spec: IonosCloudMachineSpec{ - ProviderID: "ionos://ee090ff2-1eef-48ec-a246-a51a33aa4f3a", - Network: &Network{}, - }, - Status: IonosCloudMachineStatus{}, - } -} - -func completeMachine() *IonosCloudMachine { return &IonosCloudMachine{ ObjectMeta: metav1.ObjectMeta{ Name: "test-machine", @@ -71,18 +57,24 @@ func completeMachine() *IonosCloudMachine { var _ = Describe("IonosCloudMachine Tests", func() { AfterEach(func() { - err := k8sClient.Delete(context.Background(), defaultMachine()) - Expect(client.IgnoreNotFound(err)).To(Succeed()) + m := &IonosCloudMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-machine", + Namespace: metav1.NamespaceDefault, + }, + } + err := k8sClient.Delete(context.Background(), m) + Expect(client.IgnoreNotFound(err)).ToNot(HaveOccurred()) }) Context("Validation", func() { It("shouldn't fail if everything is seems to be properly set", func() { - m := completeMachine() + m := defaultMachine() Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) }) It("should not fail if providerId is empty", func() { - m := completeMachine() + m := defaultMachine() want := "" m.Spec.ProviderID = want Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) @@ -91,16 +83,15 @@ var _ = Describe("IonosCloudMachine Tests", func() { When("data center id", func() { It("it should fail if dataCenterId is not a uuid", func() { - m := completeMachine() + m := defaultMachine() want := "" m.Spec.DataCenterID = want Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) - Expect(m.Spec.DataCenterID).To(Equal(want)) }) It("should be immutable", func() { - m := completeMachine() + m := defaultMachine() Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(m.Spec.DataCenterID).To(Equal(completeMachine().Spec.DataCenterID)) + Expect(m.Spec.DataCenterID).To(Equal(defaultMachine().Spec.DataCenterID)) m.Spec.DataCenterID = "6ded8c5f-8df2-46ef-b4ce-61833daf0961" Expect(k8sClient.Update(context.Background(), m)).ToNot(Succeed()) }) @@ -108,19 +99,19 @@ var _ = Describe("IonosCloudMachine Tests", func() { When("the number of cores, ", func() { It("is less than 1, it should fail", func() { - m := completeMachine() + m := defaultMachine() m.Spec.NumCores = -1 Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }) It("should have a minimum value of 1", func() { - m := completeMachine() + m := defaultMachine() want := int32(1) m.Spec.NumCores = want Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.NumCores).To(Equal(want)) }) It("isn't set, it should work and default to 1", func() { - m := completeMachine() + m := defaultMachine() // because NumCores is int32, setting the value as 0 is the same as not setting anything m.Spec.NumCores = 0 Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) @@ -130,19 +121,19 @@ var _ = Describe("IonosCloudMachine Tests", func() { When("the number of cores, ", func() { It("is less than 1, it should fail", func() { - m := completeMachine() + m := defaultMachine() m.Spec.NumCores = -1 Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }) It("should have a minimum value of 1", func() { - m := completeMachine() + m := defaultMachine() want := int32(1) m.Spec.NumCores = want Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.NumCores).To(Equal(want)) }) It("isn't set, it should work and default to 1", func() { - m := completeMachine() + m := defaultMachine() m.Spec.NumCores = 0 Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.NumCores).To(Equal(int32(1))) @@ -151,20 +142,20 @@ var _ = Describe("IonosCloudMachine Tests", func() { When("the machine availability zone", func() { It("isn't set, should default to AUTO", func() { - m := completeMachine() + m := defaultMachine() // because AvailabilityZone is a string, setting the value as "" is the same as not setting anything m.Spec.AvailabilityZone = "" Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.AvailabilityZone).To(Equal(AvailabilityZoneAuto)) }) It("it not part of the enum it should not work", func() { - m := completeMachine() + m := defaultMachine() m.Spec.AvailabilityZone = "this-should-not-work" Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }) DescribeTable("should work for these values", func(zone AvailabilityZone) { - m := completeMachine() + m := defaultMachine() m.Spec.AvailabilityZone = zone Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.AvailabilityZone).To(Equal(zone)) @@ -178,31 +169,31 @@ var _ = Describe("IonosCloudMachine Tests", func() { When("the machine memory size", func() { It("isn't set, should default to 3072MB", func() { - m := completeMachine() + m := defaultMachine() // because MemoryMB is an int32, setting the value as 0 is the same as not setting anything m.Spec.MemoryMB = 0 Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.MemoryMB).To(Equal(int32(3072))) }) It("should be at least 2048, therefore less than it should not work", func() { - m := completeMachine() + m := defaultMachine() m.Spec.MemoryMB = 1024 Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }) It("should be at least 2048, therefore 2048 should work", func() { - m := completeMachine() + m := defaultMachine() want := int32(2048) m.Spec.MemoryMB = want Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.MemoryMB).To(Equal(want)) }) It("it should be a multiple of 1024", func() { - m := completeMachine() + m := defaultMachine() m.Spec.MemoryMB = 2100 Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }) It("should be at least 2048 and a multiple of 1024, therefore 4096 should work", func() { - m := completeMachine() + m := defaultMachine() want := int32(4096) m.Spec.MemoryMB = want Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) @@ -212,7 +203,7 @@ var _ = Describe("IonosCloudMachine Tests", func() { Context("Volume", func() { It("can have an optional name", func() { - m := completeMachine() + m := defaultMachine() want := "" m.Spec.Disk.Name = want Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) @@ -220,20 +211,20 @@ var _ = Describe("IonosCloudMachine Tests", func() { }) When("the disk availability zone", func() { It("isn't set, should default to AUTO", func() { - m := completeMachine() + m := defaultMachine() // because AvailabilityZone is a string, setting the value as "" is the same as not setting anything m.Spec.Disk.AvailabilityZone = "" Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.Disk.AvailabilityZone).To(Equal(AvailabilityZoneAuto)) }) It("is not part of the enum it should not work", func() { - m := completeMachine() + m := defaultMachine() m.Spec.Disk.AvailabilityZone = "this-should-not-work" Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }) DescribeTable("should work for these values", func(zone AvailabilityZone) { - m := completeMachine() + m := defaultMachine() m.Spec.Disk.AvailabilityZone = zone Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.Disk.AvailabilityZone).To(Equal(zone)) @@ -245,39 +236,39 @@ var _ = Describe("IonosCloudMachine Tests", func() { ) }) It("can be created without SSH keys", func() { - m := completeMachine() + m := defaultMachine() var want []string m.Spec.Disk.SSHKeys = want Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.Disk.SSHKeys).To(Equal(want)) }) It("should prevent setting identical SSH keys", func() { - m := completeMachine() + m := defaultMachine() m.Spec.Disk.SSHKeys = []string{"Key1", "Key1", "Key2", "Key3"} - Expect(k8sClient.Create(context.Background(), m)).To(HaveOccurred()) + Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }) When("the disk size (in GB)", func() { It("is less than 5, it should fail", func() { - m := completeMachine() + m := defaultMachine() m.Spec.Disk.SizeGB = 4 Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }) It("it is not set, it should default to 5", func() { - m := completeMachine() + m := defaultMachine() // Because disk size is an int, setting it as 0 is the same as not setting anything m.Spec.Disk.SizeGB = 0 Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.Disk.SizeGB).To(Equal(5)) }) - It("It should be at least 5; therefore 5 should work", func() { - m := completeMachine() + It("should be at least 5; therefore 5 should work", func() { + m := defaultMachine() want := 5 m.Spec.Disk.SizeGB = want Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.Disk.SizeGB).To(Equal(want)) }) - It("It should be at least 5; therefore 6 should work", func() { - m := completeMachine() + It("should be at least 5; therefore 6 should work", func() { + m := defaultMachine() want := 6 m.Spec.Disk.SizeGB = want Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) @@ -286,20 +277,20 @@ var _ = Describe("IonosCloudMachine Tests", func() { }) When("the disk type", func() { It("isn't set, should default to HDD", func() { - m := completeMachine() + m := defaultMachine() // because DiskType is a string, setting the value as "" is the same as not setting anything m.Spec.Disk.DiskType = "" Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.Disk.DiskType).To(Equal(VolumeDiskTypeHDD)) }) It("is not part of the enum it should not work", func() { - m := completeMachine() + m := defaultMachine() m.Spec.Disk.AvailabilityZone = "tape" Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }) DescribeTable("should work for these values", func(diskType VolumeDiskType) { - m := completeMachine() + m := defaultMachine() m.Spec.Disk.DiskType = diskType Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.Disk.DiskType).To(Equal(diskType)) @@ -312,13 +303,13 @@ var _ = Describe("IonosCloudMachine Tests", func() { }) Context("Network", func() { It("network config should be optional", func() { - m := completeMachine() + m := defaultMachine() m.Spec.Network = nil Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.Network).To(BeNil()) }) - It("If UseDHCP is not set, it should default to true", func() { - m := completeMachine() + It("if UseDHCP is not set, it should default to true", func() { + m := defaultMachine() m.Spec.Network.UseDHCP = nil Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.Network.UseDHCP).ToNot(BeNil()) @@ -326,7 +317,7 @@ var _ = Describe("IonosCloudMachine Tests", func() { }) DescribeTable("if set UseDHCP can be", func(useDHCP *bool) { - m := completeMachine() + m := defaultMachine() m.Spec.Network.UseDHCP = useDHCP Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.Network.UseDHCP).To(Equal(useDHCP)) @@ -334,8 +325,8 @@ var _ = Describe("IonosCloudMachine Tests", func() { Entry("true", ptr.To(true)), Entry("false", ptr.To(false)), ) - It("reserved ips should be optional", func() { - m := completeMachine() + It("reserved IPs should be optional", func() { + m := defaultMachine() m.Spec.Network.IPs = nil Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.Network.IPs).To(BeNil()) diff --git a/api/v1alpha1/suite_test.go b/api/v1alpha1/suite_test.go index efc2b39e..812201fa 100644 --- a/api/v1alpha1/suite_test.go +++ b/api/v1alpha1/suite_test.go @@ -47,6 +47,9 @@ var _ = BeforeSuite(func() { CRDDirectoryPaths: []string{ filepath.Join("..", "..", "config", "crd", "bases"), }, + // NOTE(gfariasalves): To be removed after I finish the PR comments + BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", "1.28.0-linux-amd64"), + ErrorIfCRDPathMissing: true, } From 2328d04b8500a8967b753804c8e1002d858a595a Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 13:54:03 +0100 Subject: [PATCH 052/109] change api function to not return a pointer to a slice --- internal/ionoscloud/client.go | 2 +- internal/ionoscloud/client/client.go | 13 +++++-- internal/ionoscloud/clienttest/mock_client.go | 14 ++++---- internal/service/cloud/network.go | 4 +-- internal/service/cloud/request.go | 36 ------------------- 5 files changed, 20 insertions(+), 49 deletions(-) delete mode 100644 internal/service/cloud/request.go diff --git a/internal/ionoscloud/client.go b/internal/ionoscloud/client.go index cb43ee16..7b8bd18c 100644 --- a/internal/ionoscloud/client.go +++ b/internal/ionoscloud/client.go @@ -64,5 +64,5 @@ type Client interface { // WaitForRequest waits for the completion of the provided request, return an error if it fails. WaitForRequest(ctx context.Context, requestURL string) error // GetRequests returns the requests made in the last 24 hours that match the provided method and path. - GetRequests(ctx context.Context, method, path string) (*[]ionoscloud.Request, error) + GetRequests(ctx context.Context, method, path string) ([]ionoscloud.Request, error) } diff --git a/internal/ionoscloud/client/client.go b/internal/ionoscloud/client/client.go index b21914d1..59d125f4 100644 --- a/internal/ionoscloud/client/client.go +++ b/internal/ionoscloud/client/client.go @@ -297,7 +297,7 @@ func (c *IonosCloudClient) CheckRequestStatus(ctx context.Context, requestURL st } // GetRequests returns the requests made in the last 24 hours that match the provided method and path. -func (c *IonosCloudClient) GetRequests(ctx context.Context, method, path string) (*[]sdk.Request, error) { +func (c *IonosCloudClient) GetRequests(ctx context.Context, method, path string) ([]sdk.Request, error) { if path == "" { return nil, errors.New("path needs to be provided") } @@ -305,7 +305,8 @@ func (c *IonosCloudClient) GetRequests(ctx context.Context, method, path string) return nil, errors.New("method needs to be provided") } - lookback := time.Now().Add(-24 * time.Hour).Format(time.DateTime) + const defaultLookbackTime = 24 * time.Hour + lookback := time.Now().Add(-defaultLookbackTime).Format(time.DateTime) reqs, _, err := c.API.RequestsApi.RequestsGet(ctx). Depth(3). FilterMethod(method). @@ -315,13 +316,19 @@ func (c *IonosCloudClient) GetRequests(ctx context.Context, method, path string) if err != nil { return nil, fmt.Errorf("failed to get requests: %w", err) } + if reqs.Items == nil { + // NOTE(lubedacht): This shouldn't happen, but we shouldn't deref + // a pointer without a nil check + return nil, nil + } + items := *reqs.Items slices.SortFunc(items, func(a, b sdk.Request) int { // We invert the value to sort in descending order return -a.Metadata.CreatedDate.Compare(b.Metadata.CreatedDate.Time) }) - return &items, nil + return items, nil } // WaitForRequest waits for the completion of the provided request, return an error if it fails. diff --git a/internal/ionoscloud/clienttest/mock_client.go b/internal/ionoscloud/clienttest/mock_client.go index c8be0776..a9960347 100644 --- a/internal/ionoscloud/clienttest/mock_client.go +++ b/internal/ionoscloud/clienttest/mock_client.go @@ -570,19 +570,19 @@ func (_c *MockClient_GetLAN_Call) RunAndReturn(run func(context.Context, string, } // GetRequests provides a mock function with given fields: ctx, method, path -func (_m *MockClient) GetRequests(ctx context.Context, method string, path string) (*[]ionoscloud.Request, error) { +func (_m *MockClient) GetRequests(ctx context.Context, method string, path string) ([]ionoscloud.Request, error) { ret := _m.Called(ctx, method, path) - var r0 *[]ionoscloud.Request + var r0 []ionoscloud.Request var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) (*[]ionoscloud.Request, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]ionoscloud.Request, error)); ok { return rf(ctx, method, path) } - if rf, ok := ret.Get(0).(func(context.Context, string, string) *[]ionoscloud.Request); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string) []ionoscloud.Request); ok { r0 = rf(ctx, method, path) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*[]ionoscloud.Request) + r0 = ret.Get(0).([]ionoscloud.Request) } } @@ -615,12 +615,12 @@ func (_c *MockClient_GetRequests_Call) Run(run func(ctx context.Context, method return _c } -func (_c *MockClient_GetRequests_Call) Return(_a0 *[]ionoscloud.Request, _a1 error) *MockClient_GetRequests_Call { +func (_c *MockClient_GetRequests_Call) Return(_a0 []ionoscloud.Request, _a1 error) *MockClient_GetRequests_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *MockClient_GetRequests_Call) RunAndReturn(run func(context.Context, string, string) (*[]ionoscloud.Request, error)) *MockClient_GetRequests_Call { +func (_c *MockClient_GetRequests_Call) RunAndReturn(run func(context.Context, string, string) ([]ionoscloud.Request, error)) *MockClient_GetRequests_Call { _c.Call.Return(run) return _c } diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index d04f72d9..4ebe099b 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -221,9 +221,9 @@ func (s *Service) checkForPendingLANRequest(method string, lanID string) (status } lanPath := path.Join("datacenters", s.dataCenterID(), "lan") - requests, err := s.findRelatedRequests(method, lanPath) + requests, err := s.api().GetRequests(s.ctx, method, lanPath) if err != nil { - return "", err + return "", fmt.Errorf("could not get requests: %w", err) } for _, r := range requests { diff --git a/internal/service/cloud/request.go b/internal/service/cloud/request.go deleted file mode 100644 index 11b4fc97..00000000 --- a/internal/service/cloud/request.go +++ /dev/null @@ -1,36 +0,0 @@ -/* -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 cloud - -import ( - "fmt" - - sdk "github.com/ionos-cloud/sdk-go/v6" -) - -func (s *Service) findRelatedRequests(method, resourcePath string) ([]sdk.Request, error) { - requests, err := s.api().GetRequests(s.ctx, method, resourcePath) - if err != nil { - return nil, fmt.Errorf("could not get requests: %w", err) - } - - if requests == nil { - return nil, nil - } - - return *requests, nil -} From c62e1fa02276de484034e9a0191bce676e75431e Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 15:00:17 +0100 Subject: [PATCH 053/109] fixed incorrect initialism naming --- api/v1alpha1/ionoscloudmachine_types.go | 10 +++++----- ...tructure.cluster.x-k8s.io_ionoscloudmachines.yaml | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index 80048d47..cbafd512 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -77,13 +77,13 @@ 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 datacenter where the machine should be created in. - // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="datacenterId is immutable" + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="datacenterID is immutable" // +kubebuilder:validation:Type=string // +kubebuilder:validation:Format=uuid - DataCenterID string `json:"datacenterId"` + DataCenterID string `json:"datacenterID"` // NumCores defines the number of cores for the VM. // +kubebuilder:validation:Minimum=1 @@ -104,7 +104,7 @@ type IonosCloudMachineSpec struct { // +kubebuilder:validation:Minimum=2048 // +kubebuilder:default=3072 // +optional - MemoryMB int32 `json:"memoryMb,omitempty"` + MemoryMB int32 `json:"memoryMB,omitempty"` // CPUFamily defines the CPU architecture, which will be used for this VM. // The not all CPU architectures are available in all datacenters. @@ -132,7 +132,7 @@ type Network struct { // therefore dhcp must be set to true. // +kubebuilder:default=true // +optional - UseDHCP *bool `json:"useDhcp,omitempty"` + UseDHCP *bool `json:"useDHCP,omitempty"` } // Volume is the physical storage on the machine. 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 9fd5095a..65626799 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml @@ -50,13 +50,13 @@ spec: all datacenters. example: AMD_OPTERON type: string - datacenterId: + datacenterID: description: DataCenterID is the ID of the datacenter where the machine should be created in. format: uuid type: string x-kubernetes-validations: - - message: datacenterId is immutable + - message: datacenterID is immutable rule: self == oldSelf disk: description: Disk defines the boot volume of the VM. @@ -95,7 +95,7 @@ spec: type: array x-kubernetes-list-type: set type: object - memoryMb: + memoryMB: default: 3072 description: MemoryMB is the memory size for the VM in MB. Size must be specified in multiples of 256 MB with a minimum of 1024 MB which @@ -114,7 +114,7 @@ spec: type: string type: array x-kubernetes-list-type: set - useDhcp: + useDHCP: default: true description: UseDHCP sets whether DHCP should be used or not. NOTE(lubedacht) currently we do not support private clusters @@ -127,13 +127,13 @@ spec: format: int32 minimum: 1 type: integer - providerId: + providerID: description: ProviderID is the IONOS Cloud provider ID will be in the format ionos://ee090ff2-1eef-48ec-a246-a51a33aa4f3a type: string required: - cpuFamily - - datacenterId + - datacenterID - disk type: object status: From ced5cd2315a6a0dcd5cf7647c272a2462f715cde Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 15:08:51 +0100 Subject: [PATCH 054/109] regroup interface function --- internal/ionoscloud/client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/ionoscloud/client.go b/internal/ionoscloud/client.go index 7b8bd18c..8e7374ba 100644 --- a/internal/ionoscloud/client.go +++ b/internal/ionoscloud/client.go @@ -53,14 +53,14 @@ type Client interface { GetLAN(ctx context.Context, dataCenterID, lanID string) (*ionoscloud.Lan, error) // DeleteLAN deletes the LAN that matches the provided lanID in the specified data center, returning the request location. DeleteLAN(ctx context.Context, dataCenterID, lanID string) (string, error) - // CheckRequestStatus checks the status of a provided request identified by requestID - CheckRequestStatus(ctx context.Context, requestID string) (*ionoscloud.RequestStatus, error) // ListVolumes returns a list of volumes in a specified data center. ListVolumes(ctx context.Context, dataCenterID string) (*ionoscloud.Volumes, error) // GetVolume returns the volume that matches volumeID in the specified data center. GetVolume(ctx context.Context, dataCenterID, volumeID string) (*ionoscloud.Volume, error) // DestroyVolume deletes the volume that matches volumeID in the specified data center. DestroyVolume(ctx context.Context, dataCenterID, volumeID string) error + // CheckRequestStatus checks the status of a provided request identified by requestID + CheckRequestStatus(ctx context.Context, requestID string) (*ionoscloud.RequestStatus, error) // WaitForRequest waits for the completion of the provided request, return an error if it fails. WaitForRequest(ctx context.Context, requestURL string) error // GetRequests returns the requests made in the last 24 hours that match the provided method and path. From 5d654722f5ce399ebc9b30422c43ef1b3dc3e82c Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 15:10:21 +0100 Subject: [PATCH 055/109] consistent docs --- internal/ionoscloud/client.go | 2 +- internal/ionoscloud/client/client.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/ionoscloud/client.go b/internal/ionoscloud/client.go index 8e7374ba..f30d9333 100644 --- a/internal/ionoscloud/client.go +++ b/internal/ionoscloud/client.go @@ -61,7 +61,7 @@ type Client interface { DestroyVolume(ctx context.Context, dataCenterID, volumeID string) error // CheckRequestStatus checks the status of a provided request identified by requestID CheckRequestStatus(ctx context.Context, requestID string) (*ionoscloud.RequestStatus, error) - // WaitForRequest waits for the completion of the provided request, return an error if it fails. + // WaitForRequest waits for the completion of the provided request. WaitForRequest(ctx context.Context, requestURL string) error // GetRequests returns the requests made in the last 24 hours that match the provided method and path. GetRequests(ctx context.Context, method, path string) ([]ionoscloud.Request, error) diff --git a/internal/ionoscloud/client/client.go b/internal/ionoscloud/client/client.go index 59d125f4..3e816ae8 100644 --- a/internal/ionoscloud/client/client.go +++ b/internal/ionoscloud/client/client.go @@ -331,7 +331,7 @@ func (c *IonosCloudClient) GetRequests(ctx context.Context, method, path string) return items, nil } -// WaitForRequest waits for the completion of the provided request, return an error if it fails. +// WaitForRequest waits for the completion of the provided request. func (c *IonosCloudClient) WaitForRequest(ctx context.Context, requestURL string) error { if requestURL == "" { return errRequestURLIsEmpty From 955536cff2d7f28496b68245684e8bb2e1ab1b05 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 15:20:19 +0100 Subject: [PATCH 056/109] check for location header in delete function --- internal/ionoscloud/client/client.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/ionoscloud/client/client.go b/internal/ionoscloud/client/client.go index 3e816ae8..30dcc4de 100644 --- a/internal/ionoscloud/client/client.go +++ b/internal/ionoscloud/client/client.go @@ -237,7 +237,10 @@ func (c *IonosCloudClient) DeleteLAN(ctx context.Context, dataCenterID, lanID st if err != nil { return "", fmt.Errorf(apiCallErrWrapper, err) } - return req.Header.Get("Location"), nil + if location := req.Header.Get("Location"); location != "" { + return location, nil + } + return "", errors.New(apiNoLocationErrMessage) } // ListVolumes returns a list of volumes in the specified data center. From 6a6380f4da5c557506cea7bd62eabfdedef321bc Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 15:22:31 +0100 Subject: [PATCH 057/109] use template text for error --- internal/ionoscloud/client/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/ionoscloud/client/client.go b/internal/ionoscloud/client/client.go index 30dcc4de..22780bc2 100644 --- a/internal/ionoscloud/client/client.go +++ b/internal/ionoscloud/client/client.go @@ -317,7 +317,7 @@ func (c *IonosCloudClient) GetRequests(ctx context.Context, method, path string) FilterCreatedAfter(lookback). Execute() if err != nil { - return nil, fmt.Errorf("failed to get requests: %w", err) + return nil, fmt.Errorf(apiCallErrWrapper, err) } if reqs.Items == nil { // NOTE(lubedacht): This shouldn't happen, but we shouldn't deref From 80925238a59c0d2b6285071152bb3cecdc059ed4 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 15:35:12 +0100 Subject: [PATCH 058/109] inverted comparison --- internal/ionoscloud/client/client.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/ionoscloud/client/client.go b/internal/ionoscloud/client/client.go index 22780bc2..aef94a51 100644 --- a/internal/ionoscloud/client/client.go +++ b/internal/ionoscloud/client/client.go @@ -29,6 +29,10 @@ import ( "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud" ) +const ( + depthRequestsMetadataStatusMetadata = 2 // for LISTing requests and their metadata status metadata +) + // IonosCloudClient is a concrete implementation of the Client interface defined in the internal client package, that // communicates with Cloud API using its SDK. type IonosCloudClient struct { @@ -311,7 +315,7 @@ func (c *IonosCloudClient) GetRequests(ctx context.Context, method, path string) const defaultLookbackTime = 24 * time.Hour lookback := time.Now().Add(-defaultLookbackTime).Format(time.DateTime) reqs, _, err := c.API.RequestsApi.RequestsGet(ctx). - Depth(3). + Depth(depthRequestsMetadataStatusMetadata). FilterMethod(method). FilterUrl(path). FilterCreatedAfter(lookback). @@ -327,8 +331,7 @@ func (c *IonosCloudClient) GetRequests(ctx context.Context, method, path string) items := *reqs.Items slices.SortFunc(items, func(a, b sdk.Request) int { - // We invert the value to sort in descending order - return -a.Metadata.CreatedDate.Compare(b.Metadata.CreatedDate.Time) + return b.Metadata.CreatedDate.Compare(a.Metadata.CreatedDate.Time) }) return items, nil From e74e0a86985491fd1bf8779cb68f9e697cc7549f Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 15:44:08 +0100 Subject: [PATCH 059/109] improve loop --- internal/service/cloud/network.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index 4ebe099b..4558e5bb 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -95,8 +95,9 @@ func (s *Service) GetLAN() (*sdk.Lan, error) { return nil, fmt.Errorf("could not list LANs in datacenter %s: %w", s.dataCenterID(), err) } + expectedName := s.LANName() for _, l := range *lans.Items { - if name := l.Properties.Name; name != nil && *l.Properties.Name == s.LANName() { + if l.Properties.HasName() && *l.Properties.Name == expectedName { return &l, nil } } From 1abb85db8608e0e12af49421031067737177da38 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 15:56:46 +0100 Subject: [PATCH 060/109] added additional check for multiple LANs of the same name --- internal/service/cloud/network.go | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index 4558e5bb..c5ef009b 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -95,13 +95,27 @@ func (s *Service) GetLAN() (*sdk.Lan, error) { return nil, fmt.Errorf("could not list LANs in datacenter %s: %w", s.dataCenterID(), err) } - expectedName := s.LANName() + var ( + expectedName = s.LANName() + lanCount = 0 + foundLAN *sdk.Lan + ) + for _, l := range *lans.Items { if l.Properties.HasName() && *l.Properties.Name == expectedName { - return &l, nil + l := l + foundLAN = &l + lanCount++ + } + + // If there are multiple LANs with the same name, we should return an error. + // Our logic won't be able to proceed as we cannot select the correct lan. + if lanCount > 1 { + return nil, fmt.Errorf("found multiple LANs with the name: %s", expectedName) } } - return nil, nil + + return foundLAN, nil } // ReconcileLANDeletion ensures there's no cluster LAN available, requesting for deletion (if no other resource From a9a3ee1d10633efe0345f3db6f4b8e5739104d0b Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 15:59:06 +0100 Subject: [PATCH 061/109] use correct request path --- internal/service/cloud/network.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index c5ef009b..f96558b0 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -235,7 +235,7 @@ func (s *Service) checkForPendingLANRequest(method string, lanID string) (status )) } - lanPath := path.Join("datacenters", s.dataCenterID(), "lan") + lanPath := path.Join("datacenters", s.dataCenterID(), "lans") requests, err := s.api().GetRequests(s.ctx, method, lanPath) if err != nil { return "", fmt.Errorf("could not get requests: %w", err) From 849d9f91c6f1c631c0ec9b8f448309dc01c47295 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 16:09:52 +0100 Subject: [PATCH 062/109] check Targets slice --- internal/service/cloud/network.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index f96558b0..617d14d2 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -243,8 +243,12 @@ func (s *Service) checkForPendingLANRequest(method string, lanID string) (status for _, r := range requests { if method != http.MethodPost { - id := *(*r.Metadata.RequestStatus.Metadata.Targets)[0].Target.Id - if id != lanID { + targets := *r.Metadata.RequestStatus.Metadata.Targets + if targets == nil { + continue + } + + if *targets[0].Target.Id != lanID { continue } } else { From f03ee4f38c96369e5096a5affb379958ab80f15d Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 16:12:21 +0100 Subject: [PATCH 063/109] call before loop --- internal/service/cloud/network.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index 617d14d2..baee995c 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -241,6 +241,7 @@ func (s *Service) checkForPendingLANRequest(method string, lanID string) (status return "", fmt.Errorf("could not get requests: %w", err) } + expectedLANName := s.LANName() for _, r := range requests { if method != http.MethodPost { targets := *r.Metadata.RequestStatus.Metadata.Targets @@ -257,7 +258,7 @@ func (s *Service) checkForPendingLANRequest(method string, lanID string) (status if err != nil { return "", fmt.Errorf("could not unmarshal request into LAN: %w", err) } - if *lan.Properties.Name != s.LANName() { + if *lan.Properties.Name != expectedLANName { continue } } From b7c2732661a95c87308babc7bb9ef82b097a6384 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 16:33:47 +0100 Subject: [PATCH 064/109] update logging messages --- internal/controller/ionoscloudcluster_controller.go | 4 ++-- internal/controller/ionoscloudmachine_controller.go | 4 ++-- internal/service/cloud/network.go | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/controller/ionoscloudcluster_controller.go b/internal/controller/ionoscloudcluster_controller.go index 944ad60e..b0af388a 100644 --- a/internal/controller/ionoscloudcluster_controller.go +++ b/internal/controller/ionoscloudcluster_controller.go @@ -78,7 +78,7 @@ func (r *IonosCloudClusterReconciler) Reconcile(ctx context.Context, req ctrl.Re } if cluster == nil { - logger.Info("Waiting for cluster controller to set OwnerRef on IonosCloudCluster") + logger.Info("waiting for cluster controller to set OwnerRef on IonosCloudCluster") return ctrl.Result{}, nil } @@ -86,7 +86,7 @@ func (r *IonosCloudClusterReconciler) Reconcile(ctx context.Context, req ctrl.Re ctx = ctrl.LoggerInto(ctx, logger) if annotations.IsPaused(cluster, ionosCloudCluster) { - logger.Info("Either IonosCloudCluster or owner cluster is marked as paused. Reconciliation is skipped") + logger.Info("either IonosCloudCluster or owner cluster is marked as paused. Reconciliation is skipped") return ctrl.Result{}, nil } diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index 628efaa5..b86ba413 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -170,10 +170,10 @@ func (r *IonosCloudMachineReconciler) isInfrastructureReady(machineScope *scope. } func (r *IonosCloudMachineReconciler) reconcileNormal(machineScope *scope.MachineScope, _ *scope.ClusterScope, cloudService *cloud.Service) (ctrl.Result, error) { - machineScope.V(4).Info("Reconciling IonosCloudMachine") + machineScope.V(4).Info("reconciling IonosCloudMachine") if machineScope.HasFailed() { - machineScope.Info("Error state detected, skipping reconciliation") + machineScope.Info("error state detected, skipping reconciliation") return ctrl.Result{}, nil } diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index baee995c..74acb883 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -58,6 +58,7 @@ func (s *Service) ReconcileLAN() (requeue bool, err error) { // We want to requeue and check again after some time if requestStatus == sdk.RequestStatusRunning || requestStatus == sdk.RequestStatusQueued { + log.Info("request is ongoing, re-triggering reconciliaton", "request status", requestStatus) return true, nil } @@ -78,7 +79,7 @@ func (s *Service) ReconcileLAN() (requeue bool, err error) { // is bigger than the created time of the LAN POST request. } - log.V(4).Info("No LAN was found. Creating new LAN") + log.V(4).Info("no LAN was found. Creating new LAN") if err := s.createLAN(); err != nil { return false, err } @@ -264,9 +265,8 @@ func (s *Service) checkForPendingLANRequest(method string, lanID string) (status } status := *r.Metadata.RequestStatus.Metadata.Status - if status == sdk.RequestStatusFailed { - // We just log the error but not return it, so we can retry the request. + // We just log the error but do not return it, so we can retry the request. message := r.Metadata.RequestStatus.Metadata.Message s.scope.Logger.WithValues("requestID", r.Id, "requestStatus", status). Error(errors.New(*message), "last request for LAN has failed. logging it for debugging purposes") From e05b05c80d85fbd85c1d8e3f060a976a44250e88 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 16:37:26 +0100 Subject: [PATCH 065/109] update logger names --- internal/service/cloud/network.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index 74acb883..92d5b3d7 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -172,7 +172,7 @@ func (s *Service) ReconcileLANDeletion() (requeue bool, err error) { } func (s *Service) createLAN() error { - log := s.scope.Logger.WithName("CreateLAN") + log := s.scope.Logger.WithName("createLAN") requestPath, err := s.api().CreateLAN(s.ctx, s.dataCenterID(), sdk.LanPropertiesPost{ Name: ptr.To(s.LANName()), @@ -199,7 +199,7 @@ func (s *Service) createLAN() error { } func (s *Service) deleteLAN(lanID string) error { - log := s.scope.Logger.WithName("DeleteLAN") + log := s.scope.Logger.WithName("deleteLAN") requestPath, err := s.api().DeleteLAN(s.ctx, s.dataCenterID(), lanID) if err != nil { From 5091316d5d21519cd4309e0a0e734ba49b476eee Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 16:46:21 +0100 Subject: [PATCH 066/109] fix typo --- internal/controller/ionoscloudmachine_controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index b86ba413..9c72b441 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -258,7 +258,7 @@ func (r *IonosCloudMachineReconciler) getClusterScope( IonosClient: r.IonosCloudClient, }) if err != nil { - return nil, fmt.Errorf("failed to creat cluster scope: %w", err) + return nil, fmt.Errorf("failed to create cluster scope: %w", err) } return clusterScope, nil From 5d1ac9d05265b4e97b0001840f744f3d5ce2370f Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 16:48:15 +0100 Subject: [PATCH 067/109] wrapped log line into error --- internal/controller/ionoscloudmachine_controller.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index 9c72b441..bf6e855a 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -119,8 +119,7 @@ func (r *IonosCloudMachineReconciler) Reconcile(ctx context.Context, req ctrl.Re Logger: &logger, }) if err != nil { - logger.Error(err, "failed to create scope") - return ctrl.Result{}, err + return ctrl.Result{}, fmt.Errorf("failed to create scope: %w", err) } defer func() { From 1b7ab428f07d89ecf0d67e240a7f6a43fae6c205 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 15 Jan 2024 17:13:52 +0100 Subject: [PATCH 068/109] added importas for ionos sdk --- .golangci.yml | 2 ++ .mockery.yaml | 2 +- cmd/main.go | 6 +++--- internal/ionoscloud/client.go | 38 +++++++++++++++++------------------ 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 1fc43331..433e465e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -69,6 +69,8 @@ linters-settings: # 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 loggercheck: require-string-key: true no-printf-like: true diff --git a/.mockery.yaml b/.mockery.yaml index bffe1989..35655e19 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -10,4 +10,4 @@ packages: configs: - filename: "mock_{{ .InterfaceName | snakecase }}.go" dir: "{{.InterfaceDir}}/clienttest" - outpkg: "client_test" \ No newline at end of file + outpkg: "client_test" diff --git a/cmd/main.go b/cmd/main.go index bcb053b6..962a33b8 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -22,7 +22,7 @@ import ( "flag" "os" - ionoscloud "github.com/ionos-cloud/sdk-go/v6" + sdk "github.com/ionos-cloud/sdk-go/v6" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" @@ -88,8 +88,8 @@ func main() { os.Exit(1) } ionosCloudClient, err := icc.NewClient( - os.Getenv(ionoscloud.IonosUsernameEnvVar), os.Getenv(ionoscloud.IonosPasswordEnvVar), - os.Getenv(ionoscloud.IonosTokenEnvVar), os.Getenv(ionoscloud.IonosApiUrlEnvVar)) + os.Getenv(sdk.IonosUsernameEnvVar), os.Getenv(sdk.IonosPasswordEnvVar), + os.Getenv(sdk.IonosTokenEnvVar), os.Getenv(sdk.IonosApiUrlEnvVar)) if err != nil { setupLog.Error(err, "could not create IONOS client") os.Exit(1) diff --git a/internal/ionoscloud/client.go b/internal/ionoscloud/client.go index f30d9333..afeb7f20 100644 --- a/internal/ionoscloud/client.go +++ b/internal/ionoscloud/client.go @@ -20,49 +20,49 @@ package ionoscloud import ( "context" - ionoscloud "github.com/ionos-cloud/sdk-go/v6" + sdk "github.com/ionos-cloud/sdk-go/v6" ) // Client is an interface for abstracting Cloud API SDK, making it possible to create mocks for testing purposes. type Client interface { // CreateDataCenter creates a new data center with its specification based on provided properties. - CreateDataCenter(ctx context.Context, properties ionoscloud.DatacenterProperties) ( - *ionoscloud.Datacenter, error) + CreateDataCenter(ctx context.Context, properties sdk.DatacenterProperties) ( + *sdk.Datacenter, error) // GetDataCenter returns the data center that matches the provided datacenterID. - GetDataCenter(ctx context.Context, dataCenterID string) (*ionoscloud.Datacenter, error) + GetDataCenter(ctx context.Context, dataCenterID string) (*sdk.Datacenter, error) // CreateServer creates a new server with provided properties in the specified data center. - CreateServer(ctx context.Context, dataCenterID string, properties ionoscloud.ServerProperties) ( - *ionoscloud.Server, error) + CreateServer(ctx context.Context, dataCenterID string, properties sdk.ServerProperties) ( + *sdk.Server, error) // ListServers returns a list with the servers in the specified data center. - ListServers(ctx context.Context, dataCenterID string) (*ionoscloud.Servers, error) + ListServers(ctx context.Context, dataCenterID string) (*sdk.Servers, error) // GetServer returns the server that matches the provided serverID in the specified data center. - GetServer(ctx context.Context, dataCenterID, serverID string) (*ionoscloud.Server, error) + GetServer(ctx context.Context, dataCenterID, serverID string) (*sdk.Server, error) // DestroyServer deletes the server that matches the provided serverID in the specified data center. DestroyServer(ctx context.Context, dataCenterID, serverID string) error // CreateLAN creates a new LAN with the provided properties in the specified data center, returning the request location. - CreateLAN(ctx context.Context, dataCenterID string, properties ionoscloud.LanPropertiesPost) (string, error) + CreateLAN(ctx context.Context, dataCenterID string, properties sdk.LanPropertiesPost) (string, error) // UpdateLAN updates a LAN with the provided properties in the specified data center. - UpdateLAN(ctx context.Context, dataCenterID string, lanID string, properties ionoscloud.LanProperties) ( - *ionoscloud.Lan, error) + UpdateLAN(ctx context.Context, dataCenterID string, lanID string, properties sdk.LanProperties) ( + *sdk.Lan, error) // AttachToLAN attaches a provided NIC to a provided LAN in a specified data center. - AttachToLAN(ctx context.Context, dataCenterID, lanID string, nic ionoscloud.Nic) ( - *ionoscloud.Nic, error) + AttachToLAN(ctx context.Context, dataCenterID, lanID string, nic sdk.Nic) ( + *sdk.Nic, error) // ListLANs returns a list of LANs in the specified data center. - ListLANs(ctx context.Context, dataCenterID string) (*ionoscloud.Lans, error) + ListLANs(ctx context.Context, dataCenterID string) (*sdk.Lans, error) // GetLAN returns the LAN that matches lanID in the specified data center. - GetLAN(ctx context.Context, dataCenterID, lanID string) (*ionoscloud.Lan, error) + GetLAN(ctx context.Context, dataCenterID, lanID string) (*sdk.Lan, error) // DeleteLAN deletes the LAN that matches the provided lanID in the specified data center, returning the request location. DeleteLAN(ctx context.Context, dataCenterID, lanID string) (string, error) // ListVolumes returns a list of volumes in a specified data center. - ListVolumes(ctx context.Context, dataCenterID string) (*ionoscloud.Volumes, error) + ListVolumes(ctx context.Context, dataCenterID string) (*sdk.Volumes, error) // GetVolume returns the volume that matches volumeID in the specified data center. - GetVolume(ctx context.Context, dataCenterID, volumeID string) (*ionoscloud.Volume, error) + GetVolume(ctx context.Context, dataCenterID, volumeID string) (*sdk.Volume, error) // DestroyVolume deletes the volume that matches volumeID in the specified data center. DestroyVolume(ctx context.Context, dataCenterID, volumeID string) error // CheckRequestStatus checks the status of a provided request identified by requestID - CheckRequestStatus(ctx context.Context, requestID string) (*ionoscloud.RequestStatus, error) + CheckRequestStatus(ctx context.Context, requestID string) (*sdk.RequestStatus, error) // WaitForRequest waits for the completion of the provided request. WaitForRequest(ctx context.Context, requestURL string) error // GetRequests returns the requests made in the last 24 hours that match the provided method and path. - GetRequests(ctx context.Context, method, path string) ([]ionoscloud.Request, error) + GetRequests(ctx context.Context, method, path string) ([]sdk.Request, error) } From a0b76fa8736061f36ad44062ef50ede123bd9817 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Tue, 16 Jan 2024 11:11:27 +0100 Subject: [PATCH 069/109] adds tests and updates to API type --- api/v1alpha1/ionoscloudmachine_types.go | 5 ++- api/v1alpha1/ionoscloudmachine_types_test.go | 39 +++++++++++++++++-- ...e.cluster.x-k8s.io_ionoscloudmachines.yaml | 6 +-- 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index cbafd512..f8523a31 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -92,7 +92,7 @@ type IonosCloudMachineSpec struct { NumCores int32 `json:"numCores,omitempty"` // AvailabilityZone is the availability zone in which the VM should be provisioned. - // +kubebuilder:validation:Enum=AUTO;ZONE_1;ZONE_2;ZONE_3 + // +kubebuilder:validation:Enum=AUTO;ZONE_1;ZONE_2 // +kubebuilder:default=AUTO // +optional AvailabilityZone AvailabilityZone `json:"availabilityZone,omitempty"` @@ -107,8 +107,9 @@ type IonosCloudMachineSpec struct { MemoryMB int32 `json:"memoryMB,omitempty"` // CPUFamily defines the CPU architecture, which will be used for this VM. - // The not all CPU architectures are available in all datacenters. + // Not all CPU architectures are available in all datacenters. // +kubebuilder:example=AMD_OPTERON + // +kubebuilder:validation:MinLength=1 CPUFamily string `json:"cpuFamily"` // Disk defines the boot volume of the VM. diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index 08668c2e..b1586c99 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -21,9 +21,11 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" - + "sigs.k8s.io/cluster-api/util/conditions" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -44,7 +46,7 @@ func defaultMachine() *IonosCloudMachine { Name: "disk", DiskType: VolumeDiskTypeSSDStandard, SizeGB: 23, - AvailabilityZone: AvailabilityZoneThree, + AvailabilityZone: AvailabilityZoneOne, SSHKeys: []string{"public-key"}, }, Network: &Network{ @@ -82,7 +84,7 @@ var _ = Describe("IonosCloudMachine Tests", func() { }) When("data center id", func() { - It("it should fail if dataCenterId is not a uuid", func() { + It("it should fail if data center ID is not a UUID", func() { m := defaultMachine() want := "" m.Spec.DataCenterID = want @@ -163,8 +165,12 @@ var _ = Describe("IonosCloudMachine Tests", func() { Entry("AUTO", AvailabilityZoneAuto), Entry("ZONE_1", AvailabilityZoneOne), Entry("ZONE_2", AvailabilityZoneTwo), - Entry("ZONE_3", AvailabilityZoneThree), ) + It("Should not work for ZONE_3", func() { + m := defaultMachine() + m.Spec.AvailabilityZone = AvailabilityZoneThree + Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) + }) }) When("the machine memory size", func() { @@ -200,6 +206,13 @@ var _ = Describe("IonosCloudMachine Tests", func() { Expect(m.Spec.MemoryMB).To(Equal(want)) }) }) + When("the machine CPU family", func() { + It("isn't set, it should fail", func() { + m := defaultMachine() + m.Spec.CPUFamily = "" + Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) + }) + }) Context("Volume", func() { It("can have an optional name", func() { @@ -332,5 +345,23 @@ var _ = Describe("IonosCloudMachine Tests", func() { Expect(m.Spec.Network.IPs).To(BeNil()) }) }) + 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 + conditions.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() + Expect(machineConditions).To(HaveLen(1)) + Expect(machineConditions[0].Type).To(Equal(MachineProvisionedCondition)) + Expect(machineConditions[0].Status).To(Equal(corev1.ConditionTrue)) + }) + }) }) }) 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 65626799..6adb0802 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml @@ -42,13 +42,13 @@ spec: - AUTO - ZONE_1 - ZONE_2 - - ZONE_3 type: string cpuFamily: description: CPUFamily defines the CPU architecture, which will be - used for this VM. The not all CPU architectures are available in - all datacenters. + used for this VM. Not all CPU architectures are available in all + datacenters. example: AMD_OPTERON + minLength: 1 type: string datacenterID: description: DataCenterID is the ID of the datacenter where the machine From d5b638b2cb4d280642c33358778052175799e7a5 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Tue, 16 Jan 2024 11:22:58 +0100 Subject: [PATCH 070/109] unify naming of data center --- api/v1alpha1/ionoscloudmachine_types.go | 6 +++--- internal/controller/ionoscloudmachine_controller.go | 2 +- internal/ionoscloud/client.go | 2 +- internal/ionoscloud/client/client.go | 2 +- internal/service/cloud/network.go | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index f8523a31..74dfef6b 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -79,7 +79,7 @@ type IonosCloudMachineSpec struct { // +optional ProviderID string `json:"providerID,omitempty"` - // DataCenterID is the ID of the datacenter where the machine should be created in. + // DataCenterID is the ID of the data center where the machine should be created in. // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="datacenterID is immutable" // +kubebuilder:validation:Type=string // +kubebuilder:validation:Format=uuid @@ -107,7 +107,7 @@ type IonosCloudMachineSpec struct { MemoryMB int32 `json:"memoryMB,omitempty"` // CPUFamily defines the CPU architecture, which will be used for this VM. - // Not all CPU architectures are available in all datacenters. + // Not all CPU architectures are available in all data centers. // +kubebuilder:example=AMD_OPTERON // +kubebuilder:validation:MinLength=1 CPUFamily string `json:"cpuFamily"` @@ -123,7 +123,7 @@ type IonosCloudMachineSpec struct { // Network contains a network config. type Network struct { // IPs is an optional set of IP addresses, which have been - // reserved in the corresponding datacenter. + // reserved in the corresponding data center. // +listType=set // +optional IPs []string `json:"ips,omitempty"` diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index bf6e855a..f654e314 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -194,7 +194,7 @@ func (r *IonosCloudMachineReconciler) reconcileNormal(machineScope *scope.Machin // * Queued, Running => Requeue the current request // * Failed => We need to discuss this, log error and continue (retry last request in the corresponding reconcile function) - // Ensure that a LAN is created in the datacenter + // Ensure that a LAN is created in the data center if requeue, err := cloudService.ReconcileLAN(); err != nil || requeue { if requeue { return ctrl.Result{RequeueAfter: defaultReconcileDuration}, err diff --git a/internal/ionoscloud/client.go b/internal/ionoscloud/client.go index afeb7f20..89ca4cad 100644 --- a/internal/ionoscloud/client.go +++ b/internal/ionoscloud/client.go @@ -28,7 +28,7 @@ type Client interface { // CreateDataCenter creates a new data center with its specification based on provided properties. CreateDataCenter(ctx context.Context, properties sdk.DatacenterProperties) ( *sdk.Datacenter, error) - // GetDataCenter returns the data center that matches the provided datacenterID. + // GetDataCenter returns the data center that matches the provided data center ID. GetDataCenter(ctx context.Context, dataCenterID string) (*sdk.Datacenter, error) // CreateServer creates a new server with provided properties in the specified data center. CreateServer(ctx context.Context, dataCenterID string, properties sdk.ServerProperties) ( diff --git a/internal/ionoscloud/client/client.go b/internal/ionoscloud/client/client.go index aef94a51..b5f3a4bd 100644 --- a/internal/ionoscloud/client/client.go +++ b/internal/ionoscloud/client/client.go @@ -78,7 +78,7 @@ func (c *IonosCloudClient) CreateDataCenter(ctx context.Context, properties sdk. return &dc, nil } -// GetDataCenter returns the data center that matches the provided datacenterID. +// GetDataCenter returns the data center that matches the provided data center ID. func (c *IonosCloudClient) GetDataCenter(ctx context.Context, id string) (*sdk.Datacenter, error) { if id == "" { return nil, errDataCenterIDIsEmpty diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index 92d5b3d7..e2023b88 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -88,12 +88,12 @@ func (s *Service) ReconcileLAN() (requeue bool, err error) { return true, nil } -// GetLAN tries to retrieve the cluster related LAN in the datacenter. +// GetLAN tries to retrieve the cluster related LAN in the data center. func (s *Service) GetLAN() (*sdk.Lan, error) { // check if the LAN exists lans, err := s.api().ListLANs(s.ctx, s.dataCenterID()) if err != nil { - return nil, fmt.Errorf("could not list LANs in datacenter %s: %w", s.dataCenterID(), err) + return nil, fmt.Errorf("could not list LANs in data center %s: %w", s.dataCenterID(), err) } var ( From 22f15bb93cd17580b2fd14e74d383b31be867077 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Tue, 16 Jan 2024 11:23:33 +0100 Subject: [PATCH 071/109] update manifests --- .../infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 6adb0802..9445b96e 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml @@ -46,12 +46,12 @@ spec: cpuFamily: description: CPUFamily defines the CPU architecture, which will be used for this VM. Not all CPU architectures are available in all - datacenters. + data centers. example: AMD_OPTERON minLength: 1 type: string datacenterID: - description: DataCenterID is the ID of the datacenter where the machine + description: DataCenterID is the ID of the data center where the machine should be created in. format: uuid type: string @@ -109,7 +109,7 @@ spec: properties: ips: description: IPs is an optional set of IP addresses, which have - been reserved in the corresponding datacenter. + been reserved in the corresponding data center. items: type: string type: array From 46bb11868151816b771d51214de7f3807fc0916e Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Tue, 16 Jan 2024 11:31:46 +0100 Subject: [PATCH 072/109] logs to uppercase --- cmd/main.go | 2 +- internal/controller/ionoscloudcluster_controller.go | 4 ++-- internal/controller/ionoscloudmachine_controller.go | 8 ++++---- internal/service/cloud/network.go | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 962a33b8..4ec78742 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -124,7 +124,7 @@ func main() { os.Exit(1) } - setupLog.Info("starting manager") + setupLog.Info("Starting manager") if err := mgr.Start(ctx); err != nil { setupLog.Error(err, "problem running manager") os.Exit(1) diff --git a/internal/controller/ionoscloudcluster_controller.go b/internal/controller/ionoscloudcluster_controller.go index b0af388a..944ad60e 100644 --- a/internal/controller/ionoscloudcluster_controller.go +++ b/internal/controller/ionoscloudcluster_controller.go @@ -78,7 +78,7 @@ func (r *IonosCloudClusterReconciler) Reconcile(ctx context.Context, req ctrl.Re } if cluster == nil { - logger.Info("waiting for cluster controller to set OwnerRef on IonosCloudCluster") + logger.Info("Waiting for cluster controller to set OwnerRef on IonosCloudCluster") return ctrl.Result{}, nil } @@ -86,7 +86,7 @@ func (r *IonosCloudClusterReconciler) Reconcile(ctx context.Context, req ctrl.Re ctx = ctrl.LoggerInto(ctx, logger) if annotations.IsPaused(cluster, ionosCloudCluster) { - logger.Info("either IonosCloudCluster or owner cluster is marked as paused. Reconciliation is skipped") + logger.Info("Either IonosCloudCluster or owner cluster is marked as paused. Reconciliation is skipped") return ctrl.Result{}, nil } diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index f654e314..4278fb1d 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -80,7 +80,7 @@ func (r *IonosCloudMachineReconciler) Reconcile(ctx context.Context, req ctrl.Re return ctrl.Result{}, err } if machine == nil { - logger.Info("machine controller has not yet set OwnerRef") + logger.Info("Machine controller has not yet set OwnerRef") return ctrl.Result{}, nil } @@ -89,7 +89,7 @@ func (r *IonosCloudMachineReconciler) Reconcile(ctx context.Context, req ctrl.Re // Fetch the Cluster. cluster, err := util.GetClusterFromMetadata(ctx, r.Client, machine.ObjectMeta) if err != nil { - logger.Info("machine is missing cluster label or cluster does not exist") + logger.Info("Machine is missing cluster label or cluster does not exist") return ctrl.Result{}, err } @@ -169,10 +169,10 @@ func (r *IonosCloudMachineReconciler) isInfrastructureReady(machineScope *scope. } func (r *IonosCloudMachineReconciler) reconcileNormal(machineScope *scope.MachineScope, _ *scope.ClusterScope, cloudService *cloud.Service) (ctrl.Result, error) { - machineScope.V(4).Info("reconciling IonosCloudMachine") + machineScope.V(4).Info("Reconciling IonosCloudMachine") if machineScope.HasFailed() { - machineScope.Info("error state detected, skipping reconciliation") + machineScope.Info("Error state detected, skipping reconciliation") return ctrl.Result{}, nil } diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index e2023b88..add18cb2 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -58,7 +58,7 @@ func (s *Service) ReconcileLAN() (requeue bool, err error) { // We want to requeue and check again after some time if requestStatus == sdk.RequestStatusRunning || requestStatus == sdk.RequestStatusQueued { - log.Info("request is ongoing, re-triggering reconciliaton", "request status", requestStatus) + log.Info("Request is ongoing, re-triggering reconciliaton", "request status", requestStatus) return true, nil } @@ -79,7 +79,7 @@ func (s *Service) ReconcileLAN() (requeue bool, err error) { // is bigger than the created time of the LAN POST request. } - log.V(4).Info("no LAN was found. Creating new LAN") + log.V(4).Info("No LAN was found. Creating new LAN") if err := s.createLAN(); err != nil { return false, err } @@ -160,7 +160,7 @@ func (s *Service) ReconcileLANDeletion() (requeue bool, err error) { } if clusterLAN != nil && len(*clusterLAN.Entities.Nics.Items) > 0 { - log.Info("the cluster LAN is still being used by another resource. skipping deletion") + log.Info("The cluster LAN is still being used by another resource. skipping deletion") return false, nil } // Request for LAN destruction From 05dad0fbaec07be608fc9a632e6b8c4240b47c3e Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Tue, 16 Jan 2024 11:36:57 +0100 Subject: [PATCH 073/109] fix search replace --- internal/controller/ionoscloudmachine_controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index 4278fb1d..6dec26b7 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -57,7 +57,7 @@ type IonosCloudMachineReconciler struct { // 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 IonosMachine object against the actual cluster state, and then +// the IonosCloudMachine object against the actual cluster state, and then // perform operations to make the cluster state reflect the state specified by // the user. // From 9ae83d8cec25b872c2550d87d6cd3440d8027420 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Tue, 16 Jan 2024 11:46:52 +0100 Subject: [PATCH 074/109] more updates --- api/v1alpha1/ionoscloudmachine_types.go | 4 ++-- api/v1alpha1/ionoscloudmachine_types_test.go | 19 ++++++------------- ...e.cluster.x-k8s.io_ionoscloudmachines.yaml | 4 ++-- internal/ionoscloud/client/client.go | 4 ++-- 4 files changed, 12 insertions(+), 19 deletions(-) diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index 74dfef6b..493092ce 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -149,8 +149,8 @@ type Volume struct { DiskType VolumeDiskType `json:"diskType,omitempty"` // SizeGB defines the size of the volume in GB - // +kubebuilder:validation:Minimum=5 - // +kubebuilder:default=5 + // +kubebuilder:validation:Minimum=10 + // +kubebuilder:default=20 // +optional SizeGB int `json:"sizeGB,omitempty"` diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index b1586c99..7faafabf 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -261,28 +261,21 @@ var _ = Describe("IonosCloudMachine Tests", func() { Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }) When("the disk size (in GB)", func() { - It("is less than 5, it should fail", func() { + It("is less than 10, it should fail", func() { m := defaultMachine() - m.Spec.Disk.SizeGB = 4 + m.Spec.Disk.SizeGB = 9 Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }) - It("it is not set, it should default to 5", func() { + It("it is not set, it should default to 20", func() { m := defaultMachine() // Because disk size is an int, setting it as 0 is the same as not setting anything m.Spec.Disk.SizeGB = 0 Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(m.Spec.Disk.SizeGB).To(Equal(5)) + Expect(m.Spec.Disk.SizeGB).To(Equal(20)) }) - It("should be at least 5; therefore 5 should work", func() { + It("should be at least 10; therefore 10 should work", func() { m := defaultMachine() - want := 5 - m.Spec.Disk.SizeGB = want - Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(m.Spec.Disk.SizeGB).To(Equal(want)) - }) - It("should be at least 5; therefore 6 should work", func() { - m := defaultMachine() - want := 6 + want := 10 m.Spec.Disk.SizeGB = want Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.Disk.SizeGB).To(Equal(want)) 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 9445b96e..082bfafa 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml @@ -83,9 +83,9 @@ spec: description: Name is the name of the volume type: string sizeGB: - default: 5 + default: 20 description: SizeGB defines the size of the volume in GB - minimum: 5 + minimum: 10 type: integer sshKeys: description: SSHKeys contains a set of public SSH keys which will diff --git a/internal/ionoscloud/client/client.go b/internal/ionoscloud/client/client.go index b5f3a4bd..5c2cc4d5 100644 --- a/internal/ionoscloud/client/client.go +++ b/internal/ionoscloud/client/client.go @@ -312,8 +312,8 @@ func (c *IonosCloudClient) GetRequests(ctx context.Context, method, path string) return nil, errors.New("method needs to be provided") } - const defaultLookbackTime = 24 * time.Hour - lookback := time.Now().Add(-defaultLookbackTime).Format(time.DateTime) + const lookbackTime = 24 * time.Hour + lookback := time.Now().Add(-lookbackTime).Format(time.DateTime) reqs, _, err := c.API.RequestsApi.RequestsGet(ctx). Depth(depthRequestsMetadataStatusMetadata). FilterMethod(method). From 618323821b9ed7084436c14ab24ffd1ee9d28418 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Tue, 16 Jan 2024 11:49:38 +0100 Subject: [PATCH 075/109] updates --- api/v1alpha1/ionoscloudmachine_types_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index 7faafabf..13df6385 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -75,7 +75,7 @@ var _ = Describe("IonosCloudMachine Tests", func() { Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) }) - It("should not fail if providerId is empty", func() { + It("should not fail if providerID is empty", func() { m := defaultMachine() want := "" m.Spec.ProviderID = want From 1c1413fe1a0d5a9bc8450e0b7e4f3ced77a1250d Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Tue, 16 Jan 2024 12:13:47 +0100 Subject: [PATCH 076/109] tidy go.mod --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 88dd44c5..82a76617 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/onsi/ginkgo/v2 v2.13.2 github.com/onsi/gomega v1.30.0 github.com/stretchr/testify v1.8.4 + k8s.io/api v0.28.4 k8s.io/apimachinery v0.28.4 k8s.io/client-go v0.28.4 k8s.io/klog/v2 v2.110.1 @@ -71,7 +72,6 @@ require ( 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/api v0.28.4 // indirect k8s.io/apiextensions-apiserver v0.28.4 // indirect k8s.io/component-base v0.28.4 // indirect k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect From f0366a919f7260cb263dfd6d7955b6b0b2f3e482 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Tue, 16 Jan 2024 12:46:51 +0100 Subject: [PATCH 077/109] remove duplicate block --- api/v1alpha1/ionoscloudmachine_types_test.go | 21 -------------------- internal/util/ptr/ptr.go | 1 + 2 files changed, 1 insertion(+), 21 deletions(-) create mode 100644 internal/util/ptr/ptr.go diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index 13df6385..2078c796 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -121,27 +121,6 @@ var _ = Describe("IonosCloudMachine Tests", func() { }) }) - When("the number of cores, ", func() { - It("is less than 1, it should fail", func() { - m := defaultMachine() - m.Spec.NumCores = -1 - Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) - }) - It("should have a minimum value of 1", func() { - m := defaultMachine() - want := int32(1) - m.Spec.NumCores = want - Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(m.Spec.NumCores).To(Equal(want)) - }) - It("isn't set, it should work and default to 1", func() { - m := defaultMachine() - m.Spec.NumCores = 0 - Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(m.Spec.NumCores).To(Equal(int32(1))) - }) - }) - When("the machine availability zone", func() { It("isn't set, should default to AUTO", func() { m := defaultMachine() diff --git a/internal/util/ptr/ptr.go b/internal/util/ptr/ptr.go new file mode 100644 index 00000000..910edea4 --- /dev/null +++ b/internal/util/ptr/ptr.go @@ -0,0 +1 @@ +package ptr From 1f18ca92be72043cc093b94c8aeccddfb9be4bd8 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Tue, 16 Jan 2024 12:54:44 +0100 Subject: [PATCH 078/109] add ptr functions to avoid using k8s utils package --- api/v1alpha1/ionoscloudmachine_types_test.go | 3 +- go.mod | 2 +- internal/service/cloud/network.go | 2 +- internal/util/ptr/ptr.go | 31 ++++++++++++++++++++ scope/machine_test.go | 2 +- 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index 2078c796..e1c51976 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -19,12 +19,13 @@ package v1alpha1 import ( "context" + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/ptr" "sigs.k8s.io/cluster-api/util/conditions" "sigs.k8s.io/controller-runtime/pkg/client" ) diff --git a/go.mod b/go.mod index 82a76617..a08a188e 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,6 @@ require ( k8s.io/apimachinery v0.28.4 k8s.io/client-go v0.28.4 k8s.io/klog/v2 v2.110.1 - k8s.io/utils v0.0.0-20240102154912-e7106e64919e sigs.k8s.io/cluster-api v1.6.0 sigs.k8s.io/controller-runtime v0.16.3 ) @@ -75,6 +74,7 @@ require ( k8s.io/apiextensions-apiserver v0.28.4 // indirect k8s.io/component-base v0.28.4 // indirect k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect + k8s.io/utils v0.0.0-20240102154912-e7106e64919e // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index add18cb2..3ae4d640 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -25,9 +25,9 @@ import ( sdk "github.com/ionos-cloud/sdk-go/v6" "k8s.io/apimachinery/pkg/util/json" - "k8s.io/utils/ptr" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" ) // LANName returns the name of the cluster LAN. diff --git a/internal/util/ptr/ptr.go b/internal/util/ptr/ptr.go index 910edea4..f72a7dc7 100644 --- a/internal/util/ptr/ptr.go +++ b/internal/util/ptr/ptr.go @@ -1 +1,32 @@ +/* +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 ptr offers generic pointer utility functions. package ptr + +// To returns a pointer to the given value. +func To[T any](v T) *T { + return &v +} + +// Deref dereferences attempts to dereference a pointer +// and returns the default value if the pointer is nil. +func Deref[T any](ptr *T, def T) T { + if ptr != nil { + return *ptr + } + return def +} diff --git a/scope/machine_test.go b/scope/machine_test.go index 38c42e92..520ab64a 100644 --- a/scope/machine_test.go +++ b/scope/machine_test.go @@ -17,12 +17,12 @@ limitations under the License. package scope import ( + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" "testing" "github.com/go-logr/logr" "github.com/stretchr/testify/require" "k8s.io/client-go/kubernetes/scheme" - "k8s.io/utils/ptr" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" capierrors "sigs.k8s.io/cluster-api/errors" "sigs.k8s.io/controller-runtime/pkg/client/fake" From 7fca15e44650e9615d8ef81be3751b34221cc32e Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Tue, 16 Jan 2024 13:21:57 +0100 Subject: [PATCH 079/109] add tests for ptr package --- internal/util/ptr/ptr_test.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 internal/util/ptr/ptr_test.go diff --git a/internal/util/ptr/ptr_test.go b/internal/util/ptr/ptr_test.go new file mode 100644 index 00000000..c1636959 --- /dev/null +++ b/internal/util/ptr/ptr_test.go @@ -0,0 +1,26 @@ +package ptr + +import ( + "github.com/stretchr/testify/require" + "testing" +) + +func TestPtr_To(t *testing.T) { + type testType struct{} + + require.IsType(t, To(testType{}), (*testType)(nil)) + require.IsType(t, To((*testType)(nil)), (**testType)(nil)) +} + +func TestPtr_Deref(t *testing.T) { + type testType struct{} + + testTypeInstance := &testType{} + // check result types + require.IsType(t, Deref(&testType{}, testType{}), testType{}) + require.IsType(t, Deref(&testTypeInstance, &testType{}), &testType{}) + // validate that deref returns default when passing a nil value + var nilTestType *testType + require.Equal(t, Deref[testType](nilTestType, testType{}), testType{}) + require.Equal(t, Deref[testType](nil, testType{}), testType{}) +} From 2feff257ea9de07b6a608fd1d4e9fb8efecbd648 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Tue, 16 Jan 2024 13:24:06 +0100 Subject: [PATCH 080/109] linter fixes --- internal/util/ptr/ptr_test.go | 3 ++- scope/machine_test.go | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/util/ptr/ptr_test.go b/internal/util/ptr/ptr_test.go index c1636959..ea14f9b3 100644 --- a/internal/util/ptr/ptr_test.go +++ b/internal/util/ptr/ptr_test.go @@ -1,8 +1,9 @@ package ptr import ( - "github.com/stretchr/testify/require" "testing" + + "github.com/stretchr/testify/require" ) func TestPtr_To(t *testing.T) { diff --git a/scope/machine_test.go b/scope/machine_test.go index 520ab64a..f265b85e 100644 --- a/scope/machine_test.go +++ b/scope/machine_test.go @@ -17,9 +17,10 @@ limitations under the License. package scope import ( - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" "testing" + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" + "github.com/go-logr/logr" "github.com/stretchr/testify/require" "k8s.io/client-go/kubernetes/scheme" From bc352a76cee349843fc46be648bb5cea6ab22143 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Tue, 16 Jan 2024 13:25:04 +0100 Subject: [PATCH 081/109] add copyright header --- internal/util/ptr/ptr_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/internal/util/ptr/ptr_test.go b/internal/util/ptr/ptr_test.go index ea14f9b3..f6e99a7f 100644 --- a/internal/util/ptr/ptr_test.go +++ b/internal/util/ptr/ptr_test.go @@ -1,3 +1,19 @@ +/* +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 ptr import ( From 78e423146c4a40bc9c2cc0a8d245240c59b0d0a6 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Tue, 16 Jan 2024 13:26:13 +0100 Subject: [PATCH 082/109] fix LAN spelling --- internal/service/cloud/network.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index 3ae4d640..6749a338 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -110,7 +110,7 @@ func (s *Service) GetLAN() (*sdk.Lan, error) { } // If there are multiple LANs with the same name, we should return an error. - // Our logic won't be able to proceed as we cannot select the correct lan. + // Our logic won't be able to proceed as we cannot select the correct LAN. if lanCount > 1 { return nil, fmt.Errorf("found multiple LANs with the name: %s", expectedName) } From baba95425d7b910eec302b51b8f37031c8cd91c7 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Tue, 16 Jan 2024 13:31:47 +0100 Subject: [PATCH 083/109] remove type overwrite --- api/v1alpha1/ionoscloudmachine_types.go | 1 - 1 file changed, 1 deletion(-) diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index 493092ce..0a5d825b 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -81,7 +81,6 @@ type IonosCloudMachineSpec struct { // DataCenterID is the ID of the data center where the machine should be created in. // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="datacenterID is immutable" - // +kubebuilder:validation:Type=string // +kubebuilder:validation:Format=uuid DataCenterID string `json:"datacenterID"` From 5c0998059757a0a84701dd6bf084783ba8e62acc Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Tue, 16 Jan 2024 13:37:03 +0100 Subject: [PATCH 084/109] wording update --- api/v1alpha1/ionoscloudmachine_types_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index e1c51976..9e271a9d 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -84,7 +84,7 @@ var _ = Describe("IonosCloudMachine Tests", func() { Expect(m.Spec.ProviderID).To(Equal(want)) }) - When("data center id", func() { + Context("data center ID", func() { It("it should fail if data center ID is not a UUID", func() { m := defaultMachine() want := "" From a7682b3be322da5203078897a114e8a288366eff Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Tue, 16 Jan 2024 14:20:37 +0100 Subject: [PATCH 085/109] add test for unique IPs. Update naming --- api/v1alpha1/ionoscloudmachine_types_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index 9e271a9d..6448e60d 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -100,7 +100,7 @@ var _ = Describe("IonosCloudMachine Tests", func() { }) }) - When("the number of cores, ", func() { + Context("number of cores", func() { It("is less than 1, it should fail", func() { m := defaultMachine() m.Spec.NumCores = -1 @@ -317,6 +317,11 @@ var _ = Describe("IonosCloudMachine Tests", func() { Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.Network.IPs).To(BeNil()) }) + It("should prevent setting identical IPs", func() { + m := defaultMachine() + m.Spec.Network.IPs = []string{"192.0.2.0", "192.0.2.0"} + Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) + }) }) Context("Conditions", func() { It("should correctly set and get the conditions", func() { From 7fca9be8e746bf7a350997f18c083daedb3f1414 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Tue, 16 Jan 2024 14:22:19 +0100 Subject: [PATCH 086/109] remove test --- api/v1alpha1/ionoscloudmachine_types_test.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index 6448e60d..74cb3ec3 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -301,16 +301,6 @@ var _ = Describe("IonosCloudMachine Tests", func() { Expect(m.Spec.Network.UseDHCP).ToNot(BeNil()) Expect(*m.Spec.Network.UseDHCP).To(BeTrue()) }) - DescribeTable("if set UseDHCP can be", - func(useDHCP *bool) { - m := defaultMachine() - m.Spec.Network.UseDHCP = useDHCP - Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(m.Spec.Network.UseDHCP).To(Equal(useDHCP)) - }, - Entry("true", ptr.To(true)), - Entry("false", ptr.To(false)), - ) It("reserved IPs should be optional", func() { m := defaultMachine() m.Spec.Network.IPs = nil From 82af07454e979e10adbb1c36772d84295d0df1f6 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Tue, 16 Jan 2024 16:22:15 +0100 Subject: [PATCH 087/109] update comment --- api/v1alpha1/ionoscloudmachine_types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index 0a5d825b..51e6228a 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -58,7 +58,7 @@ const ( VolumeDiskTypeSSDPremium VolumeDiskType = "SSD Premium" ) -// AvailabilityZone is the availability zone where volumes are created in. +// AvailabilityZone is the availability zone where different cloud resources are created in. type AvailabilityZone string const ( From d817e003a182e82bd93e29d8a44ff2c12fea5661 Mon Sep 17 00:00:00 2001 From: Matthias Bastian Date: Tue, 16 Jan 2024 19:14:11 +0100 Subject: [PATCH 088/109] Add tests for required fields Interesting observation: If the disk is not a pointer, its null value is enough to consider the field as being set during validation. As we get the sent data from marshalling a Go type, that zero value is used when not setting a disk explicitly. --- api/v1alpha1/ionoscloudmachine_types.go | 2 +- api/v1alpha1/ionoscloudmachine_types_test.go | 22 +++++++++++++++----- api/v1alpha1/zz_generated.deepcopy.go | 6 +++++- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index 51e6228a..b621c976 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -112,7 +112,7 @@ type IonosCloudMachineSpec struct { CPUFamily string `json:"cpuFamily"` // Disk defines the boot volume of the VM. - Disk Volume `json:"disk"` + Disk *Volume `json:"disk"` // Network defines the network configuration for the VM. // +optional diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index 74cb3ec3..63b2097a 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -43,7 +43,7 @@ func defaultMachine() *IonosCloudMachine { AvailabilityZone: AvailabilityZoneTwo, MemoryMB: 2048, CPUFamily: "AMD_OPTERON", - Disk: Volume{ + Disk: &Volume{ Name: "disk", DiskType: VolumeDiskTypeSSDStandard, SizeGB: 23, @@ -85,9 +85,15 @@ var _ = Describe("IonosCloudMachine Tests", func() { }) Context("data center ID", func() { + It("should fail if not set", func() { + m := defaultMachine() + m.Spec.DataCenterID = "" + Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) + }) + It("it should fail if data center ID is not a UUID", func() { m := defaultMachine() - want := "" + want := "not-a-UUID" m.Spec.DataCenterID = want Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }) @@ -186,15 +192,21 @@ var _ = Describe("IonosCloudMachine Tests", func() { Expect(m.Spec.MemoryMB).To(Equal(want)) }) }) - When("the machine CPU family", func() { - It("isn't set, it should fail", func() { + + Context("CPU family", func() { + It("should fail if not set", func() { m := defaultMachine() m.Spec.CPUFamily = "" Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }) }) - Context("Volume", func() { + Context("Disk", func() { + It("should fail if not set", func() { + m := defaultMachine() + m.Spec.Disk = nil + Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) + }) It("can have an optional name", func() { m := defaultMachine() want := "" diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index f786b125..3f69ed02 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -192,7 +192,11 @@ 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 - in.Disk.DeepCopyInto(&out.Disk) + if in.Disk != nil { + in, out := &in.Disk, &out.Disk + *out = new(Volume) + (*in).DeepCopyInto(*out) + } if in.Network != nil { in, out := &in.Network, &out.Network *out = new(Network) From 273b86f7212366102299eac83b9a4b1b11244cf3 Mon Sep 17 00:00:00 2001 From: Matthias Bastian Date: Tue, 16 Jan 2024 19:39:16 +0100 Subject: [PATCH 089/109] Improve semantics and structure in API tests a bit --- api/v1alpha1/ionoscloudcluster_types_test.go | 2 +- api/v1alpha1/ionoscloudmachine_types_test.go | 116 ++++++++++--------- 2 files changed, 62 insertions(+), 56 deletions(-) diff --git a/api/v1alpha1/ionoscloudcluster_types_test.go b/api/v1alpha1/ionoscloudcluster_types_test.go index 93ada629..a85b5dde 100644 --- a/api/v1alpha1/ionoscloudcluster_types_test.go +++ b/api/v1alpha1/ionoscloudcluster_types_test.go @@ -55,7 +55,7 @@ func defaultCluster() *IonosCloudCluster { var _ = Describe("IonosCloudCluster", func() { AfterEach(func() { err := k8sClient.Delete(context.Background(), defaultCluster()) - Expect(client.IgnoreNotFound(err)).To(Succeed()) + Expect(client.IgnoreNotFound(err)).ToNot(HaveOccurred()) }) Context("Create", func() { diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index 63b2097a..f5d337d8 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -71,27 +71,29 @@ var _ = Describe("IonosCloudMachine Tests", func() { }) Context("Validation", func() { - It("shouldn't fail if everything is seems to be properly set", func() { + It("should work if everything is set properly", func() { m := defaultMachine() Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) }) - It("should not fail if providerID is empty", func() { - m := defaultMachine() - want := "" - m.Spec.ProviderID = want - Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(m.Spec.ProviderID).To(Equal(want)) + Context("Provider ID", func() { + It("should work if not set", func() { + m := defaultMachine() + want := "" + m.Spec.ProviderID = want + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(m.Spec.ProviderID).To(Equal(want)) + }) }) - Context("data center ID", func() { + Context("Data center ID", func() { It("should fail if not set", func() { m := defaultMachine() m.Spec.DataCenterID = "" Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }) - It("it should fail if data center ID is not a UUID", func() { + It("should fail if not a UUID", func() { m := defaultMachine() want := "not-a-UUID" m.Spec.DataCenterID = want @@ -106,8 +108,8 @@ var _ = Describe("IonosCloudMachine Tests", func() { }) }) - Context("number of cores", func() { - It("is less than 1, it should fail", func() { + Context("Number of cores", func() { + It("should fail if less than 1", func() { m := defaultMachine() m.Spec.NumCores = -1 Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) @@ -119,7 +121,7 @@ var _ = Describe("IonosCloudMachine Tests", func() { Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.NumCores).To(Equal(want)) }) - It("isn't set, it should work and default to 1", func() { + It("should default to 1", func() { m := defaultMachine() // because NumCores is int32, setting the value as 0 is the same as not setting anything m.Spec.NumCores = 0 @@ -128,20 +130,20 @@ var _ = Describe("IonosCloudMachine Tests", func() { }) }) - When("the machine availability zone", func() { - It("isn't set, should default to AUTO", func() { + Context("Availability zone", func() { + It("should default to AUTO", func() { m := defaultMachine() // because AvailabilityZone is a string, setting the value as "" is the same as not setting anything m.Spec.AvailabilityZone = "" Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.AvailabilityZone).To(Equal(AvailabilityZoneAuto)) }) - It("it not part of the enum it should not work", func() { + It("should fail if not part of the enum", func() { m := defaultMachine() m.Spec.AvailabilityZone = "this-should-not-work" Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }) - DescribeTable("should work for these values", + DescribeTable("should work for value", func(zone AvailabilityZone) { m := defaultMachine() m.Spec.AvailabilityZone = zone @@ -152,22 +154,22 @@ var _ = Describe("IonosCloudMachine Tests", func() { Entry("ZONE_1", AvailabilityZoneOne), Entry("ZONE_2", AvailabilityZoneTwo), ) - It("Should not work for ZONE_3", func() { + It("Should fail for ZONE_3", func() { m := defaultMachine() m.Spec.AvailabilityZone = AvailabilityZoneThree Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }) }) - When("the machine memory size", func() { - It("isn't set, should default to 3072MB", func() { + Context("Memory size", func() { + It("should default to 3072MB", func() { m := defaultMachine() // because MemoryMB is an int32, setting the value as 0 is the same as not setting anything m.Spec.MemoryMB = 0 Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.MemoryMB).To(Equal(int32(3072))) }) - It("should be at least 2048, therefore less than it should not work", func() { + It("should be at least 2048, therefore less than it should fail", func() { m := defaultMachine() m.Spec.MemoryMB = 1024 Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) @@ -179,7 +181,7 @@ var _ = Describe("IonosCloudMachine Tests", func() { Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.MemoryMB).To(Equal(want)) }) - It("it should be a multiple of 1024", func() { + It("should be a multiple of 1024", func() { m := defaultMachine() m.Spec.MemoryMB = 2100 Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) @@ -214,20 +216,20 @@ var _ = Describe("IonosCloudMachine Tests", func() { Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.Disk.Name).To(Equal(want)) }) - When("the disk availability zone", func() { - It("isn't set, should default to AUTO", func() { + Context("Availability zone", func() { + It("should default to AUTO", func() { m := defaultMachine() // because AvailabilityZone is a string, setting the value as "" is the same as not setting anything m.Spec.Disk.AvailabilityZone = "" Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.Disk.AvailabilityZone).To(Equal(AvailabilityZoneAuto)) }) - It("is not part of the enum it should not work", func() { + It("should fail if not part of the enum", func() { m := defaultMachine() m.Spec.Disk.AvailabilityZone = "this-should-not-work" Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }) - DescribeTable("should work for these values", + DescribeTable("should work for value", func(zone AvailabilityZone) { m := defaultMachine() m.Spec.Disk.AvailabilityZone = zone @@ -240,25 +242,27 @@ var _ = Describe("IonosCloudMachine Tests", func() { Entry("ZONE_3", AvailabilityZoneThree), ) }) - It("can be created without SSH keys", func() { - m := defaultMachine() - var want []string - m.Spec.Disk.SSHKeys = want - Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(m.Spec.Disk.SSHKeys).To(Equal(want)) - }) - It("should prevent setting identical SSH keys", func() { - m := defaultMachine() - m.Spec.Disk.SSHKeys = []string{"Key1", "Key1", "Key2", "Key3"} - Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) + Context("SSH keys", func() { + It("can be created without them", func() { + m := defaultMachine() + var want []string + m.Spec.Disk.SSHKeys = want + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(m.Spec.Disk.SSHKeys).To(Equal(want)) + }) + It("should prevent duplicates", func() { + m := defaultMachine() + m.Spec.Disk.SSHKeys = []string{"Key1", "Key1", "Key2", "Key3"} + Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) + }) }) - When("the disk size (in GB)", func() { - It("is less than 10, it should fail", func() { + Context("Size (in GB)", func() { + It("should fail if less than 10", func() { m := defaultMachine() m.Spec.Disk.SizeGB = 9 Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }) - It("it is not set, it should default to 20", func() { + It("should default to 20", func() { m := defaultMachine() // Because disk size is an int, setting it as 0 is the same as not setting anything m.Spec.Disk.SizeGB = 0 @@ -273,20 +277,20 @@ var _ = Describe("IonosCloudMachine Tests", func() { Expect(m.Spec.Disk.SizeGB).To(Equal(want)) }) }) - When("the disk type", func() { - It("isn't set, should default to HDD", func() { + Context("Type", func() { + It("should default to HDD", func() { m := defaultMachine() // because DiskType is a string, setting the value as "" is the same as not setting anything m.Spec.Disk.DiskType = "" Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.Disk.DiskType).To(Equal(VolumeDiskTypeHDD)) }) - It("is not part of the enum it should not work", func() { + It("should fail if not part of the enum", func() { m := defaultMachine() m.Spec.Disk.AvailabilityZone = "tape" Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }) - DescribeTable("should work for these values", + DescribeTable("should work for value", func(diskType VolumeDiskType) { m := defaultMachine() m.Spec.Disk.DiskType = diskType @@ -300,29 +304,31 @@ var _ = Describe("IonosCloudMachine Tests", func() { }) }) Context("Network", func() { - It("network config should be optional", func() { + It("should be optional", func() { m := defaultMachine() m.Spec.Network = nil Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.Network).To(BeNil()) }) - It("if UseDHCP is not set, it should default to true", func() { + It("should default UseDHCP to true", func() { m := defaultMachine() m.Spec.Network.UseDHCP = nil Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) Expect(m.Spec.Network.UseDHCP).ToNot(BeNil()) Expect(*m.Spec.Network.UseDHCP).To(BeTrue()) }) - It("reserved IPs should be optional", func() { - m := defaultMachine() - m.Spec.Network.IPs = nil - Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(m.Spec.Network.IPs).To(BeNil()) - }) - It("should prevent setting identical IPs", func() { - m := defaultMachine() - m.Spec.Network.IPs = []string{"192.0.2.0", "192.0.2.0"} - Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) + Context("IPs", func() { + It("should work without them", func() { + m := defaultMachine() + m.Spec.Network.IPs = nil + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(m.Spec.Network.IPs).To(BeNil()) + }) + It("should prevent duplicates", func() { + m := defaultMachine() + m.Spec.Network.IPs = []string{"192.0.2.0", "192.0.2.0"} + Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) + }) }) }) Context("Conditions", func() { From c8aef1bf80c275990e7cc919a45f6c5db6236dee Mon Sep 17 00:00:00 2001 From: Matthias Bastian Date: Tue, 16 Jan 2024 19:40:43 +0100 Subject: [PATCH 090/109] Move condition tests ouf of validation context --- api/v1alpha1/ionoscloudmachine_types_test.go | 33 ++++++++++---------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index f5d337d8..b2b68495 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -18,15 +18,15 @@ package v1alpha1 import ( "context" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/cluster-api/util/conditions" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/cluster-api/util/conditions" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -331,23 +331,24 @@ var _ = Describe("IonosCloudMachine Tests", func() { }) }) }) - 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 - conditions.MarkTrue(m, MachineProvisionedCondition) + 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()) - 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()) + // Calls SetConditions with required fields + conditions.MarkTrue(m, MachineProvisionedCondition) - machineConditions := m.GetConditions() - Expect(machineConditions).To(HaveLen(1)) - Expect(machineConditions[0].Type).To(Equal(MachineProvisionedCondition)) - Expect(machineConditions[0].Status).To(Equal(corev1.ConditionTrue)) - }) + 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() + Expect(machineConditions).To(HaveLen(1)) + Expect(machineConditions[0].Type).To(Equal(MachineProvisionedCondition)) + Expect(machineConditions[0].Status).To(Equal(corev1.ConditionTrue)) }) }) }) From 441a5ef40d14084e0ee3869d64cd596fd13831b0 Mon Sep 17 00:00:00 2001 From: Matthias Bastian Date: Tue, 16 Jan 2024 19:52:02 +0100 Subject: [PATCH 091/109] Satisfy the linter --- api/v1alpha1/ionoscloudmachine_types_test.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index b2b68495..64e5cd38 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -18,16 +18,15 @@ package v1alpha1 import ( "context" - corev1 "k8s.io/api/core/v1" - "sigs.k8s.io/cluster-api/util/conditions" - - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/cluster-api/util/conditions" "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" ) func defaultMachine() *IonosCloudMachine { From 5f697383147aa13f146178cc388f768fecb34295 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Wed, 17 Jan 2024 10:52:08 +0100 Subject: [PATCH 092/109] redefine network in spec --- api/v1alpha1/ionoscloudmachine_types.go | 24 +++++------ api/v1alpha1/ionoscloudmachine_types_test.go | 41 +++++++------------ api/v1alpha1/types.go | 4 ++ api/v1alpha1/zz_generated.deepcopy.go | 37 ++++++++++------- ...e.cluster.x-k8s.io_ionoscloudmachines.yaml | 34 +++++++-------- 5 files changed, 68 insertions(+), 72 deletions(-) diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index b621c976..51d8ea44 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -114,25 +114,21 @@ type IonosCloudMachineSpec struct { // Disk defines the boot volume of the VM. Disk *Volume `json:"disk"` - // Network defines the network configuration for the VM. + // AdditionalNetworks defines the additional network configurations for the VM. + // NOTE(lubedacht): We currently only support networks with DHCP enabled. // +optional - Network *Network `json:"network,omitempty"` + AdditionalNetworks Networks `json:"additionalNetworks,omitempty"` } +// Networks contains a list of network configs. +type Networks []Network + // Network contains a network config. type Network struct { - // IPs is an optional set of IP addresses, which have been - // reserved in the corresponding data center. - // +listType=set - // +optional - IPs []string `json:"ips,omitempty"` - - // UseDHCP sets whether DHCP should be used or not. - // NOTE(lubedacht) currently we do not support private clusters - // therefore dhcp must be set to true. - // +kubebuilder:default=true - // +optional - UseDHCP *bool `json:"useDHCP,omitempty"` + // NetworkID represents an ID an existing LAN in the data center. + // This LAN will be excluded from the deletion process. + // +kubebuilder:validation:Minimum=1 + NetworkID int32 `json:"networkID"` } // Volume is the physical storage on the machine. diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index 64e5cd38..6f04f7db 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -21,12 +21,11 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/cluster-api/util/conditions" "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" ) func defaultMachine() *IonosCloudMachine { @@ -49,9 +48,10 @@ func defaultMachine() *IonosCloudMachine { AvailabilityZone: AvailabilityZoneOne, SSHKeys: []string{"public-key"}, }, - Network: &Network{ - IPs: []string{"1.2.3.4"}, - UseDHCP: ptr.To(true), + AdditionalNetworks: Networks{ + { + NetworkID: 1, + }, }, }, } @@ -302,32 +302,19 @@ var _ = Describe("IonosCloudMachine Tests", func() { ) }) }) - Context("Network", func() { - It("should be optional", func() { + Context("Additional Networks", func() { + It("network config should be optional", func() { m := defaultMachine() - m.Spec.Network = nil + m.Spec.AdditionalNetworks = nil Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(m.Spec.Network).To(BeNil()) + Expect(m.Spec.AdditionalNetworks).To(BeNil()) }) - It("should default UseDHCP to true", func() { + It("network ID must be greater than 0", func() { m := defaultMachine() - m.Spec.Network.UseDHCP = nil - Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(m.Spec.Network.UseDHCP).ToNot(BeNil()) - Expect(*m.Spec.Network.UseDHCP).To(BeTrue()) - }) - Context("IPs", func() { - It("should work without them", func() { - m := defaultMachine() - m.Spec.Network.IPs = nil - Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(m.Spec.Network.IPs).To(BeNil()) - }) - It("should prevent duplicates", func() { - m := defaultMachine() - m.Spec.Network.IPs = []string{"192.0.2.0", "192.0.2.0"} - Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) - }) + m.Spec.AdditionalNetworks[0].NetworkID = 0 + Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) + m.Spec.AdditionalNetworks[0].NetworkID = -1 + Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }) }) }) diff --git a/api/v1alpha1/types.go b/api/v1alpha1/types.go index 01355608..fd1ffef3 100644 --- a/api/v1alpha1/types.go +++ b/api/v1alpha1/types.go @@ -16,6 +16,10 @@ limitations under the License. package v1alpha1 +const ( + ManagedLANAnnotation = "cloud.ionos.com/managed-lan" +) + // RequestStatus shows the status of the current request. type RequestStatus string diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 3f69ed02..6151ee35 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -197,10 +197,10 @@ func (in *IonosCloudMachineSpec) DeepCopyInto(out *IonosCloudMachineSpec) { *out = new(Volume) (*in).DeepCopyInto(*out) } - if in.Network != nil { - in, out := &in.Network, &out.Network - *out = new(Network) - (*in).DeepCopyInto(*out) + if in.AdditionalNetworks != nil { + in, out := &in.AdditionalNetworks, &out.AdditionalNetworks + *out = make(Networks, len(*in)) + copy(*out, *in) } } @@ -254,16 +254,6 @@ func (in *IonosCloudMachineStatus) DeepCopy() *IonosCloudMachineStatus { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Network) DeepCopyInto(out *Network) { *out = *in - if in.IPs != nil { - in, out := &in.IPs, &out.IPs - *out = make([]string, len(*in)) - copy(*out, *in) - } - if in.UseDHCP != nil { - in, out := &in.UseDHCP, &out.UseDHCP - *out = new(bool) - **out = **in - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Network. @@ -276,6 +266,25 @@ func (in *Network) DeepCopy() *Network { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in Networks) DeepCopyInto(out *Networks) { + { + in := &in + *out = make(Networks, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Networks. +func (in Networks) DeepCopy() Networks { + if in == nil { + return nil + } + out := new(Networks) + in.DeepCopyInto(out) + return *out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ProvisioningRequest) DeepCopyInto(out *ProvisioningRequest) { *out = *in 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 082bfafa..d2078bb8 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml @@ -34,6 +34,23 @@ spec: spec: description: IonosCloudMachineSpec defines the desired state of IonosCloudMachine. properties: + additionalNetworks: + description: 'AdditionalNetworks defines the additional network configurations + for the VM. NOTE(lubedacht): We currently only support networks + with DHCP enabled.' + items: + description: Network contains a network config. + properties: + networkID: + description: NetworkID represents an ID an existing LAN in the + data center. This LAN will be excluded from the deletion process. + format: int32 + minimum: 1 + type: integer + required: + - networkID + type: object + type: array availabilityZone: default: AUTO description: AvailabilityZone is the availability zone in which the @@ -104,23 +121,6 @@ spec: minimum: 2048 multipleOf: 1024 type: integer - network: - description: Network defines the network configuration for the VM. - properties: - ips: - description: IPs is an optional set of IP addresses, which have - been reserved in the corresponding data center. - items: - type: string - type: array - x-kubernetes-list-type: set - useDHCP: - default: true - description: UseDHCP sets whether DHCP should be used or not. - NOTE(lubedacht) currently we do not support private clusters - therefore dhcp must be set to true. - type: boolean - type: object numCores: default: 1 description: NumCores defines the number of cores for the VM. From 383b3f460fde0e6fa4f60fdcc45faf3176c0896a Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Wed, 17 Jan 2024 11:18:12 +0100 Subject: [PATCH 093/109] rename CurrentRequest --- api/v1alpha1/ionoscloudcluster_types.go | 4 ++-- api/v1alpha1/zz_generated.deepcopy.go | 4 ++-- .../infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml | 4 ++-- internal/service/cloud/network.go | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/api/v1alpha1/ionoscloudcluster_types.go b/api/v1alpha1/ionoscloudcluster_types.go index ae4887d3..289c8cbb 100644 --- a/api/v1alpha1/ionoscloudcluster_types.go +++ b/api/v1alpha1/ionoscloudcluster_types.go @@ -55,9 +55,9 @@ type IonosCloudClusterStatus struct { // +optional Conditions clusterv1.Conditions `json:"conditions,omitempty"` - // CurrentRequest maps data center IDs to a pending provisioning request made during reconciliation. + // CurrentRequestByDatacenter maps data center IDs to a pending provisioning request made during reconciliation. // +optional - CurrentRequest map[string]ProvisioningRequest `json:"currentRequest,omitempty"` + CurrentRequestByDatacenter map[string]ProvisioningRequest `json:"currentRequest,omitempty"` } //+kubebuilder:object:root=true diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 6151ee35..4723311a 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -111,8 +111,8 @@ func (in *IonosCloudClusterStatus) DeepCopyInto(out *IonosCloudClusterStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } - if in.CurrentRequest != nil { - in, out := &in.CurrentRequest, &out.CurrentRequest + if in.CurrentRequestByDatacenter != nil { + in, out := &in.CurrentRequestByDatacenter, &out.CurrentRequestByDatacenter *out = make(map[string]ProvisioningRequest, len(*in)) for key, val := range *in { (*out)[key] = *val.DeepCopy() 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 6dae51f2..5bb717c0 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml @@ -148,8 +148,8 @@ spec: - method - requestPath type: object - description: CurrentRequest maps data center IDs to a pending provisioning - request made during reconciliation. + description: CurrentRequestByDatacenter maps data center IDs to a + pending provisioning request made during reconciliation. type: object ready: default: false diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index 6749a338..f29b0495 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -182,7 +182,7 @@ func (s *Service) createLAN() error { return fmt.Errorf("unable to create LAN in data center %s: %w", s.dataCenterID(), err) } - s.scope.ClusterScope.IonosCluster.Status.CurrentRequest[s.dataCenterID()] = infrav1.ProvisioningRequest{ + s.scope.ClusterScope.IonosCluster.Status.CurrentRequestByDatacenter[s.dataCenterID()] = infrav1.ProvisioningRequest{ Method: http.MethodPost, RequestPath: requestPath, State: infrav1.RequestStatusQueued, @@ -206,7 +206,7 @@ func (s *Service) deleteLAN(lanID string) error { return fmt.Errorf("unable to request LAN deletion in data center: %w", err) } - s.scope.ClusterScope.IonosCluster.Status.CurrentRequest[s.dataCenterID()] = infrav1.ProvisioningRequest{ + s.scope.ClusterScope.IonosCluster.Status.CurrentRequestByDatacenter[s.dataCenterID()] = infrav1.ProvisioningRequest{ Method: http.MethodDelete, RequestPath: requestPath, State: infrav1.RequestStatusQueued, @@ -278,7 +278,7 @@ func (s *Service) checkForPendingLANRequest(method string, lanID string) (status } func (s *Service) removeLANPendingRequestFromCluster() error { - delete(s.scope.ClusterScope.IonosCluster.Status.CurrentRequest, s.dataCenterID()) + delete(s.scope.ClusterScope.IonosCluster.Status.CurrentRequestByDatacenter, s.dataCenterID()) if err := s.scope.ClusterScope.PatchObject(); err != nil { return fmt.Errorf("could not remove stale LAN pending request from cluster: %w", err) } From 1283424a1a97e8ab840839cec0cfc7d4c2022bb3 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Wed, 17 Jan 2024 11:56:27 +0100 Subject: [PATCH 094/109] add status test --- api/v1alpha1/ionoscloudmachine_types_test.go | 31 ++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index 6f04f7db..3492d24e 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -19,8 +19,12 @@ package v1alpha1 import ( "context" + "github.com/google/go-cmp/cmp" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "sigs.k8s.io/cluster-api/errors" + + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -337,4 +341,31 @@ var _ = Describe("IonosCloudMachine Tests", func() { Expect(machineConditions[0].Status).To(Equal(corev1.ConditionTrue)) }) }) + Context("Status", func() { + It("should correctly set and get the status", 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()) + + m.Status.Ready = true + conditions.MarkTrue(m, MachineProvisionedCondition) + m.Status.CurrentRequest = &ProvisioningRequest{ + Method: "GET", + RequestPath: "path/to/resource", + State: RequestStatusRunning, + Message: nil, + } + m.Status.FailureReason = ptr.To(errors.InvalidConfigurationMachineError) + m.Status.FailureMessage = ptr.To("Failure message") + + want := *m.DeepCopy() + + 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()) + + // Gomega matcher seems to have issues with comparing the dates. + diff := cmp.Diff(want.Status, m.Status) + Expect(diff).To(BeEmpty(), "m.Status differs from want.Status (-want +got):\n%s", diff) + }) + }) }) From 0f2812634fe27fcd7a3dc07ad0756506dca266f5 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Wed, 17 Jan 2024 11:58:07 +0100 Subject: [PATCH 095/109] tidy --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index a08a188e..11eedee1 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.21 require ( github.com/go-logr/logr v1.4.1 + github.com/google/go-cmp v0.6.0 github.com/ionos-cloud/sdk-go/v6 v6.1.11 github.com/onsi/ginkgo/v2 v2.13.2 github.com/onsi/gomega v1.30.0 @@ -35,7 +36,6 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect github.com/google/uuid v1.3.1 // indirect From 442a3b146704f7d3c490e7d21369dfbb4469abd4 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Wed, 17 Jan 2024 13:04:20 +0100 Subject: [PATCH 096/109] use len check instead of nil --- internal/service/cloud/network.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index f29b0495..eae98eb3 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -246,7 +246,7 @@ func (s *Service) checkForPendingLANRequest(method string, lanID string) (status for _, r := range requests { if method != http.MethodPost { targets := *r.Metadata.RequestStatus.Metadata.Targets - if targets == nil { + if len(targets) == 0 { continue } From 18e44197ceaf2f958af646de33eba893cd02b8a1 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Wed, 17 Jan 2024 14:08:21 +0100 Subject: [PATCH 097/109] Remove UpdateLAN for now --- internal/ionoscloud/client.go | 3 - internal/ionoscloud/client/client.go | 18 ------ internal/ionoscloud/clienttest/mock_client.go | 57 ------------------- 3 files changed, 78 deletions(-) diff --git a/internal/ionoscloud/client.go b/internal/ionoscloud/client.go index 89ca4cad..0b081ed3 100644 --- a/internal/ionoscloud/client.go +++ b/internal/ionoscloud/client.go @@ -41,9 +41,6 @@ type Client interface { DestroyServer(ctx context.Context, dataCenterID, serverID string) error // CreateLAN creates a new LAN with the provided properties in the specified data center, returning the request location. CreateLAN(ctx context.Context, dataCenterID string, properties sdk.LanPropertiesPost) (string, error) - // UpdateLAN updates a LAN with the provided properties in the specified data center. - UpdateLAN(ctx context.Context, dataCenterID string, lanID string, properties sdk.LanProperties) ( - *sdk.Lan, error) // AttachToLAN attaches a provided NIC to a provided LAN in a specified data center. AttachToLAN(ctx context.Context, dataCenterID, lanID string, nic sdk.Nic) ( *sdk.Nic, error) diff --git a/internal/ionoscloud/client/client.go b/internal/ionoscloud/client/client.go index 5c2cc4d5..3872993b 100644 --- a/internal/ionoscloud/client/client.go +++ b/internal/ionoscloud/client/client.go @@ -168,24 +168,6 @@ func (c *IonosCloudClient) CreateLAN(ctx context.Context, dataCenterID string, p return "", errors.New(apiNoLocationErrMessage) } -// UpdateLAN updates a LAN with the provided properties in the specified data center. -func (c *IonosCloudClient) UpdateLAN( - ctx context.Context, dataCenterID string, lanID string, properties sdk.LanProperties, -) (*sdk.Lan, error) { - if dataCenterID == "" { - return nil, errDataCenterIDIsEmpty - } - if lanID == "" { - return nil, errLANIDIsEmpty - } - l, _, err := c.API.LANsApi.DatacentersLansPatch(ctx, dataCenterID, lanID). - Lan(properties).Execute() - if err != nil { - return nil, fmt.Errorf(apiCallErrWrapper, err) - } - return &l, nil -} - // AttachToLAN attaches a provided NIC to a provided LAN in the specified data center. func (c *IonosCloudClient) AttachToLAN(ctx context.Context, dataCenterID, lanID string, nic sdk.Nic, ) (*sdk.Nic, error) { diff --git a/internal/ionoscloud/clienttest/mock_client.go b/internal/ionoscloud/clienttest/mock_client.go index a9960347..f4788fc3 100644 --- a/internal/ionoscloud/clienttest/mock_client.go +++ b/internal/ionoscloud/clienttest/mock_client.go @@ -902,63 +902,6 @@ func (_c *MockClient_ListVolumes_Call) RunAndReturn(run func(context.Context, st return _c } -// UpdateLAN provides a mock function with given fields: ctx, dataCenterID, lanID, properties -func (_m *MockClient) UpdateLAN(ctx context.Context, dataCenterID string, lanID string, properties ionoscloud.LanProperties) (*ionoscloud.Lan, error) { - ret := _m.Called(ctx, dataCenterID, lanID, properties) - - var r0 *ionoscloud.Lan - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, ionoscloud.LanProperties) (*ionoscloud.Lan, error)); ok { - return rf(ctx, dataCenterID, lanID, properties) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string, ionoscloud.LanProperties) *ionoscloud.Lan); ok { - r0 = rf(ctx, dataCenterID, lanID, properties) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*ionoscloud.Lan) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string, ionoscloud.LanProperties) error); ok { - r1 = rf(ctx, dataCenterID, lanID, properties) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockClient_UpdateLAN_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateLAN' -type MockClient_UpdateLAN_Call struct { - *mock.Call -} - -// UpdateLAN is a helper method to define mock.On call -// - ctx context.Context -// - dataCenterID string -// - lanID string -// - properties ionoscloud.LanProperties -func (_e *MockClient_Expecter) UpdateLAN(ctx interface{}, dataCenterID interface{}, lanID interface{}, properties interface{}) *MockClient_UpdateLAN_Call { - return &MockClient_UpdateLAN_Call{Call: _e.mock.On("UpdateLAN", ctx, dataCenterID, lanID, properties)} -} - -func (_c *MockClient_UpdateLAN_Call) Run(run func(ctx context.Context, dataCenterID string, lanID string, properties ionoscloud.LanProperties)) *MockClient_UpdateLAN_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(ionoscloud.LanProperties)) - }) - return _c -} - -func (_c *MockClient_UpdateLAN_Call) Return(_a0 *ionoscloud.Lan, _a1 error) *MockClient_UpdateLAN_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockClient_UpdateLAN_Call) RunAndReturn(run func(context.Context, string, string, ionoscloud.LanProperties) (*ionoscloud.Lan, error)) *MockClient_UpdateLAN_Call { - _c.Call.Return(run) - return _c -} - // WaitForRequest provides a mock function with given fields: ctx, requestURL func (_m *MockClient) WaitForRequest(ctx context.Context, requestURL string) error { ret := _m.Called(ctx, requestURL) From a58cb864f5bd7a004f9f1020dc2f76c99c8f63e6 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Wed, 17 Jan 2024 16:32:29 +0100 Subject: [PATCH 098/109] add tests for cluster status --- api/v1alpha1/ionoscloudcluster_types.go | 9 +++++ api/v1alpha1/ionoscloudcluster_types_test.go | 42 ++++++++++++++++++-- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/api/v1alpha1/ionoscloudcluster_types.go b/api/v1alpha1/ionoscloudcluster_types.go index 289c8cbb..a274a3b1 100644 --- a/api/v1alpha1/ionoscloudcluster_types.go +++ b/api/v1alpha1/ionoscloudcluster_types.go @@ -97,3 +97,12 @@ func (i *IonosCloudCluster) GetConditions() clusterv1.Conditions { func (i *IonosCloudCluster) SetConditions(conditions clusterv1.Conditions) { i.Status.Conditions = conditions } + +// SetCurrentRequest sets the current provisioning request for the given data center. +// This function makes sure that the map is initialized before setting the request. +func (i *IonosCloudCluster) SetCurrentRequest(dataCenterID string, request ProvisioningRequest) { + if i.Status.CurrentRequestByDatacenter == nil { + i.Status.CurrentRequestByDatacenter = map[string]ProvisioningRequest{} + } + i.Status.CurrentRequestByDatacenter[dataCenterID] = request +} diff --git a/api/v1alpha1/ionoscloudcluster_types_test.go b/api/v1alpha1/ionoscloudcluster_types_test.go index a85b5dde..e03afc5f 100644 --- a/api/v1alpha1/ionoscloudcluster_types_test.go +++ b/api/v1alpha1/ionoscloudcluster_types_test.go @@ -18,6 +18,7 @@ package v1alpha1 import ( "context" + "sigs.k8s.io/cluster-api/util/conditions" "testing" . "github.com/onsi/ginkgo/v2" @@ -29,11 +30,11 @@ import ( ) func TestIonosCloudCluster_Conditions(t *testing.T) { - conditions := clusterv1.Conditions{{Type: "type"}} + conds := clusterv1.Conditions{{Type: "type"}} cluster := &IonosCloudCluster{} - cluster.SetConditions(conditions) - require.Equal(t, conditions, cluster.GetConditions()) + cluster.SetConditions(conds) + require.Equal(t, conds, cluster.GetConditions()) } func defaultCluster() *IonosCloudCluster { @@ -73,4 +74,39 @@ var _ = Describe("IonosCloudCluster", func() { Expect(k8sClient.Update(context.Background(), cluster)).Should(MatchError(ContainSubstring("contractNumber is immutable"))) }) }) + 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.Ready).To(BeFalse()) + Expect(fetched.Status.CurrentRequestByDatacenter).To(BeEmpty()) + Expect(fetched.Status.Conditions).To(BeEmpty()) + + By("retrieving the cluster and setting the status") + fetched.Status.Ready = true + wantProvisionRequest := ProvisioningRequest{ + Method: "POST", + RequestPath: "/path/to/resource", + State: RequestStatusQueued, + } + fetched.SetCurrentRequest("123", wantProvisionRequest) + conditions.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.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()) + }) + }) }) From ededc3600df822e9dfa18f8c6573a3d045f68389 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Wed, 17 Jan 2024 16:35:42 +0100 Subject: [PATCH 099/109] linter be mad bro --- api/v1alpha1/ionoscloudcluster_types_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/v1alpha1/ionoscloudcluster_types_test.go b/api/v1alpha1/ionoscloudcluster_types_test.go index e03afc5f..61105410 100644 --- a/api/v1alpha1/ionoscloudcluster_types_test.go +++ b/api/v1alpha1/ionoscloudcluster_types_test.go @@ -18,7 +18,6 @@ package v1alpha1 import ( "context" - "sigs.k8s.io/cluster-api/util/conditions" "testing" . "github.com/onsi/ginkgo/v2" @@ -26,6 +25,7 @@ import ( "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/cluster-api/util/conditions" "sigs.k8s.io/controller-runtime/pkg/client" ) From 26a25eb9649baa6e2754b7f6e8f384225c527b92 Mon Sep 17 00:00:00 2001 From: Gustavo Alves <112630064+gfariasalves-ionos@users.noreply.github.com> Date: Fri, 19 Jan 2024 14:58:34 +0100 Subject: [PATCH 100/109] Implement unit tests for network (LAN) service (#26) --- .golangci.yml | 1 + api/v1alpha1/suite_test.go | 3 - internal/service/cloud/network.go | 51 +- internal/service/cloud/network_test.go | 434 ++++++++++++++++++ internal/service/cloud/service.go | 5 + .../cloud/{datacenter.go => service_test.go} | 9 +- internal/service/cloud/suite_test.go | 153 ++++++ 7 files changed, 629 insertions(+), 27 deletions(-) create mode 100644 internal/service/cloud/network_test.go rename internal/service/cloud/{datacenter.go => service_test.go} (70%) create mode 100644 internal/service/cloud/suite_test.go diff --git a/.golangci.yml b/.golangci.yml index 433e465e..a493ac78 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -6,6 +6,7 @@ linters: - dogsled - errcheck - exportloopref + - ginkgolinter - goconst - gocritic - gocyclo diff --git a/api/v1alpha1/suite_test.go b/api/v1alpha1/suite_test.go index 812201fa..efc2b39e 100644 --- a/api/v1alpha1/suite_test.go +++ b/api/v1alpha1/suite_test.go @@ -47,9 +47,6 @@ var _ = BeforeSuite(func() { CRDDirectoryPaths: []string{ filepath.Join("..", "..", "config", "crd", "bases"), }, - // NOTE(gfariasalves): To be removed after I finish the PR comments - BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", "1.28.0-linux-amd64"), - ErrorIfCRDPathMissing: true, } diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index eae98eb3..fac3b0ed 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -30,10 +30,10 @@ import ( "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" ) -// LANName returns the name of the cluster LAN. -func (s *Service) LANName() string { +// lanName returns the name of the cluster LAN. +func (s *Service) lanName() string { return fmt.Sprintf( - "k8s-lan-%s-%s", + "k8s-%s-%s", s.scope.ClusterScope.Cluster.Namespace, s.scope.ClusterScope.Cluster.Name) } @@ -97,7 +97,7 @@ func (s *Service) GetLAN() (*sdk.Lan, error) { } var ( - expectedName = s.LANName() + expectedName = s.lanName() lanCount = 0 foundLAN *sdk.Lan ) @@ -126,13 +126,13 @@ func (s *Service) ReconcileLANDeletion() (requeue bool, err error) { // try to retrieve the cluster LAN clusterLAN, err := s.GetLAN() + if err != nil { + return false, err + } if clusterLAN == nil { err = s.removeLANPendingRequestFromCluster() return err != nil, err } - if err != nil { - return false, err - } // if we found a LAN, we check if there is a deletion already in process requestStatus, err := s.checkForPendingLANRequest(http.MethodDelete, *clusterLAN.Id) @@ -149,18 +149,18 @@ func (s *Service) ReconcileLANDeletion() (requeue bool, err error) { // Here we can check if the LAN is indeed gone or there's some inconsistency in the last request or // this request points to an old, far gone LAN with the same ID. clusterLAN, err = s.GetLAN() + if err != nil { + return false, err + } if clusterLAN == nil { err = s.removeLANPendingRequestFromCluster() return err != nil, err } - if err != nil { - return false, err - } } } - if clusterLAN != nil && len(*clusterLAN.Entities.Nics.Items) > 0 { - log.Info("The cluster LAN is still being used by another resource. skipping deletion") + if clusterLAN != nil && clusterLAN.Entities.HasNics() && len(*clusterLAN.Entities.Nics.Items) > 0 { + log.Info("the cluster LAN is still being used by another resource. skipping deletion") return false, nil } // Request for LAN destruction @@ -175,13 +175,16 @@ func (s *Service) createLAN() error { log := s.scope.Logger.WithName("createLAN") requestPath, err := s.api().CreateLAN(s.ctx, s.dataCenterID(), sdk.LanPropertiesPost{ - Name: ptr.To(s.LANName()), + Name: ptr.To(s.lanName()), Public: ptr.To(true), }) if err != nil { return fmt.Errorf("unable to create LAN in data center %s: %w", s.dataCenterID(), err) } + if len(s.scope.ClusterScope.IonosCluster.Status.CurrentRequestByDatacenter) == 0 { + s.scope.ClusterScope.IonosCluster.Status.CurrentRequestByDatacenter = make(map[string]infrav1.ProvisioningRequest) + } s.scope.ClusterScope.IonosCluster.Status.CurrentRequestByDatacenter[s.dataCenterID()] = infrav1.ProvisioningRequest{ Method: http.MethodPost, RequestPath: requestPath, @@ -193,7 +196,7 @@ func (s *Service) createLAN() error { return fmt.Errorf("unable to patch the cluster: %w", err) } - log.WithValues("requestPath", requestPath).Info("Successfully requested for LAN creation") + log.Info("Successfully requested for LAN creation", "requestPath", requestPath) return nil } @@ -206,6 +209,9 @@ func (s *Service) deleteLAN(lanID string) error { return fmt.Errorf("unable to request LAN deletion in data center: %w", err) } + if s.scope.ClusterScope.IonosCluster.Status.CurrentRequestByDatacenter == nil { + s.scope.ClusterScope.IonosCluster.Status.CurrentRequestByDatacenter = make(map[string]infrav1.ProvisioningRequest) + } s.scope.ClusterScope.IonosCluster.Status.CurrentRequestByDatacenter[s.dataCenterID()] = infrav1.ProvisioningRequest{ Method: http.MethodDelete, RequestPath: requestPath, @@ -225,13 +231,16 @@ func (s *Service) deleteLAN(lanID string) error { func (s *Service) checkForPendingLANRequest(method string, lanID string) (status string, err error) { switch method { case http.MethodPost: - case http.MethodDelete, http.MethodPatch: + if lanID != "" { + return status, errors.New("lanID must be empty for POST requests") + } + case http.MethodDelete: if lanID == "" { - return "", errors.New("lanID cannot be empty for DELETE and PATCH requests") + return "", errors.New("lanID cannot be empty for DELETE requests") } default: return "", fmt.Errorf("unsupported method %s, allowed methods are %s", method, strings.Join( - []string{http.MethodPost, http.MethodDelete, http.MethodPatch}, + []string{http.MethodPost, http.MethodDelete}, ",", )) } @@ -242,9 +251,9 @@ func (s *Service) checkForPendingLANRequest(method string, lanID string) (status return "", fmt.Errorf("could not get requests: %w", err) } - expectedLANName := s.LANName() + expectedLANName := s.lanName() for _, r := range requests { - if method != http.MethodPost { + if method == http.MethodDelete { targets := *r.Metadata.RequestStatus.Metadata.Targets if len(targets) == 0 { continue @@ -268,8 +277,8 @@ func (s *Service) checkForPendingLANRequest(method string, lanID string) (status if status == sdk.RequestStatusFailed { // We just log the error but do not return it, so we can retry the request. message := r.Metadata.RequestStatus.Metadata.Message - s.scope.Logger.WithValues("requestID", r.Id, "requestStatus", status). - Error(errors.New(*message), "last request for LAN has failed. logging it for debugging purposes") + s.scope.Logger.WithValues("requestID", r.Id, "requestStatus", status, "message", *message). + Error(nil, "last request for LAN has failed. logging it for debugging purposes") } return status, nil diff --git a/internal/service/cloud/network_test.go b/internal/service/cloud/network_test.go new file mode 100644 index 00000000..db80ce3a --- /dev/null +++ b/internal/service/cloud/network_test.go @@ -0,0 +1,434 @@ +/* +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 cloud + +import ( + "fmt" + "net/http" + "path" + + sdk "github.com/ionos-cloud/sdk-go/v6" + "github.com/stretchr/testify/mock" + + infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" + clienttest "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud/clienttest" + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" +) + +func (s *ServiceTestSuite) TestNetworkLANName() { + s.Equal("k8s-default-test-cluster", s.service.lanName()) +} + +func (s *ServiceTestSuite) Test_Network_CreateLAN_Successful() { + s.createLANCall().Return(reqPath, nil).Once() + s.NoError(s.service.createLAN()) + s.Contains(s.infraCluster.Status.CurrentRequestByDatacenter, s.service.dataCenterID(), "request should be stored in status") + req := s.infraCluster.Status.CurrentRequestByDatacenter[s.service.dataCenterID()] + s.Equal(reqPath, req.RequestPath, "request path should be stored in status") + s.Equal(http.MethodPost, req.Method, "request method should be stored in status") + s.Equal(infrav1.RequestStatusQueued, req.State, "request status should be stored in status") +} + +func (s *ServiceTestSuite) Test_Network_DeleteLAN_Successful() { + s.deleteLANCall(lanID).Return(reqPath, nil).Once() + s.NoError(s.service.deleteLAN(lanID)) + s.Contains(s.infraCluster.Status.CurrentRequestByDatacenter, s.service.dataCenterID(), "request should be stored in status") + req := s.infraCluster.Status.CurrentRequestByDatacenter[s.service.dataCenterID()] + s.Equal(reqPath, req.RequestPath, "request path should be stored in status") + s.Equal(http.MethodDelete, req.Method, "request method should be stored in status") + s.Equal(infrav1.RequestStatusQueued, req.State, "request status should be stored in status") +} + +func (s *ServiceTestSuite) Test_Network_GetLAN_Successful() { + lan := s.exampleLAN() + s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{lan}}, nil).Once() + foundLAN, err := s.service.GetLAN() + s.NoError(err) + s.NotNil(foundLAN) + s.Equal(lan, *foundLAN) +} + +func (s *ServiceTestSuite) Test_Network_GetLAN_NotFound() { + s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{}}, nil).Once() + lan, err := s.service.GetLAN() + s.NoError(err) + s.Nil(lan) +} + +func (s *ServiceTestSuite) Test_Network_GetLAN_Error_NotUnique() { + s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{s.exampleLAN(), s.exampleLAN()}}, nil).Once() + lan, err := s.service.GetLAN() + s.Error(err) + s.Nil(lan) +} + +func (s *ServiceTestSuite) Test_Network_CheckForPendingLANRequest_NoPendingRequest() { + s.getRequestsCall().Return([]sdk.Request{}, nil).Once() + request, err := s.service.checkForPendingLANRequest(http.MethodDelete, lanID) + s.NoError(err) + s.Empty(request) +} + +func (s *ServiceTestSuite) Test_Network_CheckForPendingLANRequest_DeleteRequest() { + s.getRequestsCall().Return([]sdk.Request{ + { + Id: ptr.To("1"), + Metadata: &sdk.RequestMetadata{ + RequestStatus: &sdk.RequestStatus{ + Metadata: &sdk.RequestStatusMetadata{ + Targets: &[]sdk.RequestTarget{ + { + Target: &sdk.ResourceReference{ + Id: ptr.To(lanID), + }, + }, + }, + Status: ptr.To(sdk.RequestStatusQueued), + Message: ptr.To("test"), + }, + }, + }, + }, + }, nil).Once() + status, err := s.service.checkForPendingLANRequest(http.MethodDelete, lanID) + s.NoError(err) + s.Equal(sdk.RequestStatusQueued, status) +} + +func (s *ServiceTestSuite) Test_Network_CheckForPendingLANRequest_PostRequest() { + s.getRequestsCall().Return(s.examplePostRequest(sdk.RequestStatusQueued), nil).Once() + status, err := s.service.checkForPendingLANRequest(http.MethodPost, "") + s.NoError(err) + s.Equal(sdk.RequestStatusQueued, status) +} + +func (s *ServiceTestSuite) Test_Network_CheckForPendingLANRequest_Error_DifferentName_DeleteRequest() { + requests := []sdk.Request{ + { + Id: ptr.To("1"), + Metadata: &sdk.RequestMetadata{ + RequestStatus: &sdk.RequestStatus{ + Metadata: &sdk.RequestStatusMetadata{ + Targets: &[]sdk.RequestTarget{ + { + Target: &sdk.ResourceReference{ + Id: ptr.To("different"), + }, + }, + }, + Status: ptr.To(sdk.RequestStatusQueued), + Message: ptr.To("test"), + }, + }, + }, + }, + } + s.getRequestsCall().Return(requests, nil).Once() + status, err := s.service.checkForPendingLANRequest(http.MethodDelete, lanID) + s.NoError(err) + s.Empty(status) +} + +func (s *ServiceTestSuite) Test_Network_CheckForPendingLANRequest_Error_DifferentName_PostRequest() { + requests := []sdk.Request{ + { + Id: ptr.To("1"), + Metadata: &sdk.RequestMetadata{ + RequestStatus: &sdk.RequestStatus{ + Metadata: &sdk.RequestStatusMetadata{ + Status: ptr.To(sdk.RequestStatusQueued), + Message: ptr.To("test"), + }, + }, + }, + Properties: &sdk.RequestProperties{ + Method: ptr.To(http.MethodPost), + Body: ptr.To(`{"properties": {"name": "different"}}`), + }, + }, + } + s.getRequestsCall().Return(requests, nil).Once() + status, err := s.service.checkForPendingLANRequest(http.MethodPost, "") + s.NoError(err) + s.Empty(status) +} + +func (s *ServiceTestSuite) Test_Network_CheckForPendingLANRequest_Error_UnsupportedMethod() { + request, err := s.service.checkForPendingLANRequest(http.MethodTrace, lanID) + s.Error(err) + s.Empty(request) +} + +func (s *ServiceTestSuite) Test_Network_CheckForPendingLANRequest_Error_Delete_NoLanID() { + request, err := s.service.checkForPendingLANRequest(http.MethodDelete, "") + s.Error(err) + s.Empty(request) +} + +func (s *ServiceTestSuite) Test_Network_CheckForPendingLANRequest_Error_Post_WithLanID() { + request, err := s.service.checkForPendingLANRequest(http.MethodPost, lanID) + s.Error(err) + s.Empty(request) +} + +func (s *ServiceTestSuite) Test_Network_RemoveLANPendingRequestFromCluster_Successful() { + s.infraCluster.Status.CurrentRequestByDatacenter = map[string]infrav1.ProvisioningRequest{ + s.service.dataCenterID(): { + RequestPath: reqPath, + Method: http.MethodDelete, + State: sdk.RequestStatusQueued, + }, + } + s.NoError(s.service.removeLANPendingRequestFromCluster()) + s.NotContains(s.infraCluster.Status.CurrentRequestByDatacenter, s.service.dataCenterID(), "request should be removed from status") +} + +func (s *ServiceTestSuite) Test_Network_RemoveLANPendingRequestFromCluster_NoRequest() { + s.NoError(s.service.removeLANPendingRequestFromCluster()) +} + +func (s *ServiceTestSuite) Test_Network_ReconcileLAN_NoExistingLAN_NoRequest_Create() { + s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{}}, nil).Once() + s.getRequestsCall().Return([]sdk.Request{}, nil).Once() + s.createLANCall().Return(reqPath, nil).Once() + requeue, err := s.service.ReconcileLAN() + s.NoError(err) + s.True(requeue) + s.Contains(s.infraCluster.Status.CurrentRequestByDatacenter, s.service.dataCenterID()) + req := s.infraCluster.Status.CurrentRequestByDatacenter[s.service.dataCenterID()] + s.Equal(reqPath, req.RequestPath, "Request path is different than expected") + s.Equal(http.MethodPost, req.Method, "Request method is different than expected") + s.Equal(infrav1.RequestStatusQueued, req.State, "Request state is different than expected") +} + +func (s *ServiceTestSuite) Test_Network_ReconcileLAN_NoExistingLAN_ExistingRequest_NotFailed() { + testCases := []struct { + name string + status string + }{ + { + name: "request is queued", + status: sdk.RequestStatusQueued, + }, + { + name: "request is running", + status: sdk.RequestStatusRunning, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{}}, nil).Once() + s.getRequestsCall().Return(s.examplePostRequest(tc.status), nil).Once() + requeue, err := s.service.ReconcileLAN() + s.NoError(err) + s.True(requeue) + }) + } +} + +func (s *ServiceTestSuite) Test_Network_ReconcileLAN_NoExistingLAN_ExistingRequest_Failed_Create() { + s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{}}, nil).Once() + s.getRequestsCall().Return(s.examplePostRequest(sdk.RequestStatusFailed), nil).Once() + s.createLANCall().Return(reqPath, nil).Once() + requeue, err := s.service.ReconcileLAN() + s.NoError(err) + s.True(requeue) +} + +func (s *ServiceTestSuite) Test_Network_ReconcileLAN_NoExistingLAN_ExistingRequest_Done_Retry() { + s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{}}, nil).Once() + s.getRequestsCall().Return(s.examplePostRequest(sdk.RequestStatusDone), nil).Once() + s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{s.exampleLAN()}}, nil).Once() + requeue, err := s.service.ReconcileLAN() + s.NoError(err) + s.False(requeue) +} + +func (s *ServiceTestSuite) Test_Network_ReconcileLAN_ExistingLAN() { + s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{s.exampleLAN()}}, nil).Once() + requeue, err := s.service.ReconcileLAN() + s.NoError(err) + s.False(requeue) +} + +func (s *ServiceTestSuite) Test_Network_ReconcileLANDelete_LANExists_NoPendingRequests_NoOtherUsers_Delete() { + s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{s.exampleLAN()}}, nil).Once() + s.getRequestsCall().Return([]sdk.Request{}, nil).Once() + s.deleteLANCall(lanID).Return(reqPath, nil).Once() + requeue, err := s.service.ReconcileLANDeletion() + s.NoError(err) + s.True(requeue) + s.Contains(s.infraCluster.Status.CurrentRequestByDatacenter, s.service.dataCenterID()) + req := s.infraCluster.Status.CurrentRequestByDatacenter[s.service.dataCenterID()] + s.Equal(reqPath, req.RequestPath, "Request path is different than expected") + s.Equal(http.MethodDelete, req.Method, "Request method is different than expected") + s.Equal(infrav1.RequestStatusQueued, req.State, "Request state is different than expected") +} + +func (s *ServiceTestSuite) Test_Network_ReconcileLANDelete_LANExists_NoPendingRequests_HasOtherUsers_NoDelete() { + lan := s.exampleLAN() + lan.Entities.Nics.Items = &[]sdk.Nic{{Id: ptr.To("1")}} + s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{lan}}, nil).Once() + s.getRequestsCall().Return([]sdk.Request{}, nil).Once() + requeue, err := s.service.ReconcileLANDeletion() + s.NoError(err) + s.False(requeue) + s.NotContains(s.infraCluster.Status.CurrentRequestByDatacenter, s.service.dataCenterID()) +} + +func (s *ServiceTestSuite) Test_Network_ReconcileLANDelete_LANExists_ExistingRequest_InProgress() { + testCases := []struct { + name string + status string + }{ + { + name: "request is queued", + status: sdk.RequestStatusQueued, + }, + { + name: "request is running", + status: sdk.RequestStatusRunning, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{s.exampleLAN()}}, nil).Once() + requests := s.exampleDeleteRequest(tc.status) + s.getRequestsCall().Return(requests, nil).Once() + requeue, err := s.service.ReconcileLANDeletion() + s.NoError(err) + s.True(requeue) + }) + } +} + +func (s *ServiceTestSuite) Test_Network_ReconcileLANDelete_LANExists_ExistingRequest_Failed() { + s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{s.exampleLAN()}}, nil).Once() + s.getRequestsCall().Return(s.exampleDeleteRequest(sdk.RequestStatusFailed), nil).Once() + s.deleteLANCall(lanID).Return(reqPath, nil).Once() + requeue, err := s.service.ReconcileLANDeletion() + s.NoError(err) + s.True(requeue) +} + +func (s *ServiceTestSuite) Test_Network_ReconcileLANDelete_LANExists_ExistingRequest_Done() { + s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{s.exampleLAN()}}, nil).Once() + s.getRequestsCall().Return(s.exampleDeleteRequest(sdk.RequestStatusDone), nil).Once() + s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{}}, nil).Once() + requeue, err := s.service.ReconcileLANDeletion() + s.NoError(err) + s.False(requeue) + s.NotContains(s.infraCluster.Status.CurrentRequestByDatacenter, s.service.dataCenterID()) +} + +func (s *ServiceTestSuite) Test_Network_ReconcileLANDelete_LANDoesNotExist() { + s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{}}, nil).Once() + requeue, err := s.service.ReconcileLANDeletion() + s.NoError(err) + s.False(requeue) +} + +func (s *ServiceTestSuite) Test_Network_ReconcileLANDelete_NoLANExists() { + s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{}}, nil).Once() + requeue, err := s.service.ReconcileLANDeletion() + s.NoError(err) + s.False(requeue) + s.NotContains(s.infraCluster.Status.CurrentRequestByDatacenter, s.service.dataCenterID()) +} + +const ( + reqPath = "this/is/a/path" + lanID = "1" +) + +func (s *ServiceTestSuite) exampleLAN() sdk.Lan { + return sdk.Lan{ + Id: ptr.To(lanID), + Properties: &sdk.LanProperties{ + Name: ptr.To(s.service.lanName()), + }, + Entities: &sdk.LanEntities{ + Nics: &sdk.LanNics{ + Items: &[]sdk.Nic{}, + }, + }, + } +} + +func (s *ServiceTestSuite) examplePostRequest(status string) []sdk.Request { + body := fmt.Sprintf(`{"properties": {"name": "%s"}}`, s.service.lanName()) + return []sdk.Request{ + { + Id: ptr.To("1"), + Metadata: &sdk.RequestMetadata{ + RequestStatus: &sdk.RequestStatus{ + Metadata: &sdk.RequestStatusMetadata{ + Status: ptr.To(status), + Message: ptr.To("test"), + }, + }, + }, + Properties: &sdk.RequestProperties{ + Method: ptr.To(http.MethodPost), + Body: ptr.To(body), + }, + }, + } +} + +func (s *ServiceTestSuite) exampleDeleteRequest(status string) []sdk.Request { + return []sdk.Request{ + { + Id: ptr.To("1"), + Metadata: &sdk.RequestMetadata{ + RequestStatus: &sdk.RequestStatus{ + Metadata: &sdk.RequestStatusMetadata{ + Status: ptr.To(status), + Message: ptr.To("test"), + Targets: &[]sdk.RequestTarget{ + { + Target: &sdk.ResourceReference{Id: ptr.To(lanID)}, + }, + }, + }, + }, + }, + }, + } +} + +func (s *ServiceTestSuite) createLANCall() *clienttest.MockClient_CreateLAN_Call { + return s.ionosClient.EXPECT().CreateLAN(s.ctx, s.service.dataCenterID(), sdk.LanPropertiesPost{ + Name: ptr.To(s.service.lanName()), + Public: ptr.To(true), + }) +} + +func (s *ServiceTestSuite) deleteLANCall(id string) *clienttest.MockClient_DeleteLAN_Call { + return s.ionosClient.EXPECT().DeleteLAN(s.ctx, s.service.dataCenterID(), id) +} + +func (s *ServiceTestSuite) listLANsCall() *clienttest.MockClient_ListLANs_Call { + return s.ionosClient.EXPECT().ListLANs(s.ctx, s.service.dataCenterID()) +} + +func (s *ServiceTestSuite) getRequestsCall() *clienttest.MockClient_GetRequests_Call { + return s.ionosClient.EXPECT().GetRequests(s.ctx, mock.Anything, + path.Join("datacenters", s.service.dataCenterID(), "lans")) +} diff --git a/internal/service/cloud/service.go b/internal/service/cloud/service.go index fce54c53..aff60a5f 100644 --- a/internal/service/cloud/service.go +++ b/internal/service/cloud/service.go @@ -46,3 +46,8 @@ func NewService(ctx context.Context, s *scope.MachineScope) (*Service, error) { func (s *Service) api() ionoscloud.Client { return s.scope.ClusterScope.IonosClient } + +// dataCenterID is a shortcut for getting the data center ID used by the IONOS Cloud machine. +func (s *Service) dataCenterID() string { + return s.scope.IonosMachine.Spec.DataCenterID +} diff --git a/internal/service/cloud/datacenter.go b/internal/service/cloud/service_test.go similarity index 70% rename from internal/service/cloud/datacenter.go rename to internal/service/cloud/service_test.go index 55b42fbe..33305f6a 100644 --- a/internal/service/cloud/datacenter.go +++ b/internal/service/cloud/service_test.go @@ -16,7 +16,10 @@ limitations under the License. package cloud -// dataCenterID is a shortcut for getting the data center ID used by the IONOS Cloud machine. -func (s *Service) dataCenterID() string { - return s.scope.IonosMachine.Spec.DataCenterID +func (s *ServiceTestSuite) TestDatacenterID() { + s.Equal(s.service.scope.IonosMachine.Spec.DataCenterID, s.service.dataCenterID()) +} + +func (s *ServiceTestSuite) TestAPI() { + s.Equal(s.service.scope.ClusterScope.IonosClient, s.service.api()) } diff --git a/internal/service/cloud/suite_test.go b/internal/service/cloud/suite_test.go new file mode 100644 index 00000000..2ca55eda --- /dev/null +++ b/internal/service/cloud/suite_test.go @@ -0,0 +1,153 @@ +/* +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 cloud + +import ( + "context" + "testing" + + "github.com/go-logr/logr" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + 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" + clienttest "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud/clienttest" + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/scope" +) + +type ServiceTestSuite struct { + *require.Assertions + suite.Suite + k8sClient client.Client + ctx context.Context + machineScope *scope.MachineScope + clusterScope *scope.ClusterScope + log logr.Logger + service *Service + capiCluster *clusterv1.Cluster + capiMachine *clusterv1.Machine + infraCluster *infrav1.IonosCloudCluster + infraMachine *infrav1.IonosCloudMachine + ionosClient *clienttest.MockClient +} + +func (s *ServiceTestSuite) SetupSuite() { + s.log = logr.Discard() + s.ctx = context.Background() + s.Assertions = s.Require() +} + +func TestServiceTestSuite(t *testing.T) { + suite.Run(t, new(ServiceTestSuite)) +} + +func (s *ServiceTestSuite) SetupTest() { + var err error + s.ionosClient = &clienttest.MockClient{} + + s.capiCluster = &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: metav1.NamespaceDefault, + Name: "test-cluster", + }, + Spec: clusterv1.ClusterSpec{ + Paused: false, + }, + } + s.infraCluster = &infrav1.IonosCloudCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: metav1.NamespaceDefault, + Name: s.capiCluster.Name, + }, + Spec: infrav1.IonosCloudClusterSpec{ + ContractNumber: "12345678", + }, + Status: infrav1.IonosCloudClusterStatus{}, + } + s.capiMachine = &clusterv1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: metav1.NamespaceDefault, + Name: "test-machine", + }, + Spec: clusterv1.MachineSpec{ + ClusterName: s.capiCluster.Name, + Version: ptr.To("v1.26.12"), + ProviderID: ptr.To("ionos://dd426c63-cd1d-4c02-aca3-13b4a27c2ebf"), + }, + } + s.infraMachine = &infrav1.IonosCloudMachine{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: metav1.NamespaceDefault, + Name: "test-machine", + }, + Spec: infrav1.IonosCloudMachineSpec{ + ProviderID: "ionos://8c19a898-fda9-4783-a939-d778aeee217f", + DataCenterID: "ccf27092-34e8-499e-a2f5-2bdee9d34a12", + NumCores: 2, + AvailabilityZone: infrav1.AvailabilityZoneAuto, + MemoryMB: 4096, + CPUFamily: "AMD_OPTERON", + Disk: &infrav1.Volume{ + Name: "test-machine-hdd", + DiskType: infrav1.VolumeDiskTypeHDD, + SizeGB: 20, + AvailabilityZone: infrav1.AvailabilityZoneAuto, + SSHKeys: []string{"ssh-rsa AAAAB3Nz"}, + }, + }, + Status: infrav1.IonosCloudMachineStatus{}, + } + + scheme := runtime.NewScheme() + s.NoError(clusterv1.AddToScheme(scheme), "failed to extend scheme with Cluster API types") + s.NoError(infrav1.AddToScheme(scheme), "failed to extend scheme with IonosCloud types") + + initObjects := []client.Object{s.infraMachine, s.infraCluster, s.capiCluster, s.capiMachine} + s.k8sClient = fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(initObjects...). + WithStatusSubresource(initObjects...). + Build() + + s.clusterScope, err = scope.NewClusterScope(scope.ClusterScopeParams{ + Client: s.k8sClient, + Logger: &s.log, + Cluster: s.capiCluster, + IonosCluster: s.infraCluster, + IonosClient: s.ionosClient, + }) + s.NoError(err, "failed to create cluster scope") + + s.machineScope, err = scope.NewMachineScope(scope.MachineScopeParams{ + Client: s.k8sClient, + Logger: &s.log, + Cluster: s.capiCluster, + Machine: s.capiMachine, + ClusterScope: s.clusterScope, + IonosMachine: s.infraMachine, + }) + s.NoError(err, "failed to create machine scope") + + s.service, err = NewService(s.ctx, s.machineScope) + s.NoError(err, "failed to create service") +} From 703ba4f4f12ad08ed066f6b0e85bf3434155a264 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 22 Jan 2024 11:11:41 +0100 Subject: [PATCH 101/109] patch cluster with owned ready condition --- scope/cluster.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scope/cluster.go b/scope/cluster.go index 355289ac..16ba2785 100644 --- a/scope/cluster.go +++ b/scope/cluster.go @@ -111,7 +111,11 @@ func (c *ClusterScope) PatchObject() error { timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() - return c.patchHelper.Patch(timeoutCtx, c.IonosCluster) + return c.patchHelper.Patch(timeoutCtx, c.IonosCluster, patch.WithOwnedConditions{ + Conditions: []clusterv1.ConditionType{ + clusterv1.ReadyCondition, + }, + }) } // Finalize will make sure to apply a patch to the current IonosCloudCluster. From 245def93b08c1c669cd3a2c92f6aa54030611298 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 22 Jan 2024 11:19:01 +0100 Subject: [PATCH 102/109] adds gci to linters --- .golangci.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.golangci.yml b/.golangci.yml index a493ac78..d6509124 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -6,6 +6,7 @@ linters: - dogsled - errcheck - exportloopref + - gci - ginkgolinter - goconst - gocritic @@ -42,6 +43,14 @@ linters-settings: - [] # 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 severity: error + gci: + sections: + - standard + - default + - prefix(github.com/ionos-cloud/cluster-api-provider-ionoscloud) + - blank + - dot + goimports: local-prefixes: github.com/ionos-cloud/cluster-api-provider-ionoscloud/ @@ -94,6 +103,9 @@ issues: - (Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less) - (G104|G307) exclude-rules: + - linters: + - gci + path: main\.go - linters: - gosec text: "G108: Profiling endpoint is automatically exposed on /debug/pprof" From 8d6c45468a65bb8acc41d941505a1b34c94e0db2 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 22 Jan 2024 11:20:16 +0100 Subject: [PATCH 103/109] run gci linter --- api/v1alpha1/ionoscloudcluster_types_test.go | 5 +++-- api/v1alpha1/ionoscloudmachine_types_test.go | 12 ++++++------ api/v1alpha1/suite_test.go | 5 +++-- internal/controller/ionoscloudmachine_controller.go | 8 +++----- internal/controller/suite_test.go | 5 +++-- scope/cluster.go | 3 +-- scope/machine.go | 8 +++----- scope/machine_test.go | 3 +-- 8 files changed, 23 insertions(+), 26 deletions(-) diff --git a/api/v1alpha1/ionoscloudcluster_types_test.go b/api/v1alpha1/ionoscloudcluster_types_test.go index 61105410..af16335f 100644 --- a/api/v1alpha1/ionoscloudcluster_types_test.go +++ b/api/v1alpha1/ionoscloudcluster_types_test.go @@ -20,13 +20,14 @@ import ( "context" "testing" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" "sigs.k8s.io/cluster-api/util/conditions" "sigs.k8s.io/controller-runtime/pkg/client" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" ) func TestIonosCloudCluster_Conditions(t *testing.T) { diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index 3492d24e..0e0c6a6d 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -20,16 +20,16 @@ import ( "context" "github.com/google/go-cmp/cmp" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "sigs.k8s.io/cluster-api/errors" - - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/cluster-api/errors" "sigs.k8s.io/cluster-api/util/conditions" "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" ) func defaultMachine() *IonosCloudMachine { diff --git a/api/v1alpha1/suite_test.go b/api/v1alpha1/suite_test.go index efc2b39e..96f12c78 100644 --- a/api/v1alpha1/suite_test.go +++ b/api/v1alpha1/suite_test.go @@ -20,13 +20,14 @@ import ( "path/filepath" "testing" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" ) var k8sClient client.Client diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index 6dec26b7..791e591b 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -22,11 +22,6 @@ import ( "fmt" "time" - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/service/cloud" - - "sigs.k8s.io/cluster-api/util/conditions" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "github.com/go-logr/logr" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" @@ -34,12 +29,15 @@ import ( clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" "sigs.k8s.io/cluster-api/util" "sigs.k8s.io/cluster-api/util/annotations" + "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/controllerutil" "sigs.k8s.io/controller-runtime/pkg/handler" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud" + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/service/cloud" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/scope" ) diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index 32837925..910be4a9 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -22,8 +22,6 @@ import ( "runtime" "testing" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" @@ -32,6 +30,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" ) // These tests use Ginkgo (BDD-style Go testing framework). Refer to diff --git a/scope/cluster.go b/scope/cluster.go index 16ba2785..caaa24cf 100644 --- a/scope/cluster.go +++ b/scope/cluster.go @@ -23,9 +23,8 @@ import ( "fmt" "time" - "k8s.io/client-go/util/retry" - "github.com/go-logr/logr" + "k8s.io/client-go/util/retry" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" "sigs.k8s.io/cluster-api/util/conditions" "sigs.k8s.io/cluster-api/util/patch" diff --git a/scope/machine.go b/scope/machine.go index 362e8d25..19b6f67b 100644 --- a/scope/machine.go +++ b/scope/machine.go @@ -22,14 +22,12 @@ import ( "fmt" "time" - ctrl "sigs.k8s.io/controller-runtime" - - "k8s.io/client-go/util/retry" - "sigs.k8s.io/cluster-api/util/conditions" - "github.com/go-logr/logr" + "k8s.io/client-go/util/retry" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + "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" diff --git a/scope/machine_test.go b/scope/machine_test.go index f265b85e..063ae123 100644 --- a/scope/machine_test.go +++ b/scope/machine_test.go @@ -19,8 +19,6 @@ package scope import ( "testing" - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" - "github.com/go-logr/logr" "github.com/stretchr/testify/require" "k8s.io/client-go/kubernetes/scheme" @@ -29,6 +27,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" ) func exampleParams(t *testing.T) MachineScopeParams { From 4e30a8e716c0bd9035241ff2e645b51a0f0c15dc Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 22 Jan 2024 11:57:35 +0100 Subject: [PATCH 104/109] make use of proper grammar --- internal/util/ptr/ptr.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/util/ptr/ptr.go b/internal/util/ptr/ptr.go index f72a7dc7..17221915 100644 --- a/internal/util/ptr/ptr.go +++ b/internal/util/ptr/ptr.go @@ -22,8 +22,8 @@ func To[T any](v T) *T { return &v } -// Deref dereferences attempts to dereference a pointer -// and returns the default value if the pointer is nil. +// 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 { if ptr != nil { return *ptr From 7476303a0cae487b027df046517778be4b5fbc95 Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 22 Jan 2024 13:05:40 +0100 Subject: [PATCH 105/109] remove IONOS --- .codespellignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.codespellignore b/.codespellignore index 4a82a4bd..ce223b56 100644 --- a/.codespellignore +++ b/.codespellignore @@ -1,4 +1,3 @@ capi capic decorder -IONOS From 11ed9383a6209f10c8b227333edcd3be05f2684c Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 22 Jan 2024 13:11:40 +0100 Subject: [PATCH 106/109] enhance error logic in getClusterScope --- internal/controller/ionoscloudmachine_controller.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index 791e591b..cfd0fe83 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -242,8 +242,13 @@ func (r *IonosCloudMachineReconciler) getClusterScope( } if err := r.Client.Get(ctx, infraClusterName, ionosCloudCluster); err != nil { - // IonosCloudCluster is not ready - return nil, nil //nolint:nilerr + if apierrors.IsNotFound(err) { + // Cluster has not yet been created + return nil, nil + } + // We at most expect that the cluster cannot be found. + // If the error is different, we should return that particular error. + return nil, err } // Create the cluster scope From 052f3130d49e0992c0eafc4531cb2488c429186e Mon Sep 17 00:00:00 2001 From: Ludwig Bedacht Date: Mon, 22 Jan 2024 13:15:01 +0100 Subject: [PATCH 107/109] mimimimi --- internal/controller/ionoscloudmachine_controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index cfd0fe83..4ba9630b 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -244,7 +244,7 @@ func (r *IonosCloudMachineReconciler) getClusterScope( if err := r.Client.Get(ctx, infraClusterName, ionosCloudCluster); err != nil { if apierrors.IsNotFound(err) { // Cluster has not yet been created - return nil, nil + return nil, nil } // We at most expect that the cluster cannot be found. // If the error is different, we should return that particular error. From 3e3224aabb65e34996a92775a78a4a4542ca3594 Mon Sep 17 00:00:00 2001 From: Matthias Bastian Date: Tue, 23 Jan 2024 13:08:26 +0100 Subject: [PATCH 108/109] Refactor request queue inspection (#33) --- api/v1alpha1/ionoscloudmachine_types.go | 4 +- .../ionoscloudmachine_controller.go | 5 + internal/ionoscloud/client/client.go | 5 +- internal/service/cloud/network.go | 227 ++++------- internal/service/cloud/network_test.go | 372 +++++------------- internal/service/cloud/request.go | 212 ++++++++++ internal/service/cloud/request_test.go | 200 ++++++++++ internal/service/cloud/suite_test.go | 45 +++ 8 files changed, 650 insertions(+), 420 deletions(-) create mode 100644 internal/service/cloud/request.go create mode 100644 internal/service/cloud/request_test.go diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index 51d8ea44..08deb5ff 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -36,11 +36,11 @@ const ( // the underlying VM. MachineProvisionedCondition clusterv1.ConditionType = "MachineProvisioned" - // WaitingForClusterInfrastructureReason (Severity=Info) indicates, that the IonosCloudMachine is currently + // WaitingForClusterInfrastructureReason (Severity=Info) indicates that the IonosCloudMachine is currently // waiting for the cluster infrastructure to become ready. WaitingForClusterInfrastructureReason = "WaitingForClusterInfrastructure" - // WaitingForBootstrapDataReason (Severity=Info) indicates, that the bootstrap provider has not yet finished + // WaitingForBootstrapDataReason (Severity=Info) indicates that the bootstrap provider has not yet finished // creating the bootstrap data secret and store it in the Cluster API Machine. WaitingForBootstrapDataReason = "WaitingForBootstrapData" ) diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index 4ba9630b..76a31db0 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -193,6 +193,7 @@ func (r *IonosCloudMachineReconciler) reconcileNormal(machineScope *scope.Machin // * Failed => We need to discuss this, log error and continue (retry last request in the corresponding reconcile function) // Ensure that a LAN is created in the data center + // TODO(piepmatz): This is not thread-safe, but needs to be. Add locking. if requeue, err := cloudService.ReconcileLAN(); err != nil || requeue { if requeue { return ctrl.Result{RequeueAfter: defaultReconcileDuration}, err @@ -204,6 +205,10 @@ func (r *IonosCloudMachineReconciler) reconcileNormal(machineScope *scope.Machin } func (r *IonosCloudMachineReconciler) reconcileDelete(cloudService *cloud.Service) (ctrl.Result, error) { + // TODO(piepmatz): This is not thread-safe, but needs to be. Add locking. + // Moreover, should only be attempted if it's the last machine using that LAN. We should check that our machines + // at least, but need to accept that users added their own infrastructure into our LAN (in that case a LAN deletion + // attempt will be denied with HTTP 422). requeue, err := cloudService.ReconcileLANDeletion() if err != nil { return ctrl.Result{}, fmt.Errorf("could not reconcile LAN deletion: %w", err) diff --git a/internal/ionoscloud/client/client.go b/internal/ionoscloud/client/client.go index 3872993b..6a33f020 100644 --- a/internal/ionoscloud/client/client.go +++ b/internal/ionoscloud/client/client.go @@ -31,9 +31,10 @@ import ( const ( depthRequestsMetadataStatusMetadata = 2 // for LISTing requests and their metadata status metadata + depthLANEntities = 2 // for LISTing LANs and their NICs (w/o NIC details) ) -// IonosCloudClient is a concrete implementation of the Client interface defined in the internal client package, that +// IonosCloudClient is a concrete implementation of the Client interface defined in the internal client package that // communicates with Cloud API using its SDK. type IonosCloudClient struct { API *sdk.APIClient @@ -189,7 +190,7 @@ func (c *IonosCloudClient) ListLANs(ctx context.Context, dataCenterID string) (* if dataCenterID == "" { return nil, errDataCenterIDIsEmpty } - lans, _, err := c.API.LANsApi.DatacentersLansGet(ctx, dataCenterID).Execute() + lans, _, err := c.API.LANsApi.DatacentersLansGet(ctx, dataCenterID).Depth(depthLANEntities).Execute() if err != nil { return nil, fmt.Errorf(apiCallErrWrapper, err) } diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index fac3b0ed..7b92e669 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -17,14 +17,11 @@ limitations under the License. package cloud import ( - "errors" "fmt" "net/http" "path" - "strings" sdk "github.com/ionos-cloud/sdk-go/v6" - "k8s.io/apimachinery/pkg/util/json" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" @@ -38,45 +35,38 @@ func (s *Service) lanName() string { s.scope.ClusterScope.Cluster.Name) } +func (s *Service) lanURL(id string) string { + return path.Join("datacenters", s.dataCenterID(), "lans", id) +} + +func (s *Service) lansURL() string { + return path.Join("datacenters", s.dataCenterID(), "lans") +} + // ReconcileLAN ensures the cluster LAN exist, creating one if it doesn't. func (s *Service) ReconcileLAN() (requeue bool, err error) { log := s.scope.Logger.WithName("ReconcileLAN") - // try to retrieve the cluster LAN - clusterLAN, err := s.GetLAN() - if clusterLAN != nil || err != nil { - // If we found the LAN, we don't need to create one. - // TODO(lubedacht) check if patching is required => future task. - return false, err - } - - // if we didn't find a LAN, we check if a LAN is already in creation - requestStatus, err := s.checkForPendingLANRequest(http.MethodPost, "") + lan, request, err := findResource( + func() (*sdk.Lan, error) { return s.getLAN() }, + func() (*requestInfo, error) { return s.getLatestLANCreationRequest() }, + ) if err != nil { - return false, fmt.Errorf("unable to list pending LAN requests: %w", err) + return false, err } - // We want to requeue and check again after some time - if requestStatus == sdk.RequestStatusRunning || requestStatus == sdk.RequestStatusQueued { - log.Info("Request is ongoing, re-triggering reconciliaton", "request status", requestStatus) + if request != nil && request.isPending() { + // We want to requeue and check again after some time + log.Info("Request is pending", "location", request.location) return true, nil } - // check again as the request might be done right after we checked - // to prevent duplicate creation - if requestStatus == sdk.RequestStatusDone { - clusterLAN, err = s.GetLAN() - if clusterLAN != nil || err != nil { - return false, err + if lan != nil { + if state := getState(lan); !isAvailable(state) { + log.Info("LAN is not available yet", "state", state) + return true, nil } - - // If we still don't get a LAN here even though we found request, which was done - // the LAN was probably deleted before. - // Therefore, we will attempt to create the LAN again. - // - // TODO(lubedacht) - // Another solution would be to query for a deletion request and check if the created time - // is bigger than the created time of the LAN POST request. + return false, nil } log.V(4).Info("No LAN was found. Creating new LAN") @@ -88,8 +78,54 @@ func (s *Service) ReconcileLAN() (requeue bool, err error) { return true, nil } -// GetLAN tries to retrieve the cluster related LAN in the data center. -func (s *Service) GetLAN() (*sdk.Lan, error) { +// ReconcileLANDeletion ensures there's no cluster LAN available, requesting for deletion (if no other resource +// uses it) otherwise. +func (s *Service) ReconcileLANDeletion() (requeue bool, err error) { + log := s.scope.Logger.WithName("ReconcileLANDeletion") + + // Try to retrieve the cluster LAN or even check if it's currently still being created. + lan, request, err := findResource( + func() (*sdk.Lan, error) { return s.getLAN() }, + func() (*requestInfo, error) { return s.getLatestLANCreationRequest() }, + ) + if err != nil { + return false, err + } + + if request != nil && request.isPending() { + // We want to requeue and check again after some time + log.Info("Creation request is pending", "location", request.location) + return true, nil + } + + if lan == nil { + err = s.removeLANPendingRequestFromCluster() + return err != nil, err + } + + // If we found a LAN, we check if there is a deletion already in progress. + request, err = s.getLatestLANDeletionRequest(*lan.Id) + if err != nil { + return false, err + } + if request != nil && request.isPending() { + // We want to requeue and check again after some time + log.Info("Deletion request is pending", "location", request.location) + return true, nil + } + + if len(*lan.Entities.Nics.Items) > 0 { + log.Info("The cluster LAN is still being used by another resource. Skipping deletion.") + return false, nil + } + + // Request for LAN deletion + err = s.deleteLAN(*lan.Id) + return err == nil, err +} + +// getLAN tries to retrieve the cluster-related LAN in the data center. +func (s *Service) getLAN() (*sdk.Lan, error) { // check if the LAN exists lans, err := s.api().ListLANs(s.ctx, s.dataCenterID()) if err != nil { @@ -119,58 +155,6 @@ func (s *Service) GetLAN() (*sdk.Lan, error) { return foundLAN, nil } -// ReconcileLANDeletion ensures there's no cluster LAN available, requesting for deletion (if no other resource -// uses it) otherwise. -func (s *Service) ReconcileLANDeletion() (requeue bool, err error) { - log := s.scope.Logger.WithName("ReconcileLANDeletion") - - // try to retrieve the cluster LAN - clusterLAN, err := s.GetLAN() - if err != nil { - return false, err - } - if clusterLAN == nil { - err = s.removeLANPendingRequestFromCluster() - return err != nil, err - } - - // if we found a LAN, we check if there is a deletion already in process - requestStatus, err := s.checkForPendingLANRequest(http.MethodDelete, *clusterLAN.Id) - if err != nil { - return false, fmt.Errorf("unable to list pending LAN requests: %w", err) - } - if requestStatus != "" { - // We want to requeue and check again after some time - if requestStatus == sdk.RequestStatusRunning || requestStatus == sdk.RequestStatusQueued { - return true, nil - } - - if requestStatus == sdk.RequestStatusDone { - // Here we can check if the LAN is indeed gone or there's some inconsistency in the last request or - // this request points to an old, far gone LAN with the same ID. - clusterLAN, err = s.GetLAN() - if err != nil { - return false, err - } - if clusterLAN == nil { - err = s.removeLANPendingRequestFromCluster() - return err != nil, err - } - } - } - - if clusterLAN != nil && clusterLAN.Entities.HasNics() && len(*clusterLAN.Entities.Nics.Items) > 0 { - log.Info("the cluster LAN is still being used by another resource. skipping deletion") - return false, nil - } - // Request for LAN destruction - err = s.deleteLAN(*clusterLAN.Id) - if err != nil { - return false, err - } - return true, nil -} - func (s *Service) createLAN() error { log := s.scope.Logger.WithName("createLAN") @@ -226,64 +210,23 @@ func (s *Service) deleteLAN(lanID string) error { return nil } -// checkForPendingLANRequest checks if there is a request for the creation, update or deletion of a LAN in the data center. -// For update and deletion requests, it is also necessary to provide the LAN ID (value will be ignored for creation). -func (s *Service) checkForPendingLANRequest(method string, lanID string) (status string, err error) { - switch method { - case http.MethodPost: - if lanID != "" { - return status, errors.New("lanID must be empty for POST requests") - } - case http.MethodDelete: - if lanID == "" { - return "", errors.New("lanID cannot be empty for DELETE requests") - } - default: - return "", fmt.Errorf("unsupported method %s, allowed methods are %s", method, strings.Join( - []string{http.MethodPost, http.MethodDelete}, - ",", - )) - } - - lanPath := path.Join("datacenters", s.dataCenterID(), "lans") - requests, err := s.api().GetRequests(s.ctx, method, lanPath) - if err != nil { - return "", fmt.Errorf("could not get requests: %w", err) - } - - expectedLANName := s.lanName() - for _, r := range requests { - if method == http.MethodDelete { - targets := *r.Metadata.RequestStatus.Metadata.Targets - if len(targets) == 0 { - continue - } - - if *targets[0].Target.Id != lanID { - continue - } - } else { - var lan sdk.Lan - err = json.Unmarshal([]byte(*r.Properties.Body), &lan) - if err != nil { - return "", fmt.Errorf("could not unmarshal request into LAN: %w", err) - } - if *lan.Properties.Name != expectedLANName { - continue - } - } - - status := *r.Metadata.RequestStatus.Metadata.Status - if status == sdk.RequestStatusFailed { - // We just log the error but do not return it, so we can retry the request. - message := r.Metadata.RequestStatus.Metadata.Message - s.scope.Logger.WithValues("requestID", r.Id, "requestStatus", status, "message", *message). - Error(nil, "last request for LAN has failed. logging it for debugging purposes") - } +func (s *Service) getLatestLANCreationRequest() (*requestInfo, error) { + return getMatchingRequest( + s, + http.MethodPost, + path.Join("datacenters", s.dataCenterID(), "lans"), + func(resource sdk.Lan, _ sdk.Request) bool { + return *resource.Properties.Name == s.lanName() + }, + ) +} - return status, nil - } - return "", nil +func (s *Service) getLatestLANDeletionRequest(lanID string) (*requestInfo, error) { + return getMatchingRequest[sdk.Lan]( + s, + http.MethodDelete, + path.Join("datacenters", s.dataCenterID(), "lans", lanID), + ) } func (s *Service) removeLANPendingRequestFromCluster() error { diff --git a/internal/service/cloud/network_test.go b/internal/service/cloud/network_test.go index db80ce3a..c756bfde 100644 --- a/internal/service/cloud/network_test.go +++ b/internal/service/cloud/network_test.go @@ -19,22 +19,38 @@ package cloud import ( "fmt" "net/http" - "path" + "testing" sdk "github.com/ionos-cloud/sdk-go/v6" - "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" clienttest "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud/clienttest" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" ) -func (s *ServiceTestSuite) TestNetworkLANName() { +type lanSuite struct { + ServiceTestSuite +} + +func TestLANSuite(t *testing.T) { + suite.Run(t, new(lanSuite)) +} + +func (s *lanSuite) TestNetworkLANName() { s.Equal("k8s-default-test-cluster", s.service.lanName()) } -func (s *ServiceTestSuite) Test_Network_CreateLAN_Successful() { - s.createLANCall().Return(reqPath, nil).Once() +func (s *lanSuite) TestLANURL() { + s.Equal("datacenters/"+s.service.dataCenterID()+"/lans/1", s.service.lanURL("1")) +} + +func (s *lanSuite) TestLANURLs() { + s.Equal("datacenters/"+s.service.dataCenterID()+"/lans", s.service.lansURL()) +} + +func (s *lanSuite) Test_Network_CreateLAN_Successful() { + s.mockCreateLANCall().Return(reqPath, nil).Once() s.NoError(s.service.createLAN()) s.Contains(s.infraCluster.Status.CurrentRequestByDatacenter, s.service.dataCenterID(), "request should be stored in status") req := s.infraCluster.Status.CurrentRequestByDatacenter[s.service.dataCenterID()] @@ -43,8 +59,8 @@ func (s *ServiceTestSuite) Test_Network_CreateLAN_Successful() { s.Equal(infrav1.RequestStatusQueued, req.State, "request status should be stored in status") } -func (s *ServiceTestSuite) Test_Network_DeleteLAN_Successful() { - s.deleteLANCall(lanID).Return(reqPath, nil).Once() +func (s *lanSuite) Test_Network_DeleteLAN_Successful() { + s.mockDeleteLANCall(lanID).Return(reqPath, nil).Once() s.NoError(s.service.deleteLAN(lanID)) s.Contains(s.infraCluster.Status.CurrentRequestByDatacenter, s.service.dataCenterID(), "request should be stored in status") req := s.infraCluster.Status.CurrentRequestByDatacenter[s.service.dataCenterID()] @@ -53,139 +69,30 @@ func (s *ServiceTestSuite) Test_Network_DeleteLAN_Successful() { s.Equal(infrav1.RequestStatusQueued, req.State, "request status should be stored in status") } -func (s *ServiceTestSuite) Test_Network_GetLAN_Successful() { +func (s *lanSuite) Test_Network_GetLAN_Successful() { lan := s.exampleLAN() - s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{lan}}, nil).Once() - foundLAN, err := s.service.GetLAN() + s.mockListLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{lan}}, nil).Once() + foundLAN, err := s.service.getLAN() s.NoError(err) s.NotNil(foundLAN) s.Equal(lan, *foundLAN) } -func (s *ServiceTestSuite) Test_Network_GetLAN_NotFound() { - s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{}}, nil).Once() - lan, err := s.service.GetLAN() +func (s *lanSuite) Test_Network_GetLAN_NotFound() { + s.mockListLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{}}, nil).Once() + lan, err := s.service.getLAN() s.NoError(err) s.Nil(lan) } -func (s *ServiceTestSuite) Test_Network_GetLAN_Error_NotUnique() { - s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{s.exampleLAN(), s.exampleLAN()}}, nil).Once() - lan, err := s.service.GetLAN() +func (s *lanSuite) Test_Network_GetLAN_Error_NotUnique() { + s.mockListLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{s.exampleLAN(), s.exampleLAN()}}, nil).Once() + lan, err := s.service.getLAN() s.Error(err) s.Nil(lan) } -func (s *ServiceTestSuite) Test_Network_CheckForPendingLANRequest_NoPendingRequest() { - s.getRequestsCall().Return([]sdk.Request{}, nil).Once() - request, err := s.service.checkForPendingLANRequest(http.MethodDelete, lanID) - s.NoError(err) - s.Empty(request) -} - -func (s *ServiceTestSuite) Test_Network_CheckForPendingLANRequest_DeleteRequest() { - s.getRequestsCall().Return([]sdk.Request{ - { - Id: ptr.To("1"), - Metadata: &sdk.RequestMetadata{ - RequestStatus: &sdk.RequestStatus{ - Metadata: &sdk.RequestStatusMetadata{ - Targets: &[]sdk.RequestTarget{ - { - Target: &sdk.ResourceReference{ - Id: ptr.To(lanID), - }, - }, - }, - Status: ptr.To(sdk.RequestStatusQueued), - Message: ptr.To("test"), - }, - }, - }, - }, - }, nil).Once() - status, err := s.service.checkForPendingLANRequest(http.MethodDelete, lanID) - s.NoError(err) - s.Equal(sdk.RequestStatusQueued, status) -} - -func (s *ServiceTestSuite) Test_Network_CheckForPendingLANRequest_PostRequest() { - s.getRequestsCall().Return(s.examplePostRequest(sdk.RequestStatusQueued), nil).Once() - status, err := s.service.checkForPendingLANRequest(http.MethodPost, "") - s.NoError(err) - s.Equal(sdk.RequestStatusQueued, status) -} - -func (s *ServiceTestSuite) Test_Network_CheckForPendingLANRequest_Error_DifferentName_DeleteRequest() { - requests := []sdk.Request{ - { - Id: ptr.To("1"), - Metadata: &sdk.RequestMetadata{ - RequestStatus: &sdk.RequestStatus{ - Metadata: &sdk.RequestStatusMetadata{ - Targets: &[]sdk.RequestTarget{ - { - Target: &sdk.ResourceReference{ - Id: ptr.To("different"), - }, - }, - }, - Status: ptr.To(sdk.RequestStatusQueued), - Message: ptr.To("test"), - }, - }, - }, - }, - } - s.getRequestsCall().Return(requests, nil).Once() - status, err := s.service.checkForPendingLANRequest(http.MethodDelete, lanID) - s.NoError(err) - s.Empty(status) -} - -func (s *ServiceTestSuite) Test_Network_CheckForPendingLANRequest_Error_DifferentName_PostRequest() { - requests := []sdk.Request{ - { - Id: ptr.To("1"), - Metadata: &sdk.RequestMetadata{ - RequestStatus: &sdk.RequestStatus{ - Metadata: &sdk.RequestStatusMetadata{ - Status: ptr.To(sdk.RequestStatusQueued), - Message: ptr.To("test"), - }, - }, - }, - Properties: &sdk.RequestProperties{ - Method: ptr.To(http.MethodPost), - Body: ptr.To(`{"properties": {"name": "different"}}`), - }, - }, - } - s.getRequestsCall().Return(requests, nil).Once() - status, err := s.service.checkForPendingLANRequest(http.MethodPost, "") - s.NoError(err) - s.Empty(status) -} - -func (s *ServiceTestSuite) Test_Network_CheckForPendingLANRequest_Error_UnsupportedMethod() { - request, err := s.service.checkForPendingLANRequest(http.MethodTrace, lanID) - s.Error(err) - s.Empty(request) -} - -func (s *ServiceTestSuite) Test_Network_CheckForPendingLANRequest_Error_Delete_NoLanID() { - request, err := s.service.checkForPendingLANRequest(http.MethodDelete, "") - s.Error(err) - s.Empty(request) -} - -func (s *ServiceTestSuite) Test_Network_CheckForPendingLANRequest_Error_Post_WithLanID() { - request, err := s.service.checkForPendingLANRequest(http.MethodPost, lanID) - s.Error(err) - s.Empty(request) -} - -func (s *ServiceTestSuite) Test_Network_RemoveLANPendingRequestFromCluster_Successful() { +func (s *lanSuite) Test_Network_RemoveLANPendingRequestFromCluster_Successful() { s.infraCluster.Status.CurrentRequestByDatacenter = map[string]infrav1.ProvisioningRequest{ s.service.dataCenterID(): { RequestPath: reqPath, @@ -197,155 +104,83 @@ func (s *ServiceTestSuite) Test_Network_RemoveLANPendingRequestFromCluster_Succe s.NotContains(s.infraCluster.Status.CurrentRequestByDatacenter, s.service.dataCenterID(), "request should be removed from status") } -func (s *ServiceTestSuite) Test_Network_RemoveLANPendingRequestFromCluster_NoRequest() { +func (s *lanSuite) Test_Network_RemoveLANPendingRequestFromCluster_NoRequest() { s.NoError(s.service.removeLANPendingRequestFromCluster()) } -func (s *ServiceTestSuite) Test_Network_ReconcileLAN_NoExistingLAN_NoRequest_Create() { - s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{}}, nil).Once() - s.getRequestsCall().Return([]sdk.Request{}, nil).Once() - s.createLANCall().Return(reqPath, nil).Once() +func (s *lanSuite) Test_Network_ReconcileLAN_NoExistingLAN_NoRequest_Create() { + s.mockListLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{}}, nil).Once() + s.mockGetLANCreationRequestsCall().Return([]sdk.Request{}, nil).Once() + s.mockCreateLANCall().Return(reqPath, nil).Once() requeue, err := s.service.ReconcileLAN() s.NoError(err) s.True(requeue) - s.Contains(s.infraCluster.Status.CurrentRequestByDatacenter, s.service.dataCenterID()) - req := s.infraCluster.Status.CurrentRequestByDatacenter[s.service.dataCenterID()] - s.Equal(reqPath, req.RequestPath, "Request path is different than expected") - s.Equal(http.MethodPost, req.Method, "Request method is different than expected") - s.Equal(infrav1.RequestStatusQueued, req.State, "Request state is different than expected") } -func (s *ServiceTestSuite) Test_Network_ReconcileLAN_NoExistingLAN_ExistingRequest_NotFailed() { - testCases := []struct { - name string - status string - }{ - { - name: "request is queued", - status: sdk.RequestStatusQueued, - }, - { - name: "request is running", - status: sdk.RequestStatusRunning, - }, - } - - for _, tc := range testCases { - s.Run(tc.name, func() { - s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{}}, nil).Once() - s.getRequestsCall().Return(s.examplePostRequest(tc.status), nil).Once() - requeue, err := s.service.ReconcileLAN() - s.NoError(err) - s.True(requeue) - }) - } -} - -func (s *ServiceTestSuite) Test_Network_ReconcileLAN_NoExistingLAN_ExistingRequest_Failed_Create() { - s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{}}, nil).Once() - s.getRequestsCall().Return(s.examplePostRequest(sdk.RequestStatusFailed), nil).Once() - s.createLANCall().Return(reqPath, nil).Once() +func (s *lanSuite) Test_Network_ReconcileLAN_NoExistingLAN_ExistingRequest_Pending() { + s.mockListLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{}}, nil).Once() + s.mockGetLANCreationRequestsCall().Return(s.examplePostRequest(sdk.RequestStatusQueued), nil).Once() requeue, err := s.service.ReconcileLAN() s.NoError(err) s.True(requeue) } -func (s *ServiceTestSuite) Test_Network_ReconcileLAN_NoExistingLAN_ExistingRequest_Done_Retry() { - s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{}}, nil).Once() - s.getRequestsCall().Return(s.examplePostRequest(sdk.RequestStatusDone), nil).Once() - s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{s.exampleLAN()}}, nil).Once() +func (s *lanSuite) Test_Network_ReconcileLAN_ExistingLAN() { + s.mockListLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{s.exampleLAN()}}, nil).Once() requeue, err := s.service.ReconcileLAN() s.NoError(err) s.False(requeue) } -func (s *ServiceTestSuite) Test_Network_ReconcileLAN_ExistingLAN() { - s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{s.exampleLAN()}}, nil).Once() +func (s *lanSuite) Test_Network_ReconcileLAN_ExistingLAN_Unavailable() { + lan := s.exampleLAN() + lan.Metadata.State = ptr.To("BUSY") + s.mockListLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{lan}}, nil).Once() requeue, err := s.service.ReconcileLAN() s.NoError(err) - s.False(requeue) + s.True(requeue) } -func (s *ServiceTestSuite) Test_Network_ReconcileLANDelete_LANExists_NoPendingRequests_NoOtherUsers_Delete() { - s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{s.exampleLAN()}}, nil).Once() - s.getRequestsCall().Return([]sdk.Request{}, nil).Once() - s.deleteLANCall(lanID).Return(reqPath, nil).Once() +func (s *lanSuite) Test_Network_ReconcileLANDelete_LANExists_NoPendingRequests_NoOtherUsers_Delete() { + s.mockListLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{s.exampleLAN()}}, nil).Once() + s.mockGetLANDeletionRequestsCall().Return([]sdk.Request{}, nil).Once() + s.mockDeleteLANCall(lanID).Return(reqPath, nil).Once() requeue, err := s.service.ReconcileLANDeletion() s.NoError(err) s.True(requeue) - s.Contains(s.infraCluster.Status.CurrentRequestByDatacenter, s.service.dataCenterID()) - req := s.infraCluster.Status.CurrentRequestByDatacenter[s.service.dataCenterID()] - s.Equal(reqPath, req.RequestPath, "Request path is different than expected") - s.Equal(http.MethodDelete, req.Method, "Request method is different than expected") - s.Equal(infrav1.RequestStatusQueued, req.State, "Request state is different than expected") } -func (s *ServiceTestSuite) Test_Network_ReconcileLANDelete_LANExists_NoPendingRequests_HasOtherUsers_NoDelete() { +func (s *lanSuite) Test_Network_ReconcileLANDelete_LANExists_NoPendingRequests_HasOtherUsers_NoDelete() { lan := s.exampleLAN() lan.Entities.Nics.Items = &[]sdk.Nic{{Id: ptr.To("1")}} - s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{lan}}, nil).Once() - s.getRequestsCall().Return([]sdk.Request{}, nil).Once() + s.mockListLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{lan}}, nil).Once() + s.mockGetLANDeletionRequestsCall().Return([]sdk.Request{}, nil).Once() requeue, err := s.service.ReconcileLANDeletion() s.NoError(err) s.False(requeue) s.NotContains(s.infraCluster.Status.CurrentRequestByDatacenter, s.service.dataCenterID()) } -func (s *ServiceTestSuite) Test_Network_ReconcileLANDelete_LANExists_ExistingRequest_InProgress() { - testCases := []struct { - name string - status string - }{ - { - name: "request is queued", - status: sdk.RequestStatusQueued, - }, - { - name: "request is running", - status: sdk.RequestStatusRunning, - }, - } - - for _, tc := range testCases { - s.Run(tc.name, func() { - s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{s.exampleLAN()}}, nil).Once() - requests := s.exampleDeleteRequest(tc.status) - s.getRequestsCall().Return(requests, nil).Once() - requeue, err := s.service.ReconcileLANDeletion() - s.NoError(err) - s.True(requeue) - }) - } -} - -func (s *ServiceTestSuite) Test_Network_ReconcileLANDelete_LANExists_ExistingRequest_Failed() { - s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{s.exampleLAN()}}, nil).Once() - s.getRequestsCall().Return(s.exampleDeleteRequest(sdk.RequestStatusFailed), nil).Once() - s.deleteLANCall(lanID).Return(reqPath, nil).Once() +func (s *lanSuite) Test_Network_ReconcileLANDelete_NoExistingLAN_ExistingRequest_Pending() { + s.mockListLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{}}, nil).Once() + s.mockGetLANCreationRequestsCall().Return(s.examplePostRequest(sdk.RequestStatusQueued), nil).Once() requeue, err := s.service.ReconcileLANDeletion() s.NoError(err) s.True(requeue) } -func (s *ServiceTestSuite) Test_Network_ReconcileLANDelete_LANExists_ExistingRequest_Done() { - s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{s.exampleLAN()}}, nil).Once() - s.getRequestsCall().Return(s.exampleDeleteRequest(sdk.RequestStatusDone), nil).Once() - s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{}}, nil).Once() - requeue, err := s.service.ReconcileLANDeletion() - s.NoError(err) - s.False(requeue) - s.NotContains(s.infraCluster.Status.CurrentRequestByDatacenter, s.service.dataCenterID()) -} - -func (s *ServiceTestSuite) Test_Network_ReconcileLANDelete_LANDoesNotExist() { - s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{}}, nil).Once() +func (s *lanSuite) Test_Network_ReconcileLANDelete_LANExists_ExistingRequest_Pending() { + s.mockListLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{s.exampleLAN()}}, nil).Once() + requests := s.exampleDeleteRequest(sdk.RequestStatusQueued) + s.mockGetLANDeletionRequestsCall().Return(requests, nil).Once() requeue, err := s.service.ReconcileLANDeletion() s.NoError(err) - s.False(requeue) + s.True(requeue) } -func (s *ServiceTestSuite) Test_Network_ReconcileLANDelete_NoLANExists() { - s.listLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{}}, nil).Once() +func (s *lanSuite) Test_Network_ReconcileLANDelete_LANDoesNotExist() { + s.mockListLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{}}, nil).Once() + s.mockGetLANCreationRequestsCall().Return(nil, nil).Once() requeue, err := s.service.ReconcileLANDeletion() s.NoError(err) s.False(requeue) @@ -357,12 +192,15 @@ const ( lanID = "1" ) -func (s *ServiceTestSuite) exampleLAN() sdk.Lan { +func (s *lanSuite) exampleLAN() sdk.Lan { return sdk.Lan{ Id: ptr.To(lanID), Properties: &sdk.LanProperties{ Name: ptr.To(s.service.lanName()), }, + Metadata: &sdk.DatacenterElementMetadata{ + State: ptr.To(stateAvailable), + }, Entities: &sdk.LanEntities{ Nics: &sdk.LanNics{ Items: &[]sdk.Nic{}, @@ -371,64 +209,50 @@ func (s *ServiceTestSuite) exampleLAN() sdk.Lan { } } -func (s *ServiceTestSuite) examplePostRequest(status string) []sdk.Request { - body := fmt.Sprintf(`{"properties": {"name": "%s"}}`, s.service.lanName()) - return []sdk.Request{ - { - Id: ptr.To("1"), - Metadata: &sdk.RequestMetadata{ - RequestStatus: &sdk.RequestStatus{ - Metadata: &sdk.RequestStatusMetadata{ - Status: ptr.To(status), - Message: ptr.To("test"), - }, - }, - }, - Properties: &sdk.RequestProperties{ - Method: ptr.To(http.MethodPost), - Body: ptr.To(body), - }, - }, +func (s *lanSuite) examplePostRequest(status string) []sdk.Request { + opts := requestBuildOptions{ + status: status, + method: http.MethodPost, + url: s.service.lansURL(), + body: fmt.Sprintf(`{"properties": {"name": "%s"}}`, s.service.lanName()), + href: reqPath, + targetID: lanID, + targetType: sdk.LAN, } + return []sdk.Request{s.exampleRequest(opts)} } -func (s *ServiceTestSuite) exampleDeleteRequest(status string) []sdk.Request { - return []sdk.Request{ - { - Id: ptr.To("1"), - Metadata: &sdk.RequestMetadata{ - RequestStatus: &sdk.RequestStatus{ - Metadata: &sdk.RequestStatusMetadata{ - Status: ptr.To(status), - Message: ptr.To("test"), - Targets: &[]sdk.RequestTarget{ - { - Target: &sdk.ResourceReference{Id: ptr.To(lanID)}, - }, - }, - }, - }, - }, - }, +func (s *lanSuite) exampleDeleteRequest(status string) []sdk.Request { + opts := requestBuildOptions{ + status: status, + method: http.MethodDelete, + url: s.service.lanURL(lanID), + href: reqPath, + targetID: lanID, + targetType: sdk.LAN, } + return []sdk.Request{s.exampleRequest(opts)} } -func (s *ServiceTestSuite) createLANCall() *clienttest.MockClient_CreateLAN_Call { +func (s *lanSuite) mockCreateLANCall() *clienttest.MockClient_CreateLAN_Call { return s.ionosClient.EXPECT().CreateLAN(s.ctx, s.service.dataCenterID(), sdk.LanPropertiesPost{ Name: ptr.To(s.service.lanName()), Public: ptr.To(true), }) } -func (s *ServiceTestSuite) deleteLANCall(id string) *clienttest.MockClient_DeleteLAN_Call { +func (s *lanSuite) mockDeleteLANCall(id string) *clienttest.MockClient_DeleteLAN_Call { return s.ionosClient.EXPECT().DeleteLAN(s.ctx, s.service.dataCenterID(), id) } -func (s *ServiceTestSuite) listLANsCall() *clienttest.MockClient_ListLANs_Call { +func (s *lanSuite) mockListLANsCall() *clienttest.MockClient_ListLANs_Call { return s.ionosClient.EXPECT().ListLANs(s.ctx, s.service.dataCenterID()) } -func (s *ServiceTestSuite) getRequestsCall() *clienttest.MockClient_GetRequests_Call { - return s.ionosClient.EXPECT().GetRequests(s.ctx, mock.Anything, - path.Join("datacenters", s.service.dataCenterID(), "lans")) +func (s *lanSuite) mockGetLANCreationRequestsCall() *clienttest.MockClient_GetRequests_Call { + return s.ionosClient.EXPECT().GetRequests(s.ctx, http.MethodPost, s.service.lansURL()) +} + +func (s *lanSuite) mockGetLANDeletionRequestsCall() *clienttest.MockClient_GetRequests_Call { + return s.ionosClient.EXPECT().GetRequests(s.ctx, http.MethodDelete, s.service.lanURL(lanID)) } diff --git a/internal/service/cloud/request.go b/internal/service/cloud/request.go new file mode 100644 index 00000000..60638b1c --- /dev/null +++ b/internal/service/cloud/request.go @@ -0,0 +1,212 @@ +/* +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 cloud + +import ( + "encoding/json" + "fmt" + "strings" + + sdk "github.com/ionos-cloud/sdk-go/v6" +) + +// resourceTypeMap maps a resource type to its corresponding IONOS Cloud type identifier. +// Each type mapping for usage in getMatchingRequest() needs to be present here. +var resourceTypeMap = map[any]sdk.Type{ + sdk.Lan{}: sdk.LAN, +} + +// getMatchingRequest is a helper function intended for finding a single request based on certain filtering constraints, +// such as HTTP method and URL. +// Requests only containing the given URL, but not ending with it, will be ignored. Note that query parameters are +// stripped before the comparison. +// The function is generic, but only supports types that are present in resourceTypeMap. +// Moreover, it optionally allows to specify additional matcher functions for the found resource and the request it was +// found in. If multiple matchers are given, all need to match. +// If no matching request is found, nil is returned. +func getMatchingRequest[T any]( + s *Service, + method string, + url string, + matchers ...func(resource T, request sdk.Request) bool, +) (*requestInfo, error) { + var zeroResource T + resourceType := resourceTypeMap[zeroResource] + if resourceType == "" { + return nil, fmt.Errorf("unsupported resource type %T", zeroResource) + } + + // As we later on ignore query parameters in found requests, we need to do the same here for consistency. + urlWithoutQueryParams := strings.Split(url, "?")[0] + + requests, err := s.api().GetRequests(s.ctx, method, urlWithoutQueryParams) + if err != nil { + return nil, fmt.Errorf("could not get requests: %w", err) + } + +requestLoop: + for _, req := range requests { + // We only want to look at requests for the desired resource type. We ignore requests for other types. + // We perform this check because we might deal with a request for URL /resource/123/subresource/456 when looking + // for /resource/123. + // In theory the other URL check below is sufficient, but better be safe than sorry. + if !hasRequestTargetType(req, resourceType) { + continue + } + + // Compare the given URL with the one found in the request. We ignore query parameters here as well. + // For normalization, we also remove trailing slashes. + // 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.Properties.Url, "?")[0] + trimmedRequestURL = strings.TrimSuffix(trimmedRequestURL, "/") + trimmedURL := strings.TrimSuffix(urlWithoutQueryParams, "/") + if !strings.HasSuffix(trimmedRequestURL, trimmedURL) { + continue + } + + if len(matchers) > 0 { + // As at least 1 additional matcher function is given, reconstruct the resource from the request body. + var unmarshalled T + if err = json.Unmarshal([]byte(*req.Properties.Body), &unmarshalled); err != nil { + s.scope.Logger.WithValues("requestID", *req.Id, "body", *req.Properties.Body). + Info("could not unmarshal request") + return nil, fmt.Errorf("could not unmarshal request into %T: %w", unmarshalled, err) + } + + for _, matcher := range matchers { + if !matcher(unmarshalled, req) { + // All matcher functions need to match the current resource. + continue requestLoop + } + } + } + + status := *req.Metadata.RequestStatus.Metadata.Status + if status == sdk.RequestStatusFailed { + message := req.Metadata.RequestStatus.Metadata.Message + s.scope.Logger.Error(nil, + "Last request has failed, logging it for debugging purposes", + "resourceType", resourceType, + "requestID", req.Id, "requestStatus", status, + "message", *message, + ) + } + + return &requestInfo{ + status: status, + location: *req.Metadata.RequestStatus.Href, + }, nil + } + return nil, nil +} + +func hasRequestTargetType(req sdk.Request, typeName sdk.Type) bool { + if req.Metadata.RequestStatus.Metadata.Targets == nil || len(*req.Metadata.RequestStatus.Metadata.Targets) == 0 { + return false + } + return *(*req.Metadata.RequestStatus.Metadata.Targets)[0].Target.Type == typeName +} + +// findResource is a helper function intended for finding a single resource based on certain filtering constraints, +// such as a unique name. It lists and filters the existing resources and checks the request queue for matching +// creations. +// The function expects two callbacks: +// - A function listing and looking for a single resource, returning its pointer if found. If errors occur or multiple +// resources match the constraints, it returns an error. If no resource it found, it returns nil. +// - A function checking the request queue for a matching resource creation and returning information about the +// request if found. If no request is found, nil is returned. If errors occur, they are returned. +// +// As request queue lookups are rather expensive, we list and filter first. If no resource is found, we check the +// request queue. If a request is found and if it is reported as DONE, we assume a possible race condition: +// When initially listing the resources, the request was possibly not DONE yet. Not it is, so we list and filter again +// to see if the resource was created in the meantime. +// As this process is similar independent of the resource type, this generic function helps to reduce boilerplate. +func findResource[T any]( + listAndFilter func() (*T, error), + checkQueue func() (*requestInfo, error), +) ( + resource *T, + request *requestInfo, + err error, +) { + resource, err = listAndFilter() + if err != nil { + return nil, nil, err // Found multiple resources or another error occurred. + } + if resource != nil { + return resource, nil, nil // Something was found, great. No need to look any further. + } + + request, err = checkQueue() + if err != nil { + return nil, nil, err + } + + if request == nil { + return nil, nil, nil // resource not found + } + + if request.isDone() { + // To prevent duplicate creation, check again. The request might have been completed right after we listed + // initially. + // Note that it can happen that even now we don't find a resource. This can happen if we found an old creation + // request, but the resource was already deleted later on. + resource, err = listAndFilter() + if err != nil { + return nil, nil, err // Found multiple resources or another error occurred. + } + if resource != nil { + return resource, nil, nil // Something was found this time, great. + } + } + + return nil, request, nil // found no existing resource, but at least a matching request +} + +// requestInfo is a stripped-down version of sdk.RequestStatus, containing only the request status and the request's +// location URL which can be used for polling. +type requestInfo struct { + status string + location string +} + +// isPending returns true if the request was not finished yet, i.e. it's still queued or currently running. +func (ri *requestInfo) isPending() bool { + return ri.status == sdk.RequestStatusQueued || ri.status == sdk.RequestStatusRunning +} + +// isDone returns true if the request was finished successfully. +func (ri *requestInfo) isDone() bool { + return ri.status == sdk.RequestStatusDone +} + +type metadataHolder interface { + GetMetadata() *sdk.DatacenterElementMetadata +} + +func getState(resource metadataHolder) string { + return *resource.GetMetadata().State +} + +const stateAvailable = "AVAILABLE" + +// isAvailable returns true if the resource is available. Note that not all resource types have this state. +func isAvailable(state string) bool { + return state == stateAvailable +} diff --git a/internal/service/cloud/request_test.go b/internal/service/cloud/request_test.go new file mode 100644 index 00000000..f0304645 --- /dev/null +++ b/internal/service/cloud/request_test.go @@ -0,0 +1,200 @@ +/* +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 cloud + +import ( + "fmt" + "net/http" + "strings" + "testing" + + sdk "github.com/ionos-cloud/sdk-go/v6" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" +) + +type getMatchingRequestSuite struct { + ServiceTestSuite +} + +func TestGetMatchingRequestTestSuite(t *testing.T) { + suite.Run(t, new(getMatchingRequestSuite)) +} + +func (s *getMatchingRequestSuite) examplePostRequest(href, status string) sdk.Request { + // we use a LAN as an example target type here + opts := requestBuildOptions{ + status: status, + method: http.MethodPost, + url: "https://url.tld/path/?depth=10", + body: fmt.Sprintf(`{"properties": {"name": "%s"}}`, s.service.lanName()), + href: href, + targetID: "1", + targetType: sdk.LAN, + } + return s.exampleRequest(opts) +} + +func (s *getMatchingRequestSuite) TestUnsupportedResourceType() { + request, err := getMatchingRequest[int]( + s.service, + http.MethodPost, + "/path", + ) + s.ErrorContains(err, "unsupported") + s.Nil(request) +} + +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) + + // req2 has a mismatch in its URL + req2 := s.examplePostRequest("req2", sdk.RequestStatusQueued) + *req2.Properties.Url = "https://url.tld/path/action?depth=10" + + // req3 doesn't fulfill the matcher function + req3 := s.examplePostRequest("req3", sdk.RequestStatusQueued) + renamed := strings.Replace(*req3.Properties.Body, s.service.lanName(), "wrongName", 1) + req3.Properties.Body = ptr.To(renamed) + + // req4 is the one we want to find + req4 := s.examplePostRequest("req4", sdk.RequestStatusFailed) + + // req5 would also match, but req4 is found first + req5 := s.examplePostRequest("req6", sdk.RequestStatusDone) + + s.ionosClient.EXPECT().GetRequests(s.ctx, http.MethodPost, "path"). + Return([]sdk.Request{req1, req2, req3, req4, req5}, nil) + + request, err := getMatchingRequest( + s.service, + http.MethodPost, + "path?foo=bar&baz=qux", + func(resource sdk.Lan, _ sdk.Request) bool { + return *resource.Properties.Name == s.service.lanName() + }, + ) + s.NoError(err) + s.NotNil(request) + s.Equal("req4", request.location) + s.Equal(sdk.RequestStatusFailed, request.status) +} + +func TestHasRequestTargetType(t *testing.T) { + req := sdk.Request{ + Metadata: &sdk.RequestMetadata{ + RequestStatus: &sdk.RequestStatus{ + Metadata: &sdk.RequestStatusMetadata{}, + }, + }, + } + require.False(t, hasRequestTargetType(req, sdk.LAN)) + + req.Metadata.RequestStatus.Metadata.Targets = &[]sdk.RequestTarget{} + require.False(t, hasRequestTargetType(req, sdk.LAN)) + + req.Metadata.RequestStatus.Metadata.Targets = &[]sdk.RequestTarget{ + { + Target: &sdk.ResourceReference{Type: ptr.To(sdk.SERVER)}, + }, + } + require.False(t, hasRequestTargetType(req, sdk.LAN)) + + (*req.Metadata.RequestStatus.Metadata.Targets)[0].Target.Type = ptr.To(sdk.LAN) + require.True(t, hasRequestTargetType(req, sdk.LAN)) +} + +type findResourceSuite struct { + ServiceTestSuite +} + +func TestFindResourceTestSuite(t *testing.T) { + suite.Run(t, new(findResourceSuite)) +} + +func (s *findResourceSuite) TestListingIsEnough() { + resource, request, err := findResource( + func() (*int, error) { return ptr.To(42), nil }, + func() (*requestInfo, error) { panic("don't call me") }, + ) + s.NoError(err) + s.Nil(request) + s.NotNil(resource) + s.Equal(42, *resource) +} + +func (s *findResourceSuite) TestFoundRequest() { + wantedRequest := &requestInfo{status: sdk.RequestStatusQueued} + + resource, gotRequest, err := findResource( + func() (*int, error) { return nil, nil }, + func() (*requestInfo, error) { return wantedRequest, nil }, + ) + s.NoError(err) + s.Nil(resource) + s.NotNil(gotRequest) + s.Equal(wantedRequest, gotRequest) +} + +func (s *findResourceSuite) TestFoundOnSecondListing() { + listCalls := 0 + resource, gotRequest, err := findResource( + func() (*int, error) { + listCalls++ + if listCalls == 1 { + return nil, nil + } + return ptr.To(42), nil + }, + func() (*requestInfo, error) { return &requestInfo{status: sdk.RequestStatusDone}, nil }, + ) + s.Equal(2, listCalls) + s.NoError(err) + s.Nil(gotRequest) + s.NotNil(resource) + s.Equal(42, *resource) +} + +func TestRequestInfo(t *testing.T) { + req := requestInfo{status: sdk.RequestStatusFailed} + require.False(t, req.isPending()) + require.False(t, req.isDone()) + + req.status = sdk.RequestStatusQueued + require.True(t, req.isPending()) + require.False(t, req.isDone()) + + req.status = sdk.RequestStatusRunning + require.True(t, req.isPending()) + require.False(t, req.isDone()) + + req.status = sdk.RequestStatusDone + require.False(t, req.isPending()) + require.True(t, req.isDone()) +} + +func TestMetadataHolder(t *testing.T) { + lan1 := &sdk.Lan{Metadata: &sdk.DatacenterElementMetadata{State: ptr.To("BUSY")}} + lan2 := &sdk.Lan{Metadata: &sdk.DatacenterElementMetadata{State: ptr.To(stateAvailable)}} + + require.False(t, isAvailable(getState(lan1))) + require.True(t, isAvailable(getState(lan2))) +} diff --git a/internal/service/cloud/suite_test.go b/internal/service/cloud/suite_test.go index 2ca55eda..b91bd7fe 100644 --- a/internal/service/cloud/suite_test.go +++ b/internal/service/cloud/suite_test.go @@ -21,6 +21,7 @@ import ( "testing" "github.com/go-logr/logr" + sdk "github.com/ionos-cloud/sdk-go/v6" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -151,3 +152,47 @@ func (s *ServiceTestSuite) SetupTest() { s.service, err = NewService(s.ctx, s.machineScope) s.NoError(err, "failed to create service") } + +type requestBuildOptions struct { + status, + method, + url, + body, + href, + requestID, + targetID string + targetType sdk.Type +} + +func (s *ServiceTestSuite) exampleRequest(opts requestBuildOptions) sdk.Request { + req := sdk.Request{ + Id: ptr.To(opts.requestID), + Metadata: &sdk.RequestMetadata{ + RequestStatus: &sdk.RequestStatus{ + Href: ptr.To(opts.href), + Metadata: &sdk.RequestStatusMetadata{ + Status: ptr.To(opts.status), + Message: ptr.To("test"), + }, + }, + }, + Properties: &sdk.RequestProperties{ + Url: ptr.To(opts.url), + Method: ptr.To(opts.method), + Body: ptr.To(opts.body), + }, + } + + if opts.targetType != "" || opts.targetID != "" { + req.Metadata.RequestStatus.Metadata.Targets = &[]sdk.RequestTarget{ + { + Target: &sdk.ResourceReference{ + Id: ptr.To(opts.targetID), + Type: ptr.To(opts.targetType), + }, + }, + } + } + + return req +} From 3b3321c3dd5a97f089f5a1db28ed71ad1531852e Mon Sep 17 00:00:00 2001 From: Matthias Bastian Date: Tue, 23 Jan 2024 13:35:18 +0100 Subject: [PATCH 109/109] Unify "datacenter" spelling In var names etc., we treat it as a single world, e.g. `datacenter`, so no camel casing. In text, we treat it as two words: `data center`. Also remove unused data center-related IONOS client methods. --- api/v1alpha1/ionoscloudcluster_types.go | 4 +- api/v1alpha1/ionoscloudmachine_types.go | 4 +- api/v1alpha1/ionoscloudmachine_types_test.go | 10 +- ...e.cluster.x-k8s.io_ionoscloudmachines.yaml | 2 +- internal/ionoscloud/client.go | 29 +- internal/ionoscloud/client/client.go | 119 +++--- internal/ionoscloud/client/errors.go | 2 +- internal/ionoscloud/clienttest/mock_client.go | 342 ++++++------------ internal/service/cloud/network.go | 24 +- internal/service/cloud/network_test.go | 26 +- internal/service/cloud/service.go | 6 +- internal/service/cloud/service_test.go | 2 +- internal/service/cloud/suite_test.go | 2 +- 13 files changed, 217 insertions(+), 355 deletions(-) diff --git a/api/v1alpha1/ionoscloudcluster_types.go b/api/v1alpha1/ionoscloudcluster_types.go index a274a3b1..4186fb18 100644 --- a/api/v1alpha1/ionoscloudcluster_types.go +++ b/api/v1alpha1/ionoscloudcluster_types.go @@ -100,9 +100,9 @@ func (i *IonosCloudCluster) SetConditions(conditions clusterv1.Conditions) { // SetCurrentRequest sets the current provisioning request for the given data center. // This function makes sure that the map is initialized before setting the request. -func (i *IonosCloudCluster) SetCurrentRequest(dataCenterID string, request ProvisioningRequest) { +func (i *IonosCloudCluster) SetCurrentRequest(datacenterID string, request ProvisioningRequest) { if i.Status.CurrentRequestByDatacenter == nil { i.Status.CurrentRequestByDatacenter = map[string]ProvisioningRequest{} } - i.Status.CurrentRequestByDatacenter[dataCenterID] = request + i.Status.CurrentRequestByDatacenter[datacenterID] = request } diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index 08deb5ff..b6fb8903 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -79,10 +79,10 @@ type IonosCloudMachineSpec struct { // +optional ProviderID string `json:"providerID,omitempty"` - // DataCenterID is the ID of the data center where the machine should be created in. + // DatacenterID is the ID of the data center where the machine should be created in. // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="datacenterID is immutable" // +kubebuilder:validation:Format=uuid - DataCenterID string `json:"datacenterID"` + DatacenterID string `json:"datacenterID"` // NumCores defines the number of cores for the VM. // +kubebuilder:validation:Minimum=1 diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index 0e0c6a6d..65b5f17e 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -40,7 +40,7 @@ func defaultMachine() *IonosCloudMachine { }, Spec: IonosCloudMachineSpec{ ProviderID: "ionos://ee090ff2-1eef-48ec-a246-a51a33aa4f3a", - DataCenterID: "ee090ff2-1eef-48ec-a246-a51a33aa4f3a", + DatacenterID: "ee090ff2-1eef-48ec-a246-a51a33aa4f3a", NumCores: 1, AvailabilityZone: AvailabilityZoneTwo, MemoryMB: 2048, @@ -92,21 +92,21 @@ var _ = Describe("IonosCloudMachine Tests", func() { Context("Data center ID", func() { It("should fail if not set", func() { m := defaultMachine() - m.Spec.DataCenterID = "" + m.Spec.DatacenterID = "" Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }) It("should fail if not a UUID", func() { m := defaultMachine() want := "not-a-UUID" - m.Spec.DataCenterID = want + m.Spec.DatacenterID = want Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) }) It("should be immutable", func() { m := defaultMachine() Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) - Expect(m.Spec.DataCenterID).To(Equal(defaultMachine().Spec.DataCenterID)) - m.Spec.DataCenterID = "6ded8c5f-8df2-46ef-b4ce-61833daf0961" + Expect(m.Spec.DatacenterID).To(Equal(defaultMachine().Spec.DatacenterID)) + m.Spec.DatacenterID = "6ded8c5f-8df2-46ef-b4ce-61833daf0961" Expect(k8sClient.Update(context.Background(), m)).ToNot(Succeed()) }) }) 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 d2078bb8..9c73765a 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml @@ -68,7 +68,7 @@ spec: minLength: 1 type: string datacenterID: - description: DataCenterID is the ID of the data center where the machine + description: DatacenterID is the ID of the data center where the machine should be created in. format: uuid type: string diff --git a/internal/ionoscloud/client.go b/internal/ionoscloud/client.go index 0b081ed3..27c38ac9 100644 --- a/internal/ionoscloud/client.go +++ b/internal/ionoscloud/client.go @@ -25,37 +25,32 @@ import ( // Client is an interface for abstracting Cloud API SDK, making it possible to create mocks for testing purposes. type Client interface { - // CreateDataCenter creates a new data center with its specification based on provided properties. - CreateDataCenter(ctx context.Context, properties sdk.DatacenterProperties) ( - *sdk.Datacenter, error) - // GetDataCenter returns the data center that matches the provided data center ID. - GetDataCenter(ctx context.Context, dataCenterID string) (*sdk.Datacenter, error) // CreateServer creates a new server with provided properties in the specified data center. - CreateServer(ctx context.Context, dataCenterID string, properties sdk.ServerProperties) ( + CreateServer(ctx context.Context, datacenterID string, properties sdk.ServerProperties) ( *sdk.Server, error) // ListServers returns a list with the servers in the specified data center. - ListServers(ctx context.Context, dataCenterID string) (*sdk.Servers, error) + ListServers(ctx context.Context, datacenterID string) (*sdk.Servers, error) // GetServer returns the server that matches the provided serverID in the specified data center. - GetServer(ctx context.Context, dataCenterID, serverID string) (*sdk.Server, error) + GetServer(ctx context.Context, datacenterID, serverID string) (*sdk.Server, error) // DestroyServer deletes the server that matches the provided serverID in the specified data center. - DestroyServer(ctx context.Context, dataCenterID, serverID string) error + DestroyServer(ctx context.Context, datacenterID, serverID string) error // CreateLAN creates a new LAN with the provided properties in the specified data center, returning the request location. - CreateLAN(ctx context.Context, dataCenterID string, properties sdk.LanPropertiesPost) (string, error) + CreateLAN(ctx context.Context, datacenterID string, properties sdk.LanPropertiesPost) (string, error) // AttachToLAN attaches a provided NIC to a provided LAN in a specified data center. - AttachToLAN(ctx context.Context, dataCenterID, lanID string, nic sdk.Nic) ( + AttachToLAN(ctx context.Context, datacenterID, lanID string, nic sdk.Nic) ( *sdk.Nic, error) // ListLANs returns a list of LANs in the specified data center. - ListLANs(ctx context.Context, dataCenterID string) (*sdk.Lans, error) + ListLANs(ctx context.Context, datacenterID string) (*sdk.Lans, error) // GetLAN returns the LAN that matches lanID in the specified data center. - GetLAN(ctx context.Context, dataCenterID, lanID string) (*sdk.Lan, error) + GetLAN(ctx context.Context, datacenterID, lanID string) (*sdk.Lan, error) // DeleteLAN deletes the LAN that matches the provided lanID in the specified data center, returning the request location. - DeleteLAN(ctx context.Context, dataCenterID, lanID string) (string, error) + DeleteLAN(ctx context.Context, datacenterID, lanID string) (string, error) // ListVolumes returns a list of volumes in a specified data center. - ListVolumes(ctx context.Context, dataCenterID string) (*sdk.Volumes, error) + ListVolumes(ctx context.Context, datacenterID string) (*sdk.Volumes, error) // GetVolume returns the volume that matches volumeID in the specified data center. - GetVolume(ctx context.Context, dataCenterID, volumeID string) (*sdk.Volume, error) + GetVolume(ctx context.Context, datacenterID, volumeID string) (*sdk.Volume, error) // DestroyVolume deletes the volume that matches volumeID in the specified data center. - DestroyVolume(ctx context.Context, dataCenterID, volumeID string) error + DestroyVolume(ctx context.Context, datacenterID, volumeID string) error // CheckRequestStatus checks the status of a provided request identified by requestID CheckRequestStatus(ctx context.Context, requestID string) (*sdk.RequestStatus, error) // WaitForRequest waits for the completion of the provided request. diff --git a/internal/ionoscloud/client/client.go b/internal/ionoscloud/client/client.go index 6a33f020..0c9d5ca8 100644 --- a/internal/ionoscloud/client/client.go +++ b/internal/ionoscloud/client/client.go @@ -68,40 +68,17 @@ func validate(username, password, token string) error { return nil } -// CreateDataCenter creates a new data center with its specification based on provided properties. -func (c *IonosCloudClient) CreateDataCenter(ctx context.Context, properties sdk.DatacenterProperties, -) (*sdk.Datacenter, error) { - dc := sdk.Datacenter{Properties: &properties} - dc, _, err := c.API.DataCentersApi.DatacentersPost(ctx).Datacenter(dc).Execute() - if err != nil { - return nil, fmt.Errorf(apiCallErrWrapper, err) - } - return &dc, nil -} - -// GetDataCenter returns the data center that matches the provided data center ID. -func (c *IonosCloudClient) GetDataCenter(ctx context.Context, id string) (*sdk.Datacenter, error) { - if id == "" { - return nil, errDataCenterIDIsEmpty - } - dc, _, err := c.API.DataCentersApi.DatacentersFindById(ctx, id).Execute() - if err != nil { - return nil, fmt.Errorf(apiCallErrWrapper, err) - } - return &dc, nil -} - // CreateServer creates a new server with provided properties in the specified data center. func (c *IonosCloudClient) CreateServer( - ctx context.Context, dataCenterID string, properties sdk.ServerProperties, + ctx context.Context, datacenterID string, properties sdk.ServerProperties, ) (*sdk.Server, error) { - if dataCenterID == "" { - return nil, errDataCenterIDIsEmpty + if datacenterID == "" { + return nil, errDatacenterIDIsEmpty } server := sdk.Server{ Properties: &properties, } - s, _, err := c.API.ServersApi.DatacentersServersPost(ctx, dataCenterID).Server(server).Execute() + s, _, err := c.API.ServersApi.DatacentersServersPost(ctx, datacenterID).Server(server).Execute() if err != nil { return nil, fmt.Errorf(apiCallErrWrapper, err) } @@ -109,11 +86,11 @@ func (c *IonosCloudClient) CreateServer( } // ListServers returns a list with servers in the specified data center. -func (c *IonosCloudClient) ListServers(ctx context.Context, dataCenterID string) (*sdk.Servers, error) { - if dataCenterID == "" { - return nil, errDataCenterIDIsEmpty +func (c *IonosCloudClient) ListServers(ctx context.Context, datacenterID string) (*sdk.Servers, error) { + if datacenterID == "" { + return nil, errDatacenterIDIsEmpty } - servers, _, err := c.API.ServersApi.DatacentersServersGet(ctx, dataCenterID).Execute() + servers, _, err := c.API.ServersApi.DatacentersServersGet(ctx, datacenterID).Execute() if err != nil { return nil, fmt.Errorf(apiCallErrWrapper, err) } @@ -121,14 +98,14 @@ func (c *IonosCloudClient) ListServers(ctx context.Context, dataCenterID string) } // GetServer returns the server that matches the provided serverID in the specified data center. -func (c *IonosCloudClient) GetServer(ctx context.Context, dataCenterID, serverID string) (*sdk.Server, error) { - if dataCenterID == "" { - return nil, errDataCenterIDIsEmpty +func (c *IonosCloudClient) GetServer(ctx context.Context, datacenterID, serverID string) (*sdk.Server, error) { + if datacenterID == "" { + return nil, errDatacenterIDIsEmpty } if serverID == "" { return nil, errServerIDIsEmpty } - server, _, err := c.API.ServersApi.DatacentersServersFindById(ctx, dataCenterID, serverID).Execute() + server, _, err := c.API.ServersApi.DatacentersServersFindById(ctx, datacenterID, serverID).Execute() if err != nil { return nil, fmt.Errorf(apiCallErrWrapper, err) } @@ -136,14 +113,14 @@ func (c *IonosCloudClient) GetServer(ctx context.Context, dataCenterID, serverID } // DestroyServer deletes the server that matches the provided serverID in the specified data center. -func (c *IonosCloudClient) DestroyServer(ctx context.Context, dataCenterID, serverID string) error { - if dataCenterID == "" { - return errDataCenterIDIsEmpty +func (c *IonosCloudClient) DestroyServer(ctx context.Context, datacenterID, serverID string) error { + if datacenterID == "" { + return errDatacenterIDIsEmpty } if serverID == "" { return errServerIDIsEmpty } - _, err := c.API.ServersApi.DatacentersServersDelete(ctx, dataCenterID, serverID).Execute() + _, err := c.API.ServersApi.DatacentersServersDelete(ctx, datacenterID, serverID).Execute() if err != nil { return fmt.Errorf(apiCallErrWrapper, err) } @@ -151,15 +128,15 @@ func (c *IonosCloudClient) DestroyServer(ctx context.Context, dataCenterID, serv } // CreateLAN creates a new LAN with the provided properties in the specified data center, returning the request location. -func (c *IonosCloudClient) CreateLAN(ctx context.Context, dataCenterID string, properties sdk.LanPropertiesPost, +func (c *IonosCloudClient) CreateLAN(ctx context.Context, datacenterID string, properties sdk.LanPropertiesPost, ) (string, error) { - if dataCenterID == "" { - return "", errDataCenterIDIsEmpty + if datacenterID == "" { + return "", errDatacenterIDIsEmpty } lanPost := sdk.LanPost{ Properties: &properties, } - _, req, err := c.API.LANsApi.DatacentersLansPost(ctx, dataCenterID).Lan(lanPost).Execute() + _, req, err := c.API.LANsApi.DatacentersLansPost(ctx, datacenterID).Lan(lanPost).Execute() if err != nil { return "", fmt.Errorf(apiCallErrWrapper, err) } @@ -170,15 +147,15 @@ func (c *IonosCloudClient) CreateLAN(ctx context.Context, dataCenterID string, p } // AttachToLAN attaches a provided NIC to a provided LAN in the specified data center. -func (c *IonosCloudClient) AttachToLAN(ctx context.Context, dataCenterID, lanID string, nic sdk.Nic, +func (c *IonosCloudClient) AttachToLAN(ctx context.Context, datacenterID, lanID string, nic sdk.Nic, ) (*sdk.Nic, error) { - if dataCenterID == "" { - return nil, errDataCenterIDIsEmpty + if datacenterID == "" { + return nil, errDatacenterIDIsEmpty } if lanID == "" { return nil, errLANIDIsEmpty } - n, _, err := c.API.LANsApi.DatacentersLansNicsPost(ctx, dataCenterID, lanID).Nic(nic).Execute() + n, _, err := c.API.LANsApi.DatacentersLansNicsPost(ctx, datacenterID, lanID).Nic(nic).Execute() if err != nil { return nil, fmt.Errorf(apiCallErrWrapper, err) } @@ -186,11 +163,11 @@ func (c *IonosCloudClient) AttachToLAN(ctx context.Context, dataCenterID, lanID } // ListLANs returns a list of LANs in the specified data center. -func (c *IonosCloudClient) ListLANs(ctx context.Context, dataCenterID string) (*sdk.Lans, error) { - if dataCenterID == "" { - return nil, errDataCenterIDIsEmpty +func (c *IonosCloudClient) ListLANs(ctx context.Context, datacenterID string) (*sdk.Lans, error) { + if datacenterID == "" { + return nil, errDatacenterIDIsEmpty } - lans, _, err := c.API.LANsApi.DatacentersLansGet(ctx, dataCenterID).Depth(depthLANEntities).Execute() + lans, _, err := c.API.LANsApi.DatacentersLansGet(ctx, datacenterID).Depth(depthLANEntities).Execute() if err != nil { return nil, fmt.Errorf(apiCallErrWrapper, err) } @@ -198,14 +175,14 @@ func (c *IonosCloudClient) ListLANs(ctx context.Context, dataCenterID string) (* } // GetLAN returns the LAN that matches lanID in the specified data center. -func (c *IonosCloudClient) GetLAN(ctx context.Context, dataCenterID, lanID string) (*sdk.Lan, error) { - if dataCenterID == "" { - return nil, errDataCenterIDIsEmpty +func (c *IonosCloudClient) GetLAN(ctx context.Context, datacenterID, lanID string) (*sdk.Lan, error) { + if datacenterID == "" { + return nil, errDatacenterIDIsEmpty } if lanID == "" { return nil, errLANIDIsEmpty } - lan, _, err := c.API.LANsApi.DatacentersLansFindById(ctx, dataCenterID, lanID).Execute() + lan, _, err := c.API.LANsApi.DatacentersLansFindById(ctx, datacenterID, lanID).Execute() if err != nil { return nil, fmt.Errorf(apiCallErrWrapper, err) } @@ -213,14 +190,14 @@ func (c *IonosCloudClient) GetLAN(ctx context.Context, dataCenterID, lanID strin } // DeleteLAN deletes the LAN that matches the provided lanID in the specified data center, returning the request location. -func (c *IonosCloudClient) DeleteLAN(ctx context.Context, dataCenterID, lanID string) (string, error) { - if dataCenterID == "" { - return "", errDataCenterIDIsEmpty +func (c *IonosCloudClient) DeleteLAN(ctx context.Context, datacenterID, lanID string) (string, error) { + if datacenterID == "" { + return "", errDatacenterIDIsEmpty } if lanID == "" { return "", errLANIDIsEmpty } - req, err := c.API.LANsApi.DatacentersLansDelete(ctx, dataCenterID, lanID).Execute() + req, err := c.API.LANsApi.DatacentersLansDelete(ctx, datacenterID, lanID).Execute() if err != nil { return "", fmt.Errorf(apiCallErrWrapper, err) } @@ -231,12 +208,12 @@ func (c *IonosCloudClient) DeleteLAN(ctx context.Context, dataCenterID, lanID st } // ListVolumes returns a list of volumes in the specified data center. -func (c *IonosCloudClient) ListVolumes(ctx context.Context, dataCenterID string, +func (c *IonosCloudClient) ListVolumes(ctx context.Context, datacenterID string, ) (*sdk.Volumes, error) { - if dataCenterID == "" { - return nil, errDataCenterIDIsEmpty + if datacenterID == "" { + return nil, errDatacenterIDIsEmpty } - volumes, _, err := c.API.VolumesApi.DatacentersVolumesGet(ctx, dataCenterID).Execute() + volumes, _, err := c.API.VolumesApi.DatacentersVolumesGet(ctx, datacenterID).Execute() if err != nil { return nil, fmt.Errorf(apiCallErrWrapper, err) } @@ -244,15 +221,15 @@ func (c *IonosCloudClient) ListVolumes(ctx context.Context, dataCenterID string, } // GetVolume returns the volume that matches volumeID in the specified data center. -func (c *IonosCloudClient) GetVolume(ctx context.Context, dataCenterID, volumeID string, +func (c *IonosCloudClient) GetVolume(ctx context.Context, datacenterID, volumeID string, ) (*sdk.Volume, error) { - if dataCenterID == "" { - return nil, errDataCenterIDIsEmpty + if datacenterID == "" { + return nil, errDatacenterIDIsEmpty } if volumeID == "" { return nil, errVolumeIDIsEmpty } - volume, _, err := c.API.VolumesApi.DatacentersVolumesFindById(ctx, dataCenterID, volumeID).Execute() + volume, _, err := c.API.VolumesApi.DatacentersVolumesFindById(ctx, datacenterID, volumeID).Execute() if err != nil { return nil, fmt.Errorf(apiCallErrWrapper, err) } @@ -260,14 +237,14 @@ func (c *IonosCloudClient) GetVolume(ctx context.Context, dataCenterID, volumeID } // DestroyVolume deletes the volume that matches volumeID in the specified data center. -func (c *IonosCloudClient) DestroyVolume(ctx context.Context, dataCenterID, volumeID string) error { - if dataCenterID == "" { - return errDataCenterIDIsEmpty +func (c *IonosCloudClient) DestroyVolume(ctx context.Context, datacenterID, volumeID string) error { + if datacenterID == "" { + return errDatacenterIDIsEmpty } if volumeID == "" { return errVolumeIDIsEmpty } - _, err := c.API.VolumesApi.DatacentersVolumesDelete(ctx, dataCenterID, volumeID).Execute() + _, err := c.API.VolumesApi.DatacentersVolumesDelete(ctx, datacenterID, volumeID).Execute() if err != nil { return fmt.Errorf(apiCallErrWrapper, err) } diff --git a/internal/ionoscloud/client/errors.go b/internal/ionoscloud/client/errors.go index 4cdf721c..27b9cce2 100644 --- a/internal/ionoscloud/client/errors.go +++ b/internal/ionoscloud/client/errors.go @@ -19,7 +19,7 @@ package client import "errors" var ( - errDataCenterIDIsEmpty = errors.New("error parsing data center ID: value cannot be empty") + errDatacenterIDIsEmpty = errors.New("error parsing data center ID: value cannot be empty") errServerIDIsEmpty = errors.New("error parsing server ID: value cannot be empty") errLANIDIsEmpty = errors.New("error parsing LAN ID: value cannot be empty") errVolumeIDIsEmpty = errors.New("error parsing volume ID: value cannot be empty") diff --git a/internal/ionoscloud/clienttest/mock_client.go b/internal/ionoscloud/clienttest/mock_client.go index f4788fc3..923b8d2b 100644 --- a/internal/ionoscloud/clienttest/mock_client.go +++ b/internal/ionoscloud/clienttest/mock_client.go @@ -39,17 +39,17 @@ func (_m *MockClient) EXPECT() *MockClient_Expecter { return &MockClient_Expecter{mock: &_m.Mock} } -// AttachToLAN provides a mock function with given fields: ctx, dataCenterID, lanID, nic -func (_m *MockClient) AttachToLAN(ctx context.Context, dataCenterID string, lanID string, nic ionoscloud.Nic) (*ionoscloud.Nic, error) { - ret := _m.Called(ctx, dataCenterID, lanID, nic) +// AttachToLAN provides a mock function with given fields: ctx, datacenterID, lanID, nic +func (_m *MockClient) AttachToLAN(ctx context.Context, datacenterID string, lanID string, nic ionoscloud.Nic) (*ionoscloud.Nic, error) { + ret := _m.Called(ctx, datacenterID, lanID, nic) var r0 *ionoscloud.Nic var r1 error if rf, ok := ret.Get(0).(func(context.Context, string, string, ionoscloud.Nic) (*ionoscloud.Nic, error)); ok { - return rf(ctx, dataCenterID, lanID, nic) + return rf(ctx, datacenterID, lanID, nic) } if rf, ok := ret.Get(0).(func(context.Context, string, string, ionoscloud.Nic) *ionoscloud.Nic); ok { - r0 = rf(ctx, dataCenterID, lanID, nic) + r0 = rf(ctx, datacenterID, lanID, nic) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*ionoscloud.Nic) @@ -57,7 +57,7 @@ func (_m *MockClient) AttachToLAN(ctx context.Context, dataCenterID string, lanI } if rf, ok := ret.Get(1).(func(context.Context, string, string, ionoscloud.Nic) error); ok { - r1 = rf(ctx, dataCenterID, lanID, nic) + r1 = rf(ctx, datacenterID, lanID, nic) } else { r1 = ret.Error(1) } @@ -72,14 +72,14 @@ type MockClient_AttachToLAN_Call struct { // AttachToLAN is a helper method to define mock.On call // - ctx context.Context -// - dataCenterID string +// - datacenterID string // - lanID string // - nic ionoscloud.Nic -func (_e *MockClient_Expecter) AttachToLAN(ctx interface{}, dataCenterID interface{}, lanID interface{}, nic interface{}) *MockClient_AttachToLAN_Call { - return &MockClient_AttachToLAN_Call{Call: _e.mock.On("AttachToLAN", ctx, dataCenterID, lanID, nic)} +func (_e *MockClient_Expecter) AttachToLAN(ctx interface{}, datacenterID interface{}, lanID interface{}, nic interface{}) *MockClient_AttachToLAN_Call { + return &MockClient_AttachToLAN_Call{Call: _e.mock.On("AttachToLAN", ctx, datacenterID, lanID, nic)} } -func (_c *MockClient_AttachToLAN_Call) Run(run func(ctx context.Context, dataCenterID string, lanID string, nic ionoscloud.Nic)) *MockClient_AttachToLAN_Call { +func (_c *MockClient_AttachToLAN_Call) Run(run func(ctx context.Context, datacenterID string, lanID string, nic ionoscloud.Nic)) *MockClient_AttachToLAN_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(ionoscloud.Nic)) }) @@ -151,78 +151,23 @@ func (_c *MockClient_CheckRequestStatus_Call) RunAndReturn(run func(context.Cont return _c } -// CreateDataCenter provides a mock function with given fields: ctx, properties -func (_m *MockClient) CreateDataCenter(ctx context.Context, properties ionoscloud.DatacenterProperties) (*ionoscloud.Datacenter, error) { - ret := _m.Called(ctx, properties) - - var r0 *ionoscloud.Datacenter - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, ionoscloud.DatacenterProperties) (*ionoscloud.Datacenter, error)); ok { - return rf(ctx, properties) - } - if rf, ok := ret.Get(0).(func(context.Context, ionoscloud.DatacenterProperties) *ionoscloud.Datacenter); ok { - r0 = rf(ctx, properties) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*ionoscloud.Datacenter) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, ionoscloud.DatacenterProperties) error); ok { - r1 = rf(ctx, properties) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockClient_CreateDataCenter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateDataCenter' -type MockClient_CreateDataCenter_Call struct { - *mock.Call -} - -// CreateDataCenter is a helper method to define mock.On call -// - ctx context.Context -// - properties ionoscloud.DatacenterProperties -func (_e *MockClient_Expecter) CreateDataCenter(ctx interface{}, properties interface{}) *MockClient_CreateDataCenter_Call { - return &MockClient_CreateDataCenter_Call{Call: _e.mock.On("CreateDataCenter", ctx, properties)} -} - -func (_c *MockClient_CreateDataCenter_Call) Run(run func(ctx context.Context, properties ionoscloud.DatacenterProperties)) *MockClient_CreateDataCenter_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(ionoscloud.DatacenterProperties)) - }) - return _c -} - -func (_c *MockClient_CreateDataCenter_Call) Return(_a0 *ionoscloud.Datacenter, _a1 error) *MockClient_CreateDataCenter_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockClient_CreateDataCenter_Call) RunAndReturn(run func(context.Context, ionoscloud.DatacenterProperties) (*ionoscloud.Datacenter, error)) *MockClient_CreateDataCenter_Call { - _c.Call.Return(run) - return _c -} - -// CreateLAN provides a mock function with given fields: ctx, dataCenterID, properties -func (_m *MockClient) CreateLAN(ctx context.Context, dataCenterID string, properties ionoscloud.LanPropertiesPost) (string, error) { - ret := _m.Called(ctx, dataCenterID, properties) +// CreateLAN provides a mock function with given fields: ctx, datacenterID, properties +func (_m *MockClient) CreateLAN(ctx context.Context, datacenterID string, properties ionoscloud.LanPropertiesPost) (string, error) { + ret := _m.Called(ctx, datacenterID, properties) var r0 string var r1 error if rf, ok := ret.Get(0).(func(context.Context, string, ionoscloud.LanPropertiesPost) (string, error)); ok { - return rf(ctx, dataCenterID, properties) + return rf(ctx, datacenterID, properties) } if rf, ok := ret.Get(0).(func(context.Context, string, ionoscloud.LanPropertiesPost) string); ok { - r0 = rf(ctx, dataCenterID, properties) + r0 = rf(ctx, datacenterID, properties) } else { r0 = ret.Get(0).(string) } if rf, ok := ret.Get(1).(func(context.Context, string, ionoscloud.LanPropertiesPost) error); ok { - r1 = rf(ctx, dataCenterID, properties) + r1 = rf(ctx, datacenterID, properties) } else { r1 = ret.Error(1) } @@ -237,13 +182,13 @@ type MockClient_CreateLAN_Call struct { // CreateLAN is a helper method to define mock.On call // - ctx context.Context -// - dataCenterID string +// - datacenterID string // - properties ionoscloud.LanPropertiesPost -func (_e *MockClient_Expecter) CreateLAN(ctx interface{}, dataCenterID interface{}, properties interface{}) *MockClient_CreateLAN_Call { - return &MockClient_CreateLAN_Call{Call: _e.mock.On("CreateLAN", ctx, dataCenterID, properties)} +func (_e *MockClient_Expecter) CreateLAN(ctx interface{}, datacenterID interface{}, properties interface{}) *MockClient_CreateLAN_Call { + return &MockClient_CreateLAN_Call{Call: _e.mock.On("CreateLAN", ctx, datacenterID, properties)} } -func (_c *MockClient_CreateLAN_Call) Run(run func(ctx context.Context, dataCenterID string, properties ionoscloud.LanPropertiesPost)) *MockClient_CreateLAN_Call { +func (_c *MockClient_CreateLAN_Call) Run(run func(ctx context.Context, datacenterID string, properties ionoscloud.LanPropertiesPost)) *MockClient_CreateLAN_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string), args[2].(ionoscloud.LanPropertiesPost)) }) @@ -260,17 +205,17 @@ func (_c *MockClient_CreateLAN_Call) RunAndReturn(run func(context.Context, stri return _c } -// CreateServer provides a mock function with given fields: ctx, dataCenterID, properties -func (_m *MockClient) CreateServer(ctx context.Context, dataCenterID string, properties ionoscloud.ServerProperties) (*ionoscloud.Server, error) { - ret := _m.Called(ctx, dataCenterID, properties) +// CreateServer provides a mock function with given fields: ctx, datacenterID, properties +func (_m *MockClient) CreateServer(ctx context.Context, datacenterID string, properties ionoscloud.ServerProperties) (*ionoscloud.Server, error) { + ret := _m.Called(ctx, datacenterID, properties) var r0 *ionoscloud.Server var r1 error if rf, ok := ret.Get(0).(func(context.Context, string, ionoscloud.ServerProperties) (*ionoscloud.Server, error)); ok { - return rf(ctx, dataCenterID, properties) + return rf(ctx, datacenterID, properties) } if rf, ok := ret.Get(0).(func(context.Context, string, ionoscloud.ServerProperties) *ionoscloud.Server); ok { - r0 = rf(ctx, dataCenterID, properties) + r0 = rf(ctx, datacenterID, properties) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*ionoscloud.Server) @@ -278,7 +223,7 @@ func (_m *MockClient) CreateServer(ctx context.Context, dataCenterID string, pro } if rf, ok := ret.Get(1).(func(context.Context, string, ionoscloud.ServerProperties) error); ok { - r1 = rf(ctx, dataCenterID, properties) + r1 = rf(ctx, datacenterID, properties) } else { r1 = ret.Error(1) } @@ -293,13 +238,13 @@ type MockClient_CreateServer_Call struct { // CreateServer is a helper method to define mock.On call // - ctx context.Context -// - dataCenterID string +// - datacenterID string // - properties ionoscloud.ServerProperties -func (_e *MockClient_Expecter) CreateServer(ctx interface{}, dataCenterID interface{}, properties interface{}) *MockClient_CreateServer_Call { - return &MockClient_CreateServer_Call{Call: _e.mock.On("CreateServer", ctx, dataCenterID, properties)} +func (_e *MockClient_Expecter) CreateServer(ctx interface{}, datacenterID interface{}, properties interface{}) *MockClient_CreateServer_Call { + return &MockClient_CreateServer_Call{Call: _e.mock.On("CreateServer", ctx, datacenterID, properties)} } -func (_c *MockClient_CreateServer_Call) Run(run func(ctx context.Context, dataCenterID string, properties ionoscloud.ServerProperties)) *MockClient_CreateServer_Call { +func (_c *MockClient_CreateServer_Call) Run(run func(ctx context.Context, datacenterID string, properties ionoscloud.ServerProperties)) *MockClient_CreateServer_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string), args[2].(ionoscloud.ServerProperties)) }) @@ -316,23 +261,23 @@ func (_c *MockClient_CreateServer_Call) RunAndReturn(run func(context.Context, s return _c } -// DeleteLAN provides a mock function with given fields: ctx, dataCenterID, lanID -func (_m *MockClient) DeleteLAN(ctx context.Context, dataCenterID string, lanID string) (string, error) { - ret := _m.Called(ctx, dataCenterID, lanID) +// DeleteLAN provides a mock function with given fields: ctx, datacenterID, lanID +func (_m *MockClient) DeleteLAN(ctx context.Context, datacenterID string, lanID string) (string, error) { + ret := _m.Called(ctx, datacenterID, lanID) var r0 string var r1 error if rf, ok := ret.Get(0).(func(context.Context, string, string) (string, error)); ok { - return rf(ctx, dataCenterID, lanID) + return rf(ctx, datacenterID, lanID) } if rf, ok := ret.Get(0).(func(context.Context, string, string) string); ok { - r0 = rf(ctx, dataCenterID, lanID) + r0 = rf(ctx, datacenterID, lanID) } else { r0 = ret.Get(0).(string) } if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { - r1 = rf(ctx, dataCenterID, lanID) + r1 = rf(ctx, datacenterID, lanID) } else { r1 = ret.Error(1) } @@ -347,13 +292,13 @@ type MockClient_DeleteLAN_Call struct { // DeleteLAN is a helper method to define mock.On call // - ctx context.Context -// - dataCenterID string +// - datacenterID string // - lanID string -func (_e *MockClient_Expecter) DeleteLAN(ctx interface{}, dataCenterID interface{}, lanID interface{}) *MockClient_DeleteLAN_Call { - return &MockClient_DeleteLAN_Call{Call: _e.mock.On("DeleteLAN", ctx, dataCenterID, lanID)} +func (_e *MockClient_Expecter) DeleteLAN(ctx interface{}, datacenterID interface{}, lanID interface{}) *MockClient_DeleteLAN_Call { + return &MockClient_DeleteLAN_Call{Call: _e.mock.On("DeleteLAN", ctx, datacenterID, lanID)} } -func (_c *MockClient_DeleteLAN_Call) Run(run func(ctx context.Context, dataCenterID string, lanID string)) *MockClient_DeleteLAN_Call { +func (_c *MockClient_DeleteLAN_Call) Run(run func(ctx context.Context, datacenterID string, lanID string)) *MockClient_DeleteLAN_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string), args[2].(string)) }) @@ -370,13 +315,13 @@ func (_c *MockClient_DeleteLAN_Call) RunAndReturn(run func(context.Context, stri return _c } -// DestroyServer provides a mock function with given fields: ctx, dataCenterID, serverID -func (_m *MockClient) DestroyServer(ctx context.Context, dataCenterID string, serverID string) error { - ret := _m.Called(ctx, dataCenterID, serverID) +// DestroyServer provides a mock function with given fields: ctx, datacenterID, serverID +func (_m *MockClient) DestroyServer(ctx context.Context, datacenterID string, serverID string) error { + ret := _m.Called(ctx, datacenterID, serverID) var r0 error if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { - r0 = rf(ctx, dataCenterID, serverID) + r0 = rf(ctx, datacenterID, serverID) } else { r0 = ret.Error(0) } @@ -391,13 +336,13 @@ type MockClient_DestroyServer_Call struct { // DestroyServer is a helper method to define mock.On call // - ctx context.Context -// - dataCenterID string +// - datacenterID string // - serverID string -func (_e *MockClient_Expecter) DestroyServer(ctx interface{}, dataCenterID interface{}, serverID interface{}) *MockClient_DestroyServer_Call { - return &MockClient_DestroyServer_Call{Call: _e.mock.On("DestroyServer", ctx, dataCenterID, serverID)} +func (_e *MockClient_Expecter) DestroyServer(ctx interface{}, datacenterID interface{}, serverID interface{}) *MockClient_DestroyServer_Call { + return &MockClient_DestroyServer_Call{Call: _e.mock.On("DestroyServer", ctx, datacenterID, serverID)} } -func (_c *MockClient_DestroyServer_Call) Run(run func(ctx context.Context, dataCenterID string, serverID string)) *MockClient_DestroyServer_Call { +func (_c *MockClient_DestroyServer_Call) Run(run func(ctx context.Context, datacenterID string, serverID string)) *MockClient_DestroyServer_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string), args[2].(string)) }) @@ -414,13 +359,13 @@ func (_c *MockClient_DestroyServer_Call) RunAndReturn(run func(context.Context, return _c } -// DestroyVolume provides a mock function with given fields: ctx, dataCenterID, volumeID -func (_m *MockClient) DestroyVolume(ctx context.Context, dataCenterID string, volumeID string) error { - ret := _m.Called(ctx, dataCenterID, volumeID) +// DestroyVolume provides a mock function with given fields: ctx, datacenterID, volumeID +func (_m *MockClient) DestroyVolume(ctx context.Context, datacenterID string, volumeID string) error { + ret := _m.Called(ctx, datacenterID, volumeID) var r0 error if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { - r0 = rf(ctx, dataCenterID, volumeID) + r0 = rf(ctx, datacenterID, volumeID) } else { r0 = ret.Error(0) } @@ -435,13 +380,13 @@ type MockClient_DestroyVolume_Call struct { // DestroyVolume is a helper method to define mock.On call // - ctx context.Context -// - dataCenterID string +// - datacenterID string // - volumeID string -func (_e *MockClient_Expecter) DestroyVolume(ctx interface{}, dataCenterID interface{}, volumeID interface{}) *MockClient_DestroyVolume_Call { - return &MockClient_DestroyVolume_Call{Call: _e.mock.On("DestroyVolume", ctx, dataCenterID, volumeID)} +func (_e *MockClient_Expecter) DestroyVolume(ctx interface{}, datacenterID interface{}, volumeID interface{}) *MockClient_DestroyVolume_Call { + return &MockClient_DestroyVolume_Call{Call: _e.mock.On("DestroyVolume", ctx, datacenterID, volumeID)} } -func (_c *MockClient_DestroyVolume_Call) Run(run func(ctx context.Context, dataCenterID string, volumeID string)) *MockClient_DestroyVolume_Call { +func (_c *MockClient_DestroyVolume_Call) Run(run func(ctx context.Context, datacenterID string, volumeID string)) *MockClient_DestroyVolume_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string), args[2].(string)) }) @@ -458,72 +403,17 @@ func (_c *MockClient_DestroyVolume_Call) RunAndReturn(run func(context.Context, return _c } -// GetDataCenter provides a mock function with given fields: ctx, dataCenterID -func (_m *MockClient) GetDataCenter(ctx context.Context, dataCenterID string) (*ionoscloud.Datacenter, error) { - ret := _m.Called(ctx, dataCenterID) - - var r0 *ionoscloud.Datacenter - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) (*ionoscloud.Datacenter, error)); ok { - return rf(ctx, dataCenterID) - } - if rf, ok := ret.Get(0).(func(context.Context, string) *ionoscloud.Datacenter); ok { - r0 = rf(ctx, dataCenterID) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*ionoscloud.Datacenter) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, dataCenterID) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockClient_GetDataCenter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetDataCenter' -type MockClient_GetDataCenter_Call struct { - *mock.Call -} - -// GetDataCenter is a helper method to define mock.On call -// - ctx context.Context -// - dataCenterID string -func (_e *MockClient_Expecter) GetDataCenter(ctx interface{}, dataCenterID interface{}) *MockClient_GetDataCenter_Call { - return &MockClient_GetDataCenter_Call{Call: _e.mock.On("GetDataCenter", ctx, dataCenterID)} -} - -func (_c *MockClient_GetDataCenter_Call) Run(run func(ctx context.Context, dataCenterID string)) *MockClient_GetDataCenter_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string)) - }) - return _c -} - -func (_c *MockClient_GetDataCenter_Call) Return(_a0 *ionoscloud.Datacenter, _a1 error) *MockClient_GetDataCenter_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockClient_GetDataCenter_Call) RunAndReturn(run func(context.Context, string) (*ionoscloud.Datacenter, error)) *MockClient_GetDataCenter_Call { - _c.Call.Return(run) - return _c -} - -// GetLAN provides a mock function with given fields: ctx, dataCenterID, lanID -func (_m *MockClient) GetLAN(ctx context.Context, dataCenterID string, lanID string) (*ionoscloud.Lan, error) { - ret := _m.Called(ctx, dataCenterID, lanID) +// GetLAN provides a mock function with given fields: ctx, datacenterID, lanID +func (_m *MockClient) GetLAN(ctx context.Context, datacenterID string, lanID string) (*ionoscloud.Lan, error) { + ret := _m.Called(ctx, datacenterID, lanID) var r0 *ionoscloud.Lan var r1 error if rf, ok := ret.Get(0).(func(context.Context, string, string) (*ionoscloud.Lan, error)); ok { - return rf(ctx, dataCenterID, lanID) + return rf(ctx, datacenterID, lanID) } if rf, ok := ret.Get(0).(func(context.Context, string, string) *ionoscloud.Lan); ok { - r0 = rf(ctx, dataCenterID, lanID) + r0 = rf(ctx, datacenterID, lanID) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*ionoscloud.Lan) @@ -531,7 +421,7 @@ func (_m *MockClient) GetLAN(ctx context.Context, dataCenterID string, lanID str } if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { - r1 = rf(ctx, dataCenterID, lanID) + r1 = rf(ctx, datacenterID, lanID) } else { r1 = ret.Error(1) } @@ -546,13 +436,13 @@ type MockClient_GetLAN_Call struct { // GetLAN is a helper method to define mock.On call // - ctx context.Context -// - dataCenterID string +// - datacenterID string // - lanID string -func (_e *MockClient_Expecter) GetLAN(ctx interface{}, dataCenterID interface{}, lanID interface{}) *MockClient_GetLAN_Call { - return &MockClient_GetLAN_Call{Call: _e.mock.On("GetLAN", ctx, dataCenterID, lanID)} +func (_e *MockClient_Expecter) GetLAN(ctx interface{}, datacenterID interface{}, lanID interface{}) *MockClient_GetLAN_Call { + return &MockClient_GetLAN_Call{Call: _e.mock.On("GetLAN", ctx, datacenterID, lanID)} } -func (_c *MockClient_GetLAN_Call) Run(run func(ctx context.Context, dataCenterID string, lanID string)) *MockClient_GetLAN_Call { +func (_c *MockClient_GetLAN_Call) Run(run func(ctx context.Context, datacenterID string, lanID string)) *MockClient_GetLAN_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string), args[2].(string)) }) @@ -625,17 +515,17 @@ func (_c *MockClient_GetRequests_Call) RunAndReturn(run func(context.Context, st return _c } -// GetServer provides a mock function with given fields: ctx, dataCenterID, serverID -func (_m *MockClient) GetServer(ctx context.Context, dataCenterID string, serverID string) (*ionoscloud.Server, error) { - ret := _m.Called(ctx, dataCenterID, serverID) +// GetServer provides a mock function with given fields: ctx, datacenterID, serverID +func (_m *MockClient) GetServer(ctx context.Context, datacenterID string, serverID string) (*ionoscloud.Server, error) { + ret := _m.Called(ctx, datacenterID, serverID) var r0 *ionoscloud.Server var r1 error if rf, ok := ret.Get(0).(func(context.Context, string, string) (*ionoscloud.Server, error)); ok { - return rf(ctx, dataCenterID, serverID) + return rf(ctx, datacenterID, serverID) } if rf, ok := ret.Get(0).(func(context.Context, string, string) *ionoscloud.Server); ok { - r0 = rf(ctx, dataCenterID, serverID) + r0 = rf(ctx, datacenterID, serverID) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*ionoscloud.Server) @@ -643,7 +533,7 @@ func (_m *MockClient) GetServer(ctx context.Context, dataCenterID string, server } if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { - r1 = rf(ctx, dataCenterID, serverID) + r1 = rf(ctx, datacenterID, serverID) } else { r1 = ret.Error(1) } @@ -658,13 +548,13 @@ type MockClient_GetServer_Call struct { // GetServer is a helper method to define mock.On call // - ctx context.Context -// - dataCenterID string +// - datacenterID string // - serverID string -func (_e *MockClient_Expecter) GetServer(ctx interface{}, dataCenterID interface{}, serverID interface{}) *MockClient_GetServer_Call { - return &MockClient_GetServer_Call{Call: _e.mock.On("GetServer", ctx, dataCenterID, serverID)} +func (_e *MockClient_Expecter) GetServer(ctx interface{}, datacenterID interface{}, serverID interface{}) *MockClient_GetServer_Call { + return &MockClient_GetServer_Call{Call: _e.mock.On("GetServer", ctx, datacenterID, serverID)} } -func (_c *MockClient_GetServer_Call) Run(run func(ctx context.Context, dataCenterID string, serverID string)) *MockClient_GetServer_Call { +func (_c *MockClient_GetServer_Call) Run(run func(ctx context.Context, datacenterID string, serverID string)) *MockClient_GetServer_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string), args[2].(string)) }) @@ -681,17 +571,17 @@ func (_c *MockClient_GetServer_Call) RunAndReturn(run func(context.Context, stri return _c } -// GetVolume provides a mock function with given fields: ctx, dataCenterID, volumeID -func (_m *MockClient) GetVolume(ctx context.Context, dataCenterID string, volumeID string) (*ionoscloud.Volume, error) { - ret := _m.Called(ctx, dataCenterID, volumeID) +// GetVolume provides a mock function with given fields: ctx, datacenterID, volumeID +func (_m *MockClient) GetVolume(ctx context.Context, datacenterID string, volumeID string) (*ionoscloud.Volume, error) { + ret := _m.Called(ctx, datacenterID, volumeID) var r0 *ionoscloud.Volume var r1 error if rf, ok := ret.Get(0).(func(context.Context, string, string) (*ionoscloud.Volume, error)); ok { - return rf(ctx, dataCenterID, volumeID) + return rf(ctx, datacenterID, volumeID) } if rf, ok := ret.Get(0).(func(context.Context, string, string) *ionoscloud.Volume); ok { - r0 = rf(ctx, dataCenterID, volumeID) + r0 = rf(ctx, datacenterID, volumeID) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*ionoscloud.Volume) @@ -699,7 +589,7 @@ func (_m *MockClient) GetVolume(ctx context.Context, dataCenterID string, volume } if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { - r1 = rf(ctx, dataCenterID, volumeID) + r1 = rf(ctx, datacenterID, volumeID) } else { r1 = ret.Error(1) } @@ -714,13 +604,13 @@ type MockClient_GetVolume_Call struct { // GetVolume is a helper method to define mock.On call // - ctx context.Context -// - dataCenterID string +// - datacenterID string // - volumeID string -func (_e *MockClient_Expecter) GetVolume(ctx interface{}, dataCenterID interface{}, volumeID interface{}) *MockClient_GetVolume_Call { - return &MockClient_GetVolume_Call{Call: _e.mock.On("GetVolume", ctx, dataCenterID, volumeID)} +func (_e *MockClient_Expecter) GetVolume(ctx interface{}, datacenterID interface{}, volumeID interface{}) *MockClient_GetVolume_Call { + return &MockClient_GetVolume_Call{Call: _e.mock.On("GetVolume", ctx, datacenterID, volumeID)} } -func (_c *MockClient_GetVolume_Call) Run(run func(ctx context.Context, dataCenterID string, volumeID string)) *MockClient_GetVolume_Call { +func (_c *MockClient_GetVolume_Call) Run(run func(ctx context.Context, datacenterID string, volumeID string)) *MockClient_GetVolume_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string), args[2].(string)) }) @@ -737,17 +627,17 @@ func (_c *MockClient_GetVolume_Call) RunAndReturn(run func(context.Context, stri return _c } -// ListLANs provides a mock function with given fields: ctx, dataCenterID -func (_m *MockClient) ListLANs(ctx context.Context, dataCenterID string) (*ionoscloud.Lans, error) { - ret := _m.Called(ctx, dataCenterID) +// ListLANs provides a mock function with given fields: ctx, datacenterID +func (_m *MockClient) ListLANs(ctx context.Context, datacenterID string) (*ionoscloud.Lans, error) { + ret := _m.Called(ctx, datacenterID) var r0 *ionoscloud.Lans var r1 error if rf, ok := ret.Get(0).(func(context.Context, string) (*ionoscloud.Lans, error)); ok { - return rf(ctx, dataCenterID) + return rf(ctx, datacenterID) } if rf, ok := ret.Get(0).(func(context.Context, string) *ionoscloud.Lans); ok { - r0 = rf(ctx, dataCenterID) + r0 = rf(ctx, datacenterID) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*ionoscloud.Lans) @@ -755,7 +645,7 @@ func (_m *MockClient) ListLANs(ctx context.Context, dataCenterID string) (*ionos } if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, dataCenterID) + r1 = rf(ctx, datacenterID) } else { r1 = ret.Error(1) } @@ -770,12 +660,12 @@ type MockClient_ListLANs_Call struct { // ListLANs is a helper method to define mock.On call // - ctx context.Context -// - dataCenterID string -func (_e *MockClient_Expecter) ListLANs(ctx interface{}, dataCenterID interface{}) *MockClient_ListLANs_Call { - return &MockClient_ListLANs_Call{Call: _e.mock.On("ListLANs", ctx, dataCenterID)} +// - datacenterID string +func (_e *MockClient_Expecter) ListLANs(ctx interface{}, datacenterID interface{}) *MockClient_ListLANs_Call { + return &MockClient_ListLANs_Call{Call: _e.mock.On("ListLANs", ctx, datacenterID)} } -func (_c *MockClient_ListLANs_Call) Run(run func(ctx context.Context, dataCenterID string)) *MockClient_ListLANs_Call { +func (_c *MockClient_ListLANs_Call) Run(run func(ctx context.Context, datacenterID string)) *MockClient_ListLANs_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string)) }) @@ -792,17 +682,17 @@ func (_c *MockClient_ListLANs_Call) RunAndReturn(run func(context.Context, strin return _c } -// ListServers provides a mock function with given fields: ctx, dataCenterID -func (_m *MockClient) ListServers(ctx context.Context, dataCenterID string) (*ionoscloud.Servers, error) { - ret := _m.Called(ctx, dataCenterID) +// ListServers provides a mock function with given fields: ctx, datacenterID +func (_m *MockClient) ListServers(ctx context.Context, datacenterID string) (*ionoscloud.Servers, error) { + ret := _m.Called(ctx, datacenterID) var r0 *ionoscloud.Servers var r1 error if rf, ok := ret.Get(0).(func(context.Context, string) (*ionoscloud.Servers, error)); ok { - return rf(ctx, dataCenterID) + return rf(ctx, datacenterID) } if rf, ok := ret.Get(0).(func(context.Context, string) *ionoscloud.Servers); ok { - r0 = rf(ctx, dataCenterID) + r0 = rf(ctx, datacenterID) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*ionoscloud.Servers) @@ -810,7 +700,7 @@ func (_m *MockClient) ListServers(ctx context.Context, dataCenterID string) (*io } if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, dataCenterID) + r1 = rf(ctx, datacenterID) } else { r1 = ret.Error(1) } @@ -825,12 +715,12 @@ type MockClient_ListServers_Call struct { // ListServers is a helper method to define mock.On call // - ctx context.Context -// - dataCenterID string -func (_e *MockClient_Expecter) ListServers(ctx interface{}, dataCenterID interface{}) *MockClient_ListServers_Call { - return &MockClient_ListServers_Call{Call: _e.mock.On("ListServers", ctx, dataCenterID)} +// - datacenterID string +func (_e *MockClient_Expecter) ListServers(ctx interface{}, datacenterID interface{}) *MockClient_ListServers_Call { + return &MockClient_ListServers_Call{Call: _e.mock.On("ListServers", ctx, datacenterID)} } -func (_c *MockClient_ListServers_Call) Run(run func(ctx context.Context, dataCenterID string)) *MockClient_ListServers_Call { +func (_c *MockClient_ListServers_Call) Run(run func(ctx context.Context, datacenterID string)) *MockClient_ListServers_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string)) }) @@ -847,17 +737,17 @@ func (_c *MockClient_ListServers_Call) RunAndReturn(run func(context.Context, st return _c } -// ListVolumes provides a mock function with given fields: ctx, dataCenterID -func (_m *MockClient) ListVolumes(ctx context.Context, dataCenterID string) (*ionoscloud.Volumes, error) { - ret := _m.Called(ctx, dataCenterID) +// ListVolumes provides a mock function with given fields: ctx, datacenterID +func (_m *MockClient) ListVolumes(ctx context.Context, datacenterID string) (*ionoscloud.Volumes, error) { + ret := _m.Called(ctx, datacenterID) var r0 *ionoscloud.Volumes var r1 error if rf, ok := ret.Get(0).(func(context.Context, string) (*ionoscloud.Volumes, error)); ok { - return rf(ctx, dataCenterID) + return rf(ctx, datacenterID) } if rf, ok := ret.Get(0).(func(context.Context, string) *ionoscloud.Volumes); ok { - r0 = rf(ctx, dataCenterID) + r0 = rf(ctx, datacenterID) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*ionoscloud.Volumes) @@ -865,7 +755,7 @@ func (_m *MockClient) ListVolumes(ctx context.Context, dataCenterID string) (*io } if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, dataCenterID) + r1 = rf(ctx, datacenterID) } else { r1 = ret.Error(1) } @@ -880,12 +770,12 @@ type MockClient_ListVolumes_Call struct { // ListVolumes is a helper method to define mock.On call // - ctx context.Context -// - dataCenterID string -func (_e *MockClient_Expecter) ListVolumes(ctx interface{}, dataCenterID interface{}) *MockClient_ListVolumes_Call { - return &MockClient_ListVolumes_Call{Call: _e.mock.On("ListVolumes", ctx, dataCenterID)} +// - datacenterID string +func (_e *MockClient_Expecter) ListVolumes(ctx interface{}, datacenterID interface{}) *MockClient_ListVolumes_Call { + return &MockClient_ListVolumes_Call{Call: _e.mock.On("ListVolumes", ctx, datacenterID)} } -func (_c *MockClient_ListVolumes_Call) Run(run func(ctx context.Context, dataCenterID string)) *MockClient_ListVolumes_Call { +func (_c *MockClient_ListVolumes_Call) Run(run func(ctx context.Context, datacenterID string)) *MockClient_ListVolumes_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string)) }) diff --git a/internal/service/cloud/network.go b/internal/service/cloud/network.go index 7b92e669..26d56d5a 100644 --- a/internal/service/cloud/network.go +++ b/internal/service/cloud/network.go @@ -36,11 +36,11 @@ func (s *Service) lanName() string { } func (s *Service) lanURL(id string) string { - return path.Join("datacenters", s.dataCenterID(), "lans", id) + return path.Join("datacenters", s.datacenterID(), "lans", id) } func (s *Service) lansURL() string { - return path.Join("datacenters", s.dataCenterID(), "lans") + return path.Join("datacenters", s.datacenterID(), "lans") } // ReconcileLAN ensures the cluster LAN exist, creating one if it doesn't. @@ -127,9 +127,9 @@ func (s *Service) ReconcileLANDeletion() (requeue bool, err error) { // getLAN tries to retrieve the cluster-related LAN in the data center. func (s *Service) getLAN() (*sdk.Lan, error) { // check if the LAN exists - lans, err := s.api().ListLANs(s.ctx, s.dataCenterID()) + lans, err := s.api().ListLANs(s.ctx, s.datacenterID()) if err != nil { - return nil, fmt.Errorf("could not list LANs in data center %s: %w", s.dataCenterID(), err) + return nil, fmt.Errorf("could not list LANs in data center %s: %w", s.datacenterID(), err) } var ( @@ -158,18 +158,18 @@ func (s *Service) getLAN() (*sdk.Lan, error) { func (s *Service) createLAN() error { log := s.scope.Logger.WithName("createLAN") - requestPath, err := s.api().CreateLAN(s.ctx, s.dataCenterID(), sdk.LanPropertiesPost{ + requestPath, err := s.api().CreateLAN(s.ctx, s.datacenterID(), sdk.LanPropertiesPost{ Name: ptr.To(s.lanName()), Public: ptr.To(true), }) if err != nil { - return fmt.Errorf("unable to create LAN in data center %s: %w", s.dataCenterID(), err) + return fmt.Errorf("unable to create LAN in data center %s: %w", s.datacenterID(), err) } if len(s.scope.ClusterScope.IonosCluster.Status.CurrentRequestByDatacenter) == 0 { s.scope.ClusterScope.IonosCluster.Status.CurrentRequestByDatacenter = make(map[string]infrav1.ProvisioningRequest) } - s.scope.ClusterScope.IonosCluster.Status.CurrentRequestByDatacenter[s.dataCenterID()] = infrav1.ProvisioningRequest{ + s.scope.ClusterScope.IonosCluster.Status.CurrentRequestByDatacenter[s.datacenterID()] = infrav1.ProvisioningRequest{ Method: http.MethodPost, RequestPath: requestPath, State: infrav1.RequestStatusQueued, @@ -188,7 +188,7 @@ func (s *Service) createLAN() error { func (s *Service) deleteLAN(lanID string) error { log := s.scope.Logger.WithName("deleteLAN") - requestPath, err := s.api().DeleteLAN(s.ctx, s.dataCenterID(), lanID) + requestPath, err := s.api().DeleteLAN(s.ctx, s.datacenterID(), lanID) if err != nil { return fmt.Errorf("unable to request LAN deletion in data center: %w", err) } @@ -196,7 +196,7 @@ func (s *Service) deleteLAN(lanID string) error { if s.scope.ClusterScope.IonosCluster.Status.CurrentRequestByDatacenter == nil { s.scope.ClusterScope.IonosCluster.Status.CurrentRequestByDatacenter = make(map[string]infrav1.ProvisioningRequest) } - s.scope.ClusterScope.IonosCluster.Status.CurrentRequestByDatacenter[s.dataCenterID()] = infrav1.ProvisioningRequest{ + s.scope.ClusterScope.IonosCluster.Status.CurrentRequestByDatacenter[s.datacenterID()] = infrav1.ProvisioningRequest{ Method: http.MethodDelete, RequestPath: requestPath, State: infrav1.RequestStatusQueued, @@ -214,7 +214,7 @@ func (s *Service) getLatestLANCreationRequest() (*requestInfo, error) { return getMatchingRequest( s, http.MethodPost, - path.Join("datacenters", s.dataCenterID(), "lans"), + path.Join("datacenters", s.datacenterID(), "lans"), func(resource sdk.Lan, _ sdk.Request) bool { return *resource.Properties.Name == s.lanName() }, @@ -225,12 +225,12 @@ func (s *Service) getLatestLANDeletionRequest(lanID string) (*requestInfo, error return getMatchingRequest[sdk.Lan]( s, http.MethodDelete, - path.Join("datacenters", s.dataCenterID(), "lans", lanID), + path.Join("datacenters", s.datacenterID(), "lans", lanID), ) } func (s *Service) removeLANPendingRequestFromCluster() error { - delete(s.scope.ClusterScope.IonosCluster.Status.CurrentRequestByDatacenter, s.dataCenterID()) + delete(s.scope.ClusterScope.IonosCluster.Status.CurrentRequestByDatacenter, s.datacenterID()) if err := s.scope.ClusterScope.PatchObject(); err != nil { return fmt.Errorf("could not remove stale LAN pending request from cluster: %w", err) } diff --git a/internal/service/cloud/network_test.go b/internal/service/cloud/network_test.go index c756bfde..69afe154 100644 --- a/internal/service/cloud/network_test.go +++ b/internal/service/cloud/network_test.go @@ -42,18 +42,18 @@ func (s *lanSuite) TestNetworkLANName() { } func (s *lanSuite) TestLANURL() { - s.Equal("datacenters/"+s.service.dataCenterID()+"/lans/1", s.service.lanURL("1")) + s.Equal("datacenters/"+s.service.datacenterID()+"/lans/1", s.service.lanURL("1")) } func (s *lanSuite) TestLANURLs() { - s.Equal("datacenters/"+s.service.dataCenterID()+"/lans", s.service.lansURL()) + s.Equal("datacenters/"+s.service.datacenterID()+"/lans", s.service.lansURL()) } func (s *lanSuite) Test_Network_CreateLAN_Successful() { s.mockCreateLANCall().Return(reqPath, nil).Once() s.NoError(s.service.createLAN()) - s.Contains(s.infraCluster.Status.CurrentRequestByDatacenter, s.service.dataCenterID(), "request should be stored in status") - req := s.infraCluster.Status.CurrentRequestByDatacenter[s.service.dataCenterID()] + s.Contains(s.infraCluster.Status.CurrentRequestByDatacenter, s.service.datacenterID(), "request should be stored in status") + req := s.infraCluster.Status.CurrentRequestByDatacenter[s.service.datacenterID()] s.Equal(reqPath, req.RequestPath, "request path should be stored in status") s.Equal(http.MethodPost, req.Method, "request method should be stored in status") s.Equal(infrav1.RequestStatusQueued, req.State, "request status should be stored in status") @@ -62,8 +62,8 @@ func (s *lanSuite) Test_Network_CreateLAN_Successful() { func (s *lanSuite) Test_Network_DeleteLAN_Successful() { s.mockDeleteLANCall(lanID).Return(reqPath, nil).Once() s.NoError(s.service.deleteLAN(lanID)) - s.Contains(s.infraCluster.Status.CurrentRequestByDatacenter, s.service.dataCenterID(), "request should be stored in status") - req := s.infraCluster.Status.CurrentRequestByDatacenter[s.service.dataCenterID()] + s.Contains(s.infraCluster.Status.CurrentRequestByDatacenter, s.service.datacenterID(), "request should be stored in status") + req := s.infraCluster.Status.CurrentRequestByDatacenter[s.service.datacenterID()] s.Equal(reqPath, req.RequestPath, "request path should be stored in status") s.Equal(http.MethodDelete, req.Method, "request method should be stored in status") s.Equal(infrav1.RequestStatusQueued, req.State, "request status should be stored in status") @@ -94,14 +94,14 @@ func (s *lanSuite) Test_Network_GetLAN_Error_NotUnique() { func (s *lanSuite) Test_Network_RemoveLANPendingRequestFromCluster_Successful() { s.infraCluster.Status.CurrentRequestByDatacenter = map[string]infrav1.ProvisioningRequest{ - s.service.dataCenterID(): { + s.service.datacenterID(): { RequestPath: reqPath, Method: http.MethodDelete, State: sdk.RequestStatusQueued, }, } s.NoError(s.service.removeLANPendingRequestFromCluster()) - s.NotContains(s.infraCluster.Status.CurrentRequestByDatacenter, s.service.dataCenterID(), "request should be removed from status") + s.NotContains(s.infraCluster.Status.CurrentRequestByDatacenter, s.service.datacenterID(), "request should be removed from status") } func (s *lanSuite) Test_Network_RemoveLANPendingRequestFromCluster_NoRequest() { @@ -158,7 +158,7 @@ func (s *lanSuite) Test_Network_ReconcileLANDelete_LANExists_NoPendingRequests_H requeue, err := s.service.ReconcileLANDeletion() s.NoError(err) s.False(requeue) - s.NotContains(s.infraCluster.Status.CurrentRequestByDatacenter, s.service.dataCenterID()) + s.NotContains(s.infraCluster.Status.CurrentRequestByDatacenter, s.service.datacenterID()) } func (s *lanSuite) Test_Network_ReconcileLANDelete_NoExistingLAN_ExistingRequest_Pending() { @@ -184,7 +184,7 @@ func (s *lanSuite) Test_Network_ReconcileLANDelete_LANDoesNotExist() { requeue, err := s.service.ReconcileLANDeletion() s.NoError(err) s.False(requeue) - s.NotContains(s.infraCluster.Status.CurrentRequestByDatacenter, s.service.dataCenterID()) + s.NotContains(s.infraCluster.Status.CurrentRequestByDatacenter, s.service.datacenterID()) } const ( @@ -235,18 +235,18 @@ func (s *lanSuite) exampleDeleteRequest(status string) []sdk.Request { } func (s *lanSuite) mockCreateLANCall() *clienttest.MockClient_CreateLAN_Call { - return s.ionosClient.EXPECT().CreateLAN(s.ctx, s.service.dataCenterID(), sdk.LanPropertiesPost{ + return s.ionosClient.EXPECT().CreateLAN(s.ctx, s.service.datacenterID(), sdk.LanPropertiesPost{ Name: ptr.To(s.service.lanName()), Public: ptr.To(true), }) } func (s *lanSuite) mockDeleteLANCall(id string) *clienttest.MockClient_DeleteLAN_Call { - return s.ionosClient.EXPECT().DeleteLAN(s.ctx, s.service.dataCenterID(), id) + return s.ionosClient.EXPECT().DeleteLAN(s.ctx, s.service.datacenterID(), id) } func (s *lanSuite) mockListLANsCall() *clienttest.MockClient_ListLANs_Call { - return s.ionosClient.EXPECT().ListLANs(s.ctx, s.service.dataCenterID()) + return s.ionosClient.EXPECT().ListLANs(s.ctx, s.service.datacenterID()) } func (s *lanSuite) mockGetLANCreationRequestsCall() *clienttest.MockClient_GetRequests_Call { diff --git a/internal/service/cloud/service.go b/internal/service/cloud/service.go index aff60a5f..5bded9c4 100644 --- a/internal/service/cloud/service.go +++ b/internal/service/cloud/service.go @@ -47,7 +47,7 @@ func (s *Service) api() ionoscloud.Client { return s.scope.ClusterScope.IonosClient } -// dataCenterID is a shortcut for getting the data center ID used by the IONOS Cloud machine. -func (s *Service) dataCenterID() string { - return s.scope.IonosMachine.Spec.DataCenterID +// datacenterID is a shortcut for getting the data center ID used by the IONOS Cloud machine. +func (s *Service) datacenterID() string { + return s.scope.IonosMachine.Spec.DatacenterID } diff --git a/internal/service/cloud/service_test.go b/internal/service/cloud/service_test.go index 33305f6a..bd166b33 100644 --- a/internal/service/cloud/service_test.go +++ b/internal/service/cloud/service_test.go @@ -17,7 +17,7 @@ limitations under the License. package cloud func (s *ServiceTestSuite) TestDatacenterID() { - s.Equal(s.service.scope.IonosMachine.Spec.DataCenterID, s.service.dataCenterID()) + s.Equal(s.service.scope.IonosMachine.Spec.DatacenterID, s.service.datacenterID()) } func (s *ServiceTestSuite) TestAPI() { diff --git a/internal/service/cloud/suite_test.go b/internal/service/cloud/suite_test.go index b91bd7fe..a96dd39e 100644 --- a/internal/service/cloud/suite_test.go +++ b/internal/service/cloud/suite_test.go @@ -103,7 +103,7 @@ func (s *ServiceTestSuite) SetupTest() { }, Spec: infrav1.IonosCloudMachineSpec{ ProviderID: "ionos://8c19a898-fda9-4783-a939-d778aeee217f", - DataCenterID: "ccf27092-34e8-499e-a2f5-2bdee9d34a12", + DatacenterID: "ccf27092-34e8-499e-a2f5-2bdee9d34a12", NumCores: 2, AvailabilityZone: infrav1.AvailabilityZoneAuto, MemoryMB: 4096,