diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c86f224..8da8e3ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -130,9 +130,6 @@ jobs: - name: Run E2E Tests run: mise run e2e-test - - name: Run Cilium BGP e2e test - run: mise run e2e-test-bgp - - name: Run subnet filtering test run: mise run e2e-test-subnet diff --git a/README.md b/README.md index 5cd14b26..c1adadf6 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ A Cloud Controller Manager (CCM) is a Kubernetes control plane component that em ### Core Components #### Node Controller + - Initializes node configuration with Linode-specific information - Sets node addresses (public/private IPs) - Labels nodes with region/zone information @@ -25,6 +26,7 @@ A Cloud Controller Manager (CCM) is a Kubernetes control plane component that em - Manages node cleanup #### Service Controller + - Manages LoadBalancer service implementations using Linode NodeBalancers - Creates and configures NodeBalancers - Updates backend pools @@ -39,6 +41,7 @@ A Cloud Controller Manager (CCM) is a Kubernetes control plane component that em - Custom firewall rules and security configurations #### Route Controller + - Manages VPC and private network integration - Configures routes for pod CIDR ranges - Handles cross-node pod communication @@ -48,6 +51,7 @@ A Cloud Controller Manager (CCM) is a Kubernetes control plane component that em - Configures network routes for optimal communication #### NodeIPAM Controller + - Manages and configures pod CIDRs to nodes ## Requirements @@ -60,6 +64,7 @@ A Cloud Controller Manager (CCM) is a Kubernetes control plane component that em ## Documentation ### Quick Start + - [Getting Started Guide](docs/getting-started/README.md) - Start here for installation and setup - [Overview](docs/getting-started/overview.md) - Learn about CCM basics - [Requirements](docs/getting-started/requirements.md) - Check prerequisites @@ -70,6 +75,7 @@ A Cloud Controller Manager (CCM) is a Kubernetes control plane component that em - [Troubleshooting](docs/getting-started/troubleshooting.md) - Common issues and solutions ### Configuration + - [Configuration Guide](docs/configuration/README.md) - Detailed configuration options - [LoadBalancer Services](docs/configuration/loadbalancer.md) - [Service Annotations](docs/configuration/annotations.md) @@ -81,6 +87,7 @@ A Cloud Controller Manager (CCM) is a Kubernetes control plane component that em - [NodeIPAM Configuration](docs/configuration/nodeipam.md) ### Examples and Development + - [Examples](docs/examples/README.md) - Real-world usage examples - [Basic Services](docs/examples/basic.md) - [Advanced Configuration](docs/examples/advanced.md) @@ -90,13 +97,14 @@ A Cloud Controller Manager (CCM) is a Kubernetes control plane component that em ### Community Support -For general help or discussion, join us in #linode on the [Kubernetes Slack](https://kubernetes.slack.com/messages/CD4B15LUR/details/). +For general help or discussion, join us in #linode on the [Kubernetes Slack](https://kubernetes.slack.com/messages/CD4B15LUR/details/). To sign up for Kubernetes Slack, use the [Kubernetes Slack inviter](http://slack.kubernetes.io/). ### Issue Tracking If you've found a bug or want to request a feature: + - Check the [GitHub Issues](https://github.com/linode/linode-cloud-controller-manager/issues) - Submit a [Pull Request](https://github.com/linode/linode-cloud-controller-manager/pulls) diff --git a/e2e/bgp-test/lb-cilium-bgp/chainsaw-test.yaml b/e2e/bgp-test/lb-cilium-bgp/chainsaw-test.yaml index 79de9215..1800fe79 100644 --- a/e2e/bgp-test/lb-cilium-bgp/chainsaw-test.yaml +++ b/e2e/bgp-test/lb-cilium-bgp/chainsaw-test.yaml @@ -3,8 +3,7 @@ apiVersion: chainsaw.kyverno.io/v1alpha1 kind: Test metadata: name: cilium-bgp-test - labels: - all: + labels: {} spec: namespace: "cilium-bgp-test" catch: @@ -13,9 +12,26 @@ spec: shellArgs: - -c content: | - set -euo pipefail + set -uo pipefail echo "Test failed. Fetching CCM logs..." - kubectl logs -n kube-system daemonsets/ccm-linode | grep "cilium-bgp-test" | tail -100 + kubectl logs -n kube-system daemonsets/ccm-linode | grep "cilium-bgp-test" | tail -100 || true + + # In BGP mode CCM creates a nanode (a small Linode instance) to hold the + # shared LoadBalancer IP once the Service is created. The final step + # deletes it, but if an earlier step fails that step never runs and the + # nanode would leak. Delete it here by its label suffix (best-effort, + # idempotent). chainsaw does not track instances created via the API. + echo "Cleaning up BGP IP-holder nanode (if any)..." + if [[ -n "${CLUSTER_NAME:-}" ]]; then + nanode_id=$(curl -s -H "Authorization: Bearer $LINODE_TOKEN" \ + "$LINODE_URL/v4/linode/instances" \ + | jq -r --arg cluster "$CLUSTER_NAME" '.data[] | select(.label | endswith($cluster)) | .id' 2>/dev/null || true) + if [[ "$nanode_id" =~ ^[0-9]+$ ]]; then + echo "Deleting nanode $nanode_id" + curl -s -o /dev/null -X DELETE -H "Authorization: Bearer $LINODE_TOKEN" \ + "$LINODE_URL/v4/linode/instances/$nanode_id" || true + fi + fi steps: - name: Check if CCM is deployed try: diff --git a/e2e/test/fw-use-specified-nb/chainsaw-test.yaml b/e2e/test/fw-use-specified-nb/chainsaw-test.yaml index e0d2ee7c..f39ae858 100644 --- a/e2e/test/fw-use-specified-nb/chainsaw-test.yaml +++ b/e2e/test/fw-use-specified-nb/chainsaw-test.yaml @@ -16,10 +16,30 @@ spec: shell: bash shellArgs: - -c + env: + - name: FWLABEL + value: ($fwname) content: | - set -euo pipefail + set -uo pipefail echo "Test failed. Fetching CCM logs..." - kubectl logs -n kube-system daemonsets/ccm-linode | grep "fw-use-specified-nb" | tail -100 + kubectl logs -n kube-system daemonsets/ccm-linode | grep "fw-use-specified-nb" | tail -100 || true + + # This test creates a Firewall directly via the Linode API, which chainsaw + # does not track and will not auto-delete. If a step fails before the test + # deletes it, the firewall would leak. Look it up by its (deterministic) + # label and delete it here (best-effort, idempotent). + echo "Cleaning up firewall (if any)..." + fwid=$(curl -s \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + -H "Content-Type: application/json" \ + -H "X-Filter: {\"label\": \"$FWLABEL\"}" \ + "$LINODE_URL/v4/networking/firewalls" | jq -r '.data[0].id // empty' 2>/dev/null || true) + if [[ "$fwid" =~ ^[0-9]+$ ]]; then + echo "Deleting firewall $fwid" + curl -s -o /dev/null -X DELETE \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + "$LINODE_URL/v4/networking/firewalls/$fwid" || true + fi steps: - name: Create firewall, Create pods and services try: diff --git a/e2e/test/lb-created-with-new-nb-id/chainsaw-test.yaml b/e2e/test/lb-created-with-new-nb-id/chainsaw-test.yaml index 8de4585e..e13b961c 100644 --- a/e2e/test/lb-created-with-new-nb-id/chainsaw-test.yaml +++ b/e2e/test/lb-created-with-new-nb-id/chainsaw-test.yaml @@ -14,9 +14,28 @@ spec: shellArgs: - -c content: | - set -euo pipefail + set -uo pipefail echo "Test failed. Fetching CCM logs..." - kubectl logs -n kube-system daemonsets/ccm-linode | grep "lb-created-with-new-nb-id" | tail -100 + kubectl logs -n kube-system daemonsets/ccm-linode | grep "lb-created-with-new-nb-id" | tail -100 || true + + # This test creates NodeBalancers directly via the Linode API, which + # chainsaw does not track and will not auto-delete. Their ids are recorded + # in the nb-cleanup ConfigMap as they are created. If a step fails before + # a NodeBalancer is adopted by a Service (so CCM would clean it up), delete + # it here so it does not leak (best-effort, idempotent). + echo "Cleaning up NodeBalancers created via the API (if any)..." + nbids="" + if kubectl get configmap nb-cleanup -n "$NAMESPACE" >/dev/null 2>&1; then + nbids="$(kubectl get configmap nb-cleanup -n "$NAMESPACE" -o jsonpath='{.data.nb1} {.data.nb2}' 2>/dev/null || true)" + fi + for nbid in $nbids; do + if [[ "$nbid" =~ ^[0-9]+$ ]]; then + echo "Deleting NodeBalancer $nbid" + curl -s -o /dev/null -X DELETE \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + "$LINODE_URL/v4/nodebalancers/$nbid" || true + fi + done steps: - name: Create dummy service try: @@ -66,6 +85,11 @@ spec: exit 1 fi + # Record the NodeBalancer id so the test-level catch can delete it if + # a later step fails before it is adopted by a Service (chainsaw does + # not track NodeBalancers created via the API). + kubectl create configmap nb-cleanup -n "$NAMESPACE" --from-literal=nb1="$nbid" --dry-run=client -o yaml | kubectl apply -f - >/dev/null 2>&1 || true + echo "{\"id\": \"$nbid\"}" check: ($error == null): true @@ -148,6 +172,11 @@ spec: if ! [[ $nbid =~ $re ]]; then echo "Nodebalancer id [$nbid] is incorrect, failed to create nodebalancer" else + # Record both NodeBalancer ids so the test-level catch can clean + # up either one if a later step fails (chainsaw does not track + # NodeBalancers created via the API). + kubectl create configmap nb-cleanup -n "$NAMESPACE" --from-literal=nb1="$old_nbid" --from-literal=nb2="$nbid" --dry-run=client -o yaml | kubectl apply -f - >/dev/null 2>&1 || true + kubectl annotate --overwrite svc svc-test -n $NAMESPACE service.beta.kubernetes.io/linode-loadbalancer-nodebalancer-id=$nbid for i in {1..10}; do 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 54c74ad2..f4ff9961 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,6 +105,26 @@ spec: content: | set -euo pipefail + # Delete the NodeBalancer created via the API and release the reserved + # IP if they still exist. On the happy path CCM already did both when + # svc-test was torn down, so these are best-effort, idempotent no-ops; + # on failure paths they prevent leaks (chainsaw does not track + # NodeBalancers or reserved IPs created via the API). + cleanup_nb_id=$(kubectl get configmap nb-config -o=jsonpath='{.data.NB_ID}' -n $NAMESPACE 2>/dev/null || true) + if [[ "$cleanup_nb_id" =~ ^[0-9]+$ ]]; then + echo "Deleting NodeBalancer $cleanup_nb_id (best-effort)" + curl -s -o /dev/null --request DELETE \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + "${LINODE_URL}/v4beta/nodebalancers/$cleanup_nb_id" || true + fi + primary_ip=$(kubectl get configmap nb-config -o=jsonpath='{.data.Reserved_IP}' -n $NAMESPACE 2>/dev/null || true) + if [[ "$primary_ip" =~ ^[0-9.]+$ ]]; then + echo "Releasing reserved IP $primary_ip (best-effort)" + curl -s -o /dev/null --request DELETE \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + "${LINODE_URL}/v4beta/networking/reserved/ips/$primary_ip" || true + fi + delete_cm=$(kubectl delete configmap nb-config -n $NAMESPACE) if [[ "$delete_cm" == *'configmap "nb-config" deleted'* ]]; then echo "Configmap deleted successfully" 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 81bacdae..068cb3c2 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 @@ -122,6 +122,17 @@ spec: echo "Unable to delete NB: $nb_id. Error: $del_nb" fi + # The reserved IP is attached to the NodeBalancer deleted above. This + # is a negative test, so CCM never adopts svc-test and never releases + # the reserved IP; release it here (best-effort) so it does not leak. + primary_ip=$(kubectl get configmap nb-config -o=jsonpath='{.data.Reserved_IP}' -n $NAMESPACE 2>/dev/null || true) + if [[ "$primary_ip" =~ ^[0-9.]+$ ]]; then + echo "Releasing reserved IP $primary_ip (best-effort)" + curl -s -o /dev/null --request DELETE \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + "${LINODE_URL}/v4beta/networking/reserved/ips/$primary_ip" || true + fi + delete_cm=$(kubectl delete configmap nb-config -n $NAMESPACE) if [[ "$delete_cm" == *'configmap "nb-config" deleted'* ]]; then echo "Configmap deleted successfully" 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 168ea2f1..be2475ce 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 @@ -106,6 +106,19 @@ spec: echo "Unable to delete reserved ip: $reserved_ip2. Error: $delete_rip2" fi + # Release the primary reserved IP if it still exists. On the happy + # path CCM already released it when svc-test was torn down, so this + # is a best-effort, idempotent no-op; on failure paths it prevents + # the reserved IP from leaking (chainsaw does not track reserved IPs + # created via the API). + primary_ip=$(kubectl get configmap reserved-ip-config -o=jsonpath='{.data.ReservedIP}' -n $NAMESPACE 2>/dev/null || true) + if [[ "$primary_ip" =~ ^[0-9.]+$ ]]; then + echo "Releasing reserved IP $primary_ip (best-effort)" + curl -s -o /dev/null --request DELETE \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + "${LINODE_URL}/v4beta/networking/reserved/ips/$primary_ip" || true + fi + delete_cm=$(kubectl delete configmap reserved-ip-config -n $NAMESPACE) if [[ "$delete_cm" == *'configmap "reserved-ip-config" deleted'* ]]; then echo "Configmap deleted successfully" 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 a006d639..dbbc2622 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 @@ -75,6 +75,20 @@ spec: set -euo pipefail + # Release the primary reserved IP if it still exists. On the happy + # path CCM already released it when svc-test was torn down, so this + # is a best-effort, idempotent no-op; on failure paths it prevents + # the reserved IP from leaking (chainsaw does not track reserved IPs + # created via the API). The UnreservedIP value is not a real + # reservation, so it is intentionally left alone. + primary_ip=$(kubectl get configmap reserved-ip-config -o=jsonpath='{.data.ReservedIP}' -n $NAMESPACE 2>/dev/null || true) + if [[ "$primary_ip" =~ ^[0-9.]+$ ]]; then + echo "Releasing reserved IP $primary_ip (best-effort)" + curl -s -o /dev/null --request DELETE \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + "${LINODE_URL}/v4beta/networking/reserved/ips/$primary_ip" || true + fi + delete_cm=$(kubectl delete configmap reserved-ip-config -n $NAMESPACE) if [[ "$delete_cm" == *'configmap "reserved-ip-config" deleted'* ]]; then echo "Configmap deleted successfully" 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 f2e4f919..b6a12194 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 @@ -104,6 +104,19 @@ spec: echo "Unable to delete reserved ip: $reserved_ip2. Error: $delete_rip2" fi + # Release the primary reserved IP if it still exists. On the happy + # path CCM already released it when svc-test was torn down, so this + # is a best-effort, idempotent no-op; on failure paths it prevents + # the reserved IP from leaking (chainsaw does not track reserved IPs + # created via the API). + primary_ip=$(kubectl get configmap reserved-ip-config -o=jsonpath='{.data.ReservedIP}' -n $NAMESPACE 2>/dev/null || true) + if [[ "$primary_ip" =~ ^[0-9.]+$ ]]; then + echo "Releasing reserved IP $primary_ip (best-effort)" + curl -s -o /dev/null --request DELETE \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + "${LINODE_URL}/v4beta/networking/reserved/ips/$primary_ip" || true + fi + delete_cm=$(kubectl delete configmap reserved-ip-config -n $NAMESPACE) if [[ "$delete_cm" == *'configmap "reserved-ip-config" deleted'* ]]; then echo "Configmap deleted successfully" 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 fbe6723f..7823b1a6 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 @@ -121,6 +121,19 @@ spec: echo "Unable to delete firewall: $fw_id. Error: $delete_fw" fi + # Release the primary reserved IP if it still exists. On the happy + # path CCM already released it when svc-test was torn down, so this + # is a best-effort, idempotent no-op; on failure paths it prevents + # the reserved IP from leaking (chainsaw does not track reserved IPs + # created via the API). + primary_ip=$(kubectl get configmap reserved-ip-config -o=jsonpath='{.data.ReservedIP}' -n $NAMESPACE 2>/dev/null || true) + if [[ "$primary_ip" =~ ^[0-9.]+$ ]]; then + echo "Releasing reserved IP $primary_ip (best-effort)" + curl -s -o /dev/null --request DELETE \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + "${LINODE_URL}/v4beta/networking/reserved/ips/$primary_ip" || true + fi + delete_cm=$(kubectl delete configmap reserved-ip-config -n $NAMESPACE) if [[ "$delete_cm" == *'configmap "reserved-ip-config" deleted'* ]]; then echo "Configmap deleted successfully" 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 548641cb..fb74fbcc 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 @@ -107,6 +107,19 @@ spec: echo "Unable to delete reserved ip: $reserved_ip2. Error: $delete_rip2" fi + # Release the primary reserved IP if it still exists. On the happy + # path CCM already released it when svc-test was torn down, so this + # is a best-effort, idempotent no-op; on failure paths it prevents + # the reserved IP from leaking (chainsaw does not track reserved IPs + # created via the API). + primary_ip=$(kubectl get configmap reserved-ip-config -o=jsonpath='{.data.ReservedIP}' -n $NAMESPACE 2>/dev/null || true) + if [[ "$primary_ip" =~ ^[0-9.]+$ ]]; then + echo "Releasing reserved IP $primary_ip (best-effort)" + curl -s -o /dev/null --request DELETE \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + "${LINODE_URL}/v4beta/networking/reserved/ips/$primary_ip" || true + fi + delete_cm=$(kubectl delete configmap reserved-ip-config -n $NAMESPACE) if [[ "$delete_cm" == *'configmap "reserved-ip-config" deleted'* ]]; then echo "Configmap deleted successfully" 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 11a8c1ff..3ade0e58 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 @@ -154,6 +154,19 @@ spec: echo "Unable to delete firewall: $fw_id. Error: $delete_fw" fi + # Release the primary reserved IP if it still exists. On the happy + # path CCM already released it when svc-test was torn down, so this + # is a best-effort, idempotent no-op; on failure paths it prevents + # the reserved IP from leaking (chainsaw does not track reserved IPs + # created via the API). + primary_ip=$(kubectl get configmap reserved-ip-config -o=jsonpath='{.data.ReservedIP}' -n $NAMESPACE 2>/dev/null || true) + if [[ "$primary_ip" =~ ^[0-9.]+$ ]]; then + echo "Releasing reserved IP $primary_ip (best-effort)" + curl -s -o /dev/null --request DELETE \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + "${LINODE_URL}/v4beta/networking/reserved/ips/$primary_ip" || true + fi + delete_cm=$(kubectl delete configmap reserved-ip-config -n $NAMESPACE) if [[ "$delete_cm" == *'configmap "reserved-ip-config" deleted'* ]]; then echo "Configmap deleted successfully" 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 cc6884a5..ad8b3c52 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 @@ -106,6 +106,26 @@ spec: set -euo pipefail + # Delete the NodeBalancer created via the API and release the reserved + # IP if they still exist. On the happy path CCM already did both when + # svc-test was torn down, so these are best-effort, idempotent no-ops; + # on failure paths they prevent leaks (chainsaw does not track + # NodeBalancers or reserved IPs created via the API). + cleanup_nb_id=$(kubectl get configmap nb-config -o=jsonpath='{.data.NB_ID}' -n $NAMESPACE 2>/dev/null || true) + if [[ "$cleanup_nb_id" =~ ^[0-9]+$ ]]; then + echo "Deleting NodeBalancer $cleanup_nb_id (best-effort)" + curl -s -o /dev/null --request DELETE \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + "${LINODE_URL}/v4beta/nodebalancers/$cleanup_nb_id" || true + fi + primary_ip=$(kubectl get configmap nb-config -o=jsonpath='{.data.Reserved_IP}' -n $NAMESPACE 2>/dev/null || true) + if [[ "$primary_ip" =~ ^[0-9.]+$ ]]; then + echo "Releasing reserved IP $primary_ip (best-effort)" + curl -s -o /dev/null --request DELETE \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + "${LINODE_URL}/v4beta/networking/reserved/ips/$primary_ip" || true + fi + delete_cm=$(kubectl delete configmap nb-config -n $NAMESPACE) if [[ "$delete_cm" == *'configmap "nb-config" deleted'* ]]; then echo "Configmap deleted successfully" diff --git a/e2e/test/lb-created-with-specified-nb-id/chainsaw-test.yaml b/e2e/test/lb-created-with-specified-nb-id/chainsaw-test.yaml index 5dacb8ea..c8976d6e 100644 --- a/e2e/test/lb-created-with-specified-nb-id/chainsaw-test.yaml +++ b/e2e/test/lb-created-with-specified-nb-id/chainsaw-test.yaml @@ -14,9 +14,28 @@ spec: shellArgs: - -c content: | - set -euo pipefail + set -uo pipefail echo "Test failed. Fetching CCM logs..." - kubectl logs -n kube-system daemonsets/ccm-linode | grep "lb-created-with-specified-nb-id" | tail -100 + kubectl logs -n kube-system daemonsets/ccm-linode | grep "lb-created-with-specified-nb-id" | tail -100 || true + + # This test creates a NodeBalancer directly via the Linode API, which + # chainsaw does not track and will not auto-delete. Its id is recorded in + # the nb-cleanup ConfigMap as it is created. If a step fails before the + # NodeBalancer is adopted by a Service (so CCM would clean it up), delete + # it here so it does not leak (best-effort, idempotent). + echo "Cleaning up NodeBalancers created via the API (if any)..." + nbids="" + if kubectl get configmap nb-cleanup -n "$NAMESPACE" >/dev/null 2>&1; then + nbids="$(kubectl get configmap nb-cleanup -n "$NAMESPACE" -o jsonpath='{.data.nb1} {.data.nb2}' 2>/dev/null || true)" + fi + for nbid in $nbids; do + if [[ "$nbid" =~ ^[0-9]+$ ]]; then + echo "Deleting NodeBalancer $nbid" + curl -s -o /dev/null -X DELETE \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + "$LINODE_URL/v4/nodebalancers/$nbid" || true + fi + done steps: - name: Create dummy service try: @@ -66,6 +85,11 @@ spec: exit 1 fi + # Record the NodeBalancer id so the test-level catch can delete it if + # a later step fails before it is adopted by a Service (chainsaw does + # not track NodeBalancers created via the API). + kubectl create configmap nb-cleanup -n "$NAMESPACE" --from-literal=nb1="$nbid" --dry-run=client -o yaml | kubectl apply -f - >/dev/null 2>&1 || true + echo "{\"id\": \"$nbid\"}" check: ($error == null): true 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 6e5e2d8d..e65649ba 100644 --- a/e2e/test/lb-delete-svc-no-nb/chainsaw-test.yaml +++ b/e2e/test/lb-delete-svc-no-nb/chainsaw-test.yaml @@ -14,9 +14,28 @@ spec: shellArgs: - -c content: | - set -euo pipefail + set -uo pipefail echo "Test failed. Fetching CCM logs..." - kubectl logs -n kube-system daemonsets/ccm-linode | grep "lb-delete-svc-no-nb" | tail -100 + kubectl logs -n kube-system daemonsets/ccm-linode | grep "lb-delete-svc-no-nb" | tail -100 || true + + # This test creates a NodeBalancer directly via the Linode API, which + # chainsaw does not track and will not auto-delete. Its id is recorded in + # the nb-cleanup ConfigMap as it is created. If a step fails before the + # NodeBalancer is adopted by a Service (so CCM would clean it up), delete + # it here so it does not leak (best-effort, idempotent). + echo "Cleaning up NodeBalancers created via the API (if any)..." + nbids="" + if kubectl get configmap nb-cleanup -n "$NAMESPACE" >/dev/null 2>&1; then + nbids="$(kubectl get configmap nb-cleanup -n "$NAMESPACE" -o jsonpath='{.data.nb1} {.data.nb2}' 2>/dev/null || true)" + fi + for nbid in $nbids; do + if [[ "$nbid" =~ ^[0-9]+$ ]]; then + echo "Deleting NodeBalancer $nbid" + curl -s -o /dev/null -X DELETE \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + "$LINODE_URL/v4/nodebalancers/$nbid" || true + fi + done steps: - name: Create dummy service try: @@ -37,7 +56,6 @@ spec: status: (loadBalancer.ingress[0].ip != null): true - name: Create nodebalancer and create resources - skipDelete: true try: - script: shell: bash @@ -67,6 +85,11 @@ spec: exit 1 fi + # Record the NodeBalancer id so the test-level catch can delete it if + # a later step fails before it is adopted by a Service (chainsaw does + # not track NodeBalancers created via the API). + kubectl create configmap nb-cleanup -n "$NAMESPACE" --from-literal=nb1="$nbid" --dry-run=client -o yaml | kubectl apply -f - >/dev/null 2>&1 || true + echo "{\"id\": \"$nbid\"}" check: ($error == null): true diff --git a/e2e/test/lb-delete-svc-use-new-nbid/chainsaw-test.yaml b/e2e/test/lb-delete-svc-use-new-nbid/chainsaw-test.yaml index 81bda5d2..1b4e8c44 100644 --- a/e2e/test/lb-delete-svc-use-new-nbid/chainsaw-test.yaml +++ b/e2e/test/lb-delete-svc-use-new-nbid/chainsaw-test.yaml @@ -14,9 +14,28 @@ spec: shellArgs: - -c content: | - set -euo pipefail + set -uo pipefail echo "Test failed. Fetching CCM logs..." - kubectl logs -n kube-system daemonsets/ccm-linode | grep "lb-delete-svc-use-new-nbid" | tail -100 + kubectl logs -n kube-system daemonsets/ccm-linode | grep "lb-delete-svc-use-new-nbid" | tail -100 || true + + # This test creates NodeBalancers directly via the Linode API, which + # chainsaw does not track and will not auto-delete. Their ids are recorded + # in the nb-cleanup ConfigMap as they are created. If a step fails before + # a NodeBalancer is adopted by a Service (so CCM would clean it up), delete + # it here so it does not leak (best-effort, idempotent). + echo "Cleaning up NodeBalancers created via the API (if any)..." + nbids="" + if kubectl get configmap nb-cleanup -n "$NAMESPACE" >/dev/null 2>&1; then + nbids="$(kubectl get configmap nb-cleanup -n "$NAMESPACE" -o jsonpath='{.data.nb1} {.data.nb2}' 2>/dev/null || true)" + fi + for nbid in $nbids; do + if [[ "$nbid" =~ ^[0-9]+$ ]]; then + echo "Deleting NodeBalancer $nbid" + curl -s -o /dev/null -X DELETE \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + "$LINODE_URL/v4/nodebalancers/$nbid" || true + fi + done steps: - name: Create dummy service try: @@ -66,6 +85,11 @@ spec: exit 1 fi + # Record the NodeBalancer id so the test-level catch can delete it if + # a later step fails before it is adopted by a Service (chainsaw does + # not track NodeBalancers created via the API). + kubectl create configmap nb-cleanup -n "$NAMESPACE" --from-literal=nb1="$nbid" --dry-run=client -o yaml | kubectl apply -f - >/dev/null 2>&1 || true + echo "{\"id\": \"$nbid\"}" check: ($error == null): true @@ -150,6 +174,11 @@ spec: exit 1 fi + # Record both NodeBalancer ids so the test-level catch can clean up + # either one if a later step fails (chainsaw does not track + # NodeBalancers created via the API). + kubectl create configmap nb-cleanup -n "$NAMESPACE" --from-literal=nb1="$old_nbid" --from-literal=nb2="$nbid" --dry-run=client -o yaml | kubectl apply -f - >/dev/null 2>&1 || true + kubectl annotate --overwrite svc svc-test -n $NAMESPACE service.beta.kubernetes.io/linode-loadbalancer-nodebalancer-id=$nbid for i in {1..10}; do diff --git a/e2e/test/lb-delete-svc-use-specified-nb/chainsaw-test.yaml b/e2e/test/lb-delete-svc-use-specified-nb/chainsaw-test.yaml index fa92c03a..24101504 100644 --- a/e2e/test/lb-delete-svc-use-specified-nb/chainsaw-test.yaml +++ b/e2e/test/lb-delete-svc-use-specified-nb/chainsaw-test.yaml @@ -14,9 +14,28 @@ spec: shellArgs: - -c content: | - set -euo pipefail + set -uo pipefail echo "Test failed. Fetching CCM logs..." - kubectl logs -n kube-system daemonsets/ccm-linode | grep "lb-delete-svc-use-specified-nb" | tail -100 + kubectl logs -n kube-system daemonsets/ccm-linode | grep "lb-delete-svc-use-specified-nb" | tail -100 || true + + # This test creates a NodeBalancer directly via the Linode API, which + # chainsaw does not track and will not auto-delete. Its id is recorded in + # the nb-cleanup ConfigMap as it is created. If a step fails before the + # NodeBalancer is adopted by a Service (so CCM would clean it up), delete + # it here so it does not leak (best-effort, idempotent). + echo "Cleaning up NodeBalancers created via the API (if any)..." + nbids="" + if kubectl get configmap nb-cleanup -n "$NAMESPACE" >/dev/null 2>&1; then + nbids="$(kubectl get configmap nb-cleanup -n "$NAMESPACE" -o jsonpath='{.data.nb1} {.data.nb2}' 2>/dev/null || true)" + fi + for nbid in $nbids; do + if [[ "$nbid" =~ ^[0-9]+$ ]]; then + echo "Deleting NodeBalancer $nbid" + curl -s -o /dev/null -X DELETE \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + "$LINODE_URL/v4/nodebalancers/$nbid" || true + fi + done steps: - name: Create dummy service try: @@ -66,6 +85,11 @@ spec: exit 1 fi + # Record the NodeBalancer id so the test-level catch can delete it if + # a later step fails before it is adopted by a Service (chainsaw does + # not track NodeBalancers created via the API). + kubectl create configmap nb-cleanup -n "$NAMESPACE" --from-literal=nb1="$nbid" --dry-run=client -o yaml | kubectl apply -f - >/dev/null 2>&1 || true + echo "{\"id\": \"$nbid\"}" check: ($error == null): true diff --git a/e2e/test/lb-fw-delete-acl/chainsaw-test.yaml b/e2e/test/lb-fw-delete-acl/chainsaw-test.yaml index 632a2280..1311ed10 100644 --- a/e2e/test/lb-fw-delete-acl/chainsaw-test.yaml +++ b/e2e/test/lb-fw-delete-acl/chainsaw-test.yaml @@ -19,7 +19,6 @@ spec: kubectl logs -n kube-system daemonsets/ccm-linode | grep "lb-fw-delete-acl" | tail -100 steps: - name: Create pods and services - skipDelete: true try: - apply: file: create-pods-services.yaml @@ -119,7 +118,6 @@ spec: ($error == null): true (contains($stdout, 'firewall detatched and deleted')): true - name: Refresh service by adding the ACL again - skipDelete: true try: - apply: file: create-pods-services.yaml diff --git a/e2e/test/lb-preserve-annotation-new-nb-specified/chainsaw-test.yaml b/e2e/test/lb-preserve-annotation-new-nb-specified/chainsaw-test.yaml index 26396930..7831d3a5 100644 --- a/e2e/test/lb-preserve-annotation-new-nb-specified/chainsaw-test.yaml +++ b/e2e/test/lb-preserve-annotation-new-nb-specified/chainsaw-test.yaml @@ -14,9 +14,38 @@ spec: shellArgs: - -c content: | - set -euo pipefail + set -uo pipefail echo "Test failed. Fetching CCM logs..." - kubectl logs -n kube-system daemonsets/ccm-linode | grep "lb-preserve-annotation-new-nb-specified" | tail -100 + kubectl logs -n kube-system daemonsets/ccm-linode | grep "lb-preserve-annotation-new-nb-specified" | tail -100 || true + + # svc-test carries the preserve annotation (so CCM will not delete its + # NodeBalancer when the service is removed), and this test also creates a + # second NodeBalancer via the API. If the test fails partway, delete any + # NodeBalancers it created here (best-effort, idempotent) so they do not + # leak. Candidate ids come from the nb-cleanup ConfigMap (written as the + # ids are obtained) plus the one the service currently points to. + echo "Cleaning up NodeBalancers (if any)..." + nbids="" + if kubectl get configmap nb-cleanup -n "$NAMESPACE" >/dev/null 2>&1; then + nbids="$(kubectl get configmap nb-cleanup -n "$NAMESPACE" -o jsonpath='{.data.old} {.data.new}' 2>/dev/null || true)" + fi + ip=$(kubectl get svc svc-test -n "$NAMESPACE" -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || true) + if [[ -n "$ip" ]]; then + current=$(curl -s \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + -H "X-Filter: {\"ipv4\": \"$ip\"}" \ + "$LINODE_URL/v4/nodebalancers" | jq -r '.data[0].id // empty' || true) + nbids="$nbids $current" + fi + for nbid in $nbids; do + if [[ "$nbid" =~ ^[0-9]+$ ]]; then + echo "Deleting NodeBalancer $nbid" + curl -s -o /dev/null -X DELETE \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + "$LINODE_URL/v4/nodebalancers/$nbid" || true + fi + done + kubectl delete svc svc-test -n "$NAMESPACE" --ignore-not-found --timeout=60s || true steps: - name: Create resources try: @@ -53,6 +82,11 @@ spec: # Get existing nodebalancer id old_nbid=$(KUBECONFIG=$KUBECONFIG NAMESPACE=$NAMESPACE LINODE_TOKEN=$LINODE_TOKEN ../scripts/get-nb-id.sh) + # Record the existing NodeBalancer id so the test-level catch can + # clean it up if a later command fails (svc-test uses the preserve + # annotation, so CCM will not delete it automatically). + kubectl create configmap nb-cleanup -n $NAMESPACE --from-literal=old=$old_nbid --dry-run=client -o yaml | kubectl apply -f - + # Create new nodebalancer and use it LABEL="ccm-$(head /dev/urandom | tr -dc 'a-z0-9' | head -c 5)" @@ -85,6 +119,10 @@ spec: exit 1 fi + # Record the newly created NodeBalancer id as well so the catch block + # can clean up both NodeBalancers if a later command fails. + kubectl create configmap nb-cleanup -n $NAMESPACE --from-literal=old=$old_nbid --from-literal=new=$nbid --dry-run=client -o yaml | kubectl apply -f - + kubectl annotate svc svc-test -n $NAMESPACE service.beta.kubernetes.io/linode-loadbalancer-nodebalancer-id=$nbid for i in {1..10}; do diff --git a/e2e/test/lb-preserve-annotation-svc-delete/chainsaw-test.yaml b/e2e/test/lb-preserve-annotation-svc-delete/chainsaw-test.yaml index ce221e94..6cccc7e7 100644 --- a/e2e/test/lb-preserve-annotation-svc-delete/chainsaw-test.yaml +++ b/e2e/test/lb-preserve-annotation-svc-delete/chainsaw-test.yaml @@ -14,12 +14,39 @@ spec: shellArgs: - -c content: | - set -euo pipefail + set -uo pipefail echo "Test failed. Fetching CCM logs..." - kubectl logs -n kube-system daemonsets/ccm-linode | grep "lb-preserve-annotation-svc-delete" | tail -100 + kubectl logs -n kube-system daemonsets/ccm-linode | grep "lb-preserve-annotation-svc-delete" | tail -100 || true + + # svc-test carries the preserve annotation, so CCM does not delete its + # NodeBalancer when the service is removed. If the test fails partway, + # clean up the NodeBalancer here (best-effort, idempotent) so it does + # not leak. The id is read from the nb-cleanup ConfigMap when present + # (written before the service is deleted), otherwise looked up via the + # service's ingress IP. + echo "Cleaning up preserved NodeBalancer (if any)..." + nbid="" + if kubectl get configmap nb-cleanup -n "$NAMESPACE" >/dev/null 2>&1; then + nbid=$(kubectl get configmap nb-cleanup -n "$NAMESPACE" -o jsonpath='{.data.nbid}' 2>/dev/null || true) + fi + if ! [[ "$nbid" =~ ^[0-9]+$ ]]; then + ip=$(kubectl get svc svc-test -n "$NAMESPACE" -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || true) + if [[ -n "$ip" ]]; then + nbid=$(curl -s \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + -H "X-Filter: {\"ipv4\": \"$ip\"}" \ + "$LINODE_URL/v4/nodebalancers" | jq -r '.data[0].id // empty' || true) + fi + fi + if [[ "$nbid" =~ ^[0-9]+$ ]]; then + echo "Deleting NodeBalancer $nbid" + curl -s -o /dev/null -X DELETE \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + "$LINODE_URL/v4/nodebalancers/$nbid" || true + fi + kubectl delete svc svc-test -n "$NAMESPACE" --ignore-not-found --timeout=60s || true steps: - name: Create resources - skipDelete: true try: - apply: file: create-pods-services.yaml @@ -51,6 +78,11 @@ spec: nbid=$(KUBECONFIG=$KUBECONFIG NAMESPACE=$NAMESPACE LINODE_TOKEN=$LINODE_TOKEN ../scripts/get-nb-id.sh) + # Record the NodeBalancer id so the test-level catch can delete it if + # a later command fails after the service is gone (the preserve + # annotation means CCM will not delete it automatically). + kubectl create configmap nb-cleanup -n $NAMESPACE --from-literal=nbid=$nbid --dry-run=client -o yaml | kubectl apply -f - + kubectl --timeout=60s -n $NAMESPACE delete deploy test kubectl --timeout=60s -n $NAMESPACE delete svc svc-test sleep 20 diff --git a/e2e/test/lb-simple/chainsaw-test.yaml b/e2e/test/lb-simple/chainsaw-test.yaml index 8c24f4e4..3838d69f 100644 --- a/e2e/test/lb-simple/chainsaw-test.yaml +++ b/e2e/test/lb-simple/chainsaw-test.yaml @@ -19,7 +19,6 @@ spec: kubectl logs -n kube-system daemonsets/ccm-linode | grep "lb-simple" | tail -100 steps: - name: Create pods and services - skipDelete: true try: - apply: file: create-pods-services.yaml @@ -82,15 +81,3 @@ spec: check: ($error == null): true (contains($stdout, 'all pods responded')): true - - name: Delete Pods - try: - - delete: - ref: - apiVersion: v1 - kind: Pod - - name: Delete Service - try: - - delete: - ref: - apiVersion: v1 - kind: Service diff --git a/e2e/test/lb-single-tls/chainsaw-test.yaml b/e2e/test/lb-single-tls/chainsaw-test.yaml index a17551f3..5c4bfa98 100644 --- a/e2e/test/lb-single-tls/chainsaw-test.yaml +++ b/e2e/test/lb-single-tls/chainsaw-test.yaml @@ -30,7 +30,6 @@ spec: check: ($error == null): true - name: Create pods and services - skipDelete: true try: - apply: file: create-pods-services.yaml @@ -93,15 +92,3 @@ spec: check: ($error == null): true (contains($stdout, 'all pods responded')): true - - name: Delete Pods - try: - - delete: - ref: - apiVersion: v1 - kind: Pod - - name: Delete Service - try: - - delete: - ref: - apiVersion: v1 - kind: Service diff --git a/e2e/test/lb-update-port/chainsaw-test.yaml b/e2e/test/lb-update-port/chainsaw-test.yaml index 107ef22a..b0defaa4 100644 --- a/e2e/test/lb-update-port/chainsaw-test.yaml +++ b/e2e/test/lb-update-port/chainsaw-test.yaml @@ -19,7 +19,6 @@ spec: kubectl logs -n kube-system daemonsets/ccm-linode | grep "lb-update-port" | tail -100 steps: - name: Create pods and services - skipDelete: true try: - apply: file: create-pods-services.yaml @@ -83,7 +82,6 @@ spec: ($error == null): true (contains($stdout, 'all pods responded')): true - name: Update service - skipDelete: true try: - apply: file: update-port-service.yaml @@ -156,15 +154,3 @@ spec: check: ($error == null): true (contains($stdout, 'firewall rule updated with new port')): true - - name: Delete Pods - try: - - delete: - ref: - apiVersion: v1 - kind: Pod - - name: Delete Service - try: - - delete: - ref: - apiVersion: v1 - kind: Service diff --git a/e2e/test/lb-updated-with-nb-id/chainsaw-test.yaml b/e2e/test/lb-updated-with-nb-id/chainsaw-test.yaml index de9f3eca..0b5669b2 100644 --- a/e2e/test/lb-updated-with-nb-id/chainsaw-test.yaml +++ b/e2e/test/lb-updated-with-nb-id/chainsaw-test.yaml @@ -14,9 +14,28 @@ spec: shellArgs: - -c content: | - set -euo pipefail + set -uo pipefail echo "Test failed. Fetching CCM logs..." - kubectl logs -n kube-system daemonsets/ccm-linode | grep "lb-updated-with-nb-id" | tail -100 + kubectl logs -n kube-system daemonsets/ccm-linode | grep "lb-updated-with-nb-id" | tail -100 || true + + # This test creates a NodeBalancer directly via the Linode API, which + # chainsaw does not track and will not auto-delete. Its id is recorded in + # the nb-cleanup ConfigMap as it is created. If a step fails before the + # NodeBalancer is adopted by the Service (so CCM would clean it up), + # delete it here so it does not leak (best-effort, idempotent). + echo "Cleaning up NodeBalancers created via the API (if any)..." + nbids="" + if kubectl get configmap nb-cleanup -n "$NAMESPACE" >/dev/null 2>&1; then + nbids="$(kubectl get configmap nb-cleanup -n "$NAMESPACE" -o jsonpath='{.data.nb1} {.data.nb2}' 2>/dev/null || true)" + fi + for nbid in $nbids; do + if [[ "$nbid" =~ ^[0-9]+$ ]]; then + echo "Deleting NodeBalancer $nbid" + curl -s -o /dev/null -X DELETE \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + "$LINODE_URL/v4/nodebalancers/$nbid" || true + fi + done steps: - name: Create pods and services try: @@ -65,6 +84,11 @@ spec: if ! [[ $nbid =~ $re ]]; then echo "Nodebalancer id [$nbid] for label [$lABEL] is incorrect, failed to create nodebalancer" else + # Record the NodeBalancer id so the test-level catch can delete it + # if a later step fails before the Service adopts it (chainsaw does + # not track NodeBalancers created via the API). + kubectl create configmap nb-cleanup -n "$NAMESPACE" --from-literal=nb1="$nbid" --dry-run=client -o yaml | kubectl apply -f - >/dev/null 2>&1 || true + kubectl annotate svc svc-test -n $NAMESPACE service.beta.kubernetes.io/linode-loadbalancer-nodebalancer-id=$nbid sleep 5 diff --git a/e2e/test/lb-with-ipv6-backends/chainsaw-test.yaml b/e2e/test/lb-with-ipv6-backends/chainsaw-test.yaml index 071d1b3b..4454d45e 100644 --- a/e2e/test/lb-with-ipv6-backends/chainsaw-test.yaml +++ b/e2e/test/lb-with-ipv6-backends/chainsaw-test.yaml @@ -18,7 +18,6 @@ spec: kubectl logs -n kube-system daemonsets/ccm-linode | grep "lb-with-ipv6-backends" | tail -100 steps: - name: Create pods and service - skipDelete: true try: - apply: file: create-pods-services.yaml @@ -178,15 +177,3 @@ spec: check: ($error == null): true (contains($stdout, 'all pods responded')): true - - name: Delete Pods - try: - - delete: - ref: - apiVersion: v1 - kind: Pod - - name: Delete Service - try: - - delete: - ref: - apiVersion: v1 - kind: Service diff --git a/renovate.json5 b/renovate.json5 index ffa96e98..e2d64c41 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -11,7 +11,8 @@ "labels": ["dependencies"], "postUpdateOptions": ["gomodTidy"], "vulnerabilityAlerts": { - "enabled": true + "enabled": true, + "groupName": "security updates" }, "osvVulnerabilityAlerts": true, "platformAutomerge": true,