7.3 KiB
vantagectl (vantage-ctl)
The backup and restore CLI for a Vantage control plane. Extracted from the
vantage monorepo with its history; the command is the repository root, so the
module is gitea.hostxtra.co.uk/vantage/vantage-ctl.
The command is still vantagectl. Only the repository is vantage-ctl -
the binary, the cobra Use: string and every runbook keep the name an operator
actually types.
vantage-ctl/
├── main.go
├── internal/cmd/ # argument parsing and operator-facing output only
├── Dockerfile # scratch runtime, with an explicitly copied /tmp
└── .gitea/workflows/release.yml
Why this is not a subcommand of the server
server imports the whole control-plane dependency graph, and spf13/cobra has
no business in a process that also terminates gRPC streams and serves the REST
API. More to the point: vantagectl has to run when the control plane does
not. A backup or restore against a database with no server container alive is
the normal case, not the exception, so it cannot be a mode of the binary whose
failure is the reason you reached for it.
Where the logic actually lives
Almost none of it is here. Dump, restore, verify, manifest and fingerprint are
shared/backup in vantage-shared; internal/cmd holds argument parsing
and the words an operator reads. That split is what would let the control plane
import shared/backup later - a scheduled in-process backup, say - without a
second implementation to keep in sync. shared/cryptobox is the same move one
layer down: the single AES-256-GCM implementation, which the server delegates to
rather than keeping its own copy that backup would have had to duplicate to
decrypt a probe value during verify.
So a fix to backup behaviour is usually a vantage-shared release plus a pin
bump here, and it is not live until this repository cuts a tag. That is
deliberate - an operator restoring a database should be running a version they
can name - but it does mean the fix is not in anybody's hands the moment it
merges.
The refusals, and why each exists
These are the load-bearing parts. They are refusals rather than warnings because each one guards something with no way back.
backupwill not run withoutKEY_ENCRYPTION_KEYunless--allow-no-keyis passed. The archive stores a SHA-256 fingerprint of the key, never the key. An archive with no fingerprint cannot tell a later restore that the wrong key is in hand - it can only find that out when the data comes back as noise.restorerefuses a non-empty target database, and there are no merge semantics. No code path upserts an archive's documents over existing ones: merging two control planes reconciles nothing about which SSH keys are still valid or which users still exist, and an upsert would resurrect a revoked key or a deleted member from the older side.--forceneeds a second assurance:--confirm-db NAMEmatching the target exactly, which works everywhere, or - on a terminal only, and only when--confirm-dbwas not given - the database name typed back at a prompt.--confirm-dbis accepted on a terminal too, and is the stronger of the two: naming the target in the command itself means a copied command carries its intended target and cannot destroy a different one by accident. Without a terminal and without--confirm-db,--forceis refused.--forcedrops only what the archive names. Collections in the target that the archive does not carry are left alone and named in a warning - an archive taken with--exclude workflow_log_lines, restored over a live database, leaves the old lines joined to restored runs and the operator must be told. Dropping them instead would delete data nobody asked to delete.
Three decisions that look like details and are not
- Collections are enumerated live, not read from a static list - the
opposite of what instance-deletion purge does in the control plane. Purge must
never miss a tenant-scoped collection, so it keeps a hand-maintained registry;
a backup must never miss any collection, including the ones carrying no
instance_idat all (migrations,vulndb_meta). - Index specifications are replayed verbatim, never reconstructed.
dumpIndexesstores each spec as extended JSON over the raw BSON the server reported, andreplayIndexeshands it back throughcreateIndexeswith onlyvandnsstripped and_id_skipped. Rebuilding amongo.IndexModelfrom hand-picked options droppedpartialFilterExpression, which the control plane relies on, so a partial unique index came back as a full one, failed on duplicate keys, and aborted the restore mid-write. Reconstructing the key document from JSON also loses compound key order, which is significant. - A file-backed
backupwrites<name>.tar.gz.partialand renames on success, the same discipline the agent uses forauthorized_keys. A failed dump must not leave a partial file named exactly like a good archive.--out -is untouched: a broken pipe has no file to mislead anyone.
The mirror that fails silently
backup.ciphertextFields lives in vantage-shared and mirrors
server/internal/models in the vantage repository by hand - shared/ cannot
import it. The map naming each collection's *_enc fields (keys, secrets,
auth_providers, console_sessions) must change in the same commit as any of
those bson tags.
Wrong field names are silent: verify's live probe simply finds no
ciphertext and reports "this database stores no ciphertext yet", so the one gate
that catches what a key fingerprint cannot becomes a no-op. settings is
deliberately in neither that map nor CiphertextCollections() - its ESO read
token is a SHA-256 hash, not ciphertext.
Building and releasing
GOPRIVATE=gitea.hostxtra.co.uk/* plus a credential, since vantage-shared is
private. CI writes a netrc from REGISTRY_USER + RELEASE_TOKEN in both
jobs - they do not share a filesystem - and the image build takes it as a
BuildKit secret rather than a build arg, which would survive in the builder
layer's history. RELEASE_TOKEN needs read access to the vantage org.
Tags are a bare v* now this is its own repository. The old
vantagectl/v* prefix namespaced one component's releases inside a shared
repository; unlike the agent's agent/v* - which the control plane greps
release tag names for - nothing reads this one programmatically. Releases from
before the move keep their prefixed tags in the monorepo's release list.
git tag v0.2.0 && git push origin v0.2.0
Builds linux/amd64, linux/arm64, darwin/arm64 and windows/amd64, writes
checksums.txt, creates the Gitea release, and pushes
vantage/vantage-ctl:<version> and :latest.
The runtime stage is scratch, and it carries an explicitly copied /tmp.
restore extracts an archive to a temporary directory before verifying its
checksums, and a scratch image has none for os.MkdirTemp to find. Without it
every restore fails - the same omission costs the control plane only its
vulnerability scanning, but here it is the whole tool.
Writing style
Never use em dashes (the long dash character) anywhere: code, comments, UI copy, docs, commit messages. Use a plain hyphen -, a comma, a colon, or split the sentence instead.