diff --git a/cloud/annotations/annotations.go b/cloud/annotations/annotations.go index ecbf96dc..2de0674d 100644 --- a/cloud/annotations/annotations.go +++ b/cloud/annotations/annotations.go @@ -30,6 +30,10 @@ const ( AnnLinodeCloudFirewallID = "service.beta.kubernetes.io/linode-loadbalancer-firewall-id" AnnLinodeCloudFirewallACL = "service.beta.kubernetes.io/linode-loadbalancer-firewall-acl" + // AnnLinodeEnableIPv6Ingress is the annotation used to specify that a service should include both IPv4 and IPv6 + // addresses for its LoadBalancer ingress. When set to "true", both addresses will be included in the status. + AnnLinodeEnableIPv6Ingress = "service.beta.kubernetes.io/linode-loadbalancer-enable-ipv6-ingress" + AnnLinodeNodePrivateIP = "node.k8s.linode.com/private-ip" AnnLinodeHostUUID = "node.k8s.linode.com/host-uuid" diff --git a/cloud/linode/cloud.go b/cloud/linode/cloud.go index 8d4f2798..8190e258 100644 --- a/cloud/linode/cloud.go +++ b/cloud/linode/cloud.go @@ -39,16 +39,17 @@ var Options struct { EnableRouteController bool EnableTokenHealthChecker bool // Deprecated: use VPCNames instead - VPCName string - VPCNames string - SubnetNames string - LoadBalancerType string - BGPNodeSelector string - IpHolderSuffix string - LinodeExternalNetwork *net.IPNet - NodeBalancerTags []string - DefaultNBType string - GlobalStopChannel chan<- struct{} + VPCName string + VPCNames string + SubnetNames string + LoadBalancerType string + BGPNodeSelector string + IpHolderSuffix string + LinodeExternalNetwork *net.IPNet + NodeBalancerTags []string + DefaultNBType string + GlobalStopChannel chan<- struct{} + EnableIPv6ForLoadBalancers bool } type linodeCloud struct { diff --git a/cloud/linode/loadbalancers.go b/cloud/linode/loadbalancers.go index b3f67023..ded3cb41 100644 --- a/cloud/linode/loadbalancers.go +++ b/cloud/linode/loadbalancers.go @@ -1052,13 +1052,44 @@ func makeLoadBalancerStatus(service *v1.Service, nb *linodego.NodeBalancer) *v1. ingress := v1.LoadBalancerIngress{ Hostname: *nb.Hostname, } - if !getServiceBoolAnnotation(service, annotations.AnnLinodeHostnameOnlyIngress) { - if val := envBoolOptions("LINODE_HOSTNAME_ONLY_INGRESS"); val { - klog.Infof("LINODE_HOSTNAME_ONLY_INGRESS: (%v)", val) - } else { - ingress.IP = *nb.IPv4 + + // Return hostname-only if annotation is set or environment variable is set + if getServiceBoolAnnotation(service, annotations.AnnLinodeHostnameOnlyIngress) { + return &v1.LoadBalancerStatus{ + Ingress: []v1.LoadBalancerIngress{ingress}, + } + } + + if val := envBoolOptions("LINODE_HOSTNAME_ONLY_INGRESS"); val { + klog.Infof("LINODE_HOSTNAME_ONLY_INGRESS: (%v)", val) + return &v1.LoadBalancerStatus{ + Ingress: []v1.LoadBalancerIngress{ingress}, + } + } + + // Check for per-service IPv6 annotation first, then fall back to global setting + useIPv6 := getServiceBoolAnnotation(service, annotations.AnnLinodeEnableIPv6Ingress) || Options.EnableIPv6ForLoadBalancers + + // When IPv6 is enabled (either per-service or globally), include both IPv4 and IPv6 + if useIPv6 && nb.IPv6 != nil && *nb.IPv6 != "" { + ingresses := []v1.LoadBalancerIngress{ + { + Hostname: *nb.Hostname, + IP: *nb.IPv4, + }, + { + Hostname: *nb.Hostname, + IP: *nb.IPv6, + }, + } + klog.V(4).Infof("Using both IPv4 and IPv6 addresses for NodeBalancer (%d): %s, %s", nb.ID, *nb.IPv4, *nb.IPv6) + return &v1.LoadBalancerStatus{ + Ingress: ingresses, } } + + // Default case - just use IPv4 + ingress.IP = *nb.IPv4 return &v1.LoadBalancerStatus{ Ingress: []v1.LoadBalancerIngress{ingress}, } diff --git a/cloud/linode/loadbalancers_test.go b/cloud/linode/loadbalancers_test.go index 60261b36..e986ab6c 100644 --- a/cloud/linode/loadbalancers_test.go +++ b/cloud/linode/loadbalancers_test.go @@ -256,6 +256,10 @@ func TestCCMLoadBalancers(t *testing.T) { name: "makeLoadBalancerStatus", f: testMakeLoadBalancerStatus, }, + { + name: "makeLoadBalancerStatusWithIPv6", + f: testMakeLoadBalancerStatusWithIPv6, + }, { name: "makeLoadBalancerStatusEnvVar", f: testMakeLoadBalancerStatusEnvVar, @@ -3191,6 +3195,71 @@ func testMakeLoadBalancerStatus(t *testing.T, client *linodego.Client, _ *fakeAP } } +func testMakeLoadBalancerStatusWithIPv6(t *testing.T, client *linodego.Client, _ *fakeAPI) { + ipv4 := "192.168.0.1" + ipv6 := "2600:3c00::f03c:91ff:fe24:3a2f" + hostname := "nb-192-168-0-1.newark.nodebalancer.linode.com" + nb := &linodego.NodeBalancer{ + IPv4: &ipv4, + IPv6: &ipv6, + Hostname: &hostname, + } + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Annotations: make(map[string]string, 1), + }, + } + + // Test with EnableIPv6ForLoadBalancers = false (default) + Options.EnableIPv6ForLoadBalancers = false + expectedStatus := &v1.LoadBalancerStatus{ + Ingress: []v1.LoadBalancerIngress{{ + Hostname: hostname, + IP: ipv4, + }}, + } + status := makeLoadBalancerStatus(svc, nb) + if !reflect.DeepEqual(status, expectedStatus) { + t.Errorf("expected status with EnableIPv6ForLoadBalancers=false to be %#v; got %#v", expectedStatus, status) + } + + // Test with EnableIPv6ForLoadBalancers = true + Options.EnableIPv6ForLoadBalancers = true + expectedStatus = &v1.LoadBalancerStatus{ + Ingress: []v1.LoadBalancerIngress{ + { + Hostname: hostname, + IP: ipv4, + }, + { + Hostname: hostname, + IP: ipv6, + }, + }, + } + status = makeLoadBalancerStatus(svc, nb) + if !reflect.DeepEqual(status, expectedStatus) { + t.Errorf("expected status with EnableIPv6ForLoadBalancers=true to be %#v; got %#v", expectedStatus, status) + } + + // Test with per-service annotation + // Reset the global flag to false and set the annotation + Options.EnableIPv6ForLoadBalancers = false + svc.Annotations[annotations.AnnLinodeEnableIPv6Ingress] = "true" + + // Expect the same result as when the global flag is enabled + status = makeLoadBalancerStatus(svc, nb) + if !reflect.DeepEqual(status, expectedStatus) { + t.Errorf("expected status with %s=true annotation to be %#v; got %#v", + annotations.AnnLinodeEnableIPv6Ingress, expectedStatus, status) + } + + // Reset the flag to its default value + Options.EnableIPv6ForLoadBalancers = false +} + func testMakeLoadBalancerStatusEnvVar(t *testing.T, client *linodego.Client, _ *fakeAPI) { ipv4 := "192.168.0.1" hostname := "nb-192-168-0-1.newark.nodebalancer.linode.com" diff --git a/deploy/chart/templates/daemonset.yaml b/deploy/chart/templates/daemonset.yaml index a9d984eb..590d6a18 100644 --- a/deploy/chart/templates/daemonset.yaml +++ b/deploy/chart/templates/daemonset.yaml @@ -100,6 +100,9 @@ spec: {{- if .Values.defaultNBType }} - --default-nodebalancer-type={{ .Values.defaultNBType }} {{- end }} + {{- if .Values.enableIPv6ForLoadBalancers }} + - --enable-ipv6-for-loadbalancers={{ .Values.enableIPv6ForLoadBalancers }} + {{- end }} {{- with .Values.containerSecurityContext }} securityContext: {{- toYaml . | nindent 12 }} diff --git a/deploy/chart/values.yaml b/deploy/chart/values.yaml index d44fb880..5be0f47e 100644 --- a/deploy/chart/values.yaml +++ b/deploy/chart/values.yaml @@ -86,6 +86,11 @@ tolerations: # Default NodeBalancer type to create("common" or "premium"). Default is "common" # defaultNBType: "common" +# Enable IPv6 ingress addresses for LoadBalancer services +# When enabled, both IPv4 and IPv6 addresses will be included in the LoadBalancer status for all services +# This can also be controlled per-service using the "service.beta.kubernetes.io/linode-loadbalancer-enable-ipv6-ingress" annotation +# enableIPv6ForLoadBalancers: true + # This section adds the ability to pass environment variables to adjust CCM defaults # https://github.com/linode/linode-cloud-controller-manager/blob/master/cloud/linode/loadbalancers.go # LINODE_HOSTNAME_ONLY_INGRESS type bool is supported diff --git a/docs/configuration/README.md b/docs/configuration/README.md index 9d8f2531..b78a2bd3 100644 --- a/docs/configuration/README.md +++ b/docs/configuration/README.md @@ -28,12 +28,13 @@ The Linode Cloud Controller Manager (CCM) offers extensive configuration options - Node controller behavior - [See node management](nodes.md#node-controller-behavior) -4. **[Environment Variables](environment.md)** +4. **[Environment Variables and Flags](environment.md)** - Cache settings - API configuration - Network settings - BGP configuration - - [See environment reference](environment.md#available-variables) + - IPv6 configuration + - [See configuration reference](environment.md#flags) 5. **[Firewall Setup](firewall.md)** - CCM-managed firewalls diff --git a/docs/configuration/annotations.md b/docs/configuration/annotations.md index 9e3127ae..fb20b71c 100644 --- a/docs/configuration/annotations.md +++ b/docs/configuration/annotations.md @@ -33,6 +33,7 @@ For implementation details, see: | `firewall-id` | string | | An existing Cloud Firewall ID to be attached to the NodeBalancer instance. See [Firewall Setup](firewall.md) | | `firewall-acl` | string | | The Firewall rules to be applied to the NodeBalancer. See [Firewall Configuration](#firewall-configuration) | | `nodebalancer-type` | string | | The type of NodeBalancer to create (options: common, premium). See [NodeBalancer Types](#nodebalancer-type) | +| `enable-ipv6-ingress` | bool | `false` | When `true`, both IPv4 and IPv6 addresses will be included in the LoadBalancerStatus ingress | | `backend-ipv4-range` | string | | The IPv4 range from VPC subnet to be applied to the NodeBalancer backend. See [Nodebalancer VPC Configuration](#nodebalancer-vpc-configuration) | | `backend-vpc-name` | string | | VPC which is connected to the NodeBalancer backend. See [Nodebalancer VPC Configuration](#nodebalancer-vpc-configuration) | | `backend-subnet-name` | string | | Subnet within VPC which is connected to the NodeBalancer backend. See [Nodebalancer VPC Configuration](#nodebalancer-vpc-configuration) | @@ -124,6 +125,13 @@ metadata: service.beta.kubernetes.io/linode-loadbalancer-subnet-name: "subnet1" ``` +### Service with IPv6 Address +```yaml +metadata: + annotations: + service.beta.kubernetes.io/linode-loadbalancer-enable-ipv6-ingress: "true" +``` + For more examples and detailed configuration options, see: - [LoadBalancer Configuration](loadbalancer.md) - [Firewall Configuration](firewall.md) diff --git a/docs/configuration/environment.md b/docs/configuration/environment.md index 15ad47f0..c4a891ed 100644 --- a/docs/configuration/environment.md +++ b/docs/configuration/environment.md @@ -1,10 +1,10 @@ -# Environment Variables +# Environment Variables and Flags ## Overview -Environment variables provide global configuration options for the CCM. These settings affect caching, API behavior, and networking configurations. +The CCM can be configured using environment variables and flags. Environment variables provide global configuration options, while flags control specific features. -## Available Variables +## Environment Variables ### Cache Configuration @@ -28,6 +28,24 @@ Environment variables provide global configuration options for the CCM. These se | `BGP_CUSTOM_ID_MAP` | "" | Use your own map instead of default region map for BGP | | `BGP_PEER_PREFIX` | `2600:3c0f` | Use your own BGP peer prefix instead of default one | +## Flags + +The CCM supports the following flags: + +| Flag | Default | Description | +|------|---------|-------------| +| `--linodego-debug` | `false` | Enables debug output for the LinodeAPI wrapper | +| `--enable-route-controller` | `false` | Enables route_controller for CCM | +| `--enable-token-health-checker` | `false` | Enables Linode API token health checker | +| `--vpc-names` | `""` | Comma separated VPC names whose routes will be managed by route-controller | +| `--subnet-names` | `""` | Comma separated subnet names whose routes will be managed by route-controller (requires vpc-names flag) | +| `--load-balancer-type` | `nodebalancer` | Configures which type of load-balancing to use (options: nodebalancer, cilium-bgp) | +| `--bgp-node-selector` | `""` | Node selector to use to perform shared IP fail-over with BGP | +| `--ip-holder-suffix` | `""` | Suffix to append to the IP holder name when using shared IP fail-over with BGP | +| `--default-nodebalancer-type` | `common` | Default type of NodeBalancer to create (options: common, premium) | +| `--nodebalancer-tags` | `[]` | Linode tags to apply to all NodeBalancers | +| `--enable-ipv6-for-loadbalancers` | `false` | Set both IPv4 and IPv6 addresses for all LoadBalancer services (when disabled, only IPv4 is used). This can also be configured per-service using the `service.beta.kubernetes.io/linode-loadbalancer-enable-ipv6-ingress` annotation. | + ## Configuration Methods ### Helm Chart @@ -36,6 +54,9 @@ Configure via `values.yaml`: env: - name: LINODE_INSTANCE_CACHE_TTL value: "30" +args: + - --enable-ipv6-for-loadbalancers + - --enable-route-controller ``` ### Manual Deployment @@ -49,6 +70,9 @@ spec: env: - name: LINODE_INSTANCE_CACHE_TTL value: "30" + args: + - --enable-ipv6-for-loadbalancers + - --enable-route-controller ``` ## Usage Guidelines diff --git a/docs/configuration/loadbalancer.md b/docs/configuration/loadbalancer.md index 952b2081..e6b8846d 100644 --- a/docs/configuration/loadbalancer.md +++ b/docs/configuration/loadbalancer.md @@ -18,6 +18,32 @@ When using NodeBalancers, the CCM automatically: For more details, see [Linode NodeBalancer Documentation](https://www.linode.com/docs/products/networking/nodebalancers/). +### IPv6 Support + +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: + +```yaml +spec: + template: + spec: + containers: + - name: ccm-linode + args: + - --enable-ipv6-for-loadbalancers=true +``` + +Alternatively, you can enable IPv6 addresses for individual services using the annotation: + +```yaml +metadata: + annotations: + service.beta.kubernetes.io/linode-loadbalancer-enable-ipv6-ingress: "true" +``` + +When IPv6 is enabled (either globally or per-service), both IPv4 and IPv6 addresses will be included in the service's LoadBalancer status. + ### Basic Configuration Create a LoadBalancer service: @@ -219,7 +245,7 @@ metadata: - [Service Annotations](annotations.md) - [Firewall Configuration](firewall.md) - [Session Affinity](session-affinity.md) -- [Environment Variables](environment.md) +- [Environment Variables and Flags](environment.md) - [Route Configuration](routes.md) - [Linode NodeBalancer Documentation](https://www.linode.com/docs/products/networking/nodebalancers/) - [Cilium BGP Documentation](https://docs.cilium.io/en/stable/network/bgp-control-plane/bgp-control-plane/) diff --git a/main.go b/main.go index 10d4aa5b..e8f7006a 100644 --- a/main.go +++ b/main.go @@ -91,6 +91,7 @@ func main() { command.Flags().StringVar(&linode.Options.IpHolderSuffix, "ip-holder-suffix", "", "suffix to append to the ip holder name when using shared IP fail-over with BGP (e.g. ip-holder-suffix=my-cluster-name") command.Flags().StringVar(&linode.Options.DefaultNBType, "default-nodebalancer-type", string(linodego.NBTypeCommon), "default type of NodeBalancer to create (options: common, premium)") command.Flags().StringSliceVar(&linode.Options.NodeBalancerTags, "nodebalancer-tags", []string{}, "Linode tags to apply to all NodeBalancers") + command.Flags().BoolVar(&linode.Options.EnableIPv6ForLoadBalancers, "enable-ipv6-for-loadbalancers", false, "set both IPv4 and IPv6 addresses for all LoadBalancer services (when disabled, only IPv4 is used)") // Set static flags command.Flags().VisitAll(func(fl *pflag.Flag) {