From ac2d7a1401706363521655778058153124d58dc7 Mon Sep 17 00:00:00 2001 From: Moshe Vayner Date: Fri, 10 Jul 2026 18:52:46 -0400 Subject: [PATCH 1/3] [feat] add support for deleting reserved IPv4 addresses in NodeBalancer deletion Default behavior is still to retain the reserved IP, just to maintain backwards compatibility Signed-off-by: Moshe Vayner --- cloud/annotations/annotations.go | 4 + cloud/linode/fake_linode_test.go | 4 + cloud/linode/loadbalancers.go | 16 ++++ cloud/linode/loadbalancers_test.go | 114 ++++++++++++++++------------- docs/configuration/annotations.md | 17 ++++- docs/configuration/loadbalancer.md | 51 +++++++++++-- 6 files changed, 149 insertions(+), 57 deletions(-) diff --git a/cloud/annotations/annotations.go b/cloud/annotations/annotations.go index 5e608294..c6f7a588 100644 --- a/cloud/annotations/annotations.go +++ b/cloud/annotations/annotations.go @@ -29,6 +29,10 @@ const ( // for the NodeBalancer. If not specified, Linode will automatically assign an IPv4 address. AnnLinodeLoadBalancerReservedIPv4 = "service.beta.kubernetes.io/linode-loadbalancer-reserved-ipv4" + // AnnLinodeLoadBalancerRetainReservedIPv4 is the annotation used to specify whether a reserved IPv4 address + // should be retained when the NodeBalancer is deleted. Defaults to "true". + AnnLinodeLoadBalancerRetainReservedIPv4 = "service.beta.kubernetes.io/linode-loadbalancer-retain-reserved-ipv4" + AnnLinodeLoadBalancerPreserve = "service.beta.kubernetes.io/linode-loadbalancer-preserve" AnnLinodeNodeBalancerID = "service.beta.kubernetes.io/linode-loadbalancer-nodebalancer-id" AnnLinodeNodeBalancerType = "service.beta.kubernetes.io/linode-loadbalancer-nodebalancer-type" diff --git a/cloud/linode/fake_linode_test.go b/cloud/linode/fake_linode_test.go index 588b5f30..89ae6baf 100644 --- a/cloud/linode/fake_linode_test.go +++ b/cloud/linode/fake_linode_test.go @@ -333,6 +333,10 @@ func (f *fakeAPI) setupRoutes() { _, _ = w.Write(resp) }) + f.mux.HandleFunc("DELETE /v4/networking/reserved/ips/{ipAddress}", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + f.mux.HandleFunc("POST /v4/nodebalancers", func(w http.ResponseWriter, r *http.Request) { nbco := linodego.NodeBalancerCreateOptions{} if err := json.NewDecoder(r.Body).Decode(&nbco); err != nil { diff --git a/cloud/linode/loadbalancers.go b/cloud/linode/loadbalancers.go index 99586f50..549db380 100644 --- a/cloud/linode/loadbalancers.go +++ b/cloud/linode/loadbalancers.go @@ -606,6 +606,13 @@ func (l *loadbalancers) shouldPreserveNodeBalancer(service *v1.Service) bool { return shouldPreserve != nil && *shouldPreserve } +// shouldRetainReservedIP determines whether a reserved IP should be retained based on the +// service's retain reserved IP annotation. If the annotation is not present, the default behavior is to retain the reserved IP. +func (l *loadbalancers) shouldRetainReservedIP(service *v1.Service) bool { + shouldRetain := getServiceBoolAnnotation(service, annotations.AnnLinodeLoadBalancerRetainReservedIPv4) + return shouldRetain == nil || *shouldRetain +} + // EnsureLoadBalancerDeleted deletes the specified loadbalancer if it exists. // nil is returned if the load balancer for service does not exist or is // successfully deleted. @@ -668,6 +675,15 @@ func (l *loadbalancers) EnsureLoadBalancerDeleted(ctx context.Context, clusterNa return err } + if !l.shouldRetainReservedIP(service) { + klog.Infof("deleting reserved IP (%s) for NodeBalancer (%d) for service (%s)", *nb.IPv4, nb.ID, serviceNn) + if err = l.client.DeleteReservedIPAddress(ctx, *nb.IPv4); err != nil { + klog.Errorf("failed to delete reserved IP (%s) for NodeBalancer (%d) for service (%s): %s", *nb.IPv4, nb.ID, serviceNn, err) + sentry.CaptureError(ctx, err) + return err + } + } + if err = l.client.DeleteNodeBalancer(ctx, nb.ID); err != nil { klog.Errorf("failed to delete NodeBalancer (%d) for service (%s): %s", nb.ID, serviceNn, err) sentry.CaptureError(ctx, err) diff --git a/cloud/linode/loadbalancers_test.go b/cloud/linode/loadbalancers_test.go index 571bcf88..f30c9cfd 100644 --- a/cloud/linode/loadbalancers_test.go +++ b/cloud/linode/loadbalancers_test.go @@ -4667,60 +4667,60 @@ func testEnsureLoadBalancerPreserveAnnotation(t *testing.T, client *linodego.Cli func testEnsureLoadBalancerDeleted(t *testing.T, client *linodego.Client, fake *fakeAPI) { t.Helper() - svc := &v1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - UID: "foobar123", - Annotations: map[string]string{ - annotations.AnnLinodeDefaultProtocol: "tcp", + newService := func(name string, annotationsMap map[string]string) *v1.Service { + return &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + UID: types.UID(name + "-uid"), + Annotations: annotationsMap, }, - }, - Spec: v1.ServiceSpec{ - Ports: []v1.ServicePort{ - { - Name: "test", - Protocol: "TCP", - Port: int32(80), - NodePort: int32(30000), + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Name: "test", + Protocol: "TCP", + Port: int32(80), + NodePort: int32(30000), + }, }, }, - }, + } } + testcases := []struct { - name string - clusterName string - service *v1.Service - err error + name string + clusterName string + service *v1.Service + createNodeBalancer bool + expectReservedIPDelete bool + err error }{ { - "load balancer delete", - "linodelb", - svc, - nil, + name: "load balancer delete retains reserved ip by default", + clusterName: "linodelb", + service: newService("test-retain-default", map[string]string{annotations.AnnLinodeDefaultProtocol: "tcp"}), + createNodeBalancer: true, + expectReservedIPDelete: false, + err: nil, }, { - "load balancer not exists", - "linodelb", - &v1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: "notexists", - UID: "notexists123", - Annotations: map[string]string{ - annotations.AnnLinodeDefaultProtocol: "tcp", - }, - }, - Spec: v1.ServiceSpec{ - Ports: []v1.ServicePort{ - { - Name: "test", - Protocol: "TCP", - Port: int32(80), - NodePort: int32(30000), - }, - }, - }, - }, - nil, + name: "load balancer delete removes reserved ip when retain annotation is false", + clusterName: "linodelb", + service: newService("test-delete-reserved-ip", map[string]string{ + annotations.AnnLinodeDefaultProtocol: "tcp", + annotations.AnnLinodeLoadBalancerRetainReservedIPv4: "false", + }), + createNodeBalancer: true, + expectReservedIPDelete: true, + err: nil, + }, + { + name: "load balancer not exists", + clusterName: "linodelb", + service: newService("notexists", map[string]string{annotations.AnnLinodeDefaultProtocol: "tcp"}), + createNodeBalancer: false, + expectReservedIPDelete: false, + err: nil, }, } @@ -4728,21 +4728,33 @@ func testEnsureLoadBalancerDeleted(t *testing.T, client *linodego.Client, fake * if !assertion { t.Error("type assertion failed") } - configs := []linodego.NodeBalancerConfigCreateOptions{} - _, err := lb.createNodeBalancer(t.Context(), "linodelb", svc, configs) - if err != nil { - t.Fatal(err) - } - defer func() { _ = lb.EnsureLoadBalancerDeleted(t.Context(), "linodelb", svc) }() for _, test := range testcases { t.Run(test.name, func(t *testing.T) { + fake.ResetRequests() + + if test.createNodeBalancer { + nb, err := lb.createNodeBalancer(t.Context(), test.clusterName, test.service, []linodego.NodeBalancerConfigCreateOptions{}) + if err != nil { + t.Fatal(err) + } + test.service.Status.LoadBalancer = *makeLoadBalancerStatus(test.service, nb) + } + err := lb.EnsureLoadBalancerDeleted(t.Context(), test.clusterName, test.service) if !reflect.DeepEqual(err, test.err) { t.Error("unexpected error") t.Logf("expected: %v", test.err) t.Logf("actual: %v", err) } + + if test.createNodeBalancer { + ingressIP := test.service.Status.LoadBalancer.Ingress[0].IP + didDeleteReservedIP := fake.didRequestOccur(http.MethodDelete, fmt.Sprintf("/networking/reserved/ips/%s", ingressIP), "") + if didDeleteReservedIP != test.expectReservedIPDelete { + t.Fatalf("reserved IP delete request mismatch: got %v, want %v", didDeleteReservedIP, test.expectReservedIPDelete) + } + } }) } } diff --git a/docs/configuration/annotations.md b/docs/configuration/annotations.md index dc708cfc..bc07809d 100644 --- a/docs/configuration/annotations.md +++ b/docs/configuration/annotations.md @@ -5,6 +5,7 @@ Service annotations allow you to customize the behavior of your LoadBalancer services. All Service annotations must be prefixed with: `service.beta.kubernetes.io/linode-loadbalancer-` For implementation details, see: + - [LoadBalancer Configuration](loadbalancer.md) - [Basic Service Examples](../examples/basic.md) - [Advanced Configuration Examples](../examples/advanced.md) @@ -50,7 +51,8 @@ The keys and the values in [annotations must be strings](https://kubernetes.io/d | `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)) | +| `reserved-ipv4` | string | | An existing Reserved IPv4 address that will be used to initialize the NodeBalancer instance. See [LoadBalancer Configuration](loadbalancer.md#reserved-ipv4-addresses) | +| `retain-reserved-ipv4` | bool | `true` | When `false`, deleting a `LoadBalancer` service also releases the reserved IPv4 address attached to the NodeBalancer | ### Port Specific Configuration @@ -70,6 +72,7 @@ metadata: ``` Available port options: + - `protocol`: Protocol for this port (tcp, http, https) - `tls-secret-name`: Name of TLS secret for HTTPS. The secret type should be `kubernetes.io/tls` - `proxy-protocol`: Proxy protocol version for this port @@ -84,7 +87,9 @@ Available port options: | `proxy-protocol` | `none`, `v1`, `v2` | `none` | Specifies whether to use a version of Proxy Protocol on the underlying NodeBalancer | Q4 2021 | ### Annotation Boolean Values + For annotations with bool value types, the following string values are interpreted as `true`: + - `"1"` - `"t"` - `"T"` @@ -97,6 +102,7 @@ Any other values will be interpreted as `false`. For more details, see [strconv. ## Examples ### Basic HTTP Service + ```yaml metadata: annotations: @@ -106,6 +112,7 @@ metadata: ``` ### HTTPS Service with TLS + ```yaml metadata: annotations: @@ -117,6 +124,7 @@ metadata: ``` ### Firewall Configuration + ```yaml metadata: annotations: @@ -130,9 +138,11 @@ metadata: ``` ### NodeBalancer Type + Linode supports nodebalancers of different types: common, premium, and premium_40gb. By default, nodebalancers of type common are provisioned. If an account is allowed to provision premium nodebalancers and one wants to use them, it can be achieved by specifying the annotation: **Note:** NodeBalancer types should always be specified in lowercase. The controller will automatically convert uppercase values to lowercase for safety. + ```yaml metadata: annotations: @@ -140,6 +150,7 @@ metadata: ``` ### Nodebalancer VPC Configuration + ```yaml metadata: annotations: @@ -149,6 +160,7 @@ metadata: ``` Frontend VPC configuration: + ```yaml metadata: annotations: @@ -159,6 +171,7 @@ metadata: ``` ### Service with IPv6 Address + ```yaml metadata: annotations: @@ -166,6 +179,7 @@ metadata: ``` For more examples and detailed configuration options, see: + - [LoadBalancer Configuration](loadbalancer.md) - [Firewall Configuration](firewall.md) - [Basic Service Examples](../examples/basic.md) @@ -173,6 +187,7 @@ For more examples and detailed configuration options, see: - [Examples](../examples/README.md) See also: + - [Environment Variables](environment.md) - [Route Configuration](routes.md) - [Session Affinity](session-affinity.md) diff --git a/docs/configuration/loadbalancer.md b/docs/configuration/loadbalancer.md index 21c93801..52901e3c 100644 --- a/docs/configuration/loadbalancer.md +++ b/docs/configuration/loadbalancer.md @@ -3,6 +3,7 @@ ## Overview The CCM supports two types of LoadBalancer implementations: + 1. Linode NodeBalancers (default) 2. BGP-based IP sharing @@ -11,6 +12,7 @@ For implementation examples, see [Basic Service Examples](../examples/basic.md#l ## NodeBalancer Implementation When using NodeBalancers, the CCM automatically: + 1. Creates and configures a NodeBalancer 2. Sets up backend nodes 3. Manages health checks @@ -20,7 +22,7 @@ For more details, see [Linode NodeBalancer Documentation](https://www.linode.com ### IPv6 Support -NodeBalancers support both IPv4 and IPv6 ingress addresses. By default, the CCM uses only IPv4 address for LoadBalancer services. +NodeBalancers support both IPv4 and IPv6 ingress addresses. By default, the CCM uses only IPv4 address for LoadBalancer services. You can enable IPv6 addresses globally for all services by setting the `enable-ipv6-for-loadbalancers` flag: @@ -71,6 +73,7 @@ metadata: ``` When IPv6 backends are enabled: + - NodeBalancer backend targets use the node public IPv6 annotation `node.k8s.linode.com/public-ipv6` - IPv6 NodeBalancer backends are currently supported only through node public IPv6 addresses, not VPC IPv6 backend addresses - both VPC-backed and non-VPC-backed NodeBalancer services are affected @@ -109,6 +112,7 @@ If your cluster does not provide IPv6-capable NodePort routing, the NodeBalancer ### Basic Configuration Create a LoadBalancer service: + ```yaml apiVersion: v1 kind: Service @@ -128,13 +132,16 @@ See [Advanced Configuration Examples](../examples/advanced.md#loadbalancer-servi ### NodeBalancer Settings #### Protocol Configuration + Available protocols: + - `tcp` (default) - `http` - `https` - `udp` Set the default protocol: + ```yaml metadata: annotations: @@ -146,6 +153,7 @@ See [Service Annotations](annotations.md#basic-configuration) for all protocol o ### Health Checks Configure health checks using annotations: + ```yaml metadata: annotations: @@ -157,6 +165,7 @@ metadata: ``` Available check types: + - `none`: No health check - `connection`: TCP connection check - `http`: HTTP status check @@ -167,6 +176,7 @@ For more details, see [Health Check Configuration](annotations.md#health-check-c ### SSL/TLS Configuration 1. Create a TLS secret: + ```yaml apiVersion: v1 kind: Secret @@ -178,7 +188,8 @@ data: tls.key: ``` -2. Reference in service annotation: +1. Reference in service annotation: + ```yaml metadata: annotations: @@ -192,6 +203,7 @@ metadata: ### Connection Throttling Limit connections from the same client IP: + ```yaml metadata: annotations: @@ -201,6 +213,7 @@ metadata: ### Proxy Protocol Enable proxy protocol for client IP preservation: + ```yaml metadata: annotations: @@ -212,6 +225,7 @@ metadata: 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/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 @@ -219,6 +233,7 @@ BGP-based IP sharing provides a more cost-effective solution for multiple LoadBa ### Configuration 1. Enable BGP in CCM deployment: + ```yaml args: - --load-balancer-type=cilium-bgp @@ -226,25 +241,30 @@ args: - --ip-holder-suffix=mycluster ``` -2. Label nodes that should participate in BGP peering: +1. Label nodes that should participate in BGP peering: + ```bash kubectl label node my-node cilium-bgp-peering=true ``` -3. Create LoadBalancer services as normal - the CCM will automatically use BGP-based IP sharing instead of creating NodeBalancers. +1. Create LoadBalancer services as normal - the CCM will automatically use BGP-based IP sharing instead of creating NodeBalancers. ### Environment Variables + - `BGP_CUSTOM_ID_MAP`: Use your own map instead of default region map for BGP - `BGP_PEER_PREFIX`: Use your own BGP peer prefix instead of default one 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: @@ -252,6 +272,7 @@ metadata: ``` 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: @@ -277,11 +298,13 @@ Frontend VPC configuration supports the following annotations: - `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: @@ -300,6 +323,7 @@ If CCM is started with `--nodebalancer-backend-ipv4-subnet` flag, then it will n ### Using Existing NodeBalancers Specify an existing NodeBalancer: + ```yaml metadata: annotations: @@ -309,17 +333,31 @@ metadata: ### Reserved IPv4 addresses Create an new NodeBalancer with an existing Reserved IPv4 Address: -``` + +```yaml metadata: annotations: service.beta.kubernetes.io/linode-loadbalancer-reserved-ipv4: "100.100.100.100" ``` + The annotation must be present when the Service is created in order to take effect. +### Retaining Reserved IPv4 addresses + +Retain a reserved IPv4 address when the NodeBalancer is deleted: + +```yaml +metadata: + annotations: + service.beta.kubernetes.io/linode-loadbalancer-retain-reserved-ipv4: "true" +``` + +The default behavior is to retain the reserved IPv4 address when the NodeBalancer is deleted. Setting this annotation to "false" will release the reserved IPv4 address when the NodeBalancer is deleted. ### NodeBalancer Preservation Prevent NodeBalancer deletion when service is deleted: + ```yaml metadata: annotations: @@ -329,6 +367,7 @@ metadata: ### Port Configuration Configure individual ports: + ```yaml metadata: annotations: @@ -343,6 +382,7 @@ metadata: ### Tags Add tags to NodeBalancer: + ```yaml metadata: annotations: @@ -350,6 +390,7 @@ metadata: ``` ### Excluding nodes from nodebalancer + Add a label to the node object to exclude ```yaml From f54b348bda4a84c94a01d728daa3d472fa47b44b Mon Sep 17 00:00:00 2001 From: Moshe Vayner Date: Fri, 10 Jul 2026 18:53:01 -0400 Subject: [PATCH 2/3] [test] add retain-reserved-ipv4 annotation to service configurations Signed-off-by: Moshe Vayner --- .../chainsaw-test.yaml | 17 ---------------- .../create-pods-services.yaml | 1 + .../chainsaw-test.yaml | 17 ---------------- .../create-pods-services.yaml | 1 + .../chainsaw-test.yaml | 17 ---------------- .../create-pods-services.yaml | 1 + .../chainsaw-test.yaml | 16 --------------- .../create-pods-services.yaml | 1 + .../chainsaw-test.yaml | 15 -------------- .../create-pods-services.yaml | 1 + .../chainsaw-test.yaml | 20 +------------------ .../create-pods-services.yaml | 1 + .../chainsaw-test.yaml | 17 ---------------- .../create-pods-services.yaml | 1 + .../chainsaw-test.yaml | 19 +----------------- .../create-pods-services.yaml | 1 + .../chainsaw-test.yaml | 18 +---------------- .../create-pods-services.yaml | 1 + .../lb-delete-svc-no-nb/chainsaw-test.yaml | 1 + 19 files changed, 13 insertions(+), 153 deletions(-) diff --git a/e2e/test/lb-created-with-reserved-ip-and-nb-id-annotations/chainsaw-test.yaml b/e2e/test/lb-created-with-reserved-ip-and-nb-id-annotations/chainsaw-test.yaml index 069acba5..54c74ad2 100644 --- a/e2e/test/lb-created-with-reserved-ip-and-nb-id-annotations/chainsaw-test.yaml +++ b/e2e/test/lb-created-with-reserved-ip-and-nb-id-annotations/chainsaw-test.yaml @@ -105,22 +105,6 @@ spec: content: | set -euo pipefail - reserved_ip=$(kubectl get configmap nb-config -o=jsonpath='{.data.Reserved_IP}' -n $NAMESPACE) - if [ -z "$reserved_ip" ]; then - echo "Error: No reserved ip found in configmap" - fi - - delete_rip=$(curl -s --request DELETE \ - -H "Authorization: Bearer $LINODE_TOKEN" \ - -H "Content-Type: application/json" --fail-early --retry 3 \ - "${LINODE_URL}/v4beta/networking/reserved/ips/$reserved_ip" ) - - if [[ "$delete_rip" == "{}" ]]; then - echo "Reserved IP $reserved_ip deleted successfully" - else - echo "Unable to delete reserved ip: $reserved_ip. Error: $delete_rip" - fi - delete_cm=$(kubectl delete configmap nb-config -n $NAMESPACE) if [[ "$delete_cm" == *'configmap "nb-config" deleted'* ]]; then echo "Configmap deleted successfully" @@ -132,7 +116,6 @@ spec: ($error == null): true (contains($stdout, 'No reserved ip found in configmap')): false (contains($stdout, 'Unable to delete the configmap')): false - (contains($stdout, 'Unable to delete reserved ip')): false - name: Check that loadbalancer ip is assigned try: - assert: diff --git a/e2e/test/lb-created-with-reserved-ip-and-nb-id-annotations/create-pods-services.yaml b/e2e/test/lb-created-with-reserved-ip-and-nb-id-annotations/create-pods-services.yaml index d8014284..d6bbb308 100644 --- a/e2e/test/lb-created-with-reserved-ip-and-nb-id-annotations/create-pods-services.yaml +++ b/e2e/test/lb-created-with-reserved-ip-and-nb-id-annotations/create-pods-services.yaml @@ -35,6 +35,7 @@ metadata: name: svc-test annotations: service.beta.kubernetes.io/linode-loadbalancer-reserved-ipv4: ($nb_conf.reserved_ip) + service.beta.kubernetes.io/linode-loadbalancer-retain-reserved-ipv4: "false" service.beta.kubernetes.io/linode-loadbalancer-nodebalancer-id: ($nb_conf.nb_id) labels: app: created-with-reserved-ip-and-nb-id-annotations diff --git a/e2e/test/lb-created-with-reserved-ip-attached-to-nb/chainsaw-test.yaml b/e2e/test/lb-created-with-reserved-ip-attached-to-nb/chainsaw-test.yaml index 162b525c..81bacdae 100644 --- a/e2e/test/lb-created-with-reserved-ip-attached-to-nb/chainsaw-test.yaml +++ b/e2e/test/lb-created-with-reserved-ip-attached-to-nb/chainsaw-test.yaml @@ -106,27 +106,11 @@ spec: content: | set -euo pipefail - reserved_ip=$(kubectl get configmap nb-config -o=jsonpath='{.data.Reserved_IP}' -n $NAMESPACE) - if [ -z "$reserved_ip" ]; then - echo "Error: No reserved ip found in configmap" - fi - nb_id=$(kubectl get configmap nb-config -o=jsonpath='{.data.NB_ID}' -n $NAMESPACE) if [ -z "$nb_id" ]; then echo "Error: No nb id found in configmap" fi - delete_rip=$(curl -s --request DELETE \ - -H "Authorization: Bearer $LINODE_TOKEN" \ - -H "Content-Type: application/json" --fail-early --retry 3 \ - "${LINODE_URL}/v4beta/networking/reserved/ips/$reserved_ip" ) - - if [[ "$delete_rip" == "{}" ]]; then - echo "Reserved IP $reserved_ip deleted successfully" - else - echo "Unable to delete reserved ip: $reserved_ip. Error: $delete_rip" - fi - del_nb=$(curl -s --request DELETE \ -H "Authorization: Bearer $LINODE_TOKEN" \ -H "Content-Type: application/json" --fail-early --retry 3 \ @@ -148,7 +132,6 @@ spec: ($error == null): true (contains($stdout, 'No reserved ip found in configmap')): false (contains($stdout, 'Unable to delete the configmap')): false - (contains($stdout, 'Unable to delete reserved ip')): false (contains($stdout, 'No nb id found in configmap')): false (contains($stdout, 'Unable to delete NB')): false - name: Check that loadbalancer ip is assigned diff --git a/e2e/test/lb-created-with-reserved-ip-attached-to-nb/create-pods-services.yaml b/e2e/test/lb-created-with-reserved-ip-attached-to-nb/create-pods-services.yaml index c158d583..1615515f 100644 --- a/e2e/test/lb-created-with-reserved-ip-attached-to-nb/create-pods-services.yaml +++ b/e2e/test/lb-created-with-reserved-ip-attached-to-nb/create-pods-services.yaml @@ -35,6 +35,7 @@ metadata: name: svc-test annotations: service.beta.kubernetes.io/linode-loadbalancer-reserved-ipv4: ($nb_conf.reserved_ip) + service.beta.kubernetes.io/linode-loadbalancer-retain-reserved-ipv4: "false" labels: app: created-with-reserved-ip-attached-to-nb spec: diff --git a/e2e/test/lb-created-with-reserved-ip-change-ip-concurrently/chainsaw-test.yaml b/e2e/test/lb-created-with-reserved-ip-change-ip-concurrently/chainsaw-test.yaml index 95b74ee3..168ea2f1 100644 --- a/e2e/test/lb-created-with-reserved-ip-change-ip-concurrently/chainsaw-test.yaml +++ b/e2e/test/lb-created-with-reserved-ip-change-ip-concurrently/chainsaw-test.yaml @@ -89,29 +89,12 @@ spec: content: | set -euo pipefail - reserved_ip=$(kubectl get configmap reserved-ip-config -o=jsonpath='{.data.ReservedIP}' -n $NAMESPACE) - if [ -z "$reserved_ip" ]; then - echo "Error: No reserved ip found in configmap" - exit 1 - fi - reserved_ip2=$(kubectl get configmap reserved-ip-config -o=jsonpath='{.data.ReservedIP2}' -n $NAMESPACE) if [ -z "$reserved_ip2" ]; then echo "Error: No reserved ip found in configmap" exit 1 fi - delete_rip=$(curl -s --request DELETE \ - -H "Authorization: Bearer $LINODE_TOKEN" \ - -H "Content-Type: application/json" --fail-early --retry 3 \ - "${LINODE_URL}/v4beta/networking/reserved/ips/$reserved_ip" ) - - if [[ "$delete_rip" == "{}" ]]; then - echo "Reserved IP $reserved_ip deleted successfully" - else - echo "Unable to delete reserved ip: $reserved_ip. Error: $delete_rip" - fi - delete_rip2=$(curl -s --request DELETE \ -H "Authorization: Bearer $LINODE_TOKEN" \ -H "Content-Type: application/json" --fail-early --retry 3 \ diff --git a/e2e/test/lb-created-with-reserved-ip-change-ip-concurrently/create-pods-services.yaml b/e2e/test/lb-created-with-reserved-ip-change-ip-concurrently/create-pods-services.yaml index edaa87f1..11c47015 100644 --- a/e2e/test/lb-created-with-reserved-ip-change-ip-concurrently/create-pods-services.yaml +++ b/e2e/test/lb-created-with-reserved-ip-change-ip-concurrently/create-pods-services.yaml @@ -35,6 +35,7 @@ metadata: name: svc-test annotations: service.beta.kubernetes.io/linode-loadbalancer-reserved-ipv4: ($ip.reserved_ip) + service.beta.kubernetes.io/linode-loadbalancer-retain-reserved-ipv4: "false" labels: app: created-with-reserved-ip-change-ip-concurrently spec: diff --git a/e2e/test/lb-created-with-reserved-ip-change-ip-unreserved/chainsaw-test.yaml b/e2e/test/lb-created-with-reserved-ip-change-ip-unreserved/chainsaw-test.yaml index 7221825c..a006d639 100644 --- a/e2e/test/lb-created-with-reserved-ip-change-ip-unreserved/chainsaw-test.yaml +++ b/e2e/test/lb-created-with-reserved-ip-change-ip-unreserved/chainsaw-test.yaml @@ -74,21 +74,6 @@ spec: content: | set -euo pipefail - reserved_ip=$(kubectl get configmap reserved-ip-config -o=jsonpath='{.data.ReservedIP}' -n $NAMESPACE) - if [ -z "$reserved_ip" ]; then - echo "Error: No reserved ip found in configmap" - fi - - delete_rip=$(curl -s --request DELETE \ - -H "Authorization: Bearer $LINODE_TOKEN" \ - -H "Content-Type: application/json" --fail-early --retry 3 \ - "${LINODE_URL}/v4beta/networking/reserved/ips/$reserved_ip" ) - - if [[ "$delete_rip" == "{}" ]]; then - echo "Reserved IP $reserved_ip deleted successfully" - else - echo "Unable to delete reserved ip: $reserved_ip. Error: $delete_rip" - fi delete_cm=$(kubectl delete configmap reserved-ip-config -n $NAMESPACE) if [[ "$delete_cm" == *'configmap "reserved-ip-config" deleted'* ]]; then @@ -100,7 +85,6 @@ spec: ($error == null): true (contains($stdout, 'No reserved ip found in configmap')): false (contains($stdout, 'Unable to delete the configmap')): false - (contains($stdout, 'Unable to delete reserved ip')): false - name: Check that loadbalancer ip is assigned try: - assert: diff --git a/e2e/test/lb-created-with-reserved-ip-change-ip-unreserved/create-pods-services.yaml b/e2e/test/lb-created-with-reserved-ip-change-ip-unreserved/create-pods-services.yaml index 1f35826f..5f1523e1 100644 --- a/e2e/test/lb-created-with-reserved-ip-change-ip-unreserved/create-pods-services.yaml +++ b/e2e/test/lb-created-with-reserved-ip-change-ip-unreserved/create-pods-services.yaml @@ -35,6 +35,7 @@ metadata: name: svc-test annotations: service.beta.kubernetes.io/linode-loadbalancer-reserved-ipv4: ($ip.reserved_ip) + service.beta.kubernetes.io/linode-loadbalancer-retain-reserved-ipv4: "false" labels: app: created-with-reserved-ip-change-ip-unreserved spec: diff --git a/e2e/test/lb-created-with-reserved-ip-change-ip/chainsaw-test.yaml b/e2e/test/lb-created-with-reserved-ip-change-ip/chainsaw-test.yaml index 5d5ace15..f2e4f919 100644 --- a/e2e/test/lb-created-with-reserved-ip-change-ip/chainsaw-test.yaml +++ b/e2e/test/lb-created-with-reserved-ip-change-ip/chainsaw-test.yaml @@ -88,25 +88,10 @@ spec: content: | set -euo pipefail - reserved_ip=$(kubectl get configmap reserved-ip-config -o=jsonpath='{.data.ReservedIP}' -n $NAMESPACE) - if [ -z "$reserved_ip" ]; then - echo "Error: No reserved ip found in configmap" - fi - reserved_ip2=$(kubectl get configmap reserved-ip-config -o=jsonpath='{.data.ReservedIP2}' -n $NAMESPACE) if [ -z "$reserved_ip2" ]; then echo "Error: No reserved ip found in configmap" fi - delete_rip=$(curl -s --request DELETE \ - -H "Authorization: Bearer $LINODE_TOKEN" \ - -H "Content-Type: application/json" --fail-early --retry 3 \ - "${LINODE_URL}/v4beta/networking/reserved/ips/$reserved_ip" ) - - if [[ "$delete_rip" == "{}" ]]; then - echo "Reserved IP $reserved_ip deleted successfully" - else - echo "Unable to delete reserved ip: $reserved_ip. Error: $delete_rip" - fi delete_rip2=$(curl -s --request DELETE \ -H "Authorization: Bearer $LINODE_TOKEN" \ diff --git a/e2e/test/lb-created-with-reserved-ip-change-ip/create-pods-services.yaml b/e2e/test/lb-created-with-reserved-ip-change-ip/create-pods-services.yaml index 2818f772..ac297820 100644 --- a/e2e/test/lb-created-with-reserved-ip-change-ip/create-pods-services.yaml +++ b/e2e/test/lb-created-with-reserved-ip-change-ip/create-pods-services.yaml @@ -35,6 +35,7 @@ metadata: name: svc-test annotations: service.beta.kubernetes.io/linode-loadbalancer-reserved-ipv4: ($ip.reserved_ip) + service.beta.kubernetes.io/linode-loadbalancer-retain-reserved-ipv4: "false" labels: app: created-with-reserved-ip-change-ip spec: diff --git a/e2e/test/lb-created-with-reserved-ip-linode-range/chainsaw-test.yaml b/e2e/test/lb-created-with-reserved-ip-linode-range/chainsaw-test.yaml index 6dd7af17..fbe6723f 100644 --- a/e2e/test/lb-created-with-reserved-ip-linode-range/chainsaw-test.yaml +++ b/e2e/test/lb-created-with-reserved-ip-linode-range/chainsaw-test.yaml @@ -105,12 +105,6 @@ spec: - -c content: | set -euo pipefail - - reserved_ip=$(kubectl get configmap reserved-ip-config -o=jsonpath='{.data.ReservedIP}' -n $NAMESPACE) - if [ -z "$reserved_ip" ]; then - echo "Error: No reserved ip found in configmap" - fi - fw_id=$(kubectl get configmap reserved-ip-config -o=jsonpath='{.data.FW_ID}' -n $NAMESPACE) if [ -z "$fw_id" ]; then echo "Error: No firewall id found in configmap" @@ -121,23 +115,12 @@ spec: -H "Content-Type: application/json" --fail-early --retry 3 \ "$LINODE_URL/v4/networking/firewalls/$fw_id") - if [[ "$delete_fw" == "{}" ]]; then + if [[ "$delete_fw" == "{}" || "$delete_fw" == *'"reason": "Not found"'* ]]; then echo "Firewall $fw_id deleted successfully" else echo "Unable to delete firewall: $fw_id. Error: $delete_fw" fi - delete_rip=$(curl -s --request DELETE \ - -H "Authorization: Bearer $LINODE_TOKEN" \ - -H "Content-Type: application/json" --fail-early --retry 3 \ - "${LINODE_URL}/v4beta/networking/reserved/ips/$reserved_ip" ) - - if [[ "$delete_rip" == "{}" ]]; then - echo "Reserved IP $reserved_ip deleted successfully" - else - echo "Unable to delete reserved ip: $reserved_ip. Error: $delete_rip" - fi - delete_cm=$(kubectl delete configmap reserved-ip-config -n $NAMESPACE) if [[ "$delete_cm" == *'configmap "reserved-ip-config" deleted'* ]]; then echo "Configmap deleted successfully" @@ -149,7 +132,6 @@ spec: (contains($stdout, 'No firewall id found in configmap')): false (contains($stdout, 'Unable to delete firewall')): false (contains($stdout, 'Unable to delete the configmap')): false - (contains($stdout, 'Unable to delete reserved ip')): false - name: Check that loadbalancer ip is assigned try: - assert: diff --git a/e2e/test/lb-created-with-reserved-ip-linode-range/create-pods-services.yaml b/e2e/test/lb-created-with-reserved-ip-linode-range/create-pods-services.yaml index 10b0a890..3683b38f 100644 --- a/e2e/test/lb-created-with-reserved-ip-linode-range/create-pods-services.yaml +++ b/e2e/test/lb-created-with-reserved-ip-linode-range/create-pods-services.yaml @@ -35,6 +35,7 @@ metadata: name: svc-test annotations: service.beta.kubernetes.io/linode-loadbalancer-reserved-ipv4: ($ip.reserved_ip) + service.beta.kubernetes.io/linode-loadbalancer-retain-reserved-ipv4: "false" service.beta.kubernetes.io/linode-loadbalancer-firewall-id: ($ip.fw_id) labels: app: created-with-reserved-ip-linode-range diff --git a/e2e/test/lb-created-with-reserved-ip-multiple-change-ip/chainsaw-test.yaml b/e2e/test/lb-created-with-reserved-ip-multiple-change-ip/chainsaw-test.yaml index 266141ed..548641cb 100644 --- a/e2e/test/lb-created-with-reserved-ip-multiple-change-ip/chainsaw-test.yaml +++ b/e2e/test/lb-created-with-reserved-ip-multiple-change-ip/chainsaw-test.yaml @@ -90,29 +90,12 @@ spec: content: | set -euo pipefail - reserved_ip=$(kubectl get configmap reserved-ip-config -o=jsonpath='{.data.ReservedIP}' -n $NAMESPACE) - if [ -z "$reserved_ip" ]; then - echo "Error: No reserved ip found in configmap" - exit 1 - fi - reserved_ip2=$(kubectl get configmap reserved-ip-config -o=jsonpath='{.data.ReservedIP2}' -n $NAMESPACE) if [ -z "$reserved_ip2" ]; then echo "Error: No reserved ip found in configmap" exit 1 fi - delete_rip=$(curl -s --request DELETE \ - -H "Authorization: Bearer $LINODE_TOKEN" \ - -H "Content-Type: application/json" --fail-early --retry 3 \ - "${LINODE_URL}/v4beta/networking/reserved/ips/$reserved_ip" ) - - if [[ "$delete_rip" == "{}" ]]; then - echo "Reserved IP $reserved_ip deleted successfully" - else - echo "Unable to delete reserved ip: $reserved_ip. Error: $delete_rip" - fi - delete_rip2=$(curl -s --request DELETE \ -H "Authorization: Bearer $LINODE_TOKEN" \ -H "Content-Type: application/json" --fail-early --retry 3 \ diff --git a/e2e/test/lb-created-with-reserved-ip-multiple-change-ip/create-pods-services.yaml b/e2e/test/lb-created-with-reserved-ip-multiple-change-ip/create-pods-services.yaml index e8a19f8d..45cc44b7 100644 --- a/e2e/test/lb-created-with-reserved-ip-multiple-change-ip/create-pods-services.yaml +++ b/e2e/test/lb-created-with-reserved-ip-multiple-change-ip/create-pods-services.yaml @@ -35,6 +35,7 @@ metadata: name: svc-test annotations: service.beta.kubernetes.io/linode-loadbalancer-reserved-ipv4: ($ip.reserved_ip) + service.beta.kubernetes.io/linode-loadbalancer-retain-reserved-ipv4: "false" labels: app: created-with-reserved-ip-multiple-change-ip spec: diff --git a/e2e/test/lb-created-with-reserved-ip-nb-range/chainsaw-test.yaml b/e2e/test/lb-created-with-reserved-ip-nb-range/chainsaw-test.yaml index 50848cac..11a8c1ff 100644 --- a/e2e/test/lb-created-with-reserved-ip-nb-range/chainsaw-test.yaml +++ b/e2e/test/lb-created-with-reserved-ip-nb-range/chainsaw-test.yaml @@ -137,11 +137,6 @@ spec: - -c content: | set -euo pipefail - - reserved_ip=$(kubectl get configmap reserved-ip-config -o=jsonpath='{.data.ReservedIP}' -n $NAMESPACE) - if [ -z "$reserved_ip" ]; then - echo "Error: No reserved ip found in configmap" - fi fw_id=$(kubectl get configmap reserved-ip-config -o=jsonpath='{.data.FW_ID}' -n $NAMESPACE) if [ -z "$fw_id" ]; then @@ -153,22 +148,11 @@ spec: -H "Content-Type: application/json" --fail-early --retry 3 \ "$LINODE_URL/v4/networking/firewalls/$fw_id") - if [[ "$delete_fw" == "{}" ]]; then + if [[ "$delete_fw" == "{}" || "$delete_fw" == *'"reason": "Not found"'* ]]; then echo "Firewall $fw_id deleted successfully" else echo "Unable to delete firewall: $fw_id. Error: $delete_fw" fi - - delete_rip=$(curl -s --request DELETE \ - -H "Authorization: Bearer $LINODE_TOKEN" \ - -H "Content-Type: application/json" --fail-early --retry 3 \ - "${LINODE_URL}/v4beta/networking/reserved/ips/$reserved_ip" ) - - if [[ "$delete_rip" == "{}" ]]; then - echo "Reserved IP $reserved_ip deleted successfully" - else - echo "Unable to delete reserved ip: $reserved_ip. Error: $delete_rip" - fi delete_cm=$(kubectl delete configmap reserved-ip-config -n $NAMESPACE) if [[ "$delete_cm" == *'configmap "reserved-ip-config" deleted'* ]]; then @@ -181,7 +165,6 @@ spec: (contains($stdout, 'No firewall id found in configmap')): false (contains($stdout, 'Unable to delete firewall')): false (contains($stdout, 'Unable to delete the configmap')): false - (contains($stdout, 'Unable to delete reserved ip')): false - name: Check that loadbalancer ip is assigned try: - assert: diff --git a/e2e/test/lb-created-with-reserved-ip-nb-range/create-pods-services.yaml b/e2e/test/lb-created-with-reserved-ip-nb-range/create-pods-services.yaml index 254f8375..930a8183 100644 --- a/e2e/test/lb-created-with-reserved-ip-nb-range/create-pods-services.yaml +++ b/e2e/test/lb-created-with-reserved-ip-nb-range/create-pods-services.yaml @@ -35,6 +35,7 @@ metadata: name: svc-test annotations: service.beta.kubernetes.io/linode-loadbalancer-reserved-ipv4: ($ip.reserved_ip) + service.beta.kubernetes.io/linode-loadbalancer-retain-reserved-ipv4: "false" service.beta.kubernetes.io/linode-loadbalancer-firewall-id: ($ip.fw_id) labels: app: created-with-reserved-ip-nb-range diff --git a/e2e/test/lb-created-with-specified-nb-id-reserved/chainsaw-test.yaml b/e2e/test/lb-created-with-specified-nb-id-reserved/chainsaw-test.yaml index 8a1ea8c1..cc6884a5 100644 --- a/e2e/test/lb-created-with-specified-nb-id-reserved/chainsaw-test.yaml +++ b/e2e/test/lb-created-with-specified-nb-id-reserved/chainsaw-test.yaml @@ -105,21 +105,6 @@ spec: content: | set -euo pipefail - reserved_ip=$(kubectl get configmap nb-config -o=jsonpath='{.data.Reserved_IP}' -n $NAMESPACE) - if [ -z "$reserved_ip" ]; then - echo "Error: No reserved ip found in configmap" - fi - - delete_rip=$(curl -s --request DELETE \ - -H "Authorization: Bearer $LINODE_TOKEN" \ - -H "Content-Type: application/json" --fail-early --retry 3 \ - "${LINODE_URL}/v4beta/networking/reserved/ips/$reserved_ip" ) - - if [[ "$delete_rip" == "{}" ]]; then - echo "Reserved IP $reserved_ip deleted successfully" - else - echo "Unable to delete reserved ip: $reserved_ip. Error: $delete_rip" - fi delete_cm=$(kubectl delete configmap nb-config -n $NAMESPACE) if [[ "$delete_cm" == *'configmap "nb-config" deleted'* ]]; then @@ -131,7 +116,6 @@ spec: ($error == null): true (contains($stdout, 'No reserved ip found in configmap')): false (contains($stdout, 'Unable to delete the configmap')): false - (contains($stdout, 'Unable to delete reserved ip')): false - name: Check that loadbalancer ip is assigned try: - assert: @@ -166,7 +150,7 @@ spec: echo "Error: No service ip found for service svc-test" fi - expectedId=$(kubectl get svc svc-test -n $NAMESPACE -o json | jq -r .metadata.annotations[]) + expectedId=$(kubectl get svc svc-test -n $NAMESPACE -o json | jq -r '.metadata.annotations."service.beta.kubernetes.io/linode-loadbalancer-nodebalancer-id"') nbid=$(KUBECONFIG=$KUBECONFIG NAMESPACE=$NAMESPACE LINODE_TOKEN=$LINODE_TOKEN ../scripts/get-nb-id.sh) if [[ $nbid == $expectedId ]]; then diff --git a/e2e/test/lb-created-with-specified-nb-id-reserved/create-pods-services.yaml b/e2e/test/lb-created-with-specified-nb-id-reserved/create-pods-services.yaml index fb6fe3e9..3273c25d 100644 --- a/e2e/test/lb-created-with-specified-nb-id-reserved/create-pods-services.yaml +++ b/e2e/test/lb-created-with-specified-nb-id-reserved/create-pods-services.yaml @@ -35,6 +35,7 @@ metadata: name: svc-test annotations: service.beta.kubernetes.io/linode-loadbalancer-nodebalancer-id: ($nb_conf.nb_id) + service.beta.kubernetes.io/linode-loadbalancer-retain-reserved-ipv4: "false" labels: app: created-with-specified-nb-id-reserved spec: diff --git a/e2e/test/lb-delete-svc-no-nb/chainsaw-test.yaml b/e2e/test/lb-delete-svc-no-nb/chainsaw-test.yaml index 8df9ffa4..6e5e2d8d 100644 --- a/e2e/test/lb-delete-svc-no-nb/chainsaw-test.yaml +++ b/e2e/test/lb-delete-svc-no-nb/chainsaw-test.yaml @@ -37,6 +37,7 @@ spec: status: (loadBalancer.ingress[0].ip != null): true - name: Create nodebalancer and create resources + skipDelete: true try: - script: shell: bash From b15eb93bdeebbefc42b9eb2aaea69ef971a17442 Mon Sep 17 00:00:00 2001 From: Moshe Vayner Date: Fri, 10 Jul 2026 21:19:24 -0400 Subject: [PATCH 3/3] test: add scenario for handling reserved IP deletion failure in load balancer Signed-off-by: Moshe Vayner --- cloud/linode/fake_linode_test.go | 8 ++++++++ cloud/linode/loadbalancers_test.go | 31 ++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/cloud/linode/fake_linode_test.go b/cloud/linode/fake_linode_test.go index 89ae6baf..737c629f 100644 --- a/cloud/linode/fake_linode_test.go +++ b/cloud/linode/fake_linode_test.go @@ -29,6 +29,8 @@ type fakeAPI struct { vpc map[int]*linodego.VPC subnet map[int]*linodego.VPCSubnet + failDeleteReservedIP bool + requests map[fakeRequest]struct{} mux *http.ServeMux } @@ -334,6 +336,12 @@ func (f *fakeAPI) setupRoutes() { }) f.mux.HandleFunc("DELETE /v4/networking/reserved/ips/{ipAddress}", func(w http.ResponseWriter, r *http.Request) { + if f.failDeleteReservedIP { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(`{"errors":[{"reason":"forced reserved IP delete failure"}]}`)) + return + } + w.WriteHeader(http.StatusOK) }) diff --git a/cloud/linode/loadbalancers_test.go b/cloud/linode/loadbalancers_test.go index f30c9cfd..3bfdc4ef 100644 --- a/cloud/linode/loadbalancers_test.go +++ b/cloud/linode/loadbalancers_test.go @@ -4757,6 +4757,37 @@ func testEnsureLoadBalancerDeleted(t *testing.T, client *linodego.Client, fake * } }) } + + t.Run("load balancer delete returns error when reserved ip deletion fails", func(t *testing.T) { + fake.ResetRequests() + fake.failDeleteReservedIP = true + defer func() { fake.failDeleteReservedIP = false }() + + service := newService("test-delete-reserved-ip-fails", map[string]string{ + annotations.AnnLinodeDefaultProtocol: "tcp", + annotations.AnnLinodeLoadBalancerRetainReservedIPv4: "false", + }) + + nb, err := lb.createNodeBalancer(t.Context(), "linodelb", service, []linodego.NodeBalancerConfigCreateOptions{}) + if err != nil { + t.Fatal(err) + } + service.Status.LoadBalancer = *makeLoadBalancerStatus(service, nb) + + err = lb.EnsureLoadBalancerDeleted(t.Context(), "linodelb", service) + if err == nil { + t.Fatal("expected reserved IP deletion error") + } + + ingressIP := service.Status.LoadBalancer.Ingress[0].IP + if !fake.didRequestOccur(http.MethodDelete, fmt.Sprintf("/networking/reserved/ips/%s", ingressIP), "") { + t.Fatal("expected reserved IP delete request") + } + + if fake.didRequestOccur(http.MethodDelete, fmt.Sprintf("/nodebalancers/%d", nb.ID), "") { + t.Fatal("unexpected node balancer delete request after reserved IP deletion failure") + } + }) } func testEnsureExistingLoadBalancer(t *testing.T, client *linodego.Client, _ *fakeAPI) {