diff --git a/api/v1beta1/oscmachinetemplate_webhook_test.go b/api/v1beta1/oscmachinetemplate_webhook_test.go index 382c2bda3..755ae19f4 100644 --- a/api/v1beta1/oscmachinetemplate_webhook_test.go +++ b/api/v1beta1/oscmachinetemplate_webhook_test.go @@ -265,6 +265,7 @@ func TestOscMachineTemplate_ValidateUpdate(t *testing.T) { ImageId: "ami-00000000", DeviceName: "/dev/xvda", VmType: "tinav3.c2r4p2", + PublicIp: false, }, Volumes: []*OscVolume{ { @@ -320,7 +321,7 @@ func TestOscMachineTemplate_ValidateUpdate(t *testing.T) { }, }, }, - expValidateUpdateErr: fmt.Errorf("OscMachineTemplate.infrastructure.cluster.x-k8s.io \"webhook-test\" is invalid: OscMachineTemplate.spec.template.spec: Invalid value: v1beta1.OscMachineTemplate{TypeMeta:v1.TypeMeta{Kind:\"\", APIVersion:\"\"}, ObjectMeta:v1.ObjectMeta{Name:\"webhook-test\", GenerateName:\"\", Namespace:\"default\", SelfLink:\"\", UID:\"\", ResourceVersion:\"\", Generation:0, CreationTimestamp:time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), DeletionTimestamp:, DeletionGracePeriodSeconds:(*int64)(nil), Labels:map[string]string(nil), Annotations:map[string]string(nil), OwnerReferences:[]v1.OwnerReference(nil), Finalizers:[]string(nil), ManagedFields:[]v1.ManagedFieldsEntry(nil)}, Spec:v1beta1.OscMachineTemplateSpec{Template:v1beta1.OscMachineTemplateResource{ObjectMeta:v1beta1.ObjectMeta{Labels:map[string]string(nil), Annotations:map[string]string(nil)}, Spec:v1beta1.OscMachineSpec{ProviderID:(*string)(nil), Node:v1beta1.OscNode{Vm:v1beta1.OscVm{Name:\"\", ImageId:\"ami-00000000\", KeypairName:\"test-webhook-2\", VmType:\"tinav3.c2r4p2\", VolumeName:\"\", VolumeDeviceName:\"\", DeviceName:\"/dev/xvda\", SubnetName:\"\", RootDisk:v1beta1.OscRootDisk{RootDiskIops:0, RootDiskSize:0, RootDiskType:\"\"}, LoadBalancerName:\"\", PublicIpName:\"\", SubregionName:\"\", PrivateIps:[]v1beta1.OscPrivateIpElement(nil), SecurityGroupNames:[]v1beta1.OscSecurityGroupElement(nil), ResourceId:\"\", Role:\"\", ClusterName:\"\", Replica:0}, Image:v1beta1.OscImage{Name:\"\", ResourceId:\"\"}, Volumes:[]*v1beta1.OscVolume(nil), KeyPair:v1beta1.OscKeypair{Name:\"\", PublicKey:\"\", ResourceId:\"\", ClusterName:\"\", DeleteKeypair:false}, ClusterName:\"\"}}}}, Status:v1beta1.OscMachineTemplateStatus{Capacity:v1.ResourceList(nil), Conditions:v1beta1.Conditions(nil)}}: OscMachineTemplate spec.template.spec field is immutable."), + expValidateUpdateErr: fmt.Errorf("OscMachineTemplate.infrastructure.cluster.x-k8s.io \"webhook-test\" is invalid: OscMachineTemplate.spec.template.spec: Invalid value: v1beta1.OscMachineTemplate{TypeMeta:v1.TypeMeta{Kind:\"\", APIVersion:\"\"}, ObjectMeta:v1.ObjectMeta{Name:\"webhook-test\", GenerateName:\"\", Namespace:\"default\", SelfLink:\"\", UID:\"\", ResourceVersion:\"\", Generation:0, CreationTimestamp:time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), DeletionTimestamp:, DeletionGracePeriodSeconds:(*int64)(nil), Labels:map[string]string(nil), Annotations:map[string]string(nil), OwnerReferences:[]v1.OwnerReference(nil), Finalizers:[]string(nil), ManagedFields:[]v1.ManagedFieldsEntry(nil)}, Spec:v1beta1.OscMachineTemplateSpec{Template:v1beta1.OscMachineTemplateResource{ObjectMeta:v1beta1.ObjectMeta{Labels:map[string]string(nil), Annotations:map[string]string(nil)}, Spec:v1beta1.OscMachineSpec{ProviderID:(*string)(nil), Node:v1beta1.OscNode{Vm:v1beta1.OscVm{Name:\"\", ImageId:\"ami-00000000\", KeypairName:\"test-webhook-2\", VmType:\"tinav3.c2r4p2\", VolumeName:\"\", VolumeDeviceName:\"\", DeviceName:\"/dev/xvda\", SubnetName:\"\", RootDisk:v1beta1.OscRootDisk{RootDiskIops:0, RootDiskSize:0, RootDiskType:\"\"}, LoadBalancerName:\"\", PublicIpName:\"\", PublicIp:false, SubregionName:\"\", PrivateIps:[]v1beta1.OscPrivateIpElement(nil), SecurityGroupNames:[]v1beta1.OscSecurityGroupElement(nil), ResourceId:\"\", Role:\"\", ClusterName:\"\", Replica:0}, Image:v1beta1.OscImage{Name:\"\", ResourceId:\"\"}, Volumes:[]*v1beta1.OscVolume(nil), KeyPair:v1beta1.OscKeypair{Name:\"\", PublicKey:\"\", ResourceId:\"\", ClusterName:\"\", DeleteKeypair:false}, ClusterName:\"\"}}}}, Status:v1beta1.OscMachineTemplateStatus{Capacity:v1.ResourceList(nil), Conditions:v1beta1.Conditions(nil)}}: OscMachineTemplate spec.template.spec field is immutable."), }, } for _, mtc := range machineTestCases { diff --git a/api/v1beta1/types.go b/api/v1beta1/types.go index 7bfeadcef..9b2dbe9c2 100644 --- a/api/v1beta1/types.go +++ b/api/v1beta1/types.go @@ -326,6 +326,7 @@ type OscNodeResource struct { KeypairRef OscResourceReference `json:"keypairRef,omitempty"` VmRef OscResourceReference `json:"vmRef,omitempty"` LinkPublicIpRef OscResourceReference `json:"linkPublicIpRef,omitempty"` + PublicIpIdRef OscResourceReference `json:"publicIpIdRef,omitempty"` } type OscImage struct { @@ -362,6 +363,7 @@ type OscVm struct { RootDisk OscRootDisk `json:"rootDisk,omitempty"` LoadBalancerName string `json:"loadBalancerName,omitempty"` PublicIpName string `json:"publicIpName,omitempty"` + PublicIp bool `json:"publicIp,omitempty"` SubregionName string `json:"subregionName,omitempty"` PrivateIps []OscPrivateIpElement `json:"privateIps,omitempty"` SecurityGroupNames []OscSecurityGroupElement `json:"securityGroupNames,omitempty"` diff --git a/cloud/scope/machine.go b/cloud/scope/machine.go index 6442c6fae..c4910b3a9 100644 --- a/cloud/scope/machine.go +++ b/cloud/scope/machine.go @@ -20,6 +20,7 @@ import ( "context" "errors" "fmt" + "github.com/go-logr/logr" infrastructurev1beta1 "github.com/outscale-dev/cluster-api-provider-outscale.git/api/v1beta1" osc "github.com/outscale/osc-sdk-go/v2" @@ -192,6 +193,11 @@ func (m *MachineScope) GetLinkPublicIpRef() *infrastructurev1beta1.OscResourceRe return &m.OscMachine.Status.Node.LinkPublicIpRef } +// GetLinkPublicIpRef get the status of linkPublicIpRef (a Map with tag name with machine uid associate with resource response id) +func (m *MachineScope) GetPublicIpIdRef() *infrastructurev1beta1.OscResourceReference { + return &m.OscMachine.Status.Node.PublicIpIdRef +} + // GetKeyPair return the keypair of the cluster func (m *MachineScope) GetKeypair() *infrastructurev1beta1.OscKeypair { return &m.OscMachine.Spec.Node.KeyPair diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_oscmachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_oscmachines.yaml index d29df3b66..e3e29980b 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_oscmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_oscmachines.yaml @@ -88,6 +88,8 @@ spec: - privateIp type: object type: array + publicIp: + type: boolean publicIpName: type: string replica: @@ -249,6 +251,15 @@ spec: type: string type: object type: object + publicIpIdRef: + description: Map between resourceId and resourceName (tag Name + with cluster UID) + properties: + resourceMap: + additionalProperties: + type: string + type: object + type: object vmRef: description: Map between resourceId and resourceName (tag Name with cluster UID) diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_oscmachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_oscmachinetemplates.yaml index c8e8b589a..0222ff727 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_oscmachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_oscmachinetemplates.yaml @@ -140,6 +140,8 @@ spec: - privateIp type: object type: array + publicIp: + type: boolean publicIpName: type: string replica: diff --git a/controllers/oscmachine_vm_controller.go b/controllers/oscmachine_vm_controller.go index 84b61ece9..dae57290d 100644 --- a/controllers/oscmachine_vm_controller.go +++ b/controllers/oscmachine_vm_controller.go @@ -149,18 +149,21 @@ func checkVmSubnetOscAssociateResourceName(machineScope *scope.MachineScope, clu } } -// checkVmPublicIpOscAssociateResourceName check that PublicIp dependancies tag name in both resource configuration are the same. +// checkVmPublicIpOscAssociateResourceName check that PublicIp dependencies tag name in both resource configuration are the same. func checkVmPublicIpOscAssociateResourceName(machineScope *scope.MachineScope, clusterScope *scope.ClusterScope) error { var resourceNameList []string vmSpec := machineScope.GetVm() vmSpec.SetDefaultValue() + if vmSpec.PublicIp { + return nil + } vmPublicIpName := vmSpec.PublicIpName + "-" + clusterScope.GetUID() publicIpsSpec := clusterScope.GetPublicIp() for _, publicIpSpec := range publicIpsSpec { publicIpName := publicIpSpec.Name + "-" + clusterScope.GetUID() resourceNameList = append(resourceNameList, publicIpName) } - machineScope.V(2).Info("Check match publicip with vm") + machineScope.V(2).Info("Check match publicip with vm on cluster") checkOscAssociate := Contains(resourceNameList, vmPublicIpName) if checkOscAssociate { return nil @@ -337,11 +340,33 @@ func reconcileVm(ctx context.Context, clusterScope *scope.ClusterScope, machineS var publicIpId string var vmPublicIpName string var linkPublicIpRef *infrastructurev1beta1.OscResourceReference + if vmSpec.PublicIp { + vmSpec.PublicIpName = vmSpec.Name + "-publicIp" + vmPublicIpName = vmSpec.PublicIpName + "-" + clusterScope.GetUID() + var ipFound bool + publicIpIdRef := machineScope.GetPublicIpIdRef() + publicIpId, ipFound = publicIpIdRef.ResourceMap[vmPublicIpName] + if !ipFound { + publicIp, err := publicIpSvc.CreatePublicIp(vmPublicIpName) + if err != nil { + return reconcile.Result{}, fmt.Errorf("%w Can not create publicIp for Vm %s/%s", err, clusterScope.GetNamespace(), clusterScope.GetName()) + } + clusterScope.V(4).Info("Get publicIp for Vm", "publicip", publicIp) + publicIpId = publicIp.GetPublicIpId() + + if len(publicIpIdRef.ResourceMap) == 0 { + publicIpIdRef.ResourceMap = make(map[string]string) + } + publicIpIdRef.ResourceMap[vmPublicIpName] = publicIpId + } + } if vmSpec.PublicIpName != "" { vmPublicIpName = vmSpec.PublicIpName + "-" + clusterScope.GetUID() - publicIpId, err = getPublicIpResourceId(vmPublicIpName, clusterScope) - if err != nil { - return reconcile.Result{}, err + if publicIpId == "" { + publicIpId, err = getPublicIpResourceId(vmPublicIpName, clusterScope) + if err != nil { + return reconcile.Result{}, err + } } linkPublicIpRef = machineScope.GetLinkPublicIpRef() if len(linkPublicIpRef.ResourceMap) == 0 { @@ -573,7 +598,15 @@ func reconcileDeleteVm(ctx context.Context, clusterScope *scope.ClusterScope, ma if err != nil { return reconcile.Result{}, fmt.Errorf("%w Can not unlink publicIp for OscCluster %s/%s", err, machineScope.GetNamespace(), machineScope.GetName()) } - + } + if vmSpec.PublicIp { + publicIpIdRef := machineScope.GetPublicIpIdRef() + publicIpName := vmSpec.PublicIpName + "-" + clusterScope.GetUID() + clusterScope.V(2).Info("Delete the desired Vm publicip", "publicIpName", publicIpName) + err = publicIpSvc.DeletePublicIp(publicIpIdRef.ResourceMap[publicIpName]) + if err != nil { + return reconcile.Result{}, fmt.Errorf("%w Can not delete Vm publicIp for Osccluster %s/%s", err, clusterScope.GetNamespace(), clusterScope.GetName()) + } } if vmSpec.LoadBalancerName != "" { vmIds := []string{vmId} diff --git a/controllers/oscmachine_vm_controller_unit_test.go b/controllers/oscmachine_vm_controller_unit_test.go index 712e721db..60c6115af 100644 --- a/controllers/oscmachine_vm_controller_unit_test.go +++ b/controllers/oscmachine_vm_controller_unit_test.go @@ -315,6 +315,50 @@ var ( }, } + defaultVmInitializeWithPublicIp = infrastructurev1beta1.OscMachineSpec{ + Node: infrastructurev1beta1.OscNode{ + Volumes: []*infrastructurev1beta1.OscVolume{ + { + Name: "test-volume", + Iops: 1000, + Size: 50, + VolumeType: "io1", + SubregionName: "eu-west-2a", + }, + }, + Vm: infrastructurev1beta1.OscVm{ + ClusterName: "test-cluster", + Name: "test-vm", + ImageId: "ami-00000000", + Role: "controlplane", + DeviceName: "/dev/sda1", + RootDisk: infrastructurev1beta1.OscRootDisk{ + RootDiskSize: 30, + RootDiskIops: 1500, + RootDiskType: "gp2", + }, + KeypairName: "rke", + SubregionName: "eu-west-2a", + SubnetName: "test-subnet", + LoadBalancerName: "test-loadbalancer", + PublicIpName: "test-publicip", + VmType: "tinav3.c2r4p2", + Replica: 1, + SecurityGroupNames: []infrastructurev1beta1.OscSecurityGroupElement{ + { + Name: "test-securitygroup", + }, + }, + PrivateIps: []infrastructurev1beta1.OscPrivateIpElement{ + { + Name: "test-privateip", + PrivateIp: "10.0.0.17", + }, + }, + }, + }, + } + defaultMultiVmInitialize = infrastructurev1beta1.OscMachineSpec{ Node: infrastructurev1beta1.OscNode{ Volumes: []*infrastructurev1beta1.OscVolume{ @@ -408,6 +452,52 @@ var ( }, }, } + defaultVmReconcileWithDedicatedIp = infrastructurev1beta1.OscMachineSpec{ + Node: infrastructurev1beta1.OscNode{ + Volumes: []*infrastructurev1beta1.OscVolume{ + { + Name: "test-volume", + Iops: 1000, + Size: 50, + VolumeType: "io1", + SubregionName: "eu-west-2a", + ResourceId: "volume-test-volume-uid", + }, + }, + Vm: infrastructurev1beta1.OscVm{ + ClusterName: "test-cluster", + Name: "test-vm", + ImageId: "ami-00000000", + Role: "controlplane", + DeviceName: "/dev/xvdb", + KeypairName: "rke", + RootDisk: infrastructurev1beta1.OscRootDisk{ + + RootDiskSize: 30, + RootDiskIops: 1500, + RootDiskType: "io1", + }, + SubregionName: "eu-west-2a", + SubnetName: "test-subnet", + LoadBalancerName: "test-loadbalancer", + VmType: "tinav3.c2r4p2", + ResourceId: "i-test-vm-uid", + PublicIp: true, + Replica: 1, + SecurityGroupNames: []infrastructurev1beta1.OscSecurityGroupElement{ + { + Name: "test-securitygroup", + }, + }, + PrivateIps: []infrastructurev1beta1.OscPrivateIpElement{ + { + Name: "test-privateip", + PrivateIp: "10.0.0.17", + }, + }, + }, + }, + } ) // SetupWithVmMock set vmMock with clusterScope, machineScope and oscmachine @@ -3443,6 +3533,7 @@ func TestReconcileVmResourceId(t *testing.T) { expLinkPublicIpFound bool expSecurityGroupFound bool expLoadBalancerSecurityGroupFound bool + expCreatePublicIpFound bool expReadTagErr error expReconcileVmErr error }{ @@ -3457,6 +3548,7 @@ func TestReconcileVmResourceId(t *testing.T) { expSecurityGroupFound: true, expLoadBalancerSecurityGroupFound: true, expTagFound: false, + expCreatePublicIpFound: false, expReadTagErr: nil, expReconcileVmErr: fmt.Errorf("test-volume-uid does not exist"), }, @@ -3471,11 +3563,12 @@ func TestReconcileVmResourceId(t *testing.T) { expSecurityGroupFound: true, expLoadBalancerSecurityGroupFound: true, expTagFound: false, + expCreatePublicIpFound: false, expReadTagErr: nil, expReconcileVmErr: fmt.Errorf("test-subnet-uid does not exist"), }, { - name: "PublicIp does not exist ", + name: "PublicIp does not exist on clusterScope", clusterSpec: defaultVmClusterInitialize, machineSpec: defaultVmInitialize, expVolumeFound: true, @@ -3485,6 +3578,7 @@ func TestReconcileVmResourceId(t *testing.T) { expSecurityGroupFound: true, expLoadBalancerSecurityGroupFound: true, expTagFound: false, + expCreatePublicIpFound: false, expReadTagErr: nil, expReconcileVmErr: fmt.Errorf("test-publicip-uid does not exist"), }, @@ -3499,6 +3593,7 @@ func TestReconcileVmResourceId(t *testing.T) { expSecurityGroupFound: false, expLoadBalancerSecurityGroupFound: false, expTagFound: false, + expCreatePublicIpFound: false, expReadTagErr: nil, expReconcileVmErr: fmt.Errorf("test-securitygroup-uid does not exist"), }, @@ -3513,6 +3608,7 @@ func TestReconcileVmResourceId(t *testing.T) { expSecurityGroupFound: true, expLoadBalancerSecurityGroupFound: true, expTagFound: true, + expCreatePublicIpFound: false, expReadTagErr: fmt.Errorf("ReadTag generic error"), expReconcileVmErr: fmt.Errorf("ReadTag generic error Can not get tag for OscMachine test-system/test-osc"), }, @@ -3621,6 +3717,7 @@ func TestReconcileDeleteVm(t *testing.T) { expListMachine bool expDeleteInboundSecurityGroupRuleFound bool expDeleteOutboundSecurityGroupRuleFound bool + expDeleteDedicatedPublicIpFound bool expUnlinkLoadBalancerBackendMachineErr error expDeleteInboundSecurityGroupRuleErr error expDeleteOutboundSecurityGroupRuleErr error @@ -3638,6 +3735,7 @@ func TestReconcileDeleteVm(t *testing.T) { expListMachine: false, expDeleteInboundSecurityGroupRuleFound: true, expDeleteOutboundSecurityGroupRuleFound: true, + expDeleteDedicatedPublicIpFound: false, expUnlinkLoadBalancerBackendMachineErr: nil, expDeleteInboundSecurityGroupRuleErr: nil, expDeleteOutboundSecurityGroupRuleErr: nil, @@ -3665,6 +3763,23 @@ func TestReconcileDeleteVm(t *testing.T) { expCheckUnlinkPublicIpErr: nil, expReconcileDeleteVmErr: nil, }, + { + name: "delete vm with publicIp", + clusterSpec: defaultVmClusterReconcile, + machineSpec: defaultVmReconcileWithDedicatedIp, + expDeleteInboundSecurityGroupRuleFound: true, + expDeleteOutboundSecurityGroupRuleFound: true, + expDeleteDedicatedPublicIpFound: true, + expUnlinkLoadBalancerBackendMachineErr: nil, + expDeleteInboundSecurityGroupRuleErr: nil, + expDeleteOutboundSecurityGroupRuleErr: nil, + expSecurityGroupRuleFound: true, + expDeleteVmErr: nil, + expGetVmFound: true, + expGetVmErr: nil, + expCheckUnlinkPublicIpErr: nil, + expReconcileDeleteVmErr: nil, + }, { name: "delete first vm in group", clusterSpec: defaultVmClusterReconcile, @@ -3689,6 +3804,7 @@ func TestReconcileDeleteVm(t *testing.T) { expListMachine: false, expDeleteInboundSecurityGroupRuleFound: true, expDeleteOutboundSecurityGroupRuleFound: true, + expDeleteDedicatedPublicIpFound: false, expUnlinkLoadBalancerBackendMachineErr: nil, expSecurityGroupRuleFound: true, expDeleteVmErr: fmt.Errorf("DeleteVm generic error"), @@ -3721,6 +3837,14 @@ func TestReconcileDeleteVm(t *testing.T) { securityGroupsRef.ResourceMap[securityGroupName] = securityGroupId securityGroupIds = append(securityGroupIds, securityGroupId) } + + if vtc.expDeleteDedicatedPublicIpFound { + publicIpName := machineScope.GetName() + "-publicIp" + machineScope.OscMachine.Spec.Node.Vm.PublicIpName = publicIpName + vtc.machineSpec.Node.Vm.PublicIpName = publicIpName + machineScope.GetPublicIpIdRef().ResourceMap = map[string]string{publicIpName + "-uid": "eipassoc-" + publicIpName + "-uid"} + } + publicIpName := vtc.machineSpec.Node.Vm.PublicIpName + "-uid" linkPublicIpId := "eipassoc-" + publicIpName linkPublicIpRef := machineScope.GetLinkPublicIpRef() @@ -3801,6 +3925,12 @@ func TestReconcileDeleteVm(t *testing.T) { DeleteSecurityGroupRule(gomock.Eq(associateSecurityGroupId), gomock.Eq("Outbound"), gomock.Eq(ipProtocol), "", gomock.Eq(securityGroupIds[0]), gomock.Eq(fromPortRange), gomock.Eq(toPortRange)). Return(vtc.expDeleteOutboundSecurityGroupRuleErr) } + if vtc.expDeleteDedicatedPublicIpFound { + mockOscPublicIpInterface. + EXPECT(). + DeletePublicIp(gomock.Eq("eipassoc-test-osc-publicIp-uid")). + Return(nil) + } if vtc.expDeleteInboundSecurityGroupRuleFound { mockOscSecurityGroupInterface.