§ Reference · Language

CUE for Terraform people

The CUE that a pravo specification uses, on one page, for a reader who knows HCL. Values, unification, constraints, definitions, references, comprehensions, and what each HCL construct becomes.

View as Markdown

On this page

A pravo specification is CUE. This page is the CUE that the specification pages assume, for a reader who writes HCL and has not written CUE. Each idea has a file and the value that pravo eval prints for it, which evaluates CUE and needs no provider. Track A teaches the same ideas in lessons with exercises, and the CUE cheat sheet has the syntax on one page.

Data first

CUE is a superset of JSON. A file is a struct of fields, and the quotes on labels, the commas and the outer braces are optional. Nesting can be written on one line: a: b: c: 1. The files of a directory that share a package clause are one value.

bucket.cue
package demo

bucket: {
	name:   "app-logs"
	region: "eu-west-1"
	tags: team: "platform"
	versioning: true
}
terminal
$pravo eval -e bucket
name:   "app-logs"
region: "eu-west-1"
tags: {
	team: "platform"
}
versioning: true
In HCL In CUE
name = "app-logs" name: "app-logs"
tags = { team = "platform" } tags: team: "platform"
ports = [80, 443] ports: [80, 443]
enabled = true enabled: true
"${var.env}-db" "\(env)-db"
# comment // comment

A field is written as often as you like

HCL assigns a value once. CUE unifies: a field may be written in several places, and the value is what all of them agree on. The same value twice is one value, and two different values are an error that names both. This is how a file of defaults and a file of overrides combine without a merge function, and how a rule and the data it governs sit in different files.

env.cue
package demo

bucket: region: "eu-west-1"
bucket: tags: env: "prod"
terminal
$pravo eval -e bucket.tags
env:  "prod"
team: "platform"

A second value for region conflicts:

other.cuerejected
package demo

bucket: region: "us-east-1"
terminalexit 6
$pravo eval -e bucket.region
Error: evaluating .:
bucket.region: conflicting values "us-east-1" and "eu-west-1":
    ./bucket.cue:5:10
    ./other.cue:3:17

Types and constraints are values

string, int, bool and >=1 are values too, and they unify with data. A field that holds a type is not complete until data gives it a concrete value; a field whose data breaks the constraint is an error. This is what HCL's type and validation blocks do, written in the same language as the data and placed wherever a value should be checked.

shape.cue
package demo

bucket: {
	name:       string & =~"^[a-z][a-z0-9-]*$"
	region:     "eu-west-1" | "eu-central-1"
	versioning: bool
	retention:  int & >0 | *30
}
terminal
$pravo eval -e bucket.retention
30
Written Means
string, int, float, bool, number A value of that type.
>0, <=100, !="" A bound. Several combine with &.
=~"^[a-z]" Matches a regular expression.
"a" | "b" One of the alternatives: a disjunction.
*30 | int A default. The value is 30 unless data says otherwise.
[...string] A list of strings, any length.
{...} A struct that accepts fields it does not name.

pravo validate checks that every resource is concrete and satisfies its constraints before any provider starts. Constraints narrow a value and Choices and defaults go further.

Definitions reject typos

A struct is open: a field that nobody declared is accepted. A definition, written #Name, is closed: a value built from it may not add fields. The resource types that a provider package generates are definitions, which is why a misspelt attribute is an error at evaluation and not a surprise at apply.

def.cue
package demo

#Bucket: {
	name:       string
	region:     string
	versioning: bool | *false
	tags: [string]: string
}

logs: #Bucket & {name: "logs", region: "eu-west-1"}
terminal
$pravo eval -e logs
name:       "logs"
region:     "eu-west-1"
versioning: false
tags: {}
typo.cuerejected
package demo

logs: versionning: true
terminalexit 6
$pravo eval -e logs
Error: evaluating .:
logs.versionning: field not allowed:
    ./typo.cue:3:7

[string]: string is a pattern: every field of tags, whatever its name, is a string. A field marked ? is optional, and one marked ! is required, so that name!: string fails until something sets it. Definitions reject typos and Required, optional, regular show the three kinds of field.

References need no quotes

A value refers to another by its path, and a reference in a string is \( ). There is no var., local. or module. prefix: a name is looked up where it is written, then in the enclosing structs, then at the top of the package. A resource that reads an attribute of another resource depends on it, and pravo orders the plan by those references.

refs.cue
package demo

env: "prod"

db: {
	name: "\(env)-db"
	port: 5432
}

app: {
	database_url: "postgres://\(db.name):\(db.port)/app"
	replicas:     3
}

_perReplica: 256
app: memory: _perReplica * app.replicas
terminal
$pravo eval -e app
database_url: "postgres://prod-db:5432/app"
memory:       768
replicas:     3

A field whose name starts with _ is hidden: it is a value for the author and is not part of the output, which is what locals are for. Naming a value once adds let.

Loops are comprehensions

count and for_each become a comprehension: a for that produces fields or list elements, and an if that guards them. The result is a struct of resources, one for each entry, with the name of the entry as the name of the resource.

loops.cue
package demo

regions: ["eu-west-1", "eu-central-1", "us-east-1"]

buckets: {
	for r in regions if r != "us-east-1" {
		"logs-\(r)": #Bucket & {name: "logs-\(r)", region: r}
	}
}

ports: [for p in [80, 443, 8080] if p < 1024 {p}]
terminal
$pravo eval -e ports
$pravo eval -e buckets
[80, 443]
"logs-eu-west-1": {
	name:       "logs-eu-west-1"
	region:     "eu-west-1"
	versioning: false
	tags: {}
}
"logs-eu-central-1": {
	name:       "logs-eu-central-1"
	region:     "eu-central-1"
	versioning: false
	tags: {}
}

A conditional expression, cond ? a : b, is written with if fields or with a disjunction that data decides. Generating fields from data and Conditions and strings show both.

Inputs are tags

A value that a command supplies, where HCL has a variable, is a field with an attribute: env: string @tag(env). pravo plan -t env=prod fills it, and a default keeps the plan runnable without the flag. Tags and variables lists the forms.

inputs.cue
package demo

size: *"small" | "large" @tag(size)
terminal
$pravo eval -e size
$pravo eval -e size -t size=large
"small"
"large"

Packages and the standard library

A directory is one package, named by its package clause, and import brings in another: a provider package that pravo init generates, the pravo.dev/pravo package with #Output, #Policy and the settings, or a package of the CUE standard library such as strings and list. The functions of HCL are mostly there: strings.ToUpper, strings.Join, list.Concat, len, math.Round. Packages and the standard library tours them, and the function map says where each HCL function went.

From HCL to a specification

HCL In a pravo specification Page
provider "aws" {} $aws: awsp.#Provider & {region: "eu-west-1"} Providers
resource "aws_s3_bucket" "logs" {} logs: $aws.#Resource.aws_s3_bucket & {...} Resources
data "aws_ami" "base" {} base: $aws.#Data.aws_ami & {...} Resources
variable "env" {} env: string @tag(env) Tags and variables
locals { } A hidden field, _name: ..., or a plain field that no provider reads Naming a value once
output "url" {} url: pravo.#Output & {value: ...} Outputs
count, for_each A comprehension Resources
module "net" {} An instance of a definition, or of a package, that holds resources Reuse
depends_on, lifecycle $depends_on, $lifecycle Meta fields
moved {}, import {} $from, $import Moves and imports
backend "s3" {} $settings: state: {backend: "s3", ...} Settings
validation {}, Sentinel, OPA A constraint on the provider definition, or a pravo.#Policy Policies
terraform.tfvars -t flags, or a file of tag values Tags and variables
Workspaces A tag that selects the state Run one specification in several environments

The concept map of the migration guide explains each row.

Reading an error

An error of evaluation names a path, the values that conflict or the rule that failed, and the positions in the files. bucket.region: conflicting values "us-east-1" and "eu-west-1" means two places set bucket.region and they differ, and the lines below name both. incomplete value means something is still a type and no data filled it. field not allowed means a definition did not declare the field. Reading a CUE error and Why is my value incomplete? read one of each line by line.

See also