diff --git a/cloud/annotations/annotations.go b/cloud/annotations/annotations.go index 76e2d83f..197542bb 100644 --- a/cloud/annotations/annotations.go +++ b/cloud/annotations/annotations.go @@ -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" ) diff --git a/cloud/linode/loadbalancers.go b/cloud/linode/loadbalancers.go index 681f4b75..8482d26d 100644 --- a/cloud/linode/loadbalancers.go +++ b/cloud/linode/loadbalancers.go @@ -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) @@ -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 @@ -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 @@ -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) diff --git a/cloud/linode/loadbalancers_test.go b/cloud/linode/loadbalancers_test.go index fdb09b7c..0072d0cf 100644 --- a/cloud/linode/loadbalancers_test.go +++ b/cloud/linode/loadbalancers_test.go @@ -18,6 +18,7 @@ import ( "testing" ciliumclient "github.com/cilium/cilium/pkg/k8s/client/clientset/versioned/typed/cilium.io/v2alpha1" + "github.com/golang/mock/gomock" "github.com/linode/linodego" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" @@ -29,6 +30,7 @@ import ( "github.com/linode/linode-cloud-controller-manager/cloud/annotations" "github.com/linode/linode-cloud-controller-manager/cloud/linode/client" + "github.com/linode/linode-cloud-controller-manager/cloud/linode/client/mocks" "github.com/linode/linode-cloud-controller-manager/cloud/linode/options" "github.com/linode/linode-cloud-controller-manager/cloud/linode/services" ) @@ -5430,3 +5432,348 @@ func Test_validateNodeBalancerBackendIPv4Range(t *testing.T) { }) } } + +func Test_validateNodeBalancerFrontendIPRange(t *testing.T) { + type args struct { + frontendIPRange string + ipVersion string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Valid IPv4 range", + args: args{frontendIPRange: "10.100.5.0/24", ipVersion: "IPv4"}, + wantErr: false, + }, + { + name: "Invalid IPv4 range - no CIDR", + args: args{frontendIPRange: "10.100.5.0", ipVersion: "IPv4"}, + wantErr: true, + }, + { + name: "Invalid IPv4 range - malformed", + args: args{frontendIPRange: "not-an-ip", ipVersion: "IPv4"}, + wantErr: true, + }, + { + name: "Empty IPv4 range should pass", + args: args{frontendIPRange: "", ipVersion: "IPv4"}, + wantErr: false, + }, + { + name: "Valid IPv6 range", + args: args{frontendIPRange: "2001:db80:1005::/48", ipVersion: "IPv6"}, + wantErr: false, + }, + { + name: "Invalid IPv6 range - no CIDR", + args: args{frontendIPRange: "2001:db80:1005::", ipVersion: "IPv6"}, + wantErr: true, + }, + { + name: "Invalid IPv6 range - malformed", + args: args{frontendIPRange: "not-an-ipv6", ipVersion: "IPv6"}, + wantErr: true, + }, + { + name: "Empty IPv6 range should pass", + args: args{frontendIPRange: "", ipVersion: "IPv6"}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := validateNodeBalancerFrontendIPRange(tt.args.frontendIPRange, tt.args.ipVersion); (err != nil) != tt.wantErr { + t.Errorf("validateNodeBalancerFrontendIPRange() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_makeLoadBalancerStatus_FrontendVPC(t *testing.T) { + type args struct { + service *v1.Service + nb *linodego.NodeBalancer + } + tests := []struct { + name string + args args + want *v1.LoadBalancerStatus + }{ + { + name: "Frontend VPC NodeBalancer with IPv4", + args: args{ + service: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{}, + }, + }, + nb: &linodego.NodeBalancer{ + ID: 123, + Hostname: &[]string{"nb-123.example.com"}[0], + IPv4: &[]string{"10.100.5.10"}[0], + IPv6: nil, + FrontendAddressType: &[]string{"vpc"}[0], + }, + }, + want: &v1.LoadBalancerStatus{ + Ingress: []v1.LoadBalancerIngress{ + { + Hostname: "nb-123.example.com", + IP: "10.100.5.10", + }, + }, + }, + }, + { + name: "Frontend VPC NodeBalancer with IPv4 and IPv6", + args: args{ + service: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + annotations.AnnLinodeEnableIPv6Ingress: "true", + }, + }, + }, + nb: &linodego.NodeBalancer{ + ID: 123, + Hostname: &[]string{"nb-123.example.com"}[0], + IPv4: &[]string{"10.100.5.10"}[0], + IPv6: &[]string{"2001:db80:1005::10"}[0], + FrontendAddressType: &[]string{"vpc"}[0], + }, + }, + want: &v1.LoadBalancerStatus{ + Ingress: []v1.LoadBalancerIngress{ + { + Hostname: "nb-123.example.com", + IP: "10.100.5.10", + }, + { + Hostname: "nb-123.example.com", + IP: "2001:db80:1005::10", + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := makeLoadBalancerStatus(tt.args.service, tt.args.nb) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("makeLoadBalancerStatus() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_getFrontendVPCCreateOptions(t *testing.T) { + type args struct { + service *v1.Service + } + tests := []struct { + name string + args args + want []linodego.NodeBalancerVPCOptions + wantErr bool + prepareMock func(*mocks.MockClient) + }{ + { + name: "No frontend VPC annotations", + args: args{ + service: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{}, + }, + }, + }, + want: nil, + wantErr: false, + }, + { + name: "Frontend subnet id only", + args: args{ + service: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + annotations.NodeBalancerFrontendSubnetID: "123", + }, + }, + }, + }, + want: []linodego.NodeBalancerVPCOptions{ + { + SubnetID: 123, + }, + }, + wantErr: false, + }, + { + name: "Frontend IPv4 range + subnet id", + args: args{ + service: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + annotations.NodeBalancerFrontendIPv4Range: "10.100.5.0/24", + annotations.NodeBalancerFrontendSubnetID: "123", + }, + }, + }, + }, + want: []linodego.NodeBalancerVPCOptions{ + { + SubnetID: 123, + IPv4Range: "10.100.5.0/24", + }, + }, + wantErr: false, + }, + { + name: "Frontend IPv6 range + subnet id", + args: args{ + service: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + annotations.NodeBalancerFrontendIPv6Range: "2001:db80:1005::/48", + annotations.NodeBalancerFrontendSubnetID: "123", + }, + }, + }, + }, + want: []linodego.NodeBalancerVPCOptions{ + { + SubnetID: 123, + IPv6Range: "2001:db80:1005::/48", + }, + }, + wantErr: false, + }, + { + name: "Frontend ranges without subnet selector should error", + args: args{ + service: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + annotations.NodeBalancerFrontendIPv4Range: "10.100.5.0/24", + }, + }, + }, + }, + want: nil, + wantErr: true, + }, + { + name: "Frontend vpc-name only should error", + args: args{ + service: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + annotations.NodeBalancerFrontendVPCName: "my-vpc", + }, + }, + }, + }, + want: nil, + wantErr: true, + }, + { + name: "Frontend subnet-name only should error", + args: args{ + service: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + annotations.NodeBalancerFrontendSubnetName: "frontend-subnet", + }, + }, + }, + }, + want: nil, + wantErr: true, + }, + { + name: "Frontend invalid subnet-id should error", + args: args{ + service: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + annotations.NodeBalancerFrontendSubnetID: "abc", + }, + }, + }, + }, + want: nil, + wantErr: true, + }, + { + name: "Frontend VPC and subnet names", + args: args{ + service: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + annotations.NodeBalancerFrontendVPCName: "my-vpc", + annotations.NodeBalancerFrontendSubnetName: "frontend-subnet", + }, + }, + }, + }, + want: []linodego.NodeBalancerVPCOptions{ + { + SubnetID: 456, + }, + }, + wantErr: false, + prepareMock: func(m *mocks.MockClient) { + m.EXPECT().ListVPCs(gomock.Any(), gomock.Any()).Return([]linodego.VPC{{ID: 111, Label: "my-vpc"}}, nil) + m.EXPECT().ListVPCSubnets(gomock.Any(), 111, gomock.Any()).Return([]linodego.VPCSubnet{{ID: 456, Label: "frontend-subnet"}}, nil) + }, + }, + { + name: "Frontend subnet-id should take precedence over names", + args: args{ + service: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + annotations.NodeBalancerFrontendSubnetID: "123", + annotations.NodeBalancerFrontendVPCName: "my-vpc", + annotations.NodeBalancerFrontendSubnetName: "frontend-subnet", + }, + }, + }, + }, + want: []linodego.NodeBalancerVPCOptions{ + { + SubnetID: 123, + }, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockClient(ctrl) + if tt.prepareMock != nil { + tt.prepareMock(mockClient) + } + + l := &loadbalancers{ + client: mockClient, + } + got, err := l.getFrontendVPCCreateOptions(context.Background(), tt.args.service) + if (err != nil) != tt.wantErr { + t.Errorf("getFrontendVPCCreateOptions() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && !reflect.DeepEqual(got, tt.want) { + t.Errorf("getFrontendVPCCreateOptions() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cloud/linode/route_controller.go b/cloud/linode/route_controller.go index 31fc0b9d..bf785849 100644 --- a/cloud/linode/route_controller.go +++ b/cloud/linode/route_controller.go @@ -245,9 +245,8 @@ func (r *routes) DeleteRoute(ctx context.Context, clusterName string, route *clo func (r *routes) handleInterfaces(ctx context.Context, intfRoutes []string, linodeInterfaceRoutes []linodego.VPCInterfaceIPv4RangeCreateOptions, instance *linodego.Instance, intfVPCIP linodego.VPCIP, route *cloudprovider.Route) error { if instance.InterfaceGeneration == linodego.GenerationLinode { interfaceUpdateOptions := linodego.LinodeInterfaceUpdateOptions{ - VPC: &linodego.VPCInterfaceCreateOptions{ - SubnetID: intfVPCIP.SubnetID, - IPv4: &linodego.VPCInterfaceIPv4CreateOptions{Ranges: linodeInterfaceRoutes}, + VPC: &linodego.VPCInterfaceUpdateOptions{ + IPv4: &linodego.VPCInterfaceIPv4CreateOptions{Ranges: &linodeInterfaceRoutes}, }, } resp, err := r.client.UpdateInterface(ctx, instance.ID, intfVPCIP.InterfaceID, interfaceUpdateOptions) diff --git a/docs/configuration/annotations.md b/docs/configuration/annotations.md index a497032b..2d81ad21 100644 --- a/docs/configuration/annotations.md +++ b/docs/configuration/annotations.md @@ -43,6 +43,12 @@ The keys and the values in [annotations must be strings](https://kubernetes.io/d | `backend-ipv4-range` | string | | The IPv4 range from VPC subnet to be applied to the NodeBalancer backend. See [Nodebalancer VPC Configuration](#nodebalancer-vpc-configuration) | | `backend-vpc-name` | string | | VPC which is connected to the NodeBalancer backend. See [Nodebalancer VPC Configuration](#nodebalancer-vpc-configuration) | | `backend-subnet-name` | string | | Subnet within VPC which is connected to the NodeBalancer backend. See [Nodebalancer VPC Configuration](#nodebalancer-vpc-configuration) | +| `backend-subnet-id` | string | | Subnet ID within VPC which is connected to the NodeBalancer backend. See [Nodebalancer VPC Configuration](#nodebalancer-vpc-configuration) | +| `frontend-subnet-id` | string | | Subnet ID for the NodeBalancer frontend VPC configuration. See [Nodebalancer VPC Configuration](#nodebalancer-vpc-configuration) | +| `frontend-vpc-name` | string | | Frontend VPC name for the NodeBalancer frontend VPC configuration. See [Nodebalancer VPC Configuration](#nodebalancer-vpc-configuration) | +| `frontend-subnet-name` | string | | Frontend subnet name for the NodeBalancer frontend VPC configuration. See [Nodebalancer VPC Configuration](#nodebalancer-vpc-configuration) | +| `frontend-ipv4-range` | string | | Optional IPv4 CIDR range from the frontend subnet. See [Nodebalancer VPC Configuration](#nodebalancer-vpc-configuration) | +| `frontend-ipv6-range` | string | | Optional IPv6 CIDR range from the frontend subnet. See [Nodebalancer VPC Configuration](#nodebalancer-vpc-configuration) | | `reserved-ipv4` | string | | An existing Reserved IPv4 address that wil be used to initialize the NodeBalancer instance. See [LoadBalancer Configuration](loadbalancer.md#reserved-ipv4-addresses)) | ### Port Specific Configuration @@ -135,8 +141,18 @@ metadata: metadata: annotations: service.beta.kubernetes.io/linode-loadbalancer-backend-ipv4-range: "10.100.0.0/30" - service.beta.kubernetes.io/linode-loadbalancer-vpc-name: "vpc1" - service.beta.kubernetes.io/linode-loadbalancer-subnet-name: "subnet1" + service.beta.kubernetes.io/linode-loadbalancer-backend-vpc-name: "vpc1" + service.beta.kubernetes.io/linode-loadbalancer-backend-subnet-name: "subnet1" +``` + +Frontend VPC configuration: +```yaml +metadata: + annotations: + service.beta.kubernetes.io/linode-loadbalancer-frontend-subnet-id: "169341" + # Optional: + # service.beta.kubernetes.io/linode-loadbalancer-frontend-ipv4-range: "10.0.0.0/24" + # service.beta.kubernetes.io/linode-loadbalancer-frontend-ipv6-range: "2001:db8::/64" ``` ### Service with IPv6 Address @@ -151,7 +167,7 @@ For more examples and detailed configuration options, see: - [Firewall Configuration](firewall.md) - [Basic Service Examples](../examples/basic.md) - [Advanced Configuration Examples](../examples/advanced.md) -- [Complete Stack Example](../examples/complete-stack.md) +- [Examples](../examples/README.md) See also: - [Environment Variables](environment.md) diff --git a/docs/configuration/loadbalancer.md b/docs/configuration/loadbalancer.md index 0cce5f7a..742d3e37 100644 --- a/docs/configuration/loadbalancer.md +++ b/docs/configuration/loadbalancer.md @@ -194,10 +194,43 @@ By default, CCM uses first VPC and Subnet name configured with it to attach Node metadata: annotations: service.beta.kubernetes.io/linode-loadbalancer-backend-ipv4-range: "10.100.0.4/30" - service.beta.kubernetes.io/linode-loadbalancer-vpc-name: "vpc1" - service.beta.kubernetes.io/linode-loadbalancer-subnet-name: "subnet1" + service.beta.kubernetes.io/linode-loadbalancer-backend-vpc-name: "vpc1" + service.beta.kubernetes.io/linode-loadbalancer-backend-subnet-name: "subnet1" ``` +### Configuring NodeBalancer frontend with VPC + +NodeBalancers can optionally be configured with a VPC-based frontend address. + +Frontend VPC configuration supports the following annotations: + +1. Choose the frontend subnet: + + - `service.beta.kubernetes.io/linode-loadbalancer-frontend-subnet-id` (preferred) + - OR `service.beta.kubernetes.io/linode-loadbalancer-frontend-vpc-name` and `service.beta.kubernetes.io/linode-loadbalancer-frontend-subnet-name` + +2. Optionally constrain the frontend address assignment within the subnet: + + - `service.beta.kubernetes.io/linode-loadbalancer-frontend-ipv4-range` (CIDR) + - `service.beta.kubernetes.io/linode-loadbalancer-frontend-ipv6-range` (CIDR) + +Order of precedence: +- If `frontend-subnet-id` is set, it is used. +- Otherwise, `frontend-vpc-name` + `frontend-subnet-name` are used. +- IPv4/IPv6 range annotations are optional add-ons and require one of the subnet selectors above. + +Example: +```yaml +metadata: + annotations: + service.beta.kubernetes.io/linode-loadbalancer-frontend-subnet-id: "169341" + # Optional: + # service.beta.kubernetes.io/linode-loadbalancer-frontend-ipv4-range: "10.0.0.0/24" + # service.beta.kubernetes.io/linode-loadbalancer-frontend-ipv6-range: "2001:db8::/64" +``` + +For a complete working example, see `examples/vpc-frontend-example.yaml`. + If CCM is started with `--nodebalancer-backend-ipv4-subnet` flag, then it will not allow provisioning of nodebalancer unless subnet specified in service annotation lie within the subnet specified using the flag. This is to prevent accidental overlap between nodebalancer backend ips and pod CIDRs. ## Advanced Configuration diff --git a/docs/examples/README.md b/docs/examples/README.md index 156ebb70..235891da 100644 --- a/docs/examples/README.md +++ b/docs/examples/README.md @@ -16,6 +16,9 @@ This section provides working examples of common CCM configurations. Each exampl - Shared IP Load-Balancing - Custom Node Selection +3. **Frontend VPC NodeBalancer** + - `examples/vpc-frontend-example.yaml` + Note: To test UDP based NBs, one can use [test-server](https://github.com/rahulait/test-server) repo to run server using UDP protocol and then use the client commands in repo's readme to connect to the server. For testing these examples, see the [test script](https://github.com/linode/linode-cloud-controller-manager/blob/master/examples/test.sh). diff --git a/examples/vpc-frontend-example.yaml b/examples/vpc-frontend-example.yaml new file mode 100644 index 00000000..f1032509 --- /dev/null +++ b/examples/vpc-frontend-example.yaml @@ -0,0 +1,44 @@ +apiVersion: v1 +kind: Service +metadata: + name: svc-frontend-vpc-test + annotations: + # Specify the Subnet ID for the NodeBalancer Frontend + service.beta.kubernetes.io/linode-loadbalancer-frontend-subnet-id: "169341" + service.beta.kubernetes.io/linode-loadbalancer-nodebalancer-type: "premium" + # Optional: Specify an IPv4/IPv6 range within the subnet + # service.beta.kubernetes.io/linode-loadbalancer-frontend-ipv4-range: "10.0.0.0/24" + # service.beta.kubernetes.io/linode-loadbalancer-frontend-ipv6-range: "2001:db8::/64" + labels: + app: frontend-vpc-test +spec: + type: LoadBalancer + selector: + app: frontend-vpc-test + ports: + - name: http + protocol: TCP + port: 80 + targetPort: 80 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend-vpc-test + labels: + app: frontend-vpc-test +spec: + replicas: 1 + selector: + matchLabels: + app: frontend-vpc-test + template: + metadata: + labels: + app: frontend-vpc-test + spec: + containers: + - name: nginx + image: nginx:latest + ports: + - containerPort: 80 diff --git a/go.mod b/go.mod index 3ffe33a4..17af4e92 100644 --- a/go.mod +++ b/go.mod @@ -141,7 +141,7 @@ require ( golang.org/x/crypto v0.45.0 // indirect golang.org/x/mod v0.29.0 // indirect golang.org/x/net v0.47.0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/oauth2 v0.33.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/term v0.37.0 // indirect @@ -171,6 +171,7 @@ require ( ) replace ( + github.com/linode/linodego => github.com/komer3/linodego v0.0.0-20251201202808-852faf57b6e0 k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.34.1 k8s.io/cri-api => k8s.io/cri-api v0.34.1 k8s.io/cri-client => k8s.io/cri-client v0.34.1 diff --git a/go.sum b/go.sum index c1d5b23e..d7e5b6a9 100644 --- a/go.sum +++ b/go.sum @@ -186,6 +186,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/komer3/linodego v0.0.0-20251201202808-852faf57b6e0 h1:mlJ8Z51Aa6xG1cLbH7HjacHdb1raPQlLIJTO5s726dE= +github.com/komer3/linodego v0.0.0-20251201202808-852faf57b6e0/go.mod h1:u+mbth1igHGsd8VasP+8LKHrxuCYsVMHbY3fOYRh/FU= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -194,8 +196,6 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= -github.com/linode/linodego v1.58.0 h1:yf/tDTf5v74qcxqWWvu3bEDOmDT1ulG9NBDdivp0/oM= -github.com/linode/linodego v1.58.0/go.mod h1:ViH3Tun41yQdknbSyrdHz/iFDXsquLu+YwFdFneEZbY= github.com/mackerelio/go-osstat v0.2.6 h1:gs4U8BZeS1tjrL08tt5VUliVvSWP26Ai2Ob8Lr7f2i0= github.com/mackerelio/go-osstat v0.2.6/go.mod h1:lRy8V9ZuHpuRVZh+vyTkODeDPl3/d5MgXHtLSaqG8bA= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= @@ -393,8 +393,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= +golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=