Introduce RuntimeConfig wrapper for vMCP ConfigMap surface#5238
Draft
ChrisJBurns wants to merge 4 commits intomainfrom
Draft
Introduce RuntimeConfig wrapper for vMCP ConfigMap surface#5238ChrisJBurns wants to merge 4 commits intomainfrom
ChrisJBurns wants to merge 4 commits intomainfrom
Conversation
VirtualMCPServerSpec.Config is typed as pkg/vmcp/config.Config in
v1beta1, so controller-gen walks every field reachable from Config into
the public CRD schema. That blocks adding operator-resolved sidecar
fields (per-backend secret-identifier maps, resolved CA bundle paths,
etc.) without churning the CRD.
Introduce RuntimeConfig: a wrapper that embeds Config inline and is the
designated place for operator-resolved fields. Today RuntimeConfig adds
nothing — marshalled YAML is byte-identical, parsed YAML is identical,
and `task operator-manifests` produces zero CRD diff. Future PRs add
sidecar fields here without touching the public Config or v1beta1.
Wiring:
- YAMLLoader.Load now returns *RuntimeConfig. Callers that only need
user-facing Config fields read them through the embed (rc.Name,
rc.Group, etc.); callers that consume sidecars read them off the
wrapper directly.
- The operator wraps *Config in RuntimeConfig{} before marshal to the
vMCP ConfigMap (single write path, single read path).
- The CLI boundary (loadAndValidateConfig in pkg/vmcp/cli/serve.go and
validate.go) unwraps to *Config to keep the existing pipeline tight;
a comment marks where to thread the wrapper through when a sidecar
consumer arrives.
Tests pinning the seam:
- pkg/vmcp/config/runtime_config_test.go: byte-identity vs Config,
round-trip through Load, and a disjoint-tag check that catches a
future field on RuntimeConfig sharing a JSON or YAML key with any
Config field (encoding/json and yaml.v3 handle key collisions
differently).
- cmd/thv-operator/pkg/spectoconfig/runtime_config_drift_test.go:
asserts RuntimeConfig is a strict superset of Config and that any
extras must appear in runtimeOnlyLeafJustifications with a written
rationale. Lives operator-side because the drift harness in
cmd/thv-operator/internal/testutil is operator-internal.
Acceptance gate verified: task operator-manifests and task
operator-generate produce zero diff under deploy/charts/operator-crds/
and docs/operator/crd-api.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI's gci/gofmt step wanted an extra space in the column-aligned method declaration on fakeEnv. No semantic change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #5238 +/- ##
==========================================
+ Coverage 67.91% 67.96% +0.05%
==========================================
Files 610 612 +2
Lines 62522 62724 +202
==========================================
+ Hits 42464 42633 +169
- Misses 16879 16900 +21
- Partials 3179 3191 +12 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
CI's crdref-gen step (task crdref-gen) globs pkg/vmcp/config/*.go and iterates every type for documentation rendering. The shared template at docs/operator/templates/markdown/gv_details.tpl emits two newlines per loop iteration regardless of whether the inner type renders content, which means a non-+gendoc type leaks two blank lines into docs/operator/crd-api.md. Every existing type in pkg/vmcp/config has +gendoc, so the latent template bug never surfaced — until RuntimeConfig arrived without one (intentionally; we don't want it documented as CRD surface). Two principled fixes were considered. Patching the gv_details.tpl loop to suppress per-iteration whitespace eats the section separators that the existing rendered docs depend on, so it would require a deeper template rework outside this PR's scope. Instead, move RuntimeConfig into the pkg/vmcp/config/runtime subpackage. The Taskfile sources at cmd/thv-operator/Taskfile.yml:289 only glob *.go directly inside pkg/vmcp/config (not subpackages), so crdref-gen never iterates the subpackage and the template bug stays dormant. Side effect: pkg/vmcp/config can no longer reference RuntimeConfig without a circular import (runtime imports config). The YAMLLoader.Load signature reverts to returning *Config — runtime.RuntimeConfig is a write-side-only wrapper for now. When a future PR lands a sidecar field on RuntimeConfig and a vMCP runtime consumer needs it, that PR adds a runtime.Load helper that does its own RuntimeConfig parse with strict validation. The drift test, byte-identity test, round-trip test, and disjoint-tag test all move with the type into the runtime subpackage. The operator's ConfigMap write path imports vmcpruntimeconfig and wraps Config in RuntimeConfig before marshal — single write path, single read path. Acceptance gate (verified): - task operator-manifests, task operator-generate, task crdref-gen all produce zero diff under deploy/charts/operator-crds/ and docs/operator/crd-api.md. - All tests pass: pkg/vmcp/config, pkg/vmcp/config/runtime, cmd/thv-operator/pkg/spectoconfig. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
revive flagged runtime.RuntimeConfig as a stutter — packages should not repeat their name in the type. The idiomatic fix is to rename the type to Config; disambiguation between this package's Config and the parent pkg/vmcp/config.Config happens via import alias at call sites (vmcpconfig vs vmcpruntimeconfig), which is the standard Go pattern. Test function names (TestRuntimeConfig_*) and subtest descriptions are descriptive labels so they keep their full names. Production-code references update to the qualified runtime.Config form, including failure messages in the drift test so a developer who hits one can grep straight to the right symbol. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
11 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
VirtualMCPServerSpec.Configis typed aspkg/vmcp/config.Configin v1beta1, socontroller-genwalks every field reachable fromConfiginto the public CRD schema. That blocks adding operator-resolved sidecar fields (per-backend secret-identifier maps, resolved CA bundle paths, futureBackendHeaderForwardforMCPServerEntryreferences, etc.) without churning the CRD and triggering the v1beta1 stability gate.This PR introduces
pkg/vmcp/config.RuntimeConfig: a wrapper that embedsConfiginline and is the designated home for operator-resolved fields. Today the wrapper adds nothing — marshalled YAML is byte-identical, parsed YAML is identical, andtask operator-manifestsproduces zero CRD diff. Future PRs add sidecar fields ontoRuntimeConfigwithout touching the publicConfigor v1beta1.This is a foundational refactor for #4996 / #5013 (forward
MCPServerEntry.headerForwardto vMCP outbound requests). The current #5013 implementation adds 96 lines of CRD YAML and 86 lines ofcrd-api.mdbecauseHeaderForwardwas placed onStaticBackendConfig(a Config-reachable type). Once this PR lands, #5013 can be reworked to put the field onRuntimeConfiginstead — net CRD diff drops to zero.Wiring
YAMLLoader.Loadnow returns*RuntimeConfig. Callers that only need user-facingConfigfields read them through the embed (rc.Name,rc.Group, etc.); callers that consume sidecars read them off the wrapper directly. StrictKnownFields(true)validation is preserved.cmd/thv-operator/controllers/virtualmcpserver_vmcpconfig.gowraps*ConfiginRuntimeConfig{}before YAML marshal. Single write path, single read path.loadAndValidateConfiginpkg/vmcp/cli/serve.goandvalidate.gounwrap to*Configto keep the existing serve pipeline tight. A comment marks where to thread the wrapper through when a sidecar consumer arrives.Tests pinning the seam
pkg/vmcp/config/runtime_config_test.go:TestRuntimeConfig_MarshalsIdenticallyToConfig— byte-identity vsConfigtoday.TestRuntimeConfig_Load_RoundTrip— Load through the operator's write shape.TestRuntimeConfig_DisjointTopLevelTags— reflect-based check that catches a future field onRuntimeConfigsharing a JSON or YAML key with anyConfigfield.encoding/json(anonymous-field promotion) andyaml.v3(,inline) handle key collisions differently, so a collision would silently produce divergent serialization. This is forward-looking — today the wrapper has no extras and the test is trivially green.cmd/thv-operator/pkg/spectoconfig/runtime_config_drift_test.go:RuntimeConfigis a strict superset ofConfig.runtimeOnlyLeafJustificationswith a written rationale (today empty).cmd/thv-operator/internal/testutil/is operator-internal.Acceptance gate
git diff main -- deploy/charts/operator-crds/ docs/operator/crd-api.mdis empty after running both:The wrapper is invisible to
controller-genbecauseRuntimeConfigis not field-referenced from any v1beta1 type. The doc onRuntimeConfigcalls out the only way to break that invariant (retypingVirtualMCPServerSpec.Configfromconfig.Configtoconfig.RuntimeConfig— don't).Type of change
Test plan
task buildpasses (verified viago build ./...)task testpasses for all touched packages (pkg/vmcp/...,cmd/thv-operator/pkg/spectoconfig/...)TestRuntimeConfig_MarshalsIdenticallyToConfigTestRuntimeConfig_Load_RoundTripTestRuntimeConfig_DisjointTopLevelTagsTestRuntimeConfigSeam(operator-side drift test)task operator-manifestsandtask operator-generateproduce zero diff underdeploy/charts/operator-crds/anddocs/operator/crd-api.mdSpecial notes for reviewers
Configdirectly. The point is to establish the seam now so the next operator-resolved field doesn't have to fight v1beta1 stability.Load()signature change.YAMLLoader.Load()now returns*RuntimeConfiginstead of*Config. Most existing callers are field-access only (works transparently through embed). Two callers needed&cfg.ConfigforValidator.Validate(*Config). The CLI'sloadAndValidateConfigunwraps to*Configat its return boundary to keep the serve pipeline tight; a comment documents the migration path when a sidecar consumer is added.cmd/thv-operator/pkg/spectoconfig/runtime_config_drift_test.gobecause the drift harness lives incmd/thv-operator/internal/testutil/and is operator-internal. The TYPES it tests live inpkg/vmcp/config/. This layering is acceptable; moving the harness up to a shared location is out of scope for this PR.Implementation plan
Approved plan (AI-assisted)
Three agents reviewed the design before commit:
Load()covered, single ConfigMap write path, no checksum fixtures pinned to current YAML.,inlinesemantics, strict-decode behaviour). One refinement landed in the type doc — the only way to leakRuntimeConfigfields into the CRD is retypingVirtualMCPServerSpec.Config, now explicitly called out.Load/LoadRuntimeinto oneLoad()returning*RuntimeConfig(interface pollution; lossy view would have become a latent bug); (2) added the disjoint-tag reflect test to catch the top forward-looking hazard.🤖 Generated with Claude Code