@@ -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+
863944func (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.
14191522func isCIDRWithinCIDR (outer , inner string ) (bool , error ) {
14201523 _ , ipNet1 , err := net .ParseCIDR (outer )
0 commit comments