diff --git a/Makefile b/Makefile index 76be534f..ee410b5b 100644 --- a/Makefile +++ b/Makefile @@ -162,6 +162,7 @@ create-capl-cluster: .PHONY: patch-linode-ccm patch-linode-ccm: KUBECONFIG=$(KUBECONFIG_PATH) kubectl patch -n kube-system daemonset ccm-linode --type='json' -p="[{'op': 'replace', 'path': '/spec/template/spec/containers/0/image', 'value': '${IMG}'}]" + KUBECONFIG=$(KUBECONFIG_PATH) kubectl patch -n kube-system daemonset ccm-linode --type='json' -p='[{"op": "add", "path": "/spec/template/spec/containers/0/env/-", "value": {"name": "LINODE_API_VERSION", "value": "v4beta"}}]' KUBECONFIG=$(KUBECONFIG_PATH) kubectl rollout status -n kube-system daemonset/ccm-linode --timeout=600s KUBECONFIG=$(KUBECONFIG_PATH) kubectl -n kube-system get daemonset/ccm-linode -o yaml diff --git a/cloud/annotations/annotations.go b/cloud/annotations/annotations.go index 08df1e0b..ecbf96dc 100644 --- a/cloud/annotations/annotations.go +++ b/cloud/annotations/annotations.go @@ -34,4 +34,9 @@ const ( AnnLinodeHostUUID = "node.k8s.linode.com/host-uuid" AnnLinodeNodeIPSharingUpdated = "node.k8s.linode.com/ip-sharing-updated" + + NodeBalancerBackendIPv4Range = "service.beta.kubernetes.io/linode-loadbalancer-backend-ipv4-range" + + NodeBalancerBackendVPCName = "service.beta.kubernetes.io/linode-loadbalancer-backend-vpc-name" + NodeBalancerBackendSubnetName = "service.beta.kubernetes.io/linode-loadbalancer-backend-subnet-name" ) diff --git a/cloud/linode/fake_linode_test.go b/cloud/linode/fake_linode_test.go index aeb069d8..dbedc245 100644 --- a/cloud/linode/fake_linode_test.go +++ b/cloud/linode/fake_linode_test.go @@ -19,12 +19,15 @@ import ( const apiVersion = "v4" type fakeAPI struct { - t *testing.T - nb map[string]*linodego.NodeBalancer - nbc map[string]*linodego.NodeBalancerConfig - nbn map[string]*linodego.NodeBalancerNode - fw map[int]*linodego.Firewall // map of firewallID -> firewall - fwd map[int]map[int]*linodego.FirewallDevice // map of firewallID -> firewallDeviceID:FirewallDevice + t *testing.T + nb map[string]*linodego.NodeBalancer + nbc map[string]*linodego.NodeBalancerConfig + nbn map[string]*linodego.NodeBalancerNode + fw map[int]*linodego.Firewall // map of firewallID -> firewall + fwd map[int]map[int]*linodego.FirewallDevice // map of firewallID -> firewallDeviceID:FirewallDevice + nbvpcc map[string]*linodego.NodeBalancerVPCConfig + vpc map[int]*linodego.VPC + subnet map[int]*linodego.VPCSubnet requests map[fakeRequest]struct{} mux *http.ServeMux @@ -44,6 +47,9 @@ func newFake(t *testing.T) *fakeAPI { nbn: make(map[string]*linodego.NodeBalancerNode), fw: make(map[int]*linodego.Firewall), fwd: make(map[int]map[int]*linodego.FirewallDevice), + nbvpcc: make(map[string]*linodego.NodeBalancerVPCConfig), + vpc: make(map[int]*linodego.VPC), + subnet: make(map[int]*linodego.VPCSubnet), requests: make(map[fakeRequest]struct{}), mux: http.NewServeMux(), } @@ -117,6 +123,54 @@ func (f *fakeAPI) setupRoutes() { _, _ = w.Write(rr) }) + f.mux.HandleFunc("GET /v4/vpcs", func(w http.ResponseWriter, r *http.Request) { + res := 0 + data := []linodego.VPC{} + filter := r.Header.Get("X-Filter") + if filter == "" { + for _, v := range f.vpc { + data = append(data, *v) + } + } else { + var fs map[string]string + err := json.Unmarshal([]byte(filter), &fs) + if err != nil { + f.t.Fatal(err) + } + for _, v := range f.vpc { + if v.Label != "" && fs["label"] != "" && v.Label == fs["label"] { + data = append(data, *v) + } + } + } + + resp := paginatedResponse[linodego.VPC]{ + Page: 1, + Pages: 1, + Results: res, + Data: data, + } + rr, _ := json.Marshal(resp) + _, _ = w.Write(rr) + }) + + f.mux.HandleFunc("GET /v4/vpcs/{vpcId}/subnets", func(w http.ResponseWriter, r *http.Request) { + res := 0 + vpcID, err := strconv.Atoi(r.PathValue("vpcId")) + if err != nil { + f.t.Fatal(err) + } + + resp := paginatedResponse[linodego.VPCSubnet]{ + Page: 1, + Pages: 1, + Results: res, + Data: f.vpc[vpcID].Subnets, + } + rr, _ := json.Marshal(resp) + _, _ = w.Write(rr) + }) + f.mux.HandleFunc("GET /v4/nodebalancers/{nodeBalancerId}", func(w http.ResponseWriter, r *http.Request) { nb, found := f.nb[r.PathValue("nodeBalancerId")] if !found { @@ -462,6 +516,54 @@ func (f *fakeAPI) setupRoutes() { _, _ = w.Write(resp) }) + f.mux.HandleFunc("POST /v4/vpcs", func(w http.ResponseWriter, r *http.Request) { + vco := linodego.VPCCreateOptions{} + if err := json.NewDecoder(r.Body).Decode(&vco); err != nil { + f.t.Fatal(err) + } + + subnets := []linodego.VPCSubnet{} + for _, s := range vco.Subnets { + subnet := linodego.VPCSubnet{ + ID: rand.Intn(9999), + IPv4: s.IPv4, + Label: s.Label, + } + subnets = append(subnets, subnet) + f.subnet[subnet.ID] = &subnet + } + vpc := linodego.VPC{ + ID: rand.Intn(9999), + Label: vco.Label, + Description: vco.Description, + Region: vco.Region, + Subnets: subnets, + } + + f.vpc[vpc.ID] = &vpc + resp, err := json.Marshal(vpc) + if err != nil { + f.t.Fatal(err) + } + _, _ = w.Write(resp) + }) + + f.mux.HandleFunc("DELETE /v4/vpcs/{vpcId}", func(w http.ResponseWriter, r *http.Request) { + vpcid, err := strconv.Atoi(r.PathValue("vpcId")) + if err != nil { + f.t.Fatal(err) + } + + for k, v := range f.vpc { + if v.ID == vpcid { + for _, s := range v.Subnets { + delete(f.subnet, s.ID) + } + delete(f.vpc, k) + } + } + }) + f.mux.HandleFunc("POST /v4/networking/firewalls/{firewallId}/devices", func(w http.ResponseWriter, r *http.Request) { fdco := linodego.FirewallDeviceCreateOptions{} if err := json.NewDecoder(r.Body).Decode(&fdco); err != nil { diff --git a/cloud/linode/loadbalancers.go b/cloud/linode/loadbalancers.go index c945f7d0..b3f67023 100644 --- a/cloud/linode/loadbalancers.go +++ b/cloud/linode/loadbalancers.go @@ -383,8 +383,18 @@ func (l *loadbalancers) updateNodeBalancer( } // Add all of the Nodes to the config newNBNodes := make([]linodego.NodeBalancerConfigRebuildNodeOptions, 0, len(nodes)) + subnetID := 0 + _, ok := service.GetAnnotations()[annotations.NodeBalancerBackendIPv4Range] + if ok { + id, err := l.getSubnetIDForSVC(ctx, service) + if err != nil { + sentry.CaptureError(ctx, err) + return fmt.Errorf("Error getting subnet ID for service %s: %v", service.Name, err) + } + subnetID = id + } for _, node := range nodes { - newNodeOpts := l.buildNodeBalancerNodeConfigRebuildOptions(node, port.NodePort) + newNodeOpts := l.buildNodeBalancerNodeConfigRebuildOptions(node, port.NodePort, subnetID) oldNodeID, ok := oldNBNodeIDs[newNodeOpts.Address] if ok { newNodeOpts.ID = oldNodeID @@ -652,6 +662,20 @@ func (l *loadbalancers) createNodeBalancer(ctx context.Context, clusterName stri Type: nbType, } + backendIPv4Range, ok := service.GetAnnotations()[annotations.NodeBalancerBackendIPv4Range] + if ok { + subnetID, err := l.getSubnetIDForSVC(ctx, service) + if err != nil { + return nil, err + } + createOpts.VPCs = []linodego.NodeBalancerVPCOptions{ + { + SubnetID: subnetID, + IPv4Range: backendIPv4Range, + }, + } + } + fwid, ok := service.GetAnnotations()[annotations.AnnLinodeCloudFirewallID] if ok { firewallID, err := strconv.Atoi(fwid) @@ -768,6 +792,28 @@ func (l *loadbalancers) addTLSCert(ctx context.Context, service *v1.Service, nbC return nil } +// getSubnetIDForSVC returns the subnet ID for the service's VPC and subnet. +// By default, first VPCName and SubnetName are used to calculate subnet id for the service. +// If the service has annotations specifying VPCName and SubnetName, they are used instead. +func (l *loadbalancers) getSubnetIDForSVC(ctx context.Context, service *v1.Service) (int, error) { + if Options.VPCNames == "" { + return 0, fmt.Errorf("CCM not configured with VPC, cannot create NodeBalancer with specified annotation") + } + vpcName := strings.Split(Options.VPCNames, ",")[0] + if specifiedVPCName, ok := service.GetAnnotations()[annotations.NodeBalancerBackendVPCName]; ok { + vpcName = specifiedVPCName + } + vpcID, err := GetVPCID(ctx, l.client, vpcName) + if err != nil { + return 0, err + } + subnetName := strings.Split(Options.SubnetNames, ",")[0] + if specifiedSubnetName, ok := service.GetAnnotations()[annotations.NodeBalancerBackendSubnetName]; ok { + subnetName = specifiedSubnetName + } + return GetSubnetID(ctx, l.client, vpcID, subnetName) +} + // buildLoadBalancerRequest returns a linodego.NodeBalancer // requests for service across nodes. func (l *loadbalancers) buildLoadBalancerRequest(ctx context.Context, clusterName string, service *v1.Service, nodes []*v1.Node) (*linodego.NodeBalancer, error) { @@ -777,6 +823,16 @@ func (l *loadbalancers) buildLoadBalancerRequest(ctx context.Context, clusterNam ports := service.Spec.Ports configs := make([]*linodego.NodeBalancerConfigCreateOptions, 0, len(ports)) + subnetID := 0 + _, ok := service.GetAnnotations()[annotations.NodeBalancerBackendIPv4Range] + if ok { + id, err := l.getSubnetIDForSVC(ctx, service) + if err != nil { + return nil, err + } + subnetID = id + } + for _, port := range ports { if port.Protocol == v1.ProtocolUDP { return nil, fmt.Errorf("error creating NodeBalancer Config: ports with the UDP protocol are not supported") @@ -789,7 +845,7 @@ func (l *loadbalancers) buildLoadBalancerRequest(ctx context.Context, clusterNam createOpt := config.GetCreateOptions() for _, n := range nodes { - createOpt.Nodes = append(createOpt.Nodes, l.buildNodeBalancerNodeConfigRebuildOptions(n, port.NodePort).NodeBalancerNodeCreateOptions) + createOpt.Nodes = append(createOpt.Nodes, l.buildNodeBalancerNodeConfigRebuildOptions(n, port.NodePort, subnetID).NodeBalancerNodeCreateOptions) } configs = append(configs, &createOpt) @@ -809,10 +865,10 @@ func coerceString(s string, minLen, maxLen int, padding string) string { return s } -func (l *loadbalancers) buildNodeBalancerNodeConfigRebuildOptions(node *v1.Node, nodePort int32) linodego.NodeBalancerConfigRebuildNodeOptions { - return linodego.NodeBalancerConfigRebuildNodeOptions{ +func (l *loadbalancers) buildNodeBalancerNodeConfigRebuildOptions(node *v1.Node, nodePort int32, subnetID int) linodego.NodeBalancerConfigRebuildNodeOptions { + nodeOptions := linodego.NodeBalancerConfigRebuildNodeOptions{ NodeBalancerNodeCreateOptions: linodego.NodeBalancerNodeCreateOptions{ - Address: fmt.Sprintf("%v:%v", getNodePrivateIP(node), nodePort), + Address: fmt.Sprintf("%v:%v", getNodePrivateIP(node, subnetID), nodePort), // NodeBalancer backends must be 3-32 chars in length // If < 3 chars, pad node name with "node-" prefix Label: coerceString(node.Name, 3, 32, "node-"), @@ -820,6 +876,10 @@ func (l *loadbalancers) buildNodeBalancerNodeConfigRebuildOptions(node *v1.Node, Weight: 100, }, } + if subnetID != 0 { + nodeOptions.NodeBalancerNodeCreateOptions.SubnetID = subnetID + } + return nodeOptions } func (l *loadbalancers) retrieveKubeClient() error { @@ -926,13 +986,17 @@ func getPortConfigAnnotation(service *v1.Service, port int) (portConfigAnnotatio return annotation, nil } -// getNodePrivateIP should provide the Linode Private IP the NodeBalance -// will communicate with. When using a VLAN or VPC for the Kubernetes cluster -// network, this will not be the NodeInternalIP, so this prefers an annotation -// cluster operators may specify in such a situation. -func getNodePrivateIP(node *v1.Node) string { - if address, exists := node.Annotations[annotations.AnnLinodeNodePrivateIP]; exists { - return address +// getNodePrivateIP provides the Linode Backend IP the NodeBalancer will communicate with. +// If a service specifies NodeBalancerBackendIPv4Range annotation, it will +// use NodeInternalIP of node. +// For services which don't have NodeBalancerBackendIPv4Range annotation, +// Backend IP can be overwritten to the one specified using AnnLinodeNodePrivateIP +// annotation over the NodeInternalIP. +func getNodePrivateIP(node *v1.Node, subnetID int) string { + if subnetID == 0 { + if address, exists := node.Annotations[annotations.AnnLinodeNodePrivateIP]; exists { + return address + } } klog.Infof("Node %s, assigned IP addresses: %v", node.Name, node.Status.Addresses) diff --git a/cloud/linode/loadbalancers_test.go b/cloud/linode/loadbalancers_test.go index 7a0e9cc8..60261b36 100644 --- a/cloud/linode/loadbalancers_test.go +++ b/cloud/linode/loadbalancers_test.go @@ -152,6 +152,14 @@ func TestCCMLoadBalancers(t *testing.T) { name: "Create Load Balancer With Invalid Firewall ACL - NO Allow Or Deny", f: testCreateNodeBalanceWithNoAllowOrDenyList, }, + { + name: "Create Load Balancer With VPC Backend", + f: testCreateNodeBalancerWithVPCBackend, + }, + { + name: "Create Load Balancer With VPC Backend - Overwrite VPC Name and Subnet with Annotation", + f: testCreateNodeBalancerWithVPCAnnotationOverwrite, + }, { name: "Create Load Balancer With Global Tags set", f: testCreateNodeBalancerWithGlobalTags, @@ -481,6 +489,86 @@ func testCreateNodeBalancerWithGlobalTags(t *testing.T, client *linodego.Client, } } +func testCreateNodeBalancerWithVPCBackend(t *testing.T, client *linodego.Client, f *fakeAPI) { + // test when no VPCs are present + ann := map[string]string{ + annotations.NodeBalancerBackendIPv4Range: "10.100.0.0/30", + } + if err := testCreateNodeBalancer(t, client, f, ann, nil); err == nil { + t.Fatalf("expected nodebalancer creation to fail") + } + + f.ResetRequests() + + // provision vpc and test again + vpcNames := Options.VPCNames + subnetNames := Options.SubnetNames + defer func() { + Options.VPCNames = vpcNames + Options.SubnetNames = subnetNames + }() + Options.VPCNames = "test1" + Options.SubnetNames = "default" + _, _ = client.CreateVPC(context.TODO(), linodego.VPCCreateOptions{ + Label: "test1", + Description: "", + Region: "us-west", + Subnets: []linodego.VPCSubnetCreateOptions{ + { + Label: "default", + IPv4: "10.0.0.0/8", + }, + }, + }) + + err := testCreateNodeBalancer(t, client, f, ann, nil) + if err != nil { + t.Fatalf("expected a nil error, got %v", err) + } +} + +func testCreateNodeBalancerWithVPCAnnotationOverwrite(t *testing.T, client *linodego.Client, f *fakeAPI) { + // provision multiple vpcs + vpcNames := Options.VPCNames + defer func() { + Options.VPCNames = vpcNames + }() + Options.VPCNames = "test1" + + _, _ = client.CreateVPC(context.TODO(), linodego.VPCCreateOptions{ + Label: "test1", + Description: "", + Region: "us-west", + Subnets: []linodego.VPCSubnetCreateOptions{ + { + Label: "default", + IPv4: "10.0.0.0/8", + }, + }, + }) + + _, _ = client.CreateVPC(context.TODO(), linodego.VPCCreateOptions{ + Label: "test2", + Description: "", + Region: "us-west", + Subnets: []linodego.VPCSubnetCreateOptions{ + { + Label: "subnet1", + IPv4: "10.0.0.0/8", + }, + }, + }) + ann := map[string]string{ + annotations.NodeBalancerBackendIPv4Range: "10.100.0.0/30", + annotations.NodeBalancerBackendVPCName: "test2", + annotations.NodeBalancerBackendSubnetName: "subnet1", + } + err := testCreateNodeBalancer(t, client, f, ann, nil) + if err != nil { + t.Fatalf("expected a nil error, got %v", err) + } +} + func testUpdateLoadBalancerAddNode(t *testing.T, client *linodego.Client, f *fakeAPI) { svc := &v1.Service{ ObjectMeta: metav1.ObjectMeta{ @@ -2637,9 +2725,10 @@ func Test_getHealthCheckType(t *testing.T) { func Test_getNodePrivateIP(t *testing.T) { testcases := []struct { - name string - node *v1.Node - address string + name string + node *v1.Node + address string + subnetID int }{ { "node internal ip specified", @@ -2654,6 +2743,7 @@ func Test_getNodePrivateIP(t *testing.T) { }, }, "127.0.0.1", + 0, }, { "node internal ip not specified", @@ -2668,6 +2758,7 @@ func Test_getNodePrivateIP(t *testing.T) { }, }, "", + 0, }, { "node internal ip annotation present", @@ -2687,12 +2778,33 @@ func Test_getNodePrivateIP(t *testing.T) { }, }, "192.168.42.42", + 0, + }, + { + "node internal ip annotation present and subnet id is not zero", + &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + annotations.AnnLinodeNodePrivateIP: "192.168.42.42", + }, + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + { + Type: v1.NodeInternalIP, + Address: "10.0.1.1", + }, + }, + }, + }, + "10.0.1.1", + 100, }, } for _, test := range testcases { t.Run(test.name, func(t *testing.T) { - ip := getNodePrivateIP(test.node) + ip := getNodePrivateIP(test.node, test.subnetID) if ip != test.address { t.Error("unexpected certificate") t.Logf("expected: %q", test.address) diff --git a/docs/configuration/annotations.md b/docs/configuration/annotations.md index 5146ee9b..9e3127ae 100644 --- a/docs/configuration/annotations.md +++ b/docs/configuration/annotations.md @@ -33,6 +33,9 @@ For implementation details, see: | `firewall-id` | string | | An existing Cloud Firewall ID to be attached to the NodeBalancer instance. See [Firewall Setup](firewall.md) | | `firewall-acl` | string | | The Firewall rules to be applied to the NodeBalancer. See [Firewall Configuration](#firewall-configuration) | | `nodebalancer-type` | string | | The type of NodeBalancer to create (options: common, premium). See [NodeBalancer Types](#nodebalancer-type) | +| `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) | ### Port Specific Configuration @@ -111,6 +114,14 @@ Linode supports nodebalancers of different types: common and premium. By default metadata: annotations: service.beta.kubernetes.io/linode-loadbalancer-nodebalancer-type: premium + +### Nodebalancer VPC Configuration +```yaml +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" ``` For more examples and detailed configuration options, see: diff --git a/docs/configuration/loadbalancer.md b/docs/configuration/loadbalancer.md index c0781fe2..952b2081 100644 --- a/docs/configuration/loadbalancer.md +++ b/docs/configuration/loadbalancer.md @@ -120,10 +120,10 @@ metadata: ## BGP-based IP Sharing Implementation -BGP-based IP sharing provides a more cost-effective solution for multiple LoadBalancer services. For detailed setup instructions, see [Cilium BGP Documentation](https://docs.cilium.io/en/stable/network/bgp-control-plane/). +BGP-based IP sharing provides a more cost-effective solution for multiple LoadBalancer services. For detailed setup instructions, see [Cilium BGP Documentation](https://docs.cilium.io/en/stable/network/bgp-control-plane/bgp-control-plane/). ### Prerequisites -- [Cilium CNI](https://docs.cilium.io/en/stable/network/bgp-control-plane/) with BGP control plane enabled +- [Cilium CNI](https://docs.cilium.io/en/stable/network/bgp-control-plane/bgp-control-plane/) with BGP control plane enabled - Additional IP provisioning enabled on your account (contact [Linode Support](https://www.linode.com/support/)) - Nodes labeled for BGP peering @@ -150,6 +150,27 @@ kubectl label node my-node cilium-bgp-peering=true For more details, see [Environment Variables](environment.md#network-configuration). +## Configuring NodeBalancers directly with VPC +NodeBalancers can be configured to have VPC specific ips configured as backend nodes. It requires: +1. VPC with a subnet and Linodes in VPC +2. Each NodeBalancer created within that VPC needs a free /30 or bigger subnet from the subnet to which Linodes are connected + +Specify NodeBalancer backend ipv4 range when creating service: +```yaml +metadata: + annotations: + service.beta.kubernetes.io/linode-loadbalancer-backend-ipv4-range: "10.100.0.0/30" +``` + +By default, CCM uses first VPC and Subnet name configured with it to attach NodeBalancers to that VPC subnet. To overwrite those, use: +```yaml +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" +``` + ## Advanced Configuration ### Using Existing NodeBalancers @@ -201,6 +222,6 @@ metadata: - [Environment Variables](environment.md) - [Route Configuration](routes.md) - [Linode NodeBalancer Documentation](https://www.linode.com/docs/products/networking/nodebalancers/) -- [Cilium BGP Documentation](https://docs.cilium.io/en/stable/network/bgp-control-plane/) +- [Cilium BGP Documentation](https://docs.cilium.io/en/stable/network/bgp-control-plane/bgp-control-plane/) - [Basic Service Examples](../examples/basic.md) - [Advanced Configuration Examples](../examples/advanced.md) diff --git a/e2e/test/lb-with-vpc-backends/chainsaw-test.yaml b/e2e/test/lb-with-vpc-backends/chainsaw-test.yaml new file mode 100644 index 00000000..50788d39 --- /dev/null +++ b/e2e/test/lb-with-vpc-backends/chainsaw-test.yaml @@ -0,0 +1,75 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json +apiVersion: chainsaw.kyverno.io/v1alpha1 +kind: Test +metadata: + name: lb-with-vpc-backends + labels: + all: +spec: + namespace: "lb-with-vpc-backends" + steps: + - name: Create pods and services + try: + - apply: + file: create-pods-services.yaml + catch: + - describe: + apiVersion: v1 + kind: Pod + - describe: + apiVersion: v1 + kind: Service + - name: Check endpoints exist + try: + - assert: + resource: + apiVersion: v1 + kind: Endpoints + metadata: + name: svc-test + (subsets[0].addresses != null): true + (subsets[0].ports != null): true + - name: Check that loadbalancer ip is assigned + try: + - assert: + resource: + apiVersion: v1 + kind: Service + metadata: + name: svc-test + status: + (loadBalancer.ingress[0].ip != null): true + - name: Check NodeBalancerConfig for backend ips + try: + - script: + content: | + set -e + + nbid=$(KUBECONFIG=$KUBECONFIG NAMESPACE=$NAMESPACE LINODE_TOKEN=$LINODE_TOKEN ../scripts/get-nb-id.sh) + + nbconfig=$(curl -s \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + -H "Content-Type: application/json" \ + "https://api.linode.com/v4/nodebalancers/$nbid/configs") + + config_id=$(echo $nbconfig | jq -r '.data[] | select(.port == 80) | .id') + + # Get nodes from the config + nodes=$(curl -s \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + -H "Content-Type: application/json" \ + "https://api.linode.com/v4/nodebalancers/$nbid/configs/$config_id/nodes") + + # Extract all addresses and remove ports + addresses=$(echo "$json_data" | jq -r '.data[].address' | sed 's/:[0-9]*$//') + + for ip in $addresses; do + if [[ $ip =~ ^10\.0\.0\.[0-9]+$ ]]; then + echo "$ip is in the 10.0.0.0/8 subnet" + else + echo "$ip is NOT in the 10.0.0.0/8 subnet" + fi + done + check: + ($error): ~ + (contains($stdout, 'is NOT in the 10.0.0.0/8 subnet')): false diff --git a/e2e/test/lb-with-vpc-backends/create-pods-services.yaml b/e2e/test/lb-with-vpc-backends/create-pods-services.yaml new file mode 100644 index 00000000..91d5017a --- /dev/null +++ b/e2e/test/lb-with-vpc-backends/create-pods-services.yaml @@ -0,0 +1,49 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: vpc-backends + name: test +spec: + replicas: 1 + selector: + matchLabels: + app: vpc-backends + template: + metadata: + labels: + app: vpc-backends + spec: + containers: + - image: appscode/test-server:2.3 + name: test + ports: + - name: http-1 + containerPort: 80 + protocol: TCP + env: + - name: POD_NAME + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.name +--- +apiVersion: v1 +kind: Service +metadata: + name: svc-test + annotations: + service.beta.kubernetes.io/linode-loadbalancer-backend-ipv4-range: "10.100.0.0/30" + labels: + app: vpc-backends +spec: + type: LoadBalancer + selector: + app: vpc-backends + ports: + - name: http-1 + protocol: TCP + port: 80 + targetPort: 80 + sessionAffinity: None diff --git a/go.mod b/go.mod index ee9ee0e7..55469637 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/golang/mock v1.6.0 github.com/google/uuid v1.6.0 github.com/hexdigest/gowrap v1.4.2 - github.com/linode/linodego v1.47.1-0.20250228182220-41a199a7ed93 + github.com/linode/linodego v1.48.0 github.com/prometheus/client_golang v1.21.0 github.com/spf13/pflag v1.0.6 github.com/stretchr/testify v1.10.0 diff --git a/go.sum b/go.sum index 5ac7dbb4..8933fe76 100644 --- a/go.sum +++ b/go.sum @@ -201,8 +201,8 @@ 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.47.1-0.20250228182220-41a199a7ed93 h1:ZhHBbs+z/HISVA3vuP6tpUiqh87ycU5ztSUqB+nJHl0= -github.com/linode/linodego v1.47.1-0.20250228182220-41a199a7ed93/go.mod h1:k/lRz48xUtGaeVYyvF2X2iNxMpt8JJ+DR4I77R8I1Vg= +github.com/linode/linodego v1.48.0 h1:Xn00rWYSpK5arNEFwymW58jpsdnK8axxhwS/9+cFkQ0= +github.com/linode/linodego v1.48.0/go.mod h1:k/lRz48xUtGaeVYyvF2X2iNxMpt8JJ+DR4I77R8I1Vg= github.com/mackerelio/go-osstat v0.2.5 h1:+MqTbZUhoIt4m8qzkVoXUJg1EuifwlAJSk4Yl2GXh+o= github.com/mackerelio/go-osstat v0.2.5/go.mod h1:atxwWF+POUZcdtR1wnsUcQxTytoHG4uhl2AKKzrOajY= github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= diff --git a/main.go b/main.go index 06a45510..10d4aa5b 100644 --- a/main.go +++ b/main.go @@ -85,7 +85,7 @@ func main() { command.Flags().BoolVar(&linode.Options.EnableTokenHealthChecker, "enable-token-health-checker", false, "enables Linode API token health checker") command.Flags().StringVar(&linode.Options.VPCName, "vpc-name", "", "[deprecated: use vpc-names instead] vpc name whose routes will be managed by route-controller") command.Flags().StringVar(&linode.Options.VPCNames, "vpc-names", "", "comma separated vpc names whose routes will be managed by route-controller") - command.Flags().StringVar(&linode.Options.SubnetNames, "subnet-names", "", "comma separated subnet names whose routes will be managed by route-controller (requires vpc-names flag to also be set)") + command.Flags().StringVar(&linode.Options.SubnetNames, "subnet-names", "default", "comma separated subnet names whose routes will be managed by route-controller (requires vpc-names flag to also be set)") command.Flags().StringVar(&linode.Options.LoadBalancerType, "load-balancer-type", "nodebalancer", "configures which type of load-balancing to use for LoadBalancer Services (options: nodebalancer, cilium-bgp)") command.Flags().StringVar(&linode.Options.BGPNodeSelector, "bgp-node-selector", "", "node selector to use to perform shared IP fail-over with BGP (e.g. cilium-bgp-peering=true") command.Flags().StringVar(&linode.Options.IpHolderSuffix, "ip-holder-suffix", "", "suffix to append to the ip holder name when using shared IP fail-over with BGP (e.g. ip-holder-suffix=my-cluster-name")