diff --git a/api/v1alpha2/linodemachine_types.go b/api/v1alpha2/linodemachine_types.go index e0cf6ad27..05869a332 100644 --- a/api/v1alpha2/linodemachine_types.go +++ b/api/v1alpha2/linodemachine_types.go @@ -112,6 +112,12 @@ type LinodeMachineSpec struct { // +optional VPCID *int `json:"vpcID,omitempty"` + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" + // IPv6Options defines the IPv6 options for the instance. + // If not specified, IPv6 ranges won't be allocated to instance. + // +optional + IPv6Options *IPv6CreateOptions `json:"ipv6Options,omitempty"` + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" // +optional // NetworkHelper is an option usually enabled on account level. It helps configure networking automatically for instances. @@ -121,6 +127,31 @@ type LinodeMachineSpec struct { NetworkHelper *bool `json:"networkHelper,omitempty"` } +// IPv6CreateOptions defines the IPv6 options for the instance. +type IPv6CreateOptions struct { + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" + // EnableSLAAC is an option to enable SLAAC (Stateless Address Autoconfiguration) for the instance. + // This is useful for IPv6 addresses, allowing the instance to automatically configure its own IPv6 address. + // Defaults to false. + // +optional + EnableSLAAC *bool `json:"enableSLAAC,omitempty"` + + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" + // EnableRanges is an option to enable IPv6 ranges for the instance. + // If set to true, the instance will have a range of IPv6 addresses. + // This is useful for instances that require multiple IPv6 addresses. + // Defaults to false. + // +optional + EnableRanges *bool `json:"enableRanges,omitempty"` + + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" + // IsPublicIPv6 is an option to enable public IPv6 for the instance. + // If set to true, the instance will have a publicly routable IPv6 range. + // Defaults to false. + // +optional + IsPublicIPv6 *bool `json:"isPublicIPv6,omitempty"` +} + // InstanceDisk defines a list of disks to use for an instance type InstanceDisk struct { // DiskID is the linode assigned ID of the disk diff --git a/api/v1alpha2/zz_generated.deepcopy.go b/api/v1alpha2/zz_generated.deepcopy.go index 50b06c517..aca089cd7 100644 --- a/api/v1alpha2/zz_generated.deepcopy.go +++ b/api/v1alpha2/zz_generated.deepcopy.go @@ -277,6 +277,36 @@ func (in *GeneratedSecret) DeepCopy() *GeneratedSecret { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IPv6CreateOptions) DeepCopyInto(out *IPv6CreateOptions) { + *out = *in + if in.EnableSLAAC != nil { + in, out := &in.EnableSLAAC, &out.EnableSLAAC + *out = new(bool) + **out = **in + } + if in.EnableRanges != nil { + in, out := &in.EnableRanges, &out.EnableRanges + *out = new(bool) + **out = **in + } + if in.IsPublicIPv6 != nil { + in, out := &in.IsPublicIPv6, &out.IsPublicIPv6 + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPv6CreateOptions. +func (in *IPv6CreateOptions) DeepCopy() *IPv6CreateOptions { + if in == nil { + return nil + } + out := new(IPv6CreateOptions) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InstanceConfigInterfaceCreateOptions) DeepCopyInto(out *InstanceConfigInterfaceCreateOptions) { *out = *in @@ -878,6 +908,11 @@ func (in *LinodeMachineSpec) DeepCopyInto(out *LinodeMachineSpec) { *out = new(int) **out = **in } + if in.IPv6Options != nil { + in, out := &in.IPv6Options, &out.IPv6Options + *out = new(IPv6CreateOptions) + (*in).DeepCopyInto(*out) + } if in.NetworkHelper != nil { in, out := &in.NetworkHelper, &out.NetworkHelper *out = new(bool) 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 d668d998b..aaaefa4a5 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,43 @@ spec: x-kubernetes-validations: - message: Value is immutable rule: self == oldSelf + ipv6Options: + description: |- + IPv6Options defines the IPv6 options for the instance. + If not specified, IPv6 ranges won't be allocated to instance. + properties: + enableRanges: + description: |- + EnableRanges is an option to enable IPv6 ranges for the instance. + If set to true, the instance will have a range of IPv6 addresses. + This is useful for instances that require multiple IPv6 addresses. + Defaults to false. + type: boolean + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + enableSLAAC: + description: |- + EnableSLAAC is an option to enable SLAAC (Stateless Address Autoconfiguration) for the instance. + This is useful for IPv6 addresses, allowing the instance to automatically configure its own IPv6 address. + Defaults to false. + type: boolean + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + isPublicIPv6: + description: |- + IsPublicIPv6 is an option to enable public IPv6 for the instance. + If set to true, the instance will have a publicly routable IPv6 range. + Defaults to false. + type: boolean + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + type: object + 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. 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 0ccaf93ba..9c674af88 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,43 @@ spec: x-kubernetes-validations: - message: Value is immutable rule: self == oldSelf + ipv6Options: + description: |- + IPv6Options defines the IPv6 options for the instance. + If not specified, IPv6 ranges won't be allocated to instance. + properties: + enableRanges: + description: |- + EnableRanges is an option to enable IPv6 ranges for the instance. + If set to true, the instance will have a range of IPv6 addresses. + This is useful for instances that require multiple IPv6 addresses. + Defaults to false. + type: boolean + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + enableSLAAC: + description: |- + EnableSLAAC is an option to enable SLAAC (Stateless Address Autoconfiguration) for the instance. + This is useful for IPv6 addresses, allowing the instance to automatically configure its own IPv6 address. + Defaults to false. + type: boolean + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + isPublicIPv6: + description: |- + IsPublicIPv6 is an option to enable public IPv6 for the instance. + If set to true, the instance will have a publicly routable IPv6 range. + Defaults to false. + type: boolean + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + type: object + 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. diff --git a/docs/src/reference/out.md b/docs/src/reference/out.md index 500ef39fb..2895d1ada 100644 --- a/docs/src/reference/out.md +++ b/docs/src/reference/out.md @@ -239,6 +239,24 @@ _Appears in:_ | `format` _object (keys:string, values:string)_ | How to format the data stored in the generated Secret.
It supports Go template syntax and interpolating the following values: .AccessKey, .SecretKey .BucketName .BucketEndpoint .S3Endpoint
If no format is supplied then a generic one is used containing the values specified. | | | +#### IPv6CreateOptions + + + +IPv6CreateOptions defines the IPv6 options for the instance. + + + +_Appears in:_ +- [LinodeMachineSpec](#linodemachinespec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `enableSLAAC` _boolean_ | EnableSLAAC is an option to enable SLAAC (Stateless Address Autoconfiguration) for the instance.
This is useful for IPv6 addresses, allowing the instance to automatically configure its own IPv6 address.
Defaults to false. | | | +| `enableRanges` _boolean_ | EnableRanges is an option to enable IPv6 ranges for the instance.
If set to true, the instance will have a range of IPv6 addresses.
This is useful for instances that require multiple IPv6 addresses.
Defaults to false. | | | +| `isPublicIPv6` _boolean_ | IsPublicIPv6 is an option to enable public IPv6 for the instance.
If set to true, the instance will have a publicly routable IPv6 range.
Defaults to false. | | | + + #### InstanceConfigInterfaceCreateOptions @@ -621,6 +639,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. | | | +| `ipv6Options` _[IPv6CreateOptions](#ipv6createoptions)_ | IPv6Options defines the IPv6 options for the instance.
If not specified, IPv6 ranges won't be allocated to instance. | | | | `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. | | | diff --git a/internal/controller/linodemachine_controller_helpers.go b/internal/controller/linodemachine_controller_helpers.go index d73a44bd1..21f794974 100644 --- a/internal/controller/linodemachine_controller_helpers.go +++ b/internal/controller/linodemachine_controller_helpers.go @@ -463,12 +463,12 @@ func getVPCInterfaceConfig(ctx context.Context, machineScope *scope.MachineScope subnetName := machineScope.LinodeCluster.Spec.Network.SubnetName // name of subnet to use - var ipv6RangeConfig []linodego.InstanceConfigInterfaceCreateOptionsIPv6Range + var ipv6Config *linodego.InstanceConfigInterfaceCreateOptionsIPv6 if subnetName != "" { for _, subnet := range linodeVPC.Spec.Subnets { if subnet.Label == subnetName { subnetID = subnet.SubnetID - ipv6RangeConfig = machineIPv6RangeConfig(len(subnet.IPv6)) + ipv6Config = getMachineIPv6Config(machineScope, len(subnet.IPv6)) break } } @@ -478,7 +478,7 @@ func getVPCInterfaceConfig(ctx context.Context, machineScope *scope.MachineScope } } else { subnetID = linodeVPC.Spec.Subnets[0].SubnetID // get first subnet if nothing specified - ipv6RangeConfig = machineIPv6RangeConfig(len(linodeVPC.Spec.Subnets[0].IPv6)) + ipv6Config = getMachineIPv6Config(machineScope, len(linodeVPC.Spec.Subnets[0].IPv6)) } if subnetID == 0 { @@ -488,10 +488,9 @@ func getVPCInterfaceConfig(ctx context.Context, machineScope *scope.MachineScope for i, netInterface := range interfaces { if netInterface.Purpose == linodego.InterfacePurposeVPC { interfaces[i].SubnetID = &subnetID - if len(ipv6RangeConfig) > 0 { - interfaces[i].IPv6 = &linodego.InstanceConfigInterfaceCreateOptionsIPv6{ - Ranges: ipv6RangeConfig, - } + // If IPv6 range config is not empty, add it to the interface configuration + if !isIPv6ConfigEmpty(ipv6Config) { + interfaces[i].IPv6 = ipv6Config } return nil, nil //nolint:nilnil // it is important we don't return an interface if a VPC interface already exists } @@ -506,11 +505,9 @@ func getVPCInterfaceConfig(ctx context.Context, machineScope *scope.MachineScope }, } - // If IPv6 range config is not empty, add it to the interface configuration - if len(ipv6RangeConfig) > 0 { - vpcIntfCreateOpts.IPv6 = &linodego.InstanceConfigInterfaceCreateOptionsIPv6{ - Ranges: ipv6RangeConfig, - } + // If IPv6 config is not empty, add it to the interface configuration + if !isIPv6ConfigEmpty(ipv6Config) { + vpcIntfCreateOpts.IPv6 = ipv6Config } return vpcIntfCreateOpts, nil @@ -538,12 +535,12 @@ func getVPCInterfaceConfigFromDirectID(ctx context.Context, machineScope *scope. } // If subnet name specified, find matching subnet; otherwise use first subnet - var ipv6RangeConfig []linodego.InstanceConfigInterfaceCreateOptionsIPv6Range + var ipv6Config *linodego.InstanceConfigInterfaceCreateOptionsIPv6 if subnetName != "" { for _, subnet := range vpc.Subnets { if subnet.Label == subnetName { subnetID = subnet.ID - ipv6RangeConfig = machineIPv6RangeConfig(len(subnet.IPv6)) + ipv6Config = getMachineIPv6Config(machineScope, len(subnet.IPv6)) break } } @@ -552,17 +549,15 @@ func getVPCInterfaceConfigFromDirectID(ctx context.Context, machineScope *scope. } } else { subnetID = vpc.Subnets[0].ID - ipv6RangeConfig = machineIPv6RangeConfig(len(vpc.Subnets[0].IPv6)) + ipv6Config = getMachineIPv6Config(machineScope, len(vpc.Subnets[0].IPv6)) } // Check if a VPC interface already exists for i, netInterface := range interfaces { if netInterface.Purpose == linodego.InterfacePurposeVPC { interfaces[i].SubnetID = &subnetID - if len(ipv6RangeConfig) > 0 { - interfaces[i].IPv6 = &linodego.InstanceConfigInterfaceCreateOptionsIPv6{ - Ranges: ipv6RangeConfig, - } + if !isIPv6ConfigEmpty(ipv6Config) { + interfaces[i].IPv6 = ipv6Config } return nil, nil //nolint:nilnil // it is important we don't return an interface if a VPC interface already exists } @@ -579,27 +574,55 @@ func getVPCInterfaceConfigFromDirectID(ctx context.Context, machineScope *scope. } // If IPv6 range config is not empty, add it to the interface configuration - if len(ipv6RangeConfig) > 0 { - vpcIntfCreateOpts.IPv6 = &linodego.InstanceConfigInterfaceCreateOptionsIPv6{ - Ranges: ipv6RangeConfig, - } + if !isIPv6ConfigEmpty(ipv6Config) { + vpcIntfCreateOpts.IPv6 = ipv6Config } return vpcIntfCreateOpts, nil } -// machineIPv6RangeConfig returns the IPv6 range configuration if subnet has IPv6 ranges. -// For now, we support only a single IPv6 range for machine per subnet. -// If this changes, we may need to adjust this logic. -func machineIPv6RangeConfig(numIPv6RangesInSubnet int) []linodego.InstanceConfigInterfaceCreateOptionsIPv6Range { - if numIPv6RangesInSubnet == 0 { - return nil // No IPv6 ranges available in subnet, return empty slice +// isIPv6ConfigEmpty checks if the IPv6 configuration is empty. +func isIPv6ConfigEmpty(opts *linodego.InstanceConfigInterfaceCreateOptionsIPv6) bool { + return opts == nil || + len(opts.SLAAC) == 0 && + len(opts.Ranges) == 0 && + (opts.IsPublic == nil) +} + +// getMachineIPv6Config returns the IPv6 configuration for a LinodeMachine. +// It checks the LinodeMachine's IPv6Options for SLAAC and Ranges settings. +// If `EnableSLAAC` is set, it will enable SLAAC with the default IPv6 CIDR range. +// If `EnableRanges` is set, it will enable IPv6 ranges with the default IPv6 CIDR range. +// If `IsPublicIPv6` is set, it will be used to determine if the IPv6 range should be publicly routable or not. +func getMachineIPv6Config(machineScope *scope.MachineScope, numIPv6RangesInSubnet int) *linodego.InstanceConfigInterfaceCreateOptionsIPv6 { + intfOpts := &linodego.InstanceConfigInterfaceCreateOptionsIPv6{} + + // If there are no IPv6 ranges in the subnet or if IPv6 options are not specified, return nil. + if numIPv6RangesInSubnet == 0 || machineScope.LinodeMachine.Spec.IPv6Options == nil { + return intfOpts } - return []linodego.InstanceConfigInterfaceCreateOptionsIPv6Range{ - { - Range: ptr.To(defaultNodeIPv6CIDRRange), - }, + + if machineScope.LinodeMachine.Spec.IPv6Options.IsPublicIPv6 != nil { + // Set the public IPv6 flag based on the IsPublicIPv6 specification. + intfOpts.IsPublic = machineScope.LinodeMachine.Spec.IPv6Options.IsPublicIPv6 } + + if machineScope.LinodeMachine.Spec.IPv6Options.EnableSLAAC != nil && *machineScope.LinodeMachine.Spec.IPv6Options.EnableSLAAC { + intfOpts.SLAAC = []linodego.InstanceConfigInterfaceCreateOptionsIPv6SLAAC{ + { + Range: defaultNodeIPv6CIDRRange, + }, + } + } + if machineScope.LinodeMachine.Spec.IPv6Options.EnableRanges != nil && *machineScope.LinodeMachine.Spec.IPv6Options.EnableRanges { + intfOpts.Ranges = []linodego.InstanceConfigInterfaceCreateOptionsIPv6Range{ + { + Range: ptr.To(defaultNodeIPv6CIDRRange), + }, + } + } + + return intfOpts } func linodeMachineSpecToInstanceCreateConfig(machineSpec infrav1alpha2.LinodeMachineSpec, machineTags []string) *linodego.InstanceCreateOptions { diff --git a/internal/controller/linodemachine_controller_helpers_test.go b/internal/controller/linodemachine_controller_helpers_test.go index 8e0cac29e..e02f93529 100644 --- a/internal/controller/linodemachine_controller_helpers_test.go +++ b/internal/controller/linodemachine_controller_helpers_test.go @@ -409,7 +409,9 @@ func validateInterfaceExpectations( if expectInterface { require.NotNil(t, iface) require.Equal(t, linodego.InterfacePurposeVPC, iface.Purpose) - if iface.IPv6 != nil { + if iface.IPv6 != nil && iface.IPv6.SLAAC != nil { + require.Equal(t, defaultNodeIPv6CIDRRange, iface.IPv6.SLAAC[0].Range) + } else if iface.IPv6 != nil && iface.IPv6.Ranges != nil { require.Equal(t, defaultNodeIPv6CIDRRange, *iface.IPv6.Ranges[0].Range) } require.True(t, iface.Primary) @@ -485,6 +487,35 @@ func TestGetVPCInterfaceConfigFromDirectID(t *testing.T) { expectInterface: true, expectSubnetID: 789, // Matching subnet ID }, + { + name: "Success - Valid VPC with subnets and ipv6 ranges, specific subnet name", + vpcID: 123, + interfaces: []linodego.InstanceConfigInterfaceCreateOptions{}, + subnetName: "subnet-2", + mockSetup: func(mockLinodeClient *mock.MockLinodeClient) { + mockLinodeClient.EXPECT().GetVPC(gomock.Any(), 123).Return(&linodego.VPC{ + ID: 123, + Subnets: []linodego.VPCSubnet{ + { + ID: 456, + Label: "subnet-1", + }, + { + ID: 789, + Label: "subnet-2", + IPv6: []linodego.VPCIPv6Range{ + { + Range: "2001:0db8::/56", + }, + }, + }, + }, + }, nil) + }, + expectErr: false, + expectInterface: true, + expectSubnetID: 789, // Matching subnet ID + }, { name: "Success - VPC interface already exists", vpcID: 123, @@ -1176,7 +1207,12 @@ func TestGetVPCInterfaceConfig(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Namespace: "default", }, - Spec: infrav1alpha2.LinodeMachineSpec{}, + Spec: infrav1alpha2.LinodeMachineSpec{ + IPv6Options: &infrav1alpha2.IPv6CreateOptions{ + EnableSLAAC: ptr.To(true), + IsPublicIPv6: ptr.To(true), + }, + }, }, LinodeCluster: &infrav1alpha2.LinodeCluster{ Spec: infrav1alpha2.LinodeClusterSpec{