Version: v1.0

Learning CUE

This document will explain more about how to use CUE to encapsulate and abstract a given capability in Kubernetes in detail.

Please make sure you have already learned about Application custom resource before reading the following guide.

Overview

The reasons for KubeVela supports CUE as a first-class solution to design abstraction can be concluded as below:

  • CUE is designed for large scale configuration. CUE has the ability to understand a configuration worked on by engineers across a whole company and to safely change a value that modifies thousands of objects in a configuration. This aligns very well with KubeVela’s original goal to define and ship production level applications at web scale.
  • CUE supports first-class code generation and automation. CUE can integrate with existing tools and workflows naturally while other tools would have to build complex custom solutions. For example, generate OpenAPI schemas wigh Go code. This is how KubeVela build developer tools and GUI interfaces based on the CUE templates.
  • CUE integrates very well with Go. KubeVela is built with GO just like most projects in Kubernetes system. CUE is also implemented in and exposes a rich API in Go. KubeVela integrates with CUE as its core library and works as a Kubernetes controller. With the help of CUE, KubeVela can easily handle data constraint problems.

Pleas also check The Configuration Complexity Curse and The Logic of CUE for more details.

Prerequisites

Please make sure below CLIs are present in your environment:

CUE CLI Basic

Below is the basic CUE data, you can define both schema and value in the same file with the almost same format:

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

CUE is a superset of JSON, we can use it like json with following convenience:

  • C style comments,
  • quotes may be omitted from field names without special characters,
  • commas at the end of fields are optional,
  • comma after last element in list is allowed,
  • outer curly braces are optional.

CUE has powerful CLI commands. Let’s keep the data in a file named first.cue and try.

  • Format the CUE file. If you’re using Goland or similar JetBrains IDE, you can configure save on format instead. This command will not only format the CUE, but also point out the wrong schema. That’s very useful.

    1. cue fmt first.cue
  • Schema Check, besides cue fmt, you can also use vue vet to check schema.

    1. cue vet first.cue
  • Calculate/Render the result. cue eval will calculate the CUE file and render out the result. You can see the results don’t contain a: float and b: int, because these two variables are calculated. While the e: string doesn’t have definitive results, so it keeps as it is.

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

    1. $ cue eval -e b first.cue
    2. 1
  • Export the result. cue export will export the result with final value. It will report an error if some variables are not definitive.

    1. $ cue export first.cue
    2. e: cannot convert incomplete value "string" to JSON:
    3. ./first.cue:9:4

    We can complete the value by giving a value to e, for example:

    1. echo "e: \"abc\"" >> first.cue

    Then, the command will work. By default, the result will be rendered in json format.

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

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

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

For now, you have learned all useful CUE cli operations.

CUE Language Basic

  • Data structure: Below is the basic data structure of CUE.
  1. // float
  2. a: 1.5
  3. // int
  4. b: 1
  5. // string
  6. c: "blahblahblah"
  7. // array
  8. d: [1, 2, 3, 1, 2, 3, 1, 2, 3]
  9. // bool
  10. e: true
  11. // struct
  12. f: {
  13. a: 1.5
  14. b: 1
  15. d: [1, 2, 3, 1, 2, 3, 1, 2, 3]
  16. g: {
  17. h: "abc"
  18. }
  19. }
  20. // null
  21. j: null
  • Define a custom CUE type. You can use a # symbol to specify some variable represents a CUE type.
  1. #abc: string

Let’s name it second.cue. Then the cue export won’t complain as the #abc is a type not incomplete value.

  1. $ cue export second.cue
  2. {}

You can also define a more complex custom struct, such as:

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

It’s widely used in KubeVela to define templates and do validation.

CUE Templating and References

Let’s try to define a CUE template with the knowledge just learned.

  1. Define a struct variable parameter.
  1. parameter: {
  2. name: string
  3. image: string
  4. }

Let’s save it in a file called deployment.cue.

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

People who are familiar with Kubernetes may have understood that is a template of K8s Deployment. The parameter part is the parameters of the template.

Add it into the deployment.cue.

  1. Then, let’s add the value by adding following code block:
  1. parameter:{
  2. name: "mytest"
  3. image: "nginx:v1"
  4. }
  1. Finally, let’s export it in yaml:
  1. $ cue export deployment.cue -e template --out yaml
  2. apiVersion: apps/v1
  3. kind: Deployment
  4. spec:
  5. template:
  6. spec:
  7. containers:
  8. - name: mytest
  9. image: nginx:v1
  10. metadata:
  11. labels:
  12. app.oam.dev/component: mytest
  13. selector:
  14. matchLabels:
  15. app.oam.dev/component: mytest

Advanced CUE Schematic

  • Open struct and list. Using ... in a list or struct means the object is open.

    • A list like [...string] means it can hold multiple string elements. If we don’t add ..., then [string] means the list can only have one string element in it.
    • A struct like below means the struct can contain unknown fields.

      1. {
      2. abc: string
      3. ...
      4. }
  • Operator |, it represents a value could be both case. Below is an example that the variable a could be in string or int type.

  1. a: string | int
  • Default Value, we can use * symbol to represent a default value for variable. That’s usually used with |, which represents a default value for some type. Below is an example that variable a is int and it’s default value is 1.
  1. a: *1 | int
  • Optional Variable. In some cases, a variable could not be used, they’re optional variables, we can use ?: to define it. In the below example, a is an optional variable, x and z in #my is optional while y is a required variable.
  1. a ?: int
  2. #my: {
  3. x ?: string
  4. y : int
  5. z ?:float
  6. }

Optional variables can be skipped, that usually works together with conditional logic. Specifically, if some field does not exit, the CUE grammar is if _variable_ != _|_, the example is like below:

  1. parameter: {
  2. name: string
  3. image: string
  4. config?: [...#Config]
  5. }
  6. output: {
  7. ...
  8. spec: {
  9. containers: [{
  10. name: parameter.name
  11. image: parameter.image
  12. if parameter.config != _|_ {
  13. config: parameter.config
  14. }
  15. }]
  16. }
  17. ...
  18. }
  • Operator &, it used to calculate two variables.
  1. a: *1 | int
  2. b: 3
  3. c: a & b

Saving it in third.cue file.

You can evaluate the result by using cue eval:

  1. $ cue eval third.cue
  2. a: 1
  3. b: 3
  4. c: 3
  • Conditional statement, it’s really useful when you have some cascade operations that different value affects different results. So you can do if..else logic in the template.
  1. price: number
  2. feel: *"good" | string
  3. // Feel bad if price is too high
  4. if price > 100 {
  5. feel: "bad"
  6. }
  7. price: 200

Saving it in fourth.cue file.

You can evaluate the result by using cue eval:

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

Another example is to use bool type as parameter.

  1. parameter: {
  2. name: string
  3. image: string
  4. useENV: bool
  5. }
  6. output: {
  7. ...
  8. spec: {
  9. containers: [{
  10. name: parameter.name
  11. image: parameter.image
  12. if parameter.useENV == true {
  13. env: [{name: "my-env", value: "my-value"}]
  14. }
  15. }]
  16. }
  17. ...
  18. }
  • For Loop: if you want to avoid duplicate, you may want to use for loop.

    • Loop for Map

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

      ```

      a: {

      1. "hello": "Barcelona"
      2. "nihao": "Shanghai"

      }

  1. for k, v in #a {
  2. "\(k)": {
  3. nameLen: len(v)
  4. value: v
  5. }
  6. }
  7. ```
  8. - Loop for Slice
  9. ```
  10. parameter: {
  11. name: string
  12. image: string
  13. env: [...{name:string,value:string}]
  14. }
  15. output: {
  16. ...
  17. spec: {
  18. containers: [{
  19. name: parameter.name
  20. image: parameter.image
  21. env: [
  22. for _, v in parameter.env {
  23. name: v.name
  24. value: v.value
  25. },
  26. ]
  27. }]
  28. }
  29. }
  30. ```

Note that we use "\( _my-statement_ )" for inner calculation in string.

Import CUE Internal Packages

CUE has many internal packages which also can be used in KubeVela.

Below is an example that use strings.Join to concat string list to one string.

  1. import ("strings")
  2. parameter: {
  3. outputs: [{ip: "1.1.1.1", hostname: "xxx.com"}, {ip: "2.2.2.2", hostname: "yyy.com"}]
  4. }
  5. output: {
  6. spec: {
  7. if len(parameter.outputs) > 0 {
  8. _x: [ for _, v in parameter.outputs {
  9. "\(v.ip) \(v.hostname)"
  10. }]
  11. message: "Visiting URL: " + strings.Join(_x, "")
  12. }
  13. }
  14. }

Import Kube Package

KubeVela automatically generates all K8s resources as internal packages by reading K8s openapi from the installed K8s cluster.

You can use these packages with the format kube/<apiVersion> in CUE Template of KubeVela just like the same way with the CUE internal packages.

For example, Deployment can be used as:

  1. import (
  2. apps "kube/apps/v1"
  3. )
  4. parameter: {
  5. name: string
  6. }
  7. output: apps.#Deployment
  8. output: {
  9. metadata: name: parameter.name
  10. }

Service can be used as (import package with an alias is not necessary):

  1. import ("kube/v1")
  2. output: v1.#Service
  3. output: {
  4. metadata: {
  5. "name": parameter.name
  6. }
  7. spec: type: "ClusterIP",
  8. }
  9. parameter: {
  10. name: "myapp"
  11. }

Even the installed CRD works:

  1. import (
  2. oam "kube/core.oam.dev/v1alpha2"
  3. )
  4. output: oam.#Application
  5. output: {
  6. metadata: {
  7. "name": parameter.name
  8. }
  9. }
  10. parameter: {
  11. name: "myapp"
  12. }