pruneLocalSnapshots writes prose to stdout unconditionally, so prune --json | jq still fails after the banner fix
#108
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found while verifying
issue #106 end to end.
Filing rather than fixing drive-by, for the same reason #106 was filed
out of #82: this is a different writer on a different code path, and it
is not the banner.
Problem
pruneLocalSnapshots(internal/vaultik/snapshot.go:845-878) is theonly stdout writer on a
--jsonpath with no--jsonawareness at all.Every other one is gated —
Prune's blob phase checksopts.JSONbefore each of its five writes (
internal/vaultik/prune.go:113-150),RemoveSnapshotreturns early onopts.JSON(
internal/vaultik/snapshot.go:1042),remote infowraps its wholehuman report in
if !jsonOutput(internal/vaultik/info.go:216). Thisfunction does not, and
Prunecalls it before the blob phase, so itsline lands ahead of the document:
Three writes are affected, covering every branch of the function, so
there is no input that avoids it:
v.stdoutf("Removing stale local record: %s\n", id)(line 858)v.printlnStdout("No stale local snapshots found.")(line 873)v.stdoutf("Removed %d stale local snapshot record(s).\n", removed)(line 875)
Not the same defect as #106, and not fixed by it
mainbefore the #106 change and afterit. #106 removes the three banner lines; these remain.
-qdoes not suppress it either, so it is not covered by theworkaround #106 retired.
printlnStdoutandstdoutfwrite straightto
v.Stdoutand never consultv.UI, which is whatSetQuietaffects.
vaultik prune -q --json | jqfails today for this reason.pruneis the only affected command:snapshot list --json,snapshot verify --json,snapshot remove --jsonandremote info --jsonall pipe intojqcleanly once the banner isgone. Verified against a real
file://destination store with a realsnapshot.
Expected
prune --jsonemits itsPruneBlobsResultand nothing else, the samecontract issue #82
established for the logger and #106 for the banner.
Notes
The narrow fix is to thread
opts.JSONintopruneLocalSnapshotstheway the surrounding code already does. Worth considering instead
whether these three lines want to be
log.Inforecords — they areprogress narration, not output the caller asked for, and the logger is
already on stderr. That choice is what this issue needs decided; the
count of stale records is not currently in
PruneBlobsResult, so if itshould survive into the document that is a third option.
Plan
Implementing this together with
issue #110 as one PR
against
main; they are unrelated in the code but both small.The decision this issue asks for
Of the three options in the description I am taking the first — thread
opts.JSONinto the function — and explicitly rejecting the other two.Not
log.Info. The logger's default level isslog.LevelWarn(
internal/log/log.go:68), so moving these three lines tolog.Infowould not relocate them to stderr, it would delete them from a plain
vaultik prune. "Removing stale local record: <id>" narrates thedeletion of rows from the local index; a user who runs
prunewith noflags should see that it happened, and making it visible only under
--verboseis a behaviour regression rather than a stream fix.Not
PruneBlobsResult. Every field of that struct is blob-scoped(
blobs_found,blobs_deleted,blobs_failed,bytes_freed) and itis the result of the blob phase, which runs after this reconciliation.
Adding a stale-record count to it would make the document's name
inaccurate and would change a published
--jsonschema as a sideeffect of a stream-hygiene fix. If the count should be
machine-readable, the right shape is a prune document covering both
phases, and that is a separate design question, not this patch.
So:
CleanupLocalSnapshotstakesopts *PruneOptions, symmetric withits sibling phase
PruneBlobs(opts), and each of the three writes isgated on
!opts.JSONexactly asprune.go:113-150already gates itsown. The two informational events additionally get
log.Inforecords,mirroring
PruneBlobs, which already logs and prints in parallel.Tests
Failing-before, at two layers:
internal/vaultik:CleanupLocalSnapshotswithJSON: trueleavesstdout empty in both branches (stale records present, and none), and
with
JSON: falsestill emits all three lines — so the guard cannotbe satisfied by deleting the output.
internal/cli:prune --jsonrun end to end throughEntry, cobraand fx over the process's real stdout descriptor against a
hermetic
file://store, asserting stdout holds exactly one JSONdocument. Both branches, by seeding the index database with a
snapshot that has no remote manifest. This is the assertion the
issue makes —
vaultik prune --json | jq .with no other flags —with the pipe replaced by a decoder.
Plus a manual end-to-end run of the built binary against a real config
and
file://destination store, in both branches, and a re-check thatthe other four
--jsoncommands still pipe cleanly.README.md:142will be adjusted: it currently describes the banner as"the other thing that writes to stdout", which this issue contradicts.
Implemented in
PR #111, on branch
fix-prune-json-and-build.What was built
CleanupLocalSnapshotsnow takes*PruneOptionsand gates all threewrites on
!opts.JSON, matchingPruneBlobs— its sibling phase, whichalready takes the same struct — and
RemoveSnapshotandremote info.The two informational events are additionally emitted as
log.Inforecords, the pattern
PruneBlobsalready uses, so they still exist onstderr for anyone running
--verbose.The decision the issue asked for, restated with the reasons in full in
the PR body: the
log.Info-only option was rejected because thelogger's default level is
slog.LevelWarn, so it would not move thoselines to stderr, it would delete them from a plain
vaultik prune—narrating the deletion of local index rows only under
--verboseis abehaviour regression, not a stream fix. The
PruneBlobsResultoptionwas rejected because every field of that struct is blob-scoped and it is
produced by the phase that runs after this reconciliation; adding a
stale-record count would change a published
--jsonschema as a sideeffect. Per the issue's request, that is flagged rather than silently
added — if the count should be machine-readable, the right shape is a
prune document covering both phases, which is its own design question.
How it was verified
Against a real config and a
file://destination store with a realsnapshot, using the binary from
make build, with no other flags andno
-q:vaultik prune --json | jq .exits 0.od -conthe raw stdout shows the document and nothing else.
metadata/removed under a livelocal record):
vaultik prune --json | jq .exits 0,{"blobs_found": 1, "blobs_deleted": 1, "bytes_freed": 229}, and therecord is gone from the index afterwards.
--json, all three lines are still printed — confirmed on thesame store: "Removing stale local record: ...", "Removed 1 stale local
snapshot record(s).", and "No stale local snapshots found." on the
other branch.
snapshot list --json,snapshot verify <id> --json,snapshot remove <id> --json,remote info --json,jqexit 0for each.
Tests, each confirmed to fail with the fix reverted rather than assumed
to: unit coverage of
CleanupLocalSnapshotsasserting stdout staysempty under
--jsonin all three branches (stale records, none, emptyindex) and that the human output is retained without it, so the guard
cannot be satisfied by deleting the lines; plus an end-to-end
prune --jsonthroughEntry, cobra and fx over the process's realstdout descriptor against a hermetic
file://store, asserting exactlyone JSON document, in both branches.
make checkgreen;script/cibuildexit 0 with the check layersconfirmed to have executed rather than replayed from cache.