§ A3 · Track A · CUE from zero
Writing a field twice
Writing a field twice adds detail. The copies must agree, and a disagreement is an error, not an override.
On this page
A bucket module declares encrypted with a default of true. A caller sets it to false while debugging an upload failure. OpenTofu uses the caller's value.
module "logs" {
source = "../../modules/bucket"
name = "logs"
encrypted = false
}
// estate.cue
buckets: logs: encrypted: true
// team.cue
buckets: logs: encrypted: false
The module declares variable "encrypted" { default = true }, and the caller's line replaces it. The same rule appears across HCL.
merge({ encrypted = true }, { encrypted = false })returns a map in whichencryptedisfalse.- A variable that is set in
terraform.tfvarsand again with-vartakes the value of the last source. - A
validationblock can refusefalse, but only when the module author wrote one.
In HCL the last writer wins. CUE has no last writer. A field is a constraint, and writing it twice adds a second constraint. The value must satisfy both. That is unification. When no value can satisfy both, you have a conflict, and pravo stops. You caused one in Start, step 8. This lesson shows why it happens.
The widget below applies the rule to a type and a range. Pick a value to write against them. The four programs under it are the ones that pravo eval ran for this page.
A value can also be a type. Writing a field twice adds detail. A conflict is an error.
package main
size: int & >=1 & <=10
size: 3
size: 3
package main
size: int & >=1 & <=10
size: 0
Error: evaluating .:
size: invalid value 0 (out of bound >=1):
./main.cue:3:13
./main.cue:4:7
package main
size: int & >=1 & <=10
size: 11
Error: evaluating .:
size: invalid value 11 (out of bound <=10):
./main.cue:3:19
./main.cue:4:7
package main
size: int & >=1 & <=10
size: "3"
Error: evaluating .:
size: conflicting values int and "3" (mismatched types int and string):
./main.cue:3:7
./main.cue:4:7
Printed by pravo eval (pravo 0.1.0-dev) when the page was built.
Copies that agree
Work in the directory from A2. If you start here, write this file.
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"
}
}
A second file can say more about the same fields. Both files start with package estate, so pravo reads them as one specification. A4 explains how. Write team.cue.
package estate
// The same value, said again.
buckets: logs: encrypted: true
// A bound that the value already meets.
buckets: logs: retention_days: >=90
// A new field in a struct that already exists.
buckets: logs: tags: cost_center: "4711"
Pitfall
Start every file with package estate. A file without a package line, or with another one, stops the load, and the error names the file. A4 shows the case.
Ask for the logs bucket.
name: "logs"
versioning: true
encrypted: true
retention_days: 365
tags: {
cost_center: "4711"
owner: "platform"
}
Each pair of copies became one value.
encryptedappears twice with the same value. The result is that value.retention_daysis365in one file and>=90in the other. The value meets the bound, so the result is365.tagsis a struct in one file and a struct in the other. The result is one struct with the fields of both.
In each case the result is the more specific copy, or the details of both. Nothing was replaced.
The operator behind it
Writing a field twice is the same as joining its values with &. The value int & 3 is 3, in either order.
3
3
A copy that disagrees
Someone debugs an upload failure and adds one line to team.cue. Replace the file.
package estate
// Added while debugging uploads.
buckets: logs: encrypted: false
Error: evaluating .:
buckets.logs.encrypted: conflicting values false and true:
./estate.cue:7:19
./team.cue:4:27
In HCL, the same two statements give a plan that uses the caller's value. In CUE they give no plan until someone decides which line is wrong.
Read the error from left to right.
- Stage.
evaluatingsays that both files are valid CUE and that their values do not unify. - Path.
buckets.logs.encryptedis the field in dispute. - Values.
false and trueare the two values that cannot both hold. The message does not say which came from where. Open both locations. - Locations. Each
file:line:columnpoints at one side. Line 7 ofestate.cueholdstrue. Line 4 ofteam.cueholdsfalse. The column is the start of the value.
pravo stops at evaluation and prints no data. It reports one conflict at a time: fix this one and run the command again to see the next. Reading a CUE error lists the messages you meet.
Every pair of copies falls into one of these rows.
| First copy | Second copy | Result |
|---|---|---|
true |
true |
true |
365 |
>=90 |
365 |
int |
30 |
30 |
a struct with owner |
a struct with cost_center |
one struct with both fields |
true |
false |
a conflict |
To let a caller change a value, mark the value as a default. That is A6. A plain value is a rule.
Predict
Write the four pairs below into a scratch directory. Each pair is one field written twice.
package predict
a: 3
a: 3
b: int
b: 3
c: owner: "web"
c: stage: "prod"
d: 3
d: "3"
Decide before you open the answer. Then run pravo -C predict eval, which runs pravo in the directory predict.
Predict Which pair makes pravo stop?
Only d. The values 3 and "3" are different values of different types, so they conflict.
Error: evaluating ./predict:
d: conflicting values 3 and "3" (mismatched types int and string):
./predict/p.cue:12:4
./predict/p.cue:13:4
pravo stops at the first conflict and prints no data. The other three pairs print only after d is gone. Remove it and run again.
package predict
a: 3
a: 3
b: int
b: 3
c: owner: "web"
c: stage: "prod"
a: 3
b: 3
c: {
owner: "web"
stage: "prod"
}
Exercise
Clear the conflict, then merge two structs
team.cue still holds the conflicting line.
package estate
// Added while debugging uploads.
buckets: logs: encrypted: false
Make two changes. First, clear the conflict by deleting the line that breaks the rule. Second, add two separate lines. Together they give buckets.uploads.tags the field cost_center with the value "4711" and the field env with the value "prod".
want() { [ "$(pravo eval -e "$1" 2>/dev/null)" = "$2" ] || { echo "$1 should be $2"; exit 1; }; }
want buckets.logs.encrypted true
want buckets.uploads.tags.owner '"web"'
want buckets.uploads.tags.cost_center '"4711"'
want buckets.uploads.tags.env '"prod"'
echo ok
Run the check on the start state. It fails.
buckets.logs.encrypted should be true
Hint 1
estate.cue states the rule: logs is encrypted. Delete the line in team.cue that breaks it. Then add one line for cost_center and one for env under buckets: uploads: tags.
Hint 2
Two structs for the same field merge into one. See unification.
Solution
package estate
buckets: uploads: tags: cost_center: "4711"
buckets: uploads: tags: env: "prod"
Recap
- Writing a field twice adds a second constraint. The value must satisfy both, so the result is the more specific copy or the details of both.
- Two concrete values that differ are a conflict. pravo stops, prints the path, the values and both locations, and reports no data.
- HCL replaces an earlier value with a later one. CUE never replaces. A value that may change is a default, which A6 introduces.
Check yourself
What does pravo print when two files give a field different concrete values?
An error that names the path of the field, the two values and the file, line and column of each. It prints no data.
A field is int in one file and 30 in another. What is the result?
30. The value satisfies the constraint, so the more specific copy is the result.
The next lesson shows that the order of lines and files does not matter.