Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)

Expand Down
24 changes: 20 additions & 4 deletions e2e/bgp-test/lb-cilium-bgp/chainsaw-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
24 changes: 22 additions & 2 deletions e2e/test/fw-use-specified-nb/chainsaw-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
33 changes: 31 additions & 2 deletions e2e/test/lb-created-with-new-nb-id/chainsaw-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
13 changes: 13 additions & 0 deletions e2e/test/lb-created-with-reserved-ip-change-ip/chainsaw-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
13 changes: 13 additions & 0 deletions e2e/test/lb-created-with-reserved-ip-nb-range/chainsaw-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading
Loading