Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions api/v1alpha2/linodecluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,16 @@ type NetworkSpec struct {
// example: 10.10.10.0/30
// +optional
NodeBalancerBackendIPv4Range string `json:"nodeBalancerBackendIPv4Range,omitempty"`

// EnableVPCBackends toggles VPC-scoped NodeBalancer and VPC backend IP usage.
// If set to false (default), the NodeBalancer will not be created in a VPC and
// backends will use Linode private IPs. If true, the NodeBalancer will be
// created in the configured VPC (when VPCRef or VPCID is set) and backends
// will use VPC IPs.
// +kubebuilder:default=false
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable"
// +optional
EnableVPCBackends bool `json:"enableVPCBackends,omitempty"`
}

type LinodeNBPortConfig struct {
Expand Down
46 changes: 25 additions & 21 deletions cloud/services/loadbalancers.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,21 @@ const (
DefaultKonnectivityLBPort = 8132
)

// DetermineAPIServerLBPort returns the configured API server load balancer port,
// or the provider default when not explicitly set.
func DetermineAPIServerLBPort(clusterScope *scope.ClusterScope) int {
if clusterScope.LinodeCluster.Spec.Network.ApiserverLoadBalancerPort != 0 {
return clusterScope.LinodeCluster.Spec.Network.ApiserverLoadBalancerPort
}
return DefaultApiserverLBPort
}

// ShouldUseVPC decides whether VPC IPs/backends should be preferred and a VPC-scoped
// NodeBalancer should be created. It requires both the feature flag and a VPC reference/ID.
func ShouldUseVPC(clusterScope *scope.ClusterScope) bool {
return clusterScope.LinodeCluster.Spec.Network.EnableVPCBackends && (clusterScope.LinodeCluster.Spec.VPCRef != nil || clusterScope.LinodeCluster.Spec.VPCID != nil)
}

// FindSubnet selects a subnet from the provided subnets based on the subnet name
// It handles both direct VPC subnets and VPCRef subnets
// If subnet name is provided, it looks for a matching subnet; otherwise, it uses the first subnet
Expand Down Expand Up @@ -125,19 +140,18 @@ func EnsureNodeBalancer(ctx context.Context, clusterScope *scope.ClusterScope, l
Tags: []string{string(clusterScope.LinodeCluster.UID)},
}

// if NodeBalancerBackendIPv4Range is set, create the NodeBalancer in the specified VPC
if clusterScope.LinodeCluster.Spec.Network.NodeBalancerBackendIPv4Range != "" && (clusterScope.LinodeCluster.Spec.VPCRef != nil || clusterScope.LinodeCluster.Spec.VPCID != nil) {
logger.Info("Creating NodeBalancer in VPC", "NodeBalancerBackendIPv4Range", clusterScope.LinodeCluster.Spec.Network.NodeBalancerBackendIPv4Range)
// if enableVPCBackends is true and vpcRef or vpcID is set, create the NodeBalancer in the specified VPC
if ShouldUseVPC(clusterScope) {
logger.Info("Creating NodeBalancer in VPC")
subnetID, err := getSubnetID(ctx, clusterScope, logger)
if err != nil {
logger.Error(err, "Failed to fetch Linode Subnet ID")
return nil, err
}
createConfig.VPCs = []linodego.NodeBalancerVPCOptions{
{
IPv4Range: clusterScope.LinodeCluster.Spec.Network.NodeBalancerBackendIPv4Range,
SubnetID: subnetID,
},

createConfig.VPCs = []linodego.NodeBalancerVPCOptions{{SubnetID: subnetID}}
if clusterScope.LinodeCluster.Spec.Network.NodeBalancerBackendIPv4Range != "" {
createConfig.VPCs[0].IPv4Range = clusterScope.LinodeCluster.Spec.Network.NodeBalancerBackendIPv4Range
}
}

Expand Down Expand Up @@ -264,10 +278,7 @@ func EnsureNodeBalancerConfigs(
nbConfigs := []*linodego.NodeBalancerConfig{}
var apiserverLinodeNBConfig *linodego.NodeBalancerConfig
var err error
apiLBPort := DefaultApiserverLBPort
if clusterScope.LinodeCluster.Spec.Network.ApiserverLoadBalancerPort != 0 {
apiLBPort = clusterScope.LinodeCluster.Spec.Network.ApiserverLoadBalancerPort
}
apiLBPort := DetermineAPIServerLBPort(clusterScope)

if clusterScope.LinodeCluster.Spec.Network.ApiserverNodeBalancerConfigID != nil {
apiserverLinodeNBConfig, err = clusterScope.LinodeClient.GetNodeBalancerConfig(
Expand Down Expand Up @@ -325,10 +336,7 @@ func EnsureNodeBalancerConfigs(
}

func processAndCreateNodeBalancerNodes(ctx context.Context, ipAddress string, clusterScope *scope.ClusterScope, nodeBalancerNodes []linodego.NodeBalancerNode, subnetID int) error {
apiserverLBPort := DefaultApiserverLBPort
if clusterScope.LinodeCluster.Spec.Network.ApiserverLoadBalancerPort != 0 {
apiserverLBPort = clusterScope.LinodeCluster.Spec.Network.ApiserverLoadBalancerPort
}
apiserverLBPort := DetermineAPIServerLBPort(clusterScope)

// Set the port number and NB config ID for standard ports
portsToBeAdded := make([]map[string]int, 0)
Expand Down Expand Up @@ -382,12 +390,8 @@ func AddNodesToNB(ctx context.Context, logger logr.Logger, clusterScope *scope.C
return errors.New("nil NodeBalancer Config ID")
}

// if NodeBalancerBackendIPv4Range is set, we want to prioritize finding the VPC IP address
// otherwise, we will use the private IP address
subnetID := 0
useVPCIps := clusterScope.LinodeCluster.Spec.Network.NodeBalancerBackendIPv4Range != "" && clusterScope.LinodeCluster.Spec.VPCRef != nil
if useVPCIps {
// Get subnetID
if ShouldUseVPC(clusterScope) {
subnetID, err := getSubnetID(ctx, clusterScope, logger)
if err != nil {
logger.Error(err, "Failed to fetch Linode Subnet ID")
Expand Down
11 changes: 11 additions & 0 deletions cloud/services/loadbalancers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ func TestEnsureNodeBalancer(t *testing.T) {
Namespace: "default",
},
Network: infrav1alpha2.NetworkSpec{
EnableVPCBackends: true,
NodeBalancerBackendIPv4Range: "10.0.0.0/24",
},
},
Expand Down Expand Up @@ -339,6 +340,7 @@ func TestEnsureNodeBalancer(t *testing.T) {
Region: "us-east",
VPCID: ptr.To(456),
Network: infrav1alpha2.NetworkSpec{
EnableVPCBackends: true,
NodeBalancerBackendIPv4Range: "10.0.0.0/24",
},
},
Expand Down Expand Up @@ -393,6 +395,7 @@ func TestEnsureNodeBalancer(t *testing.T) {
Region: "us-east",
VPCID: ptr.To(789),
Network: infrav1alpha2.NetworkSpec{
EnableVPCBackends: true,
NodeBalancerBackendIPv4Range: "10.0.0.0/24",
},
},
Expand Down Expand Up @@ -420,6 +423,7 @@ func TestEnsureNodeBalancer(t *testing.T) {
Namespace: "default",
},
Network: infrav1alpha2.NetworkSpec{
EnableVPCBackends: true,
NodeBalancerBackendIPv4Range: "10.0.0.0/24",
},
},
Expand Down Expand Up @@ -1713,6 +1717,7 @@ func TestAddNodeToNBFullWorkflow(t *testing.T) {
Namespace: "default",
},
Network: infrav1alpha2.NetworkSpec{
EnableVPCBackends: true,
NodeBalancerID: ptr.To(1234),
ApiserverNodeBalancerConfigID: ptr.To(5678),
NodeBalancerBackendIPv4Range: "10.0.0.0/24",
Expand Down Expand Up @@ -1786,6 +1791,7 @@ func TestAddNodeToNBFullWorkflow(t *testing.T) {
Namespace: "default",
},
Network: infrav1alpha2.NetworkSpec{
EnableVPCBackends: true,
NodeBalancerID: ptr.To(1234),
ApiserverNodeBalancerConfigID: ptr.To(5678),
NodeBalancerBackendIPv4Range: "10.0.0.0/24",
Expand Down Expand Up @@ -1844,6 +1850,7 @@ func TestAddNodeToNBFullWorkflow(t *testing.T) {
Namespace: "default",
},
Network: infrav1alpha2.NetworkSpec{
EnableVPCBackends: true,
NodeBalancerID: ptr.To(1234),
ApiserverNodeBalancerConfigID: ptr.To(5678),
NodeBalancerBackendIPv4Range: "10.0.0.0/24",
Expand Down Expand Up @@ -2564,6 +2571,7 @@ func TestAddNodeToNBWithVPC(t *testing.T) {
},
Spec: infrav1alpha2.LinodeClusterSpec{
Network: infrav1alpha2.NetworkSpec{
EnableVPCBackends: true,
ApiserverNodeBalancerConfigID: ptr.To(222),
NodeBalancerID: ptr.To(111),
NodeBalancerBackendIPv4Range: "10.0.0.0/24",
Expand Down Expand Up @@ -2635,6 +2643,7 @@ func TestAddNodeToNBWithVPC(t *testing.T) {
},
Spec: infrav1alpha2.LinodeClusterSpec{
Network: infrav1alpha2.NetworkSpec{
EnableVPCBackends: true,
ApiserverNodeBalancerConfigID: ptr.To(222),
NodeBalancerID: ptr.To(111),
NodeBalancerBackendIPv4Range: "10.0.0.0/24",
Expand Down Expand Up @@ -2710,6 +2719,7 @@ func TestAddNodeToNBWithVPC(t *testing.T) {
},
Spec: infrav1alpha2.LinodeClusterSpec{
Network: infrav1alpha2.NetworkSpec{
EnableVPCBackends: true,
ApiserverNodeBalancerConfigID: ptr.To(222),
NodeBalancerID: ptr.To(111),
NodeBalancerBackendIPv4Range: "10.0.0.0/24",
Expand Down Expand Up @@ -2819,6 +2829,7 @@ func TestAddNodeToNBWithVPC(t *testing.T) {
},
Spec: infrav1alpha2.LinodeClusterSpec{
Network: infrav1alpha2.NetworkSpec{
EnableVPCBackends: true,
ApiserverNodeBalancerConfigID: ptr.To(222),
NodeBalancerID: ptr.To(111),
NodeBalancerBackendIPv4Range: "10.0.0.0/24",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,18 @@ spec:
Ignored if the LoadBalancerType is set to anything other than dns
If not set, CAPL will create a unique identifier for you
type: string
enableVPCBackends:
default: false
description: |-
EnableVPCBackends toggles VPC-scoped NodeBalancer and VPC backend IP usage.
If set to false (default), the NodeBalancer will not be created in a VPC and
backends will use Linode private IPs. If true, the NodeBalancer will be
created in the configured VPC (when VPCRef or VPCID is set) and backends
will use VPC IPs.
type: boolean
x-kubernetes-validations:
- message: Value is immutable
rule: self == oldSelf
loadBalancerType:
default: NodeBalancer
description: LoadBalancerType is the type of load balancer to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,18 @@ spec:
Ignored if the LoadBalancerType is set to anything other than dns
If not set, CAPL will create a unique identifier for you
type: string
enableVPCBackends:
default: false
description: |-
EnableVPCBackends toggles VPC-scoped NodeBalancer and VPC backend IP usage.
If set to false (default), the NodeBalancer will not be created in a VPC and
backends will use Linode private IPs. If true, the NodeBalancer will be
created in the configured VPC (when VPCRef or VPCID is set) and backends
will use VPC IPs.
type: boolean
x-kubernetes-validations:
- message: Value is immutable
rule: self == oldSelf
loadBalancerType:
default: NodeBalancer
description: LoadBalancerType is the type of load balancer
Expand Down
1 change: 1 addition & 0 deletions docs/src/reference/out.md
Original file line number Diff line number Diff line change
Expand Up @@ -1170,6 +1170,7 @@ _Appears in:_
| `subnetName` _string_ | subnetName is the name/label of the VPC subnet to be used by the cluster | | |
| `useVlan` _boolean_ | UseVlan provisions a cluster that uses VLANs instead of VPCs. IPAM is managed internally. | | |
| `nodeBalancerBackendIPv4Range` _string_ | NodeBalancerBackendIPv4Range is the subnet range we want to provide for creating nodebalancer in VPC.<br />example: 10.10.10.0/30 | | |
| `enableVPCBackends` _boolean_ | EnableVPCBackends toggles VPC-scoped NodeBalancer and VPC backend IP usage.<br />If set to false (default), the NodeBalancer will not be created in a VPC and<br />backends will use Linode private IPs. If true, the NodeBalancer will be<br />created in the configured VPC (when VPCRef or VPCID is set) and backends<br />will use VPC IPs. | false | |


#### ObjectStorageACL
Expand Down
2 changes: 1 addition & 1 deletion hack/generate-flavors.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ SUPPORTED_CLUSTERCLASSES=(
for clusterclass in ${SUPPORTED_CLUSTERCLASSES[@]}; do
# clusterctl expects clusterclass not have the "cluster-template" prefix
# except for the actual cluster template using the clusterclass
echo "****** Generating clusterclass-${clusterclass} flavor ******"
echo "****** Generating ${clusterclass} flavor ******"
kustomize build "${FLAVORS_DIR}/${clusterclass}" > "${REPO_ROOT}/templates/${clusterclass}.yaml"
cp "${FLAVORS_DIR}/${clusterclass}/cluster-template.yaml" "${REPO_ROOT}/templates/cluster-template-${clusterclass}.yaml"
done
Expand Down
82 changes: 45 additions & 37 deletions internal/controller/linodecluster_controller_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,51 +87,62 @@ func removeMachineFromNB(ctx context.Context, logger logr.Logger, clusterScope *
}

func getIPPortCombo(cscope *scope.ClusterScope) (ipPortComboList []string) {
apiserverLBPort := services.DefaultApiserverLBPort
if cscope.LinodeCluster.Spec.Network.ApiserverLoadBalancerPort != 0 {
apiserverLBPort = cscope.LinodeCluster.Spec.Network.ApiserverLoadBalancerPort
}

// Check if we're using VPC
useVPCIps := cscope.LinodeCluster.Spec.Network.NodeBalancerBackendIPv4Range != "" && cscope.LinodeCluster.Spec.VPCRef != nil
apiServerLBPort := services.DetermineAPIServerLBPort(cscope)
useVPCIPs := services.ShouldUseVPC(cscope)

for _, eachMachine := range cscope.LinodeMachines.Items {
// First try to find VPC IPs if we're using VPC
if useVPCIps {
vpcIPFound := false
for _, IPs := range eachMachine.Status.Addresses {
// Look for internal IPs that are NOT 192.168.* (likely VPC IPs)
if IPs.Type == clusterv1.MachineInternalIP && !util.IsLinodePrivateIP(IPs.Address) {
vpcIPFound = true
ipPortComboList = append(ipPortComboList, fmt.Sprintf("%s:%d", IPs.Address, apiserverLBPort))
for _, portConfig := range cscope.LinodeCluster.Spec.Network.AdditionalPorts {
ipPortComboList = append(ipPortComboList, fmt.Sprintf("%s:%d", IPs.Address, portConfig.Port))
}
break // Use first VPC IP found for this machine
}
}
var selectedIP string

// If we found a VPC IP for this machine, continue to the next machine
if vpcIPFound {
continue
if useVPCIPs {
if ip, ok := findFirstVPCInternalIP(eachMachine.Status.Addresses); ok {
selectedIP = ip
}
}

// Fall back to original behavior for this machine if no VPC IP found or not using VPC
for _, IPs := range eachMachine.Status.Addresses {
if IPs.Type != clusterv1.MachineInternalIP || !util.IsLinodePrivateIP(IPs.Address) {
continue
if selectedIP == "" {
if ip, ok := findFirstPrivateInternalIP(eachMachine.Status.Addresses); ok {
selectedIP = ip
}
ipPortComboList = append(ipPortComboList, fmt.Sprintf("%s:%d", IPs.Address, apiserverLBPort))
for _, portConfig := range cscope.LinodeCluster.Spec.Network.AdditionalPorts {
ipPortComboList = append(ipPortComboList, fmt.Sprintf("%s:%d", IPs.Address, portConfig.Port))
}
break // Use first 192.168.* IP found for this machine
}

if selectedIP != "" {
ipPortComboList = append(ipPortComboList, buildPortCombosForIP(selectedIP, apiServerLBPort, cscope.LinodeCluster.Spec.Network.AdditionalPorts)...)
}
}

return ipPortComboList
}

// findFirstVPCInternalIP returns the first internal IP that is not in Linode's private 192.168.* range.
func findFirstVPCInternalIP(addresses []clusterv1.MachineAddress) (string, bool) {
for _, addr := range addresses {
if addr.Type == clusterv1.MachineInternalIP && !util.IsLinodePrivateIP(addr.Address) {
return addr.Address, true
}
}
return "", false
}

// findFirstPrivateInternalIP returns the first internal IP in Linode's private 192.168.* range.
func findFirstPrivateInternalIP(addresses []clusterv1.MachineAddress) (string, bool) {
for _, addr := range addresses {
if addr.Type == clusterv1.MachineInternalIP && util.IsLinodePrivateIP(addr.Address) {
return addr.Address, true
}
}
return "", false
}

// buildPortCombosForIP composes ip:port pairs for the API server port and any additional ports.
func buildPortCombosForIP(ip string, apiServerLBPort int, additionalPorts []infrav1alpha2.LinodeNBPortConfig) []string {
results := make([]string, 0, 1+len(additionalPorts))
results = append(results, fmt.Sprintf("%s:%d", ip, apiServerLBPort))
for _, portConfig := range additionalPorts {
results = append(results, fmt.Sprintf("%s:%d", ip, portConfig.Port))
}
return results
}

func linodeMachineToLinodeCluster(tracedClient client.Client, logger logr.Logger) handler.MapFunc {
logger = logger.WithName("LinodeClusterReconciler").WithName("linodeMachineToLinodeCluster")

Expand Down Expand Up @@ -188,10 +199,7 @@ func handleDNS(clusterScope *scope.ClusterScope) {
subDomain = clusterScope.LinodeCluster.Spec.Network.DNSSubDomainOverride
}
dnsHost := subDomain + "." + clusterSpec.Network.DNSRootDomain
apiLBPort := services.DefaultApiserverLBPort
if clusterScope.LinodeCluster.Spec.Network.ApiserverLoadBalancerPort != 0 {
apiLBPort = clusterScope.LinodeCluster.Spec.Network.ApiserverLoadBalancerPort
}
apiLBPort := services.DetermineAPIServerLBPort(clusterScope)
clusterScope.LinodeCluster.Spec.ControlPlaneEndpoint = clusterv1.APIEndpoint{
Host: dnsHost,
Port: int32(apiLBPort), // #nosec G115: Integer overflow conversion is safe for port numbers
Expand Down
4 changes: 4 additions & 0 deletions internal/controller/linodecluster_controller_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ func TestGetIPPortCombo(t *testing.T) {
LinodeCluster: &infrav1alpha2.LinodeCluster{
Spec: infrav1alpha2.LinodeClusterSpec{
Network: infrav1alpha2.NetworkSpec{
EnableVPCBackends: true,
NodeBalancerBackendIPv4Range: "10.0.0.0/24",
},
VPCRef: &corev1.ObjectReference{
Expand Down Expand Up @@ -127,6 +128,7 @@ func TestGetIPPortCombo(t *testing.T) {
LinodeCluster: &infrav1alpha2.LinodeCluster{
Spec: infrav1alpha2.LinodeClusterSpec{
Network: infrav1alpha2.NetworkSpec{
EnableVPCBackends: true,
NodeBalancerBackendIPv4Range: "10.0.0.0/24",
},
VPCRef: &corev1.ObjectReference{
Expand Down Expand Up @@ -192,6 +194,7 @@ func TestGetIPPortCombo(t *testing.T) {
LinodeCluster: &infrav1alpha2.LinodeCluster{
Spec: infrav1alpha2.LinodeClusterSpec{
Network: infrav1alpha2.NetworkSpec{
EnableVPCBackends: true,
NodeBalancerBackendIPv4Range: "10.0.0.0/24",
AdditionalPorts: []infrav1alpha2.LinodeNBPortConfig{
{
Expand Down Expand Up @@ -227,6 +230,7 @@ func TestGetIPPortCombo(t *testing.T) {
LinodeCluster: &infrav1alpha2.LinodeCluster{
Spec: infrav1alpha2.LinodeClusterSpec{
Network: infrav1alpha2.NetworkSpec{
EnableVPCBackends: true,
NodeBalancerBackendIPv4Range: "10.0.0.0/24",
},
VPCRef: &corev1.ObjectReference{
Expand Down
Loading
Loading