Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
6 changes: 6 additions & 0 deletions cloud/annotations/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,10 @@ const (
NodeBalancerBackendVPCName = "service.beta.kubernetes.io/linode-loadbalancer-backend-vpc-name"
NodeBalancerBackendSubnetName = "service.beta.kubernetes.io/linode-loadbalancer-backend-subnet-name"
NodeBalancerBackendSubnetID = "service.beta.kubernetes.io/linode-loadbalancer-backend-subnet-id"

NodeBalancerFrontendIPv4Range = "service.beta.kubernetes.io/linode-loadbalancer-frontend-ipv4-range"
NodeBalancerFrontendIPv6Range = "service.beta.kubernetes.io/linode-loadbalancer-frontend-ipv6-range"
NodeBalancerFrontendVPCName = "service.beta.kubernetes.io/linode-loadbalancer-frontend-vpc-name"
NodeBalancerFrontendSubnetName = "service.beta.kubernetes.io/linode-loadbalancer-frontend-subnet-name"
NodeBalancerFrontendSubnetID = "service.beta.kubernetes.io/linode-loadbalancer-frontend-subnet-id"
)
107 changes: 107 additions & 0 deletions cloud/linode/loadbalancers.go
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,91 @@ func (l *loadbalancers) getVPCCreateOptions(ctx context.Context, service *v1.Ser
return vpcCreateOpts, nil
}

// getFrontendVPCCreateOptions returns the VPC options for the NodeBalancer frontend VPC creation.
// Order of precedence:
// 1. Frontend Subnet ID Annotation - Direct subnet ID
// 2. Frontend VPC/Subnet Name Annotations - Resolve by name
// 3. Frontend IPv4/IPv6 Range Annotations - Optional CIDR ranges
func (l *loadbalancers) getFrontendVPCCreateOptions(ctx context.Context, service *v1.Service) ([]linodego.NodeBalancerVPCOptions, error) {
frontendIPv4Range, hasIPv4Range := service.GetAnnotations()[annotations.NodeBalancerFrontendIPv4Range]
frontendIPv6Range, hasIPv6Range := service.GetAnnotations()[annotations.NodeBalancerFrontendIPv6Range]
_, hasVPCName := service.GetAnnotations()[annotations.NodeBalancerFrontendVPCName]
_, hasSubnetName := service.GetAnnotations()[annotations.NodeBalancerFrontendSubnetName]
frontendSubnetID, hasSubnetID := service.GetAnnotations()[annotations.NodeBalancerFrontendSubnetID]

// If no frontend VPC annotations are present, do not configure a frontend VPC.
if !hasIPv4Range && !hasIPv6Range && !hasVPCName && !hasSubnetName && !hasSubnetID {
return nil, nil
}

if err := validateNodeBalancerFrontendIPRange(frontendIPv4Range, "IPv4"); err != nil {
return nil, err
}
if err := validateNodeBalancerFrontendIPRange(frontendIPv6Range, "IPv6"); err != nil {
return nil, err
}

var subnetID int
var err error

switch {
case hasSubnetID:
subnetID, err = strconv.Atoi(frontendSubnetID)
if err != nil {
return nil, fmt.Errorf("invalid frontend subnet ID: %w", err)
}
case hasVPCName || hasSubnetName:
subnetID, err = l.getFrontendSubnetIDForSVC(ctx, service)
if err != nil {
return nil, err
}
default:
// Ranges are optional but still require a subnet to target.
return nil, fmt.Errorf("frontend VPC configuration requires either subnet-id or both vpc-name and subnet-name annotations")
}

vpcCreateOpts := []linodego.NodeBalancerVPCOptions{
{
SubnetID: subnetID,
IPv4Range: frontendIPv4Range,
IPv6Range: frontendIPv6Range,
},
}
return vpcCreateOpts, nil
}

// getFrontendSubnetIDForSVC returns the subnet ID for the frontend VPC configuration.
// Following precedence rules are applied:
// 1. If the service has an annotation for FrontendSubnetID, use that.
// 2. If the service has annotations specifying FrontendVPCName or FrontendSubnetName, use them.
// 3. Return error if no VPC configuration is found.
func (l *loadbalancers) getFrontendSubnetIDForSVC(ctx context.Context, service *v1.Service) (int, error) {
// Check if the service has an annotation for FrontendSubnetID
if specifiedSubnetID, ok := service.GetAnnotations()[annotations.NodeBalancerFrontendSubnetID]; ok {
subnetID, err := strconv.Atoi(specifiedSubnetID)
if err != nil {
return 0, fmt.Errorf("invalid frontend subnet ID: %w", err)
}
return subnetID, nil
}

specifiedVPCName, vpcOk := service.GetAnnotations()[annotations.NodeBalancerFrontendVPCName]
specifiedSubnetName, subnetOk := service.GetAnnotations()[annotations.NodeBalancerFrontendSubnetName]

// If no VPCName or SubnetName is specified and no subnet-id is provided, return error
if !vpcOk || !subnetOk {
return 0, fmt.Errorf("frontend VPC configuration requires either subnet-id annotation or both vpc-name and subnet-name annotations")
}

vpcID, err := services.GetVPCID(ctx, l.client, specifiedVPCName)
if err != nil {
return 0, fmt.Errorf("failed to get VPC ID for frontend VPC '%s': %w", specifiedVPCName, err)
}

// Use the VPC ID and Subnet Name to get the subnet ID
return services.GetSubnetID(ctx, l.client, vpcID, specifiedSubnetName)
}

func (l *loadbalancers) createNodeBalancer(ctx context.Context, clusterName string, service *v1.Service, configs []*linodego.NodeBalancerConfigCreateOptions) (lb *linodego.NodeBalancer, err error) {
connThrottle := getConnectionThrottle(service)

Expand All @@ -870,6 +955,13 @@ func (l *loadbalancers) createNodeBalancer(ctx context.Context, clusterName stri
}
}

// Add frontend VPC configuration
if frontendVPCs, err := l.getFrontendVPCCreateOptions(ctx, service); err != nil {
return nil, err
} else if len(frontendVPCs) > 0 {
createOpts.FrontendVPCs = frontendVPCs
}

// Check for static IPv4 address annotation
if ipv4, ok := service.GetAnnotations()[annotations.AnnLinodeLoadBalancerReservedIPv4]; ok {
createOpts.IPv4 = &ipv4
Expand Down Expand Up @@ -1336,6 +1428,10 @@ func makeLoadBalancerStatus(service *v1.Service, nb *linodego.NodeBalancer) *v1.
}
}

if nb.FrontendAddressType != nil && *nb.FrontendAddressType == "vpc" {
klog.V(4).Infof("NodeBalancer (%d) is using frontend VPC address type", nb.ID)
}

// Check for per-service IPv6 annotation first, then fall back to global setting
useIPv6 := getServiceBoolAnnotation(service, annotations.AnnLinodeEnableIPv6Ingress) || options.Options.EnableIPv6ForLoadBalancers

Expand Down Expand Up @@ -1403,6 +1499,17 @@ func validateNodeBalancerBackendIPv4Range(backendIPv4Range string) error {
return nil
}

func validateNodeBalancerFrontendIPRange(frontendIPRange, ipVersion string) error {
if frontendIPRange == "" {
return nil
}
_, _, err := net.ParseCIDR(frontendIPRange)
if err != nil {
return fmt.Errorf("invalid frontend %s range '%s': %w", ipVersion, frontendIPRange, err)
}
return nil
}

// isCIDRWithinCIDR returns true if the inner CIDR is within the outer CIDR.
func isCIDRWithinCIDR(outer, inner string) (bool, error) {
_, ipNet1, err := net.ParseCIDR(outer)
Expand Down
Loading
Loading