§ 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.

Time
10 min
Level
beginner
Needs
A2
You will learn
What happens when two values for one field disagree?
Done when
pravo eval prints one merged value for the fields you wrote twice, and an error for the one conflict.

View as Markdown

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.

envs/prod/main.tf
module "logs" {
  source    = "../../modules/bucket"
  name      = "logs"
  encrypted = false
}
two files, one field
// 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 which encrypted is false.
  • A variable that is set in terraform.tfvars and again with -var takes the value of the last source.
  • A validation block can refuse false, 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.

main.cuevalid
package main

size: int & >=1 & <=10
size: 3
terminal
$pravo eval
size: 3
main.cueconflict
package main

size: int & >=1 & <=10
size: 0
terminalexit 6
$pravo eval
Error: evaluating .:
size: invalid value 0 (out of bound >=1):
    ./main.cue:3:13
    ./main.cue:4:7
main.cueconflict
package main

size: int & >=1 & <=10
size: 11
terminalexit 6
$pravo eval
Error: evaluating .:
size: invalid value 11 (out of bound <=10):
    ./main.cue:3:19
    ./main.cue:4:7
main.cueconflict
package main

size: int & >=1 & <=10
size: "3"
terminalexit 6
$pravo eval
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.

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"
	}
}

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.

team.cuevalid
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.

terminal
$pravo eval -e buckets.logs
name:           "logs"
versioning:     true
encrypted:      true
retention_days: 365
tags: {
	cost_center: "4711"
	owner:       "platform"
}

Each pair of copies became one value.

  • encrypted appears twice with the same value. The result is that value.
  • retention_days is 365 in one file and >=90 in the other. The value meets the bound, so the result is 365.
  • tags is 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.

terminal
$pravo eval -e 'int & 3'
$pravo eval -e '3 & int'
3
3

A copy that disagrees

Someone debugs an upload failure and adds one line to team.cue. Replace the file.

team.cueconflict
package estate

// Added while debugging uploads.
buckets: logs: encrypted: false
terminalexit 6
$pravo eval
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. evaluating says that both files are valid CUE and that their values do not unify.
  • Path. buckets.logs.encrypted is the field in dispute.
  • Values. false and true are the two values that cannot both hold. The message does not say which came from where. Open both locations.
  • Locations. Each file:line:column points at one side. Line 7 of estate.cue holds true. Line 4 of team.cue holds false. 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.

predict/p.cue
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.

terminalexit 6
$pravo -C predict eval
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.

predict/p.cue
package predict

a: 3
a: 3

b: int
b: 3

c: owner: "web"
c: stage: "prod"
terminal
$pravo -C predict eval
a: 3
b: 3
c: {
	owner: "web"
	stage: "prod"
}

Exercise

Exercise

Clear the conflict, then merge two structs

team.cue still holds the conflicting line.

team.cueconflict
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".

check.sh
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.

terminalexit 1
$sh check.sh
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
team.cue
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.