§ A4 · Track A · CUE from zero

Order does not matter

Lines and files can come in any order. The result is the same, so there is no precedence list to learn.

Time
8 min
Level
beginner
Needs
A3
You will learn
Does the order of lines or files change the result?
Done when
pravo eval prints the same data for the estate in three file orders.

View as Markdown

On this page

In HCL, where a value is read decides which value wins. These two commands differ only in the order of their arguments.

tofu plan -var-file=base.tfvars -var-file=prod.tfvars   # prod.tfvars wins where both set a name
tofu plan -var-file=prod.tfvars -var-file=base.tfvars   # base.tfvars wins

OpenTofu reads variable values from several sources and keeps the last one it reads. The order, from first to last:

  1. TF_VAR_ environment variables.
  2. terraform.tfvars, then terraform.tfvars.json.
  3. *.auto.tfvars and *.auto.tfvars.json files, in file-name order.
  4. -var and -var-file options, in the order given.

Override files add a second order for configuration.

CUE has no precedence list. Every line and every file adds constraints, and unification gives the same result in any order. The files of one directory that start with the same package line form one package. pravo evaluates the package as a whole.

Warm-up

From earlier lessons

From A1: how do you write a struct with one field, tags holding owner, on one line?

tags: owner: "web". A struct with one field folds into a path.

From A2: is >=1 a concrete value?

No. It allows every number from 1 upward. A concrete value allows exactly one.

Start from the estate

Work in the directory from A3. If you start here, write these two files.

estate.cue
package estate

buckets: {
	logs: {
		name:           "logs"
		versioning:     true // keep old versions
		encrypted:      true
		retention_days: 365
		tags: owner: "platform"
	}
	uploads: {
		name:           "uploads"
		versioning:     false
		encrypted:      true
		retention_days: 30
		tags: owner: "web"
	}
}

rules: {
	https: {
		port:     443
		protocol: "tcp"
	}
	ssh: {
		port:     22
		protocol: "tcp"
	}
}
team.cue
package estate

buckets: uploads: tags: cost_center: "4711"

buckets: uploads: tags: env: "prod"

Save the output. You compare against it later.

terminal
$pravo eval > before.txt

Swap two lines

Add a new field to uploads in a scratch file. One line gives it a type and one line gives it a value.

scratch.cue
package estate

buckets: uploads: class: string
buckets: uploads: class: "standard"
terminal
$pravo eval -e buckets.uploads.class
"standard"

Swap the two lines.

scratch.cue
package estate

buckets: uploads: class: "standard"
buckets: uploads: class: string
terminal
$pravo eval -e buckets.uploads.class
"standard"

The value is the same. In HCL, swapping two assignments to one name changes which one wins. In CUE, a type and a value that agree give the value in either order.

Note

If the two lines name different fields, the data is the same in both orders. pravo eval may print the fields in the order you wrote them. The printed order is not part of the value.

Remove the scratch file.

terminal
$rm scratch.cue

Rename a file

Files behave like lines. pravo reads the files of a directory in the order of their names. Rename estate.cue so that it sorts after team.cue, and compare the output with the output you saved.

terminal
$mv estate.cue z-estate.cue
$pravo eval | diff before.txt - && echo same
same

Put the name back.

terminal
$mv z-estate.cue estate.cue

Two files that name the same field still have to agree. Order cannot make a conflict disappear.

Predict

You write retention_days three times: with 365, with >=90 and with int. The three lines can appear in six orders. Decide before you open the answer.

Predict How many different values do the six orders give?

One. Every order gives 365. Writing a field several times is the same as joining the values with &. The six expressions below are the six orders of the three lines.

terminal
$for e in '365 & >=90 & int' '365 & int & >=90' '>=90 & 365 & int' '>=90 & int & 365' 'int & 365 & >=90' 'int & >=90 & 365'; do pravo eval -e "$e"; done
365
365
365
365
365
365

Break it

A file that names another package cannot join the estate. Write prod.cue with package prod, as if the package were named after the file.

prod.cuerejected
package prod

buckets: logs: retention_days: >=365
terminalexit 6
$pravo eval
Error: loading .:
prod.cue is package prod; the other files are package estate

Read the error from left to right.

  • Stage. loading says that pravo could not assemble the files into one package. It reported the error before it evaluated any value.
  • Message. prod.cue is package prod; the other files are package estate names the file, its package and the package of the other files. A directory holds one package.
  • Location. The problem is the package line of a whole file, so there is no line number.

Change the first line of prod.cue to package estate.

prod.cuevalid
package estate

buckets: logs: retention_days: >=365
terminal
$pravo eval | diff before.txt - && echo same
same

The third file adds a bound that logs already meets, so the data is unchanged.

The opposite mistake is reported too. Delete the first line of prod.cue.

prod.cuerejected
buckets: logs: retention_days: >=365
terminalexit 6
$pravo eval
Error: loading .:
prod.cue has no package clause; the other files are package estate

A file without a package line cannot join the estate. pravo names the file and stops before it evaluates a value. Remove the file.

terminal
$rm prod.cue

Exercise

Exercise

Split the estate into three files

Split estate.cue into buckets.cue and rules.cue. With team.cue you have three files. Delete estate.cue. The output must not change, whatever the order of the files.

Save the output before you split.

terminal
$pravo eval > before.txt

The check copies the three files into a scratch directory in three different orders and compares each output with before.txt.

check.sh
[ ! -f estate.cue ] || { echo "estate.cue still exists"; exit 1; }
set -- *.cue
[ $# -eq 3 ] || { echo "expected three .cue files, found $#"; exit 1; }
tmp=$(mktemp -d)
for order in "$1 $2 $3" "$3 $1 $2" "$2 $3 $1"; do
  rm -rf "$tmp/d"; mkdir "$tmp/d"; n=0
  for f in $order; do n=$((n + 1)); cp "$f" "$tmp/d/$n.cue"; done
  if ! pravo -C "$tmp/d" eval | diff before.txt - > /dev/null; then
    echo "different output for the order: $order"; rm -rf "$tmp"; exit 1
  fi
done
rm -rf "$tmp"
echo ok

Run the check on the start state. It fails.

terminalexit 1
$sh check.sh
estate.cue still exists
Hint 1

Each new file starts with package estate. Move the buckets field into buckets.cue and the rules field into rules.cue.

Hint 2

The files of a package can hold any fields in any order. See package.

Solution
buckets.cue
package estate

buckets: {
	logs: {
		name:           "logs"
		versioning:     true // keep old versions
		encrypted:      true
		retention_days: 365
		tags: owner: "platform"
	}
	uploads: {
		name:           "uploads"
		versioning:     false
		encrypted:      true
		retention_days: 30
		tags: owner: "web"
	}
}
rules.cue
package estate

rules: {
	https: {
		port:     443
		protocol: "tcp"
	}
	ssh: {
		port:     22
		protocol: "tcp"
	}
}

Recap

  • Lines and files can come in any order. Unification gives the same value, so there is no precedence list.
  • The files of a directory that start with the same package line form one package. A directory holds one package.
  • Order can change the order of the printed fields. It cannot change a value.
  • A file without a package line, or with another one, is an error that names the file. Start every file with the same package line.

Check yourself

You rename a file so that it loads last. Does any value change?

No. pravo reads the files in name order, and unification does not depend on the order.

What does prod.cue is package prod; the other files are package estate mean?

prod.cue starts with a different package line than the other files. Give every file the same package name.

A file has no package line and breaks a bound. What does pravo eval print?

An error that names the file: prod.cue has no package clause; the other files are package estate. The bound is not checked until the file has the package line.

The next lesson turns a rule into a value: a range, or text of a given shape.