§ A6 · Track A · CUE from zero

Choices and defaults

Offer a short list of values, mark one as the default, and select it from the command line.

Time
9 min
Level
beginner
Needs
A5
You will learn
How does a field offer a choice? Which value does it take when nobody chooses?
Done when
pravo eval -t env=prod prints env: "prod", and pravo eval -t env=qa is refused.

View as Markdown

On this page

In HCL, a default and the list of allowed values are two separate statements.

variables.tf
variable "class" {
  type    = string
  default = "standard"
  validation {
    condition     = contains(["standard", "ia", "glacier"], var.class)
    error_message = "class must be standard, ia or glacier."
  }
}

Change the list and the default does not follow. A lookup(var.bucket, "class", "standard") elsewhere repeats the default a third time.

In CUE, a | between values offers a choice. A * in front of one choice marks the default: the value the field takes when nothing else is written.

Warm-up

From earlier lessons

Two lines give name the values "logs" and "media". What does pravo eval print?

An error that names both values and both lines. A field written twice must agree (A3).

A port is >=1 & <=65535 and a second line sets it to 70000. Is the value accepted?

No. A bound is a constraint, and 70000 is outside it (A5).

Offer a choice with a default

Work in an empty directory. Write estate.cue. The bucket has a class with three choices.

estate.cue
package estate

bucket: {
	name:  "logs"
	class: *"standard" | "ia" | "glacier"
}
terminal
$pravo eval
bucket: {
	name:  "logs"
	class: "standard"
}

Nothing sets class, so it takes the default, "standard". The other two choices stay allowed. The eight lines of HCL above say what this one line says.

Write the field again with one of the other choices. A field written twice adds a constraint.

estate.cue
package estate

bucket: {
	name:  "logs"
	class: *"standard" | "ia" | "glacier"
}

bucket: class: "ia"
terminal
$pravo eval
bucket: {
	name:  "logs"
	class: "ia"
}

The new value matches one choice, so the default is not used.

Break it

Write a value that is not one of the choices.

estate.cuerejected
package estate

bucket: {
	name:  "logs"
	class: *"standard" | "ia" | "glacier"
}

bucket: class: "cold"
terminalexit 6
$pravo eval
Error: evaluating .:
bucket.class: 3 errors in empty disjunction:
bucket.class: conflicting values "glacier" and "cold":
    ./estate.cue:5:30
    ./estate.cue:8:16
bucket.class: conflicting values "ia" and "cold":
    ./estate.cue:5:23
    ./estate.cue:8:16
bucket.class: conflicting values "standard" and "cold":
    ./estate.cue:5:10
    ./estate.cue:8:16

Read the error from the top.

  • The path is bucket.class.
  • "3 errors in empty disjunction" means pravo tested "cold" against each of the three choices. No choice is left.
  • Each error pairs one choice with your value.
  • Each error gives two locations. Line 5 wrote the choice. Line 8 wrote "cold".

Take a value from the command line

pravo has no workspaces. Select the environment with -t env=prod.

A tag is a field marked @tag(name). The flag -t name=value supplies its value. Add an env field with three choices and a default.

estate.cue
package estate

env: *"dev" | "staging" | "prod" @tag(env)

bucket: {
	name:  "logs"
	class: *"standard" | "ia" | "glacier"
	stage: env
}

stage: env copies the value of env into the bucket. A11 covers this.

terminal
$pravo eval
$pravo eval -t env=prod
env: "dev"
bucket: {
	name:  "logs"
	class: "standard"
	stage: "dev"
}
env: "prod"
bucket: {
	name:  "logs"
	class: "standard"
	stage: "prod"
}

Without a tag, env takes its default. With -t env=prod, the value is "prod". Now ask for an environment that does not exist.

terminalexit 6
$pravo eval -t env=qa
Error: evaluating .:
env: 3 errors in empty disjunction:
env: conflicting values "dev" and "qa":
    ./estate.cue:3:7
env: conflicting values "prod" and "qa":
    ./estate.cue:3:27
env: conflicting values "staging" and "qa":
    ./estate.cue:3:15

The flag does not skip the rules. -t env=qa is the same as writing env: "qa" in a file. It meets the same three choices, and none matches. The refusal happens during evaluation, before any provider starts.

Two defaults cancel each other

A second file marks a different choice as the default for class. Each file marks one choice with *, and they disagree.

prod.cueunintended
package estate

bucket: class: *"ia" | "standard" | "glacier"

Decide before you open the answer.

Predict Which default wins?

Neither. The two defaults cannot both hold, so no default remains. The field keeps all three choices.

terminalexit 1
$pravo eval
$pravo eval -c
env: "dev"
bucket: {
	name:  "logs"
	class: "standard" | "ia" | "glacier"
	stage: "dev"
}

Error: the value is not concrete:
bucket.class: incomplete value "standard" | "ia" | "glacier"

pravo eval succeeds and prints the three choices without a default. The flag -c fails unless every value is concrete. It reports incomplete value, because pravo cannot pick one choice. C2 lists every cause of an incomplete value.

Pitfall

Mark the default once per field, in one place. Every other file writes a plain value or a plain list of choices.

Remove the second file. The default works again.

terminal
$rm prod.cue
$pravo eval
env: "dev"
bucket: {
	name:  "logs"
	class: "standard"
	stage: "dev"
}

Exercise

Exercise

Add a region

Add a top-level field region to estate.cue. It offers eu-west-1, eu-central-1 and us-east-1. The default is eu-west-1. A tag named region fills it.

The check runs pravo eval -e region with no tag and with -t region=us-east-1. It also runs -t region=ap-south-1, which must be refused.

check.sh
test "$(pravo eval -e region 2>/dev/null)" = '"eu-west-1"' || { echo "with no tag, region should be eu-west-1"; exit 1; }
test "$(pravo eval -t region=us-east-1 -e region 2>/dev/null)" = '"us-east-1"' || { echo "-t region=us-east-1 should select us-east-1"; exit 1; }
if pravo eval -t region=ap-south-1 > /dev/null 2>&1; then echo "ap-south-1 should be refused"; exit 1; fi
echo ok

Run it on the start state. It fails.

terminalexit 1
$sh check.sh
with no tag, region should be eu-west-1
Hint 1

Follow the env field. Put * in front of one choice and @tag(region) after the last choice.

Hint 2

Compare with the default and tag definitions. The field needs no other line.

Solution
estate.cue
package estate

env: *"dev" | "staging" | "prod" @tag(env)

region: *"eu-west-1" | "eu-central-1" | "us-east-1" @tag(region)

bucket: {
	name:  "logs"
	class: *"standard" | "ia" | "glacier"
	stage: env
}

The refusal reads like the one for env.

terminalexit 6
$pravo eval -t region=ap-south-1 -e region
Error: evaluating .:
region: 3 errors in empty disjunction:
region: conflicting values "eu-central-1" and "ap-south-1":
    ./estate.cue:5:24
region: conflicting values "eu-west-1" and "ap-south-1":
    ./estate.cue:5:10
region: conflicting values "us-east-1" and "ap-south-1":
    ./estate.cue:5:41
What | and * do

A list of values joined by | is a disjunction. The field can be any one of them. Writing a value narrows the list to the choices that value matches.

A default is selected only when nothing narrows the list further. Four fields show the cases.

deep/estate.cue
package estate

a: *1 | 2 | 3
b: *1 | 2 | 3
b: 2
c: *1 | *2 | 3
d: 1 | 2 | 3
terminal
$pravo -C deep eval
a: 1
b: 2
c: 1 | 2
d: 1 | 2 | 3

Field a takes its default. Field b takes the value written, because that value is one of the choices. Field c marks two defaults, so both stay as choices. Field d has no default and stays open.

Recap

  • A | offers choices. The value must be one of them.
  • A * marks the default. Mark it once per field.
  • A @tag(name) field takes its value from -t name=value. The choices still apply.

Check yourself

What does class: *"standard" | "ia" hold when nothing else is written?

The default, "standard".

Why does -t env=qa fail when env offers dev, staging and prod?

A tag value is unified with the field like any other value. The value "qa" matches none of the choices.

The next lesson decides what happens to a field that nobody sets.