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)
+ })
+ }
+}