feat(api): add GET /api/v1/pods/{name}/manifest endpoint (issue #80) - #100
Merged
Conversation
PodRecord gains a RawManifest []byte field and a SetRawManifest
setter (mirroring the existing SourcePath/SetSourcePath pattern), plus
a manifest_raw SQLite column via the existing ensureColumn migration
helper. Nothing populates it yet -- follow-up commits wire each
ingestion path (HTTP POST, NATS req.spark.apply, directory watcher).
Needed for GET /api/v1/pods/{name}/manifest (issue #80 quick win 1,
verifies UC-110) to return the manifest byte-equivalent to what was
submitted. spec_json (already persisted) is a re-serialization of the
parsed PodSpec, not the original bytes, and can't satisfy that on its
own -- YAML round-tripped through the struct loses key order, comments,
and any field the parser doesn't retain.
Part of issue #80 quick win 1 (verifies UC-110).
Keeps the NATS ingestion path consistent with the HTTP POST path so
GET /api/v1/pods/{name}/manifest works regardless of how a pod was
applied. Part of issue #80 quick win 1 (verifies UC-110).
Same treatment as SourcePath: the applied file's raw content is
recorded alongside it so GET /api/v1/pods/{name}/manifest works for
pods applied from a manifest directory, not just via the API. Part of
issue #80 quick win 1 (verifies UC-110).
Returns the RawManifest bytes captured by the previous commits, verbatim -- no JSON envelope, no reconstruction from the parsed Spec. Works for a pod in any status, including a divergent/pending one, since that's exactly when an operator needs to recreate it. Closes issue #80 quick win 1's acceptance criterion: byte-equivalent (modulo whitespace) to what was POSTed. Tests exercise the real POST handler via ServeHTTP for both the YAML and JSON submission paths (never store.Apply directly) and assert exact byte equality against the original request body, plus the not-found case. Verifies: UC-110.
Discovered while writing internal/api/pods_manifest_test.go (issue #80 T4.7/T4.8): a containers: list with dash indented at the same column as its parent key silently parses to an empty list instead of erroring. Already the confirmed root cause of issue #77 (fixed in PR #90); recorded here so the next person writing a test fixture doesn't lose time to the same trap in their own worktree.
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
Adds
GET /api/v1/pods/{name}/manifest, returning the exact manifest bytes that were submitted for a pod. Issue #80 (a pod's reported status diverged from its actual container after a crash-restart under resource pressure) notes that when that happens, an operator has no way to recreate the pod faithfully:GET /api/v1/pods/{name}returns only a status summary, and/manifest404ed. This is quick win 1 of that issue -- an independent fix that doesn't require the state-divergence root cause itself to be understood or fixed (that's tracked separately, E6 in docs/plan.md).Changes
internal/state/store.go,internal/state/sqlite.go:PodRecordgains aRawManifest []bytefield plus aSetRawManifestsetter (mirrors the existingSourcePath/SetSourcePathpattern), persisted via a newmanifest_rawSQLite column added through the existingensureColumnmigration helper. This is necessary because the store's existingspec_jsoncolumn is a re-serialization of the parsedPodSpec, not the original submission -- round-tripping YAML through the struct loses key order, comments, and any field the parser doesn't retain, so it can't satisfy "byte-equivalent" on its own.internal/api/pods_mutate.go,internal/bus/handler_apply.go,cmd/spark/main.go: each of the three places a pod can be applied (HTTPPOST /api/v1/pods, the NATSreq.spark.applyhandler, and the manifest directory watcher) now callsSetRawManifestwith the raw bytes it already has in scope, so the new endpoint works no matter how the pod was created.internal/api/pods_manifest.go: the newGET /api/v1/pods/{name}/manifesthandler. WritesRawManifestverbatim (text/plain, no JSON envelope). Deliberately not filtered by pod status -- it has to work for a pod stuck in any state, including a divergent one, since that's exactly when an operator needs it. 404 matches the existingGET /api/v1/pods/{name}not-found shape.README.md: documents the new endpoint alongside the existing Get Pod section.docs/lore.md(new file): records a YAML-parser landmine found while writing this PR's tests -- acontainers:list indented at the same column as its parent key silently parses to zero containers instead of erroring. This is the already-fixed root cause of issue Pod completes instantly (exitCode=0) with zero container startup — container spec silently dropped #77 (PR fix(manifest): parse same-indent block sequences instead of dropping them (issue #77) #90); recorded here as a general landmine for the next person hand-writing a test fixture, not because this repo is still carrying the bug.Deviation worth flagging: two earlier attempts at this same goal (via kazi, this repo's grind-to-predicates tool) converged but were rejected on manual review -- both returned a JSON-wrapped reconstruction of the parsed
PodSpecrather than the literal submitted bytes, which doesn't satisfy "byte-equivalent modulo whitespace" and would drop anything the parser doesn't retain (comments, key order, fields outside the tracked subset). This PR's implementation was hand-authored after that finding. It kept the route-registration/404-handling scaffold and the lore.md finding from those attempts, but replaced the response body and rewrote the tests to assert real byte-equality against the original POST body instead of individual struct fields.Testing
go build ./...,go vet ./...,staticcheck ./...-- all clean.go test ./... -race -timeout 120s -count=1-- all 13 packages pass, no regressions.internal/api/pods_manifest_test.go, all exercising the real HTTP handler viahttptest+srv.ServeHTTP(never callingstore.Applydirectly):TestGetPodManifest: POSTs a YAML pod manifest, GETs/manifest, asserts the response body is byte-equal (modulo whitespace) to the original POST body.TestGetPodManifestJSON: same, for a JSON submission (issue POST /api/v1/pods with JSON body returns 201 {"pods":null} and creates nothing #74's content-type path), confirming the endpoint doesn't always re-render as YAML.TestGetPodManifestNotFound: 404 with the standard error shape for a pod that was never applied.kazi apply <proposal> --workspace <this branch's worktree> --check --json) against the final commits: all 10 acceptance/guard predicates pass, including a negative-space check that the found-case test genuinely POSTs through the real handler rather than seeding the pod via a direct store call.curl GET /api/v1/pods/{name}/manifestand confirm the body byte-matches the submitted manifest modulo whitespace.Linked issues / tasks
docs/plan.md)..claude/scratch/usecases-manifest.json.