@@ -848,6 +848,130 @@ func (l *loadbalancers) getVPCCreateOptions(ctx context.Context, service *v1.Ser
848848 return vpcCreateOpts , nil
849849}
850850
851+ // getFrontendVPCCreateOptions returns the VPC options for the NodeBalancer frontend VPC creation.
852+ // Order of precedence:
853+ // 1. Frontend IPv4/IPv6 Range Annotations - Explicit CIDR ranges
854+ // 2. Frontend VPC/Subnet Name Annotations - Resolve by name
855+ // 3. Frontend Subnet ID Annotation - Direct subnet ID
856+ func (l * loadbalancers ) getFrontendVPCCreateOptions (ctx context.Context , service * v1.Service ) ([]linodego.NodeBalancerVPCOptions , error ) {
857+ frontendIPv4Range , hasIPv4Range := service .GetAnnotations ()[annotations .NodeBalancerFrontendIPv4Range ]
858+ frontendIPv6Range , hasIPv6Range := service .GetAnnotations ()[annotations .NodeBalancerFrontendIPv6Range ]
859+ _ , hasVPCName := service .GetAnnotations ()[annotations .NodeBalancerFrontendVPCName ]
860+ _ , hasSubnetName := service .GetAnnotations ()[annotations .NodeBalancerFrontendSubnetName ]
861+ _ , hasSubnetID := service .GetAnnotations ()[annotations .NodeBalancerFrontendSubnetID ]
862+
863+ // If no frontend VPC annotations are present, return empty slice
864+ if ! hasIPv4Range && ! hasIPv6Range && ! hasVPCName && ! hasSubnetName && ! hasSubnetID {
865+ return nil , nil
866+ }
867+
868+ var subnetID int
869+ var err error
870+
871+ // Precedence 1: IPv4/IPv6 Range Annotations - Explicit CIDR ranges
872+ if hasIPv4Range || hasIPv6Range {
873+ if err := validateNodeBalancerFrontendIPv4Range (frontendIPv4Range ); err != nil {
874+ return nil , err
875+ }
876+ if err := validateNodeBalancerFrontendIPv6Range (frontendIPv6Range ); err != nil {
877+ return nil , err
878+ }
879+ if frontendSubnetID , ok := service .GetAnnotations ()[annotations .NodeBalancerFrontendSubnetID ]; ok {
880+ subnetID , err = strconv .Atoi (frontendSubnetID )
881+ if err != nil {
882+ return nil , fmt .Errorf ("invalid frontend subnet ID: %w" , err )
883+ }
884+ } else {
885+ subnetID , err = l .getFrontendSubnetIDForSVC (ctx , service )
886+ if err != nil {
887+ return nil , err
888+ }
889+ }
890+
891+ vpcCreateOpts := []linodego.NodeBalancerVPCOptions {
892+ {
893+ SubnetID : subnetID ,
894+ IPv4Range : frontendIPv4Range ,
895+ IPv6Range : frontendIPv6Range ,
896+ },
897+ }
898+ return vpcCreateOpts , nil
899+ }
900+
901+ // Precedence 2: VPC/Subnet Name Annotations - Resolve by name
902+ if hasVPCName || hasSubnetName {
903+ subnetID , err = l .getFrontendSubnetIDForSVC (ctx , service )
904+ if err != nil {
905+ return nil , err
906+ }
907+
908+ vpcCreateOpts := []linodego.NodeBalancerVPCOptions {
909+ {
910+ SubnetID : subnetID ,
911+ },
912+ }
913+ return vpcCreateOpts , nil
914+ }
915+
916+ // Precedence 3: Subnet ID Annotation - Direct subnet ID
917+ if hasSubnetID {
918+ frontendSubnetID := service .GetAnnotations ()[annotations .NodeBalancerFrontendSubnetID ]
919+ subnetID , err = strconv .Atoi (frontendSubnetID )
920+ if err != nil {
921+ return nil , fmt .Errorf ("invalid frontend subnet ID: %w" , err )
922+ }
923+
924+ vpcCreateOpts := []linodego.NodeBalancerVPCOptions {
925+ {
926+ SubnetID : subnetID ,
927+ },
928+ }
929+ return vpcCreateOpts , nil
930+ }
931+
932+ return nil , nil
933+ }
934+
935+ // getFrontendSubnetIDForSVC returns the subnet ID for the frontend VPC configuration.
936+ // Following precedence rules are applied:
937+ // 1. If the service has an annotation for FrontendSubnetID, use that.
938+ // 2. If the service has annotations specifying FrontendVPCName or FrontendSubnetName, use them.
939+ // 3. Return error if no VPC configuration is found.
940+ func (l * loadbalancers ) getFrontendSubnetIDForSVC (ctx context.Context , service * v1.Service ) (int , error ) {
941+ // Check if the service has an annotation for FrontendSubnetID
942+ if specifiedSubnetID , ok := service .GetAnnotations ()[annotations .NodeBalancerFrontendSubnetID ]; ok {
943+ subnetID , err := strconv .Atoi (specifiedSubnetID )
944+ if err != nil {
945+ return 0 , fmt .Errorf ("invalid frontend subnet ID: %w" , err )
946+ }
947+ return subnetID , nil
948+ }
949+
950+ specifiedVPCName , vpcOk := service .GetAnnotations ()[annotations .NodeBalancerFrontendVPCName ]
951+ specifiedSubnetName , subnetOk := service .GetAnnotations ()[annotations .NodeBalancerFrontendSubnetName ]
952+
953+ // If no VPCName or SubnetName is specified, return error
954+ if ! vpcOk && ! subnetOk {
955+ return 0 , fmt .Errorf ("frontend VPC configuration requires either vpc-name, subnet-name, or subnet-id annotations" )
956+ }
957+
958+ // Require both VPC name and subnet name when using name-based resolution
959+ if ! vpcOk {
960+ return 0 , fmt .Errorf ("frontend VPC configuration with subnet-name requires vpc-name annotation" )
961+ }
962+ if ! subnetOk {
963+ return 0 , fmt .Errorf ("frontend VPC configuration with vpc-name requires subnet-name annotation" )
964+ }
965+
966+ vpcID , err := services .GetVPCID (ctx , l .client , specifiedVPCName )
967+ if err != nil {
968+ return 0 , fmt .Errorf ("failed to get VPC ID for frontend VPC '%s': %w" , specifiedVPCName , err )
969+ }
970+
971+ // Use the VPC ID and Subnet Name to get the subnet ID
972+ return services .GetSubnetID (ctx , l .client , vpcID , specifiedSubnetName )
973+ }
974+
851975func (l * loadbalancers ) createNodeBalancer (ctx context.Context , clusterName string , service * v1.Service , configs []* linodego.NodeBalancerConfigCreateOptions ) (lb * linodego.NodeBalancer , err error ) {
852976 connThrottle := getConnectionThrottle (service )
853977
@@ -870,6 +994,13 @@ func (l *loadbalancers) createNodeBalancer(ctx context.Context, clusterName stri
870994 }
871995 }
872996
997+ // Add frontend VPC configuration
998+ if frontendVPCs , err := l .getFrontendVPCCreateOptions (ctx , service ); err != nil {
999+ return nil , err
1000+ } else if len (frontendVPCs ) > 0 {
1001+ createOpts .FrontendVPCs = frontendVPCs
1002+ }
1003+
8731004 // Check for static IPv4 address annotation
8741005 if ipv4 , ok := service .GetAnnotations ()[annotations .AnnLinodeLoadBalancerReservedIPv4 ]; ok {
8751006 createOpts .IPv4 = & ipv4
@@ -1336,6 +1467,12 @@ func makeLoadBalancerStatus(service *v1.Service, nb *linodego.NodeBalancer) *v1.
13361467 }
13371468 }
13381469
1470+ // Debug info log: Is a frontend VPC NodeBalancer?
1471+ isFrontendVPC := nb .FrontendAddressType != nil && * nb .FrontendAddressType == "vpc"
1472+ if isFrontendVPC {
1473+ klog .V (4 ).Infof ("NodeBalancer (%d) is using frontend VPC address type" , nb .ID )
1474+ }
1475+
13391476 // Check for per-service IPv6 annotation first, then fall back to global setting
13401477 useIPv6 := getServiceBoolAnnotation (service , annotations .AnnLinodeEnableIPv6Ingress ) || options .Options .EnableIPv6ForLoadBalancers
13411478
@@ -1403,6 +1540,32 @@ func validateNodeBalancerBackendIPv4Range(backendIPv4Range string) error {
14031540 return nil
14041541}
14051542
1543+ // validateNodeBalancerFrontendIPv4Range validates the frontend IPv4 range annotation.
1544+ // Performs basic CIDR format validation.
1545+ func validateNodeBalancerFrontendIPv4Range (frontendIPv4Range string ) error {
1546+ if frontendIPv4Range == "" {
1547+ return nil
1548+ }
1549+ _ , _ , err := net .ParseCIDR (frontendIPv4Range )
1550+ if err != nil {
1551+ return fmt .Errorf ("invalid frontend IPv4 range '%s': %w" , frontendIPv4Range , err )
1552+ }
1553+ return nil
1554+ }
1555+
1556+ // validateNodeBalancerFrontendIPv6Range validates the frontend IPv6 range annotation.
1557+ // Performs basic CIDR format validation.
1558+ func validateNodeBalancerFrontendIPv6Range (frontendIPv6Range string ) error {
1559+ if frontendIPv6Range == "" {
1560+ return nil
1561+ }
1562+ _ , _ , err := net .ParseCIDR (frontendIPv6Range )
1563+ if err != nil {
1564+ return fmt .Errorf ("invalid frontend IPv6 range '%s': %w" , frontendIPv6Range , err )
1565+ }
1566+ return nil
1567+ }
1568+
14061569// isCIDRWithinCIDR returns true if the inner CIDR is within the outer CIDR.
14071570func isCIDRWithinCIDR (outer , inner string ) (bool , error ) {
14081571 _ , ipNet1 , err := net .ParseCIDR (outer )
0 commit comments