CUE is an open source data constraint language designed to handle configurations, structures, data and execute them. Most people start using CUE because they want to do data validation and generate configurations. Then they go on to use CUE for data templates, runtime input validation, code generation, scripting, plumbing, and more. First you need to understand that CUE is not a programming language in the usual sense, it is a Turing non-complete programming language.

CUE

CUE continues the JSON superset idea, and additionally provides rich types, expressions, import statements and other common capabilities. Unlike JSONNET, CUE does not support custom functions, and supports external schema based on the typed feature structure idea, and supports type and data fusion through explicit unification and separation operations, but such settings and external type derivation also increase the difficulty of understanding and writing complexity.

The CUE project is written entirely in Golang, and relies on Golang to allow the necessary capabilities provided by CUE to be introduced through “import” to assist users with common functions such as encoding, strings, math, and other configuration writing. We can say that CUE is both a JSON-based template language and also comes with a lot of Configuration Language thinking, providing a good sample of both its language design ideas and the engineering approach based on the introduction of capabilities of mature high-level programming languages are worth studying in depth.

Installation

Install CUE via official binary

The installer supports multiple operating systems, including Linux, Window, and macOS, and can be downloaded from the official CUE website at https://cuelang.org/releases.

IInstallation with homebrew

Alternatively, CUE is installed via brew on MacOS and Linux.

1
brew install cue-lang/tap/cue

Installation via source code

First you need to ensure that Go 1.16 or higher is installed, then execute the following command to install it.

1
go install cuelang.org/go/cmd/cue@latest

Once the installation is complete, you can execute the cue command.

1
2
cue version
cue version v0.4.3-beta.1 darwin/amd64

CUE Command Line

CUE is a superset of JSON, and we can use CUE as JSON with the following features.

  • C-style comments
  • field names can be enclosed in double quotes, but no special characters are allowed in field names
  • Optional field endings with or without a comma
  • Comma at the end of the last element in an array is allowed
  • Outer curly brackets optional

Please first copy the following information and save it as a first.cue file.

1
2
3
4
5
6
7
8
9
a: 1.5
a: float
b: 1
b: int
d: [1, 2, 3]
g: {
 h: "abc"
}
e: string

Next, let’s use this file above as an example to learn about the CUE command line commands.

  • How to format a CUE file. The following commands not only format the CUE file, but also prompt for the wrong model.

    1
    
    cue fmt first.cue
    
  • How to calibrate the model. In addition to cue fmt, you can also use cue vet to calibrate the model.

    1
    2
    3
    4
    5
    
    $ cue vet first.cue
    some instances are incomplete; use the -c flag to show errors or suppress this message
    
    $ cue vet first.cue -c
    e: incomplete value string
    

    Hint: The variable e in this file has the data type string but is not assigned a value.

  • How to calculate/render the result. cue eval computes the CUE file and renders the final result. We see that the final result does not contain a: float and b: int, because these two variables have already been computed and populated. The e: string is not explicitly assigned, so it remains unchanged.

    1
    2
    3
    4
    5
    6
    7
    8
    
    $ cue eval first.cue
    a: 1.5
    b: 1
    d: [1, 2, 3]
    g: {
    h: "abc"
    }
    e: string
    
  • How to specify the result of the rendering. For example, if we only want to know the rendering result of b in the file, we can use this parameter -e.

    1
    2
    
    $ cue eval -e b first.cue
    1
    
  • How to export the rendering result. cue export exports the final rendering results. If some variables are not defined executing this command will give an error.

    1
    2
    
    $ cue export first.cue
    e: incomplete value string  
    

    Let’s update the first.cue file and assign a value to e.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    a: 1.5
    a: float
    b: 1
    b: int
    d: [1, 2, 3]
    g: {
    h: "abc"
    }
    e: string
    e: "abc"
    

    Then, the command works properly. By default, the rendered results are formatted as JSON.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    $ cue export first.cue
    {
        "a": 1.5,
        "b": 1,
        "d": [
            1,
            2,
            3
        ],
        "g": {
            "h": "abc"
        },
        "e": "abc"
    }
    
  • How to export rendered results in YAML format.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    $ cue export first.cue --out yaml
    a: 1.5
    b: 1
    d:
    - 1
    - 2
    - 3
    g:
    h: abc
    e: abc
    
  • How to export the result of the specified variable.

    1
    2
    3
    4
    
    $ cue export -e g first.cue
    {
        "h": "abc"
    }
    

Above, you have learned all the common CUE command line commands.

Data Types

After familiarizing yourself with the common CUE command line instructions, let’s learn more about the CUE language.

Let’s start by understanding CUE’s data types. Here are its basic data types.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// float
a: 1.5

// int
b: 1

// string
c: "blahblahblah"

// array
d: [1, 2, 3, 1, 2, 3, 1, 2, 3]

// bool
e: true

// struct
f: {
 a: 1.5
 b: 1
 d: [1, 2, 3, 1, 2, 3, 1, 2, 3]
 g: {
  h: "abc"
 }
}

// null
j: null

How do I customize the CUE type? Use the # symbol to specify some variables that indicate the CUE type.

1
#abc: string

We save the above to a second.cue file. Executing cue export will not report that #abc is an incomplete type value.

1
2
$ cue export second.cue
{}

You can also define more complex custom structures, such as

1
2
3
4
5
6
7
8
#abc: {
  x: int
  y: string
  z: {
    a: float
    b: bool
  }
}

CUE Templates

Next, let’s start trying to define the CUE template using what we just learned.

  1. Define the structure variable parameter.

    1
    2
    3
    4
    
    parameter: {
    name: string
    image: string
    }
    

    Save the above variables to the file deployment.cue .

  2. Define more complex structural variables template and refer to the variable parameter.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    template: {
    apiVersion: "apps/v1"
    kind:       "Deployment"
    spec: {
    selector: matchLabels: {
    "app.oam.dev/component": parameter.name
    }
    template: {
    metadata: labels: {
        "app.oam.dev/component": parameter.name
    }
    spec: {
        containers: [{
        name:  parameter.name
        image: parameter.image
        }]
    }}}
    }
    

    As you may already know if you are familiar with Kubernetes, this is the template for Kubernetes Deployment. parameter is the parameters section of the template.

    Add the above to the file deployment.cue.

  3. Then, we complete the variable assignment by updating the

    1
    2
    3
    4
    
    parameter:{
    name: "mytest"
    image: "nginx:v1"
    }
    
  4. Finally, the rendered results are exported in YAML format.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    $ cue export deployment.cue -e template --out yaml
    
    apiVersion: apps/v1
    kind: Deployment
    spec:
    selector:
        matchLabels:
        app.oam.dev/component: mytest
    template:
        metadata:
        labels:
            app.oam.dev/component: mytest
        spec:
        containers:
        - name: mytest
            image: nginx:v1
    

    Above, you get a template for the Kubernetes Deployment type.

Other Uses

  • Design open structures and arrays. If ... is used in an array or structure, the object is open.

    • The array object [... .string], indicating that the object can hold multiple string elements. If you do not add ..., the object [string] indicates that the array can hold only one element of type string.

    • The structure shown below illustrates that it can contain unknown fields.

      1
      2
      3
      4
      
      {
      abc: string   
      ...
      }
      
  • Use the operator | to represent two types of values. As shown below, the variable a indicates that the type can be either a string or an integer type.

    1
    
    a: string | int
    
  • Use the symbol * to define the default value of a variable. It is usually used in conjunction with the symbol | to represent the default value of a certain type. As shown below, the variable a is of type int and has a default value of 1.

    1
    
    a: *1 | int
    
  • Make some variables optional. In some cases, variables that are not necessarily used are optional variables, and we can use ? : to define such variables. As shown below, a is an optional variable, x and z are optional in the custom #my object, and y is a required field.

    1
    2
    3
    4
    5
    6
    7
    8
    
    
    a ?: int
    
    #my: {
    x ?: string
    y : int
    z ?:float
    }
    

    Optional variables can be skipped, which is often used in conjunction with conditional judgment logic. Specifically, if some field does not exist, the CUE syntax is if _variable_! = _ | _, as follows.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    
    parameter: {
        name: string
        image: string
        config?: [...#Config]
    }
    output: {
        ...
        spec: {
            containers: [{
                name:  parameter.name
                image: parameter.image
                if parameter.config != _|_ {
                    config: parameter.config
                }
            }]
        }
        ...
    }
    
  • Use the operator & to operate on two variables.

    1
    2
    3
    
    a: *1 | int
    b: 3
    c: a & b
    

    Save the above to the third.cue file.

    You can use cue eval to verify the results.

    1
    2
    3
    4
    
    $ cue eval third.cue
    a: 1
    b: 3
    c: 3
    
  • Need to perform conditional judgments. Conditional judgments are very useful when you perform some cascading operations where different values affect different results. Therefore, you can execute if..else logic in your templates.

    1
    2
    3
    4
    5
    6
    7
    8
    
    
    price: number
    feel: *"good" | string
    // Feel bad if price is too high
    if price > 100 {
        feel: "bad"
    }
    price: 200
    

    Save the above to a fourth.cue file.

    You can use cue eval to verify the results.

    1
    2
    3
    
    $ cue eval fourth.cue
    price: 200
    feel:  "bad"
    

    Another example is to include a boolean type as a parameter.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    parameter: {
        name:   string
        image:  string
        useENV: bool
    }
    output: {
        ...
        spec: {
            containers: [{
                name:  parameter.name
                image: parameter.image
                if parameter.useENV == true {
                    env: [{name: "my-env", value: "my-value"}]
                }
            }]
        }
        ...
    }
    
  • Use a For loop. We often use For loops to avoid duplicating code.

    • Mapping iterations.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      
      parameter: {
          name:  string
          image: string
          env: [string]: string
      }
      output: {
          spec: {
              containers: [{
                  name:  parameter.name
                  image: parameter.image
                  env: [
                      for k, v in parameter.env {
                          name:  k
                          value: v
                      },
                  ]
              }]
          }
      }
      
    • Type iteration

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      
      #a: {
          "hello": "Barcelona"
          "nihao": "Shanghai"
      }
      
      for k, v in #a {
          "\(k)": {
              nameLen: len(v)
              value:   v
          }
      }
      
    • Slicing iteration.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      
      parameter: {
          name:  string
          image: string
          env: [...{name:string,value:string}]
      }
      output: {
      ...
          spec: {
              containers: [{
                  name:  parameter.name
                  image: parameter.image
                  env: [
                      for _, v in parameter.env {
                          name:  v.name
                          value: v.value
                      },
                  ]
              }]
          }
      }
      

Alternatively, you can use "\( _my-statement_ )" to perform internal string calculations, such as getting the length of a value in the type loop example above, and so on.

Importing packages

For example, use the strings.Join method to stitch an array of strings into a string.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import ("strings")

parameter: {
    outputs: [{ip: "1.1.1.1", hostname: "xxx.com"}, {ip: "2.2.2.2", hostname: "yyy.com"}]
}
output: {
    spec: {
        if len(parameter.outputs) > 0 {
            _x: [ for _, v in parameter.outputs {
                "\(v.ip) \(v.hostname)"
            }]
            message: "Visiting URL: " + strings.Join(_x, "")
        }
    }
}

You can import kubernetes packages in the CUE template via kube/<apiVersion>, just as you would with CUE internal packages.

For example, Deployment can be used like this.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import (
   apps "kube/apps/v1"
)

parameter: {
    name:  string
}

output: apps.#Deployment
output: {
    metadata: name: parameter.name
}

Service can be used this way (no need to import packages with aliases).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import ("kube/v1")

output: v1.#Service
output: {
    metadata: {
        "name": parameter.name
    }
    spec: type: "ClusterIP",
}

parameter: {
    name:  "myapp"
}

Even installed CRDs can be imported and used.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import (
  oam  "kube/core.oam.dev/v1alpha2"
)

output: oam.#Application
output: {
 metadata: {
  "name": parameter.name
 }
}

parameter: {
 name:  "myapp"
}

Ref