§ Learn · Track B · Labs and project

Lab 1: break it

Cause ten failures one at a time and read each message. Match each failure to the exit code that a pipeline sees.

Time
30 min
Level
intermediate
Needs
B10
You will learn
Which stage of a run produces which failure? Which exit code does each one give?
Done when
You have caused a failure with each of the exit codes 1 to 6. You can say which stage produced it.

View as Markdown

On this page

A pipeline reads exit codes. A person reads messages. This lab produces both, one failure at a time, on a root that works.

Each fault is declared before it runs. A badge says what to expect. conflict marks a value that disagrees. rejected marks a specification that is refused before any provider starts. Each fault ends with a revert, so the root is whole again before the next one.

The root that works

Work in an empty directory. The root lives in root, a policy lives in policy, and every command names the root with -C.

root/main.cue
package main

import (
	loc "pravo.dev/providers/hashicorp/local"
	rnd "pravo.dev/providers/hashicorp/random"
)

$local:  loc.#Provider
$random: rnd.#Provider

env: *"dev" | "prod" @tag(env)

release: $random.#Resource.random_integer & {min: 1, max: 99, seed: "lab"}

manifest: $local.#Resource.local_file & {
	filename: "manifest-\(env).txt"
	content:  "release \(release.result)\n"
}
policy/no-delete.cue
// A plan may create and change. It may not delete.
changes: [...{
	addr:   string
	mode:   string
	action: string
	if mode == "managed" && action == "delete" {
		error("\(addr): delete is not allowed")
	}
}]
terminal
$pravo -C root init
$pravo -C root apply --auto-approve
Configuration does not load yet; generating schemas for the declared providers first.
Created cue.mod/module.cue
Providers:
  local = hashicorp/local v2.9.1
  random = hashicorp/random v3.9.1
...
pravo has been initialised. Run "pravo plan" to see what it would do.
...
Apply complete! Resources: 2 added, 0 changed, 0 destroyed. [txn <txn>]

Every file in root that ends in .cue is part of one package. The faults below are files that you add to it, so that the revert is deleting the file.

Before any provider starts

These faults are found while the specification is evaluated. Every command that loads the specification reports the same error with exit code 6.

Fault 1: two files disagree

A second file sets min to a different number. The block carries status=conflict.

root/fault.cueconflict
package main

release: min: 5
terminalexit 6
$pravo -C root validate
Error: evaluating ./root:
release.min: conflicting values 1 and 5:
    ./root/fault.cue:3:15
    ./root/main.cue:13:51

Read it in four parts. The path is release.min. The values are 1 and 5. The rule is that two concrete values must be equal. The locations are the two files. See A3.

Fault 2: a misspelt field

The next file misspells filename. The block carries status=rejected.

root/fault.cuerejected
package main

manifest: filenam: "x"
terminalexit 6
$pravo -C root validate
Error: evaluating ./root:
manifest.filenam: field not allowed:
    ./root/fault.cue:3:11

The provider's definition is closed, so the field is refused. The only location is the one you typed. See A8.

Fault 3: the wrong type

root/fault.cuerejected
package main

release: max: "ninety"
terminalexit 6
$pravo -C root validate
Error: evaluating ./root:
release.max: conflicting values "ninety" and 99 (mismatched types string and int):
    ./root/fault.cue:3:15
    ./root/main.cue:13:59

The message names both values and says why they cannot meet: mismatched types string and int.

Fault 4: a reference to nothing

root/fault.cuerejected
package main

extra: $local.#Resource.local_file & {filename: "extra.txt", content: relase.result}
terminalexit 6
$pravo -C root validate
Error: evaluating ./root:
extra.content: reference "relase" not found:
    ./root/fault.cue:3:71

relase is not a field in scope. The message names the path that holds the reference and the word that failed to resolve. Revert the file.

terminal
$rm root/fault.cue

Fault 5: a tag outside the set

No file changes this time. The env tag accepts "dev" or "prod".

terminalexit 6
$pravo -C root validate -t env=qa
Error: evaluating ./root:
env: 2 errors in empty disjunction:
env: conflicting values "dev" and "qa":
    ./root/main.cue:11:7
env: conflicting values "prod" and "qa":
    ./root/main.cue:11:15

CUE tries each alternative and lists every one that failed: qa is neither dev nor prod. The same specification run through plan gives the same text and the same code.

terminalexit 6
$pravo -C root plan -t env=qa
Error: evaluating ./root:
env: 2 errors in empty disjunction:
env: conflicting values "dev" and "qa":
    ./root/main.cue:11:7
env: conflicting values "prod" and "qa":
    ./root/main.cue:11:15

Every command that loads the specification reports the error in this form, with exit code 6. A job needs no special case for the command that found it.

After a plan exists

These faults need a plan, a state and a world. The root is valid in every one of them.

Code 6 is used above. Code 1 comes last.

Predict Match exit codes 2, 3, 4 and 5 to a stale plan, a policy refusal, a reserved resource and drift.

A stale plan exits with 3. A reserved resource exits with 4. A plan refused by a policy exits with 5. Drift exits with 2.

Fault 6: a stale plan

Add a valid resource and save a plan.

root/extra.cue
package main

extra: $local.#Resource.local_file & {
	filename: "extra-\(env).txt"
	content:  "extra \(release.result)\n"
}
terminal
$pravo -C root plan --out change.plan
...
  # extra (local_file) will be created
  + extra: {
  +     content:              "extra 51\n"
...
    }

Plan: 1 to add, 0 to change, 0 to destroy.
Graph: planned 1 of 3, 2 unchanged and skipped, 1 refreshed, 1 not read.

Saved the plan to change.plan. To apply exactly these actions, run:
  pravo apply change.plan

Edit the file after the plan was saved. A comment is enough.

terminal
$echo "// edited" >> root/extra.cue
terminalexit 3
$pravo -C root apply change.plan
Error: saved plan is stale; run "pravo plan" again:
  - the configuration changed since the plan was made

Exit code 3 is a saved plan that does not match its files. Revert by planning again. This time apply the specification, so that extra exists.

terminal
$pravo -C root apply --auto-approve
...
Plan: 1 to add, 0 to change, 0 to destroy.
Graph: planned 1 of 3, 2 unchanged and skipped, 1 refreshed, 1 not read.
...
Apply complete! Resources: 1 added, 0 changed, 0 destroyed. [txn <txn>]

Fault 7: a plan that a policy refuses

Delete extra.cue. The specification is valid, and the plan now deletes extra.

terminal
$rm root/extra.cue
terminalexit 5
$pravo -C root plan --policy ../policy/no-delete.cue
...
  # extra (local_file) will be destroyed
  - extra: {
...
    }

Plan: 0 to add, 0 to change, 1 to destroy.
Graph: planned 0 of 2, 2 unchanged and skipped, 1 removed from the specification, 1 refreshed, 2 not read.

Error: the plan violates the policy ../policy/no-delete.cue (sha256:a1757b6a34e5):
changes.2: extra: delete is not allowed:
    ../policy/no-delete.cue:7:3

Exit code 5 is a policy. Nothing was applied. Revert by writing the file again.

root/extra.cue
package main

extra: $local.#Resource.local_file & {
	filename: "extra-\(env).txt"
	content:  "extra \(release.result)\n"
}

Fault 8: a resource that someone else reserved

Add one more resource. Ana plans it and reserves it for fifteen minutes. A reservation is a claim in the state, and it needs no running process.

root/late.cue
package main

late: $local.#Resource.local_file & {
	filename: "late-\(env).txt"
	content:  "late\n"
}
terminal
$pravo -C root plan --reserve 15m --actor ana
...
  # late (local_file) will be created
  + late: {
  +     content:              "late\n"
...
    }

Plan: 1 to add, 0 to change, 0 to destroy.
Graph: planned 1 of 4, 3 unchanged and skipped, 3 not read.
Reserved 1 node until <time>.

pravo state locks lists the claim.

terminal
$pravo -C root state locks
late  reserved by ana until <time>: pravo -C root plan --reserve 15m --actor ana

Bob tries to apply. The actor label is not verified, and it is how reservations tell people apart.

terminalexit 4
$pravo -C root apply --auto-approve --actor bob
...
Error: plan rejected: late is reserved by ana (pravo -C root plan --reserve 15m --actor ana until <time>, since <time>)

Exit code 4 is a resource that another actor holds. Ana releases the claim, and Bob applies.

terminal
$pravo -C root state unlock late
$pravo -C root apply --auto-approve --actor bob
Unlocked late
...
Apply complete! Resources: 1 added, 0 changed, 0 destroyed. [txn <txn>]

Fault 9: a file that someone deleted

Delete a file that pravo wrote, and ask for drift.

terminal
$rm root/manifest-dev.txt
terminalexit 2
$pravo -C root drift
Drift. 1 of 4 resources changed outside pravo:
  manifest: create
    - refresh found the real state differs from the state (drift)

Exit code 2 is drift. Revert by applying the specification.

terminal
$pravo -C root apply --auto-approve
$pravo -C root drift
...
manifest: no longer exists; it will be created again
...
Apply complete! Resources: 1 added, 0 changed, 0 destroyed. [txn <txn>]
No drift. 4 resources refreshed; 0 pending changes from the specification.

Fault 10: an output that was never recorded

The last fault is none of the above. Ask for an output that the state does not hold.

terminalexit 1
$pravo -C root output nosuch
Error: output "nosuch" is not recorded in state (run "pravo apply")

Exit code 1 is any failure that has no code of its own. Examples are a missing output, an unknown flag and a provider error. Read the message.

What you caused

Exit Fault Stage Command
1 10 Any other failure. Every command
6 1 to 5 The specification does not evaluate. validate, plan, apply and every other command that loads it
2 9 The world differs from the state. drift
3 6 The plan does not match its files. apply <planfile>
4 8 Another actor holds the resource. plan, apply
5 7 A policy refused the plan. plan, apply, destroy

Faults 1 to 5 stop before any provider starts. Faults 6 to 10 need a plan, a state or a world. The reference lists every exit code: Exit codes.

Remove what the lab created.

terminal
$pravo -C root destroy --auto-approve | tail -n 1
Destroy complete! Resources: 0 added, 0 changed, 4 destroyed. [txn <txn>]

Recap

  • A specification that does not evaluate exits with 6, whichever command loads it. Any other failure exits with 1.
  • Codes 2, 3, 4 and 5 tell a pipeline what to do: report drift, plan again, wait or stop.
  • Every message names a path, the values, the rule and the places in your files.

Check yourself

Which exit code tells a job to plan again?

Exit code 3. The saved plan does not match the files or the state it was made from.

What does exit code 4 mean?

Another actor holds the resource, by a running command or by a reservation.

Lab 2 runs two applies against one state.