diff --git a/api/v1alpha2/linodemachine_types.go b/api/v1alpha2/linodemachine_types.go
index c103c4811..13fd76f05 100644
--- a/api/v1alpha2/linodemachine_types.go
+++ b/api/v1alpha2/linodemachine_types.go
@@ -59,8 +59,13 @@ type LinodeMachineSpec struct {
BackupID int `json:"backupID,omitempty"`
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable"
Image string `json:"image,omitempty"`
+ // Interfaces is a list of legacy network interfaces to use for the instance.
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable"
Interfaces []InstanceConfigInterfaceCreateOptions `json:"interfaces,omitempty"`
+ // LinodeInterfaces is a list of Linode network interfaces to use for the instance. Requires Linode Interfaces beta opt-in to use.
+ // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable"
+ // +kubebuilder:object:generate=true
+ LinodeInterfaces []LinodeInterfaceCreateOptions `json:"linodeInterfaces,omitempty"`
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable"
BackupsEnabled bool `json:"backupsEnabled,omitempty"`
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable"
@@ -125,6 +130,13 @@ type LinodeMachineSpec struct {
// For more information, see https://techdocs.akamai.com/cloud-computing/docs/automatically-configure-networking
// Defaults to true.
NetworkHelper *bool `json:"networkHelper,omitempty"`
+
+ // InterfaceGeneration is the generation of the interface to use for the cluster's
+ // nodes in interface / linodeInterface are not specified for a LinodeMachine.
+ // If not set, defaults to "legacy_config".
+ // +kubebuilder:validation:Enum=legacy_config;linode
+ // +kubebuilder:default=legacy_config
+ InterfaceGeneration linodego.InterfaceGeneration `json:"interfaceGeneration,omitempty"`
}
// IPv6CreateOptions defines the IPv6 options for the instance.
@@ -194,6 +206,96 @@ type InstanceConfigInterfaceCreateOptions struct {
IPRanges []string `json:"ipRanges,omitempty"`
}
+// LinodeInterfaceCreateOptions defines the linode network interface config
+type LinodeInterfaceCreateOptions struct {
+ FirewallID *int `json:"firewall_id,omitempty"`
+ DefaultRoute *InterfaceDefaultRoute `json:"default_route,omitempty"`
+ Public *PublicInterfaceCreateOptions `json:"public,omitempty"`
+ VPC *VPCInterfaceCreateOptions `json:"vpc,omitempty"`
+ VLAN *VLANInterface `json:"vlan,omitempty"`
+}
+
+// InterfaceDefaultRoute defines the default IPv4 and IPv6 routes for an interface
+type InterfaceDefaultRoute struct {
+ IPv4 *bool `json:"ipv4,omitempty"`
+ IPv6 *bool `json:"ipv6,omitempty"`
+}
+
+// PublicInterfaceCreateOptions defines the IPv4 and IPv6 public interface create options
+type PublicInterfaceCreateOptions struct {
+ IPv4 *PublicInterfaceIPv4CreateOptions `json:"ipv4,omitempty"`
+ IPv6 *PublicInterfaceIPv6CreateOptions `json:"ipv6,omitempty"`
+}
+
+// PublicInterfaceIPv4CreateOptions defines the PublicInterfaceIPv4AddressCreateOptions for addresses
+type PublicInterfaceIPv4CreateOptions struct {
+ Addresses []PublicInterfaceIPv4AddressCreateOptions `json:"addresses,omitempty"`
+}
+
+// PublicInterfaceIPv4AddressCreateOptions defines the public IPv4 address and whether it is primary
+type PublicInterfaceIPv4AddressCreateOptions struct {
+ Address string `json:"address"`
+ Primary *bool `json:"primary,omitempty"`
+}
+
+// PublicInterfaceIPv6CreateOptions defines the PublicInterfaceIPv6RangeCreateOptions
+type PublicInterfaceIPv6CreateOptions struct {
+ Ranges []PublicInterfaceIPv6RangeCreateOptions `json:"ranges,omitempty"`
+}
+
+// PublicInterfaceIPv6RangeCreateOptions defines the IPv6 range for a public interface
+type PublicInterfaceIPv6RangeCreateOptions struct {
+ Range string `json:"range"`
+}
+
+// VPCInterfaceCreateOptions defines the VPC interface configuration for an instance
+type VPCInterfaceCreateOptions struct {
+ SubnetID int `json:"subnet_id"`
+ IPv4 *VPCInterfaceIPv4CreateOptions `json:"ipv4,omitempty"`
+ IPv6 *VPCInterfaceIPv6CreateOptions `json:"ipv6,omitempty"`
+}
+
+// VPCInterfaceIPv6CreateOptions defines the IPv6 configuration for a VPC interface
+type VPCInterfaceIPv6CreateOptions struct {
+ SLAAC []VPCInterfaceIPv6SLAACCreateOptions `json:"slaac,omitempty"`
+ Ranges []VPCInterfaceIPv6RangeCreateOptions `json:"ranges,omitempty"`
+ IsPublic bool `json:"is_public"`
+}
+
+// VPCInterfaceIPv6SLAACCreateOptions defines the Range for IPv6 SLAAC
+type VPCInterfaceIPv6SLAACCreateOptions struct {
+ Range string `json:"range"`
+}
+
+// VPCInterfaceIPv6RangeCreateOptions defines the IPv6 range for a VPC interface
+type VPCInterfaceIPv6RangeCreateOptions struct {
+ Range string `json:"range"`
+}
+
+// VPCInterfaceIPv4CreateOptions defines the IPv4 address and range configuration for a VPC interface
+type VPCInterfaceIPv4CreateOptions struct {
+ Addresses []VPCInterfaceIPv4AddressCreateOptions `json:"addresses,omitempty"`
+ Ranges []VPCInterfaceIPv4RangeCreateOptions `json:"ranges,omitempty"`
+}
+
+// VPCInterfaceIPv4AddressCreateOptions defines the IPv4 configuration for a VPC interface
+type VPCInterfaceIPv4AddressCreateOptions struct {
+ Address string `json:"address"`
+ Primary *bool `json:"primary,omitempty"`
+ NAT1To1Address *string `json:"nat_1_1_address,omitempty"`
+}
+
+// VPCInterfaceIPv4RangeCreateOptions defines the IPv4 range for a VPC interface
+type VPCInterfaceIPv4RangeCreateOptions struct {
+ Range string `json:"range"`
+}
+
+// VLANInterface defines the VLAN interface configuration for an instance
+type VLANInterface struct {
+ VLANLabel string `json:"vlan_label"`
+ IPAMAddress *string `json:"ipam_address,omitempty"`
+}
+
// VPCIPv4 defines VPC IPV4 settings
type VPCIPv4 struct {
VPC string `json:"vpc,omitempty"`
diff --git a/api/v1alpha2/zz_generated.deepcopy.go b/api/v1alpha2/zz_generated.deepcopy.go
index aca089cd7..25df77f57 100644
--- a/api/v1alpha2/zz_generated.deepcopy.go
+++ b/api/v1alpha2/zz_generated.deepcopy.go
@@ -383,6 +383,31 @@ func (in *InstanceMetadataOptions) DeepCopy() *InstanceMetadataOptions {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *InterfaceDefaultRoute) DeepCopyInto(out *InterfaceDefaultRoute) {
+ *out = *in
+ if in.IPv4 != nil {
+ in, out := &in.IPv4, &out.IPv4
+ *out = new(bool)
+ **out = **in
+ }
+ if in.IPv6 != nil {
+ in, out := &in.IPv6, &out.IPv6
+ *out = new(bool)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InterfaceDefaultRoute.
+func (in *InterfaceDefaultRoute) DeepCopy() *InterfaceDefaultRoute {
+ if in == nil {
+ return nil
+ }
+ out := new(InterfaceDefaultRoute)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LinodeCluster) DeepCopyInto(out *LinodeCluster) {
*out = *in
@@ -758,6 +783,46 @@ func (in *LinodeFirewallStatus) DeepCopy() *LinodeFirewallStatus {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *LinodeInterfaceCreateOptions) DeepCopyInto(out *LinodeInterfaceCreateOptions) {
+ *out = *in
+ if in.FirewallID != nil {
+ in, out := &in.FirewallID, &out.FirewallID
+ *out = new(int)
+ **out = **in
+ }
+ if in.DefaultRoute != nil {
+ in, out := &in.DefaultRoute, &out.DefaultRoute
+ *out = new(InterfaceDefaultRoute)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.Public != nil {
+ in, out := &in.Public, &out.Public
+ *out = new(PublicInterfaceCreateOptions)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.VPC != nil {
+ in, out := &in.VPC, &out.VPC
+ *out = new(VPCInterfaceCreateOptions)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.VLAN != nil {
+ in, out := &in.VLAN, &out.VLAN
+ *out = new(VLANInterface)
+ (*in).DeepCopyInto(*out)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LinodeInterfaceCreateOptions.
+func (in *LinodeInterfaceCreateOptions) DeepCopy() *LinodeInterfaceCreateOptions {
+ if in == nil {
+ return nil
+ }
+ out := new(LinodeInterfaceCreateOptions)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LinodeMachine) DeepCopyInto(out *LinodeMachine) {
*out = *in
@@ -847,6 +912,13 @@ func (in *LinodeMachineSpec) DeepCopyInto(out *LinodeMachineSpec) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
+ if in.LinodeInterfaces != nil {
+ in, out := &in.LinodeInterfaces, &out.LinodeInterfaces
+ *out = make([]LinodeInterfaceCreateOptions, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
if in.PrivateIP != nil {
in, out := &in.PrivateIP, &out.PrivateIP
*out = new(bool)
@@ -1710,6 +1782,128 @@ func (in *ObjectStore) DeepCopy() *ObjectStore {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PublicInterfaceCreateOptions) DeepCopyInto(out *PublicInterfaceCreateOptions) {
+ *out = *in
+ if in.IPv4 != nil {
+ in, out := &in.IPv4, &out.IPv4
+ *out = new(PublicInterfaceIPv4CreateOptions)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.IPv6 != nil {
+ in, out := &in.IPv6, &out.IPv6
+ *out = new(PublicInterfaceIPv6CreateOptions)
+ (*in).DeepCopyInto(*out)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PublicInterfaceCreateOptions.
+func (in *PublicInterfaceCreateOptions) DeepCopy() *PublicInterfaceCreateOptions {
+ if in == nil {
+ return nil
+ }
+ out := new(PublicInterfaceCreateOptions)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PublicInterfaceIPv4AddressCreateOptions) DeepCopyInto(out *PublicInterfaceIPv4AddressCreateOptions) {
+ *out = *in
+ if in.Primary != nil {
+ in, out := &in.Primary, &out.Primary
+ *out = new(bool)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PublicInterfaceIPv4AddressCreateOptions.
+func (in *PublicInterfaceIPv4AddressCreateOptions) DeepCopy() *PublicInterfaceIPv4AddressCreateOptions {
+ if in == nil {
+ return nil
+ }
+ out := new(PublicInterfaceIPv4AddressCreateOptions)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PublicInterfaceIPv4CreateOptions) DeepCopyInto(out *PublicInterfaceIPv4CreateOptions) {
+ *out = *in
+ if in.Addresses != nil {
+ in, out := &in.Addresses, &out.Addresses
+ *out = make([]PublicInterfaceIPv4AddressCreateOptions, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PublicInterfaceIPv4CreateOptions.
+func (in *PublicInterfaceIPv4CreateOptions) DeepCopy() *PublicInterfaceIPv4CreateOptions {
+ if in == nil {
+ return nil
+ }
+ out := new(PublicInterfaceIPv4CreateOptions)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PublicInterfaceIPv6CreateOptions) DeepCopyInto(out *PublicInterfaceIPv6CreateOptions) {
+ *out = *in
+ if in.Ranges != nil {
+ in, out := &in.Ranges, &out.Ranges
+ *out = make([]PublicInterfaceIPv6RangeCreateOptions, len(*in))
+ copy(*out, *in)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PublicInterfaceIPv6CreateOptions.
+func (in *PublicInterfaceIPv6CreateOptions) DeepCopy() *PublicInterfaceIPv6CreateOptions {
+ if in == nil {
+ return nil
+ }
+ out := new(PublicInterfaceIPv6CreateOptions)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PublicInterfaceIPv6RangeCreateOptions) DeepCopyInto(out *PublicInterfaceIPv6RangeCreateOptions) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PublicInterfaceIPv6RangeCreateOptions.
+func (in *PublicInterfaceIPv6RangeCreateOptions) DeepCopy() *PublicInterfaceIPv6RangeCreateOptions {
+ if in == nil {
+ return nil
+ }
+ out := new(PublicInterfaceIPv6RangeCreateOptions)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VLANInterface) DeepCopyInto(out *VLANInterface) {
+ *out = *in
+ if in.IPAMAddress != nil {
+ in, out := &in.IPAMAddress, &out.IPAMAddress
+ *out = new(string)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VLANInterface.
+func (in *VLANInterface) DeepCopy() *VLANInterface {
+ if in == nil {
+ return nil
+ }
+ out := new(VLANInterface)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VPCCreateOptionsIPv6) DeepCopyInto(out *VPCCreateOptionsIPv6) {
*out = *in
@@ -1750,6 +1944,153 @@ func (in *VPCIPv4) DeepCopy() *VPCIPv4 {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VPCInterfaceCreateOptions) DeepCopyInto(out *VPCInterfaceCreateOptions) {
+ *out = *in
+ if in.IPv4 != nil {
+ in, out := &in.IPv4, &out.IPv4
+ *out = new(VPCInterfaceIPv4CreateOptions)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.IPv6 != nil {
+ in, out := &in.IPv6, &out.IPv6
+ *out = new(VPCInterfaceIPv6CreateOptions)
+ (*in).DeepCopyInto(*out)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPCInterfaceCreateOptions.
+func (in *VPCInterfaceCreateOptions) DeepCopy() *VPCInterfaceCreateOptions {
+ if in == nil {
+ return nil
+ }
+ out := new(VPCInterfaceCreateOptions)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VPCInterfaceIPv4AddressCreateOptions) DeepCopyInto(out *VPCInterfaceIPv4AddressCreateOptions) {
+ *out = *in
+ if in.Primary != nil {
+ in, out := &in.Primary, &out.Primary
+ *out = new(bool)
+ **out = **in
+ }
+ if in.NAT1To1Address != nil {
+ in, out := &in.NAT1To1Address, &out.NAT1To1Address
+ *out = new(string)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPCInterfaceIPv4AddressCreateOptions.
+func (in *VPCInterfaceIPv4AddressCreateOptions) DeepCopy() *VPCInterfaceIPv4AddressCreateOptions {
+ if in == nil {
+ return nil
+ }
+ out := new(VPCInterfaceIPv4AddressCreateOptions)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VPCInterfaceIPv4CreateOptions) DeepCopyInto(out *VPCInterfaceIPv4CreateOptions) {
+ *out = *in
+ if in.Addresses != nil {
+ in, out := &in.Addresses, &out.Addresses
+ *out = make([]VPCInterfaceIPv4AddressCreateOptions, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+ if in.Ranges != nil {
+ in, out := &in.Ranges, &out.Ranges
+ *out = make([]VPCInterfaceIPv4RangeCreateOptions, len(*in))
+ copy(*out, *in)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPCInterfaceIPv4CreateOptions.
+func (in *VPCInterfaceIPv4CreateOptions) DeepCopy() *VPCInterfaceIPv4CreateOptions {
+ if in == nil {
+ return nil
+ }
+ out := new(VPCInterfaceIPv4CreateOptions)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VPCInterfaceIPv4RangeCreateOptions) DeepCopyInto(out *VPCInterfaceIPv4RangeCreateOptions) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPCInterfaceIPv4RangeCreateOptions.
+func (in *VPCInterfaceIPv4RangeCreateOptions) DeepCopy() *VPCInterfaceIPv4RangeCreateOptions {
+ if in == nil {
+ return nil
+ }
+ out := new(VPCInterfaceIPv4RangeCreateOptions)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VPCInterfaceIPv6CreateOptions) DeepCopyInto(out *VPCInterfaceIPv6CreateOptions) {
+ *out = *in
+ if in.SLAAC != nil {
+ in, out := &in.SLAAC, &out.SLAAC
+ *out = make([]VPCInterfaceIPv6SLAACCreateOptions, len(*in))
+ copy(*out, *in)
+ }
+ if in.Ranges != nil {
+ in, out := &in.Ranges, &out.Ranges
+ *out = make([]VPCInterfaceIPv6RangeCreateOptions, len(*in))
+ copy(*out, *in)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPCInterfaceIPv6CreateOptions.
+func (in *VPCInterfaceIPv6CreateOptions) DeepCopy() *VPCInterfaceIPv6CreateOptions {
+ if in == nil {
+ return nil
+ }
+ out := new(VPCInterfaceIPv6CreateOptions)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VPCInterfaceIPv6RangeCreateOptions) DeepCopyInto(out *VPCInterfaceIPv6RangeCreateOptions) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPCInterfaceIPv6RangeCreateOptions.
+func (in *VPCInterfaceIPv6RangeCreateOptions) DeepCopy() *VPCInterfaceIPv6RangeCreateOptions {
+ if in == nil {
+ return nil
+ }
+ out := new(VPCInterfaceIPv6RangeCreateOptions)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VPCInterfaceIPv6SLAACCreateOptions) DeepCopyInto(out *VPCInterfaceIPv6SLAACCreateOptions) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPCInterfaceIPv6SLAACCreateOptions.
+func (in *VPCInterfaceIPv6SLAACCreateOptions) DeepCopy() *VPCInterfaceIPv6SLAACCreateOptions {
+ if in == nil {
+ return nil
+ }
+ out := new(VPCInterfaceIPv6SLAACCreateOptions)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VPCSubnetCreateOptions) DeepCopyInto(out *VPCSubnetCreateOptions) {
*out = *in
diff --git a/clients/clients.go b/clients/clients.go
index 6b6a50507..01e1817cd 100644
--- a/clients/clients.go
+++ b/clients/clients.go
@@ -23,6 +23,7 @@ type LinodeClient interface {
LinodePlacementGroupClient
LinodeFirewallClient
LinodeTokenClient
+ LinodeInterfacesClient
OnAfterResponse(m func(response *resty.Response) error)
}
@@ -125,6 +126,12 @@ type LinodeFirewallClient interface {
DeleteFirewallDevice(ctx context.Context, firewallID, deviceID int) error
}
+// LinodeInterfacesClient defines the methods that interact with Linode's Interfaces service.
+type LinodeInterfacesClient interface {
+ ListInterfaces(ctx context.Context, linodeID int, opts *linodego.ListOptions) ([]linodego.LinodeInterface, error)
+ ListInterfaceFirewalls(ctx context.Context, linodeID int, interfaceID int, opts *linodego.ListOptions) ([]linodego.Firewall, error)
+}
+
type K8sClient interface {
client.Client
}
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 32a92ace3..f952afac3 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachines.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachines.yaml
@@ -227,7 +227,19 @@ spec:
instanceID:
description: InstanceID is the Linode instance ID for this machine.
type: integer
+ interfaceGeneration:
+ default: legacy_config
+ description: |-
+ InterfaceGeneration is the generation of the interface to use for the cluster's
+ nodes in interface / linodeInterface are not specified for a LinodeMachine.
+ If not set, defaults to "legacy_config".
+ enum:
+ - legacy_config
+ - linode
+ type: string
interfaces:
+ description: Interfaces is a list of legacy network interfaces to
+ use for the instance.
items:
description: InstanceConfigInterfaceCreateOptions defines network
interface config
@@ -300,6 +312,152 @@ spec:
x-kubernetes-validations:
- message: Value is immutable
rule: self == oldSelf
+ linodeInterfaces:
+ description: LinodeInterfaces is a list of Linode network interfaces
+ to use for the instance. Requires Linode Interfaces beta opt-in
+ to use.
+ items:
+ description: LinodeInterfaceCreateOptions defines the linode network
+ interface config
+ properties:
+ default_route:
+ description: InterfaceDefaultRoute defines the default IPv4
+ and IPv6 routes for an interface
+ properties:
+ ipv4:
+ type: boolean
+ ipv6:
+ type: boolean
+ type: object
+ firewall_id:
+ type: integer
+ public:
+ description: PublicInterfaceCreateOptions defines the IPv4 and
+ IPv6 public interface create options
+ properties:
+ ipv4:
+ description: PublicInterfaceIPv4CreateOptions defines the
+ PublicInterfaceIPv4AddressCreateOptions for addresses
+ properties:
+ addresses:
+ items:
+ description: PublicInterfaceIPv4AddressCreateOptions
+ defines the public IPv4 address and whether it is
+ primary
+ properties:
+ address:
+ type: string
+ primary:
+ type: boolean
+ required:
+ - address
+ type: object
+ type: array
+ type: object
+ ipv6:
+ description: PublicInterfaceIPv6CreateOptions defines the
+ PublicInterfaceIPv6RangeCreateOptions
+ properties:
+ ranges:
+ items:
+ description: PublicInterfaceIPv6RangeCreateOptions
+ defines the IPv6 range for a public interface
+ properties:
+ range:
+ type: string
+ required:
+ - range
+ type: object
+ type: array
+ type: object
+ type: object
+ vlan:
+ description: VLANInterface defines the VLAN interface configuration
+ for an instance
+ properties:
+ ipam_address:
+ type: string
+ vlan_label:
+ type: string
+ required:
+ - vlan_label
+ type: object
+ vpc:
+ description: VPCInterfaceCreateOptions defines the VPC interface
+ configuration for an instance
+ properties:
+ ipv4:
+ description: VPCInterfaceIPv4CreateOptions defines the IPv4
+ address and range configuration for a VPC interface
+ properties:
+ addresses:
+ items:
+ description: VPCInterfaceIPv4AddressCreateOptions
+ defines the IPv4 configuration for a VPC interface
+ properties:
+ address:
+ type: string
+ nat_1_1_address:
+ type: string
+ primary:
+ type: boolean
+ required:
+ - address
+ type: object
+ type: array
+ ranges:
+ items:
+ description: VPCInterfaceIPv4RangeCreateOptions defines
+ the IPv4 range for a VPC interface
+ properties:
+ range:
+ type: string
+ required:
+ - range
+ type: object
+ type: array
+ type: object
+ ipv6:
+ description: VPCInterfaceIPv6CreateOptions defines the IPv6
+ configuration for a VPC interface
+ properties:
+ is_public:
+ type: boolean
+ ranges:
+ items:
+ description: VPCInterfaceIPv6RangeCreateOptions defines
+ the IPv6 range for a VPC interface
+ properties:
+ range:
+ type: string
+ required:
+ - range
+ type: object
+ type: array
+ slaac:
+ items:
+ description: VPCInterfaceIPv6SLAACCreateOptions defines
+ the Range for IPv6 SLAAC
+ properties:
+ range:
+ type: string
+ required:
+ - range
+ type: object
+ type: array
+ required:
+ - is_public
+ type: object
+ subnet_id:
+ type: integer
+ required:
+ - subnet_id
+ type: object
+ type: object
+ type: array
+ 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 52efc9f41..9cb0d4b12 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachinetemplates.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachinetemplates.yaml
@@ -218,7 +218,19 @@ spec:
description: InstanceID is the Linode instance ID for this
machine.
type: integer
+ interfaceGeneration:
+ default: legacy_config
+ description: |-
+ InterfaceGeneration is the generation of the interface to use for the cluster's
+ nodes in interface / linodeInterface are not specified for a LinodeMachine.
+ If not set, defaults to "legacy_config".
+ enum:
+ - legacy_config
+ - linode
+ type: string
interfaces:
+ description: Interfaces is a list of legacy network interfaces
+ to use for the instance.
items:
description: InstanceConfigInterfaceCreateOptions defines
network interface config
@@ -292,6 +304,155 @@ spec:
x-kubernetes-validations:
- message: Value is immutable
rule: self == oldSelf
+ linodeInterfaces:
+ description: LinodeInterfaces is a list of Linode network
+ interfaces to use for the instance. Requires Linode Interfaces
+ beta opt-in to use.
+ items:
+ description: LinodeInterfaceCreateOptions defines the linode
+ network interface config
+ properties:
+ default_route:
+ description: InterfaceDefaultRoute defines the default
+ IPv4 and IPv6 routes for an interface
+ properties:
+ ipv4:
+ type: boolean
+ ipv6:
+ type: boolean
+ type: object
+ firewall_id:
+ type: integer
+ public:
+ description: PublicInterfaceCreateOptions defines the
+ IPv4 and IPv6 public interface create options
+ properties:
+ ipv4:
+ description: PublicInterfaceIPv4CreateOptions defines
+ the PublicInterfaceIPv4AddressCreateOptions for
+ addresses
+ properties:
+ addresses:
+ items:
+ description: PublicInterfaceIPv4AddressCreateOptions
+ defines the public IPv4 address and whether
+ it is primary
+ properties:
+ address:
+ type: string
+ primary:
+ type: boolean
+ required:
+ - address
+ type: object
+ type: array
+ type: object
+ ipv6:
+ description: PublicInterfaceIPv6CreateOptions defines
+ the PublicInterfaceIPv6RangeCreateOptions
+ properties:
+ ranges:
+ items:
+ description: PublicInterfaceIPv6RangeCreateOptions
+ defines the IPv6 range for a public interface
+ properties:
+ range:
+ type: string
+ required:
+ - range
+ type: object
+ type: array
+ type: object
+ type: object
+ vlan:
+ description: VLANInterface defines the VLAN interface
+ configuration for an instance
+ properties:
+ ipam_address:
+ type: string
+ vlan_label:
+ type: string
+ required:
+ - vlan_label
+ type: object
+ vpc:
+ description: VPCInterfaceCreateOptions defines the VPC
+ interface configuration for an instance
+ properties:
+ ipv4:
+ description: VPCInterfaceIPv4CreateOptions defines
+ the IPv4 address and range configuration for a
+ VPC interface
+ properties:
+ addresses:
+ items:
+ description: VPCInterfaceIPv4AddressCreateOptions
+ defines the IPv4 configuration for a VPC
+ interface
+ properties:
+ address:
+ type: string
+ nat_1_1_address:
+ type: string
+ primary:
+ type: boolean
+ required:
+ - address
+ type: object
+ type: array
+ ranges:
+ items:
+ description: VPCInterfaceIPv4RangeCreateOptions
+ defines the IPv4 range for a VPC interface
+ properties:
+ range:
+ type: string
+ required:
+ - range
+ type: object
+ type: array
+ type: object
+ ipv6:
+ description: VPCInterfaceIPv6CreateOptions defines
+ the IPv6 configuration for a VPC interface
+ properties:
+ is_public:
+ type: boolean
+ ranges:
+ items:
+ description: VPCInterfaceIPv6RangeCreateOptions
+ defines the IPv6 range for a VPC interface
+ properties:
+ range:
+ type: string
+ required:
+ - range
+ type: object
+ type: array
+ slaac:
+ items:
+ description: VPCInterfaceIPv6SLAACCreateOptions
+ defines the Range for IPv6 SLAAC
+ properties:
+ range:
+ type: string
+ required:
+ - range
+ type: object
+ type: array
+ required:
+ - is_public
+ type: object
+ subnet_id:
+ type: integer
+ required:
+ - subnet_id
+ type: object
+ type: object
+ type: array
+ 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 7a1e0e5db..f23055674 100644
--- a/docs/src/reference/out.md
+++ b/docs/src/reference/out.md
@@ -316,6 +316,23 @@ _Appears in:_
+#### InterfaceDefaultRoute
+
+
+
+InterfaceDefaultRoute defines the default IPv4 and IPv6 routes for an interface
+
+
+
+_Appears in:_
+- [LinodeInterfaceCreateOptions](#linodeinterfacecreateoptions)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `ipv4` _boolean_ | | | |
+| `ipv6` _boolean_ | | | |
+
+
#### LinodeCluster
@@ -559,6 +576,26 @@ _Appears in:_
| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#condition-v1-meta) array_ | Conditions defines current service state of the LinodeFirewall. | | |
+#### LinodeInterfaceCreateOptions
+
+
+
+LinodeInterfaceCreateOptions defines the linode network interface config
+
+
+
+_Appears in:_
+- [LinodeMachineSpec](#linodemachinespec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `firewall_id` _integer_ | | | |
+| `default_route` _[InterfaceDefaultRoute](#interfacedefaultroute)_ | | | |
+| `public` _[PublicInterfaceCreateOptions](#publicinterfacecreateoptions)_ | | | |
+| `vpc` _[VPCInterfaceCreateOptions](#vpcinterfacecreateoptions)_ | | | |
+| `vlan` _[VLANInterface](#vlaninterface)_ | | | |
+
+
#### LinodeMachine
@@ -625,7 +662,8 @@ _Appears in:_
| `authorizedUsers` _string array_ | | | |
| `backupID` _integer_ | | | |
| `image` _string_ | | | |
-| `interfaces` _[InstanceConfigInterfaceCreateOptions](#instanceconfiginterfacecreateoptions) array_ | | | |
+| `interfaces` _[InstanceConfigInterfaceCreateOptions](#instanceconfiginterfacecreateoptions) array_ | Interfaces is a list of legacy network interfaces to use for the instance. | | |
+| `linodeInterfaces` _[LinodeInterfaceCreateOptions](#linodeinterfacecreateoptions) array_ | LinodeInterfaces is a list of Linode network interfaces to use for the instance. Requires Linode Interfaces beta opt-in to use. | | |
| `backupsEnabled` _boolean_ | | | |
| `privateIP` _boolean_ | | | |
| `tags` _string array_ | Tags is a list of tags to apply to the Linode instance. | | |
@@ -641,6 +679,7 @@ _Appears in:_
| `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. | | |
+| `interfaceGeneration` _[InterfaceGeneration](#interfacegeneration)_ | InterfaceGeneration is the generation of the interface to use for the cluster's
nodes in interface / linodeInterface are not specified for a LinodeMachine.
If not set, defaults to "legacy_config". | legacy_config | Enum: [legacy_config linode]
|
#### LinodeMachineStatus
@@ -1211,6 +1250,105 @@ _Appears in:_
| `credentialsRef` _[SecretReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#secretreference-v1-core)_ | CredentialsRef is a reference to a Secret that contains the credentials to use for accessing the Cluster Object Store. | | |
+#### PublicInterfaceCreateOptions
+
+
+
+PublicInterfaceCreateOptions defines the IPv4 and IPv6 public interface create options
+
+
+
+_Appears in:_
+- [LinodeInterfaceCreateOptions](#linodeinterfacecreateoptions)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `ipv4` _[PublicInterfaceIPv4CreateOptions](#publicinterfaceipv4createoptions)_ | | | |
+| `ipv6` _[PublicInterfaceIPv6CreateOptions](#publicinterfaceipv6createoptions)_ | | | |
+
+
+#### PublicInterfaceIPv4AddressCreateOptions
+
+
+
+PublicInterfaceIPv4AddressCreateOptions defines the public IPv4 address and whether it is primary
+
+
+
+_Appears in:_
+- [PublicInterfaceIPv4CreateOptions](#publicinterfaceipv4createoptions)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `address` _string_ | | | |
+| `primary` _boolean_ | | | |
+
+
+#### PublicInterfaceIPv4CreateOptions
+
+
+
+PublicInterfaceIPv4CreateOptions defines the PublicInterfaceIPv4AddressCreateOptions for addresses
+
+
+
+_Appears in:_
+- [PublicInterfaceCreateOptions](#publicinterfacecreateoptions)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `addresses` _[PublicInterfaceIPv4AddressCreateOptions](#publicinterfaceipv4addresscreateoptions) array_ | | | |
+
+
+#### PublicInterfaceIPv6CreateOptions
+
+
+
+PublicInterfaceIPv6CreateOptions defines the PublicInterfaceIPv6RangeCreateOptions
+
+
+
+_Appears in:_
+- [PublicInterfaceCreateOptions](#publicinterfacecreateoptions)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `ranges` _[PublicInterfaceIPv6RangeCreateOptions](#publicinterfaceipv6rangecreateoptions) array_ | | | |
+
+
+#### PublicInterfaceIPv6RangeCreateOptions
+
+
+
+PublicInterfaceIPv6RangeCreateOptions defines the IPv6 range for a public interface
+
+
+
+_Appears in:_
+- [PublicInterfaceIPv6CreateOptions](#publicinterfaceipv6createoptions)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `range` _string_ | | | |
+
+
+#### VLANInterface
+
+
+
+VLANInterface defines the VLAN interface configuration for an instance
+
+
+
+_Appears in:_
+- [LinodeInterfaceCreateOptions](#linodeinterfacecreateoptions)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `vlan_label` _string_ | | | |
+| `ipam_address` _string_ | | | |
+
+
#### VPCCreateOptionsIPv6
@@ -1248,6 +1386,125 @@ _Appears in:_
| `nat1to1` _string_ | | | |
+#### VPCInterfaceCreateOptions
+
+
+
+VPCInterfaceCreateOptions defines the VPC interface configuration for an instance
+
+
+
+_Appears in:_
+- [LinodeInterfaceCreateOptions](#linodeinterfacecreateoptions)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `subnet_id` _integer_ | | | |
+| `ipv4` _[VPCInterfaceIPv4CreateOptions](#vpcinterfaceipv4createoptions)_ | | | |
+| `ipv6` _[VPCInterfaceIPv6CreateOptions](#vpcinterfaceipv6createoptions)_ | | | |
+
+
+#### VPCInterfaceIPv4AddressCreateOptions
+
+
+
+VPCInterfaceIPv4AddressCreateOptions defines the IPv4 configuration for a VPC interface
+
+
+
+_Appears in:_
+- [VPCInterfaceIPv4CreateOptions](#vpcinterfaceipv4createoptions)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `address` _string_ | | | |
+| `primary` _boolean_ | | | |
+| `nat_1_1_address` _string_ | | | |
+
+
+#### VPCInterfaceIPv4CreateOptions
+
+
+
+VPCInterfaceIPv4CreateOptions defines the IPv4 address and range configuration for a VPC interface
+
+
+
+_Appears in:_
+- [VPCInterfaceCreateOptions](#vpcinterfacecreateoptions)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `addresses` _[VPCInterfaceIPv4AddressCreateOptions](#vpcinterfaceipv4addresscreateoptions) array_ | | | |
+| `ranges` _[VPCInterfaceIPv4RangeCreateOptions](#vpcinterfaceipv4rangecreateoptions) array_ | | | |
+
+
+#### VPCInterfaceIPv4RangeCreateOptions
+
+
+
+VPCInterfaceIPv4RangeCreateOptions defines the IPv4 range for a VPC interface
+
+
+
+_Appears in:_
+- [VPCInterfaceIPv4CreateOptions](#vpcinterfaceipv4createoptions)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `range` _string_ | | | |
+
+
+#### VPCInterfaceIPv6CreateOptions
+
+
+
+VPCInterfaceIPv6CreateOptions defines the IPv6 configuration for a VPC interface
+
+
+
+_Appears in:_
+- [VPCInterfaceCreateOptions](#vpcinterfacecreateoptions)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `slaac` _[VPCInterfaceIPv6SLAACCreateOptions](#vpcinterfaceipv6slaaccreateoptions) array_ | | | |
+| `ranges` _[VPCInterfaceIPv6RangeCreateOptions](#vpcinterfaceipv6rangecreateoptions) array_ | | | |
+| `is_public` _boolean_ | | | |
+
+
+#### VPCInterfaceIPv6RangeCreateOptions
+
+
+
+VPCInterfaceIPv6RangeCreateOptions defines the IPv6 range for a VPC interface
+
+
+
+_Appears in:_
+- [VPCInterfaceIPv6CreateOptions](#vpcinterfaceipv6createoptions)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `range` _string_ | | | |
+
+
+#### VPCInterfaceIPv6SLAACCreateOptions
+
+
+
+VPCInterfaceIPv6SLAACCreateOptions defines the Range for IPv6 SLAAC
+
+
+
+_Appears in:_
+- [VPCInterfaceIPv6CreateOptions](#vpcinterfaceipv6createoptions)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `range` _string_ | | | |
+
+
#### VPCStatusError
_Underlying type:_ _string_
diff --git a/go.mod b/go.mod
index 85f5420d0..6a84bbe9e 100644
--- a/go.mod
+++ b/go.mod
@@ -15,7 +15,7 @@ require (
github.com/go-logr/logr v1.4.3
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
- github.com/linode/linodego v1.53.1-0.20250728194520-172cba1c457a
+ github.com/linode/linodego v1.54.1-0.20250812173013-ca1c9b03408c
github.com/onsi/ginkgo/v2 v2.23.4
github.com/onsi/gomega v1.38.0
github.com/stretchr/testify v1.10.0
diff --git a/go.sum b/go.sum
index bcc169f9f..bc0dfcc38 100644
--- a/go.sum
+++ b/go.sum
@@ -177,8 +177,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
-github.com/linode/linodego v1.53.1-0.20250728194520-172cba1c457a h1:5PaGcDTgxlOZOaYNChSKHnzZp4oKFvzqEn8TQ7hv2Pg=
-github.com/linode/linodego v1.53.1-0.20250728194520-172cba1c457a/go.mod h1:VHlFAbhj18634Cd7B7L5D723kFKFQMOxzIutSMcWsB4=
+github.com/linode/linodego v1.54.1-0.20250812173013-ca1c9b03408c h1:EnDv76oCGlC3YGbmtdLJD67GGCkbKT5rQf+bJSQ5X9A=
+github.com/linode/linodego v1.54.1-0.20250812173013-ca1c9b03408c/go.mod h1:VHlFAbhj18634Cd7B7L5D723kFKFQMOxzIutSMcWsB4=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
diff --git a/internal/controller/linodemachine_controller.go b/internal/controller/linodemachine_controller.go
index d4e36363b..84b1e02fe 100644
--- a/internal/controller/linodemachine_controller.go
+++ b/internal/controller/linodemachine_controller.go
@@ -274,7 +274,7 @@ func (r *LinodeMachineReconciler) reconcileCreate(
}
if machineScope.LinodeMachine.Spec.FirewallRef != nil {
- if !reconciler.ConditionTrue(machineScope.LinodeMachine, string(ConditionPreflightLinodeFirewallReady)) && machineScope.LinodeMachine.Spec.ProviderID == nil {
+ if !reconciler.ConditionTrue(machineScope.LinodeMachine, ConditionPreflightLinodeFirewallReady) && machineScope.LinodeMachine.Spec.ProviderID == nil {
res, err := r.reconcilePreflightLinodeFirewallCheck(ctx, logger, machineScope)
if err != nil || !res.IsZero() {
conditions.Set(machineScope.LinodeMachine, metav1.Condition{
@@ -619,32 +619,36 @@ func (r *LinodeMachineReconciler) reconcilePreflightConfigure(ctx context.Contex
return ctrl.Result{RequeueAfter: reconciler.DefaultMachineControllerWaitForRunningDelay}, nil
}
- configData := linodego.InstanceConfigUpdateOptions{
- Helpers: &linodego.InstanceConfigHelpers{
- Network: true,
- },
- }
-
+ configData := linodego.InstanceConfigUpdateOptions{}
if machineScope.LinodeMachine.Spec.Configuration != nil && machineScope.LinodeMachine.Spec.Configuration.Kernel != "" {
configData.Kernel = machineScope.LinodeMachine.Spec.Configuration.Kernel
}
-
+ // helpers.network does not work on instances using the new linode interfaces.
// 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,
+ if machineScope.LinodeMachine.Spec.InterfaceGeneration != linodego.GenerationLinode {
+ if machineScope.LinodeMachine.Spec.NetworkHelper != nil {
+ configData.Helpers = &linodego.InstanceConfigHelpers{
+ Network: *machineScope.LinodeMachine.Spec.NetworkHelper,
+ }
+ } else {
+ configData.Helpers = &linodego.InstanceConfigHelpers{
+ Network: true,
+ }
}
}
- 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)
+ // only update the instance configuration if there are changes
+ if configData.Kernel != "" || configData.Helpers != nil {
+ 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", "configuration", configData)
+ return retryIfTransient(err, logger)
+ }
}
conditions.Set(machineScope.LinodeMachine, metav1.Condition{
@@ -788,11 +792,32 @@ func (r *LinodeMachineReconciler) reconcileUpdate(ctx context.Context, logger lo
}
func (r *LinodeMachineReconciler) reconcileFirewallID(ctx context.Context, logger logr.Logger, machineScope *scope.MachineScope, instanceID int) (ctrl.Result, error) {
- // get the instance's firewalls
- firewalls, err := machineScope.LinodeClient.ListInstanceFirewalls(ctx, instanceID, nil)
- if err != nil {
- logger.Error(err, "Failed to list firewalls for Linode instance")
- return ctrl.Result{RequeueAfter: reconciler.DefaultMachineControllerWaitForRunningDelay}, nil
+ var (
+ firewalls []linodego.Firewall
+ err error
+ )
+ // Get the instance's firewalls normally if this is not using the new linode interfaces,
+ // otherwise we have to get firewalls per linode interface
+ if machineScope.LinodeMachine.Spec.InterfaceGeneration == linodego.GenerationLinode {
+ interfaces, err := machineScope.LinodeClient.ListInterfaces(ctx, instanceID, nil)
+ if err != nil {
+ logger.Error(err, "Failed to list interfaces for Linode instance")
+ return ctrl.Result{RequeueAfter: reconciler.DefaultMachineControllerWaitForRunningDelay}, nil
+ }
+ for _, iface := range interfaces {
+ ifaceFWs, err := machineScope.LinodeClient.ListInterfaceFirewalls(ctx, instanceID, iface.ID, nil)
+ if err != nil {
+ logger.Error(err, "Failed to list firewalls for Linode instance interface", "interfaceID", iface.ID)
+ return ctrl.Result{RequeueAfter: reconciler.DefaultMachineControllerWaitForRunningDelay}, nil
+ }
+ firewalls = append(firewalls, ifaceFWs...)
+ }
+ } else {
+ firewalls, err = machineScope.LinodeClient.ListInstanceFirewalls(ctx, instanceID, nil)
+ if err != nil {
+ logger.Error(err, "Failed to list firewalls for Linode instance")
+ return ctrl.Result{RequeueAfter: reconciler.DefaultMachineControllerWaitForRunningDelay}, nil
+ }
}
attachedFWIDs := make([]int, 0, len(firewalls))
diff --git a/internal/controller/linodemachine_controller_helpers.go b/internal/controller/linodemachine_controller_helpers.go
index 21f794974..805851b9b 100644
--- a/internal/controller/linodemachine_controller_helpers.go
+++ b/internal/controller/linodemachine_controller_helpers.go
@@ -81,9 +81,27 @@ func retryIfTransient(err error, logger logr.Logger) (ctrl.Result, error) {
}
func fillCreateConfig(createConfig *linodego.InstanceCreateOptions, machineScope *scope.MachineScope) {
+ // This will only be empty if no interfaces or linodeInterfaces were specified in the LinodeMachine spec.
+ // In that case we default to legacy interfaces.
+ switch createConfig.InterfaceGeneration {
+ case linodego.GenerationLinode:
+ // networkHelper is only applicable for Linode interfaces.
+ // legacy interfaces have nework helper configured in reconcilePreflightConfigure at the instance level.
+ if machineScope.LinodeMachine.Spec.NetworkHelper != nil {
+ createConfig.NetworkHelper = machineScope.LinodeMachine.Spec.NetworkHelper
+ } else {
+ createConfig.NetworkHelper = ptr.To(true)
+ }
+ case linodego.GenerationLegacyConfig:
+ createConfig.InterfaceGeneration = linodego.GenerationLegacyConfig
+ default:
+ createConfig.InterfaceGeneration = linodego.GenerationLegacyConfig
+ }
+
if machineScope.LinodeMachine.Spec.PrivateIP != nil {
createConfig.PrivateIP = *machineScope.LinodeMachine.Spec.PrivateIP
- } else {
+ } else if createConfig.InterfaceGeneration != linodego.GenerationLinode {
+ // Supported only for legacy network interfaces.
createConfig.PrivateIP = true
}
@@ -169,15 +187,29 @@ func configureVPCInterface(ctx context.Context, machineScope *scope.MachineScope
// addVPCInterfaceFromDirectID handles adding a VPC interface from a direct ID
func addVPCInterfaceFromDirectID(ctx context.Context, machineScope *scope.MachineScope, createConfig *linodego.InstanceCreateOptions, logger logr.Logger, vpcID int) error {
- iface, err := getVPCInterfaceConfigFromDirectID(ctx, machineScope, createConfig.Interfaces, logger, vpcID)
- if err != nil {
- logger.Error(err, "Failed to get VPC interface config from direct ID")
- return err
- }
+ switch {
+ case createConfig.LinodeInterfaces != nil || (createConfig.LinodeInterfaces == nil && machineScope.LinodeMachine.Spec.InterfaceGeneration == linodego.GenerationLinode):
+ iface, err := getVPCLinodeInterfaceConfigFromDirectID(ctx, machineScope, createConfig.LinodeInterfaces, logger, vpcID)
+ if err != nil {
+ logger.Error(err, "Failed to get VPC linode interface config from direct ID")
+ return err
+ }
- if iface != nil {
- // add VPC interface as first interface
- createConfig.Interfaces = slices.Insert(createConfig.Interfaces, 0, *iface)
+ if iface != nil {
+ // add VPC interface as first interface
+ createConfig.LinodeInterfaces = slices.Insert(createConfig.LinodeInterfaces, 0, *iface)
+ }
+ default:
+ iface, err := getVPCInterfaceConfigFromDirectID(ctx, machineScope, createConfig.Interfaces, logger, vpcID)
+ if err != nil {
+ logger.Error(err, "Failed to get VPC interface config from direct ID")
+ return err
+ }
+
+ if iface != nil {
+ // add VPC interface as first interface
+ createConfig.Interfaces = slices.Insert(createConfig.Interfaces, 0, *iface)
+ }
}
return nil
@@ -185,15 +217,29 @@ func addVPCInterfaceFromDirectID(ctx context.Context, machineScope *scope.Machin
// addVPCInterfaceFromReference handles adding a VPC interface from a reference
func addVPCInterfaceFromReference(ctx context.Context, machineScope *scope.MachineScope, createConfig *linodego.InstanceCreateOptions, logger logr.Logger, vpcRef *corev1.ObjectReference) error {
- iface, err := getVPCInterfaceConfig(ctx, machineScope, createConfig.Interfaces, logger, vpcRef)
- if err != nil {
- logger.Error(err, "Failed to get VPC interface config")
- return err
- }
+ switch {
+ case createConfig.LinodeInterfaces != nil || (createConfig.LinodeInterfaces == nil && machineScope.LinodeMachine.Spec.InterfaceGeneration == linodego.GenerationLinode):
+ iface, err := getVPCLinodeInterfaceConfig(ctx, machineScope, createConfig.LinodeInterfaces, logger, vpcRef)
+ if err != nil {
+ logger.Error(err, "Failed to get VPC interface config")
+ return err
+ }
+
+ if iface != nil {
+ // add VPC interface as first interface
+ createConfig.LinodeInterfaces = slices.Insert(createConfig.LinodeInterfaces, 0, *iface)
+ }
+ default:
+ iface, err := getVPCInterfaceConfig(ctx, machineScope, createConfig.Interfaces, logger, vpcRef)
+ if err != nil {
+ logger.Error(err, "Failed to get VPC interface config")
+ return err
+ }
- if iface != nil {
- // add VPC interface as first interface
- createConfig.Interfaces = slices.Insert(createConfig.Interfaces, 0, *iface)
+ if iface != nil {
+ // add VPC interface as first interface
+ createConfig.Interfaces = slices.Insert(createConfig.Interfaces, 0, *iface)
+ }
}
return nil
@@ -238,10 +284,49 @@ func buildInstanceAddrs(ctx context.Context, machineScope *scope.MachineScope, i
}
if machineScope.LinodeCluster.Spec.Network.UseVlan {
+ vlanIps, err := handleVlanIps(ctx, machineScope, instanceID)
+ if err != nil {
+ return nil, fmt.Errorf("handle vlan ips: %w", err)
+ }
+ ips = append(ips, vlanIps...)
+ }
+
+ // if a node has private ip, store it as well
+ // NOTE: We specifically store VPC ips first so that they are used first during
+ // bootstrap when we set `registrationMethod: internal-only-ips`
+ if len(addresses.IPv4.Private) != 0 {
+ ips = append(ips, clusterv1.MachineAddress{
+ Address: addresses.IPv4.Private[0].Address,
+ Type: clusterv1.MachineInternalIP,
+ })
+ }
+
+ return ips, nil
+}
+
+func handleVlanIps(ctx context.Context, machineScope *scope.MachineScope, instanceID int) ([]clusterv1.MachineAddress, error) {
+ ips := []clusterv1.MachineAddress{}
+ switch {
+ case machineScope.LinodeMachine.Spec.LinodeInterfaces != nil || (machineScope.LinodeMachine.Spec.LinodeInterfaces == nil && machineScope.LinodeMachine.Spec.InterfaceGeneration == linodego.GenerationLinode):
+ ifaces, err := machineScope.LinodeClient.ListInterfaces(ctx, instanceID, &linodego.ListOptions{})
+ if err != nil || len(ifaces) == 0 {
+ return ips, fmt.Errorf("list interfaces: %w", err)
+ }
+ // Iterate over interfaces in config and find VLAN specific ips
+ for _, iface := range ifaces {
+ if iface.VLAN != nil {
+ // vlan addresses have a /11 appended to them - we should strip it out.
+ ips = append(ips, clusterv1.MachineAddress{
+ Address: netip.MustParsePrefix(*iface.VLAN.IPAMAddress).Addr().String(),
+ Type: clusterv1.MachineInternalIP,
+ })
+ }
+ }
+ default:
// get the default instance config
configs, err := machineScope.LinodeClient.ListInstanceConfigs(ctx, instanceID, &linodego.ListOptions{})
if err != nil || len(configs) == 0 {
- return nil, fmt.Errorf("list instance configs: %w", err)
+ return ips, fmt.Errorf("list instance configs: %w", err)
}
// Iterate over interfaces in config and find VLAN specific ips
@@ -256,16 +341,6 @@ func buildInstanceAddrs(ctx context.Context, machineScope *scope.MachineScope, i
}
}
- // if a node has private ip, store it as well
- // NOTE: We specifically store VPC ips first so that they are used first during
- // bootstrap when we set `registrationMethod: internal-only-ips`
- if len(addresses.IPv4.Private) != 0 {
- ips = append(ips, clusterv1.MachineAddress{
- Address: addresses.IPv4.Private[0].Address,
- Type: clusterv1.MachineInternalIP,
- })
- }
-
return ips, nil
}
@@ -425,8 +500,33 @@ func getVlanInterfaceConfig(ctx context.Context, machineScope *scope.MachineScop
}, nil
}
-// getVPCInterfaceConfig returns the interface configuration for a VPC based on the provided VPC reference
-func getVPCInterfaceConfig(ctx context.Context, machineScope *scope.MachineScope, interfaces []linodego.InstanceConfigInterfaceCreateOptions, logger logr.Logger, vpcRef *corev1.ObjectReference) (*linodego.InstanceConfigInterfaceCreateOptions, error) {
+func getVlanLinodeInterfaceConfig(ctx context.Context, machineScope *scope.MachineScope, interfaces []linodego.LinodeInterfaceCreateOptions, logger logr.Logger) (*linodego.LinodeInterfaceCreateOptions, error) {
+ logger = logger.WithValues("vlanName", machineScope.Cluster.Name)
+
+ // Try to obtain a IP for the machine using its name
+ ip, err := util.GetNextVlanIP(ctx, machineScope.Cluster.Name, machineScope.Cluster.Namespace, machineScope.Client)
+ if err != nil {
+ return nil, fmt.Errorf("getting vlanIP: %w", err)
+ }
+
+ logger.Info("obtained IP for machine", "name", machineScope.LinodeMachine.Name, "ip", ip)
+
+ for i, netInterface := range interfaces {
+ if netInterface.VLAN != nil {
+ interfaces[i].VLAN.IPAMAddress = ptr.To(fmt.Sprintf(vlanIPFormat, ip))
+ return nil, nil //nolint:nilnil // it is important we don't return an interface if a VLAN interface already exists
+ }
+ }
+
+ return &linodego.LinodeInterfaceCreateOptions{
+ VLAN: &linodego.VLANInterface{
+ VLANLabel: machineScope.Cluster.Name,
+ IPAMAddress: ptr.To(fmt.Sprintf(vlanIPFormat, ip)),
+ },
+ }, nil
+}
+
+func getVPCFromRef(ctx context.Context, machineScope *scope.MachineScope, logger logr.Logger, vpcRef *corev1.ObjectReference) (*infrav1alpha2.LinodeVPC, error) {
// Get namespace from VPC ref or default to machine namespace
namespace := vpcRef.Namespace
if namespace == "" {
@@ -459,11 +559,21 @@ func getVPCInterfaceConfig(ctx context.Context, machineScope *scope.MachineScope
return nil, errors.New("failed to find subnet")
}
- var subnetID int
+ return &linodeVPC, nil
+}
- subnetName := machineScope.LinodeCluster.Spec.Network.SubnetName // name of subnet to use
+// getVPCInterfaceConfig returns the interface configuration for a VPC based on the provided VPC reference
+func getVPCInterfaceConfig(ctx context.Context, machineScope *scope.MachineScope, interfaces []linodego.InstanceConfigInterfaceCreateOptions, logger logr.Logger, vpcRef *corev1.ObjectReference) (*linodego.InstanceConfigInterfaceCreateOptions, error) {
+ linodeVPC, err := getVPCFromRef(ctx, machineScope, logger, vpcRef)
+ if err != nil {
+ return nil, err
+ }
- var ipv6Config *linodego.InstanceConfigInterfaceCreateOptionsIPv6
+ var (
+ ipv6Config *linodego.InstanceConfigInterfaceCreateOptionsIPv6
+ subnetID int
+ )
+ subnetName := machineScope.LinodeCluster.Spec.Network.SubnetName // name of subnet to use
if subnetName != "" {
for _, subnet := range linodeVPC.Spec.Subnets {
if subnet.Label == subnetName {
@@ -513,8 +623,75 @@ func getVPCInterfaceConfig(ctx context.Context, machineScope *scope.MachineScope
return vpcIntfCreateOpts, nil
}
-// getVPCInterfaceConfigFromDirectID returns the interface configuration for a VPC based on a direct VPC ID
-func getVPCInterfaceConfigFromDirectID(ctx context.Context, machineScope *scope.MachineScope, interfaces []linodego.InstanceConfigInterfaceCreateOptions, logger logr.Logger, vpcID int) (*linodego.InstanceConfigInterfaceCreateOptions, error) {
+func getVPCLinodeInterfaceConfig(ctx context.Context, machineScope *scope.MachineScope, linodeInterfaces []linodego.LinodeInterfaceCreateOptions, logger logr.Logger, vpcRef *corev1.ObjectReference) (*linodego.LinodeInterfaceCreateOptions, error) {
+ linodeVPC, err := getVPCFromRef(ctx, machineScope, logger, vpcRef)
+ if err != nil {
+ return nil, err
+ }
+
+ var (
+ ipv6Config *linodego.VPCInterfaceIPv6CreateOptions
+ subnetID int
+ )
+ subnetName := machineScope.LinodeCluster.Spec.Network.SubnetName // name of subnet to use
+ if subnetName != "" {
+ for _, subnet := range linodeVPC.Spec.Subnets {
+ if subnet.Label == subnetName {
+ subnetID = subnet.SubnetID
+ ipv6Config = getVPCLinodeInterfaceIPv6Config(machineScope, len(subnet.IPv6))
+ break
+ }
+ }
+
+ if subnetID == 0 {
+ logger.Info("Failed to fetch subnet ID for specified subnet name")
+ }
+ } else {
+ subnetID = linodeVPC.Spec.Subnets[0].SubnetID // get first subnet if nothing specified
+ ipv6Config = getVPCLinodeInterfaceIPv6Config(machineScope, len(linodeVPC.Spec.Subnets[0].IPv6))
+ }
+
+ if subnetID == 0 {
+ return nil, errors.New("failed to find subnet as subnet id set is 0")
+ }
+
+ // Check if a VPC interface already exists
+ for iface, netInterface := range linodeInterfaces {
+ if netInterface.VPC != nil {
+ linodeInterfaces[iface].VPC.SubnetID = subnetID
+ // If IPv6 range config is not empty, add it to the interface configuration
+ if !isVPCInterfaceIPv6ConfigEmpty(ipv6Config) {
+ linodeInterfaces[iface].VPC.IPv6 = ipv6Config
+ }
+ return nil, nil //nolint:nilnil // it is important we don't return an interface if a VPC interface already exists
+ }
+ }
+
+ // Create a new VPC interface
+ vpcIntfCreateOpts := &linodego.LinodeInterfaceCreateOptions{
+ VPC: &linodego.VPCInterfaceCreateOptions{
+ SubnetID: subnetID,
+ IPv4: &linodego.VPCInterfaceIPv4CreateOptions{
+ Addresses: []linodego.VPCInterfaceIPv4AddressCreateOptions{{
+ Primary: ptr.To(true),
+ NAT1To1Address: ptr.To("auto"),
+ Address: "auto",
+ }},
+ },
+ },
+ }
+
+ // If IPv6 config is not empty, add it to the interface configuration
+ if !isVPCInterfaceIPv6ConfigEmpty(ipv6Config) {
+ vpcIntfCreateOpts.VPC.IPv6 = ipv6Config
+ }
+
+ logger.Info("Creating LinodeInterfaceCreateOptions", "VPC", *vpcIntfCreateOpts)
+
+ return vpcIntfCreateOpts, nil
+}
+
+func getVPCFromID(ctx context.Context, machineScope *scope.MachineScope, logger logr.Logger, vpcID int) (*linodego.VPC, error) {
vpc, err := machineScope.LinodeClient.GetVPC(ctx, vpcID)
if err != nil {
logger.Error(err, "Failed to fetch VPC from Linode API", "vpcID", vpcID)
@@ -526,8 +703,89 @@ func getVPCInterfaceConfigFromDirectID(ctx context.Context, machineScope *scope.
return nil, errors.New("no subnets found in VPC")
}
- var subnetID int
- var subnetName string
+ return vpc, nil
+}
+
+// getVPCLinodeInterfaceConfigFromDirectID returns the linode interface configuration for a VPC based on a direct VPC ID
+func getVPCLinodeInterfaceConfigFromDirectID(ctx context.Context, machineScope *scope.MachineScope, linodeInterfaces []linodego.LinodeInterfaceCreateOptions, logger logr.Logger, vpcID int) (*linodego.LinodeInterfaceCreateOptions, error) {
+ vpc, err := getVPCFromID(ctx, machineScope, logger, vpcID)
+ if err != nil {
+ return nil, err
+ }
+
+ var (
+ subnetID int
+ subnetName string
+ ipv6Config *linodego.VPCInterfaceIPv6CreateOptions
+ )
+
+ // Safety check for when LinodeCluster is nil (e.g., when using direct VPCID without cluster)
+ if machineScope.LinodeCluster != nil && machineScope.LinodeCluster.Spec.Network.SubnetName != "" {
+ subnetName = machineScope.LinodeCluster.Spec.Network.SubnetName
+ }
+
+ // If subnet name specified, find matching subnet; otherwise use first subnet
+ if subnetName != "" {
+ for _, subnet := range vpc.Subnets {
+ if subnet.Label == subnetName {
+ subnetID = subnet.ID
+ ipv6Config = getVPCLinodeInterfaceIPv6Config(machineScope, len(subnet.IPv6))
+ break
+ }
+ }
+ if subnetID == 0 {
+ return nil, fmt.Errorf("subnet with label %s not found in VPC", subnetName)
+ }
+ } else {
+ subnetID = vpc.Subnets[0].ID
+ ipv6Config = getVPCLinodeInterfaceIPv6Config(machineScope, len(vpc.Subnets[0].IPv6))
+ }
+
+ // Check if a VPC interface already exists
+ for i, netInterface := range linodeInterfaces {
+ if netInterface.VPC != nil {
+ linodeInterfaces[i].VPC.SubnetID = subnetID
+ if !isVPCInterfaceIPv6ConfigEmpty(ipv6Config) {
+ linodeInterfaces[i].VPC.IPv6 = ipv6Config
+ }
+ return nil, nil //nolint:nilnil // it is important we don't return an interface if a VPC interface already exists
+ }
+ }
+
+ // Create a new VPC interface
+ vpcIntfCreateOpts := &linodego.LinodeInterfaceCreateOptions{
+ VPC: &linodego.VPCInterfaceCreateOptions{
+ SubnetID: subnetID,
+ IPv4: &linodego.VPCInterfaceIPv4CreateOptions{
+ Addresses: []linodego.VPCInterfaceIPv4AddressCreateOptions{{
+ Primary: ptr.To(true),
+ NAT1To1Address: ptr.To("auto"),
+ Address: "auto",
+ }},
+ },
+ },
+ }
+
+ // If IPv6 range config is not empty, add it to the interface configuration
+ if !isVPCInterfaceIPv6ConfigEmpty(ipv6Config) {
+ vpcIntfCreateOpts.VPC.IPv6 = ipv6Config
+ }
+
+ return vpcIntfCreateOpts, nil
+}
+
+// getVPCInterfaceConfigFromDirectID returns the interface configuration for a VPC based on a direct VPC ID
+func getVPCInterfaceConfigFromDirectID(ctx context.Context, machineScope *scope.MachineScope, interfaces []linodego.InstanceConfigInterfaceCreateOptions, logger logr.Logger, vpcID int) (*linodego.InstanceConfigInterfaceCreateOptions, error) {
+ vpc, err := getVPCFromID(ctx, machineScope, logger, vpcID)
+ if err != nil {
+ return nil, err
+ }
+
+ var (
+ subnetID int
+ subnetName string
+ ipv6Config *linodego.InstanceConfigInterfaceCreateOptionsIPv6
+ )
// Safety check for when LinodeCluster is nil (e.g., when using direct VPCID without cluster)
if machineScope.LinodeCluster != nil && machineScope.LinodeCluster.Spec.Network.SubnetName != "" {
@@ -535,7 +793,6 @@ func getVPCInterfaceConfigFromDirectID(ctx context.Context, machineScope *scope.
}
// If subnet name specified, find matching subnet; otherwise use first subnet
- var ipv6Config *linodego.InstanceConfigInterfaceCreateOptionsIPv6
if subnetName != "" {
for _, subnet := range vpc.Subnets {
if subnet.Label == subnetName {
@@ -589,6 +846,13 @@ func isIPv6ConfigEmpty(opts *linodego.InstanceConfigInterfaceCreateOptionsIPv6)
(opts.IsPublic == nil)
}
+func isVPCInterfaceIPv6ConfigEmpty(opts *linodego.VPCInterfaceIPv6CreateOptions) bool {
+ return opts == nil ||
+ len(opts.SLAAC) == 0 &&
+ len(opts.Ranges) == 0 &&
+ !opts.IsPublic
+}
+
// 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.
@@ -625,35 +889,209 @@ func getMachineIPv6Config(machineScope *scope.MachineScope, numIPv6RangesInSubne
return intfOpts
}
+// getVPCLinodeInterfaceIPv6Config 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 getVPCLinodeInterfaceIPv6Config(machineScope *scope.MachineScope, numIPv6RangesInSubnet int) *linodego.VPCInterfaceIPv6CreateOptions {
+ intfOpts := &linodego.VPCInterfaceIPv6CreateOptions{}
+
+ // 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
+ }
+
+ 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.VPCInterfaceIPv6SLAACCreateOptions{
+ {
+ Range: defaultNodeIPv6CIDRRange,
+ },
+ }
+ }
+ if machineScope.LinodeMachine.Spec.IPv6Options.EnableRanges != nil && *machineScope.LinodeMachine.Spec.IPv6Options.EnableRanges {
+ intfOpts.Ranges = []linodego.VPCInterfaceIPv6RangeCreateOptions{
+ {
+ Range: defaultNodeIPv6CIDRRange,
+ },
+ }
+ }
+
+ return intfOpts
+}
+
+// Unfortunately, this is necessary since DeepCopy can't be generated for linodego.LinodeInterfaceCreateOptions
+// so here we manually create the options for Linode interfaces.
+func constructLinodeInterfaceCreateOpts(createOpts []infrav1alpha2.LinodeInterfaceCreateOptions) []linodego.LinodeInterfaceCreateOptions {
+ linodeInterfaces := make([]linodego.LinodeInterfaceCreateOptions, len(createOpts))
+ for idx, iface := range createOpts {
+ ifaceCreateOpts := linodego.LinodeInterfaceCreateOptions{}
+ // Handle VLAN
+ if iface.VLAN != nil {
+ ifaceCreateOpts.VLAN = &linodego.VLANInterface{
+ VLANLabel: iface.VLAN.VLANLabel,
+ IPAMAddress: iface.VLAN.IPAMAddress,
+ }
+ }
+ // Handle VPC
+ if iface.VPC != nil {
+ ifaceCreateOpts.VPC = constructLinodeInterfaceVPC(iface)
+ }
+ // Handle Public Interface
+ if iface.Public != nil {
+ ifaceCreateOpts.Public = constructLinodeInterfacePublic(iface)
+ }
+ // Handle Default Route
+ if iface.DefaultRoute != nil {
+ ifaceCreateOpts.DefaultRoute = &linodego.InterfaceDefaultRoute{
+ IPv4: iface.DefaultRoute.IPv4,
+ IPv6: iface.DefaultRoute.IPv6,
+ }
+ }
+ ifaceCreateOpts.FirewallID = iface.FirewallID
+ // createOpts is now fully populated with the interface options
+ linodeInterfaces[idx] = ifaceCreateOpts
+ }
+
+ return linodeInterfaces
+}
+
+// constructLinodeInterfaceVPC constructs a Linode VPC interface configuration from the provided LinodeInterfaceCreateOptions.
+func constructLinodeInterfaceVPC(iface infrav1alpha2.LinodeInterfaceCreateOptions) *linodego.VPCInterfaceCreateOptions {
+ var (
+ ipv4Addrs []linodego.VPCInterfaceIPv4AddressCreateOptions
+ ipv4Ranges []linodego.VPCInterfaceIPv4RangeCreateOptions
+ ipv6Ranges []linodego.VPCInterfaceIPv6RangeCreateOptions
+ ipv6SLAAC []linodego.VPCInterfaceIPv6SLAACCreateOptions
+ ipv6IsPublic bool
+ )
+ if iface.VPC.IPv4 != nil {
+ for _, addr := range iface.VPC.IPv4.Addresses {
+ ipv4Addrs = append(ipv4Addrs, linodego.VPCInterfaceIPv4AddressCreateOptions{
+ Address: addr.Address,
+ Primary: addr.Primary,
+ NAT1To1Address: addr.NAT1To1Address,
+ })
+ }
+ for _, rng := range iface.VPC.IPv4.Ranges {
+ ipv4Ranges = append(ipv4Ranges, linodego.VPCInterfaceIPv4RangeCreateOptions{
+ Range: rng.Range,
+ })
+ }
+ } else {
+ // If no IPv4 addresses are specified, we set a default NAT1To1 address to "any"
+ ipv4Addrs = []linodego.VPCInterfaceIPv4AddressCreateOptions{
+ {
+ Primary: ptr.To(true),
+ NAT1To1Address: ptr.To("auto"),
+ Address: "auto", // Default to auto-assigned address
+ },
+ }
+ }
+ if iface.VPC.IPv6 != nil {
+ for _, slaac := range iface.VPC.IPv6.SLAAC {
+ ipv6SLAAC = append(ipv6SLAAC, linodego.VPCInterfaceIPv6SLAACCreateOptions{
+ Range: slaac.Range,
+ })
+ }
+ for _, rng := range iface.VPC.IPv6.Ranges {
+ ipv6Ranges = append(ipv6Ranges, linodego.VPCInterfaceIPv6RangeCreateOptions{
+ Range: rng.Range,
+ })
+ }
+ ipv6IsPublic = iface.VPC.IPv6.IsPublic
+ }
+ return &linodego.VPCInterfaceCreateOptions{
+ SubnetID: iface.VPC.SubnetID,
+ IPv4: &linodego.VPCInterfaceIPv4CreateOptions{
+ Addresses: ipv4Addrs,
+ Ranges: ipv4Ranges,
+ },
+ IPv6: &linodego.VPCInterfaceIPv6CreateOptions{
+ SLAAC: ipv6SLAAC,
+ Ranges: ipv6Ranges,
+ IsPublic: ipv6IsPublic,
+ },
+ }
+}
+
+// constructLinodeInterfacePublic constructs a Linode Public interface configuration from the provided LinodeInterfaceCreateOptions.
+func constructLinodeInterfacePublic(iface infrav1alpha2.LinodeInterfaceCreateOptions) *linodego.PublicInterfaceCreateOptions {
+ var (
+ ipv4Addrs []linodego.PublicInterfaceIPv4AddressCreateOptions
+ ipv6Ranges []linodego.PublicInterfaceIPv6RangeCreateOptions
+ )
+ if iface.Public.IPv4 != nil {
+ for _, addr := range iface.Public.IPv4.Addresses {
+ ipv4Addrs = append(ipv4Addrs, linodego.PublicInterfaceIPv4AddressCreateOptions{
+ Address: addr.Address,
+ Primary: addr.Primary,
+ })
+ }
+ }
+ if iface.Public.IPv6 != nil {
+ for _, rng := range iface.Public.IPv6.Ranges {
+ ipv6Ranges = append(ipv6Ranges, linodego.PublicInterfaceIPv6RangeCreateOptions{
+ Range: rng.Range,
+ })
+ }
+ }
+ return &linodego.PublicInterfaceCreateOptions{
+ IPv4: &linodego.PublicInterfaceIPv4CreateOptions{
+ Addresses: ipv4Addrs,
+ },
+ IPv6: &linodego.PublicInterfaceIPv6CreateOptions{
+ Ranges: ipv6Ranges,
+ },
+ }
+}
+
+// For converting LinodeMachineSpec to linodego.InstanceCreateOptions. Any defaulting should be done in fillCreateConfig instead
func linodeMachineSpecToInstanceCreateConfig(machineSpec infrav1alpha2.LinodeMachineSpec, machineTags []string) *linodego.InstanceCreateOptions {
- interfaces := make([]linodego.InstanceConfigInterfaceCreateOptions, len(machineSpec.Interfaces))
- for idx, iface := range machineSpec.Interfaces {
- interfaces[idx] = linodego.InstanceConfigInterfaceCreateOptions{
- IPAMAddress: iface.IPAMAddress,
- Label: iface.Label,
- Purpose: iface.Purpose,
- Primary: iface.Primary,
- SubnetID: iface.SubnetID,
- IPRanges: iface.IPRanges,
- }
- }
- privateIP := false
+ instCreateOpts := &linodego.InstanceCreateOptions{
+ Region: machineSpec.Region,
+ Type: machineSpec.Type,
+ AuthorizedKeys: machineSpec.AuthorizedKeys,
+ AuthorizedUsers: machineSpec.AuthorizedUsers,
+ RootPass: machineSpec.RootPass,
+ Image: machineSpec.Image,
+ Tags: machineTags,
+ FirewallID: machineSpec.FirewallID,
+ InterfaceGeneration: machineSpec.InterfaceGeneration,
+ DiskEncryption: linodego.InstanceDiskEncryption(machineSpec.DiskEncryption),
+ }
+
if machineSpec.PrivateIP != nil {
- privateIP = *machineSpec.PrivateIP
- }
- return &linodego.InstanceCreateOptions{
- Region: machineSpec.Region,
- Type: machineSpec.Type,
- AuthorizedKeys: machineSpec.AuthorizedKeys,
- AuthorizedUsers: machineSpec.AuthorizedUsers,
- RootPass: machineSpec.RootPass,
- Image: machineSpec.Image,
- Interfaces: interfaces,
- PrivateIP: privateIP,
- Tags: machineTags,
- FirewallID: machineSpec.FirewallID,
- DiskEncryption: linodego.InstanceDiskEncryption(machineSpec.DiskEncryption),
+ instCreateOpts.PrivateIP = *machineSpec.PrivateIP
+ }
+
+ if len(machineSpec.LinodeInterfaces) > 0 {
+ instCreateOpts.LinodeInterfaces = constructLinodeInterfaceCreateOpts(machineSpec.LinodeInterfaces)
+ // If LinodeInterfaces are specified, the InterfaceGeneration must be GenerationLinode
+ instCreateOpts.InterfaceGeneration = linodego.GenerationLinode
+ } else if len(machineSpec.Interfaces) > 0 {
+ interfaces := make([]linodego.InstanceConfigInterfaceCreateOptions, len(machineSpec.Interfaces))
+ for idx, iface := range machineSpec.Interfaces {
+ interfaces[idx] = linodego.InstanceConfigInterfaceCreateOptions{
+ IPAMAddress: iface.IPAMAddress,
+ Label: iface.Label,
+ Purpose: iface.Purpose,
+ Primary: iface.Primary,
+ SubnetID: iface.SubnetID,
+ IPRanges: iface.IPRanges,
+ }
+ }
+ instCreateOpts.Interfaces = interfaces
+ // If Interfaces are specified, the InterfaceGeneration must be GenerationLegacyConfig
+ instCreateOpts.InterfaceGeneration = linodego.GenerationLegacyConfig
}
+
+ return instCreateOpts
}
func compressUserData(bootstrapData []byte) ([]byte, error) {
@@ -1016,15 +1454,29 @@ func getVPCRefFromScope(machineScope *scope.MachineScope) *corev1.ObjectReferenc
// configureVlanInterface adds a VLAN interface to the configuration
func configureVlanInterface(ctx context.Context, machineScope *scope.MachineScope, createConfig *linodego.InstanceCreateOptions, logger logr.Logger) error {
- iface, err := getVlanInterfaceConfig(ctx, machineScope, createConfig.Interfaces, logger)
- if err != nil {
- logger.Error(err, "Failed to get VLAN interface config")
- return err
- }
+ switch {
+ case createConfig.LinodeInterfaces != nil || (createConfig.LinodeInterfaces == nil && machineScope.LinodeMachine.Spec.InterfaceGeneration == linodego.GenerationLinode):
+ iface, err := getVlanLinodeInterfaceConfig(ctx, machineScope, createConfig.LinodeInterfaces, logger)
+ if err != nil {
+ logger.Error(err, "Failed to get VLAN interface config")
+ return err
+ }
- if iface != nil {
- // add VLAN interface as first interface
- createConfig.Interfaces = slices.Insert(createConfig.Interfaces, 0, *iface)
+ if iface != nil {
+ // add VLAN interface as first interface
+ createConfig.LinodeInterfaces = slices.Insert(createConfig.LinodeInterfaces, 0, *iface)
+ }
+ default:
+ iface, err := getVlanInterfaceConfig(ctx, machineScope, createConfig.Interfaces, logger)
+ if err != nil {
+ logger.Error(err, "Failed to get VLAN interface config")
+ return err
+ }
+
+ if iface != nil {
+ // add VLAN interface as first interface
+ createConfig.Interfaces = slices.Insert(createConfig.Interfaces, 0, *iface)
+ }
}
return nil
@@ -1063,6 +1515,12 @@ func configureFirewall(ctx context.Context, machineScope *scope.MachineScope, cr
}
createConfig.FirewallID = fwID
+
+ // If using LinodeInterfaces that needs to know about the firewall ID
+ for i := range createConfig.LinodeInterfaces {
+ createConfig.LinodeInterfaces[i].FirewallID = ptr.To(fwID)
+ }
+
return nil
}
diff --git a/internal/controller/linodemachine_controller_helpers_test.go b/internal/controller/linodemachine_controller_helpers_test.go
index e02f93529..09144a04c 100644
--- a/internal/controller/linodemachine_controller_helpers_test.go
+++ b/internal/controller/linodemachine_controller_helpers_test.go
@@ -390,11 +390,12 @@ func validateInterfaceExpectations(
t *testing.T,
err error,
iface *linodego.InstanceConfigInterfaceCreateOptions,
+ linodeIface *linodego.LinodeInterfaceCreateOptions,
expectErr bool,
expectErrMsg string,
expectInterface bool,
+ expectLinodeInterface bool,
expectSubnetID int,
- interfaces interface{},
) {
t.Helper()
@@ -423,6 +424,22 @@ func validateInterfaceExpectations(
} else {
require.Nil(t, iface)
}
+ if expectLinodeInterface {
+ require.NotNil(t, linodeIface)
+ require.NotNil(t, linodeIface.VPC)
+ if linodeIface.VPC.IPv6 != nil && linodeIface.VPC.IPv6.SLAAC != nil {
+ require.Equal(t, defaultNodeIPv6CIDRRange, linodeIface.VPC.IPv6.SLAAC[0].Range)
+ } else if linodeIface.VPC.IPv6 != nil && linodeIface.VPC.IPv6.Ranges != nil {
+ require.Equal(t, defaultNodeIPv6CIDRRange, linodeIface.VPC.IPv6.Ranges[0].Range)
+ }
+ require.NotNil(t, linodeIface.VPC.SubnetID)
+ require.Equal(t, expectSubnetID, linodeIface.VPC.SubnetID)
+ require.NotNil(t, linodeIface.VPC.IPv4)
+ require.NotNil(t, linodeIface.VPC.IPv4.Addresses[0].NAT1To1Address)
+ require.Equal(t, "auto", *linodeIface.VPC.IPv4.Addresses[0].NAT1To1Address)
+ } else {
+ require.Nil(t, linodeIface)
+ }
}
func TestGetVPCInterfaceConfigFromDirectID(t *testing.T) {
@@ -629,7 +646,7 @@ func TestGetVPCInterfaceConfigFromDirectID(t *testing.T) {
iface, err := getVPCInterfaceConfigFromDirectID(ctx, machineScope, tc.interfaces, logger, tc.vpcID)
// Check expectations
- validateInterfaceExpectations(t, err, iface, tc.expectErr, tc.expectErrMsg, tc.expectInterface, tc.expectSubnetID, tc.interfaces)
+ validateInterfaceExpectations(t, err, iface, nil, tc.expectErr, tc.expectErrMsg, tc.expectInterface, false, tc.expectSubnetID)
// Additional check for interface updates
if !tc.expectErr && !tc.expectInterface && len(tc.interfaces) > 0 && tc.interfaces[0].Purpose == linodego.InterfacePurposeVPC {
@@ -640,19 +657,251 @@ func TestGetVPCInterfaceConfigFromDirectID(t *testing.T) {
}
}
+func TestGetVPCLinodeInterfaceConfigFromDirectID(t *testing.T) {
+ t.Parallel()
+
+ // Setup test cases
+ testCases := []struct {
+ name string
+ vpcID int
+ linodeInterfaces []linodego.LinodeInterfaceCreateOptions
+ subnetName string
+ mockSetup func(mockLinodeClient *mock.MockLinodeClient)
+ expectErr bool
+ expectErrMsg string
+ expectLinodeInterface bool
+ expectSubnetID int
+ }{
+ {
+ name: "Success - Valid VPC with subnets, no subnet name",
+ vpcID: 123,
+ linodeInterfaces: []linodego.LinodeInterfaceCreateOptions{},
+ 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",
+ },
+ },
+ }, nil)
+ },
+ expectErr: false,
+ expectLinodeInterface: true,
+ expectSubnetID: 456, // First subnet ID
+ },
+ {
+ name: "Success - Valid VPC with subnets, specific subnet name",
+ vpcID: 123,
+ linodeInterfaces: []linodego.LinodeInterfaceCreateOptions{},
+ 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",
+ },
+ },
+ }, nil)
+ },
+ expectErr: false,
+ expectLinodeInterface: true,
+ expectSubnetID: 789, // Matching subnet ID
+ },
+ {
+ name: "Success - Valid VPC with subnets and ipv6 ranges, specific subnet name",
+ vpcID: 123,
+ linodeInterfaces: []linodego.LinodeInterfaceCreateOptions{},
+ 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,
+ expectLinodeInterface: true,
+ expectSubnetID: 789, // Matching subnet ID
+ },
+ {
+ name: "Success - VPC interface already exists",
+ vpcID: 123,
+ linodeInterfaces: []linodego.LinodeInterfaceCreateOptions{{VPC: &linodego.VPCInterfaceCreateOptions{}}},
+ mockSetup: func(mockLinodeClient *mock.MockLinodeClient) {
+ mockLinodeClient.EXPECT().GetVPC(gomock.Any(), 123).Return(&linodego.VPC{
+ ID: 123,
+ Subnets: []linodego.VPCSubnet{
+ {
+ ID: 456,
+ Label: "subnet-1",
+ IPv6: []linodego.VPCIPv6Range{
+ {
+ Range: "2001:0db8::/56",
+ },
+ },
+ },
+ },
+ }, nil)
+ },
+ expectErr: false,
+ expectLinodeInterface: false,
+ expectSubnetID: 456,
+ },
+ {
+ name: "Error - VPC does not exist",
+ vpcID: 999,
+ linodeInterfaces: []linodego.LinodeInterfaceCreateOptions{},
+ mockSetup: func(mockLinodeClient *mock.MockLinodeClient) {
+ mockLinodeClient.EXPECT().GetVPC(gomock.Any(), 999).Return(nil, fmt.Errorf("VPC not found"))
+ },
+ expectErr: true,
+ expectErrMsg: "VPC not found",
+ expectLinodeInterface: false,
+ },
+ {
+ name: "Error - VPC has no subnets",
+ vpcID: 123,
+ linodeInterfaces: []linodego.LinodeInterfaceCreateOptions{},
+ mockSetup: func(mockLinodeClient *mock.MockLinodeClient) {
+ mockLinodeClient.EXPECT().GetVPC(gomock.Any(), 123).Return(&linodego.VPC{
+ ID: 123,
+ Subnets: []linodego.VPCSubnet{},
+ }, nil)
+ },
+ expectErr: true,
+ expectErrMsg: "no subnets found in VPC",
+ expectLinodeInterface: false,
+ },
+ {
+ name: "Error - Subnet name not found",
+ vpcID: 123,
+ linodeInterfaces: []linodego.LinodeInterfaceCreateOptions{},
+ subnetName: "non-existent",
+ mockSetup: func(mockLinodeClient *mock.MockLinodeClient) {
+ mockLinodeClient.EXPECT().GetVPC(gomock.Any(), 123).Return(&linodego.VPC{
+ ID: 123,
+ Subnets: []linodego.VPCSubnet{
+ {
+ ID: 456,
+ Label: "subnet-1",
+ },
+ },
+ }, nil)
+ },
+ expectErr: true,
+ expectErrMsg: "subnet with label non-existent not found in VPC",
+ expectLinodeInterface: false,
+ },
+ }
+
+ for _, tc := range testCases {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ // Setup mock controller and client
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ mockLinodeClient := mock.NewMockLinodeClient(ctrl)
+ mockK8sClient := mock.NewMockK8sClient(ctrl)
+
+ tc.mockSetup(mockLinodeClient)
+
+ // Create test context
+ ctx := t.Context()
+ logger := testr.New(t)
+
+ // Create machine scope
+ machineScope := &scope.MachineScope{
+ LinodeClient: mockLinodeClient,
+ Client: mockK8sClient,
+ LinodeMachine: &infrav1alpha2.LinodeMachine{
+ Spec: infrav1alpha2.LinodeMachineSpec{},
+ },
+ LinodeCluster: &infrav1alpha2.LinodeCluster{
+ Spec: infrav1alpha2.LinodeClusterSpec{
+ Network: infrav1alpha2.NetworkSpec{
+ SubnetName: tc.subnetName,
+ },
+ },
+ },
+ }
+
+ // Call the function being tested
+ linodeIface, err := getVPCLinodeInterfaceConfigFromDirectID(ctx, machineScope, tc.linodeInterfaces, logger, tc.vpcID)
+
+ // Check expectations
+ validateInterfaceExpectations(t, err, nil, linodeIface, tc.expectErr, tc.expectErrMsg, false, tc.expectLinodeInterface, tc.expectSubnetID)
+
+ // Additional check for interface updates
+ if !tc.expectErr && !tc.expectLinodeInterface && len(tc.linodeInterfaces) > 0 && tc.linodeInterfaces[0].VPC != nil {
+ require.NotNil(t, tc.linodeInterfaces[0].VPC.SubnetID)
+ require.Equal(t, tc.expectSubnetID, tc.linodeInterfaces[0].VPC.SubnetID)
+ }
+ })
+ }
+}
+
func TestAddVPCInterfaceFromDirectID(t *testing.T) {
t.Parallel()
// Setup test cases
testCases := []struct {
- name string
- vpcID int
- createConfig *linodego.InstanceCreateOptions
- mockSetup func(mockLinodeClient *mock.MockLinodeClient)
- expectErr bool
- expectErrMsg string
- expectNoIface bool
+ name string
+ vpcID int
+ createConfig *linodego.InstanceCreateOptions
+ mockSetup func(mockLinodeClient *mock.MockLinodeClient)
+ expectErr bool
+ expectErrMsg string
+ expectNoIface bool
+ expectNoLinodeIface bool
}{
+ {
+ name: "Success - Interface added correctly with new network interfaces",
+ vpcID: 123,
+ createConfig: &linodego.InstanceCreateOptions{
+ LinodeInterfaces: []linodego.LinodeInterfaceCreateOptions{},
+ },
+ mockSetup: func(mockLinodeClient *mock.MockLinodeClient) {
+ mockLinodeClient.EXPECT().GetVPC(gomock.Any(), 123).Return(&linodego.VPC{
+ ID: 123,
+ Subnets: []linodego.VPCSubnet{
+ {
+ ID: 456,
+ Label: "subnet-1",
+ },
+ },
+ }, nil)
+ },
+ expectErr: false,
+ expectNoIface: true,
+ },
{
name: "Success - Interface added correctly",
vpcID: 123,
@@ -670,7 +919,8 @@ func TestAddVPCInterfaceFromDirectID(t *testing.T) {
},
}, nil)
},
- expectErr: false,
+ expectErr: false,
+ expectNoLinodeIface: true,
},
{
name: "Error - getVPCInterfaceConfigFromDirectID returns error",
@@ -684,6 +934,18 @@ func TestAddVPCInterfaceFromDirectID(t *testing.T) {
expectErr: true,
expectErrMsg: "VPC not found",
},
+ {
+ name: "Error - getVPCInterfaceConfigFromDirectID returns error with new network interfaces",
+ vpcID: 999,
+ createConfig: &linodego.InstanceCreateOptions{
+ LinodeInterfaces: []linodego.LinodeInterfaceCreateOptions{},
+ },
+ mockSetup: func(mockLinodeClient *mock.MockLinodeClient) {
+ mockLinodeClient.EXPECT().GetVPC(gomock.Any(), 999).Return(nil, fmt.Errorf("VPC not found"))
+ },
+ expectErr: true,
+ expectErrMsg: "VPC not found",
+ },
{
name: "Success - Interface already exists",
vpcID: 123,
@@ -705,8 +967,34 @@ func TestAddVPCInterfaceFromDirectID(t *testing.T) {
},
}, nil)
},
- expectErr: false,
- expectNoIface: true,
+ expectErr: false,
+ expectNoIface: true,
+ expectNoLinodeIface: true,
+ },
+ {
+ name: "Success - Interface already exists with new network interfaces",
+ vpcID: 123,
+ createConfig: &linodego.InstanceCreateOptions{
+ LinodeInterfaces: []linodego.LinodeInterfaceCreateOptions{
+ {
+ VPC: &linodego.VPCInterfaceCreateOptions{},
+ },
+ },
+ },
+ mockSetup: func(mockLinodeClient *mock.MockLinodeClient) {
+ mockLinodeClient.EXPECT().GetVPC(gomock.Any(), 123).Return(&linodego.VPC{
+ ID: 123,
+ Subnets: []linodego.VPCSubnet{
+ {
+ ID: 456,
+ Label: "subnet-1",
+ },
+ },
+ }, nil)
+ },
+ expectErr: false,
+ expectNoIface: true,
+ expectNoLinodeIface: true,
},
}
@@ -735,13 +1023,12 @@ func TestAddVPCInterfaceFromDirectID(t *testing.T) {
LinodeMachine: &infrav1alpha2.LinodeMachine{
Spec: infrav1alpha2.LinodeMachineSpec{},
},
- LinodeCluster: &infrav1alpha2.LinodeCluster{
- Spec: infrav1alpha2.LinodeClusterSpec{},
- },
+ LinodeCluster: &infrav1alpha2.LinodeCluster{},
}
// Store original interface count
- originalCount := len(tc.createConfig.Interfaces)
+ originalIfaceCount := len(tc.createConfig.Interfaces)
+ originalLinodeIfaceCount := len(tc.createConfig.LinodeInterfaces)
// Call the function being tested
err := addVPCInterfaceFromDirectID(ctx, machineScope, tc.createConfig, logger, tc.vpcID)
@@ -752,12 +1039,22 @@ func TestAddVPCInterfaceFromDirectID(t *testing.T) {
require.Contains(t, err.Error(), tc.expectErrMsg)
} else {
require.NoError(t, err)
- if tc.expectNoIface {
+ switch {
+ case tc.expectNoLinodeIface:
+ // If Linode interface already existed, count should remain the same
+ require.Len(t, tc.createConfig.LinodeInterfaces, originalLinodeIfaceCount)
+ default:
+ // If Linode interface was added, count should increase
+ require.Len(t, tc.createConfig.LinodeInterfaces, originalLinodeIfaceCount+1)
+ require.NotNil(t, tc.createConfig.LinodeInterfaces[0].VPC)
+ }
+ switch {
+ case tc.expectNoIface:
// If interface already existed, count should remain the same
- require.Len(t, tc.createConfig.Interfaces, originalCount)
- } else {
+ require.Len(t, tc.createConfig.Interfaces, originalIfaceCount)
+ default:
// If interface was added, count should increase
- require.Len(t, tc.createConfig.Interfaces, originalCount+1)
+ require.Len(t, tc.createConfig.Interfaces, originalIfaceCount+1)
require.Equal(t, linodego.InterfacePurposeVPC, tc.createConfig.Interfaces[0].Purpose)
require.True(t, tc.createConfig.Interfaces[0].Primary)
}
@@ -778,15 +1075,16 @@ func TestConfigureVPCInterface(t *testing.T) {
// Setup test cases
testCases := []struct {
- name string
- machineVPCID *int
- clusterVPCID *int
- vpcRef *corev1.ObjectReference
- createConfig *linodego.InstanceCreateOptions
- mockSetup func(mockLinodeClient *mock.MockLinodeClient, mockK8sClient *mock.MockK8sClient)
- expectErr bool
- expectErrMsg string
- expectInterface bool
+ name string
+ machineVPCID *int
+ clusterVPCID *int
+ vpcRef *corev1.ObjectReference
+ createConfig *linodego.InstanceCreateOptions
+ mockSetup func(mockLinodeClient *mock.MockLinodeClient, mockK8sClient *mock.MockK8sClient)
+ expectErr bool
+ expectErrMsg string
+ expectInterface bool
+ expectLinodeInterface bool
}{
{
name: "Success - VPCID on machine",
@@ -813,6 +1111,31 @@ func TestConfigureVPCInterface(t *testing.T) {
expectErr: false,
expectInterface: true,
},
+ {
+ name: "Success - VPCID on machine with new network interfaces",
+ machineVPCID: ptr.To(123),
+ createConfig: &linodego.InstanceCreateOptions{
+ LinodeInterfaces: []linodego.LinodeInterfaceCreateOptions{},
+ },
+ mockSetup: func(mockLinodeClient *mock.MockLinodeClient, mockK8sClient *mock.MockK8sClient) {
+ mockLinodeClient.EXPECT().GetVPC(gomock.Any(), 123).Return(&linodego.VPC{
+ ID: 123,
+ Subnets: []linodego.VPCSubnet{
+ {
+ ID: 456,
+ Label: "subnet-1",
+ IPv6: []linodego.VPCIPv6Range{
+ {
+ Range: "2001:0db8::/56",
+ },
+ },
+ },
+ },
+ }, nil)
+ },
+ expectErr: false,
+ expectLinodeInterface: true,
+ },
{
name: "Success - VPCID on cluster",
clusterVPCID: ptr.To(123),
@@ -833,6 +1156,26 @@ func TestConfigureVPCInterface(t *testing.T) {
expectErr: false,
expectInterface: true,
},
+ {
+ name: "Success - VPCID on cluster with new network interfaces",
+ clusterVPCID: ptr.To(123),
+ createConfig: &linodego.InstanceCreateOptions{
+ LinodeInterfaces: []linodego.LinodeInterfaceCreateOptions{},
+ },
+ mockSetup: func(mockLinodeClient *mock.MockLinodeClient, mockK8sClient *mock.MockK8sClient) {
+ mockLinodeClient.EXPECT().GetVPC(gomock.Any(), 123).Return(&linodego.VPC{
+ ID: 123,
+ Subnets: []linodego.VPCSubnet{
+ {
+ ID: 456,
+ Label: "subnet-1",
+ },
+ },
+ }, nil)
+ },
+ expectErr: false,
+ expectLinodeInterface: true,
+ },
{
name: "Success - VPC reference",
vpcRef: vpcRef,
@@ -859,16 +1202,52 @@ func TestConfigureVPCInterface(t *testing.T) {
expectInterface: true,
},
{
- name: "Success - No VPC configuration",
+ name: "Success - VPC reference with new network interfaces",
+ vpcRef: vpcRef,
createConfig: &linodego.InstanceCreateOptions{
- Interfaces: []linodego.InstanceConfigInterfaceCreateOptions{},
+ LinodeInterfaces: []linodego.LinodeInterfaceCreateOptions{},
},
mockSetup: func(mockLinodeClient *mock.MockLinodeClient, mockK8sClient *mock.MockK8sClient) {
- // No expectations needed
- },
- expectErr: false,
+ mockK8sClient.EXPECT().Get(gomock.Any(), client.ObjectKey{
+ Name: "test-vpc",
+ Namespace: "default",
+ }, gomock.Any()).DoAndReturn(func(_ context.Context, _ client.ObjectKey, vpc *infrav1alpha2.LinodeVPC, _ ...client.GetOption) error {
+ vpc.Status.Ready = true
+ vpc.Spec.VPCID = ptr.To(123)
+ vpc.Spec.Subnets = []infrav1alpha2.VPCSubnetCreateOptions{
+ {
+ SubnetID: subnetID,
+ Label: "subnet-1",
+ },
+ }
+ return nil
+ })
+ },
+ expectErr: false,
+ expectLinodeInterface: true,
+ },
+ {
+ name: "Success - No VPC configuration",
+ createConfig: &linodego.InstanceCreateOptions{
+ Interfaces: []linodego.InstanceConfigInterfaceCreateOptions{},
+ },
+ mockSetup: func(mockLinodeClient *mock.MockLinodeClient, mockK8sClient *mock.MockK8sClient) {
+ // No expectations needed
+ },
+ expectErr: false,
expectInterface: false,
},
+ {
+ name: "Success - No VPC configuration with new network interfaces",
+ createConfig: &linodego.InstanceCreateOptions{
+ LinodeInterfaces: []linodego.LinodeInterfaceCreateOptions{},
+ },
+ mockSetup: func(mockLinodeClient *mock.MockLinodeClient, mockK8sClient *mock.MockK8sClient) {
+ // No expectations needed
+ },
+ expectErr: false,
+ expectLinodeInterface: false,
+ },
{
name: "Error - VPCID on machine, VPC not found",
machineVPCID: ptr.To(999),
@@ -881,6 +1260,18 @@ func TestConfigureVPCInterface(t *testing.T) {
expectErr: true,
expectErrMsg: "VPC not found",
},
+ {
+ name: "Error - VPCID on machine, VPC not found with new network interfaces",
+ machineVPCID: ptr.To(999),
+ createConfig: &linodego.InstanceCreateOptions{
+ LinodeInterfaces: []linodego.LinodeInterfaceCreateOptions{},
+ },
+ mockSetup: func(mockLinodeClient *mock.MockLinodeClient, mockK8sClient *mock.MockK8sClient) {
+ mockLinodeClient.EXPECT().GetVPC(gomock.Any(), 999).Return(nil, fmt.Errorf("VPC not found"))
+ },
+ expectErr: true,
+ expectErrMsg: "VPC not found",
+ },
{
name: "Error - VPCID on cluster, VPC not found",
clusterVPCID: ptr.To(999),
@@ -893,6 +1284,18 @@ func TestConfigureVPCInterface(t *testing.T) {
expectErr: true,
expectErrMsg: "VPC not found",
},
+ {
+ name: "Error - VPCID on cluster, VPC not found with new network interfaces",
+ clusterVPCID: ptr.To(999),
+ createConfig: &linodego.InstanceCreateOptions{
+ LinodeInterfaces: []linodego.LinodeInterfaceCreateOptions{},
+ },
+ mockSetup: func(mockLinodeClient *mock.MockLinodeClient, mockK8sClient *mock.MockK8sClient) {
+ mockLinodeClient.EXPECT().GetVPC(gomock.Any(), 999).Return(nil, fmt.Errorf("VPC not found"))
+ },
+ expectErr: true,
+ expectErrMsg: "VPC not found",
+ },
}
for _, tc := range testCases {
@@ -932,7 +1335,8 @@ func TestConfigureVPCInterface(t *testing.T) {
}
// Store original interface count
- originalCount := len(tc.createConfig.Interfaces)
+ originalIfaceCount := len(tc.createConfig.Interfaces)
+ originalLinodeIfaceCount := len(tc.createConfig.LinodeInterfaces)
// Call the function being tested
err := configureVPCInterface(ctx, machineScope, tc.createConfig, logger)
@@ -943,13 +1347,24 @@ func TestConfigureVPCInterface(t *testing.T) {
require.Contains(t, err.Error(), tc.expectErrMsg)
} else {
require.NoError(t, err)
- if tc.expectInterface {
+ switch {
+ case tc.expectLinodeInterface:
+ // If Linode interface was added, count should increase
+ require.Len(t, tc.createConfig.LinodeInterfaces, originalLinodeIfaceCount+1)
+ require.NotNil(t, tc.createConfig.LinodeInterfaces[0].VPC)
+ default:
+ // If no Linode interface was added, count should remain the same
+ require.Len(t, tc.createConfig.LinodeInterfaces, originalLinodeIfaceCount)
+ }
+ switch {
+ case tc.expectInterface:
// If interface was added, count should increase
- require.Len(t, tc.createConfig.Interfaces, originalCount+1)
+ require.Len(t, tc.createConfig.Interfaces, originalIfaceCount+1)
require.Equal(t, linodego.InterfacePurposeVPC, tc.createConfig.Interfaces[0].Purpose)
- } else {
+ require.True(t, tc.createConfig.Interfaces[0].Primary)
+ default:
// If no interface was added, count should remain the same
- require.Len(t, tc.createConfig.Interfaces, originalCount)
+ require.Len(t, tc.createConfig.Interfaces, originalIfaceCount)
}
}
})
@@ -1227,7 +1642,7 @@ func TestGetVPCInterfaceConfig(t *testing.T) {
iface, err := getVPCInterfaceConfig(ctx, machineScope, tc.interfaces, logger, tc.vpcRef)
// Check expectations
- validateInterfaceExpectations(t, err, iface, tc.expectErr, tc.expectErrMsg, tc.expectInterface, tc.expectSubnetID, tc.interfaces)
+ validateInterfaceExpectations(t, err, iface, nil, tc.expectErr, tc.expectErrMsg, tc.expectInterface, false, tc.expectSubnetID)
// Additional check for interface updates
if !tc.expectErr && !tc.expectInterface && len(tc.interfaces) > 0 && tc.interfaces[0].Purpose == linodego.InterfacePurposeVPC {
@@ -1238,6 +1653,288 @@ func TestGetVPCInterfaceConfig(t *testing.T) {
}
}
+func TestGetVPCLinodeInterfaceConfig(t *testing.T) {
+ t.Parallel()
+
+ // Setup test cases
+ testCases := []struct {
+ name string
+ vpcRef *corev1.ObjectReference
+ linodeInterfaces []linodego.LinodeInterfaceCreateOptions
+ subnetName string
+ mockSetup func(mockK8sClient *mock.MockK8sClient)
+ expectErr bool
+ expectErrMsg string
+ expectLinodeInterface bool
+ expectSubnetID int
+ }{
+ {
+ name: "Success - Finding VPC with default namespace",
+ vpcRef: &corev1.ObjectReference{
+ Name: "test-vpc",
+ },
+ linodeInterfaces: []linodego.LinodeInterfaceCreateOptions{},
+ mockSetup: func(mockK8sClient *mock.MockK8sClient) {
+ mockK8sClient.EXPECT().Get(gomock.Any(), client.ObjectKey{
+ Name: "test-vpc",
+ Namespace: "default", // Default namespace
+ }, gomock.Any()).DoAndReturn(func(_ context.Context, _ client.ObjectKey, vpc *infrav1alpha2.LinodeVPC, _ ...client.GetOption) error {
+ vpc.Status.Ready = true
+ vpc.Spec.VPCID = ptr.To(123)
+ vpc.Spec.Subnets = []infrav1alpha2.VPCSubnetCreateOptions{
+ {
+ SubnetID: 456,
+ Label: "subnet-1",
+ IPv6: []linodego.VPCIPv6Range{
+ {
+ Range: "2001:0db8::/56",
+ },
+ },
+ },
+ }
+ return nil
+ })
+ },
+ expectErr: false,
+ expectLinodeInterface: true,
+ expectSubnetID: 456, // First subnet ID
+ },
+ {
+ name: "Success - Finding VPC with specific namespace",
+ vpcRef: &corev1.ObjectReference{
+ Name: "test-vpc",
+ Namespace: "custom-namespace",
+ },
+ linodeInterfaces: []linodego.LinodeInterfaceCreateOptions{},
+ mockSetup: func(mockK8sClient *mock.MockK8sClient) {
+ mockK8sClient.EXPECT().Get(gomock.Any(), client.ObjectKey{
+ Name: "test-vpc",
+ Namespace: "custom-namespace",
+ }, gomock.Any()).DoAndReturn(func(_ context.Context, _ client.ObjectKey, vpc *infrav1alpha2.LinodeVPC, _ ...client.GetOption) error {
+ vpc.Status.Ready = true
+ vpc.Spec.VPCID = ptr.To(123)
+ vpc.Spec.Subnets = []infrav1alpha2.VPCSubnetCreateOptions{
+ {
+ SubnetID: 456,
+ Label: "subnet-1",
+ },
+ }
+ return nil
+ })
+ },
+ expectErr: false,
+ expectLinodeInterface: true,
+ expectSubnetID: 456,
+ },
+ {
+ name: "Success - With subnet name specified and found",
+ vpcRef: &corev1.ObjectReference{
+ Name: "test-vpc",
+ },
+ subnetName: "subnet-2",
+ linodeInterfaces: []linodego.LinodeInterfaceCreateOptions{},
+ mockSetup: func(mockK8sClient *mock.MockK8sClient) {
+ mockK8sClient.EXPECT().Get(gomock.Any(), client.ObjectKey{
+ Name: "test-vpc",
+ Namespace: "default",
+ }, gomock.Any()).DoAndReturn(func(_ context.Context, _ client.ObjectKey, vpc *infrav1alpha2.LinodeVPC, _ ...client.GetOption) error {
+ vpc.Status.Ready = true
+ vpc.Spec.VPCID = ptr.To(123)
+ vpc.Spec.Subnets = []infrav1alpha2.VPCSubnetCreateOptions{
+ {
+ SubnetID: 456,
+ Label: "subnet-1",
+ },
+ {
+ SubnetID: 789,
+ Label: "subnet-2",
+ },
+ }
+ return nil
+ })
+ },
+ expectErr: false,
+ expectLinodeInterface: true,
+ expectSubnetID: 789,
+ },
+ {
+ name: "Success - VPC interface already exists",
+ vpcRef: &corev1.ObjectReference{
+ Name: "test-vpc",
+ },
+ linodeInterfaces: []linodego.LinodeInterfaceCreateOptions{
+ {
+ VPC: &linodego.VPCInterfaceCreateOptions{},
+ },
+ },
+ mockSetup: func(mockK8sClient *mock.MockK8sClient) {
+ mockK8sClient.EXPECT().Get(gomock.Any(), client.ObjectKey{
+ Name: "test-vpc",
+ Namespace: "default",
+ }, gomock.Any()).DoAndReturn(func(_ context.Context, _ client.ObjectKey, vpc *infrav1alpha2.LinodeVPC, _ ...client.GetOption) error {
+ vpc.Status.Ready = true
+ vpc.Spec.VPCID = ptr.To(123)
+ vpc.Spec.Subnets = []infrav1alpha2.VPCSubnetCreateOptions{
+ {
+ SubnetID: 456,
+ Label: "subnet-1",
+ IPv6: []linodego.VPCIPv6Range{
+ {
+ Range: "2001:0db8::/56",
+ },
+ },
+ },
+ }
+ return nil
+ })
+ },
+ expectErr: false,
+ expectLinodeInterface: false,
+ expectSubnetID: 456,
+ },
+ {
+ name: "Error - Failed to fetch LinodeVPC",
+ vpcRef: &corev1.ObjectReference{
+ Name: "nonexistent-vpc",
+ },
+ linodeInterfaces: []linodego.LinodeInterfaceCreateOptions{},
+ mockSetup: func(mockK8sClient *mock.MockK8sClient) {
+ mockK8sClient.EXPECT().Get(gomock.Any(), client.ObjectKey{
+ Name: "nonexistent-vpc",
+ Namespace: "default",
+ }, gomock.Any()).Return(fmt.Errorf("vpc not found"))
+ },
+ expectErr: true,
+ expectErrMsg: "vpc not found",
+ expectLinodeInterface: false,
+ },
+ {
+ name: "Error - VPC is not ready",
+ vpcRef: &corev1.ObjectReference{
+ Name: "test-vpc",
+ },
+ linodeInterfaces: []linodego.LinodeInterfaceCreateOptions{},
+ mockSetup: func(mockK8sClient *mock.MockK8sClient) {
+ mockK8sClient.EXPECT().Get(gomock.Any(), client.ObjectKey{
+ Name: "test-vpc",
+ Namespace: "default",
+ }, gomock.Any()).DoAndReturn(func(_ context.Context, _ client.ObjectKey, vpc *infrav1alpha2.LinodeVPC, _ ...client.GetOption) error {
+ vpc.Status.Ready = false
+ vpc.Spec.VPCID = ptr.To(123)
+ return nil
+ })
+ },
+ expectErr: true,
+ expectErrMsg: "vpc is not available",
+ expectLinodeInterface: false,
+ },
+ {
+ name: "Error - VPC has no subnets",
+ vpcRef: &corev1.ObjectReference{
+ Name: "test-vpc",
+ },
+ linodeInterfaces: []linodego.LinodeInterfaceCreateOptions{},
+ mockSetup: func(mockK8sClient *mock.MockK8sClient) {
+ mockK8sClient.EXPECT().Get(gomock.Any(), client.ObjectKey{
+ Name: "test-vpc",
+ Namespace: "default",
+ }, gomock.Any()).DoAndReturn(func(_ context.Context, _ client.ObjectKey, vpc *infrav1alpha2.LinodeVPC, _ ...client.GetOption) error {
+ vpc.Status.Ready = true
+ vpc.Spec.VPCID = ptr.To(123)
+ vpc.Spec.Subnets = []infrav1alpha2.VPCSubnetCreateOptions{}
+ return nil
+ })
+ },
+ expectErr: true,
+ expectErrMsg: "failed to find subnet",
+ expectLinodeInterface: false,
+ },
+ {
+ name: "Error - Subnet name not found",
+ vpcRef: &corev1.ObjectReference{
+ Name: "test-vpc",
+ },
+ subnetName: "nonexistent-subnet",
+ linodeInterfaces: []linodego.LinodeInterfaceCreateOptions{},
+ mockSetup: func(mockK8sClient *mock.MockK8sClient) {
+ mockK8sClient.EXPECT().Get(gomock.Any(), client.ObjectKey{
+ Name: "test-vpc",
+ Namespace: "default",
+ }, gomock.Any()).DoAndReturn(func(_ context.Context, _ client.ObjectKey, vpc *infrav1alpha2.LinodeVPC, _ ...client.GetOption) error {
+ vpc.Status.Ready = true
+ vpc.Spec.VPCID = ptr.To(123)
+ vpc.Spec.Subnets = []infrav1alpha2.VPCSubnetCreateOptions{
+ {
+ SubnetID: 456,
+ Label: "subnet-1",
+ },
+ }
+ return nil
+ })
+ },
+ expectErr: true,
+ expectErrMsg: "failed to find subnet as subnet id set is 0",
+ expectLinodeInterface: false,
+ },
+ }
+
+ for _, tc := range testCases {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ // Setup mock controller and client
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ mockK8sClient := mock.NewMockK8sClient(ctrl)
+ mockLinodeClient := mock.NewMockLinodeClient(ctrl)
+
+ tc.mockSetup(mockK8sClient)
+
+ // Create test context
+ ctx := t.Context()
+ logger := testr.New(t)
+
+ // Create machine scope
+ machineScope := &scope.MachineScope{
+ LinodeClient: mockLinodeClient,
+ Client: mockK8sClient,
+ LinodeMachine: &infrav1alpha2.LinodeMachine{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: "default",
+ },
+ Spec: infrav1alpha2.LinodeMachineSpec{
+ IPv6Options: &infrav1alpha2.IPv6CreateOptions{
+ EnableSLAAC: ptr.To(true),
+ IsPublicIPv6: ptr.To(true),
+ },
+ },
+ },
+ LinodeCluster: &infrav1alpha2.LinodeCluster{
+ Spec: infrav1alpha2.LinodeClusterSpec{
+ Network: infrav1alpha2.NetworkSpec{
+ SubnetName: tc.subnetName,
+ },
+ },
+ },
+ }
+
+ // Call the function being tested
+ linodeIface, err := getVPCLinodeInterfaceConfig(ctx, machineScope, tc.linodeInterfaces, logger, tc.vpcRef)
+
+ // Check expectations
+ validateInterfaceExpectations(t, err, nil, linodeIface, tc.expectErr, tc.expectErrMsg, false, tc.expectLinodeInterface, tc.expectSubnetID)
+
+ // Additional check for interface updates
+ if !tc.expectErr && !tc.expectLinodeInterface && len(tc.linodeInterfaces) > 0 && tc.linodeInterfaces[0].VPC != nil {
+ require.NotNil(t, tc.linodeInterfaces[0].VPC.SubnetID)
+ require.Equal(t, tc.expectSubnetID, tc.linodeInterfaces[0].VPC.SubnetID)
+ }
+ })
+ }
+}
+
func TestGetTags(t *testing.T) {
t.Parallel()
diff --git a/internal/controller/linodemachine_controller_test.go b/internal/controller/linodemachine_controller_test.go
index 0a8a0ad3c..c59bd7a2e 100644
--- a/internal/controller/linodemachine_controller_test.go
+++ b/internal/controller/linodemachine_controller_test.go
@@ -2370,6 +2370,7 @@ var _ = Describe("machine in VPC", Label("machine", "VPC"), Ordered, func() {
Primary: true,
},
},
+ InterfaceGeneration: linodego.GenerationLegacyConfig,
},
}
mockLinodeClient := mock.NewMockLinodeClient(mockCtrl)
@@ -2571,6 +2572,329 @@ var _ = Describe("machine in VPC", Label("machine", "VPC"), Ordered, func() {
})
})
+var _ = Describe("machine in VPC with new network interfaces", Label("machine", "newNetworkInterfaces", "VPC"), Ordered, func() {
+ var machine clusterv1.Machine
+ var secret corev1.Secret
+ var lvpcReconciler *LinodeVPCReconciler
+ var linodeVPC infrav1alpha2.LinodeVPC
+
+ var mockCtrl *gomock.Controller
+ var testLogs *bytes.Buffer
+ var logger logr.Logger
+
+ cluster := clusterv1.Cluster{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "mock",
+ Namespace: defaultNamespace,
+ },
+ }
+
+ linodeCluster := infrav1alpha2.LinodeCluster{
+ Spec: infrav1alpha2.LinodeClusterSpec{
+ Region: "us-ord",
+ Network: infrav1alpha2.NetworkSpec{
+ LoadBalancerType: "dns",
+ DNSRootDomain: "lkedevs.net",
+ DNSUniqueIdentifier: "abc123",
+ DNSTTLSec: 30,
+ SubnetName: "test",
+ },
+ VPCRef: &corev1.ObjectReference{
+ Namespace: "default",
+ Kind: "LinodeVPC",
+ Name: "test-cluster",
+ },
+ },
+ }
+
+ recorder := record.NewFakeRecorder(10)
+
+ BeforeEach(func(ctx SpecContext) {
+ secret = corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "bootstrap-secret",
+ Namespace: defaultNamespace,
+ },
+ Data: map[string][]byte{
+ "value": []byte("userdata"),
+ },
+ }
+ Expect(k8sClient.Create(ctx, &secret)).To(Succeed())
+
+ machine = clusterv1.Machine{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: defaultNamespace,
+ Labels: make(map[string]string),
+ },
+ Spec: clusterv1.MachineSpec{
+ Bootstrap: clusterv1.Bootstrap{
+ DataSecretName: ptr.To("bootstrap-secret"),
+ },
+ },
+ }
+
+ linodeVPC = infrav1alpha2.LinodeVPC{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-cluster",
+ Namespace: defaultNamespace,
+ UID: "5123122",
+ },
+ Spec: infrav1alpha2.LinodeVPCSpec{
+ VPCID: ptr.To(1),
+ Region: "us-ord",
+ Subnets: []infrav1alpha2.VPCSubnetCreateOptions{
+ {
+ IPv4: "10.0.0.0/8",
+ SubnetID: 1,
+ Label: "test",
+ },
+ },
+ },
+ Status: infrav1alpha2.LinodeVPCStatus{
+ Ready: true,
+ },
+ }
+ Expect(k8sClient.Create(ctx, &linodeVPC)).To(Succeed())
+
+ lvpcReconciler = &LinodeVPCReconciler{
+ Recorder: recorder,
+ Client: k8sClient,
+ }
+
+ mockCtrl = gomock.NewController(GinkgoT())
+ testLogs = &bytes.Buffer{}
+ logger = zap.New(
+ zap.WriteTo(GinkgoWriter),
+ zap.WriteTo(testLogs),
+ zap.UseDevMode(true),
+ )
+ })
+
+ AfterEach(func(ctx SpecContext) {
+ Expect(k8sClient.Delete(ctx, &secret)).To(Succeed())
+ var currentVPC infrav1alpha2.LinodeVPC
+ Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(&linodeVPC), ¤tVPC)).To(Succeed())
+ currentVPC.Finalizers = nil
+
+ Expect(k8sClient.Update(ctx, ¤tVPC)).To(Succeed())
+
+ Expect(k8sClient.Delete(ctx, ¤tVPC)).To(Succeed())
+
+ mockCtrl.Finish()
+ for len(recorder.Events) > 0 {
+ <-recorder.Events
+ }
+ })
+
+ It("creates a instance with vpc", func(ctx SpecContext) {
+ linodeMachine := infrav1alpha2.LinodeMachine{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "mock",
+ Namespace: defaultNamespace,
+ UID: "12345",
+ },
+ Spec: infrav1alpha2.LinodeMachineSpec{
+ ProviderID: ptr.To("linode://0"),
+ Type: "g6-nanode-1",
+ InterfaceGeneration: linodego.GenerationLinode,
+ },
+ }
+ mockLinodeClient := mock.NewMockLinodeClient(mockCtrl)
+ mockLinodeClient.EXPECT().
+ ListVPCs(ctx, gomock.Any()).
+ Return([]linodego.VPC{}, nil)
+ mockLinodeClient.EXPECT().
+ CreateVPC(ctx, gomock.Any()).
+ Return(&linodego.VPC{ID: 1, Subnets: []linodego.VPCSubnet{{
+ ID: 1,
+ Label: "test",
+ IPv4: "10.0.0.0/24",
+ }}}, nil)
+ helper, err := patch.NewHelper(&linodeVPC, k8sClient)
+ Expect(err).NotTo(HaveOccurred())
+
+ _, err = lvpcReconciler.reconcile(ctx, logger, &scope.VPCScope{
+ PatchHelper: helper,
+ Client: k8sClient,
+ LinodeClient: mockLinodeClient,
+ LinodeVPC: &linodeVPC,
+ })
+
+ Expect(err).NotTo(HaveOccurred())
+
+ mScope := scope.MachineScope{
+ Client: k8sClient,
+ LinodeClient: mockLinodeClient,
+ Cluster: &cluster,
+ Machine: &machine,
+ LinodeCluster: &linodeCluster,
+ LinodeMachine: &linodeMachine,
+ }
+
+ patchHelper, err := patch.NewHelper(mScope.LinodeMachine, k8sClient)
+ Expect(err).NotTo(HaveOccurred())
+ mScope.PatchHelper = patchHelper
+
+ createOpts, err := newCreateConfig(ctx, &mScope, gzipCompressionFlag, logger)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(createOpts).NotTo(BeNil())
+ Expect(createOpts.LinodeInterfaces).To(Equal([]linodego.LinodeInterfaceCreateOptions{
+ {
+ VPC: &linodego.VPCInterfaceCreateOptions{
+ SubnetID: 1,
+ IPv4: &linodego.VPCInterfaceIPv4CreateOptions{
+ Addresses: []linodego.VPCInterfaceIPv4AddressCreateOptions{{
+ NAT1To1Address: ptr.To("auto"),
+ Primary: ptr.To(true),
+ Address: "auto",
+ }},
+ },
+ },
+ },
+ }))
+ })
+ It("creates a instance with pre defined vpc interface", func(ctx SpecContext) {
+ linodeMachine := infrav1alpha2.LinodeMachine{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "mock",
+ Namespace: defaultNamespace,
+ UID: "12345",
+ },
+ Spec: infrav1alpha2.LinodeMachineSpec{
+ ProviderID: ptr.To("linode://0"),
+ Type: "g6-nanode-1",
+ InterfaceGeneration: linodego.GenerationLinode,
+ },
+ }
+ mockLinodeClient := mock.NewMockLinodeClient(mockCtrl)
+ mockLinodeClient.EXPECT().
+ ListVPCs(ctx, gomock.Any()).
+ Return([]linodego.VPC{}, nil)
+ mockLinodeClient.EXPECT().
+ CreateVPC(ctx, gomock.Any()).
+ Return(&linodego.VPC{ID: 1, Subnets: []linodego.VPCSubnet{{
+ ID: 1,
+ Label: "test",
+ IPv4: "10.0.0.0/24",
+ }}}, nil)
+ helper, err := patch.NewHelper(&linodeVPC, k8sClient)
+ Expect(err).NotTo(HaveOccurred())
+
+ _, err = lvpcReconciler.reconcile(ctx, logger, &scope.VPCScope{
+ PatchHelper: helper,
+ Client: k8sClient,
+ LinodeClient: mockLinodeClient,
+ LinodeVPC: &linodeVPC,
+ })
+
+ Expect(err).NotTo(HaveOccurred())
+
+ mScope := scope.MachineScope{
+ Client: k8sClient,
+ LinodeClient: mockLinodeClient,
+ Cluster: &cluster,
+ Machine: &machine,
+ LinodeCluster: &linodeCluster,
+ LinodeMachine: &linodeMachine,
+ }
+
+ patchHelper, err := patch.NewHelper(mScope.LinodeMachine, k8sClient)
+ Expect(err).NotTo(HaveOccurred())
+ mScope.PatchHelper = patchHelper
+
+ createOpts, err := newCreateConfig(ctx, &mScope, gzipCompressionFlag, logger)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(createOpts).NotTo(BeNil())
+ Expect(createOpts.LinodeInterfaces).To(Equal([]linodego.LinodeInterfaceCreateOptions{
+ {
+ VPC: &linodego.VPCInterfaceCreateOptions{
+ SubnetID: 1,
+ IPv4: &linodego.VPCInterfaceIPv4CreateOptions{
+ Addresses: []linodego.VPCInterfaceIPv4AddressCreateOptions{{
+ NAT1To1Address: ptr.To("auto"),
+ Primary: ptr.To(true),
+ Address: "auto",
+ }},
+ },
+ },
+ },
+ }))
+ })
+ It("creates an instance with vpc with a specific subnet", func(ctx SpecContext) {
+ linodeMachine := infrav1alpha2.LinodeMachine{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "mock",
+ Namespace: defaultNamespace,
+ UID: "12345",
+ },
+ Spec: infrav1alpha2.LinodeMachineSpec{
+ ProviderID: ptr.To("linode://0"),
+ Type: "g6-nanode-1",
+ InterfaceGeneration: linodego.GenerationLinode,
+ },
+ }
+ mockLinodeClient := mock.NewMockLinodeClient(mockCtrl)
+ mockLinodeClient.EXPECT().
+ ListVPCs(ctx, gomock.Any()).
+ Return([]linodego.VPC{}, nil)
+ mockLinodeClient.EXPECT().
+ CreateVPC(ctx, gomock.Any()).
+ Return(&linodego.VPC{ID: 1, Subnets: []linodego.VPCSubnet{{
+ ID: 1,
+ Label: "primary",
+ IPv4: "192.16.0.0/24",
+ },
+ {
+ ID: 27,
+ Label: "test",
+ IPv4: "10.0.0.0/24",
+ },
+ }}, nil)
+ helper, err := patch.NewHelper(&linodeVPC, k8sClient)
+ Expect(err).NotTo(HaveOccurred())
+
+ _, err = lvpcReconciler.reconcile(ctx, logger, &scope.VPCScope{
+ PatchHelper: helper,
+ Client: k8sClient,
+ LinodeClient: mockLinodeClient,
+ LinodeVPC: &linodeVPC,
+ })
+
+ Expect(err).NotTo(HaveOccurred())
+
+ mScope := scope.MachineScope{
+ Client: k8sClient,
+ LinodeClient: mockLinodeClient,
+ Cluster: &cluster,
+ Machine: &machine,
+ LinodeCluster: &linodeCluster,
+ LinodeMachine: &linodeMachine,
+ }
+
+ patchHelper, err := patch.NewHelper(mScope.LinodeMachine, k8sClient)
+ Expect(err).NotTo(HaveOccurred())
+ mScope.PatchHelper = patchHelper
+
+ createOpts, err := newCreateConfig(ctx, &mScope, gzipCompressionFlag, logger)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(createOpts).NotTo(BeNil())
+ Expect(createOpts.LinodeInterfaces).To(Equal([]linodego.LinodeInterfaceCreateOptions{
+ {
+ VPC: &linodego.VPCInterfaceCreateOptions{
+ SubnetID: 27,
+ IPv4: &linodego.VPCInterfaceIPv4CreateOptions{
+ Addresses: []linodego.VPCInterfaceIPv4AddressCreateOptions{{
+ NAT1To1Address: ptr.To("auto"),
+ Primary: ptr.To(true),
+ Address: "auto",
+ }},
+ },
+ },
+ },
+ }))
+ })
+})
+
var _ = Describe("machine in vlan", Label("machine", "vlan"), Ordered, func() {
var machine clusterv1.Machine
var secret corev1.Secret
@@ -2634,14 +2958,196 @@ var _ = Describe("machine in vlan", Label("machine", "vlan"), Ordered, func() {
Type: "g6-nanode-1",
Image: rutil.DefaultMachineControllerLinodeImage,
DiskEncryption: string(linodego.InstanceDiskEncryptionEnabled),
- Interfaces: []infrav1alpha2.InstanceConfigInterfaceCreateOptions{
- {
- Purpose: linodego.InterfacePurposePublic,
- },
- {
- Purpose: linodego.InterfacePurposeVLAN,
- },
- },
+ Interfaces: []infrav1alpha2.InstanceConfigInterfaceCreateOptions{
+ {
+ Purpose: linodego.InterfacePurposePublic,
+ },
+ {
+ Purpose: linodego.InterfacePurposeVLAN,
+ },
+ },
+ },
+ }
+
+ mockCtrl = gomock.NewController(GinkgoT())
+ testLogs = &bytes.Buffer{}
+ logger = zap.New(
+ zap.WriteTo(GinkgoWriter),
+ zap.WriteTo(testLogs),
+ zap.UseDevMode(true),
+ )
+ reconciler = &LinodeMachineReconciler{
+ Recorder: recorder,
+ }
+ })
+
+ AfterEach(func(ctx SpecContext) {
+ Expect(k8sClient.Delete(ctx, &secret)).To(Succeed())
+
+ mockCtrl.Finish()
+ for len(recorder.Events) > 0 {
+ <-recorder.Events
+ }
+ })
+
+ It("creates an instance with vlan", func(ctx SpecContext) {
+ mockLinodeClient := mock.NewMockLinodeClient(mockCtrl)
+ getRegion := mockLinodeClient.EXPECT().
+ GetRegion(ctx, gomock.Any()).
+ Return(&linodego.Region{Capabilities: []string{linodego.CapabilityMetadata, linodego.CapabilityDiskEncryption}}, nil)
+ getImage := mockLinodeClient.EXPECT().
+ GetImage(ctx, gomock.Any()).
+ After(getRegion).
+ Return(&linodego.Image{Capabilities: []string{"cloud-init"}}, nil)
+ createInst := mockLinodeClient.EXPECT().
+ CreateInstance(ctx, gomock.Any()).
+ After(getImage).
+ Return(&linodego.Instance{
+ ID: 123,
+ IPv4: []*net.IP{ptr.To(net.IPv4(192, 168, 0, 2))},
+ IPv6: "fd00::",
+ Status: linodego.InstanceOffline,
+ }, nil)
+ 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).
+ Return(nil)
+ getAddrs := mockLinodeClient.EXPECT().
+ GetInstanceIPAddresses(ctx, 123).
+ After(bootInst).
+ Return(&linodego.InstanceIPAddressResponse{
+ IPv4: &linodego.InstanceIPv4Response{
+ Private: []*linodego.InstanceIP{{Address: "192.168.0.2"}},
+ Public: []*linodego.InstanceIP{{Address: "172.0.0.2"}},
+ VPC: []*linodego.VPCIP{},
+ },
+ IPv6: &linodego.InstanceIPv6Response{
+ SLAAC: &linodego.InstanceIP{
+ Address: "fd00::",
+ },
+ },
+ }, nil)
+ mockLinodeClient.EXPECT().
+ ListInstanceConfigs(ctx, 123, gomock.Any()).
+ After(getAddrs).
+ Return([]linodego.InstanceConfig{{
+ Interfaces: []linodego.InstanceConfigInterface{
+ {
+ Purpose: linodego.InterfacePurposePublic,
+ },
+ {
+ Purpose: linodego.InterfacePurposeVLAN,
+ IPAMAddress: "10.0.0.2/11",
+ },
+ },
+ }}, nil)
+
+ mScope := scope.MachineScope{
+ Client: k8sClient,
+ LinodeClient: mockLinodeClient,
+ Cluster: &cluster,
+ Machine: &machine,
+ LinodeCluster: &linodeCluster,
+ LinodeMachine: &linodeMachine,
+ }
+
+ patchHelper, err := patch.NewHelper(mScope.LinodeMachine, k8sClient)
+ Expect(err).NotTo(HaveOccurred())
+ mScope.PatchHelper = patchHelper
+
+ _, err = reconciler.reconcileCreate(ctx, logger, &mScope)
+ Expect(err).NotTo(HaveOccurred())
+ _, err = reconciler.reconcileCreate(ctx, logger, &mScope)
+ Expect(err).NotTo(HaveOccurred())
+
+ Expect(rutil.ConditionTrue(&linodeMachine, ConditionPreflightMetadataSupportConfigured)).To(BeTrue())
+ Expect(rutil.ConditionTrue(&linodeMachine, ConditionPreflightCreated)).To(BeTrue())
+ Expect(rutil.ConditionTrue(&linodeMachine, ConditionPreflightConfigured)).To(BeTrue())
+ Expect(rutil.ConditionTrue(&linodeMachine, ConditionPreflightBootTriggered)).To(BeTrue())
+ Expect(rutil.ConditionTrue(&linodeMachine, ConditionPreflightReady)).To(BeTrue())
+ })
+})
+
+var _ = Describe("machine in vlan for new network interfaces", Label("machine", "newNetworkInterfaces", "vlan"), Ordered, func() {
+ var machine clusterv1.Machine
+ var secret corev1.Secret
+
+ var mockCtrl *gomock.Controller
+ var testLogs *bytes.Buffer
+ var logger logr.Logger
+
+ var reconciler *LinodeMachineReconciler
+ var linodeMachine infrav1alpha2.LinodeMachine
+
+ cluster := clusterv1.Cluster{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "mock",
+ Namespace: defaultNamespace,
+ },
+ }
+
+ linodeCluster := infrav1alpha2.LinodeCluster{
+ Spec: infrav1alpha2.LinodeClusterSpec{
+ Region: "us-ord",
+ Network: infrav1alpha2.NetworkSpec{
+ UseVlan: true,
+ },
+ },
+ }
+
+ recorder := record.NewFakeRecorder(10)
+
+ BeforeEach(func(ctx SpecContext) {
+ secret = corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "bootstrap-secret",
+ Namespace: defaultNamespace,
+ },
+ Data: map[string][]byte{
+ "value": []byte("userdata"),
+ },
+ }
+ Expect(k8sClient.Create(ctx, &secret)).To(Succeed())
+
+ machine = clusterv1.Machine{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: defaultNamespace,
+ Labels: make(map[string]string),
+ },
+ Spec: clusterv1.MachineSpec{
+ Bootstrap: clusterv1.Bootstrap{
+ DataSecretName: ptr.To("bootstrap-secret"),
+ },
+ },
+ }
+
+ linodeMachine = infrav1alpha2.LinodeMachine{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "mock",
+ Namespace: defaultNamespace,
+ UID: "12345",
+ },
+ Spec: infrav1alpha2.LinodeMachineSpec{
+ Type: "g6-nanode-1",
+ Image: rutil.DefaultMachineControllerLinodeImage,
+ DiskEncryption: string(linodego.InstanceDiskEncryptionEnabled),
+ LinodeInterfaces: []infrav1alpha2.LinodeInterfaceCreateOptions{{
+ VLAN: &infrav1alpha2.VLANInterface{},
+ }},
},
}
@@ -2718,17 +3224,11 @@ var _ = Describe("machine in vlan", Label("machine", "vlan"), Ordered, func() {
},
}, nil)
mockLinodeClient.EXPECT().
- ListInstanceConfigs(ctx, 123, gomock.Any()).
+ ListInterfaces(ctx, 123, gomock.Any()).
After(getAddrs).
- Return([]linodego.InstanceConfig{{
- Interfaces: []linodego.InstanceConfigInterface{
- {
- Purpose: linodego.InterfacePurposePublic,
- },
- {
- Purpose: linodego.InterfacePurposeVLAN,
- IPAMAddress: "10.0.0.2/11",
- },
+ Return([]linodego.LinodeInterface{{
+ VLAN: &linodego.VLANInterface{
+ IPAMAddress: ptr.To("10.0.0.2/11"),
},
}}, nil)
@@ -2950,6 +3450,199 @@ var _ = Describe("create machine with direct VPCID", Label("machine", "VPCID"),
})
})
+var _ = Describe("create machine with direct VPCID with new network interfaces", Label("machine", "newNetworkInterfaces", "VPCID"), Ordered, func() {
+ var (
+ reconciler LinodeMachineReconciler
+ linodeMachine infrav1alpha2.LinodeMachine
+ machineKey client.ObjectKey
+ bootstrapSecret corev1.Secret
+ )
+
+ BeforeAll(func(ctx SpecContext) {
+ reconciler = LinodeMachineReconciler{
+ Client: k8sClient,
+ Recorder: record.NewFakeRecorder(100),
+ }
+
+ linodeMachine = infrav1alpha2.LinodeMachine{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "machine-with-direct-vpcid-new-network-interfaces",
+ Namespace: defaultNamespace,
+ },
+ Spec: infrav1alpha2.LinodeMachineSpec{
+ Type: "g6-nanode-1",
+ Image: "linode/ubuntu22.04",
+ Region: "us-east",
+ VPCID: ptr.To(12345),
+ InterfaceGeneration: linodego.GenerationLinode,
+ },
+ }
+ machineKey = client.ObjectKeyFromObject(&linodeMachine)
+
+ bootstrapSecret = corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "bootstrap-secret-vpcid-new-network-interfaces",
+ Namespace: defaultNamespace,
+ },
+ Data: map[string][]byte{
+ "value": []byte("userdata"),
+ },
+ }
+
+ Expect(k8sClient.Create(ctx, &bootstrapSecret)).To(Succeed())
+ Expect(k8sClient.Create(ctx, &linodeMachine)).To(Succeed())
+ })
+
+ AfterAll(func(ctx SpecContext) {
+ Expect(k8sClient.Delete(ctx, &linodeMachine)).To(Succeed())
+ Expect(k8sClient.Delete(ctx, &bootstrapSecret)).To(Succeed())
+ })
+
+ It("creates a machine with direct VPCID", func(ctx SpecContext) {
+ mockCtrl := gomock.NewController(GinkgoT())
+ defer mockCtrl.Finish()
+
+ mockLinodeClient := mock.NewMockLinodeClient(mockCtrl)
+ mockLinodeClient.EXPECT().
+ GetRegion(gomock.Any(), gomock.Any()).
+ Return(&linodego.Region{ID: "us-east", Capabilities: []string{"Metadata"}}, nil).
+ AnyTimes()
+ mockLinodeClient.EXPECT().
+ GetImage(gomock.Any(), gomock.Any()).
+ Return(&linodego.Image{ID: "linode/ubuntu22.04", Capabilities: []string{"cloud-init"}}, nil).
+ AnyTimes()
+ mockLinodeClient.EXPECT().
+ GetVPC(gomock.Any(), gomock.Eq(12345)).
+ Return(&linodego.VPC{
+ ID: 12345,
+ Label: "test-vpc",
+ Region: "us-east",
+ Subnets: []linodego.VPCSubnet{
+ {
+ ID: 1001,
+ Label: "subnet-1",
+ },
+ },
+ }, nil).
+ AnyTimes()
+ mockLinodeClient.EXPECT().
+ CreateInstance(gomock.Any(), gomock.Any()).
+ DoAndReturn(func(_ context.Context, opts linodego.InstanceCreateOptions) (*linodego.Instance, error) {
+ // Verify that the instance is created with the correct VPC interface
+ Expect(opts.LinodeInterfaces).To(HaveLen(1))
+ Expect(opts.LinodeInterfaces[0].VPC).ToNot(BeNil())
+ Expect(opts.LinodeInterfaces[0].VPC.SubnetID).To(Equal(1001))
+
+ return &linodego.Instance{
+ ID: 12345,
+ Label: opts.Label,
+ Region: opts.Region,
+ Status: linodego.InstanceRunning,
+ IPv4: []*net.IP{ptr.To(net.ParseIP("192.168.1.2"))},
+ IPv6: "2001:db8::2",
+ }, nil
+ }).
+ AnyTimes()
+ mockLinodeClient.EXPECT().
+ OnAfterResponse(gomock.Any()).
+ Return().
+ AnyTimes()
+ mockLinodeClient.EXPECT().
+ ListInstanceConfigs(gomock.Any(), gomock.Any(), gomock.Any()).
+ Return([]linodego.InstanceConfig{
+ {
+ ID: 1,
+ Label: "My Config",
+ Devices: &linodego.InstanceConfigDeviceMap{},
+ },
+ }, 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{
+ IPv4: &linodego.InstanceIPv4Response{
+ Public: []*linodego.InstanceIP{{Address: "192.168.1.2"}},
+ Private: []*linodego.InstanceIP{},
+ },
+ IPv6: &linodego.InstanceIPv6Response{
+ SLAAC: &linodego.InstanceIP{Address: "2001:db8::2"},
+ },
+ }, nil).
+ AnyTimes()
+ mockLinodeClient.EXPECT().
+ BootInstance(gomock.Any(), gomock.Any(), gomock.Any()).
+ Return(nil).
+ AnyTimes()
+
+ // Create a machine scope with the mock client
+ machine := &clusterv1.Machine{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "machine-with-direct-vpcid-new-network-interfaces",
+ Namespace: defaultNamespace,
+ },
+ Spec: clusterv1.MachineSpec{
+ Bootstrap: clusterv1.Bootstrap{
+ DataSecretName: ptr.To("bootstrap-secret-vpcid-new-network-interfaces"),
+ },
+ },
+ }
+
+ cluster := &clusterv1.Cluster{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-cluster",
+ Namespace: defaultNamespace,
+ },
+ }
+
+ // Get the LinodeMachine
+ Expect(k8sClient.Get(ctx, machineKey, &linodeMachine)).To(Succeed())
+
+ // Create a machine scope
+ patchHelper, err := patch.NewHelper(&linodeMachine, k8sClient)
+ Expect(err).NotTo(HaveOccurred())
+
+ // Create a LinodeCluster for the machineScope
+ linodeCluster := &infrav1alpha2.LinodeCluster{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-cluster",
+ Namespace: defaultNamespace,
+ },
+ Spec: infrav1alpha2.LinodeClusterSpec{
+ Region: "us-east",
+ },
+ }
+
+ // Set the VPC preflight check condition to true
+ conditions.Set(&linodeMachine, metav1.Condition{
+ Type: ConditionPreflightLinodeVPCReady,
+ Status: metav1.ConditionTrue,
+ Reason: "VPCReady",
+ })
+
+ mScope := &scope.MachineScope{
+ Client: k8sClient,
+ Cluster: cluster,
+ Machine: machine,
+ LinodeMachine: &linodeMachine,
+ LinodeCluster: linodeCluster, // Add the LinodeCluster to the scope
+ PatchHelper: patchHelper,
+ LinodeClient: mockLinodeClient,
+ }
+
+ // Reconcile the machine
+ result, err := reconciler.reconcile(ctx, logr.Discard(), mScope)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(result.IsZero()).To(BeTrue())
+
+ // Verify that the preflight check for VPC is successful
+ Expect(rutil.ConditionTrue(&linodeMachine, ConditionPreflightLinodeVPCReady)).To(BeTrue())
+ })
+})
+
var _ = Describe("direct vpc functions", Label("machine", "vpc", "functions"), Ordered, func() {
var mockCtrl *gomock.Controller
var mockLinodeClient *mock.MockLinodeClient
diff --git a/internal/webhook/v1alpha2/linodemachine_webhook.go b/internal/webhook/v1alpha2/linodemachine_webhook.go
index 2c13730a2..6e01d7ab0 100644
--- a/internal/webhook/v1alpha2/linodemachine_webhook.go
+++ b/internal/webhook/v1alpha2/linodemachine_webhook.go
@@ -128,10 +128,17 @@ func (r *linodeMachineValidator) ValidateDelete(ctx context.Context, obj runtime
func (r *linodeMachineValidator) validateLinodeMachineSpec(ctx context.Context, linodeclient clients.LinodeClient, spec infrav1alpha2.LinodeMachineSpec, skipAPIValidation bool) field.ErrorList {
var errs field.ErrorList
- if !skipAPIValidation {
- if err := validateRegion(ctx, linodeclient, spec.Region, field.NewPath("spec").Child("region")); err != nil {
- errs = append(errs, err)
+ if !skipAPIValidation { //nolint:nestif // too simple for switch
+ if spec.LinodeInterfaces != nil {
+ if err := validateRegion(ctx, linodeclient, spec.Region, field.NewPath("spec").Child("region"), linodego.CapabilityLinodeInterfaces); err != nil {
+ errs = append(errs, err)
+ }
+ } else {
+ if err := validateRegion(ctx, linodeclient, spec.Region, field.NewPath("spec").Child("region")); err != nil {
+ errs = append(errs, err)
+ }
}
+
plan, err := validateLinodeType(ctx, linodeclient, spec.Type, field.NewPath("spec").Child("type"))
if err != nil {
errs = append(errs, err)
@@ -149,6 +156,12 @@ func (r *linodeMachineValidator) validateLinodeMachineSpec(ctx context.Context,
})
}
+ if spec.LinodeInterfaces != nil {
+ if ifaceErrs := r.validateLinodeInterfaces(spec); ifaceErrs != nil {
+ errs = append(errs, ifaceErrs...)
+ }
+ }
+
if spec.FirewallID != 0 && spec.FirewallRef != nil {
errs = append(errs, &field.Error{
Field: "spec.firewallID/spec.firewallRef",
@@ -163,6 +176,31 @@ func (r *linodeMachineValidator) validateLinodeMachineSpec(ctx context.Context,
return errs
}
+func (r *linodeMachineValidator) validateLinodeInterfaces(spec infrav1alpha2.LinodeMachineSpec) field.ErrorList {
+ var errs field.ErrorList
+
+ if spec.Interfaces != nil {
+ errs = append(errs, &field.Error{
+ Field: "spec.linodeInterfaces/spec.interfaces",
+ Type: field.ErrorTypeInvalid,
+ Detail: "Cannot specify both LinodeInterfaces and Interfaces",
+ })
+ }
+
+ if spec.PrivateIP != nil && *spec.PrivateIP {
+ errs = append(errs, &field.Error{
+ Field: "spec.linodeInterfaces/spec.privateIP",
+ Type: field.ErrorTypeInvalid,
+ Detail: "Linode Interfaces do not support private IPs",
+ })
+ }
+
+ if len(errs) == 0 {
+ return nil
+ }
+ return errs
+}
+
func (r *linodeMachineValidator) validateLinodeMachineDisks(plan *linodego.LinodeType, spec infrav1alpha2.LinodeMachineSpec) *field.Error {
// The Linode plan information is required to perform disk validation
if plan == nil {
diff --git a/internal/webhook/v1alpha2/linodemachine_webhook_test.go b/internal/webhook/v1alpha2/linodemachine_webhook_test.go
index 8303dc253..68c7ecfa2 100644
--- a/internal/webhook/v1alpha2/linodemachine_webhook_test.go
+++ b/internal/webhook/v1alpha2/linodemachine_webhook_test.go
@@ -20,6 +20,7 @@ import (
"context"
"errors"
"math"
+ "slices"
"strconv"
"testing"
@@ -54,6 +55,8 @@ func TestValidateLinodeMachine(t *testing.T) {
Type: "example",
},
}
+ region = linodego.Region{ID: "test"}
+ capabilities = []string{linodego.CapabilityLinodeInterfaces}
disk = infrav1alpha2.InstanceDisk{Size: resource.MustParse("1G")}
disk_zero = infrav1alpha2.InstanceDisk{Size: *resource.NewQuantity(0, resource.BinarySI)}
plan = linodego.LinodeType{Disk: 2 * int(disk.Size.ScaledValue(resource.Mega))}
@@ -163,6 +166,55 @@ func TestValidateLinodeMachine(t *testing.T) {
}
}),
),
+ Path(
+ Call("invalid linode interfaces with private IP", func(ctx context.Context, mck Mock) {
+ region := region
+ region.Capabilities = slices.Clone(capabilities)
+ mck.LinodeClient.EXPECT().GetRegion(gomock.Any(), gomock.Any()).Return(®ion, nil).AnyTimes()
+ mck.LinodeClient.EXPECT().GetType(gomock.Any(), gomock.Any()).Return(&plan_max, nil).AnyTimes()
+ }),
+ Result("error", func(ctx context.Context, mck Mock) {
+ machine := machine
+ machine.Spec.LinodeInterfaces = []infrav1alpha2.LinodeInterfaceCreateOptions{{}}
+ machine.Spec.PrivateIP = ptr.To(true)
+ errs := validator.validateLinodeMachineSpec(ctx, mck.LinodeClient, machine.Spec, SkipAPIValidation)
+ for _, err := range errs {
+ assert.ErrorContains(t, err, "Linode Interfaces do not support private IPs")
+ }
+ }),
+ ),
+ Path(
+ Call("invalid linode interfaces with legacy interfaces", func(ctx context.Context, mck Mock) {
+ region := region
+ region.Capabilities = slices.Clone(capabilities)
+ mck.LinodeClient.EXPECT().GetRegion(gomock.Any(), gomock.Any()).Return(®ion, nil).AnyTimes()
+ mck.LinodeClient.EXPECT().GetType(gomock.Any(), gomock.Any()).Return(&plan_max, nil).AnyTimes()
+ }),
+ Result("error", func(ctx context.Context, mck Mock) {
+ machine := machine
+ machine.Spec.LinodeInterfaces = []infrav1alpha2.LinodeInterfaceCreateOptions{{}}
+ machine.Spec.Interfaces = []infrav1alpha2.InstanceConfigInterfaceCreateOptions{{}}
+ errs := validator.validateLinodeMachineSpec(ctx, mck.LinodeClient, machine.Spec, SkipAPIValidation)
+ for _, err := range errs {
+ assert.ErrorContains(t, err, "Cannot specify both LinodeInterfaces and Interfaces")
+ }
+ }),
+ ),
+ Path(
+ Call("linode interfaces with invalid region", func(ctx context.Context, mck Mock) {
+ region := region
+ mck.LinodeClient.EXPECT().GetRegion(gomock.Any(), gomock.Any()).Return(®ion, nil).AnyTimes()
+ mck.LinodeClient.EXPECT().GetType(gomock.Any(), gomock.Any()).Return(&plan_max, nil).AnyTimes()
+ }),
+ Result("error", func(ctx context.Context, mck Mock) {
+ machine := machine
+ machine.Spec.LinodeInterfaces = []infrav1alpha2.LinodeInterfaceCreateOptions{{}}
+ errs := validator.validateLinodeMachineSpec(ctx, mck.LinodeClient, machine.Spec, SkipAPIValidation)
+ for _, err := range errs {
+ assert.ErrorContains(t, err, "no capability: Linode Interfaces")
+ }
+ }),
+ ),
),
)
}
diff --git a/mock/client.go b/mock/client.go
index b6f1f4c34..17c764974 100644
--- a/mock/client.go
+++ b/mock/client.go
@@ -726,6 +726,36 @@ func (mr *MockLinodeClientMockRecorder) ListInstances(ctx, opts any) *gomock.Cal
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListInstances", reflect.TypeOf((*MockLinodeClient)(nil).ListInstances), ctx, opts)
}
+// ListInterfaceFirewalls mocks base method.
+func (m *MockLinodeClient) ListInterfaceFirewalls(ctx context.Context, linodeID, interfaceID int, opts *linodego.ListOptions) ([]linodego.Firewall, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ListInterfaceFirewalls", ctx, linodeID, interfaceID, opts)
+ ret0, _ := ret[0].([]linodego.Firewall)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListInterfaceFirewalls indicates an expected call of ListInterfaceFirewalls.
+func (mr *MockLinodeClientMockRecorder) ListInterfaceFirewalls(ctx, linodeID, interfaceID, opts any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListInterfaceFirewalls", reflect.TypeOf((*MockLinodeClient)(nil).ListInterfaceFirewalls), ctx, linodeID, interfaceID, opts)
+}
+
+// ListInterfaces mocks base method.
+func (m *MockLinodeClient) ListInterfaces(ctx context.Context, linodeID int, opts *linodego.ListOptions) ([]linodego.LinodeInterface, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ListInterfaces", ctx, linodeID, opts)
+ ret0, _ := ret[0].([]linodego.LinodeInterface)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListInterfaces indicates an expected call of ListInterfaces.
+func (mr *MockLinodeClientMockRecorder) ListInterfaces(ctx, linodeID, opts any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListInterfaces", reflect.TypeOf((*MockLinodeClient)(nil).ListInterfaces), ctx, linodeID, opts)
+}
+
// ListNodeBalancerNodes mocks base method.
func (m *MockLinodeClient) ListNodeBalancerNodes(ctx context.Context, nodebalancerID, configID int, opts *linodego.ListOptions) ([]linodego.NodeBalancerNode, error) {
m.ctrl.T.Helper()
@@ -2167,6 +2197,59 @@ func (mr *MockLinodeFirewallClientMockRecorder) UpdateFirewallRules(ctx, firewal
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateFirewallRules", reflect.TypeOf((*MockLinodeFirewallClient)(nil).UpdateFirewallRules), ctx, firewallID, rules)
}
+// MockLinodeInterfacesClient is a mock of LinodeInterfacesClient interface.
+type MockLinodeInterfacesClient struct {
+ ctrl *gomock.Controller
+ recorder *MockLinodeInterfacesClientMockRecorder
+}
+
+// MockLinodeInterfacesClientMockRecorder is the mock recorder for MockLinodeInterfacesClient.
+type MockLinodeInterfacesClientMockRecorder struct {
+ mock *MockLinodeInterfacesClient
+}
+
+// NewMockLinodeInterfacesClient creates a new mock instance.
+func NewMockLinodeInterfacesClient(ctrl *gomock.Controller) *MockLinodeInterfacesClient {
+ mock := &MockLinodeInterfacesClient{ctrl: ctrl}
+ mock.recorder = &MockLinodeInterfacesClientMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockLinodeInterfacesClient) EXPECT() *MockLinodeInterfacesClientMockRecorder {
+ return m.recorder
+}
+
+// ListInterfaceFirewalls mocks base method.
+func (m *MockLinodeInterfacesClient) ListInterfaceFirewalls(ctx context.Context, linodeID, interfaceID int, opts *linodego.ListOptions) ([]linodego.Firewall, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ListInterfaceFirewalls", ctx, linodeID, interfaceID, opts)
+ ret0, _ := ret[0].([]linodego.Firewall)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListInterfaceFirewalls indicates an expected call of ListInterfaceFirewalls.
+func (mr *MockLinodeInterfacesClientMockRecorder) ListInterfaceFirewalls(ctx, linodeID, interfaceID, opts any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListInterfaceFirewalls", reflect.TypeOf((*MockLinodeInterfacesClient)(nil).ListInterfaceFirewalls), ctx, linodeID, interfaceID, opts)
+}
+
+// ListInterfaces mocks base method.
+func (m *MockLinodeInterfacesClient) ListInterfaces(ctx context.Context, linodeID int, opts *linodego.ListOptions) ([]linodego.LinodeInterface, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ListInterfaces", ctx, linodeID, opts)
+ ret0, _ := ret[0].([]linodego.LinodeInterface)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListInterfaces indicates an expected call of ListInterfaces.
+func (mr *MockLinodeInterfacesClientMockRecorder) ListInterfaces(ctx, linodeID, opts any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListInterfaces", reflect.TypeOf((*MockLinodeInterfacesClient)(nil).ListInterfaces), ctx, linodeID, opts)
+}
+
// MockK8sClient is a mock of K8sClient interface.
type MockK8sClient struct {
ctrl *gomock.Controller
diff --git a/observability/wrappers/linodeclient/linodeclient.gen.go b/observability/wrappers/linodeclient/linodeclient.gen.go
index 2a8ff21e5..6b7a227aa 100644
--- a/observability/wrappers/linodeclient/linodeclient.gen.go
+++ b/observability/wrappers/linodeclient/linodeclient.gen.go
@@ -1195,6 +1195,59 @@ func (_d LinodeClientWithTracing) ListInstances(ctx context.Context, opts *linod
return _d.LinodeClient.ListInstances(ctx, opts)
}
+// ListInterfaceFirewalls implements _sourceClients.LinodeClient
+func (_d LinodeClientWithTracing) ListInterfaceFirewalls(ctx context.Context, linodeID int, interfaceID int, opts *linodego.ListOptions) (fa1 []linodego.Firewall, err error) {
+ ctx, _span := tracing.Start(ctx, "_sourceClients.LinodeClient.ListInterfaceFirewalls")
+ defer func() {
+ if _d._spanDecorator != nil {
+ _d._spanDecorator(_span, map[string]interface{}{
+ "ctx": ctx,
+ "linodeID": linodeID,
+ "interfaceID": interfaceID,
+ "opts": opts}, map[string]interface{}{
+ "fa1": fa1,
+ "err": err})
+ }
+
+ if err != nil {
+ _span.RecordError(err)
+ _span.SetAttributes(
+ attribute.String("event", "error"),
+ attribute.String("message", err.Error()),
+ )
+ }
+
+ _span.End()
+ }()
+ return _d.LinodeClient.ListInterfaceFirewalls(ctx, linodeID, interfaceID, opts)
+}
+
+// ListInterfaces implements _sourceClients.LinodeClient
+func (_d LinodeClientWithTracing) ListInterfaces(ctx context.Context, linodeID int, opts *linodego.ListOptions) (la1 []linodego.LinodeInterface, err error) {
+ ctx, _span := tracing.Start(ctx, "_sourceClients.LinodeClient.ListInterfaces")
+ defer func() {
+ if _d._spanDecorator != nil {
+ _d._spanDecorator(_span, map[string]interface{}{
+ "ctx": ctx,
+ "linodeID": linodeID,
+ "opts": opts}, map[string]interface{}{
+ "la1": la1,
+ "err": err})
+ }
+
+ if err != nil {
+ _span.RecordError(err)
+ _span.SetAttributes(
+ attribute.String("event", "error"),
+ attribute.String("message", err.Error()),
+ )
+ }
+
+ _span.End()
+ }()
+ return _d.LinodeClient.ListInterfaces(ctx, linodeID, opts)
+}
+
// ListNodeBalancerNodes implements _sourceClients.LinodeClient
func (_d LinodeClientWithTracing) ListNodeBalancerNodes(ctx context.Context, nodebalancerID int, configID int, opts *linodego.ListOptions) (na1 []linodego.NodeBalancerNode, err error) {
ctx, _span := tracing.Start(ctx, "_sourceClients.LinodeClient.ListNodeBalancerNodes")
diff --git a/templates/infra/linodeMachineTemplate.yaml b/templates/infra/linodeMachineTemplate.yaml
index 586c37293..ce92b6574 100644
--- a/templates/infra/linodeMachineTemplate.yaml
+++ b/templates/infra/linodeMachineTemplate.yaml
@@ -14,6 +14,12 @@ spec:
kind: LinodeFirewall
name: ${CLUSTER_NAME}
# diskEncryption: disabled
+ #
+ # linodeInterfaces can't be used for the control plane
+ # if using the default NodeBalancer for the associated
+ # LinodeCluster's .Spec.Network.LoadBalancerType
+ # because we need a private IP to add the machine to the
+ # NodeBalancer
interfaces:
- purpose: public
authorizedKeys:
@@ -37,6 +43,12 @@ spec:
# diskEncryption: disabled
interfaces:
- purpose: public
+ # uncomment if using new network interfaces and comment out interfaces above (requires beta opt-in)
+ # linodeInterfaces:
+ # - public:
+ # ipv4:
+ # addresses:
+ # - address: "auto"
authorizedKeys:
# uncomment to include your ssh key in linode provisioning
# - ${LINODE_SSH_PUBKEY}