refactor: move vantagectl to vantage-ctl
vantagectl/ becomes the root of gitea.hostxtra.co.uk/vantage/vantage-ctl. The command keeps the name vantagectl; only the repository and the image path change, to vantage/vantage-ctl. chart-release.yml's render checks are repointed at the new image. The chart itself has no default backup.image and fails loudly without one, so an existing cluster keeps working until someone changes the value. go.work stays, now with a single use ./server entry: without it a go.work further up the developer's filesystem is picked up instead.
This commit is contained in:
@@ -58,10 +58,11 @@ vantage/
|
||||
│ └── lib/ # api client, guac console, query client
|
||||
├── installer/ # Windows: setup.ps1, nssm.exe, WiX .wxs
|
||||
├── deploy/ # docker-compose.yml, Helm chart
|
||||
└── .gitea/workflows/ # server-deploy.yml, chart-release.yml, vantagectl-release.yml
|
||||
└── .gitea/workflows/ # server-deploy.yml, chart-release.yml
|
||||
```
|
||||
|
||||
**Four repositories carry parts of Vantage that this one does not.**
|
||||
**Five repositories carry parts of Vantage that this one does not.** What is
|
||||
left here is the control plane and its UI, and nothing else.
|
||||
|
||||
| Repository | What it holds |
|
||||
| ---------------- | ------------------------------------------------------------------------------------------------- |
|
||||
@@ -70,6 +71,7 @@ vantage/
|
||||
| `vantage-site` | the marketing site (`web/`, was `site/`) and its contact-form service (`server/`, was `sitesvc/`) |
|
||||
| `vantage-docs` | the user documentation, at the repository root (was `docsite/`) |
|
||||
| `vantage-agent` | the agent, at the repository root (was `agent/`), and the Windows `installer/` |
|
||||
| `vantage-ctl` | `vantagectl`, the backup and restore CLI, at the repository root (was `vantagectl/`) |
|
||||
|
||||
**None of the three is a build dependency of anything here**, and nothing here
|
||||
is a dependency of them. `vantage-site` and `vantage-docs` are wholly
|
||||
@@ -90,9 +92,9 @@ email system: transport plus templates), `license/` (payload, sign, verify,
|
||||
trusted keys, plans), `models/` (Instance, User, Settings), `provision/`,
|
||||
`backup/`, `cryptobox/`, `indexes/`, `grpc/pb` + `grpc/codec`, and
|
||||
`cmd/lkctl/`, and `proto/vantage/v1/vantage.proto`, which documents `grpc/pb`
|
||||
and moved there to sit beside it. Two modules here depend on it — `server` and
|
||||
`vantagectl` — each pinning a version in its own `go.mod`, as do
|
||||
`vantage-admin`, `vantage-site` and `vantage-agent`. It was a
|
||||
and moved there to sit beside it. **One** module here depends on it — `server` —
|
||||
pinning a version in its own `go.mod`, as do `vantage-admin`, `vantage-site`,
|
||||
`vantage-agent` and `vantage-ctl`. It was a
|
||||
directory in this repository until it was extracted with its history; the
|
||||
`replace ../shared` directives and the `./shared` entry in `go.work` are gone
|
||||
with it.
|
||||
@@ -115,9 +117,14 @@ arg, because an arg survives in the builder layer's history and this one is a
|
||||
Gitea token. Locally, either a netrc or
|
||||
`git config --global url."git@gitea.hostxtra.co.uk:".insteadOf https://gitea.hostxtra.co.uk/`.
|
||||
|
||||
**`vantagectl` builds from its own directory as a result.** `server` still
|
||||
builds from the repository root, and only because its runtime stage copies
|
||||
`default_steps/`.
|
||||
**`server` still builds from the repository root**, and only because its runtime
|
||||
stage copies `default_steps/`. It is the only image this repository builds from
|
||||
a context wider than one directory.
|
||||
|
||||
`go.work` survives with a single `use ./server` entry. That looks pointless and
|
||||
is not: without it, a `go.work` further up the developer's filesystem is picked
|
||||
up instead and the build fails with `directory prefix . does not contain modules
|
||||
listed in go.work`.
|
||||
|
||||
---
|
||||
|
||||
@@ -608,102 +615,46 @@ inside each — all name that repository. They must agree with wherever
|
||||
|
||||
### Backup and restore
|
||||
|
||||
`vantagectl` is a standalone Go module (`vantagectl/`), not a subcommand of
|
||||
`server`. It needs its own module rather than living inside `server`'s for the
|
||||
same reason `admin` and `sitesvc` did before they left: `server` imports the rest of
|
||||
`server`'s 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 at all — so it
|
||||
cannot be a mode of the binary whose crash is the reason you need it.
|
||||
`vantagectl` is `vantage-ctl` now — its own repository, with the command still
|
||||
named `vantagectl`. It is not a subcommand of `server` and never was: `server`
|
||||
imports the whole control-plane dependency graph, `spf13/cobra` has no business
|
||||
in a process that also terminates gRPC streams, and above all **it 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, so it cannot be a mode of the
|
||||
binary whose failure is the reason you reached for it.
|
||||
|
||||
The actual logic lives in `shared/backup` (dump, restore, verify, manifest,
|
||||
fingerprint), not in `vantagectl/internal/cmd`, which holds only argument
|
||||
parsing and operator-facing output. That split is what lets `server` 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: it is now the **single** AES-256-GCM implementation, and
|
||||
`server/internal/services/crypto.go` delegates to it rather than keeping its
|
||||
own copy that `shared/backup` would otherwise have had to duplicate to decrypt
|
||||
a probe value during `verify`.
|
||||
Almost none of its logic is in that repository either: dump, restore, verify,
|
||||
manifest and fingerprint are `shared/backup` in `vantage-shared`, and
|
||||
`internal/cmd` holds only argument parsing and operator-facing output. That
|
||||
split is what would let `server` 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: it is the **single**
|
||||
AES-256-GCM implementation, and `server/internal/services/crypto.go` delegates
|
||||
to it rather than keeping its own copy that `shared/backup` would otherwise have
|
||||
had to duplicate to decrypt a probe value during `verify`.
|
||||
|
||||
**The archive stores a SHA-256 fingerprint of `KEY_ENCRYPTION_KEY`, never the
|
||||
key.** `backup` refuses to run without the key set in the environment unless
|
||||
`--allow-no-key` is passed, because an archive with no fingerprint at all
|
||||
cannot later tell a restore that the wrong key is in hand — it can only find
|
||||
that out when the data comes back as noise. The fingerprint is what turns that
|
||||
failure into a refusal at `restore` time instead.
|
||||
**The one thing this repository owes it is `backup.ciphertextFields`**, which
|
||||
lives in `vantage-shared` and mirrors `server/internal/models` **by hand** —
|
||||
`shared/` is a separate module and 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,
|
||||
and that commit is now in a different repository from the tags it tracks. Wrong
|
||||
field names are **silent**: `verify`'s live probe 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.
|
||||
|
||||
**Collections are enumerated live** — `shared/backup` lists what the database
|
||||
actually holds rather than reading `services.ScopedCollections`, the opposite
|
||||
choice from the one instance-deletion purge makes. Purge must never miss a
|
||||
tenant-scoped collection, so it keeps one hand-maintained registry; a backup
|
||||
must never miss **any** collection, tenant-scoped or not (`migrations`,
|
||||
`vulndb_meta`), so a static list is the wrong shape twice over — once for the
|
||||
collections it would still owe `instance_id` deletion but not a backup, and
|
||||
once for the two singleton collections that carry neither `instance_id` nor a
|
||||
release note.
|
||||
The chart's optional backup CronJob runs that image, and **`backup.image` has
|
||||
no default** — the template `fail`s without one rather than guessing, so a
|
||||
cluster set up before the move keeps working until someone changes the value.
|
||||
It is `gitea.hostxtra.co.uk/vantage/vantage-ctl:latest` now, was
|
||||
`mrhid6/vantage/vantagectl:latest`; `chart-release.yml`'s render checks name the
|
||||
new path.
|
||||
|
||||
**Restore refuses a non-empty target database and has no merge semantics.**
|
||||
There is no code path that upserts an archive's documents over existing ones:
|
||||
merging two control planes' data 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. `--force` drops each
|
||||
collection in the archive first, and is gated behind a second assurance:
|
||||
`--confirm-db NAME` matching the target exactly, which works everywhere, or —
|
||||
on a terminal only, and only when `--confirm-db` was not given — the target
|
||||
database's name typed back at a prompt. `--confirm-db` is accepted on a
|
||||
terminal too: it is the stronger of the two, because naming the target in the
|
||||
command itself means a copied command carries its intended target with it and
|
||||
cannot destroy a different one by accident. Without a terminal and without
|
||||
`--confirm-db`, `--force` is refused.
|
||||
|
||||
**`--force` drops only what the archive names.** Collections already in the
|
||||
target that the archive does not carry are left untouched 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, which the
|
||||
operator must be told. Dropping them instead would delete data nobody asked to
|
||||
delete, and there is no way back from that.
|
||||
|
||||
**Index specifications are replayed verbatim, never reconstructed.**
|
||||
`dumpIndexes` stores each spec as extended JSON over the raw BSON the server
|
||||
reported, and `replayIndexes` hands it back to `createIndexes` through
|
||||
`RunCommand` with only `v` and `ns` stripped and `_id_` skipped. Rebuilding a
|
||||
`mongo.IndexModel` from a hand-picked set of options dropped
|
||||
`partialFilterExpression` — which this codebase relies on in
|
||||
`services/workflows.go` and `services/settings.go` — 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 lost compound key
|
||||
order, which is significant.
|
||||
|
||||
**`backup.ciphertextFields` mirrors `server/internal/models` by hand.**
|
||||
`shared/` is a separate module — a separate *repository* now — and `models` is
|
||||
under `server/internal`, so `shared/backup` 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, the same hazard as
|
||||
`web/lib/targets.ts` and `services.MaxWorkloadLogLines`. 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
|
||||
fingerprint cannot no-ops. `settings` is deliberately in neither that map nor
|
||||
`CiphertextCollections()` — its ESO read token is a SHA-256 hash, not
|
||||
ciphertext.
|
||||
|
||||
**A file-backed `backup` writes to `<name>.tar.gz.partial` and renames on
|
||||
success**, the same discipline the agent uses for `authorized_keys`. A failed
|
||||
dump must not leave a partial file named exactly like a good archive; `--out -`
|
||||
is untouched, since a broken pipe has no file to mislead anyone.
|
||||
|
||||
**`vantagectl/Dockerfile`'s runtime stage is `scratch`, and needs the same
|
||||
explicit `/tmp` as `server/Dockerfile`.** `restore` extracts an archive to a
|
||||
temporary directory before verifying its checksums, and a scratch image has no
|
||||
`/tmp` for `os.MkdirTemp` to find — the same failure mode `vulnsched` hits on
|
||||
`server`, but here it would break every restore rather than only vulnerability
|
||||
scanning.
|
||||
|
||||
**Nothing in this repository rebuilds when `vantage-shared` changes.** It is an
|
||||
external module, so a `shared/backup` fix reaches `vantagectl` when somebody
|
||||
bumps `vantagectl/go.mod` and cuts a `vantagectl/v*` tag, and reaches nothing
|
||||
else until its pin moves too — see the CI section below.
|
||||
The rest — the refusals around `--force` and `--confirm-db`, the key
|
||||
fingerprint, live collection enumeration, verbatim index replay, the `.partial`
|
||||
rename — is documented in `vantage-ctl`.
|
||||
|
||||
### API tokens and OpenAPI
|
||||
|
||||
@@ -1329,7 +1280,7 @@ lives — one copy instead of the three that existed while they were apart.
|
||||
|
||||
### `server-deploy.yml` — triggered on every push to `main`
|
||||
|
||||
Builds and pushes **two** images to the Gitea container registry: `server` and `web`. Everything else that used to be built here now belongs to the repository that owns it — `vantage-site`, `vantage-docs` and `vantage-admin` each publish their own. **`vantagectl` is also not among them** — it is a released tool rather than a running service, and its image is version-tagged by `vantagectl-release.yml`.
|
||||
Builds and pushes **two** images to the Gitea container registry: `server` and `web`. That is now the whole of this workflow. Everything else that was once built here belongs to the repository that owns it — `vantage-site`, `vantage-docs`, `vantage-admin` and `vantage-ctl` each publish their own, and `vantage-agent` cuts releases rather than images.
|
||||
|
||||
Note that despite the name, **this workflow does not deploy** — it only builds and pushes. There is no SSH step. Rolling images out is a separate manual step on the host:
|
||||
|
||||
@@ -1358,11 +1309,10 @@ you a pin is stale.
|
||||
|
||||
Every Go build in these workflows writes a netrc from `REGISTRY_USER` +
|
||||
`RELEASE_TOKEN` before it runs, and sets `GOPRIVATE=gitea.hostxtra.co.uk/*`.
|
||||
There are **three** such places here, and each needs its own because jobs do not
|
||||
share a filesystem: `server-deploy.yml`'s single job and both jobs of
|
||||
`vantagectl-release.yml`. The other repositories each carry their own —
|
||||
`vantage-agent`'s `msi` job is the one to remember, because it is Windows, where
|
||||
Go reads `%USERPROFILE%\_netrc` and not `.netrc`. The docker builds pass
|
||||
There is exactly **one** such place left here: `server-deploy.yml`'s single job.
|
||||
The other repositories each carry their own, one per job, because jobs do not
|
||||
share a filesystem — `vantage-agent`'s `msi` job is the one to remember, because
|
||||
it is Windows, where Go reads `%USERPROFILE%\_netrc` and not `.netrc`. The docker builds pass
|
||||
it on as `--secret id=netrc`, never a build arg. **`RELEASE_TOKEN` needs read
|
||||
access to the `vantage` org** on top of its existing scopes; without it every Go
|
||||
build fails at `go mod download` with a 404 on the module, which reads like a
|
||||
@@ -1389,13 +1339,14 @@ helm install vantage vantage/vantage --version 0.1.0
|
||||
### Tagging
|
||||
|
||||
```bash
|
||||
git tag chart/v0.1.0 && git push origin chart/v0.1.0 # helm chart package
|
||||
git tag vantagectl/v0.1.0 && git push origin vantagectl/v0.1.0 # vantagectl release
|
||||
git push origin main # server + web deploy
|
||||
git tag chart/v0.1.0 && git push origin chart/v0.1.0 # helm chart package
|
||||
git push origin main # server + web deploy
|
||||
```
|
||||
|
||||
The agent is tagged in `vantage-agent`, still as `agent/v*` — that prefix is
|
||||
what the control plane greps release tag names for, so it survived the move.
|
||||
Only two things are tagged here now. Elsewhere: `vantage-agent` keeps the
|
||||
`agent/v*` prefix, because the control plane greps release tag names for exactly
|
||||
that string; `vantage-ctl` dropped its prefix for a bare `v*`, because nothing
|
||||
reads it programmatically.
|
||||
|
||||
### Secrets / variables
|
||||
|
||||
|
||||
Reference in New Issue
Block a user