Skip to content

fix: derive kubelet clusterDNS for a custom serviceSubnet (C2)#23

Merged
wrkode merged 1 commit into
mainfrom
fix/clusterdns-custom-servicesubnet
Jun 30, 2026
Merged

fix: derive kubelet clusterDNS for a custom serviceSubnet (C2)#23
wrkode merged 1 commit into
mainfrom
fix/clusterdns-custom-servicesubnet

Conversation

@wrkode

@wrkode wrkode commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

What

When serviceSubnet is non-default, the kubelet clusterDNS must be the DNS
service IP of that CIDR (the 10th host IP), not the default 10.96.0.10. The
provider previously relied on kubeadm's implicit derivation; this makes the
provider own the value, correct-by-construction and hardware-free testable.

Closes kairos-io/kairos#4201 (pitfall C2).

How (ADR-17, option B)

  • DeriveDNSIP(serviceSubnet) (internal/kubeadmconfig/dns.go): pure function
    returning the 10th host IP of the service CIDR, matching kubeadm's GetDNSIP.
    Uses the primary (first) CIDR for a dual-stack value; big-endian byte
    addition so IPv4/IPv6 and non-zero-aligned bases are correct (e.g.
    10.96.0.64/26 -> 10.96.0.74); fails fast on empty / malformed / too-small /
    empty-secondary CIDRs rather than guessing.
  • KubeletConfiguration type + BuildKubeletConfiguration: emits clusterDNS
    only when the operator set a custom serviceSubnet; an empty subnet emits no
    document, so the default path is byte-for-byte unchanged (kubeadm derives it).
  • Emitted in the init flow and in the ADR-12-R1 kubelet-start repair (both
    regenerate /var/lib/kubelet/config.yaml); the join path omits it, since
    workers inherit clusterDNS from the kubelet-config ConfigMap kubeadm uploads at
    init.

Tests

  • Full DeriveDNSIP matrix: default/custom IPv4, IPv6, dual-stack primary,
    non-zero-aligned bases, /28 boundary, and all error cases (empty, garbage,
    too-small /29 & /125, three CIDRs, empty primary/secondary).
  • Builder test (custom -> derived single-element clusterDNS; empty -> none).
  • Action behavior tests: init emits the derived clusterDNS for a custom subnet
    and omits the document for the default; join never emits it.

go build / go test ./... / vet / golangci-lint all green. Credential-free,
so no security-architect gate.

Review

staff-engineer code-reviewed this and ran a differential test reimplementing
kubeadm's GetDNSIP/GetIndexedIP from k8s.io/utils/net against DeriveDNSIP
across the matrix + 9 extra edge cases — byte-for-byte parity on value and error.
Both should-fix items (repair-path consistency; reject empty-secondary CIDR) were
actioned before this PR.

…1, C2)

When serviceSubnet is non-default, the kubelet clusterDNS must be the DNS
service IP of that CIDR, not the default 10.96.0.10. The provider previously
relied on kubeadm's implicit derivation; this makes it correct-by-construction
and unit-testable (ADR-17).

- DeriveDNSIP(serviceSubnet): pure function returning the 10th host IP of the
  service CIDR, matching kubeadm's GetDNSIP. Uses the primary (first) CIDR for a
  dual-stack value, big-endian byte addition so IPv4/IPv6 and non-zero-aligned
  bases are correct, and fails fast on empty/malformed/too-small/empty-secondary
  CIDRs rather than guessing.
- KubeletConfiguration type + BuildKubeletConfiguration: emit clusterDNS only
  when the operator set a custom serviceSubnet; empty subnet emits no document so
  the default path is byte-for-byte unchanged (kubeadm derives it).
- Emitted in the init flow and in the ADR-12-R1 kubelet-start repair (both
  regenerate /var/lib/kubelet/config.yaml); the join path omits it, as workers
  inherit clusterDNS from the kubelet-config ConfigMap uploaded at init.

Tests: full DeriveDNSIP matrix (default/custom IPv4, IPv6, dual-stack primary,
non-zero-aligned bases, /28 boundary, and error cases), a builder test, an action
behavior test (init emits the derived clusterDNS for a custom subnet, omits it for
the default), and a join-omits regression test. Credential-free; no security gate.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: William Rizzo <william.rizzo@gmail.com>

@wrkode wrkode left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LTGM

@wrkode
wrkode merged commit 2bc0e9d into main Jun 30, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

provider-kubernetes: derive clusterDNS correctly for a custom serviceSubnet (C2)

1 participant