Skip to content
Draft
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"
)
163 changes: 163 additions & 0 deletions cloud/linode/loadbalancers.go
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,130 @@ 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 IPv4/IPv6 Range Annotations - Explicit CIDR ranges
// 2. Frontend VPC/Subnet Name Annotations - Resolve by name
// 3. Frontend Subnet ID Annotation - Direct subnet ID
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]
_, hasSubnetID := service.GetAnnotations()[annotations.NodeBalancerFrontendSubnetID]

// If no frontend VPC annotations are present, return empty slice
if !hasIPv4Range && !hasIPv6Range && !hasVPCName && !hasSubnetName && !hasSubnetID {
return nil, nil
}

var subnetID int
var err error

// Precedence 1: IPv4/IPv6 Range Annotations - Explicit CIDR ranges
if hasIPv4Range || hasIPv6Range {
if err = validateNodeBalancerFrontendIPv4Range(frontendIPv4Range); err != nil {
return nil, err
}
if err = validateNodeBalancerFrontendIPv6Range(frontendIPv6Range); err != nil {
return nil, err
}
if frontendSubnetID, ok := service.GetAnnotations()[annotations.NodeBalancerFrontendSubnetID]; ok {
subnetID, err = strconv.Atoi(frontendSubnetID)
if err != nil {
return nil, fmt.Errorf("invalid frontend subnet ID: %w", err)
}
} else {
subnetID, err = l.getFrontendSubnetIDForSVC(ctx, service)
if err != nil {
return nil, err
}
}

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

// Precedence 2: VPC/Subnet Name Annotations - Resolve by name
if hasVPCName || hasSubnetName {
subnetID, err = l.getFrontendSubnetIDForSVC(ctx, service)
if err != nil {
return nil, err
}

vpcCreateOpts := []linodego.NodeBalancerVPCOptions{
{
SubnetID: subnetID,
},
}
return vpcCreateOpts, nil
}

// Precedence 3: Subnet ID Annotation - Direct subnet ID
if hasSubnetID {
frontendSubnetID := service.GetAnnotations()[annotations.NodeBalancerFrontendSubnetID]
subnetID, err = strconv.Atoi(frontendSubnetID)
if err != nil {
return nil, fmt.Errorf("invalid frontend subnet ID: %w", err)
}

vpcCreateOpts := []linodego.NodeBalancerVPCOptions{
{
SubnetID: subnetID,
},
}
return vpcCreateOpts, nil
}

return nil, 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, return error
if !vpcOk && !subnetOk {
return 0, fmt.Errorf("frontend VPC configuration requires either vpc-name, subnet-name, or subnet-id annotations")
}

// Require both VPC name and subnet name when using name-based resolution
if !vpcOk {
return 0, fmt.Errorf("frontend VPC configuration with subnet-name requires vpc-name annotation")
}
if !subnetOk {
return 0, fmt.Errorf("frontend VPC configuration with vpc-name requires subnet-name annotation")
}

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 +994,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 +1467,12 @@ func makeLoadBalancerStatus(service *v1.Service, nb *linodego.NodeBalancer) *v1.
}
}

// Debug info log: Is a frontend VPC NodeBalancer?
isFrontendVPC := nb.FrontendAddressType != nil && *nb.FrontendAddressType == "vpc"
if isFrontendVPC {
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 +1540,32 @@ func validateNodeBalancerBackendIPv4Range(backendIPv4Range string) error {
return nil
}

// validateNodeBalancerFrontendIPv4Range validates the frontend IPv4 range annotation.
// Performs basic CIDR format validation.
func validateNodeBalancerFrontendIPv4Range(frontendIPv4Range string) error {
if frontendIPv4Range == "" {
return nil
}
_, _, err := net.ParseCIDR(frontendIPv4Range)
if err != nil {
return fmt.Errorf("invalid frontend IPv4 range '%s': %w", frontendIPv4Range, err)
}
return nil
}

// validateNodeBalancerFrontendIPv6Range validates the frontend IPv6 range annotation.
// Performs basic CIDR format validation.
func validateNodeBalancerFrontendIPv6Range(frontendIPv6Range string) error {
if frontendIPv6Range == "" {
return nil
}
_, _, err := net.ParseCIDR(frontendIPv6Range)
if err != nil {
return fmt.Errorf("invalid frontend IPv6 range '%s': %w", frontendIPv6Range, 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