From 1b7fca83dd1843814c0da731d281ecf4eebdb6f1 Mon Sep 17 00:00:00 2001 From: Khaja Omer Date: Mon, 21 Jul 2025 15:58:30 -0500 Subject: [PATCH 1/5] Add NetworkHelper field to LinodeMachineSpec and update instance configuration logic - Introduced a new optional boolean field `NetworkHelper` in `LinodeMachineSpec` to indicate if the network helper should be used, defaulting to true. - Updated the instance configuration logic in `linodemachine_controller.go` to conditionally enable the network helper based on the new field. --- api/v1alpha2/linodemachine_types.go | 6 ++++ .../controller/linodemachine_controller.go | 33 ++++++++++++++----- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/api/v1alpha2/linodemachine_types.go b/api/v1alpha2/linodemachine_types.go index 20821eeae..0e7aaabea 100644 --- a/api/v1alpha2/linodemachine_types.go +++ b/api/v1alpha2/linodemachine_types.go @@ -111,6 +111,12 @@ type LinodeMachineSpec struct { // VPCID is the ID of an existing VPC in Linode. This allows using a VPC that is not managed by CAPL. // +optional VPCID *int `json:"vpcID,omitempty"` + + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" + // +optional + // NetworkHelper is a boolean that indicates if the network helper should be used. + // Defaults to true. + NetworkHelper *bool `json:"networkHelper,omitempty"` } // InstanceDisk defines a list of disks to use for an instance diff --git a/internal/controller/linodemachine_controller.go b/internal/controller/linodemachine_controller.go index 740afe252..6405f3099 100644 --- a/internal/controller/linodemachine_controller.go +++ b/internal/controller/linodemachine_controller.go @@ -618,18 +618,35 @@ func (r *LinodeMachineReconciler) reconcilePreflightConfigure(ctx context.Contex }) return ctrl.Result{RequeueAfter: reconciler.DefaultMachineControllerWaitForRunningDelay}, nil } + + configData := linodego.InstanceConfigUpdateOptions{ + Helpers: &linodego.InstanceConfigHelpers{ + Network: true, + }, + } + if machineScope.LinodeMachine.Spec.Configuration != nil && machineScope.LinodeMachine.Spec.Configuration.Kernel != "" { - instanceConfig, err := getDefaultInstanceConfig(ctx, machineScope, instanceID) - if err != nil { - logger.Error(err, "Failed to get default instance configuration") - return retryIfTransient(err, logger) - } + configData.Kernel = machineScope.LinodeMachine.Spec.Configuration.Kernel + } - if _, err := machineScope.LinodeClient.UpdateInstanceConfig(ctx, instanceID, instanceConfig.ID, linodego.InstanceConfigUpdateOptions{Kernel: machineScope.LinodeMachine.Spec.Configuration.Kernel}); err != nil { - logger.Error(err, "Failed to update default instance configuration") - return retryIfTransient(err, logger) + // For cases where the network helper is not enabled on account level, we can enable it per instance level + // Default is true, so we only need to update if it's explicitly set to false + if machineScope.LinodeMachine.Spec.NetworkHelper != nil { + configData.Helpers = &linodego.InstanceConfigHelpers{ + Network: *machineScope.LinodeMachine.Spec.NetworkHelper, } } + + instanceConfig, err := getDefaultInstanceConfig(ctx, machineScope, instanceID) + if err != nil { + logger.Error(err, "Failed to get default instance configuration") + return retryIfTransient(err, logger) + } + if _, err := machineScope.LinodeClient.UpdateInstanceConfig(ctx, instanceID, instanceConfig.ID, configData); err != nil { + logger.Error(err, "Failed to update default instance configuration") + return retryIfTransient(err, logger) + } + conditions.Set(machineScope.LinodeMachine, metav1.Condition{ Type: ConditionPreflightConfigured, Status: metav1.ConditionTrue, From 7d54becfaaa4ebc0e3e763f72b1715358deb5b49 Mon Sep 17 00:00:00 2001 From: Khaja Omer Date: Mon, 21 Jul 2025 16:05:03 -0500 Subject: [PATCH 2/5] Enhance NetworkHelper documentation in LinodeMachineSpec - Updated the comment for the `NetworkHelper` field in `LinodeMachineSpec` to provide clearer guidance on its purpose and usage. - Added a link to the relevant documentation for automatic networking configuration. --- api/v1alpha2/linodemachine_types.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/v1alpha2/linodemachine_types.go b/api/v1alpha2/linodemachine_types.go index 0e7aaabea..e0cf6ad27 100644 --- a/api/v1alpha2/linodemachine_types.go +++ b/api/v1alpha2/linodemachine_types.go @@ -114,7 +114,9 @@ type LinodeMachineSpec struct { // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" // +optional - // NetworkHelper is a boolean that indicates if the network helper should be used. + // NetworkHelper is an option usually enabled on account level. It helps configure networking automatically for instances. + // You can use this to enable/disable the network helper for a specific instance. + // For more information, see https://techdocs.akamai.com/cloud-computing/docs/automatically-configure-networking // Defaults to true. NetworkHelper *bool `json:"networkHelper,omitempty"` } From c1fc2290e3f7f18b7136208a5648ff2c934de50b Mon Sep 17 00:00:00 2001 From: Khaja Omer Date: Mon, 21 Jul 2025 16:12:25 -0500 Subject: [PATCH 3/5] Make generate --- api/v1alpha2/zz_generated.deepcopy.go | 5 +++++ ...infrastructure.cluster.x-k8s.io_linodemachines.yaml | 10 ++++++++++ ...ucture.cluster.x-k8s.io_linodemachinetemplates.yaml | 10 ++++++++++ docs/src/reference/out.md | 1 + 4 files changed, 26 insertions(+) diff --git a/api/v1alpha2/zz_generated.deepcopy.go b/api/v1alpha2/zz_generated.deepcopy.go index a5abe3618..50b06c517 100644 --- a/api/v1alpha2/zz_generated.deepcopy.go +++ b/api/v1alpha2/zz_generated.deepcopy.go @@ -878,6 +878,11 @@ func (in *LinodeMachineSpec) DeepCopyInto(out *LinodeMachineSpec) { *out = new(int) **out = **in } + if in.NetworkHelper != nil { + in, out := &in.NetworkHelper, &out.NetworkHelper + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LinodeMachineSpec. 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..d668d998b 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,16 @@ spec: x-kubernetes-validations: - message: Value is immutable rule: self == oldSelf + networkHelper: + description: |- + NetworkHelper is an option usually enabled on account level. It helps configure networking automatically for instances. + You can use this to enable/disable the network helper for a specific instance. + For more information, see https://techdocs.akamai.com/cloud-computing/docs/automatically-configure-networking + Defaults to true. + type: boolean + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf 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..0ccaf93ba 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,16 @@ spec: x-kubernetes-validations: - message: Value is immutable rule: self == oldSelf + networkHelper: + description: |- + NetworkHelper is an option usually enabled on account level. It helps configure networking automatically for instances. + You can use this to enable/disable the network helper for a specific instance. + For more information, see https://techdocs.akamai.com/cloud-computing/docs/automatically-configure-networking + Defaults to true. + type: boolean + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf 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 c1043e715..500ef39fb 100644 --- a/docs/src/reference/out.md +++ b/docs/src/reference/out.md @@ -621,6 +621,7 @@ _Appears in:_ | `firewallRef` _[ObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#objectreference-v1-core)_ | FirewallRef is a reference to a firewall object. This makes the linode use the specified firewall. | | | | `vpcRef` _[ObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#objectreference-v1-core)_ | VPCRef is a reference to a LinodeVPC resource. If specified, this takes precedence over
the cluster-level VPC configuration for multi-region support. | | | | `vpcID` _integer_ | VPCID is the ID of an existing VPC in Linode. This allows using a VPC that is not managed by CAPL. | | | +| `networkHelper` _boolean_ | NetworkHelper is an option usually enabled on account level. It helps configure networking automatically for instances.
You can use this to enable/disable the network helper for a specific instance.
For more information, see https://techdocs.akamai.com/cloud-computing/docs/automatically-configure-networking
Defaults to true. | | | #### LinodeMachineStatus From 18a3145fdc62a736d55c6dfe246d913c909c99d8 Mon Sep 17 00:00:00 2001 From: Khaja Omer Date: Mon, 21 Jul 2025 16:19:07 -0500 Subject: [PATCH 4/5] disable network helper for flatcar based cluster --- templates/flavors/kubeadm/flatcar/patch-flatcar.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/templates/flavors/kubeadm/flatcar/patch-flatcar.yaml b/templates/flavors/kubeadm/flatcar/patch-flatcar.yaml index f34c5131d..680612758 100644 --- a/templates/flavors/kubeadm/flatcar/patch-flatcar.yaml +++ b/templates/flavors/kubeadm/flatcar/patch-flatcar.yaml @@ -117,6 +117,7 @@ spec: configuration: kernel: linode/direct-disk diskEncryption: disabled + networkHelper: false --- apiVersion: infrastructure.cluster.x-k8s.io/v1alpha2 kind: LinodeMachineTemplate @@ -129,3 +130,4 @@ spec: configuration: kernel: linode/direct-disk diskEncryption: disabled + networkHelper: false From a10de84376104f7a462f31f6ba1e5caed673fb0c Mon Sep 17 00:00:00 2001 From: Khaja Omer Date: Tue, 22 Jul 2025 13:33:11 -0500 Subject: [PATCH 5/5] update unit tests --- .../linodemachine_controller_test.go | 109 ++++++++++++++++-- 1 file changed, 100 insertions(+), 9 deletions(-) diff --git a/internal/controller/linodemachine_controller_test.go b/internal/controller/linodemachine_controller_test.go index ccb94805c..f01fbcfe0 100644 --- a/internal/controller/linodemachine_controller_test.go +++ b/internal/controller/linodemachine_controller_test.go @@ -229,6 +229,17 @@ var _ = Describe("create", Label("machine", "create"), func() { createInst := mockLinodeClient.EXPECT(). OnAfterResponse(gomock.Any()). Return() + listInstConfs := mockLinodeClient.EXPECT(). + ListInstanceConfigs(ctx, 123, gomock.Any()). + After(createInst). + Return([]linodego.InstanceConfig{{ + ID: 1, + }}, nil) + mockLinodeClient.EXPECT().UpdateInstanceConfig(ctx, 123, 1, linodego.InstanceConfigUpdateOptions{ + Helpers: &linodego.InstanceConfigHelpers{Network: true}, + }). + After(listInstConfs). + Return(nil, nil) bootInst := mockLinodeClient.EXPECT(). BootInstance(ctx, 123, 0). After(createInst). @@ -374,6 +385,17 @@ var _ = Describe("create", Label("machine", "create"), func() { createInst := mockLinodeClient.EXPECT(). OnAfterResponse(gomock.Any()). Return() + listInstConfs := mockLinodeClient.EXPECT(). + ListInstanceConfigs(ctx, 123, gomock.Any()). + After(createInst). + Return([]linodego.InstanceConfig{{ + ID: 1, + }}, nil) + mockLinodeClient.EXPECT().UpdateInstanceConfig(ctx, 123, 1, linodego.InstanceConfigUpdateOptions{ + Helpers: &linodego.InstanceConfigHelpers{Network: true}, + }). + After(listInstConfs). + Return(nil, nil) bootInst := mockLinodeClient.EXPECT(). BootInstance(ctx, 123, 0). After(createInst). @@ -441,6 +463,17 @@ var _ = Describe("create", Label("machine", "create"), func() { mockLinodeClient.EXPECT(). OnAfterResponse(gomock.Any()). Return() + listInstConfs := mockLinodeClient.EXPECT(). + ListInstanceConfigs(ctx, 123, gomock.Any()). + After(createInst). + Return([]linodego.InstanceConfig{{ + ID: 1, + }}, nil) + mockLinodeClient.EXPECT().UpdateInstanceConfig(ctx, 123, 1, linodego.InstanceConfigUpdateOptions{ + Helpers: &linodego.InstanceConfigHelpers{Network: true}, + }). + After(listInstConfs). + Return(nil, nil) bootInst := mockLinodeClient.EXPECT(). BootInstance(ctx, 123, 0). After(createInst). @@ -527,6 +560,17 @@ var _ = Describe("create", Label("machine", "create"), func() { mockLinodeClient.EXPECT(). OnAfterResponse(gomock.Any()). Return() + listInstConfs := mockLinodeClient.EXPECT(). + ListInstanceConfigs(ctx, 123, gomock.Any()). + After(createInst). + Return([]linodego.InstanceConfig{{ + ID: 1, + }}, nil) + mockLinodeClient.EXPECT().UpdateInstanceConfig(ctx, 123, 1, linodego.InstanceConfigUpdateOptions{ + Helpers: &linodego.InstanceConfigHelpers{Network: true}, + }). + After(listInstConfs). + Return(nil, nil) bootInst := mockLinodeClient.EXPECT(). BootInstance(ctx, 123, 0). After(listInst). @@ -727,7 +771,7 @@ var _ = Describe("create", Label("machine", "create"), func() { GetImage(ctx, gomock.Any()). After(getRegion). Return(&linodego.Image{Capabilities: []string{"cloud-init"}}, nil) - createInst := mockLinodeClient.EXPECT(). + mockLinodeClient.EXPECT(). CreateInstance(ctx, gomock.Any()). After(getImage). Return(&linodego.Instance{ @@ -739,17 +783,19 @@ var _ = Describe("create", Label("machine", "create"), func() { mockLinodeClient.EXPECT(). OnAfterResponse(gomock.Any()). Return() - listInstConfs := mockLinodeClient.EXPECT(). + mockLinodeClient.EXPECT(). ListInstanceConfigs(ctx, 123, gomock.Any()). - After(createInst). Return([]linodego.InstanceConfig{{ + ID: 1, Devices: &linodego.InstanceConfigDeviceMap{ SDA: &linodego.InstanceConfigDevice{DiskID: 100}, }, - }}, nil).MaxTimes(2) + }}, nil).MaxTimes(3) + mockLinodeClient.EXPECT().UpdateInstanceConfig(ctx, 123, 1, linodego.InstanceConfigUpdateOptions{ + Helpers: &linodego.InstanceConfigHelpers{Network: true}, + }).Return(nil, nil) getInstDisk := mockLinodeClient.EXPECT(). GetInstanceDisk(ctx, 123, 100). - After(listInstConfs). Return(&linodego.InstanceDisk{ID: 100, Size: 15000}, nil) resizeInstDisk := mockLinodeClient.EXPECT(). ResizeInstanceDisk(ctx, 123, 100, 4262). @@ -767,12 +813,13 @@ var _ = Describe("create", Label("machine", "create"), func() { ListInstanceConfigs(ctx, 123, gomock.Any()). After(createEtcdDisk). Return([]linodego.InstanceConfig{{ + ID: 1, Devices: &linodego.InstanceConfigDeviceMap{ SDA: &linodego.InstanceConfigDevice{DiskID: 100}, }, - }}, nil).MaxTimes(2) + }}, nil).MaxTimes(3) createInstanceProfile := mockLinodeClient.EXPECT(). - UpdateInstanceConfig(ctx, 123, 0, linodego.InstanceConfigUpdateOptions{ + UpdateInstanceConfig(ctx, 123, 1, linodego.InstanceConfigUpdateOptions{ Devices: &linodego.InstanceConfigDeviceMap{ SDA: &linodego.InstanceConfigDevice{DiskID: 100}, SDB: &linodego.InstanceConfigDevice{DiskID: 101}, @@ -890,10 +937,16 @@ var _ = Describe("create", Label("machine", "create"), func() { ListInstanceConfigs(ctx, 123, gomock.Any()). After(createInst). Return([]linodego.InstanceConfig{{ + ID: 1, Devices: &linodego.InstanceConfigDeviceMap{ SDA: &linodego.InstanceConfigDevice{DiskID: 100}, }, }}, nil) + mockLinodeClient.EXPECT().UpdateInstanceConfig(ctx, 123, 1, linodego.InstanceConfigUpdateOptions{ + Helpers: &linodego.InstanceConfigHelpers{Network: true}, + }). + After(listInstConfs). + Return(nil, nil) getInstDisk := mockLinodeClient.EXPECT(). GetInstanceDisk(ctx, 123, 100). After(listInstConfs). @@ -933,6 +986,17 @@ var _ = Describe("create", Label("machine", "create"), func() { Expect(rutil.ConditionTrue(&linodeMachine, ConditionPreflightConfigured)).To(BeFalse()) Expect(rutil.ConditionTrue(&linodeMachine, ConditionPreflightAdditionalDisksCreated)).To(BeFalse()) + mockLinodeClient.EXPECT(). + ListInstanceConfigs(ctx, 123, gomock.Any()). + Return([]linodego.InstanceConfig{{ + ID: 1, + Devices: &linodego.InstanceConfigDeviceMap{ + SDA: &linodego.InstanceConfigDevice{DiskID: 100}, + }, + }}, nil).AnyTimes() + mockLinodeClient.EXPECT().UpdateInstanceConfig(ctx, 123, 1, linodego.InstanceConfigUpdateOptions{ + Helpers: &linodego.InstanceConfigHelpers{Network: true}, + }).Return(nil, nil).AnyTimes() getInst := mockLinodeClient.EXPECT(). GetInstance(ctx, 123). After(createFailedEtcdDisk). @@ -954,12 +1018,13 @@ var _ = Describe("create", Label("machine", "create"), func() { ListInstanceConfigs(ctx, 123, gomock.Any()). After(createEtcdDisk). Return([]linodego.InstanceConfig{{ + ID: 1, Devices: &linodego.InstanceConfigDeviceMap{ SDA: &linodego.InstanceConfigDevice{DiskID: 100}, }, - }}, nil) + }}, nil).AnyTimes() createInstanceProfile := mockLinodeClient.EXPECT(). - UpdateInstanceConfig(ctx, 123, 0, linodego.InstanceConfigUpdateOptions{ + UpdateInstanceConfig(ctx, 123, 1, linodego.InstanceConfigUpdateOptions{ Devices: &linodego.InstanceConfigDeviceMap{ SDA: &linodego.InstanceConfigDevice{DiskID: 100}, SDB: &linodego.InstanceConfigDevice{DiskID: 101}, @@ -1135,6 +1200,17 @@ var _ = Describe("createDNS", Label("machine", "createDNS"), func() { mockLinodeClient.EXPECT(). OnAfterResponse(gomock.Any()). Return() + listInstConfs := mockLinodeClient.EXPECT(). + ListInstanceConfigs(ctx, 123, gomock.Any()). + After(createInst). + Return([]linodego.InstanceConfig{{ + ID: 1, + }}, nil) + mockLinodeClient.EXPECT().UpdateInstanceConfig(ctx, 123, 1, linodego.InstanceConfigUpdateOptions{ + Helpers: &linodego.InstanceConfigHelpers{Network: true}, + }). + After(listInstConfs). + Return(nil, nil) bootInst := mockLinodeClient.EXPECT(). BootInstance(ctx, 123, 0). After(createInst). @@ -2435,6 +2511,17 @@ var _ = Describe("machine in vlan", Label("machine", "vlan"), Ordered, func() { mockLinodeClient.EXPECT(). OnAfterResponse(gomock.Any()). Return() + listInstConfs := mockLinodeClient.EXPECT(). + ListInstanceConfigs(ctx, 123, gomock.Any()). + After(createInst). + Return([]linodego.InstanceConfig{{ + ID: 1, + }}, nil) + mockLinodeClient.EXPECT().UpdateInstanceConfig(ctx, 123, 1, linodego.InstanceConfigUpdateOptions{ + Helpers: &linodego.InstanceConfigHelpers{Network: true}, + }). + After(listInstConfs). + Return(nil, nil) bootInst := mockLinodeClient.EXPECT(). BootInstance(ctx, 123, 0). After(createInst). @@ -2601,6 +2688,10 @@ var _ = Describe("create machine with direct VPCID", Label("machine", "VPCID"), }, }, nil). AnyTimes() + mockLinodeClient.EXPECT(). + UpdateInstanceConfig(gomock.Any(), 12345, 1, gomock.Any()). + Return(nil, nil). + AnyTimes() mockLinodeClient.EXPECT(). GetInstanceIPAddresses(gomock.Any(), gomock.Any()). Return(&linodego.InstanceIPAddressResponse{