§ Concepts
Plans
What a plan contains, how pravo decides which resources a plan reaches, what an unknown value is, how refresh age works and how a saved plan is checked.
On this page
A plan is the list of actions that make the real infrastructure match the specification. pravo computes it from the evaluated specification, the records in the state and the answers of the providers.
A plan changes nothing in the infrastructure. It can write to the state: refreshed records, moved records and imported records. Everything else stays as it was.
This page uses six small roots: zones, deferred, ages, reads, target and feed. The global flag -C DIR runs pravo in one of them.
What a plan contains
Each part of a printed plan answers one question.
| Part | Meaning |
|---|---|
# ADDRESS (TYPE) will be created |
The header of a change: the address of the resource, its provider type and the action in words. |
+, ~, -/+, -, <= |
The action as a glyph. |
| Attribute lines | The planned values. OLD -> NEW is a change, (known after apply) is an unknown value, (sensitive) is a hidden one. |
# forces replacement |
The attribute that the provider cannot change in place. |
Plan: N to add, N to change, N to destroy. |
The counts. A replacement counts as one add and one destroy. |
Hint: |
A create and a delete that look like one object under two addresses. |
Graph: |
How much of the graph the plan walked. |
Outputs: |
The outputs, with (known after apply) for the ones that are not known yet. |
The action of a change is one of these.
| Action | Glyph | When |
|---|---|---|
| create | + |
The address has no record. |
| update | ~ |
The planned state differs from the record, and the provider changes it in place. |
| replace | -/+ |
The provider reports an attribute that cannot change in place, or -replace names the resource. |
| delete | - |
A record has no address in the specification, or the plan is -destroy. |
| read | <= |
A data source whose read waits for the apply. |
| no-op | none | The planned state equals the record. The plan does not print it. |
| move | none | A $from renamed a record. The plan prints # OLD was moved to NEW. |
Work in an empty directory. The first root has two resources: a number that the provider draws, and a resource that records it.
package estate
import (
"pravo.dev/pravo"
rnd "pravo.dev/providers/hashicorp/random"
nul "pravo.dev/providers/hashicorp/null:null_"
)
$settings: pravo.#Settings & {refresh: "1h"}
$random: rnd.#Provider
$null: nul.#Provider
package estate
zones: ["a"]
ports: {
for z in zones {
"\(z)": $random.#Resource.random_integer & {min: 20000, max: 29999, seed: z}
}
}
deploy: {
for z in zones {
"\(z)": $null.#Resource.null_resource & {
triggers: {
zone: z
port: "\(ports[z].result)"
}
}
}
}
pravo has been initialised. Run "pravo plan" to see what it would do.
pravo will perform the following actions:
# ports.a (random_integer) will be created
+ ports: a: {
+ id: (known after apply)
+ max: 29999
+ min: 20000
+ result: (known after apply)
+ seed: "a"
}
# deploy.a (null_resource) will be created
+ deploy: a: {
+ id: (known after apply)
+ triggers: {
+ port: (known after apply)
+ zone: "a"
}
}
Plan: 2 to add, 0 to change, 0 to destroy.
Graph: planned 2 of 2.
deploy.a reads ports.a, so port is unknown until ports.a exists. Attribute values print in CUE syntax and are nested by address.
The plan document
pravo plan --json prints the same plan as one JSON document. A policy is judged against this document. It has these top-level fields.
| Field | Content |
|---|---|
summary |
add, change, destroy, read and blast_radius. |
stats |
The numbers behind the Graph: line. |
changes |
One entry for each node the plan ran. --include-skipped adds an entry for each node it left alone. |
outputs |
The planned outputs. |
hints, providers |
The hints, and the provider version of each provider. |
format, created, actor, reason, git_revision, trace_id, destroy |
The context of the run. |
A change always has addr, mode, type, name and action. When they apply, it also has provider, reasons, requires_replace, before, after, deferred, skipped, refreshed, drifted, applied, dependencies, dependents and warnings. A value that is not known until apply is {"$unknown": true}.
The plan ran the nodes it changes, deletes, moves or imports, the ephemeral resources it opens and the functions it calls. A node it left alone has no entry. pravo plan --json --include-skipped lists it too, as an entry with addr, mode, type, name, the action no-op and skipped. A policy is judged against a document that has an entry for every node of the specification, in the order of the specification, with these fields for the nodes the plan left alone.
{
"addr": "deploy.a",
"mode": "managed",
"type": "null_resource",
"name": "a",
"action": "create",
"provider": "null",
"reasons": [
"not in the state: a new resource"
],
"after": {
"id": {
"$unknown": true
},
"triggers": {
"port": {
"$unknown": true
},
"zone": "a"
}
},
"dependencies": [
"ports.a"
]
}
The document is not masked. A sensitive attribute appears in it as it is. Secrets and ephemeral values says what that means.
Apply complete! Resources: 2 added, 0 changed, 0 destroyed. [txn <id>]
Unknown values
pravo converts the body of a resource into a value that the provider understands before it asks the provider anything. It classifies each attribute of the body.
An attribute that refers to a resource, a data source or a call is unknown, and so is an incomplete expression over one, such as strings.ToUpper(a.id). An attribute that is not concrete and refers to nothing else is null, and the provider picks the value. An attribute that is not concrete for any other reason is an error, because no provider can resolve it. A tag without a value is the usual cause.
A reference is found structurally. It counts inside an interpolation, a function call, an expression and a list. "\(ports[z].result)" is a reference to ports.a, so port was unknown in the plan above.
package estate
env: string @tag(env)
tagged: $null.#Resource.null_resource & {
triggers: stage: "x-\(env)"
}
Error: tagged.triggers["stage"]: value depends on env, which is not concrete (is a -t tag missing?)
When pravo plans or applies a resource, it fills the known values back into the value at the address of the resource. Only where the specification was not concrete does the planned value replace it. Every expression that reads the resource is then evaluated again, so it becomes concrete when all that it reads is known. A value that the provider marks unknown stays non-concrete.
Data sources
pravo reads a data source during the plan when two things hold. Its configuration is fully known, and none of the resources it depends on has a pending change. Otherwise the read waits for the apply. The plan shows it as a read with an unknown result, and every resource that depends on it shows an unknown value too.
package estate
import loc "pravo.dev/providers/hashicorp/local"
$local: loc.#Provider
report: $local.#Resource.local_file & {
filename: "report.txt"
content: "quarterly\n"
}
copy: $local.#Data.local_file & {filename: report.filename}
pravo has been initialised. Run "pravo plan" to see what it would do.
# report (local_file) will be created
# copy (local_file) will be read (configuration depends on values known after apply)
<= copy: (known after apply)
Plan: 1 to add, 0 to change, 0 to destroy.
copy reads report.filename, and report has a pending create. The read waits.
A data source whose read returned what its record holds is not a change. The apply does not write it, takes no lock for it and leaves its revision as it is. A resource that reads it gets the value of the record. A saved plan that reads the data source stays current while nothing writes it. When the refresh age of the data source is a duration, the plan records the time of the read and keeps the revision, so the next plan within the age does not read it again. With the age always, the default, the plan reads the data source every time. With never it does not read it until the configuration changes. Neither records the time.
Scoped plans
A plan does not plan every resource, and it does not read every record. Each node in the state records the hash of the configuration that the provider last saw. It also records the provider version, the schema version and the time of the last refresh. A plan converts the body of each resource, with the values of the resources that ran before it, and compares.
A resource is skipped when all of these hold.
- The converted configuration is fully known, and its hash equals the hash of the record.
- The provider version, the provider instance and the schema version are the ones that wrote the record.
- The record has no mark. A read has not found that the object differs from the record or is gone, and the last apply of the resource finished.
A skipped resource is not validated and not planned. Its record stands in for the planned state, and the resources that depend on it see its attributes as known. Every other resource is reached. It goes to its provider.
What a plan reads follows what it reaches. A plan looks at the resources it reaches and at the resources they read, which are their inputs. It reads the record of each from its provider when the record is older than its refresh age. A resource that is neither reached nor an input is not upgraded, not read, not locked and not written, and no provider process starts for it, whatever the age of its record. Refresh age says when a record that the plan looks at is read, and which records a plan reads whatever their position.
A plan that has nothing to do starts no provider. A provider configuration or a credential that is wrong shows in the first plan or apply that reaches a resource of that provider, not in a plan with no changes. pravo plan -refresh=all starts every provider, so it finds such a fault.
The work of a plan follows the change. A change to one resource reaches that resource and the resources whose configuration changed because of it. Two costs follow the size of the estate: pravo evaluates every resource of the specification to find out which ones changed, and it opens every record of the state. State says what this means for a large estate. Take eight zones of the first root. The resources number sixteen.
package estate
zones: ["a", "b", "c", "d", "e", "f", "g", "h"]
ports: {
for z in zones {
"\(z)": $random.#Resource.random_integer & {min: 20000, max: *29999 | int, seed: z}
}
}
deploy: {
for z in zones {
"\(z)": $null.#Resource.null_resource & {
triggers: {
zone: z
port: "\(ports[z].result)"
}
}
}
}
Plan: 14 to add, 0 to change, 0 to destroy.
Graph: planned 14 of 16, 2 unchanged and skipped, 2 within refresh age.
Fourteen resources are new, so the plan reaches fourteen. ports.a and deploy.a are within their refresh age and skipped.
Apply complete! Resources: 14 added, 0 changed, 0 destroyed. [txn <id>]
Now change one number. max has a default, and a second file sets it for one zone.
package estate
ports: c: max: 29998
pravo will perform the following actions:
# ports.c (random_integer) must be replaced
-/+ ports: c: {
~ id: "28969" -> (known after apply)
~ max: 29999 -> 29998 # forces replacement
min: 20000
~ result: 28969 -> (known after apply)
seed: "c"
}
# deploy.c (null_resource) must be replaced
-/+ deploy: c: {
~ id: "<id>" -> (known after apply)
~ triggers: {
~ port: "28969" -> (known after apply)
zone: "c"
} # forces replacement
}
Plan: 2 to add, 0 to change, 2 to destroy.
Graph: planned 2 of 16, 14 unchanged and skipped, 16 within refresh age.
The provider cannot change max in place, so ports.c is replaced. A replacement makes the computed attributes of the resource unknown again, so deploy.c, which reads ports.c, is reached and replaced too. Fourteen resources are not.
The Graph: line reports what happened.
| Part | Counts |
|---|---|
planned N of M |
The resources and data sources that were reached, of all that the specification declares. |
N unchanged and skipped |
The ones that were not reached. |
N removed from the specification |
The records of resources that the specification does not declare and that the plan deletes. They are not among the M. |
N refreshed |
The ones whose real state was read from the provider during the run. |
N drifted |
The refreshed ones that differed from the record. |
N within refresh age |
The ones that were not read and whose record is within its refresh age. |
N not read |
The ones that were not read and whose record is older than its refresh age. |
N ephemeral opened, N functions called, N imported, N moved |
Work of other kinds. |
Each resource and data source that has a record is in exactly one of refreshed, within refresh age and not read. The three add up to the number of records, which are those of the declarations and the removed ones, and drifted is a part of refreshed. A resource that has no record, because the plan creates it, is in none of them, and neither is an ephemeral resource or a call. A part with a count of zero is not printed, except planned. With -refresh=false the line has neither within refresh age nor not read, because nothing is read. pravo plan -full plans every resource with its provider.
A plan that reads an input of a reached resource counts the input as refreshed, so the counts are final when the plan ends and not before.
The sentence under a plan with no change says how much the plan can promise. No changes. Your infrastructure matches the configuration. is printed when not read is zero and the refresh is not false: every record was read, or is within its refresh age. Otherwise the plan prints No changes. The specification matches the state. and says how many records it did not read. pravo drift compares them with the infrastructure. With -refresh=false the plan says that nothing was read. Refresh age shows each sentence.
Plan: 2 to add, 0 to change, 2 to destroy.
Graph: planned 16 of 16, 16 within refresh age.
pravo why prints the reason of each change, and of each skip.
ports.c: replace
- the specification changed since the last apply (hash <old> -> <new>)
- refresh skipped: the record is within its refresh age
- the provider cannot change max in place; replacement required
reaches: deploy.c
deploy.c: replace
- inputs depend on values known only after apply: ports.c
- refresh skipped: the record is within its refresh age
- the provider cannot change triggers in place; replacement required
deploy.a: no-op
- skipped: nothing the provider saw has changed
- not read: the plan did not reach it; the record is <age> old, its refresh age is 1h
summary.blast_radius in the plan document counts the nodes that a change touches. They are the changed nodes and every node that depends on them, as the state records the edges.
A plan unifies the values of a node it did not reach into the configuration only when something reads them: another resource, the configuration of a provider or an output. A constraint added to a computed attribute of a node that nothing reads is checked when that node is next planned.
Requested replacements
pravo plan -replace ADDRESS destroys a resource and creates it again although nothing in the specification changed. The request is a change in the plan like any other, and it is reviewed like one. It can be repeated, and it cannot be combined with -destroy.
["replacement requested with -replace","refresh skipped: the record is within its refresh age"]
Refresh age
Refreshing means asking the provider for the real state of a resource before planning against it. It is the most expensive part of a plan. Each node records when the provider last reported its state, and a refresh age says how old that may be. A record is within its refresh age while its age is no greater than the refresh age, and older than it once its age is greater. A record whose age equals the refresh age is within it.
Reach decides what a plan looks at. The refresh age decides whether a record that the plan looks at is read.
The refresh age is a staleness budget. It does three things:
- It spares a repeated plan the read of a record that it looks at and that was read within the age.
- On a resource, as
$lifecycle.refresh, it is a standing promise: a plan or a sweep reads the record once it is older than that age, whether or not the plan reaches the resource. - In a sweep,
pravo drift --dueandpravo plan -refresh=expiredread the records that are past their age, the oldest first.
The saving of a plan that reads one resource of ten thousand comes from reach, not from the age.
Together they keep the calls to the providers in line with the change. A plan asks about the resources it plans and the resources they read, and does not ask again about a record that it read within its age. A change to one resource costs the reads of that resource and its inputs, whether the state holds ten records or ten thousand. The price is trust: a record that a plan does not read is taken to match its object. Choosing a refresh age says how long to trust it.
The refresh age of a resource is $lifecycle.refresh, else $settings.refresh, else "always".
| Value | Meaning |
|---|---|
"always" |
An age of zero. A plan that looks at the resource reads it. |
"never" |
No limit. A plan reads the resource only when asked. |
"30m", "6h", "7d" |
A duration. A plan that looks at the resource reads it when its record is older. |
A plan reads a record in these cases, and in no other.
| Record | Read when |
|---|---|
| A resource the plan reaches | Its record is older than its refresh age. |
| A resource that a reached resource reads | Its record is older than its refresh age. |
A resource that sets its own $lifecycle.refresh |
Its record is older than that age, reached or not. $settings.refresh is not an age of its own. A resource that sets "always" is read by every plan. |
| A data source | It is not read again when its configuration is unchanged, nothing it depends on has a pending change and its record is within its age. Otherwise it is read, reached or not. |
| A resource with a mark | It is reached, and read when its record is older than its refresh age. The mark stays until an apply writes the resource. |
| A resource with a stale lock, a record with no time of a read, or whose provider now points somewhere else | It is read whatever its age. See Records a plan reads whatever their age. |
Ephemeral resources, function calls and built-in values such as pravo.#Env run in every plan.
CUE rejects any other value when it evaluates the specification. The root below has four resources, each with its own age.
package estate
import (
"pravo.dev/pravo"
rnd "pravo.dev/providers/hashicorp/random"
loc "pravo.dev/providers/hashicorp/local"
)
$settings: pravo.#Settings & {refresh: "1h"}
$random: rnd.#Provider
$local: loc.#Provider
a: $random.#Resource.random_integer & {min: 1, max: 9, seed: "a", $lifecycle: refresh: "15m"}
b: $random.#Resource.random_integer & {min: 1, max: 9, seed: "b", $lifecycle: refresh: "never"}
c: $random.#Resource.random_integer & {min: 1, max: 9, seed: "c"}
note: $local.#Resource.local_file & {
filename: "note.txt"
content: "see \(a.result)\n"
$lifecycle: refresh: "always"
}
pravo has been initialised. Run "pravo plan" to see what it would do.
Apply complete! Resources: 4 added, 0 changed, 0 destroyed. [txn <id>]
Right after the apply, a plan reads only note. The other three records are within their ages.
note: Refreshing state... [id=<sha1>]
No changes. Your infrastructure matches the configuration.
Graph: planned 0 of 4, 4 unchanged and skipped, 1 refreshed, 3 within refresh age.
pravo why says what happened to each resource.
a: no-op
- skipped: nothing the provider saw has changed
- not read: the plan did not reach it; the record is <age> old, its refresh age is 15m
b: no-op
- skipped: nothing the provider saw has changed
- not read: the plan did not reach it; the record is <age> old, its refresh age is never
c: no-op
- skipped: nothing the provider saw has changed
- not read: the plan did not reach it; the record is <age> old, its refresh age is 1h
note: no-op
- refreshed because the record is older than its refresh age; no drift
- skipped: nothing the provider saw has changed
The flag -refresh overrides the ages for one run.
| Flag | Behaviour |
|---|---|
-refresh=age |
The default. A plan reads the records it looks at that are older than their refresh age. |
-refresh=expired |
A plan reads every record that is older than its refresh age, reached or not. Use it for a scheduled sweep. |
-refresh=all |
A plan reads every record, whatever its age. |
-refresh=false |
A plan reads no record and trusts them all. A mark is still honoured. The Graph: line has neither within refresh age nor not read. |
No changes. Your infrastructure matches the configuration.
Graph: planned 0 of 4, 4 unchanged and skipped, 4 refreshed.
No changes. The specification matches the state. Nothing was read from the infrastructure.
Graph: planned 0 of 4, 4 unchanged and skipped.
Error: invalid -refresh value "sometimes": want age, expired, false or all
A refresh that happens during a plan is written back to the record with a new refresh time, in a transaction. The revision moves only when the attributes differ. The apply that follows a plan does not read the provider again: it uses what the plan read. A refresh does not by itself cause a new plan: a resource whose refresh returns the attributes that are recorded is still skipped. A resource whose attributes changed, or whose object is gone, is drift. The read writes a mark on the record, drifted_at and, for an object that is gone, gone. The resource is planned by this plan and by every later plan, saved plan and apply, until an apply writes it. pravo drift ignores the ages, reads every resource and writes the same marks.
A change reads what it reaches
The root reads has twenty files and one more resource that carries a version. It sets no refresh age, so every record is due.
package estate
import (
"list"
loc "pravo.dev/providers/hashicorp/local"
)
$local: loc.#Provider
version: *"1" | string @tag(version)
files: {for i in list.Range(0, 20, 1) {
"f\(i)": $local.#Resource.local_file & {filename: "out/f\(i).txt", content: "item \(i)\n"}
}}
release: $local.#Resource.local_file & {filename: "out/release.txt", content: "version \(version)\n"}
pravo has been initialised. Run "pravo plan" to see what it would do.
Apply complete! Resources: 21 added, 0 changed, 0 destroyed. [txn <id>]
Change the version. The plan reaches release and reads it. No resource that it reaches reads one of the files, so it does not look at them.
release: Refreshing state... [id=<sha1>]
Plan: 1 to add, 0 to change, 1 to destroy.
Graph: planned 1 of 21, 20 unchanged and skipped, 1 refreshed, 20 not read.
-refresh=expired reads every record that is older than its refresh age. With the age always, that is every record. The two counts below are the lines Refreshing state of the plan above and of the plan with -refresh=expired.
1
21
A plan that changes nothing reads nothing. It says that it did not read the records, because it cannot promise that the infrastructure matches.
No changes. The specification matches the state. 21 records were not read; run pravo drift to compare them with the infrastructure.
Graph: planned 0 of 21, 21 unchanged and skipped, 21 not read.
pravo plan -detailed-exitcode exits 0 for this plan: the plan found no change among what it looked at. It does not detect drift in a resource that it did not read. The next section finds that drift. CI schedules it.
A record that a reached resource reads can differ from its object, or the object can be gone. The read writes the mark, and the plan walks again from the records as they are: the resource is reached now, and the records it reads are read in turn. A chain of resources that each read the one before takes one walk for each resource whose record differs, however long the chain is, and no record is read twice. The plan fails with NODE changed while the plan read it; run pravo plan again only when a record is found changed in a walk after it was found changed in an earlier one.
Drift in a resource the plan does not read
Delete one of the files, as someone with a shell does. A plan does not look at it.
exit 0
pravo drift reads every record and reports the difference. The read writes a mark on the record.
Drift. 1 of 21 resources changed outside pravo:
files.f7: create
- refresh found the real state differs from the state (drift)
// gone
The next plan acts on the mark. It reaches files.f7 and reads it again, and the other twenty are still not read. The apply that follows creates the file again, writes the record and ends the mark.
Plan: 1 to add, 0 to change, 0 to destroy.
Graph: planned 1 of 21, 20 unchanged and skipped, 1 refreshed, 1 drifted, 20 not read.
Apply complete! Resources: 1 added, 0 changed, 0 destroyed. [txn <id>]
A sweep of a large estate that reads every record can take long. pravo drift --due --limit N reads only the records that are older than their refresh age, the oldest first and at most N of them, and plans nothing. A job that runs it often covers the estate in turn, and each run costs what it reads. It exits 2 when it finds drift.
No drift. 5 of 21 records read; 16 more are older than their refresh age.
Choosing a refresh age
The refresh age is the time for which a plan trusts a record that it looks at. A longer age means fewer calls to the provider and a plan that waits for fewer answers. It also means that a change made outside pravo stays unseen for longer. Choose it for the object and not for the estate.
- A resource that only pravo changes can have a long age,
"6h"or"7d". Set it once with$settings: refresh. A plan reads such a record only when it reaches the resource or a resource that reads it, and only when the record is older than the age. - A resource that people or other systems change, and whose current state a plan must see, sets
$lifecycle: refreshon itself. The age on a resource is standing: every plan reads the record once it is older than that age, whether or not the plan reaches the resource."always"makes every plan read it. - The default,
"always", reads every record that a plan looks at, so a plan sees the current state of everything it plans. It also reads every data source on every plan, so an estate with many data sources sets an age.
Records that no plan looks at are read by a job that you schedule. pravo drift --due --limit N reads the records that are older than their age, the oldest first, and pravo plan -refresh=expired reads all of them while it plans. With a long age a job reads each record rarely, and with a short one often. CI shows the job.
Records a plan reads whatever their age
A plan reads three kinds of record that it does not reach, whatever their refresh age. The refresh age says how long a record may be trusted when nothing suggests it is wrong. These suggest it.
An apply stopped while it held the node. A command that is killed does not release its locks, and it may have changed the object before it stopped. The lock stays, and once its holder is gone or has not renewed it for ten minutes, the lock is stale. A plan that finds a stale lock on a node reads the node, and the read removes the lock. With -refresh=false the plan reads nothing and prints a warning for each such node. The lock below was left by an apply of alice; the root ages is the one above.
c: no-op
- an apply (txn T1, alice on host ci-1) stopped while this node was locked; its real state is read again whatever the refresh age
- skipped: nothing the provider saw has changed
No locks or reservations held.
The provider points somewhere else. A record carries a digest of the configuration of the provider instance that wrote it. The digest covers the attributes that say where the provider points, such as a region, an endpoint or a profile. It leaves out an attribute that is sensitive, one that is not known yet and one that is built from an ephemeral value, so a credential that rotates changes nothing. A plan reads the records whose digest differs from the digest of their provider now. It plans one only when the read finds drift. A record written without a digest is not read for this; its next write or read adds one, and a provider with no such attribute leaves none. The root target configures the tls provider with a proxy.
package estate
import (
"pravo.dev/pravo"
tls "pravo.dev/providers/hashicorp/tls"
)
$settings: pravo.#Settings & {refresh: "6h"}
$tls: tls.#Provider & {proxy: [{url: "http://proxy-a.example:3128", username: "ci", password: "one"}]}
key: $tls.#Resource.tls_private_key & {algorithm: "ECDSA"}
pravo has been initialised. Run "pravo plan" to see what it would do.
Apply complete! Resources: 1 added, 0 changed, 0 destroyed. [txn <id>]
true
A new password for the proxy changes nothing the digest covers, and the record is within its age.
package estate
import (
"pravo.dev/pravo"
tls "pravo.dev/providers/hashicorp/tls"
)
$settings: pravo.#Settings & {refresh: "6h"}
$tls: tls.#Provider & {proxy: [{url: "http://proxy-a.example:3128", username: "ci", password: "two"}]}
key: $tls.#Resource.tls_private_key & {algorithm: "ECDSA"}
No changes. Your infrastructure matches the configuration.
Graph: planned 0 of 1, 1 unchanged and skipped, 1 within refresh age.
Another proxy address does.
package estate
import (
"pravo.dev/pravo"
tls "pravo.dev/providers/hashicorp/tls"
)
$settings: pravo.#Settings & {refresh: "6h"}
$tls: tls.#Provider & {proxy: [{url: "http://proxy-b.example:3128", username: "ci", password: "two"}]}
key: $tls.#Resource.tls_private_key & {algorithm: "ECDSA"}
key: no-op
- the non-sensitive configuration of its provider changed
- skipped: nothing the provider saw has changed
Graph: planned 0 of 1, 1 unchanged and skipped, 1 within refresh age.
The first command read the record, found no drift and wrote the digest of the new configuration. The plan that follows has nothing to read.
An apply could not tell what it did to the node. A provider can fail without returning a state, and an apply can exit without waiting for a provider. Interrupting an apply describes both. The apply writes the record of the node again without refreshed_at. The next plan reads the node whatever its refresh age, also when the specification has gone back to what the record says and the plan does not reach the node. The read writes refreshed_at. With -refresh=false the plan reads nothing and prints a warning for each such node.
Saved plans
pravo plan -out FILE writes the plan to a file, and pravo apply FILE applies those actions without a prompt. The reviewed file is what runs.
The file records the change of every node the plan ran, with its configuration, its prior and its planned values, unknown values included. It records the private data of the providers and the refresh and skip decisions. It records the tags of the plan, the provider versions and a fingerprint of every .cue file of the root. It records the lineage of the state, the revision of every node that the plan changes and the revision of every record that those changes read. A node that the plan left alone has no entry. Values in it can be secrets. pravo creates the file with owner-only permissions.
The next plan is the pending change of ports.c and deploy.c.
Saved the plan to c.plan. To apply exactly these actions, run:
pravo apply c.plan
pravo show FILE prints a saved plan. It adds a header with the time, the command and the actor.
Saved plan created <time> by "pravo -C zones plan --out c.plan"
pravo will perform the following actions:
A plan can be stale
Before anything is applied, pravo checks the file against the root. Four things must match.
- The fingerprint of the
.cuefiles. Any edit, a comment included, changes it. - The lineage of the state.
- The versions of the providers.
- The revision of every node that the plan changes, and of every record that those changes read.
A difference makes the plan stale. The apply refuses with the reasons and exit code 3, and nothing changes.
Error: saved plan is stale; run "pravo plan" again:
- the configuration changed since the plan was made
Restore the file. The plan is current again.
package estate
ports: c: max: 29998
Applying part of a plan
pravo apply FILE -only ADDR,ADDR applies the named changes and marks them in the file. The rest waits for a later pravo apply FILE.
Each name must be a change in the file. A node that the plan left alone has no entry in the file, and naming it is an error. The selection must be closed under dependencies. Every pending change that a selected change depends on must be selected too, or already applied. So must a data source whose read returned something other than its record, because the change was planned from what it returned. A selected delete needs every pending delete that depends on it. When the selection is not closed, nothing runs, and the command prints the smallest closed selection.
Error: the selection is not closed under dependencies; it also needs ports.c
(the smallest closed selection is -only deploy.c,ports.c)
ports.c alone is closed.
Applying 1 of 2 remaining changes from c.plan.
ports.c: Destroying... [id=28969]
ports.c: Creating replacement...
ports.c: Replacement complete after 0s [id=21730]
Apply complete! Resources: 1 added, 0 changed, 1 destroyed. [txn <id>]
1 change remains in c.plan; apply with "pravo apply c.plan".
The apply wrote ports.c and left deploy.c. The file records which change is done.
# ports.c (random_integer) must be replaced (already applied in txn <id>)
# deploy.c (null_resource) must be replaced
During a staged apply, a resource outside the selection is neither planned nor touched. The selected resources see it through its record, which the closure rule makes enough. An output that reads a resource outside the selection, one that has no record yet, is not recorded by that apply. The apply that finishes the plan records it. pravo apply shows it. A saved destroy plan is staged in the same way: a part removes the outputs that read what it destroys, leaves the others, and the part that destroys the last resources removes the rest. See Outputs of a partial destroy.
Apply complete! Resources: 1 added, 0 changed, 1 destroyed. [txn <id>]
A plan can be stale through the state
The revision of a node changes whenever a command writes it. Plan the creation of a ninth zone, then let another apply create it first.
package estate
zones: ["a", "b", "c", "d", "e", "f", "g", "h", "i"]
ports: {
for z in zones {
"\(z)": $random.#Resource.random_integer & {min: 20000, max: *29999 | int, seed: z}
}
}
deploy: {
for z in zones {
"\(z)": $null.#Resource.null_resource & {
triggers: {
zone: z
port: "\(ports[z].result)"
}
}
}
}
Plan: 2 to add, 0 to change, 0 to destroy.
Graph: planned 2 of 18, 16 unchanged and skipped, 16 within refresh age.
Apply complete! Resources: 2 added, 0 changed, 0 destroyed. [txn <id>]
Error: saved plan is stale; run "pravo plan" again:
- ports.i changed in state (revision 1, plan expected 0)
- deploy.i changed in state (revision 1, plan expected 0)
The plan expected no record for ports.i and deploy.i. The other apply wrote them.
An apply that writes a node which the plan neither changes nor reads leaves the plan current. A record that a change reads is listed in the file with its revision, and so are the records that it reads in turn, because a field of a record can be an expression over them. When one of them was written since, the plan is stale, and the reason ends with the plan reads it.
A saved plan keeps what it read
A saved plan holds the answer that each data source gave when the plan was made. The apply uses it and does not read the source again. The root below copies a file that a data source reads.
package estate
import loc "pravo.dev/providers/hashicorp/local"
$local: loc.#Provider
src: $local.#Data.local_file & {filename: "feed.txt"}
copy: $local.#Resource.local_file & {
filename: "copy.txt"
content: src.content
}
pravo has been initialised. Run "pravo plan" to see what it would do.
Apply complete! Resources: 1 added, 0 changed, 0 destroyed. [txn <id>]
Change the file and save a plan. The plan reads two. Change the file again before the plan is applied.
Plan: 1 to add, 0 to change, 1 to destroy.
Graph: planned 2 of 2, 2 refreshed.
The apply writes what the plan read.
Apply complete! Resources: 1 added, 0 changed, 1 destroyed. [txn <id>]
two
The next plan reads the file again and finds three.
Plan: 1 to add, 0 to change, 1 to destroy.
Graph: planned 2 of 2, 2 refreshed.
What apply checks while it runs
Once an apply holds the locks of the nodes it writes, it reads the revision of each of them, and of each record it reads that the plan lists, from the state itself and not from what it loaded when it started. A revision that differs from the one in the plan means that another command wrote the node while the plan waited. A record that was removed and created again while the plan waited starts at revision 1 again, so the apply compares the generation of the record as well, and a record in another generation is refused whatever its revision. The apply stops with exit code 3 before it changes anything, whether the plan is a file or the plan that pravo apply printed. A saved plan lists the reasons as it does when it is loaded. A plain apply says that the plan changed while it waited and lists them. A record that the apply only reads is not locked, so a write to it after this check is not seen.
An apply plans each change of the plan again with the values that are known by then, as an apply always does. It compares the result with the change in the plan before it touches the resource. The action must be the same, or the resource has nothing left to do. A resource that has nothing left to do must already hold every value that the plan would have given it. Every value that the plan already knew must be unchanged. A provider that resolves an unknown is accepted. A provider that changes an answer it gave stops the apply at that resource, which is not touched. The resources the apply changed before it reached that one keep their changes. pravo apply without a file makes the same comparison with the plan it has just printed. A difference is exit code 3. The message of a plain apply says that the plan changed while it waited. The message of an apply of a file names the resource and what the provider planned instead.
A value that an expression builds from a container of resources is the same in the plan and in the apply. A comprehension over a container visits the resources in the order in which the specification declares them, whatever order the providers finish in.
A data source that the plan read is not read again. The apply uses the answer the plan holds, and the resources that read the source are applied with it, whether the source answers the same now or not. A later plan reads the source as its refresh age says.
The comparison runs as the apply reaches each resource, and resources that do not depend on each other are applied at the same time. When one resource differs, the apply stops. The resources it applied by then keep their changes, and the plan file is not marked. The next pravo apply FILE finds those resources written since the plan was made and is stale. Make a new plan.
While it runs, the apply renews its locks every two minutes, and before it asks a provider to change an object it renews them if more than five minutes have passed since it last knew that they were its own. A lock that another command took over meanwhile, which can happen after the computer was suspended and the lock went stale, stops the apply with lock lost and exit code 4. A renewal that fails for another reason, so that the locks cannot be confirmed, stops it with exit code 1. Either way the apply makes no further change, and the changes it made are recorded.
The apply walks the changes of the plan and the nodes they read, and no others. A node that a change reads is taken from its record, as the plan saw it. A node that no change reads is not planned again, not read and not checked: how far its record can be from the infrastructure is decided when the plan is made, by the reach of the plan and the refresh age.
What a saved plan does not do
A saved plan is not signed. pravo checks that the file still matches the root, the state and what the providers plan. It does not check who made it.
pravo apply FILE --policy POLICY judges the changes that remain in the file, all of them, before any provider is asked to change anything. The file records the command that made it. Nothing reads that line as a requirement. A policy file is not part of the fingerprint. Policies covers the rest.
Recap
- A plan is a list of actions with reasons. It is printed for people and available as JSON for programs.
- A value that depends on a resource that does not exist yet is unknown. The plan shows it as
(known after apply). - A plan reaches the resources that changed and the resources that read them. The rest is skipped, and the
Graph:line counts both. - A saved plan is checked against the files, the state and the providers, and then each resource is planned again.
Next: State describes the records that a plan compares with.