Skip to content

feat(podspec): find PodSpecs with a CUE schema (option C) - #83

Open
MPV wants to merge 3 commits into
masterfrom
claude/podspec-c-cue-schema
Open

feat(podspec): find PodSpecs with a CUE schema (option C)#83
MPV wants to merge 3 commits into
masterfrom
claude/podspec-c-cue-schema

Conversation

@MPV

@MPV MPV commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Option C of four candidate answers to #26, raised side by side so they can be compared — this is the one the issue itself suggested. Scope feat(podspec) finds the set — #81 #82 #83 #84. Only one should be merged.

Rebased onto 2abd515 (0.4.3) — Kubernetes v0.36.3, Go 1.26. All numbers re-measured, and the CUE schema regenerated against the new Kubernetes API. That regeneration turned out to be the interesting part of this rebase; see below.

master A · reflection B · structural C · CUE ← this D · config
Custom resource (Argo Rollout) ✓ automatic ✓ automatic ✓ via --config
Custom resource holding bare containers (Argo Workflow) ✓ via --config
ReplicationController, PodTemplate
Rejects a containers lookalike ✓ strict decode ✓ closed CUE def ✓ path-exact
1000 documents 105 ms 110 ms 133 ms 2163 ms 76 ms
Binary 27.2 MB 27.2 MB 12.4 MB 25.4 MB 4.1 MB
go.sum lines 74 74 72 109 42
Code generation make schema
New CLI surface --schema --config

All four keep the existing goldens green and produce byte-identical output on the shared 1000-document corpus. Timings are best-of-7 in a single run on one machine.

Approach

Decode each document into plain Go values, walk it, and ask a CUE schema whether each node is PodSpec-shaped.

The schema is generated from k8s.io/api/core/v1 by cue get go (make schema, checked in under k8s/cue.mod/gen/) rather than written by hand — the official types, not a restatement of them. What kir maintains is the import that names them:

import corev1 "k8s.io/api/core/v1"

#Containers: [...corev1.#Container]
#EphemeralContainers: [...corev1.#EphemeralContainer]

What it buys over B

Both reach custom resources, both reject lookalikes, both drop the List special case. The one thing unique to CUE is that the schema is replaceable without a rebuild, and a --schema file loads inside the same module, so it can extend the official type rather than restate it (TestCustomSchema).

What the rebase cost — the honest headline

Regenerating the schema for Kubernetes v0.36.3 broke in two ways, and neither was visible until the bump:

  1. go mod tidy deleted k8s.io/api from go.mod. kir's Go code does not import it — only the generated CUE references it, and CUE imports are invisible to the Go toolchain. So the module the schema is generated from silently disappeared, and make schema then failed with "no required module provides package". Fixed with a build-tagged blank import (k8s/schema_source.go) whose only job is to keep it pinned.
  2. cue get go could not read the new Kubernetes source. go run cuelang.org/go/cmd/cue@v0.17.1 builds the generator with the Go version in CUE's go.mod (1.25), while k8s.io/api v0.36.3 requires 1.26 — so generation died with package requires newer Go version go1.26 (application built with go1.25). Fixed by pinning GOTOOLCHAIN=go1.26.5 in the target.

Both are one-line fixes once diagnosed. The point is that this is the maintenance surface options A, B and D do not have — B rode the same bump with no code change at all.

One cost did go away: CUE needs Go 1.25 and master is now on 1.26.5, so this no longer moves .tool-versions.

What it still costs

  • ~21× today, ~16× option B: 105 ms → 2163 ms per 1000 documents. The ratio got worse in the rebase, because master got faster on Go 1.26 while CUE unification did not.
  • A trap: cue.Value.Eval() on the definitions is ~7× faster and silently discards closedness, so every lookalike starts matching. Nothing errors; only TestSkipsNonWorkloads.Lookalike catches it. Left un-evaluated with a comment saying why.
  • An impedance mismatch: manifests decode YAML→JSON→Go, so every number is a float64, and offering that to a schema saying int32 rejects every container with a port. TestKind/StatefulSet caught this. Candidates now reach CUE as JSON; TestFindImagesIntegerFields pins it.
  • 47 generated .cue files checked in (up from 45 on the older API).

Note on a golden

TestFailure.BadYAML's stderr gains an error converting YAML to JSON: prefix — same class, same exit 1, different parser wrapping the message. Shared with B and D.

The question this PR poses

Is a user-replaceable schema worth ~16× option B's runtime plus a code-generation step that needs care on every Kubernetes bump — when B gets the same reach and the same lookalike rejection from k8s.io/api, a dependency kir already has?

gofmt, go vet, go mod tidy no-op, go test -race ./... green. ADR 0009 records the decision as proposed, and now records both regeneration hazards.

claude added 3 commits August 13, 2026 07:51
Option C of four candidate answers to #26, raised side by side for
comparison, and the one the issue itself suggested.

Each document is decoded into plain Go values and walked; a node is a
PodSpec when one of its container lists unifies with an embedded CUE
schema (k8s/podspec.cue). #Container is a CUE definition and therefore
closed, so a container carrying an unlisted field is a mismatch — the
same lookalike rejection option B gets from strict Go decoding.

What is unique here is that the schema is data, so it can be replaced
without a rebuild:

  $ kir manifests/                       # embedded schema
  $ kir --schema my-containers.cue ...   # your own

TestCustomSchema pins both halves: a resource whose containers carry an
unknown field yields nothing by default and its image under an extended
schema. The k8s unit tests are the same corpus as option B, so the two
engines are compared on identical cases.

The costs are why this may not be the answer. Over 1000 documents:
164ms -> 406ms, about twice option B's walk. cuelang.org/go adds 35
go.sum lines and takes the binary from 12.8 MB to 23.7 MB. It requires
Go 1.25, so .tool-versions moves from 1.24.3 (CI reads its version from
there). And podspec.cue restates part of the container API by hand,
where option B reuses k8s.io/api directly. See ADR 0008.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013RYRsLAGHPcRuwhodXXmWP
The hand-written podspec.cue restated part of the container API, which
could drift from k8s.io/api and was the weakest part of option C. Reuse
the official types instead: `cue get go k8s.io/api/core/v1` generates the
definitions (make schema, checked in under k8s/cue.mod/gen/), and the
schema kir maintains is now the import that names them.

  import corev1 "k8s.io/api/core/v1"

  #Containers: [...corev1.#Container]
  #EphemeralContainers: [...corev1.#EphemeralContainer]

A --schema file is loaded inside the same module, so a user schema can
embed corev1.#Container and add a field rather than transcribe it; the
TestCustomSchema fixture now does exactly that.

Two things the official types surfaced that the hand-written schema hid:

  - Manifests decode YAML->JSON->Go, so every number is a float64.
    Offering that to a schema saying int32 rejects every container
    declaring a port — TestKind/StatefulSet caught it. Candidates now
    reach CUE as JSON, which keeps whole numbers whole;
    TestFindImagesIntegerFields pins it.
  - Unifying against the full closed #Container is expensive: 2469ms per
    1000 documents, against 357ms for the hand-written schema. Calling
    Eval() on the definitions recovers ~7x of that and silently discards
    closedness, so every lookalike starts matching; only the Lookalike
    fixture catches it. Left un-evaluated, with a comment saying why.

Both are recorded in ADR 0009: reusing the official types removes the
fidelity objection and sharpens the cost one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013RYRsLAGHPcRuwhodXXmWP
Re-measured on the 0.4.3 base: 105ms -> 2163ms over 1000 documents. The
ratio worsens rather than improves, since master got faster on Go 1.26
while CUE unification did not: ~21x today and ~16x option B.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013RYRsLAGHPcRuwhodXXmWP
@MPV
MPV force-pushed the claude/podspec-c-cue-schema branch from cb6d9e5 to 4eab721 Compare August 13, 2026 07:51
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.

2 participants