§ Reference · Language

Environments

$environments and $environment: the environments one specification is applied to, the rows of what differs, $after and $default, the checks at load, the gate of a following environment and the record each state keeps.

View as Markdown

On this page

Quick reference

Name Where Meaning
$environments A top-level field. A struct with one field for each environment: the key is its name, the value its row.
a field of a row Your own. What differs between the environments: a size, a region, a domain.
$after A row. The environment whose release this one takes, or a list of them. Without it, the environment takes any release.
$default A row. true on the environment that a command without --env uses. At most one row has it.
$environment Filled in by pravo; declare it for your editor (Typing the rows). The row of the selected environment, with $name, its name.
--env NAME Every command that evaluates the specification. Selects the environment. It is the tag env: -t env=NAME does the same.
env.cue
$environments: pravo.#Environments & {
	dev: {workers: 1, $default: true}
	staging: {workers: 2, $after: "dev"}
	prod: {workers: 3, $after: "staging"}
}

$settings: pravo.#Settings & {state: path: "state/\($environment.$name)"}

server: workers: $environment.workers

pravo.#Environments checks the shape of the struct: a row is a struct, $after is a name or a list of names, $default is a boolean. Your fields are open. To give every row a field with a default, write a pattern: $environments: [_]: {replicas: *1 | int}.

Typing the rows

pravo fills $environment while it loads, in a file that is not on disk, so an editor sees no declaration of it. Declare it yourself, with a definition of the row that $environments uses too. The editor then knows $environment and its fields, and a row that misses a field or gives it a value of the wrong type is refused:

#Row: pravo.#Environment & {
	workers: int & >=1
	region:  string
}

$environments: [string]: #Row
$environments: pravo.#Environments & {
	dev: {workers: 1, region: "eu-north-1", $default: true}
	prod: {workers: 3, region: "eu-north-1", $after: "dev"}
}

$environment: #Row

pravo.#Environment brings $after, $default and $name. pravo reads #Row and $environment with the rows, before the imports resolve, as it reads $settings, so they may use pravo. definitions and your own, and nothing else that comes from an import.

Selecting an environment

--env NAME selects a row. Before the specification is evaluated, pravo fills $environment with that row and with $name. Every other field reads the row from there. Without --env, the row marked $default: true is used; a specification whose rows have no default stops and lists them. --env NAME is the tag env, so -t env=NAME selects the same row, and a command line where the two name different environments is refused.

env is not a tag of the specification. pravo takes it off the tags it gives CUE, and a field that carries @tag(env) is refused when $environments is declared, so that the environment has one source. The other tags (-t image=1.4) work as in Tags and variables.

A name is letters, digits, ., - and _, starts with a letter or a digit, and has at most 63 characters, so that it goes into a path, a bucket prefix and a command line without quoting. Quote a name with a dash in CUE: "prod-eu": {...}.

pravo keeps no current environment. Each command evaluates the specification with the tags it is given, so plan, apply, destroy, output and the state commands each take --env NAME. A saved plan or a stored plan carries the tags it was made with.

What each environment has of its own

$settings is read once for each environment, so a setting written with $environment.$name differs between them.

Setting Use
state A state of its own: path: "state/\($environment.$name)" or prefix: "app/\($environment.$name)". Two environments with one state are refused.
state.profile The AWS profile that the bucket of this environment is opened with.
state.token_env The environment variable that holds the token of an HTTP state server.
access.dir The directory of the readers file, so that a writer of staging is not a writer of prod.

The names of a profile and of a variable are settings; a credential is not. Settings describes each one.

A pravo.#RemoteOutputs reads the state of another root. Write $environment.$name into its location, and each environment reads the outputs of the same environment there: state: path: "../network/state/\($environment.$name)".

A discovery runs in the environment that --env names and keeps its findings in that environment's state. pravo discover adopt writes the resource that adopts an object inside a condition on $environment.$name.

The promotion flow

$after orders the environments into a promotion flow. An environment may follow several, and several may follow one:

main.cue
package estate

import "pravo.dev/pravo"

$environments: pravo.#Environments & {
	dev: {zone: "a", $default: true}
	staging: {zone: "b", $after: "dev"}
	"prod-eu": {zone: "c", $after: "staging"}
	"prod-us": {zone: "d", $after: "staging"}
	audit: {zone: "e", $after: ["prod-eu", "prod-us"]}
}

$settings: pravo.#Settings & {state: path: "state/\($environment.$name)"}
terminal
$pravo environments | head -n 1
dev → staging → {prod-eu, prod-us} → audit

audit takes a release only when both prod-eu and prod-us run it.

Checks

Every command that loads the specification checks the declaration first. It refuses:

  • a $environments that is not a struct of structs, or that declares no environment;
  • a name that is not valid;
  • an $after that names the environment itself, names an environment that is not declared, names one twice, or is an empty list;
  • environments that follow one another in a circle;
  • two rows with $default: true;
  • two environments whose state is in one place;
  • a --env NAME that names no environment, and a field that carries @tag(env).

pravo validate without --env evaluates the specification once for each environment and says which ones are valid. It also refuses a row that misses a field that another row has, and a field of a row that has no concrete value, which a plan of that environment would otherwise stop at. pravo validate --env NAME checks one.

main.cue
package estate

import "pravo.dev/pravo"

$environments: pravo.#Environments & {
	dev: {zone: "a", $default: true}
	staging: {zone: "b", $after: "dev"}
	"prod-eu": {zone: "c", $after: "stagign"}
}

$settings: pravo.#Settings & {state: path: "state/\($environment.$name)"}
terminalexit 6
$pravo validate
Error: $environments.prod-eu.$after names "stagign", which is not an environment (they are dev, staging and prod-eu)

The gate

An environment with $after takes only a release that was promoted into it, once its state is sealed. The release is the specification of the directory, named by its fingerprint, and the values of its tags other than env. pravo promote signs a promotion into the state of the environment, carrying the signed notes that the environments before it run the release, and pravo apply checks it against the readers file of that state and each note against the readers file of the environment it comes from. pravo promote lists the checks, and Promote a release from one environment to the next runs them.

A state that is not sealed takes any release: it has no signatures to check. The plan and the apply of such an environment say so, on a line that starts with Environment:. An environment without $after takes any release.

The record of an environment

Each state of an environment keeps a record of its place in the promotion flow, as a chain of signed versions below environment/: the environment it serves, the environments it follows, and the lineage of each of their states. The first apply writes it.

An apply is refused with exit code 5 when:

  • the record names another environment: the settings point the run at a state of another environment;
  • the record says that the environment follows others than the specification says: an edit of $after, which would add or drop a gate.

A plan is refused for the same reasons, and so is every other command that writes the records or the state: import, state rm, state mv, state import-tfstate, discover, and secret set, rm and purge.

The refusal names the command that makes the record agree, pravo environments record --env NAME, which an admin of the state runs (pravo environments). A writer adds the lineage of an environment before when the first promotion reads it, and nothing else.

Each apply also writes a note, below runs/, that says which release the state runs, the git revision it was applied from, and who applied it. pravo environments reads the newest note of each state, and pravo promote carries the notes of the environments before into a promotion. In a sealed state the record, the notes and the promotions are signed and checked against the readers file. State format lists the objects.