Cue Overviews for Beginners

Image

Overviews

Every resource in kubernetes can be declared through texts, and these texts are always duplicated. therefore, many template techniques are developed to generate texts, e.g. Helm, Customize and so on. Cue is a language specialized for generate texts, what is different in Cue is that it avoids layered designed, which is often the most difficult part when it comes to manage large configuration in kubernetes. Developers or Operators are always confused in which layer they should change the template values. With many traits different to current template techniques, I think we should take a look at Cue.

Foundations

Cue is a superset of JSON.

superset.cue

str: "hello world"
num: 42
flt: 3.14

// Special field name (and a comment)
"k8s.io/annotation": "secure-me"

// lists can have different element types
list: [
	"a", "b", "c",
	1,
	2,
	3,
]

obj: {
	foo: "bar"
	// reuse another field?!
	L: list
}

cue export superset.cue –out json

{
    "str": "hello world",
    "num": 42,
    "flt": 3.14,
    "k8s.io/annotation": "secure-me",
    "list": [
        "a",
        "b",
        "c",
        1,
        2,
        3
    ],
    "obj": {
        "foo": "bar",
        "L": [
            "a",
            "b",
            "c",
            1,
            2,
            3
        ]
    }
}

The Lattice

Every instance lives somewhere is cue’s lattice. top() -> schema -> constraint -> data -> bottom(|_)

Types and Values

Cue merges types and values into one concept, the lattice.

schema.cue

album: {
  title: string
  year: int
  live: bool
}

constraints.cue

import "strings"

album: {
  title: strings.MinRunes(5)
  year: >1952
  live: false
}

data.cue

album: {
  title: "Houses of the Holy"
  year: 1973
  live: false
}
cue eval schema.cue constraints.cue data.cue

Order Is Irrelevant

order.cue

// you can add constraints after
a: 3
a: int
a: >1

// define a struct in one place
s: {
	x: int
	y: int
}

// define a struct in parts
s: y: int
s: x: int

// the above is shorthand
// when setting a nested value

Values Cannot Be Changed

Defining Fields

Cue allows fields to be defined more than once, as long as they are consistent with each other.

fields.cue

hello: "world"
hello: "world"

// set a type
s: {a: int}

// set some data
s: {a: 1, b: 2}

// set a nested field without curly braces
s: c: d: 3

// lists must have the same elements
// and cannot change length
l: ["abc", "123"]
l: [
	"abc",
	"123",
]

Definitions (Schema)

Definitions are struct with a ‘#’ prefix, and you can leave it open with ‘…’ at last inside.

definition.cue

#Album: {
	artist: string
	title:  string
	year:   int

	// ...  // 2. uncomment to open and fix error, must be last
}

// This is a conjunction, it says "album" has to be "#Album"
album: #Album & {
	artist: "Led Zeppelin"
	title:  "Led Zeppelin I"
	year:   1969

	// studio: true  // 1. uncomment to trigger error
}

Conjunction

Conjunctions ‘meet’ values together, combining their fields, rules and data together. They are like ‘and’ and the operator is ‘&’

conjunction.cue

// conjunctions on a field
n: int & >0 & <100
n: 23
// conjunctions on a schema
val: #Def1 & #Def2
val: {
  foo: "bar"
  ans: 42
}

#Def1: {
  foo: string
  ans: int
}

#Def2: {
  foo: =~'[a-z]+'
  ans: >0
}

Disjunctions

Disjunctions ‘join’ values to create options or alternatives, they are like ‘or’ and the operator is ‘|’

disjunction.cue

// disjunction of values (like an enum)
hello: "world" | "bob" | "mary"
hello: "world"

// disjunction of types
port: string | int
port: 5432

// disjunction of schemas
val: #Def1 | #Def2
val: {foo: "bar", ans: 42}

#Def1: {
	foo: string
	ans: int
}

#Def2: {
	name: string
	port: int
}

Disjunctions have several uses:

Defaults and Options

Defaults’s operator is ‘*’ as a prefix in the value. Options’s operator is ‘?’ as a suffix in the key.

s: {
	// field with a default
	hello: string | *"world" | "apple"
	// an optional integer
	count?: int
}

Incomplete and Concrete

incomplete.cue

// incomplete values
a: _
b: int

s: {
  a: _
}

// concrete values
a: "a"
b: int

s: a: { foo: "bar" }

Open and Closed

Open means a struct can be extended, and closed means they cannot. By default, structs are open and definition are closed.

open-closed.cue

	// Closed struct
s: close({
	foo: "bar"
})

// Open definition
#d: {
	foo: "bar"
	... // must be last
}

Building up Values

You can define small schemas first, then embed them into a bigger schema, this makes schemas reusable.

building-up.cue

 
#Base: {
	name: string
	kind: string
}

#Meta: {
	// string and a semver regex
	version: string & =~"^v[0-9]+\\.[0-9]+\\.[0-9]+$"
	// list of strings
	labels: [...string]
}

#Permissions: {
	role:   string
	public: bool | *false
}

// Building up a schema using embeddings
#Schema: {
	// embed other schemas
	#Base
	#Meta

	#Permissions
	// with no '...' this is final
}

value: #Schema & {
	name:    "app"
	kind:    "deploy"
	version: "v1.0.42"
	labels: ["server", "prod"]
	role: "backend"
	// public: false  (by default)
}

Types and Values

Builtin Types

null bool string bytes number list struct | int

’ means top. ‘|_’ means bottom and represents an error.

Expressions

Interpolation

Cue supports interpolation in strings and bytes with () interpolate.cue

 
container: {
	repo:    "docker.io/cuelang"
	image:   "cue"
	version: "v0.3.0"
	full:    "\(repo)/\(image):\(version)"
}

name: "Tony"
msg:  "Hello \(name)"
// conver string to bytes
b: '\(msg)'
// convert bytes to string
s: "\(b)"

List Comprehensions

list-comp.cue

nums: [1, 2, 3, 4, 5, 6]
sqrd: [ for _, n in nums {n * n}]
even: [ for _, n in nums if mod(n, 2) == 0 {n}]

listOfStructs: [ for p, n in nums {
	pos: p
	val: n
}]

extractVals: [ for p, S in listOfStructs {S.val}]

Field Comprehensions

field-comp.cue

 
apps: ["nginx", "express", "postgres"]
#labels: [string]: string
stack: {
	for i, app in apps {
		"\(app)": {
			name:   app
			labels: #labels & {
				app:  "foo"
				tier: "\(i)"
			}
		}
	}
}

Conditional Fields

guards.cue

 
app: {
	name: string
	tech: string
	mem:  int

	if tech == "react" {
		tier: "frontend"
	}
	if tech != "react" {
		tier: "backend"
	}

	if mem < 1Gi {
		footprint: "small"
	}
	if mem >= 1Gi && mem < 4Gi {
		footprint: "medium"
	}
	if mem >= 4Gi {
		footprint: "large"
	}
}

// This will result in an error because CUE evaluates all conditions
// without short-circuiting, meaning it will still try to access app.field
// if app.field != _|_ && app.field == true {
//   foo: true
// }

// Use nested guards to check multiple conditions
if app.field != _|_ {
	if app.field == true {
		foo: true
	}
}

Scopes and Visibility

Reference Lookup

 
val: 42
A: {
	val: 23
	num: val // will be 23, matches locally

	// irregular names need to be quoted and require indexing to access
	"user-id": "abc"
	UserID:    A["user-id"]
}
A: {
	b: val   // 42, matches top-level
	c: A.num // 23, reference A.num

	// num itself is not in scope even though it is part of A
}

Paths

 
A: {
	a:    "A"
	"2f": 3
	l: ["cow", "moo"]
}

a: {
	// either is valid
	f1: A.a
	f2: A["a"]
	// must index
	f3: A["2f"]
	// list index
	f4: A.l[1]
}

Hidden Fields and Values

hidden.cue

A: {
	_hidden: "a hidden field"
	isshown: "I can be seen"
	hidrefd: _hidden + " sort of?"
}

_#NoshowDefn: {
	hello: string
	num:   int | *42
}

B: _#NoshowDefn & {hello: "world"}

Reference Cycles

Cue can resolve many cycles, as long as they can resovle to a final, concrete value.

ref-cycle.cue

// a mathematically valid releationship
// but invalid in Cue until one or the other has a value
a: b - 10
b: a + 10

// we need to set a or b and Cue will do the rest
// if not, a cycle error will be reported
a: 100

Structural Cycles

structural.cue

 
#List: {
	val:  _
	next: #List | *null
}

// Cannot do this, it is structural recursion
#Contains: {
	list:  #List
	val:   _
	found: bool | *false

	if list.val == val {
		found: true
	}
	if list.val != val && list.next != null {
		// No recursion!
		found: #Contains & {"list": list.next, "val": val}
	}
}

// We can define an infinite structure
list: #List & {val: "a", next: {val: "b"}}

// results in
list: {
	val: "a"
	next: {
		val:  "b"
		next: null
	}
}