PR: kubernetes/kubernetes#135896 Branch:
add-constants-moduleat/Users/dsrinivas/go/src/k8s.io/kubernetes-pr135896Cross-checked: all factual claims below verified against the actual branch.
For the first time, there is a k8s.io/* module that sits below k8s.io/apimachinery and k8s.io/api in the dependency graph — and both of those modules now depend on it. The old order was "everything flows from apimachinery"; the new order is "everything flows from constants."
The problem: external tools that need a single constant — DNS1123SubdomainMaxLength, LabelTopologyZone, NodeNotReady — must today import k8s.io/apimachinery or k8s.io/api/core/v1. This pulls in klog, kube-openapi, protobuf, CBOR, json-iterator, structured-merge-diff, and 26+ other modules.
Measured impact (from binary-size test harness):
| To get one constant, import... | Transitive pkgs | go.sum entries |
Binary delta |
|---|---|---|---|
k8s.io/constants/{labels,rfc,...} |
64 | 0 | +496 KB |
k8s.io/apimachinery/pkg/util/validation |
107 | 6 | +1.04 MB |
k8s.io/api/core/v1 |
272 | 41 | +7.26 MB |
That's a 14.6× binary size reduction and zero supply-chain surface (go.sum is empty because k8s.io/constants has no dependencies at all).
Who benefits immediately: admission webhooks, kubectl plugins, CLI tools, Karpenter/cluster-autoscaler/cluster-api node selectors, schema validators, policy engines (Kyverno, Gatekeeper, OPA helpers), documentation generators.
Verified: cat staging/src/k8s.io/constants/go.mod — no require block at all.
The problem (master today):
// staging/src/k8s.io/api/resource/v1/types.go:25
import "k8s.io/apimachinery/pkg/util/validation"
const PoolNameMaxLength = validation.DNS1123SubdomainMaxLengthSame in v1beta1 and v1beta2. One integer forces k8s.io/api/resource to depend on util/validation, which pulls in the regex engine, unicode, apimachinery/pkg/util/validation/field, pkg/api/validate/content, klog, and more.
After the PR:
import "k8s.io/constants/rfc"
const PoolNameMaxLength = rfc.DNS1123SubdomainMaxLengthk8s.io/api/resource no longer imports util/validation at all — confirmed (grep returns empty on the PR branch).
Impact on consumers of the DRA API: any project using k8s.io/api/resource/v1 gets a narrower transitive graph for free — no code changes needed.
The problem (master today): DNS1123SubdomainMaxLength = 253 and DNS1123LabelMaxLength = 63 are each declared independently in two places:
apimachinery/pkg/util/validation/validation.go:191,161apimachinery/pkg/api/validate/content/dns.go:57,28
Two independent integer literals that must stay identical. Nobody enforces this.
After the PR: both locations become:
import "k8s.io/constants/rfc"
const DNS1123SubdomainMaxLength int = rfc.DNS1123SubdomainMaxLength // re-exportThere is now one definition (rfc/dns.go:42) and everything else is a compile-time alias. Drift is structurally impossible.
Also fixed: FieldManagerMaxLength on master is a var (mutable at runtime), not a const. The PR pins it to limits.FieldManagerMaxLength (a proper const).
Also surfaced: labelKeyMaxLength in content/kube.go:28 was unexported — callers had no way to reference it and hard-coded 63. The PR exports it as limits.LabelKeyMaxLength.
Every legacy name is preserved as a compile-time alias:
// api/core/v1/well_known_labels.go (PR branch)
import "k8s.io/constants/labels"
const (
LabelHostname = labels.Hostname // was "kubernetes.io/hostname"
LabelTopologyZone = labels.TopologyZone // was "topology.kubernetes.io/zone"
LabelTopologyRegion = labels.TopologyRegion
...
)Same for well_known_taints.go (TaintNodeNotReady = taints.NodeNotReady) and apimachinery/pkg/util/validation/validation.go (all three DNS constants). Existing code compiles without changes. Migration is opt-in, one import at a time.
Verified: annotation_key_constants.go is byte-identical between master and PR branch — deliberately not touched. The compat surface is preserved.
Before: deprecation of a label/annotation was expressed inconsistently — sometimes a // deprecated line comment, sometimes a godoc Deprecated: line, sometimes nothing.
After: the constants module enforces two conventions throughout:
- Naming: deprecated constants have a
Deprecatedprefix (DeprecatedFailureDomainBetaZone,DeprecatedSeccompPod,DeprecatedAppArmorBetaContainerPrefix). IDEs andstaticchecksurface this automatically. - Godoc: every deprecated entry has
// Deprecated: use X instead.pointing at its replacement. - Alpha: alpha constants document their feature gate:
"This is an alpha annotation and requires enabling the PodDeletionCost feature gate."
Impact: one authoritative place to audit "what labels/annotations are deprecated in this Kubernetes version." Today the answer is distributed across 12 files.
Today, a new contributor (or an AI assistant) looking for "the Go constant for the topology zone label" must know to look in k8s.io/api/core/v1/well_known_labels.go. There is no canonical index.
After this PR: k8s.io/constants/{labels,annotations,taints} is the answer. When published as github.com/kubernetes/constants, it gets its own pkg.go.dev page organized by vocabulary category — the first hit for "Kubernetes well-known labels" in any Go package search.
The magic-string problem in-tree: there are currently ~217 literal occurrences of "kubernetes.io/hostname" in the tree even though v1.LabelHostname exists — the weight of importing k8s.io/api/core/v1 made using the constant feel expensive in many contexts (tests, kubeadm, integration). A zero-dep import removes that excuse entirely and makes lint rules enforceable.
These become straightforward after this lands:
| Follow-up | What it does | Blocker today |
|---|---|---|
Convert annotation_key_constants.go (both internal + external copies) to re-exports |
Eliminates ~300 lines of near-duplicated code + hand-maintained consistency comment | No destination to point at |
Migrate apimachinery/pkg/util/managedfields/internal/lastapplied.go:31 |
Third private copy of LastAppliedConfigAnnotation |
Same |
Migrate 9 remaining well_known_*.go files (discovery, networking, cloud-provider, kubelet) |
Consolidates all 12 well-known files into one module | No destination |
Lint rule: forbid literal strings that appear in k8s.io/constants |
Makes magic strings a CI failure | No import path light enough to mandate everywhere |
Simplify util/validation to pure functions |
Strip the constant declarations; the file becomes validation logic only | Two sets of compat re-exports are now established by the PR |
k8s.io/types module (for UID, IPFamily, ResourceName) |
Uses same pattern and same zero-dep principle | No established template |
Kubernetes is migrating from imperative Go validation to CEL expressions generated by validation-gen. CEL expressions that check length (e.g., self.size() <= 253) need to reference the same integer as Go code.
The constants/rfc module is already consumed by apimachinery/pkg/api/validate/content/dns.go (the CEL-validation-gen building block). As CEL generators template length constants into expressions, having one rfc.DNS1123SubdomainMaxLength means both the Go validator and the generated CEL expression track the same definition — drift is impossible.
The zero-dep property matters here: CEL environments embedded in CRD validation and admission webhooks are sensitive to import bloat. A pure-constant import is the cleanest integration.
- Scope is narrow today: only
core/v1labels/annotations/taints + three DNS integers. The other 9well_known_*.gofiles (discovery,networking,cloud-provider,kubelet) are untouched. annotation_key_constants.gonot migrated: 14 annotation constants incore/v1still have literal string definitions, not re-exports. Second-phase work.- No explicit stability policy document: the module exists; the written per-constant stability contract is not yet in
README.mdorSTABILITY.md. - Standalone repo question open: liggitt and thockin raised whether a staging module risks
k/k → external → k/k-stagingdependency loops. A standalonegithub.com/kubernetes/constantsrepo would avoid this but adds release overhead. Still under discussion. rfc/naming andDNS1035LabelMaxLength: jpbetz noted kube-openapi#384 usesk8s-short-name/k8s-long-name— identifier alignment pending sign-off. thockin wantsrfc/dropped from scope pending DV maturity. These may narrow the module's initial footprint.
| # | Benefit | Audience | Evidence |
|---|---|---|---|
| 1 | 14.6× binary size reduction; zero transitive deps | External consumers | Binary test harness; go.mod has no require block |
| 2 | api/resource/v* drops util/validation entirely |
DRA API consumers | grep returns empty on PR branch |
| 3 | DNS/size constant duplication eliminated | Maintainers | Was 2 independent = 253; now both are = rfc.* |
| 4 | Zero breaking changes; all legacy names preserved | Everyone | annotation_key_constants.go unchanged; LHS aliases intact |
| 5 | Uniform deprecation/alpha contract | Maintainers, IDEs, linters | Deprecated* naming + godoc throughout |
| 6 | One searchable canonical home for K8s vocabulary | Contributors, tooling, AI | 217 magic-string occurrences of "kubernetes.io/hostname" in-tree |
| 7 | Unlocks 8+ follow-on cleanups | Maintainers | annotation_key_constants, well_known networking/cloud-provider, lint rules |
| 8 | CEL/DV validation stays consistent with Go constants | Maintainers, API machinery | content/dns.go already consumes constants/rfc |