diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index e641830d..1658306a 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -90,6 +90,21 @@ func (a AvailabilityZone) String() string { return string(a) } +// ServerType is the type of server which is created (ENTERPRISE or VCPU). +type ServerType string + +const ( + // ServerTypeEnterprise server of type ENTERPRISE. + ServerTypeEnterprise ServerType = "ENTERPRISE" + // ServerTypeVCPU server of type VCPU. + ServerTypeVCPU ServerType = "VCPU" +) + +// String returns the string representation of the ServerType. +func (a ServerType) String() string { + return string(a) +} + // IonosCloudMachineSpec defines the desired state of IonosCloudMachine. type IonosCloudMachineSpec struct { // ProviderID is the IONOS Cloud provider ID @@ -149,6 +164,13 @@ type IonosCloudMachineSpec struct { //+kubebuilder:validation:XValidation:rule=`self == "AUTO" || self.matches("((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$")`,message="failoverIP must be either 'AUTO' or a valid IPv4 address" //+optional FailoverIP *string `json:"failoverIP,omitempty"` + + // Type is the server type of the VM. Can be either ENTERPRISE or VCPU. + //+kubebuilder:validation:XValidation:rule="self == oldSelf",message="type is immutable" + //+kubebuilder:validation:Enum=ENTERPRISE;VCPU + //+kubebuilder:default=ENTERPRISE + //+optional + Type ServerType `json:"type,omitempty"` } // Networks contains a list of additional LAN IDs @@ -293,6 +315,7 @@ type IonosCloudMachine struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` + //+kubebuilder:validation:XValidation:rule="self.type != 'VCPU' || !has(self.cpuFamily)",message="cpuFamily must not be specified when using VCPU" Spec IonosCloudMachineSpec `json:"spec,omitempty"` Status IonosCloudMachineStatus `json:"status,omitempty"` } diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index 533025d4..db38c274 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -280,7 +280,7 @@ var _ = Describe("IonosCloudMachine Tests", func() { Expect(m.Spec.Disk.SizeGB).To(Equal(want)) }) }) - Context("Type", func() { + Context("DiskType", 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 @@ -384,6 +384,37 @@ var _ = Describe("IonosCloudMachine Tests", func() { Expect(k8sClient.Update(context.Background(), m)).ToNot(Succeed()) }) }) + Context("ServerType", func() { + It("should default to ENTERPRISE", func() { + m := defaultMachine() + // because Type is a string, setting the value as "" is the same as not setting anything + m.Spec.Type = "" + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(m.Spec.Type).To(Equal(ServerTypeEnterprise)) + }) + It("should fail if not part of the enum", func() { + m := defaultMachine() + m.Spec.Type = "this-should-fail" + Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) + }) + It("should fail if cpuFamily is set and type is VCPU", func() { + m := defaultMachine() + m.Spec.CPUFamily = ptr.To("some-cpu-family") + m.Spec.Type = ServerTypeVCPU + Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed()) + }) + DescribeTable("should work for value", + func(serverType ServerType) { + m := defaultMachine() + m.Spec.Type = serverType + m.Spec.CPUFamily = nil + Expect(k8sClient.Create(context.Background(), m)).To(Succeed()) + Expect(m.Spec.Type).To(Equal(serverType)) + }, + Entry("ENTERPRISE", ServerTypeEnterprise), + Entry("VCPU", ServerTypeVCPU), + ) + }) Context("Conditions", func() { It("should correctly set and get the conditions", func() { m := defaultMachine() 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 bc292352..8be7386e 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml @@ -185,10 +185,24 @@ spec: ProviderID is the IONOS Cloud provider ID will be in the format ionos://ee090ff2-1eef-48ec-a246-a51a33aa4f3a type: string + type: + default: ENTERPRISE + description: Type is the server type of the VM. Can be either ENTERPRISE + or VCPU. + enum: + - ENTERPRISE + - VCPU + type: string + x-kubernetes-validations: + - message: type is immutable + rule: self == oldSelf required: - datacenterID - disk type: object + x-kubernetes-validations: + - message: cpuFamily must not be specified when using VCPU + rule: self.type != 'VCPU' || !has(self.cpuFamily) status: description: IonosCloudMachineStatus defines the observed state of IonosCloudMachine. properties: diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachinetemplates.yaml index 882c020a..bcadd90a 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachinetemplates.yaml @@ -199,6 +199,17 @@ spec: ProviderID is the IONOS Cloud provider ID will be in the format ionos://ee090ff2-1eef-48ec-a246-a51a33aa4f3a type: string + type: + default: ENTERPRISE + description: Type is the server type of the VM. Can be either + ENTERPRISE or VCPU. + enum: + - ENTERPRISE + - VCPU + type: string + x-kubernetes-validations: + - message: type is immutable + rule: self == oldSelf required: - datacenterID - disk diff --git a/internal/service/cloud/server.go b/internal/service/cloud/server.go index 10718cb3..6b789045 100644 --- a/internal/service/cloud/server.go +++ b/internal/service/cloud/server.go @@ -371,6 +371,7 @@ func (*Service) buildServerProperties( Name: ptr.To(ms.IonosMachine.Name), Ram: &machineSpec.MemoryMB, CpuFamily: machineSpec.CPUFamily, + Type: ptr.To(machineSpec.Type.String()), } return props diff --git a/internal/service/cloud/server_test.go b/internal/service/cloud/server_test.go index 45dc4069..96bed245 100644 --- a/internal/service/cloud/server_test.go +++ b/internal/service/cloud/server_test.go @@ -28,6 +28,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud/clienttest" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/util/ptr" ) @@ -148,10 +149,10 @@ func (s *serverSuite) TestReconcileServerRequestDoneStateAvailableTurnedOff() { s.True(requeue) } -func (s *serverSuite) TestReconcileServerNoRequest() { +func (s *serverSuite) TestReconcileEnterpriseServerNoRequest() { s.prepareReconcileServerRequestTest() s.mockGetServerCreationRequestCall().Return([]sdk.Request{}, nil) - s.mockCreateServerCall().Return(&sdk.Server{Id: ptr.To("12345")}, "location/to/server", nil) + s.mockCreateServerCall(infrav1.ServerTypeEnterprise).Return(&sdk.Server{Id: ptr.To("12345")}, "location/to/server", nil) s.mockListLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{{ Id: ptr.To("1"), Properties: &sdk.LanProperties{ @@ -166,6 +167,25 @@ func (s *serverSuite) TestReconcileServerNoRequest() { s.True(requeue) } +func (s *serverSuite) TestReconcileVCPUServerNoRequest() { + s.prepareReconcileServerRequestTest() + s.mockGetServerCreationRequestCall().Return([]sdk.Request{}, nil) + s.mockCreateServerCall(infrav1.ServerTypeVCPU).Return(&sdk.Server{Id: ptr.To("12345")}, "location/to/server", nil) + s.mockListLANsCall().Return(&sdk.Lans{Items: &[]sdk.Lan{{ + Id: ptr.To("1"), + Properties: &sdk.LanProperties{ + Name: ptr.To(s.service.lanName(s.clusterScope.Cluster)), + Public: ptr.To(true), + }, + }}}, nil) + + s.infraMachine.Spec.Type = infrav1.ServerTypeVCPU + requeue, err := s.service.ReconcileServer(s.ctx, s.machineScope) + s.Equal("ionos://12345", ptr.Deref(s.machineScope.IonosMachine.Spec.ProviderID, "")) + s.NoError(err) + s.True(requeue) +} + func (s *serverSuite) prepareReconcileServerRequestTest() { s.T().Helper() bootstrapSecret := &corev1.Secret{ @@ -424,11 +444,11 @@ func (s *serverSuite) mockGetServerDeletionRequestCall(serverID string) *clientt http.MethodDelete, path.Join(s.service.serversURL(s.machineScope.DatacenterID()), serverID)) } -func (s *serverSuite) mockCreateServerCall() *clienttest.MockClient_CreateServer_Call { +func (s *serverSuite) mockCreateServerCall(serverType infrav1.ServerType) *clienttest.MockClient_CreateServer_Call { return s.ionosClient.EXPECT().CreateServer( s.ctx, s.machineScope.DatacenterID(), - mock.Anything, + mock.MatchedBy(hasServerType(serverType)), mock.Anything, ) } @@ -461,3 +481,9 @@ func (s *serverSuite) examplePostRequest(status string) sdk.Request { } return s.exampleRequest(opts) } + +func hasServerType(serverType infrav1.ServerType) func(properties sdk.ServerProperties) bool { + return func(properties sdk.ServerProperties) bool { + return ptr.Deref(properties.Type, "") == serverType.String() + } +} diff --git a/internal/service/cloud/suite_test.go b/internal/service/cloud/suite_test.go index 62f427fb..98736bc3 100644 --- a/internal/service/cloud/suite_test.go +++ b/internal/service/cloud/suite_test.go @@ -161,6 +161,7 @@ func (s *ServiceTestSuite) SetupTest() { ID: "3e3e3e3e-3e3e-3e3e-3e3e-3e3e3e3e3e3e", }, }, + Type: infrav1.ServerTypeEnterprise, }, Status: infrav1.IonosCloudMachineStatus{}, }