Skip to content

Commit 39b5474

Browse files
authored
Merge pull request #501 from linode/nb-frontend-vpc
[feat] Add frontend VPC support for NodeBalancers
2 parents f9d4092 + ff73de6 commit 39b5474

File tree

9 files changed

+560
-8
lines changed

9 files changed

+560
-8
lines changed

cloud/annotations/annotations.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,10 @@ const (
5353
NodeBalancerBackendVPCName = "service.beta.kubernetes.io/linode-loadbalancer-backend-vpc-name"
5454
NodeBalancerBackendSubnetName = "service.beta.kubernetes.io/linode-loadbalancer-backend-subnet-name"
5555
NodeBalancerBackendSubnetID = "service.beta.kubernetes.io/linode-loadbalancer-backend-subnet-id"
56+
57+
NodeBalancerFrontendIPv4Range = "service.beta.kubernetes.io/linode-loadbalancer-frontend-ipv4-range"
58+
NodeBalancerFrontendIPv6Range = "service.beta.kubernetes.io/linode-loadbalancer-frontend-ipv6-range"
59+
NodeBalancerFrontendVPCName = "service.beta.kubernetes.io/linode-loadbalancer-frontend-vpc-name"
60+
NodeBalancerFrontendSubnetName = "service.beta.kubernetes.io/linode-loadbalancer-frontend-subnet-name"
61+
NodeBalancerFrontendSubnetID = "service.beta.kubernetes.io/linode-loadbalancer-frontend-subnet-id"
5662
)

cloud/linode/loadbalancers.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,19 @@ func (l *loadbalancers) getNodeBalancerByIP(ctx context.Context, service *v1.Ser
726726
if len(lbs) == 0 {
727727
return nil, lbNotFoundError{serviceNn: getServiceNn(service)}
728728
}
729+
730+
// filter by subnet ID if specified for frontend vpc ip
731+
frontendSubnetID := service.GetAnnotations()[annotations.NodeBalancerFrontendSubnetID]
732+
if frontendSubnetID != "" {
733+
for _, lb := range lbs {
734+
if lb.FrontendAddressType != nil && *lb.FrontendAddressType == "vpc" &&
735+
lb.FrontendVPCSubnetID != nil && strconv.Itoa(*lb.FrontendVPCSubnetID) == frontendSubnetID {
736+
return &lb, nil
737+
}
738+
}
739+
return nil, lbNotFoundError{serviceNn: getServiceNn(service)}
740+
}
741+
729742
klog.V(2).Infof("found NodeBalancer (%d) for service (%s) via IP (%s)", lbs[0].ID, getServiceNn(service), ip.String())
730743
return &lbs[0], nil
731744
}
@@ -860,6 +873,74 @@ func (l *loadbalancers) getVPCCreateOptions(ctx context.Context, service *v1.Ser
860873
return vpcCreateOpts, nil
861874
}
862875

876+
// getFrontendVPCCreateOptions returns the VPC options for the NodeBalancer frontend VPC creation.
877+
// Order of precedence:
878+
// 1. Frontend Subnet ID Annotation - Direct subnet ID
879+
// 2. Frontend VPC/Subnet Name Annotations - Resolve by name
880+
// 3. Frontend IPv4/IPv6 Range Annotations - Optional CIDR ranges
881+
func (l *loadbalancers) getFrontendVPCCreateOptions(ctx context.Context, service *v1.Service) ([]linodego.NodeBalancerVPCOptions, error) {
882+
frontendIPv4Range, hasIPv4Range := service.GetAnnotations()[annotations.NodeBalancerFrontendIPv4Range]
883+
frontendIPv6Range, hasIPv6Range := service.GetAnnotations()[annotations.NodeBalancerFrontendIPv6Range]
884+
vpcName, hasVPCName := service.GetAnnotations()[annotations.NodeBalancerFrontendVPCName]
885+
subnetName, hasSubnetName := service.GetAnnotations()[annotations.NodeBalancerFrontendSubnetName]
886+
frontendSubnetID, hasSubnetID := service.GetAnnotations()[annotations.NodeBalancerFrontendSubnetID]
887+
888+
// If no frontend VPC annotations are present, do not configure a frontend VPC.
889+
if !hasIPv4Range && !hasIPv6Range && !hasVPCName && !hasSubnetName && !hasSubnetID {
890+
return nil, nil
891+
}
892+
893+
if err := validateNodeBalancerFrontendIPRange(frontendIPv4Range, "IPv4"); err != nil {
894+
return nil, err
895+
}
896+
if err := validateNodeBalancerFrontendIPRange(frontendIPv6Range, "IPv6"); err != nil {
897+
return nil, err
898+
}
899+
900+
var subnetID int
901+
var err error
902+
903+
switch {
904+
case hasSubnetID:
905+
subnetID, err = strconv.Atoi(frontendSubnetID)
906+
if err != nil {
907+
return nil, fmt.Errorf("invalid frontend subnet ID: %w", err)
908+
}
909+
case hasVPCName && hasSubnetName:
910+
subnetID, err = l.getSubnetIDByVPCAndSubnetNames(ctx, vpcName, subnetName)
911+
if err != nil {
912+
return nil, err
913+
}
914+
default:
915+
// Ranges are optional but still require a subnet to target.
916+
return nil, fmt.Errorf("frontend VPC configuration requires either subnet-id or both vpc-name and subnet-name annotations")
917+
}
918+
919+
vpcCreateOpts := []linodego.NodeBalancerVPCOptions{
920+
{
921+
SubnetID: subnetID,
922+
IPv4Range: frontendIPv4Range,
923+
IPv6Range: frontendIPv6Range,
924+
},
925+
}
926+
return vpcCreateOpts, nil
927+
}
928+
929+
// getSubnetIDByVPCAndSubnetNames returns the subnet ID for the given VPC name and subnet name.
930+
func (l *loadbalancers) getSubnetIDByVPCAndSubnetNames(ctx context.Context, vpcName, subnetName string) (int, error) {
931+
if vpcName == "" || subnetName == "" {
932+
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")
933+
}
934+
935+
vpcID, err := services.GetVPCID(ctx, l.client, vpcName)
936+
if err != nil {
937+
return 0, fmt.Errorf("failed to get VPC ID for frontend VPC '%s': %w", vpcName, err)
938+
}
939+
940+
// Use the VPC ID and Subnet Name to get the subnet ID
941+
return services.GetSubnetID(ctx, l.client, vpcID, subnetName)
942+
}
943+
863944
func (l *loadbalancers) createNodeBalancer(ctx context.Context, clusterName string, service *v1.Service, configs []*linodego.NodeBalancerConfigCreateOptions) (lb *linodego.NodeBalancer, err error) {
864945
connThrottle := getConnectionThrottle(service)
865946

@@ -882,6 +963,13 @@ func (l *loadbalancers) createNodeBalancer(ctx context.Context, clusterName stri
882963
}
883964
}
884965

966+
// Add frontend VPC configuration
967+
if frontendVPCs, err := l.getFrontendVPCCreateOptions(ctx, service); err != nil {
968+
return nil, err
969+
} else if len(frontendVPCs) > 0 {
970+
createOpts.FrontendVPCs = frontendVPCs
971+
}
972+
885973
// Check for static IPv4 address annotation
886974
if ipv4, ok := service.GetAnnotations()[annotations.AnnLinodeLoadBalancerReservedIPv4]; ok {
887975
createOpts.IPv4 = &ipv4
@@ -1348,6 +1436,10 @@ func makeLoadBalancerStatus(service *v1.Service, nb *linodego.NodeBalancer) *v1.
13481436
}
13491437
}
13501438

1439+
if nb.FrontendAddressType != nil && *nb.FrontendAddressType == "vpc" {
1440+
klog.V(4).Infof("NodeBalancer (%d) is using frontend VPC address type", nb.ID)
1441+
}
1442+
13511443
// Check for per-service IPv6 annotation first, then fall back to global setting
13521444
useIPv6 := getServiceBoolAnnotation(service, annotations.AnnLinodeEnableIPv6Ingress) || options.Options.EnableIPv6ForLoadBalancers
13531445

@@ -1415,6 +1507,17 @@ func validateNodeBalancerBackendIPv4Range(backendIPv4Range string) error {
14151507
return nil
14161508
}
14171509

1510+
func validateNodeBalancerFrontendIPRange(frontendIPRange, ipVersion string) error {
1511+
if frontendIPRange == "" {
1512+
return nil
1513+
}
1514+
_, _, err := net.ParseCIDR(frontendIPRange)
1515+
if err != nil {
1516+
return fmt.Errorf("invalid frontend %s range '%s': %w", ipVersion, frontendIPRange, err)
1517+
}
1518+
return nil
1519+
}
1520+
14181521
// isCIDRWithinCIDR returns true if the inner CIDR is within the outer CIDR.
14191522
func isCIDRWithinCIDR(outer, inner string) (bool, error) {
14201523
_, ipNet1, err := net.ParseCIDR(outer)

0 commit comments

Comments
 (0)