§ A5 · Track A · CUE from zero
Constraints narrow a value
A rule in CUE is a value written on the field. Bounds and regular expressions narrow what the field may hold.
On this page
In HCL, every rule is a validation block with its own message. The value arrives from another file, and OpenTofu tests it when you plan.
variable "bucket_name" {
type = string
validation {
condition = can(regex("^[a-z0-9-]{3,63}$", var.bucket_name))
error_message = "Use 3 to 63 lowercase letters, digits or hyphens."
}
}
variable "port" {
type = number
validation {
condition = var.port >= 1 && var.port <= 65535
error_message = "Use a port from 1 to 65535."
}
}
The same two rules in CUE are two fields.
rules: https: port: int & >=1 & <=65535
buckets: audit: name: =~"^[a-z0-9-]{3,63}$"
The HCL takes 15 lines and the CUE takes 2. In CUE the rule is a value that you write on the field, next to the value it judges. The CUE has no error_message: pravo builds the message from the rule that failed, as the next sections show.
The operator & joins two values into one value that satisfies both. >=1, <=65535 and =~"^[a-z0-9-]{3,63}$" are values too. Each one is a bound: a value that allows only some numbers or some text. A field that carries a bound allows only what the bound allows.
Run the four values
The widget shows results that pravo eval -e printed. The flag evaluates an expression inside a package. The runs below use an empty package in a directory of its own. The two numbers at the end of each location are the line and the column in the expression. They point at the bound or the type that rejected the value, then at the value.
package estate
3
Error: -e "int & >=1 & <=10 & 0": invalid value 0 (out of bound >=1):
1:7
1:20
Error: -e "int & >=1 & <=10 & 11": invalid value 11 (out of bound <=10):
1:13
1:20
Error: -e "int & >=1 & <=10 & \"3\"": conflicting values int and "3" (mismatched types int and string):
1:1
1:20
Warm-up
From earlier lessons
From A2: what does pravo eval -c do?
It fails unless every value is concrete. A constraint such as int or >=1 is not concrete.
From A3: two files give retention_days the values 30 and 90. What does pravo print?
A conflict that names the path, the two values and both locations. It prints no data.
Start from the estate
Work in the directory from A4. If you start here, write these three files.
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"
}
}
package estate
rules: {
https: {
port: 443
protocol: "tcp"
}
ssh: {
port: 22
protocol: "tcp"
}
}
package estate
buckets: uploads: tags: cost_center: "4711"
buckets: uploads: tags: env: "prod"
Save the output.
A bound on a number
The platform team limits every port to the integers from 1 to 65535. It keeps its limits in a file of its own.
package estate
rules: https: port: int & >=1 & <=65535
rules: ssh: port: int & >=1 & <=65535
Both ports are inside the bound, so the data does not change.
same
A bound changes nothing for a value that meets it. It matters when a value does not.
Break it
Someone sets the port of the ssh rule to 70000. Replace rules.cue.
package estate
rules: {
https: {
port: 443
protocol: "tcp"
}
ssh: {
port: 70000
protocol: "tcp"
}
}
Error: evaluating .:
rules.ssh.port: invalid value 70000 (out of bound <=65535):
./limits.cue:5:31
./rules.cue:9:13
Read the error from left to right.
- Path.
rules.ssh.portis the field that failed. - Message.
invalid value 70000 (out of bound <=65535)names the value and the bound it breaks. The bound is named, so you know which of>=1and<=65535rejected it. - Locations. The first location is the bound that rejected the value:
limits.cue, line 5, column 31. The second is the value:rules.cue, line 9.
Put the port back.
package estate
rules: {
https: {
port: 443
protocol: "tcp"
}
ssh: {
port: 22
protocol: "tcp"
}
}
A bound is not a type
A bound allows every number in its range. Write a scratch file with a bound and a value of 22.5.
package float
port: >=1 & <=65535
port: 22.5
port: 22.5
The file is valid and the value is not a port. Add int.
package float
port: int & >=1 & <=65535
port: 22.5
Error: evaluating ./float:
port: conflicting values int and 22.5 (mismatched types int and float):
./float/f.cue:3:7
./float/f.cue:4:7
The message names both sides of the clash: the type int and the value 22.5. The reason follows in brackets, mismatched types int and float. This is why the limits above start with int &.
Bounds on text
A regular expression is a bound too. The operator =~ accepts only text that matches it. Write a scratch file with a bucket name that does not match.
package names
name: =~"^[a-z0-9-]{3,63}$"
name: "Logs_Bucket"
Error: evaluating ./names:
name: invalid value "Logs_Bucket" (out of bound =~"^[a-z0-9-]{3,63}$"):
./names/n.cue:3:7
./names/n.cue:4:7
The message reads as it did for the port. The bound is named, with its regular expression, and the first location is the bound. The expression means 3 to 63 characters, each a lowercase letter, a digit or a hyphen.
| Bound | Allows | A value that passes |
|---|---|---|
>=1 |
numbers of at least 1 | 5 |
>4 |
numbers above 4 | 5 |
<=5 |
numbers of at most 5 | 5 |
<6 |
numbers below 6 | 5 |
!=4 |
anything except 4 | 5 |
=~"^[a-z][0-9]$" |
text that matches | "a1" |
!~"^b" |
text that does not match | "a1" |
Run each bound
5
5
5
5
5
"a1"
"a1"
The two rules for ports repeat. Every bucket name needs the same rule. A8 and A9 write each rule once.
Exercise
Write the bucket-name rule
A third bucket, audit, has no name yet. Its name must follow the rule from the HCL at the top of this page.
package estate
buckets: audit: {
name: string
versioning: true
}
Add one line to limits.cue that bounds buckets.audit.name. The check joins three valid and three invalid names to that field with &.
fixture() {
if pravo eval -e "buckets.audit.name & \"$1\"" > /dev/null 2>&1; then got=valid; else got=invalid; fi
[ "$got" = "$2" ] || { echo "$1 should be $2"; exit 1; }
}
fixture logs valid
fixture my-bucket-01 valid
fixture abc valid
fixture ab invalid
fixture Logs invalid
fixture my_bucket invalid
echo ok
Run the check on the start state. It fails.
ab should be invalid
Hint 1
Write the bound with =~ and a regular expression. A name has 3 to 63 characters, each a lowercase letter, a digit or a hyphen. Add the line to limits.cue.
Hint 2
The table in Bounds on text lists =~. See bound.
Solution
package estate
rules: https: port: int & >=1 & <=65535
rules: ssh: port: int & >=1 & <=65535
buckets: audit: name: =~"^[a-z0-9-]{3,63}$"
Recap
&joins two values into one that satisfies both. A bound such as>=1is a value, so a rule is a value written on the field.- A bound that rejects a value is named in the error. The first location is the bound, and the second is the value.
- A numeric bound allows decimals. Add
intwhen the field is a whole number.
Check yourself
What do the two locations in invalid value 70000 (out of bound <=65535) point to?
The first is the bound that rejected the value. The second is the value.
Does >=1 & <=65535 reject 22.5?
No. A numeric bound allows every number in its range. int & >=1 & <=65535 rejects it.
The next lesson offers choices and a default.