Read a plan¶
nerthus-config plan prints the ordered changes that would make the estate match its declaration in
Nerthus.Infra, and does nothing else: it writes no state file, asks no provider to change anything,
and cuts no branch. apply carries out a plan rather than a comparison of its own, and the hourly
drift job runs a plan whose declaration is the recorded state: one computation, three callers. This
page is what a plan says and what each answer obliges you to do.
nerthus-config plan ../Nerthus.Infra # against the state/ tree
nerthus-config plan ../Nerthus.Infra --against live # against the providers
nerthus-config plan ../Nerthus.Infra --against recorded --recorded live.yaml
nerthus-config plan ../Nerthus.Infra --only cloudflare_dns_record
nerthus-config plan ../Nerthus.Infra --only 'cloudflare_*' # every kind the prefix names
nerthus-config --json plan ../Nerthus.Infra
Three exit codes¶
A pipeline branches on them, so they are three answers and not two:
| Code | Meaning |
|---|---|
0 |
nothing to do. The populations are printed beside it, because "agreed" and "nothing was compared" must not read the same. |
2 |
something to do. The rows say what, in dependency order. |
1 |
something is not known, or the command refused: a resource the current side could not read, a change no apply may carry out, a declaration validate refuses, a kind nobody declared. |
Six actions¶
| Action | When |
|---|---|
create |
declared, and the current side says it does not exist |
update |
it exists and an editable attribute differs |
replace |
an attribute the provider fixes at creation differs, so it is deleted and made again |
delete |
it exists and nobody declares it; the row offers import beside it, because a resource nobody declared is either adopted or removed |
noop |
it exists and every declared attribute agrees |
unknown |
the current side could not say. A plan carrying one refuses to apply. |
The generated resource reference (docs/resources.md in Nerthus.Config) marks which attributes are
immutable and which references are ordering edges (needs) rather than assignments (names).
A change may also be blocked: not a seventh action, but a sentence saying nothing this tool runs can carry it out. A host is adopted and never created, a site and an instance are Platform's, and a mesh peer is written by a converge. The row names the route that does bring such a resource into existence, and the plan refuses to apply.
--against live¶
A provider is registered only when this machine holds its credential, read from the instance's sops
store; a resource whose provider is not registered reads unknown, saying so, and the command
exits 1. An arm that reported agreement because it asked nobody would be the one answer a plan must
never give.
nerthus secrets set CLOUDFLARE_API_TOKEN --file - # the value on stdin
nerthus-config plan ../Nerthus.Infra --against live --only 'cloudflare_*'
The live arm also asks each provider what it holds, so the plan's population is the declaration,
plus what state/ records, plus what a listing enumerates. A resource in neither of the first two
appears as a delete row with the import route beside it. A listing that was refused contributes
no ids and prints a note naming the kind: a 403 is unknown, never an empty estate.
The GitLab group and the Cloudflare account every listing enumerates are in estate.yaml at the
declaration repository's root. NERTHUS_CONFIG_GITLAB_GROUP and NERTHUS_CONFIG_CLOUDFLARE_ACCOUNT
override them where set. NERTHUS_CONFIG_FIXTURES points every provider at a recorded exchange file
instead of the network, and NERTHUS_CONFIG_RECORD writes one from a real session.
A recorded live view¶
--against recorded reads a capture of a provider read, so a plan can be recomputed without
touching anybody's API. One row per resource, status being present, absent or unknown:
format: 1
resources:
- kind: cloudflare_tunnel
id: dev-hub
status: present
declared: { host: nerthus-host-1, hostname: dev.nerthus.pl }
observed: { tunnel_id: 11111111-1111-4111-8111-111111111111 }
- kind: cloudflare_dns_record
id: "CNAME:stary.nerthus.pl"
status: absent
- kind: gitlab_project
id: margonem/nerthus/it/nerthus.config
status: unknown
reason: the API answered 403
A sensitive attribute is dropped on the way in, because a capture is a file a person can edit and
state holds no secret; a status the format does not define is refused. A view that says nothing
about a resource does not say it is absent: the plan reads it as unknown, because a create
proposed on the strength of a question nobody asked is how an estate acquires a second copy of
something.
A recorded exchange file¶
What NERTHUS_CONFIG_FIXTURES names and NERTHUS_CONFIG_RECORD writes: one row per HTTP round trip,
matched on method, path and query. It is what makes the suite run without a network and what turns a
real session into a fixture.
{
"format": 1,
"exchanges": [
{
"request": {"method": "GET", "path": "/zones", "query": [["per_page", "100"]], "body": null},
"response": {"status": 200, "retry_after": null, "body": {"success": true, "result": []}},
"repeatable": true
}
]
}
An exchange answers the first call that matches it and is then used up, so a create and the read
after it can answer the same path differently. One marked repeatable is never used up: that is how
a fixture describes a world rather than a script. A recorder always writes repeatable: false; only
a person turning a recording into a fixture may claim a call is idempotent.
A request the file does not hold is a transport failure naming it, never a 404 and never an empty
result. The recorder refuses to write an exchange carrying the credential's own bytes, wherever
in the document they sit; it refuses rather than masks, because a masked fixture looks like a real
answer.