§ A12 · Track A · CUE from zero

Conditions and strings

Include a field only when a condition holds. Build strings from values. See why a condition needs a concrete value.

Time
8 min
Level
beginner
Needs
A11
You will learn
How does a field exist only in some environments? How does a string use the value of a field?
Done when
pravo eval -t env=prod prints a kms_key that -t env=dev leaves out.

View as Markdown

On this page

HCL says "only in prod" with a ternary. A resource that exists only in prod takes a count of one or zero.

main.tf
resource "aws_kms_key" "logs" {
  count = var.env == "prod" ? 1 : 0
}

locals {
  retention = var.env == "prod" ? 90 : 7
}

A nested block that exists only in prod needs a dynamic block over a list of one or zero items.

In CUE, an if inside a struct includes the fields in its braces when the condition is true. When the condition is false, the fields are not there at all.

Warm-up

From earlier lessons

One line sets retention to 7 and a second line sets it to 90. What does pravo eval print?

An error that names both values. Two different concrete values cannot both hold (A3).

A field offers *7 | int, and a second line sets it to 90. What does pravo eval print?

The value 90. A default gives way to any value that matches the other choice (A6).

Include a field only in prod

Work in an empty directory. Write estate.cue. The env field is the tag from A6.

estate.cue
package estate

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

bucket: {
	name:      "estate-logs-\(env)"
	retention: *7 | int
	if env == "prod" {
		retention: 90
		kms_key:   "alias/estate-logs"
	}
}
terminal
$pravo eval
$pravo eval -t env=prod
env: "dev"
bucket: {
	name:      "estate-logs-dev"
	retention: 7
}
env: "prod"
bucket: {
	name:      "estate-logs-prod"
	retention: 90
	kms_key:   "alias/estate-logs"
}

In dev the if adds nothing, and the default keeps retention at 7. In prod the if adds kms_key and sets retention to 90.

The condition env == "prod" is a comparison. You can also write !=, <, >, &&, || and !. There is no else. To cover the other case, write a second if with the opposite condition.

Break it

A condition adds fields. It never replaces a value. Give retention a plain 7 instead of a default.

estate.cueconflict
package estate

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

bucket: {
	name:      "estate-logs-\(env)"
	retention: 7
	if env == "prod" {
		retention: 90
		kms_key:   "alias/estate-logs"
	}
}
terminal
$pravo eval
env: "dev"
bucket: {
	name:      "estate-logs-dev"
	retention: 7
}
terminalexit 6
$pravo eval -t env=prod
Error: evaluating .:
bucket.retention: conflicting values 90 and 7:
    ./estate.cue:5:9
    ./estate.cue:7:13
    ./estate.cue:8:2
    ./estate.cue:9:14

dev works, because the if is not included. In prod the if adds 90 to a field that already holds 7. That is the conflict from A3. Read the error from the top.

  • The path is bucket.retention.
  • The values are 90 and 7.
  • The four locations are line 5 (the struct), line 7 (7), line 8 (the if) and line 9 (90).

Restore the default, so the value 7 can give way.

estate.cue
package estate

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

bucket: {
	name:      "estate-logs-\(env)"
	retention: *7 | int
	if env == "prod" {
		retention: 90
		kms_key:   "alias/estate-logs"
	}
}

Build strings from values

A string can contain \( and ). The expression between them is replaced by its value. This is interpolation. You have used it since A11.

strings.cue
package estate

region: "eu-west-1"
port:   8443

url:  "https://\(env).example.com:\(port + 1)"
arn:  "arn:aws:s3:::\(bucket.name)"
note: """
	bucket: \(bucket.name)
	region: \(region)
	"""
terminal
$pravo eval -e url
$pravo eval -e arn
$pravo eval -e note
"https://dev.example.com:8444"
"arn:aws:s3:::estate-logs-dev"
"""
	bucket: estate-logs-dev
	region: eu-west-1
	"""

The expression can be a field, a sum or any other value. Numbers and booleans become text. Three quotes open a string that spans several lines. Interpolation works inside it.

Choose a value by name

An if adds fields. To choose one value out of several, index a struct by the key.

strings.cue
package estate

days: {dev: 7, staging: 30, prod: 90}[env]
terminal
$pravo eval -e days
$pravo eval -t env=staging -e days
7
30

An if suits a field that may be absent. A lookup suits a value that always exists and changes.

Remove strings.cue. The rest of the lesson uses estate.cue only.

terminal
$rm strings.cue

A condition needs a concrete value

An if can only decide when its condition is concrete. Remove the tag and leave env as a type.

estate.cueunintended
package estate

env: string

bucket: {
	name:      "estate-logs-\(env)"
	retention: *7 | int
	if env == "prod" {
		retention: 90
		kms_key:   "alias/estate-logs"
	}
}
terminal
$pravo eval
env: string
bucket: {
	name:      "estate-logs-\(env)"
	retention: *7 | int
	if env == "prod" {
		retention: 90
		kms_key:   "alias/estate-logs"
	}
}

pravo eval does not fail. It prints the if as it is written, because env is still a type. This is the difference between a type and a value from A2. The flag -c makes the gap an error.

terminalexit 1
$pravo eval -c
Error: the value is not concrete:
env: incomplete value string:
    ./estate.cue:3:6
bucket: non-concrete value string in operand to ==:
    ./estate.cue:8:5
    ./estate.cue:3:6

The first error says env is incomplete: it is still string. The second says the if cannot compare a string with "prod". Fix both by giving env a value: a default, a tag or a line in a file.

Exercise

Exercise

Two conditions

Restore the tag. Then change estate.cue so that these three runs print what is listed.

Command bucket.retention bucket.kms_key
pravo eval 7 absent
pravo eval -t env=staging 30 absent
pravo eval -t env=prod 90 "alias/estate-logs"
estate.cue
package estate

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

bucket: {
	name:      "estate-logs-\(env)"
	retention: *7 | int
}

The check runs each command of the table. A field that is absent is an error: pravo eval -e bucket.kms_key fails in dev.

check.sh
want() { [ "$(pravo eval $1 2>/dev/null)" = "$2" ] || { echo "pravo eval $1 should print $2"; exit 1; }; }
absent() { if pravo eval $1 > /dev/null 2>&1; then echo "pravo eval $1 should fail"; exit 1; fi; }
want "-e bucket.retention" 7
want "-t env=staging -e bucket.retention" 30
want "-t env=prod -e bucket.retention" 90
want "-t env=prod -e bucket.kms_key" '"alias/estate-logs"'
absent "-e bucket.kms_key"
absent "-t env=staging -e bucket.kms_key"
echo ok

Run it on the start state. It fails.

terminalexit 1
$sh check.sh
pravo eval -t env=staging -e bucket.retention should print 30
Hint 1

Write one if for staging and one for prod. Put kms_key only in the second.

Hint 2

Compare the first example on this page. The default *7 | int lets both conditions set a different value.

Solution
estate.cue
package estate

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

bucket: {
	name:      "estate-logs-\(env)"
	retention: *7 | int
	if env == "staging" {
		retention: 30
	}
	if env == "prod" {
		retention: 90
		kms_key:   "alias/estate-logs"
	}
}
terminal
$pravo eval -t env=staging
$pravo eval -t env=prod -e bucket.kms_key
env: "staging"
bucket: {
	name:      "estate-logs-staging"
	retention: 30
}
"alias/estate-logs"

Recap

  • An if includes its fields when the condition is true. There is no else.
  • A condition adds to a field and never replaces it. Use a default so a later value can win.
  • A condition needs a concrete value. pravo eval -c finds the ones that are still types.

Check yourself

What does retention: 7 plus if env == "prod" { retention: 90 } do in prod?

It fails with conflicting values 90 and 7. Make the first value a default: *7 | int.

Why does pravo eval succeed when env is string, and pravo eval -c fail?

Without -c, an undecided if is printed as written. The flag -c requires every value to be concrete.

The next lesson imports the standard library and shares a definition between directories.