§ Reference · Policies
Writing policies
The four rules for writing a plan policy, each with a captured run, plan-wide rules, and the typos that forbid nothing.
On this page
A policy is a CUE value that the plan document must unify with. The usual shape is a list template over changes: a struct that every element of the list must unify with.
changes: [...{
addr: string
mode: string
action: string
// guards and constraints that read these fields
}]
CUE resolves a name by lexical scope. A guard reads only what the template declares: mode: string is what makes mode visible to the if below it. Without the declaration the file does not compile, and the run is refused at load.
| Rule | Use | Failure it avoids |
|---|---|---|
| Prohibit with a guard | An if on identity fields and error(). |
A rule that cannot say what is wrong. |
| Require with a constraint | A constraint on after. |
A value that is absent, null or unknown passes. |
| Do not prohibit a value with an equality guard | Write the allowed set as a constraint. | An unknown value passes. |
| Read an absent field only after a presence test | Declare the field optional and test it. | A refusal for the wrong reason. |
Work in an empty directory. The root is infra, and the policies are in policy. The commands print the plan before the verdict. This page shows the verdict.
package infra
import (
"pravo.dev/pravo"
rnd "pravo.dev/providers/hashicorp/random"
)
$random: rnd.#Provider & {$version: "~> 3.6"}
seed: $random.#Resource.random_id & {byte_length: 4}
pet: $random.#Resource.random_pet & {
length: 2
keepers: owner: "platform"
}
ticket: $random.#Resource.random_integer & {
min: 1
max: 99
keepers: source: pet.id
}
name: pravo.#Output & {value: pet.id}
Apply complete! Resources: 3 added, 0 changed, 0 destroyed. [txn <id>]
Prohibit with a guard
The identity fields (addr, mode, type, name, action) are always present, so a guard on them is reliable. Declare each field the guard reads, and name the change in the message.
// A managed resource is neither deleted nor replaced.
changes: [...{
addr: string
mode: string
action: string
if mode == "managed" && (action == "delete" || action == "replace") {
error("\(addr): \(action) is not allowed")
}
}]
Change length of the pet from 2 to 3 and byte_length of the seed from 4 to 5. The plan holds a replacement of all three resources, because ticket reads the pet:
Plan: 3 to add, 0 to change, 3 to destroy.
Graph: planned 3 of 3, 3 refreshed.
Outputs:
name: (known after apply)
Error: the plan violates the policy ../policy/no-destroy.cue (sha256:0c23a31e78bd):
changes.0: seed: replace is not allowed:
../policy/no-destroy.cue:7:3
changes.1: pet: replace is not allowed:
../policy/no-destroy.cue:7:3
changes.2: ticket: replace is not allowed:
../policy/no-destroy.cue:7:3
Each message is the one the policy gave to error(), with the address of the change. The path before it, changes.1, is the index of the change in the document's list. The plan is printed above these lines in full, so what is judged is what the reader saw.
Under --json the document is printed on standard output, and then the error object, so that a program can read both:
{"code":5,"kind":"error","message":"the plan violates the policy ../policy/no-destroy.cue (sha256:0c23a31e78bd):\nchanges.0: seed: replace is not allowed:\n ../policy/no-destroy.cue:7:3\nchanges.1: pet: replace is not allowed:\n ../policy/no-destroy.cue:7:3\nchanges.2: ticket: replace is not allowed:\n ../policy/no-destroy.cue:7:3\n"}
With --out, a refused plan writes no plan file:
refused.plan does not exist
Require with a constraint
A requirement is a constraint on a value the plan must have. A value that is absent, null or unknown until apply does not satisfy it. This policy requires an owner key on every pet that is created:
// Every random_pet that is created carries an owner.
changes: [...{
type: string
action: string
if type == "random_pet" && action == "create" {
after: keepers: owner: string & !=""
}
}]
The guard (if type == "random_pet" && action == "create") says which changes the requirement governs. The constraint (string & !="") says what they must have. Five plans follow, each with a different keepers line in the pet block. The state is empty first, so the plans create the pet.
Destroy complete! Resources: 0 added, 0 changed, 3 destroyed. [txn <id>]
| Case | In main.cue |
Verdict |
|---|---|---|
| present | keepers: owner: "platform" |
satisfied |
| absent | keepers: team: "storage" |
refused: the key owner is not there |
| null | no keepers line |
refused: keepers is null |
| unknown | keepers: owner: seed.hex |
refused: the value is {"$unknown": true} |
| empty string | keepers: owner: "" |
refused: !="" excludes it |
Present:
Policy ../policy/owner.cue (sha256:bf44cd1fce1f) is satisfied.
Absent:
Error: the plan violates the policy ../policy/owner.cue (sha256:bf44cd1fce1f):
changes.1.after.keepers.owner: incomplete value !="":
../policy/owner.cue:6:35
Null:
Error: the plan violates the policy ../policy/owner.cue (sha256:bf44cd1fce1f):
changes.1.after.keepers: conflicting values null and {owner:(string & !="")} (mismatched types null and struct):
../policy/owner.cue:5:2
../policy/owner.cue:6:19
plan:1:N
Unknown:
Error: the plan violates the policy ../policy/owner.cue (sha256:bf44cd1fce1f):
changes.1.after.keepers.owner: conflicting values {$unknown:true} and string (mismatched types struct and string):
../policy/owner.cue:5:2
../policy/owner.cue:6:26
plan:1:N
Empty string:
Error: the plan violates the policy ../policy/owner.cue (sha256:bf44cd1fce1f):
changes.1.after.keepers.owner: invalid value "" (out of bound !=""):
../policy/owner.cue:6:35
../policy/owner.cue:6:26
plan:1:N
A policy that requires a value is not satisfied by leaving it out, by a provider default that is null, or by a value that is not known until apply. string alone is satisfied by the empty string. Require string & !="" for a value that must say something.
Where an unset attribute is acceptable, write the alternative: null | <constraint>. This policy lets a created pet either set no prefix or set a lowercase one:
// A random_pet that is created either sets no prefix or sets a lowercase one.
changes: [...{
type: string
action: string
if type == "random_pet" && action == "create" {
after: prefix: null | =~"^[a-z]+$"
}
}]
prefix in the root |
Verdict |
|---|---|
| no line (null) | satisfied |
"app" |
satisfied |
"App" |
refused |
seed.hex (unknown) |
refused |
Error: the plan violates the policy ../policy/prefix.cue (sha256:f773a753cb48):
changes.1.after.prefix: 2 errors in empty disjunction:
changes.1.after.prefix: conflicting values "App" and null (mismatched types string and null):
../policy/prefix.cue:5:2
../policy/prefix.cue:6:18
plan:1:N
changes.1.after.prefix: invalid value "App" (out of bound =~"^[a-z]+$"):
../policy/prefix.cue:6:25
../policy/prefix.cue:6:18
plan:1:N
A violation of this form prints 2 errors in empty disjunction, one error for each alternative. An unknown value fails both, as it must: whether it is null or lowercase cannot be said before the apply.
Do not prohibit a value with an equality guard
An unknown value is not equal to anything, so a guard that compares it with the forbidden value finds it unequal, and the plan passes. These two policies state the same rule. The first prohibits a dot as the separator with a guard. The second allows - and _ and nothing else.
// Wrong: a prohibition written as an == guard.
changes: [...{
addr: string
type: string
after?: {...}
if type == "random_pet" && after != _|_ {
if after.separator == "." {
error("\(addr): the separator must not be a dot")
}
}
}]
// Right: the allowed set is written as a constraint.
changes: [...{
type: string
if type == "random_pet" {
after?: separator: "-" | "_"
}
}]
separator |
guard | constraint |
|---|---|---|
"." |
refused | refused |
"-" |
satisfied | satisfied |
seed.hex |
satisfied | refused |
With the separator ".", both refuse:
Error: the plan violates the policy ../policy/separator-guard.cue (sha256:4e31fa112b22):
changes.1: pet: the separator must not be a dot:
../policy/separator-guard.cue:8:4
Error: the plan violates the policy ../policy/separator-set.cue (sha256:769f5b89f838):
changes.1.after.separator: 2 errors in empty disjunction:
changes.1.after.separator: conflicting values "-" and ".":
../policy/separator-set.cue:4:2
../policy/separator-set.cue:5:22
plan:1:N
changes.1.after.separator: conflicting values "_" and ".":
../policy/separator-set.cue:4:2
../policy/separator-set.cue:5:28
plan:1:N
With seed.hex, which nobody can read yet, the guard lets the plan through and the constraint refuses it:
Policy ../policy/separator-guard.cue (sha256:4e31fa112b22) is satisfied.
Error: the plan violates the policy ../policy/separator-set.cue (sha256:769f5b89f838):
changes.1.after.separator: 2 errors in empty disjunction:
changes.1.after.separator: conflicting values {$unknown:true} and "-" (mismatched types struct and string):
../policy/separator-set.cue:4:2
../policy/separator-set.cue:5:22
plan:1:N
changes.1.after.separator: conflicting values {$unknown:true} and "_" (mismatched types struct and string):
../policy/separator-set.cue:4:2
../policy/separator-set.cue:5:28
plan:1:N
Write the allowed set as a constraint, and an unknown value is refused until it is known.
Read an absent field only after a presence test
Reading a field that may be absent is an error that CUE reports as cannot reference optional field, and it refuses the plan for the wrong reason. Declare the field optional and test it first.
// Wrong: before is read without a presence test.
changes: [...{
addr: string
before?: {id: string}
if before.id == "desired-mosquito" {
error("\(addr): this object may not be touched")
}
}]
// Right: before is read only after a presence test.
changes: [...{
addr: string
before?: {id: string}
if before != _|_ {
if before.id == "desired-mosquito" {
error("\(addr): this object may not be touched")
}
}
}]
Against a plan whose changes are creates, which have no before:
Error: the plan violates the policy ../policy/before-bare.cue (sha256:73ae9a966458):
changes.0: cannot reference optional field: before:
../policy/before-bare.cue:5:5
changes.1: cannot reference optional field: before:
../policy/before-bare.cue:5:5
changes.2: cannot reference optional field: before:
../policy/before-bare.cue:5:5
Policy ../policy/before-tested.cue (sha256:c54ae37ee36f) is satisfied.
The first policy refuses every plan that has a change without a before, which is nearly every plan. The second reads before.id only when before exists.
An omitted boolean needs a default in the template, for the same reason: drifted is not in the document when it is false.
// Wrong: drifted is omitted from the document when it is false.
changes: [...{
addr: string
drifted?: bool
if drifted {
error("\(addr): has drifted")
}
}]
// Right: an omitted boolean has a default in the template.
changes: [...{
addr: string
drifted: *false | bool
if drifted {
error("\(addr): has drifted")
}
}]
Error: the plan violates the policy ../policy/drift-bare.cue (sha256:9b3387b6279a):
changes.0: cannot reference optional field: drifted:
../policy/drift-bare.cue:5:5
changes.1: cannot reference optional field: drifted:
../policy/drift-bare.cue:5:5
changes.2: cannot reference optional field: drifted:
../policy/drift-bare.cue:5:5
Policy ../policy/drift-default.cue (sha256:f9c436a2933d) is satisfied.
The default *false | bool supplies false where the field is omitted and takes the document's true where it is there. The same holds for deferred, skipped, refreshed, drifted and applied.
Plan-wide rules
summary and stats hold counts, and a bound on them limits a plan as a whole:
// A plan touches at most two nodes.
summary: blast_radius: <=2
Error: the plan violates the policy ../policy/limit.cue (sha256:b8d1f7b6cd67):
summary.blast_radius: invalid value 3 (out of bound <=2):
../policy/limit.cue:2:24
plan:1:N
blast_radius is read from the state at the moment of the plan. A limit binds one plan: twenty plans of five changes each pass blast_radius: <=5. It is a brake on the size of one change set and not a budget.
Rules over action, mode, type and the counts are what the plan policy is for, and so is a rule that an attribute is set, such as the owner above. A rule about the value that an attribute may take can also be written on the provider definition: see Providers. A specification constraint cannot require that an attribute is set.
Mistakes that forbid nothing
CUE does not know what a policy was meant to say. Four typos, each run against a plan that destroys everything:
// The value is misspelt: no change has the action "delet".
changes: [...{action: !="delet"}]
// The field is misspelt: the document has no "chnges".
chnges: [...{action: !="delete"}]
// The field is misspelt: the document has no "sumary".
sumary: blast_radius: <=5
// The field is misspelt: the document has no "destory", and the value is concrete.
summary: destory: 0
Apply complete! Resources: 3 added, 0 changed, 0 destroyed. [txn <id>]
typo-literal
Policy ../policy/typo-literal.cue (sha256:e7cd66e0dfc0) is satisfied.
typo-template
Policy ../policy/typo-template.cue (sha256:167504bf1469) is satisfied.
typo-constraint
Error: the plan violates the policy ../policy/typo-constraint.cue (sha256:af32e6414ce4):
sumary.blast_radius: incomplete value <=5:
../policy/typo-constraint.cue:2:23
typo-concrete
Policy ../policy/typo-concrete.cue (sha256:c1ca4d6eaec1) is satisfied.
- A misspelt literal (
"delet") forbids nothing, because no change has that action. - A misspelt list template (
chnges) forbids nothing, because the document has no such field and the list is empty. - A misspelt field with a concrete value (
destory: 0) forbids nothing. It adds a field that holds. - A misspelt bound, type or pattern (
sumary: blast_radius: <=5) refuses every plan, because the document has no such field and the bound never becomes a value.
Only the last of the four is noticed. Test a policy against a plan it should refuse before relying on it. See Rolling out and testing.
See also
- The plan document in a policy says what absent, null and unknown mean.
- Operating policies covers where each command judges a policy.
- Write and roll out plan policies tests a policy on fixtures.