diff --git a/api/v1beta1/consts.go b/api/v1beta1/consts.go index e65e0a02..9848dfcc 100644 --- a/api/v1beta1/consts.go +++ b/api/v1beta1/consts.go @@ -33,6 +33,13 @@ const ( // CAPEVersionAnnotation is the annotation identifying the version of CAPE that the resource reconciled by. CAPEVersionAnnotation = "cape.infrastructure.cluster.x-k8s.io/cape-version" + + // CreatedByAnnotation is the annotation identifying the creator of the resource. + // + // The creator can be in one of the following two formats: + // 1. ${Tower username}@${Tower auth_config_id}, e.g. caas.smartx@7e98ecbb-779e-43f6-8330-1bc1d29fffc7. + // 2. ${Tower username}, e.g. root. If auth_config_id is not set, it means it is a LOCAL user. + CreatedByAnnotation = "cape.infrastructure.cluster.x-k8s.io/created-by" ) // Labels. diff --git a/controllers/elfmachine_controller.go b/controllers/elfmachine_controller.go index c7a10f7a..6ee08025 100644 --- a/controllers/elfmachine_controller.go +++ b/controllers/elfmachine_controller.go @@ -57,6 +57,7 @@ import ( labelsutil "github.com/smartxworks/cluster-api-provider-elf/pkg/util/labels" machineutil "github.com/smartxworks/cluster-api-provider-elf/pkg/util/machine" patchutil "github.com/smartxworks/cluster-api-provider-elf/pkg/util/patch" + typesutil "github.com/smartxworks/cluster-api-provider-elf/pkg/util/types" ) //+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=elfmachines,verbs=get;list;watch;create;update;patch;delete @@ -612,14 +613,14 @@ func (r *ElfMachineReconciler) reconcileVM(ctx *context.MachineContext) (*models vmRef := util.GetVMRef(vm) // If vmRef is in UUID format, it means that the ELF VM created. - if !machineutil.IsUUID(vmRef) { + if !typesutil.IsUUID(vmRef) { ctx.Logger.Info("The VM is being created", "vmRef", vmRef) return vm, false, nil } // When ELF VM created, set UUID to VMRef - if !machineutil.IsUUID(ctx.ElfMachine.Status.VMRef) { + if !typesutil.IsUUID(ctx.ElfMachine.Status.VMRef) { ctx.ElfMachine.SetVM(vmRef) } @@ -660,7 +661,7 @@ func (r *ElfMachineReconciler) getVM(ctx *context.MachineContext) (*models.VM, e return nil, err } - if machineutil.IsUUID(ctx.ElfMachine.Status.VMRef) { + if typesutil.IsUUID(ctx.ElfMachine.Status.VMRef) { vmDisconnectionTimestamp := ctx.ElfMachine.GetVMDisconnectionTimestamp() if vmDisconnectionTimestamp == nil { now := metav1.Now() diff --git a/controllers/elfmachine_controller_placement_group.go b/controllers/elfmachine_controller_placement_group.go index bb25319f..ba204e05 100644 --- a/controllers/elfmachine_controller_placement_group.go +++ b/controllers/elfmachine_controller_placement_group.go @@ -42,6 +42,7 @@ import ( annotationsutil "github.com/smartxworks/cluster-api-provider-elf/pkg/util/annotations" kcputil "github.com/smartxworks/cluster-api-provider-elf/pkg/util/kcp" machineutil "github.com/smartxworks/cluster-api-provider-elf/pkg/util/machine" + typesutil "github.com/smartxworks/cluster-api-provider-elf/pkg/util/types" "github.com/smartxworks/cluster-api-provider-elf/pkg/version" ) @@ -258,7 +259,7 @@ func (r *ElfMachineReconciler) getVMHostForRollingUpdate(ctx *context.MachineCon elfMachineMap := make(map[string]*infrav1.ElfMachine) for i := 0; i < len(elfMachines); i++ { - if machineutil.IsUUID(elfMachines[i].Status.VMRef) { + if typesutil.IsUUID(elfMachines[i].Status.VMRef) { elfMachineMap[elfMachines[i].Name] = elfMachines[i] } } @@ -359,7 +360,7 @@ func (r *ElfMachineReconciler) getPlacementGroup(ctx *context.MachineContext, pl } // Placement group is performing an operation - if !machineutil.IsUUID(*placementGroup.LocalID) || placementGroup.EntityAsyncStatus != nil { + if !typesutil.IsUUID(*placementGroup.LocalID) || placementGroup.EntityAsyncStatus != nil { ctx.Logger.Info("Waiting for placement group task done", "placementGroup", *placementGroup.Name) return nil, nil diff --git a/pkg/service/consts.go b/pkg/service/consts.go index 0e30697b..19fdf97a 100644 --- a/pkg/service/consts.go +++ b/pkg/service/consts.go @@ -23,3 +23,7 @@ const ( // SKSVMTemplateUIDLabel is the label used to find the virtual machine template. SKSVMTemplateUIDLabel = "system.cloudtower/sks-template-uid" ) + +// VMOwnerSearchForUsername is used to specify the ower source of the virtual machine. +// TODO: Tower SDK will provide the VMOwnerSearchFor enumeration types in the new version. +const VMOwnerSearchForUsername = "username" diff --git a/pkg/service/util.go b/pkg/service/util.go index 6a6a81f5..8d6819ad 100644 --- a/pkg/service/util.go +++ b/pkg/service/util.go @@ -26,6 +26,7 @@ import ( infrav1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1" "github.com/smartxworks/cluster-api-provider-elf/pkg/config" + typesutil "github.com/smartxworks/cluster-api-provider-elf/pkg/util/types" ) // GetUpdatedVMRestrictedFields returns the updated restricted fields of the VM compared to ElfMachine. @@ -309,3 +310,31 @@ func calGPUAvailableVgpusNum(vgpuInstanceNum, assignedVGPUsNum int32) int32 { return count } + +// parseOwnerFromCreatedByAnnotation parse owner from createdBy annotation. +// +// The createdBy can be in one of the following two formats: +// 1. ${Tower username}@${Tower auth_config_id}. +// 2. ${Tower username}. +// +// The owner can be in one of the following two formats: +// 1. ${Tower username}_${Tower auth_config_id}, e.g. caas.smartx_7e98ecbb-779e-43f6-8330-1bc1d29fffc7. +// 2. ${Tower username}, e.g. root. If auth_config_id is not set, it means it is a LOCAL user. +func parseOwnerFromCreatedByAnnotation(createdBy string) string { + lastIndex := strings.LastIndex(createdBy, "@") + if len(createdBy) <= 1 || lastIndex <= 0 || lastIndex == len(createdBy) { + return createdBy + } + + username := createdBy[0:lastIndex] + authConfigID := createdBy[lastIndex+1:] + + // If authConfigID is not in UUID format, it means username contains the last `@` character, + // return createdBy directly. + if !typesutil.IsUUID(authConfigID) { + return createdBy + } + + // last `@` replaced with `_`. + return fmt.Sprintf("%s_%s", username, authConfigID) +} diff --git a/pkg/service/util_test.go b/pkg/service/util_test.go index c3f4d132..d409657b 100644 --- a/pkg/service/util_test.go +++ b/pkg/service/util_test.go @@ -261,3 +261,26 @@ func TestHasGPUsCanNotBeUsedForVM(t *testing.T) { }), elfMachine)).To(gomega.BeFalse()) }) } + +func TestParseOwnerFromCreatedByAnnotation(t *testing.T) { + g := gomega.NewGomegaWithT(t) + + t.Run("parseOwnerFromCreatedByAnnotation", func(t *testing.T) { + g.Expect(parseOwnerFromCreatedByAnnotation("")).To(gomega.Equal("")) + g.Expect(parseOwnerFromCreatedByAnnotation("a")).To(gomega.Equal("a")) + g.Expect(parseOwnerFromCreatedByAnnotation("@")).To(gomega.Equal("@")) + g.Expect(parseOwnerFromCreatedByAnnotation("a@")).To(gomega.Equal("a@")) + g.Expect(parseOwnerFromCreatedByAnnotation("@a")).To(gomega.Equal("@a")) + g.Expect(parseOwnerFromCreatedByAnnotation("@@")).To(gomega.Equal("@@")) + g.Expect(parseOwnerFromCreatedByAnnotation("root")).To(gomega.Equal("root")) + g.Expect(parseOwnerFromCreatedByAnnotation("@root")).To(gomega.Equal("@root")) + g.Expect(parseOwnerFromCreatedByAnnotation("ro@ot")).To(gomega.Equal("ro@ot")) + g.Expect(parseOwnerFromCreatedByAnnotation("root@")).To(gomega.Equal("root@")) + g.Expect(parseOwnerFromCreatedByAnnotation("@ro@ot@")).To(gomega.Equal("@ro@ot@")) + g.Expect(parseOwnerFromCreatedByAnnotation("root@123456")).To(gomega.Equal("root@123456")) + g.Expect(parseOwnerFromCreatedByAnnotation("root@d8dc20fc-e197-41da-83b6-c903c88663fd")).To(gomega.Equal("root_d8dc20fc-e197-41da-83b6-c903c88663fd")) + g.Expect(parseOwnerFromCreatedByAnnotation("@root@d8dc20fc-e197-41da-83b6-c903c88663fd")).To(gomega.Equal("@root_d8dc20fc-e197-41da-83b6-c903c88663fd")) + g.Expect(parseOwnerFromCreatedByAnnotation("root@@d8dc20fc-e197-41da-83b6-c903c88663fd")).To(gomega.Equal("root@_d8dc20fc-e197-41da-83b6-c903c88663fd")) + g.Expect(parseOwnerFromCreatedByAnnotation("root@d8dc20fc-e197-41da-83b6-c903c88663fd@")).To(gomega.Equal("root@d8dc20fc-e197-41da-83b6-c903c88663fd@")) + }) +} diff --git a/pkg/service/vm.go b/pkg/service/vm.go index 2e5c7e72..d5427181 100644 --- a/pkg/service/vm.go +++ b/pkg/service/vm.go @@ -39,6 +39,7 @@ import ( infrav1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1" "github.com/smartxworks/cluster-api-provider-elf/pkg/config" "github.com/smartxworks/cluster-api-provider-elf/pkg/session" + annotationsutil "github.com/smartxworks/cluster-api-provider-elf/pkg/util/annotations" ) type VMService interface { @@ -241,11 +242,21 @@ func (svr *TowerVMService) Clone( hostID = *host.ID } + var owner *models.VMOwnerParams + if createdBy := annotationsutil.GetCreatedBy(elfCluster); createdBy != "" { + creator := parseOwnerFromCreatedByAnnotation(createdBy) + owner = &models.VMOwnerParams{ + SearchFor: TowerString(VMOwnerSearchForUsername), + Value: TowerString(creator), + } + } + vmCreateVMFromTemplateParams := &models.VMCreateVMFromContentLibraryTemplateParams{ ClusterID: cluster.ID, HostID: TowerString(hostID), Name: TowerString(elfMachine.Name), Description: TowerString(fmt.Sprintf(config.VMDescription, elfCluster.Spec.Tower.Server)), + Owner: owner, Vcpu: vCPU, CPUCores: cpuCores, CPUSockets: cpuSockets, diff --git a/pkg/util/annotations/helpers.go b/pkg/util/annotations/helpers.go index ceb61872..2534140f 100644 --- a/pkg/util/annotations/helpers.go +++ b/pkg/util/annotations/helpers.go @@ -43,6 +43,15 @@ func GetPlacementGroupName(o metav1.Object) string { return annotations[infrav1.PlacementGroupNameAnnotation] } +func GetCreatedBy(o metav1.Object) string { + annotations := o.GetAnnotations() + if annotations == nil { + return "" + } + + return annotations[infrav1.CreatedByAnnotation] +} + // AddAnnotations sets the desired annotations on the object and returns true if the annotations have changed. func AddAnnotations(o metav1.Object, desired map[string]string) bool { return annotations.AddAnnotations(o, desired) diff --git a/pkg/util/machine/machine.go b/pkg/util/machine/machine.go index cecdb964..e66ac4e0 100644 --- a/pkg/util/machine/machine.go +++ b/pkg/util/machine/machine.go @@ -29,6 +29,7 @@ import ( infrav1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1" labelsutil "github.com/smartxworks/cluster-api-provider-elf/pkg/util/labels" + typesutil "github.com/smartxworks/cluster-api-provider-elf/pkg/util/types" ) const ( @@ -39,10 +40,6 @@ const ( // ProviderIDPattern is a regex pattern and is used by ConvertProviderIDToUUID // to convert a providerID into a UUID string. ProviderIDPattern = `(?i)^` + ProviderIDPrefix + `([a-f\d]{8}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{12})$` - - // UUIDPattern is a regex pattern and is used by ConvertUUIDToProviderID - // to convert a UUID into a providerID string. - UUIDPattern = `(?i)^[a-f\d]{8}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{12}$` ) // ErrNoMachineIPAddr indicates that no valid IP addresses were found in a machine context. @@ -132,23 +129,13 @@ func ConvertProviderIDToUUID(providerID *string) string { } func ConvertUUIDToProviderID(uuid string) string { - if !IsUUID(uuid) { + if !typesutil.IsUUID(uuid) { return "" } return ProviderIDPrefix + uuid } -func IsUUID(uuid string) bool { - if uuid == "" { - return false - } - - pattern := regexp.MustCompile(UUIDPattern) - - return pattern.MatchString(uuid) -} - func GetNetworkStatus(ipsStr string) []infrav1.NetworkStatus { networks := []infrav1.NetworkStatus{} diff --git a/pkg/util/machine/machine_test.go b/pkg/util/machine/machine_test.go index 5e393312..4f2c18ad 100644 --- a/pkg/util/machine/machine_test.go +++ b/pkg/util/machine/machine_test.go @@ -183,49 +183,6 @@ func TestConvertUUIDtoProviderID(t *testing.T) { } } -func TestIsUUID(t *testing.T) { - g := gomega.NewGomegaWithT(t) - - testCases := []struct { - name string - uuid string - isUUID bool - }{ - { - name: "empty uuid", - uuid: "", - isUUID: false, - }, - { - name: "invalid uuid", - uuid: "1234", - isUUID: false, - }, - { - name: "valid uuid", - uuid: "12345678-1234-1234-1234-123456789abc", - isUUID: true, - }, - { - name: "mixed case", - uuid: "12345678-1234-1234-1234-123456789AbC", - isUUID: true, - }, - { - name: "invalid hex chars", - uuid: "12345678-1234-1234-1234-123456789abg", - isUUID: false, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - isUUID := IsUUID(tc.uuid) - g.Expect(isUUID).To(gomega.Equal(tc.isUUID)) - }) - } -} - func TestGetNetworkStatus(t *testing.T) { g := gomega.NewGomegaWithT(t) diff --git a/pkg/util/tower.go b/pkg/util/tower.go index 33f86f5a..0fe36eac 100644 --- a/pkg/util/tower.go +++ b/pkg/util/tower.go @@ -20,7 +20,7 @@ import ( "github.com/smartxworks/cloudtower-go-sdk/v2/models" "github.com/smartxworks/cluster-api-provider-elf/pkg/service" - machineutil "github.com/smartxworks/cluster-api-provider-elf/pkg/util/machine" + typesutil "github.com/smartxworks/cluster-api-provider-elf/pkg/util/types" ) // GetVMRef returns the ID or localID of the VM. @@ -34,7 +34,7 @@ func GetVMRef(vm *models.VM) string { } vmLocalID := service.GetTowerString(vm.LocalID) - if machineutil.IsUUID(vmLocalID) { + if typesutil.IsUUID(vmLocalID) { return vmLocalID } diff --git a/pkg/util/types/uuid.go b/pkg/util/types/uuid.go new file mode 100644 index 00000000..93d62800 --- /dev/null +++ b/pkg/util/types/uuid.go @@ -0,0 +1,35 @@ +/* +Copyright 2023. + +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 types + +import "regexp" + +const ( + // UUIDPattern is a regex pattern and is used by ConvertUUIDToProviderID + // to convert a UUID into a providerID string. + UUIDPattern = `(?i)^[a-f\d]{8}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{12}$` +) + +func IsUUID(uuid string) bool { + if uuid == "" { + return false + } + + pattern := regexp.MustCompile(UUIDPattern) + + return pattern.MatchString(uuid) +} diff --git a/pkg/util/types/uuid_test.go b/pkg/util/types/uuid_test.go new file mode 100644 index 00000000..b2bcf2dc --- /dev/null +++ b/pkg/util/types/uuid_test.go @@ -0,0 +1,66 @@ +/* +Copyright 2023. + +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 types + +import ( + "testing" + + "github.com/onsi/gomega" +) + +func TestIsUUID(t *testing.T) { + g := gomega.NewGomegaWithT(t) + + testCases := []struct { + name string + uuid string + isUUID bool + }{ + { + name: "empty uuid", + uuid: "", + isUUID: false, + }, + { + name: "invalid uuid", + uuid: "1234", + isUUID: false, + }, + { + name: "valid uuid", + uuid: "12345678-1234-1234-1234-123456789abc", + isUUID: true, + }, + { + name: "mixed case", + uuid: "12345678-1234-1234-1234-123456789AbC", + isUUID: true, + }, + { + name: "invalid hex chars", + uuid: "12345678-1234-1234-1234-123456789abg", + isUUID: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + isUUID := IsUUID(tc.uuid) + g.Expect(isUUID).To(gomega.Equal(tc.isUUID)) + }) + } +}