§ How-to · Operate

Share a plan before you apply it

Store a plan in the state so that a team can list it, read it, see who else plans the same resources and apply it by its id.

Time
15 min
Level
intermediate
Needs
A root that you can plan, and jq. B7 teaches saved plans. The last section uses the in-memory S3 server from pravo's source.
Done when
pravo plan list in one checkout shows the plan that another checkout stored, and pravo apply plan-ID applies it.

View as Markdown

On this page

A plan file belongs to the person who made it. Nobody else knows that it exists, and two people can plan the same resource without noticing. pravo plan --store writes the plan into the state instead. Anyone who can read the state can list the plan, read it and apply it.

This page follows two people, alice and bob. Each has a checkout of one repository, and the two checkouts share one state.

Warning

A stored plan holds the same values as a plan file, sensitive values included. Whoever can read the state can read its plans, as they can read its records. pravo serve withholds them, as it does for a plan file.

1. Share a state

Work in an empty directory. It holds the checkout of alice, the checkout of bob and the state they share. A team keeps the state in a bucket, as section 9 shows. A directory next to both checkouts stands in for it.

alice/main.cue
package demo

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

$settings: pravo.#Settings & {state: path: "../state"}

$random: rnd.#Provider

port: $random.#Resource.random_integer & {min: 20000, max: 29999, seed: "web"}

Bob's checkout is a copy of alice's. A plan applies only to the files it was made from, cue.mod included.

terminal
$pravo -C alice init > /dev/null
$cp -r alice bob

2. Store a plan

Alice makes a plan and stores it. --actor names her and --reason says why. pravo records both and does not verify them. Without --actor, the author of a plan is the operating system user.

terminal
$pravo -C alice --actor alice --reason "Reserve a port for the web server" plan --store | tail -n 4
Graph: planned 1 of 1.

Stored the plan as plan-<id>; unless it is applied it is removed after 7 days. To apply exactly these actions, run:
  pravo apply plan-<id>

The plan is the object that plan --out writes to a file. pravo writes it below plans/ in the state and prints its id: plan- and the first eight hexadecimal digits of its digest. Nothing is applied. A plan that creates, changes or destroys no resource is not stored: pravo apply applies it.

The id differs on every run. This page keeps it in a file, and $(cat alice.id) stands for the id that pravo printed.

terminal
$pravo -C alice plan list --json | jq -r '.plans[] | select(.author == "alice") | .id' > alice.id

3. Find the plan and read it

Bob lists the plans of the state.

terminal
$pravo -C bob plan list
plan-<id>  pending  alice  just now  +1 ~0 -0  expires in 7 days

The columns are the id, the status, the author and the age. Then come the counts of resources to add, change and destroy, and a note. pending means that the plan can be applied. The note says when it expires.

plan show prints the plan after a header. pravo show ID prints the same.

terminal
$pravo -C bob plan show $(cat alice.id) | head -n 7
Stored plan plan-<id> by alice, just now (pending)
  reason:  Reserve a port for the web server
  command: pravo -C alice --actor alice --reason Reserve a port for the web server plan --store
  expires in 7 days
  apply:   pravo apply plan-<id>

pravo will perform the following actions:

The header names the author, the age, the status and the reason. It adds the command that made the plan, its expiry and the command that applies it. Six hexadecimal digits are enough to find a plan, as in plan-2f9c1a, when no other plan starts with them. pravo serve lists the same plans in a browser and shows which resources they share: Explore an estate in a browser.

4. See who else plans the same resource

Bob wants a narrower range. He changes his checkout and stores a plan.

bob/main.cue
package demo

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

$settings: pravo.#Settings & {state: path: "../state"}

$random: rnd.#Provider

port: $random.#Resource.random_integer & {min: 20000, max: 29998, seed: "web"}
terminal
$pravo -C bob --actor bob --reason "Keep 29999 free" plan --store | tail -n 5
$pravo -C bob plan list --json | jq -r '.plans[] | select(.author == "bob") | .id' > bob.id
Graph: planned 1 of 1.

Warning: plan-<id> by alice (just now) also changes 1 of these resources: port
Stored the plan as plan-<id>; unless it is applied it is removed after 7 days. To apply exactly these actions, run:
  pravo apply plan-<id>

pravo compares the new plan with the stored plans that can still be applied. Alice's plan changes port too, so pravo warns bob. The warning is information. It does not stop bob and does not change alice's plan.

A plan that is not stored warns in the same way. It names every open plan that changes one of its resources.

terminal
$pravo -C bob plan | tail -n 2
Warning: plan-<id> by bob (just now) also changes 1 of these resources: port
Warning: plan-<id> by alice (just now) also changes 1 of these resources: port

--touching lists the plans that change one resource.

terminal
$pravo -C bob plan list --touching port
plan-<id>  pending  bob    just now  +1 ~0 -0  expires in 7 days
plan-<id>  pending  alice  just now  +1 ~0 -0  expires in 7 days

plan list judges a plan by the state alone. plan show and apply also compare it with the checkout and with the providers. Bob's checkout differs from the files alice planned from, and plan show says so.

terminal
$pravo -C bob plan show $(cat alice.id) | grep '^Warning'
Warning: this plan can no longer be applied: the configuration changed since the plan was made (main.cue changed)

Bob cannot apply alice's plan from this checkout. Alice can apply it from hers. The reason names the files that differ. When it names cue.mod/module.cue, the two checkouts have different module files, as they do when pravo init wrote the module in directories with different names: commit cue.mod/module.cue and use the same commit.

5. Apply by id

Alice applies her plan by its id. The checks are those of a plan file. pravo compares the configuration, the providers, the lineage of the state and the revision of every record the plan uses. A policy named with --policy judges the whole plan. --only applies a closed part.

terminal
$pravo -C alice apply $(cat alice.id)
port: Creating...
port: Creation complete after 0s [id=<id>]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed. [txn <id>]

The plan is now applied. Bob's plan assumed that port does not exist, so it is stale: pravo cannot apply it any more. A plan is stale as a whole, even when another part of it could still be applied. plan list shows the plans that can be applied, and --all adds the others.

terminal
$pravo -C bob plan list
$pravo -C bob plan list --all
No stored plan can be applied. "pravo plan list --all" lists the others.
plan-<id>  stale    bob    just now  +1 ~0 -0  port changed in state (revision 1, plan expected 0)
plan-<id>  applied  alice  just now  +1 ~0 -0  applied just now

An apply of a stale plan changes nothing and exits with code 3.

terminalexit 3
$pravo -C bob apply $(cat bob.id)
Error: saved plan is stale; run "pravo plan" again:
  - port changed in state (revision 1, plan expected 0)

Bob plans again against the new state.

6. Collect what nobody needs

pravo plan gc removes the plans that nobody will apply. The first collection that finds a plan stale marks it and removes nothing.

terminal
$pravo -C bob plan gc
1 plan can no longer be applied; a collection that runs 1 hour later removes it:
  plan-<id>  bob        stale, port changed in state (revision 1, plan expected 0)

A collection that runs more than an hour later removes the plan. Until then, a stale plan stays readable. A plan that loses its staleness loses the mark.

A plan is removed when Rule
It expires Seven days after it was made, or when --ttl says.
It is stale By the first collection that runs more than one hour after a collection saw it stale.
It was applied completely 24 hours after the last change was applied.
--older-than DURATION is given It was not applied and was made longer ago than DURATION.

A plan that an apply is running from is never removed. Nor is a plan that another collection is removing, or one that holds a reservation made with plan --reserve. pravo plan --store runs a small collection of its own.

7. Let a plan expire

A stored plan that nobody applies expires after seven days. --ttl sets another time: 90m, 36h or 7d. A short time shows what expiry does.

terminal
$pravo -C bob --actor bob plan --store --ttl 2s | tail -n 2
$sleep 3
$pravo -C bob plan list
$pravo -C bob plan list --all
Stored the plan as plan-<id>; unless it is applied it is removed after a moment. To apply exactly these actions, run:
  pravo apply plan-<id>
No stored plan can be applied. "pravo plan list --all" lists the others.
plan-<id>  expired  bob    just now  +1 ~0 -1  expired a moment ago
plan-<id>  stale    bob    just now  +1 ~0 -0  port changed in state (revision 1, plan expected 0)
plan-<id>  applied  alice  just now  +1 ~0 -0  applied just now

An expired plan is listed only with --all. apply refuses it with exit code 3, even before a collection has removed it. --dry-run says what a collection would remove and changes nothing, not even a mark.

terminal
$pravo -C bob plan gc --dry-run
$pravo -C bob plan gc
Would remove 1 stored plan:
  plan-<id>  bob        expired a moment ago
Removed 1 stored plan:
  plan-<id>  bob        expired a moment ago

8. Keep a plan longer, or take it with you

Bob stores a plan that he wants to keep for a day and a half.

terminal
$pravo -C bob --actor bob plan --store --ttl 36h | tail -n 2
$pravo -C bob plan list --json | jq -r '.plans[0].id' > keep.id
Stored the plan as plan-<id>; unless it is applied it is removed after 36 hours. To apply exactly these actions, run:
  pravo apply plan-<id>

plan pull writes a stored plan to a plan file. Send the file, attach it to a ticket or apply it elsewhere.

terminal
$pravo -C bob plan pull $(cat keep.id) bob.plan
$pravo -C bob show bob.plan | head -n 1
Wrote plan-<id> to bob.plan. To apply exactly these actions, run:
  pravo apply bob.plan
Saved plan created <time> by "pravo -C bob --actor bob plan --store --ttl 36h" (actor bob)

Applying the file records its progress in the file. The stored plan does not learn of it. Its status follows the state, which has moved, and it is stale.

terminal
$pravo -C bob apply bob.plan | tail -n 1
$pravo -C bob plan list --all | head -n 1
Apply complete! Resources: 1 added, 0 changed, 1 destroyed. [txn <id>]
plan-<id>  stale    bob    just now  +1 ~0 -1  port changed in state (revision 2, plan expected 1)

9. Use a bucket

The commands are the same when the state is in an S3 bucket. The objects below plans/ sit next to the records, under the same prefix. The commands below run against tools/fakes3, the in-memory S3 server in pravo's source, on port 9000. Keep the state in a shared bucket sets up a real bucket.

team/main.cue
package team

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

$settings: pravo.#Settings & {
	state: pravo.#S3State & {
		backend:    "s3"
		bucket:     "pravo-state"
		prefix:     "demo/team"
		region:     "us-east-1"
		endpoint:   "http://127.0.0.1:9000"
		path_style: true
	}
}

$random: rnd.#Provider

port: $random.#Resource.random_integer & {min: 20000, max: 29999, seed: "web"}
terminal
$pravo -C team init > /dev/null
$pravo -C team --actor alice plan --store | tail -n 2
$pravo -C team plan list
$pravo -C team state pull copy
$ls team/copy/plans
Stored the plan as plan-<id>; unless it is applied it is removed after 7 days. To apply exactly these actions, run:
  pravo apply plan-<id>
plan-<id>  pending  alice  just now  +1 ~0 -0  expires in 7 days
Copied 3 objects from s3://pravo-state/demo/team to copy
plan-<id>.meta.json
plan-<id>.plan.json

state pull and state push copy the plans with the records. Storing, listing and collecting plans needs the permissions that the state already needs: get, put, delete and list on the objects of the bucket. pravo creates the plan and its summary with If-None-Match: *, and replaces the progress object and deletes objects with If-Match. S3-compatible stores says how to test that a store honours them.

What next