diff --git a/Makefile b/Makefile index 3b986b4ec..bbe1f1786 100644 --- a/Makefile +++ b/Makefile @@ -360,7 +360,7 @@ S5CMD ?= $(CACHE_BIN)/s5cmd ## Tool Versions KUSTOMIZE_VERSION ?= v5.4.3 -CTLPTL_VERSION ?= v0.8.29 +CTLPTL_VERSION ?= v0.8.42 CLUSTERCTL_VERSION ?= v1.7.2 CRD_REF_DOCS_VERSION ?= v0.1.0 KUBECTL_VERSION ?= v1.28.0 diff --git a/api/v1alpha2/linodemachine_types.go b/api/v1alpha2/linodemachine_types.go index 20821eeae..b6e092820 100644 --- a/api/v1alpha2/linodemachine_types.go +++ b/api/v1alpha2/linodemachine_types.go @@ -33,6 +33,14 @@ const ( // LinodeMachineSpec defines the desired state of LinodeMachine type LinodeMachineSpec struct { + + // LabelPrefix is the prefix to use for the Linode instance label. + // If not specified, defaults are applied. + // If specified but a Machine doesn't have a owner reference, the prefix is added to the Machine name. + // If specified and a Machine has a owner reference, owner reference name is replaced with the prefix. + // +optional + LabelPrefix string `json:"labelPrefix,omitempty"` + // ProviderID is the unique identifier as specified by the cloud provider. // +optional ProviderID *string `json:"providerID,omitempty"` diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachines.yaml index 0b0bb5d64..ec58604a1 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachines.yaml @@ -264,6 +264,13 @@ spec: x-kubernetes-validations: - message: Value is immutable rule: self == oldSelf + labelPrefix: + description: |- + LabelPrefix is the prefix to use for the Linode instance label. + If not specified, defaults are applied. + If specified but a Machine doesn't have a owner reference, the prefix is added to the Machine name. + If specified and a Machine has a owner reference, owner reference name is replaced with the prefix. + type: string osDisk: description: |- OSDisk is configuration for the root disk that includes the OS, diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachinetemplates.yaml index 4b0577d11..cfccb38a8 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachinetemplates.yaml @@ -256,6 +256,13 @@ spec: x-kubernetes-validations: - message: Value is immutable rule: self == oldSelf + labelPrefix: + description: |- + LabelPrefix is the prefix to use for the Linode instance label. + If not specified, defaults are applied. + If specified but a Machine doesn't have a owner reference, the prefix is added to the Machine name. + If specified and a Machine has a owner reference, owner reference name is replaced with the prefix. + type: string osDisk: description: |- OSDisk is configuration for the root disk that includes the OS, diff --git a/docs/src/reference/out.md b/docs/src/reference/out.md index f19a309d8..c5f591b44 100644 --- a/docs/src/reference/out.md +++ b/docs/src/reference/out.md @@ -597,6 +597,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | +| `labelPrefix` _string_ | LabelPrefix is the prefix to use for the Linode instance label.
If not specified, defaults are applied.
If specified but a Machine doesn't have a owner reference, the prefix is added to the Machine name.
If specified and a Machine has a owner reference, owner reference name is replaced with the prefix. | | | | `providerID` _string_ | ProviderID is the unique identifier as specified by the cloud provider. | | | | `instanceID` _integer_ | InstanceID is the Linode instance ID for this machine. | | | | `region` _string_ | | | Required: \{\}
| diff --git a/e2e/linodemachinetemplate-controller/lmt-tags/chainsaw-test.yaml b/e2e/linodemachinetemplate-controller/lmt-tags/chainsaw-test.yaml index 58a6fe7af..acc7129ad 100644 --- a/e2e/linodemachinetemplate-controller/lmt-tags/chainsaw-test.yaml +++ b/e2e/linodemachinetemplate-controller/lmt-tags/chainsaw-test.yaml @@ -5,7 +5,7 @@ metadata: name: lmt-e2e labels: all: - linodemachine: + linodemachinetemplate: spec: bindings: # A short identifier for the E2E test run @@ -76,4 +76,4 @@ spec: catch: - describe: apiVersion: infrastructure.cluster.x-k8s.io/v1alpha2 - kind: LinodeMachine \ No newline at end of file + kind: LinodeMachine diff --git a/internal/controller/linodemachine_controller.go b/internal/controller/linodemachine_controller.go index 740afe252..b8c2bb745 100644 --- a/internal/controller/linodemachine_controller.go +++ b/internal/controller/linodemachine_controller.go @@ -21,7 +21,6 @@ import ( "errors" "fmt" "net/http" - "slices" "strings" "time" @@ -747,7 +746,7 @@ func (r *LinodeMachineReconciler) reconcileUpdate(ctx context.Context, logger lo // update the tags if needed machineTags := getTags(machineScope, linodeInstance.Tags) - if !slices.Equal(machineTags, linodeInstance.Tags) { + if !areSlicesEqual(machineTags, linodeInstance.Tags) { _, err = machineScope.LinodeClient.UpdateInstance(ctx, instanceID, linodego.InstanceUpdateOptions{Tags: &machineTags}) if err != nil { logger.Error(err, "Failed to update tags for Linode instance") diff --git a/internal/controller/linodemachine_controller_helpers.go b/internal/controller/linodemachine_controller_helpers.go index dabc34ace..c62b1613e 100644 --- a/internal/controller/linodemachine_controller_helpers.go +++ b/internal/controller/linodemachine_controller_helpers.go @@ -105,7 +105,7 @@ func fillCreateConfig(createConfig *linodego.InstanceCreateOptions, machineScope func newCreateConfig(ctx context.Context, machineScope *scope.MachineScope, gzipCompressionEnabled bool, logger logr.Logger) (*linodego.InstanceCreateOptions, error) { var err error - createConfig := linodeMachineSpecToInstanceCreateConfig(machineScope.LinodeMachine.Spec, getTags(machineScope, []string{})) + createConfig := linodeMachineSpecToInstanceCreateConfig(machineScope, getTags(machineScope, []string{})) if createConfig == nil { err = errors.New("failed to convert machine spec to create instance config") logger.Error(err, "Panic! Struct of LinodeMachineSpec is different than InstanceCreateOptions") @@ -552,7 +552,8 @@ func getVPCInterfaceConfigFromDirectID(ctx context.Context, machineScope *scope. }, nil } -func linodeMachineSpecToInstanceCreateConfig(machineSpec infrav1alpha2.LinodeMachineSpec, machineTags []string) *linodego.InstanceCreateOptions { +func linodeMachineSpecToInstanceCreateConfig(machineScope *scope.MachineScope, machineTags []string) *linodego.InstanceCreateOptions { + machineSpec := machineScope.LinodeMachine.Spec interfaces := make([]linodego.InstanceConfigInterfaceCreateOptions, len(machineSpec.Interfaces)) for idx, iface := range machineSpec.Interfaces { interfaces[idx] = linodego.InstanceConfigInterfaceCreateOptions{ @@ -569,6 +570,7 @@ func linodeMachineSpecToInstanceCreateConfig(machineSpec infrav1alpha2.LinodeMac privateIP = *machineSpec.PrivateIP } return &linodego.InstanceCreateOptions{ + Label: getDesiredLinodeInstanceLabel(machineScope), Region: machineSpec.Region, Type: machineSpec.Type, AuthorizedKeys: machineSpec.AuthorizedKeys, @@ -1021,3 +1023,46 @@ func getTags(machineScope *scope.MachineScope, instanceTags []string) []string { machineScope.LinodeMachine.Status.Tags = slices.Clone(machineScope.LinodeMachine.Spec.Tags) return outTags } + +func areSlicesEqual(a, b []string) bool { + if len(a) != len(b) { + return false + } + aSet := constructSet(a) + bSet := constructSet(b) + if len(aSet) != len(bSet) { + return false + } + for key := range aSet { + if _, ok := bSet[key]; !ok { + return false + } + } + return true +} + +func getDesiredLinodeInstanceLabel(machineScope *scope.MachineScope) string { + // If no label prefix is specified, use the machine name as the label + if machineScope.LinodeMachine.Spec.LabelPrefix == "" { + return machineScope.LinodeMachine.Name + } + + // if machine is created by a deployment / control-plane, it's name will be prefixed with the label of linode. + machineOwners := machineScope.Machine.GetOwnerReferences() + + // get the longest prefix match from machine owner names. + longestPrefix := "" + for _, owner := range machineOwners { + if strings.HasPrefix(machineScope.LinodeMachine.Name, owner.Name) && len(owner.Name) > len(longestPrefix) { + longestPrefix = owner.Name + } + } + + // If no owner name matches the prefix, use the machine name as the label + if longestPrefix == "" { + // If no owner name matches the prefix, use the label prefix + return machineScope.LinodeMachine.Spec.LabelPrefix + "-" + machineScope.LinodeMachine.Name + } else { + return strings.Replace(machineScope.LinodeMachine.Name, longestPrefix, machineScope.LinodeMachine.Spec.LabelPrefix, 1) + } +} diff --git a/internal/controller/linodemachine_controller_helpers_test.go b/internal/controller/linodemachine_controller_helpers_test.go index 884b11f36..921e9bafe 100644 --- a/internal/controller/linodemachine_controller_helpers_test.go +++ b/internal/controller/linodemachine_controller_helpers_test.go @@ -44,6 +44,7 @@ func TestLinodeMachineSpecToCreateInstanceConfig(t *testing.T) { RootPass: "rootPass", AuthorizedKeys: []string{"key"}, AuthorizedUsers: []string{"user"}, + LabelPrefix: "test-label-prefix", BackupID: 1, Image: "image", Interfaces: []infrav1alpha2.InstanceConfigInterfaceCreateOptions{ @@ -64,7 +65,12 @@ func TestLinodeMachineSpecToCreateInstanceConfig(t *testing.T) { PrivateIP: util.Pointer(true), } - createConfig := linodeMachineSpecToInstanceCreateConfig(machineSpec, []string{"tag"}) + createConfig := linodeMachineSpecToInstanceCreateConfig(&scope.MachineScope{ + LinodeMachine: &infrav1alpha2.LinodeMachine{ + Spec: machineSpec, + }, + Machine: &v1beta1.Machine{}, + }, []string{"tag"}) assert.NotNil(t, createConfig, "Failed to convert LinodeMachineSpec to InstanceCreateOptions") } @@ -1301,3 +1307,83 @@ func TestGetTags(t *testing.T) { }) } } + +func TestGetDesiredLinodeInstanceLabel(t *testing.T) { + t.Parallel() + + // Setup test cases + testCases := []struct { + name string + machineScope *scope.MachineScope + expectedLabel string + }{ + { + name: "Success - Default label", + machineScope: &scope.MachineScope{ + LinodeMachine: &infrav1alpha2.LinodeMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-machine", + Namespace: "default", + }, + }, + Machine: &v1beta1.Machine{}, + }, + expectedLabel: "test-machine", + }, + { + name: "Success - Custom label prefix", + machineScope: &scope.MachineScope{ + LinodeMachine: &infrav1alpha2.LinodeMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-machine", + Namespace: "default", + }, + Spec: infrav1alpha2.LinodeMachineSpec{ + LabelPrefix: "custom-prefix", + }, + }, + Machine: &v1beta1.Machine{}, + }, + expectedLabel: "custom-prefix-test-machine", + }, + { + name: "Success - Custom label prefix with owner reference", + machineScope: &scope.MachineScope{ + LinodeMachine: &infrav1alpha2.LinodeMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-machine", + Namespace: "default", + }, + Spec: infrav1alpha2.LinodeMachineSpec{ + LabelPrefix: "custom-prefix", + }, + }, + Machine: &v1beta1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + OwnerReferences: []metav1.OwnerReference{ + { + Kind: "MachineDeployment", + Name: "te", + }, + { + Kind: "MachineSet", + Name: "test", + }, + }, + }, + }, + }, + expectedLabel: "custom-prefix-machine", + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + label := getDesiredLinodeInstanceLabel(tc.machineScope) + require.Equal(t, tc.expectedLabel, label) + }) + } +}