Skip to content

feat(api): add GET /api/v1/pods/{name}/manifest endpoint (issue #80) - #100

Merged
dndungu merged 7 commits into
mainfrom
task/t4-7-issue80-manifest-endpoint
Aug 29, 2026
Merged

feat(api): add GET /api/v1/pods/{name}/manifest endpoint (issue #80)#100
dndungu merged 7 commits into
mainfrom
task/t4-7-issue80-manifest-endpoint

Conversation

@dndungu

@dndungu dndungu commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

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 /manifest 404ed. 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: PodRecord gains a RawManifest []byte field plus a SetRawManifest setter (mirrors the existing SourcePath/SetSourcePath pattern), persisted via a new manifest_raw SQLite column added through the existing ensureColumn migration helper. This is necessary because the store's existing spec_json column is a re-serialization of the parsed PodSpec, 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 (HTTP POST /api/v1/pods, the NATS req.spark.apply handler, and the manifest directory watcher) now calls SetRawManifest with 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 new GET /api/v1/pods/{name}/manifest handler. Writes RawManifest verbatim (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 existing GET /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 -- a containers: 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 PodSpec rather 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.
  • New tests in internal/api/pods_manifest_test.go, all exercising the real HTTP handler via httptest + srv.ServeHTTP (never calling store.Apply directly):
    • 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.
  • Verified via kazi's own predicate checker (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.
  • Not yet live-verified against the DGX -- that's the coordinating session's job post-merge/deploy per this repo's dispatch convention. Recipe: submit a pod manifest to the live Spark API, then curl GET /api/v1/pods/{name}/manifest and confirm the body byte-matches the submitted manifest modulo whitespace.

Linked issues / tasks

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.
@dndungu
dndungu merged commit f2859af into main Aug 29, 2026
1 check passed
@dndungu
dndungu deleted the task/t4-7-issue80-manifest-endpoint branch August 29, 2026 02:28
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.

1 participant