Feature: configurable IP family (dual-stack / IPv6-only) for the Tenant Control Plane Service
Summary
Allow TenantControlPlane.spec.controlPlane.service to configure the IP family of the
generated control-plane Service, so a Tenant Control Plane can be exposed as dual-stack,
IPv4-only, or IPv6-only. Today the exposing Service always inherits the management
cluster's default IP-family behaviour, with no per-TCP override.
Motivation
On dual-stack or IPv6-only management clusters there is currently no way to control how
the Tenant Control Plane endpoint Service is exposed. Operators who want an IPv6-only
control-plane endpoint, or an explicit dual-stack one with a chosen primary family, have
no knob for it. The tenant-internal networking already supports dual-stack via
networkProfile.serviceCidrs / podCidrs, but the exposing Service does not.
Current behaviour
internal/resources/k8s_service_resource.go builds the control-plane Service but never
sets spec.ipFamilies or spec.ipFamilyPolicy. The Service therefore always falls back
to the management cluster default (typically SingleStack).
The Konnectivity Service (internal/resources/konnectivity/service_resource.go) is not a
separate Service — it only appends a konnectivity-server port to the same Service named
after the TCP, so it is covered automatically.
CoreDNS (internal/resources/addons/coredns.go) already derives its IP families from
networkProfile.dnsServiceIPs (defaulted from serviceCidrs) and correctly handles both
dual-stack and IPv6-only; it is intentionally not part of this request.
Proposed change
Add two optional fields to ServiceSpec, mirroring the native Kubernetes Service fields
1:1 (pure passthrough):
spec:
controlPlane:
service:
serviceType: LoadBalancer
# IPv6-only:
ipFamilyPolicy: SingleStack
ipFamilies: [IPv6]
# or dual-stack, IPv6 primary:
# ipFamilyPolicy: RequireDualStack
# ipFamilies: [IPv6, IPv4]
ipFamilyPolicy (*corev1.IPFamilyPolicy, enum SingleStack|PreferDualStack|RequireDualStack)
ipFamilies ([]corev1.IPFamily, enum IPv4|IPv6, max 2 items)
When both are unset, behaviour is unchanged (the management cluster default applies).
Validation
Cross-field CEL on TenantControlPlaneSpec (consistent with the existing
allocateLoadBalancerNodePorts validation):
ipFamilyPolicy: SingleStack allows at most one entry in ipFamilies.
ipFamilies entries must be unique.
No immutability constraint, so the Kubernetes-supported single-stack -> dual-stack upgrade
keeps working; API-server-forbidden transitions (dual -> single, v4 <-> v6) surface as a
reconcile error rather than being blocked at admission.
Alternatives considered
- A single high-level enum (e.g.
ipFamily: SingleStackIPv6|DualStack): simpler, but
cannot express PreferDualStack vs RequireDualStack or family ordering. Rejected in
favour of faithful, familiar Kubernetes semantics.
- Delete/recreate the Service to force forbidden IP-family transitions: rejected as an
implicit behaviour because it churns the control-plane endpoint IP, can drop a
LoadBalancer IP, breaks Konnectivity and cached kubeconfigs/certs. Could be a separate
explicit opt-in feature if ever needed.
Scope
- In scope: control-plane Service (and Konnectivity, implicitly).
- Out of scope: CoreDNS changes,
RequireDualStack for CoreDNS, delete/recreate flows.
I'm happy to open a PR for this.
Feature: configurable IP family (dual-stack / IPv6-only) for the Tenant Control Plane Service
Summary
Allow
TenantControlPlane.spec.controlPlane.serviceto configure the IP family of thegenerated control-plane Service, so a Tenant Control Plane can be exposed as dual-stack,
IPv4-only, or IPv6-only. Today the exposing Service always inherits the management
cluster's default IP-family behaviour, with no per-TCP override.
Motivation
On dual-stack or IPv6-only management clusters there is currently no way to control how
the Tenant Control Plane endpoint Service is exposed. Operators who want an IPv6-only
control-plane endpoint, or an explicit dual-stack one with a chosen primary family, have
no knob for it. The tenant-internal networking already supports dual-stack via
networkProfile.serviceCidrs/podCidrs, but the exposing Service does not.Current behaviour
internal/resources/k8s_service_resource.gobuilds the control-plane Service but neversets
spec.ipFamiliesorspec.ipFamilyPolicy. The Service therefore always falls backto the management cluster default (typically SingleStack).
The Konnectivity Service (
internal/resources/konnectivity/service_resource.go) is not aseparate Service — it only appends a
konnectivity-serverport to the same Service namedafter the TCP, so it is covered automatically.
CoreDNS (
internal/resources/addons/coredns.go) already derives its IP families fromnetworkProfile.dnsServiceIPs(defaulted fromserviceCidrs) and correctly handles bothdual-stack and IPv6-only; it is intentionally not part of this request.
Proposed change
Add two optional fields to
ServiceSpec, mirroring the native Kubernetes Service fields1:1 (pure passthrough):
ipFamilyPolicy(*corev1.IPFamilyPolicy, enumSingleStack|PreferDualStack|RequireDualStack)ipFamilies([]corev1.IPFamily, enumIPv4|IPv6, max 2 items)When both are unset, behaviour is unchanged (the management cluster default applies).
Validation
Cross-field CEL on
TenantControlPlaneSpec(consistent with the existingallocateLoadBalancerNodePortsvalidation):ipFamilyPolicy: SingleStackallows at most one entry inipFamilies.ipFamiliesentries must be unique.No immutability constraint, so the Kubernetes-supported single-stack -> dual-stack upgrade
keeps working; API-server-forbidden transitions (dual -> single, v4 <-> v6) surface as a
reconcile error rather than being blocked at admission.
Alternatives considered
ipFamily: SingleStackIPv6|DualStack): simpler, butcannot express
PreferDualStackvsRequireDualStackor family ordering. Rejected infavour of faithful, familiar Kubernetes semantics.
implicit behaviour because it churns the control-plane endpoint IP, can drop a
LoadBalancer IP, breaks Konnectivity and cached kubeconfigs/certs. Could be a separate
explicit opt-in feature if ever needed.
Scope
RequireDualStackfor CoreDNS, delete/recreate flows.I'm happy to open a PR for this.