Skip to content
Open
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
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"
)
90 changes: 90 additions & 0 deletions cloud/linode/loadbalancers.go
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,74 @@ 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]
vpcName, hasVPCName := service.GetAnnotations()[annotations.NodeBalancerFrontendVPCName]
subnetName, 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.getSubnetIDByVPCAndSubnetNames(ctx, vpcName, subnetName)
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
}

// getSubnetIDByVPCAndSubnetNames returns the subnet ID for the given VPC name and subnet name.
func (l *loadbalancers) getSubnetIDByVPCAndSubnetNames(ctx context.Context, vpcName, subnetName string) (int, error) {
if vpcName == "" || subnetName == "" {
return 0, fmt.Errorf("frontend VPC configuration requires either subnet-id annotation or both vpc-name and subnet-name annotations. No vpc-name or subnet-name annotation found")
}

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

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

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 +938,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 +1411,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 +1482,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