Managing Configuration Versions
This is the hands-on companion to the
versioning model — read that first
if terms like latest vs running, declared base, or descendancy guard
are new. The examples assume $VSS (API base URL), $TOKEN (session JWT),
and $NSID (namespace); on the vsscli side, an active profile whose
namespace is set (vsscli profile), so commands don’t need the namespace
repeated. Storing versions needs config:write; promoting needs
config:apply.
Seeing where you are
Section titled “Seeing where you are”# List versions (newest first, 100/page; cursor in next_cursor)curl -s "$VSS/v1/ns/$NSID/config" -H "Authorization: Bearer $TOKEN"
# Resolve a tag to its versioncurl -s "$VSS/v1/ns/$NSID/config?tag=golden" -H "Authorization: Bearer $TOKEN"The namespace head (GET /v1/ns/$NSID) reports running_version and
latest_version — if they differ, a staged change is waiting.
vsscli config versions # oldest-first tablevsscli config versions --full # include each version's YAMLvsscli config versions --tag golden # just the tagged versionFetching a version
Section titled “Fetching a version”Version tokens are interchangeable everywhere below: a ULID, latest,
running, or a tag.
# Raw YAML (default); the ETag header is the version ULID — keep it,# it's your If-Match base for the next write.curl -s "$VSS/v1/ns/$NSID/config/running" \ -H "Authorization: Bearer $TOKEN" -o running.yaml -D headers.txt
# JSON envelope instead (version, parent_version, tag, description, yaml)curl -s "$VSS/v1/ns/$NSID/config/latest" \ -H "Authorization: Bearer $TOKEN" -H "Accept: application/json"
# The effective (post-inheritance) document instead of what you authoredcurl -s "$VSS/v1/ns/$NSID/config/running?view=effective" \ -H "Authorization: Bearer $TOKEN"?view= accepts user (the default — your authored YAML) or effective
(the merged document apply actually renders); anything else is a 400, so a
typo can’t silently hand you the wrong view. In the JSON envelope the body
arrives in effective_config_yaml instead of user_config_yaml —
whichever view you asked for is the one populated.
vsscli config get running --out running.yamlvsscli config get latest lines.1001 # just one sub-treevsscli config get golden --metadata-only # version/tag/parent, no bodyvsscli config get running --effective # post-inheritance merged view--effective shows the document as it runs — your YAML after parent
inheritance is merged in. Selectors and --out work against it the same
way; it can’t be combined with --metadata-only (metadata is the same for
both views). When a call matches a rule you never wrote, this view is
where the inherited rule will be visible.
To compare versions before acting:
There is no server-side diff — fetch both versions and diff locally
(diff <(curl … running) <(curl … latest)).
vsscli config diff running latest # what would go live on applyvsscli config diff running edited.yaml # a local file against liveThe diff is semantic (parsed YAML, not text lines), so formatting-only changes don’t show.
Validating
Section titled “Validating”Validate early and often — it’s the same compiler that gates storing, so a clean validate means the store will succeed.
# In-namespace: inheritance is merged in, references to inherited# entries resolve. Requires config:write on the namespace.curl -s -X POST "$VSS/v1/ns/$NSID/config/validate" \ -H "Authorization: Bearer $TOKEN" --data-binary @draft.yaml
# Standalone: no namespace context, no merge.curl -s -X POST "$VSS/v1/ns/config/validate" \ -H "Authorization: Bearer $TOKEN" --data-binary @draft.yamlFailures return a problem document whose validation_errors array lists
every error, not just the first.
vsscli config validate draft.yaml # ns-scoped when a namespace is activecat draft.yaml | vsscli config validate - # stdin ("-" or --stdin)Creating a new version
Section titled “Creating a new version”Full-document upload
Section titled “Full-document upload”# The If-Match base is mandatory: prove you edited the current latest.BASE=$(curl -sI "$VSS/v1/ns/$NSID/config/latest" \ -H "Authorization: Bearer $TOKEN" | awk -F'"' '/^ETag/{print $2}')
curl -s -X PUT "$VSS/v1/ns/$NSID/config" \ -H "Authorization: Bearer $TOKEN" \ -H "If-Match: \"$BASE\"" \ --data-binary @draft.yaml# → 201 {"success":true,"new_version":"01JD…"} (Location + ETag headers)A 412 means another writer got there first; the response’s
current_latest field (and ETag header) name the new latest — re-fetch,
reconcile your edit, and re-PUT against it. ?base=latest works instead of
If-Match for quick interactive use, but a concrete ULID is what actually
protects you: base=latest re-resolves at arrival time and so always
passes, racing writers included.
vsscli config push draft.yamlvsscli config push draft.yaml --tag pre-cutover --description "adds trunk B"vsscli config push draft.yaml --apply # push + promote in one stepvsscli config push draft.yaml --rebase # on 412: show diff, confirm, retrycat draft.yaml | vsscli config push --stdin--base defaults to latest as resolved when you push; the safe
workflow is config get latest --out, edit, push — the CLI carries the
fetched version as the base. On a stale base with --rebase, the CLI
shows a diff between the new latest and your document and asks before
re-pushing.
Targeted edits (patch)
Section titled “Targeted edits (patch)”For small changes, patch a version instead of re-uploading the document.
The API takes RFC 6902 JSON Patch (JSON or YAML encoded), applied to the authored document of the source version in the URL:
curl -s -X PATCH "$VSS/v1/ns/$NSID/config/latest" \ -H "Authorization: Bearer $TOKEN" \ -d '[ {"op":"test","path":"/lines/1001/line-profile","value":"standard"}, {"op":"replace","path":"/lines/1001/line-profile","value":"premium"} ]'# → 201 {"success":true,"new_version":"…"}Add ?rebase=true to replay the ops onto current latest instead of
getting a 412 when your source is stale (the response then carries
rebased_onto). A failing operation — including a test mismatch —
returns 409 naming the op and path.
Three ergonomic layers over the same PATCH:
# One value:vsscli config set latest lines.1001.line-profile premium
# An overlay file: every leaf becomes an add/replace at its path;# lists are replaced atomically.vsscli config patch latest overlay.yaml --rebase
# A sub-tree from a file, spliced in at a selector:vsscli config push trunks.carrier-b new-trunk.yaml
# Or just open the (sub)document in $EDITOR and let the CLI push:vsscli config edit latestvsscli config edit latest ring-groups.supportAll of these create a normal new version — same validation, same
provenance, same 412/--rebase behavior.
Tags and descriptions
Section titled “Tags and descriptions”curl -s -X PUT "$VSS/v1/ns/$NSID/config/01JD…/tag" \ -H "Authorization: Bearer $TOKEN" -d '{"tag":"golden"}'curl -s -X PUT "$VSS/v1/ns/$NSID/config/01JD…/description" \ -H "Authorization: Bearer $TOKEN" -d '{"description":"post-migration soak, verified 7d"}'curl -s -X DELETE "$VSS/v1/ns/$NSID/config/01JD…/tag" \ -H "Authorization: Bearer $TOKEN"Re-tagging with a name that exists moves the tag (it’s unique per
namespace) — that’s the point of golden.
vsscli config tag set running golden # move golden to what's live nowvsscli config tag listvsscli config tag clear 01JD…vsscli config describe golden "post-migration soak, verified 7d"Applying
Section titled “Applying”curl -s -X POST "$VSS/v1/ns/$NSID/config/latest/apply" \ -H "Authorization: Bearer $TOKEN"# → {"success":true,"running_version":"…","previous_version":"…"}The path token may be a ULID, latest, running (re-render repair), or a
tag. ?force=true bypasses the descendancy and schema-version guards —
reserve it for deliberate reverts and post-upgrade re-applies.
vsscli config apply latestvsscli config apply golden # tags work anywhere a version doesvsscli config apply running # re-render repair (no version change)vsscli config apply 01JD… --force(Or fold it into the write: config push --apply, config edit offers to
apply after a successful push.)
Watch the two guard refusals: not-descendant-of-running means the
version you’re promoting isn’t downstream of what’s live (stale branch or
deliberate revert — see below), and schema-version-mismatch appears
after platform upgrades (re-store the document, or --force after
review). A failed render rolls back automatically: the previous running
version is re-rendered and reported, and your namespace keeps processing
calls on the old config.
Reverting
Section titled “Reverting”Roll-forward is the recommended shape — it keeps history linear and keeps
latest matching what’s live:
# 1. Take the known-good version's document…curl -s "$VSS/v1/ns/$NSID/config/golden" \ -H "Authorization: Bearer $TOKEN" -o rollback.yaml# 2. …store it as a NEW version based on current latest…BASE=$(curl -sI "$VSS/v1/ns/$NSID/config/latest" \ -H "Authorization: Bearer $TOKEN" | awk -F'"' '/^ETag/{print $2}')NEW=$(curl -s -X PUT "$VSS/v1/ns/$NSID/config" \ -H "Authorization: Bearer $TOKEN" -H "If-Match: \"$BASE\"" \ --data-binary @rollback.yaml | jq -r .new_version)# 3. …and promote it (no force needed: it descends from running).curl -s -X POST "$VSS/v1/ns/$NSID/config/$NEW/apply" \ -H "Authorization: Bearer $TOKEN"vsscli config get golden --out rollback.yamlvsscli config push rollback.yaml --apply --description "revert to golden"In a hurry, apply <old-version> --force reverts directly — but leaves
latest ahead of running, so follow up with the roll-forward push once
the incident is over (anyone applying latest later would otherwise
silently re-deploy what you just reverted).
Troubleshooting
Section titled “Troubleshooting”| Response | Meaning | Do |
|---|---|---|
412 on PUT/PATCH |
Your base is no longer latest — another writer landed |
Re-fetch latest, reconcile, retry (or --rebase) |
400 with validation_errors |
Document failed compilation | Fix every listed error; re-validate |
409 on PATCH |
An operation failed to apply (test mismatch, missing path) |
Inspect the named op/path; re-fetch the source |
| apply refused: not a descendant | Promoting a version outside running’s lineage | Roll forward instead, or force for a deliberate revert |
| apply refused: schema version | Version pre-dates a platform schema upgrade | Re-store the same YAML (re-compiles under the new schema), then apply |
| apply failed, previous version restored | Render failed; automatic rollback ran | Read the error, fix the config or conflict, re-apply |
One habit prevents most of these: fetch → edit → push against the
fetched version — never write from memory, and never script around the
precondition with base=latest.