Cue,是一种开源语言,用于定义,生成和验证各种数据:配置,API,数据库模式,代码……。它能够将数据的结构、约束、数值作为同一层级成员,从而简化配置文件的生成。
Cue教程

Cue格式说明

  1. 使用//进行单行注释
  2. 对象被称为结构体
  3. 对象成员称为结构字段
  4. 对于没有特殊字符的字段名,可以省略引号
  5. 结构字段后面无需,
  6. 在列表中的最后一个元素后放置,
  7. 最外层的{}可省略

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 结构、约束、数据

1
2
3
4
5
6
// 结构
album: {
title: string
year: int
live: bool
}
1
2
3
4
5
6
// 约束
album: {
title: string
year: >1950
live: false
}
1
2
3
4
5
6
// 数据
album: {
title: "Houses of the Holy"
year: 1973
live: false
}

Cue的最佳实践:从开放的结构模式开始,限制上下文可能性,最终具体到数据实例。
Cue哲学:为了保证唯一性,Cue的数据不会被覆盖。

Cue核心规则

  1. 数据可被重复定义,但必须值保持一致
  2. 结构字段可以被更强限制覆盖
  3. 结构的字段会被合并,如果是列表,必须严格匹配
  4. 规则可被递规应用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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"
    ]

结构

  1. 结构并不会输出
  2. 它的值可能是不确认、不完整的
  3. 字段必须完全

使用#mydef来定义结构,使用...来定义一个开放的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#Album: {
artist: string
title: string
year: int

// ... uncomment to open, 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 (uncomment to trigger error)
}

1
2
3
4
5
6
7
8
9
#Person: {
name: string
... // open struct
}

Jim: #Person & {
name: "jim"
age: 12
}

约束

约束与数值使用&字符进行连接时,会将值进行校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// conjunctions on a field
n: int & >0 & <100
n: 23

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

#Def1: {
foo: string
ans: int
}

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

替换

使用|可以实现支持多种结构。同时它也可以为出错值设置替换值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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
}

默认值与可选

使用*来设置默认值, ?设置可选字段

1
2
3
4
5
6
s: {
// field with a default
hello: string | *"world" | "apple"
// an optional integer
count?: int
}

开放模式与封闭模式

开放模式意味着结构可以扩展,关闭模式意味着不能扩展。 默认情况下,结构是开放模式,定义是封闭模式。 可以通过定义的最后添加...来申明开放模式定义;另外通过过close强制为结构体设置为关闭模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Open definition
#d: {
foo: "bar"
... // must be last
}

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

jim: {
name: "Jim"
}
jim: {
age: 12
}

推荐从基础定义开始,复用定义

在编写Cue时,推荐从基础定义开始,这样能够有更好的复用能力。

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
28
29
30
31
32
33
#Base: {
name: string
kind: string
... // so it can be extended
}
#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 a conjunction and embedding
#Schema: #Base & {
// "embed" meta and permissions
#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)
}

定义多行字符串

  1. 使用"""来定义多行字符串
    1
    2
    3
    4
    5
    str1: #"avoid using \ to "escape""#
    str2: """
    a nested multiline
    string goes here
    """
  2. 使用反引号(`)定义原始字符串
    1
    2
    3
    4
    5
    multiline: `
    这是一个
    多行字符串
    保留了换行和空格
    `
  3. 使用#”…”# 定义原始字符串,可以避免转义
    1
    2
    3
    4
    multiline: #"
    这种写法可以包含 "引号" 而不需要转义
    还可以包含 \反斜杠\ 等特殊字符
    "#
  4. 使用 + 连接多个字符串
    1
    2
    3
    multiline: "第一行\n" +
    "第二行\n" +
    "第三行"

List

List 可被定义为开放模式,这样便可与其它数据进行合并,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
empty: []
any: [...]
ints: [...int]
nested: [...[...string]]

opened: ints & [1,2,...]
closed: ints & [1,2,3]

// list of for constrained ints
ip: 4 * [uint8]
// sets the first element
tendot: ip & [10, ...uint8]
// uses constraint as second element
one72: ip & [172, >=16 & <=32, ...]

mixed: any & [...] & ["a",1, { foo: "bar" }]
join: [1,2] + [3,4]
Join: opened & join

Struct

结构体是Cue的主要内容,也是最终数据的输出。如上介绍,默认情况下它是开放模式。除了使用Json类型形式进行设置值,还可通过级联:来设置,如a: hello: "world"

1
2
3
4
5
6
7
8
9
10
11
12
// an open struct
a: {
foo: "bar"
}

// shorthand nested field
a: hello: "world"

// a closed struct
b: close({
left: "right"
})

模式匹配约束

模式匹配允许您为与模式匹配的标签指定约束。可以将约束应用于字符串标签,并使用标识符来设置字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#schema: {
name: string
ans: string
num: int | *42
}

// match elem fields and alias labels to Name,
// unify with schema, set name to Name by label
elems: [Name=_]: #schema & { name: Name }

elems: {
one: {
ans: "solo"
num: 1
}
two: {
ans: "life"
}
}

elems: other: { ans: "id", num: 23 }

表达式

  1. 引用字段,使用\(**)显用其它字段
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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)"
  2. Cue也能够为通过\(**)来设置key
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    apps: ["nginx", "express", "postgres"]
    #labels: [string]: string
    stack: {
    for i, app in apps {
    "\(app)": {
    name: app
    labels: #labels & {
    app: "foo"
    tier: "\(i)"
    }
    }
    }
    }
  3. List遍历
    遍历List数据格式如下:[ for key, val in <iterable> [condition] { production } ]
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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 } ]
  4. 条件控制语句
    没有else,所有判断都会被执行
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    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"
    }
    }

标准库

Cue的标准库中包含了很多的帮助包(helper packages)。

  1. Encoding
    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
    package stdlib

    import (
    "encoding/json"
    )

    data: """
    {
    "hello": "world",
    "list": [ 1, 2 ],
    "nested": {
    "foo": "bar"
    }
    }
    """

    jval: json.Unmarshal(data)

    val: {
    hello: "world"
    list: [1,2]
    nested: foo: "bar"
    }

    cjson: json.Marshal(val)
  2. Strings
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package stdlib

    import "strings"

    s: "HelloWorld"

    u: strings.ToUpper(s)
    l: strings.ToLower(s)

    line: "Cue stands for configure, unify, execute"
    words: strings.Split(line, " ")
    lined: strings.Join(words, " ")

    haspre: strings.HasPrefix(line, "Cue")
    index: strings.Index(line, "unify")
  3. List
    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
    28
    29
    package stdlib

    import "list"

    l1: [1,2,3,4,5]
    l2: ["c","b","a"]

    // constrain length
    l2: list.MinItems(1)
    l2: list.MaxItems(3)

    // slice a list
    l3: list.Slice(l1, 2,4)

    // get the sum and product
    sum: list.Sum(l1)
    prd: list.Product(l1)

    // linear search for list (no binary)
    lc: list.Contains(l1, 2)

    // sort a list
    ls: list.Sort(l2, list.Ascending)
    l2s: list.IsSorted(l2, list.Ascending)
    lss: list.IsSorted(ls, list.Ascending)

    // Flatten a list
    ll: [1,[2,3],[4,[5]]]
    lf: list.FlattenN(ll, 1)
  4. Constrain
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    package stdlib

    import (
    "net"
    "time"
    )

    // string with ip format
    ip: net.IPv4
    ip: "10.1.2.3"

    // string with time format
    ts: time.Format(time.ANSIC)
    ts: "Mon Jan 2 15:04:05 2006"

模块和包

cuelang有module和package系统,可以import依赖

  1. 模块定义
  • 通过在项目根目录创建cue.mod/module.cue文件来定义模块。通过 cue mod init <模块名>来初始化。
  • 模块名格式通常为domain.com/namegithub.com/owner/repo
  1. package组织
  • 一个模块可以包含多个package
  • 允许在一个目录中包含多个package
  1. 导入package
  • 使用绝对路径导入,不允许相对路径导入
  • 导入时可以省略domain,表示导入内置标准包
  • 可以在导入时重命名包
  • 同一个包内的定义和值可以直接访问,无需导入

例子

1
2
3
4
5
6
7
8
9
package deploy
import (
p_spec "douhua.com/name/devops/pkg/spec:spec"
)

a: p_spec.#A & {
name: "test"
}

使用Cue制作脚本命令工具

Cue 拥有制作脚本命令工具的功能,它有一个工具层,可用来执行脚本、读写文件以及网络访问等。
规范:

  • 脚本文件以_tool.cue结尾
  • 执行命令为cue cmd <name> or cue <name>
    例子:
  1. 脚本文件名为ex_tool.cue
    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
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    package foo

    import (
    "tool/cli"
    "tool/exec"
    "tool/file"
    )

    // moved to the data.cue file to show how we can reference "pure" Cue files
    // city: "Amsterdam"

    // A command named "prompter"
    command: prompter: {

    // save transcript to this file
    var: file: *"out.txt" | string @tag(file) // you can use "-t flag=filename.txt" to change the output file, see "cue help injection" for more details

    // prompt the user for some input
    ask: cli.Ask & {
    prompt: "What is your name?"
    response: string
    }

    // run an external command, starts after ask
    echo: exec.Run & {
    // note the reference to ask and city here
    cmd: ["echo", "Hello", ask.response + "!", "Have you been to", city + "?"]
    stdout: string // capture stdout, don't print to the terminal
    }

    // append to a file, starts after echo
    append: file.Append & {
    filename: var.file
    contents: echo.stdout // becuase we reference the echo task
    }

    // also starts after echo, and concurrently with append
    print: cli.Print & {
    text: echo.stdout // write the output to the terminal since we captured it previously
    }
    }
  • prompter为命令名
  • ask/echo/append/print为唯一标识
  • cli.Ask/exec.Run/file.Append为函数,
  • &{…}为函数参数
  1. 创建data.cue
    1
    2
    3
    package foo

    city: "Amsterdam"
  2. 运行:cue cmd prompter
    1
    2
    3
    4
    5
    $ cue cmd prompter
    What is your name? he
    Hello he! Have you been to Amsterdam?
    $ cat out.txt
    Hello he! Have you been to Amsterdam?
  3. 使用cuelang exec.Run 执行多行代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    package foo

    import (
    "tool/exec"
    )

    command: {
    hello: {
    script: #"""
    #!/bin/bash
    echo hello world
    key1="hello you"
    ls
    echo $key1
    """#
    run: cmd: _
    run: exec.Run & {
    cmd: "bash"
    stdin: script
    }
    }
    }

Tips

  • A & B === B & A
  • A === A
  • 路径短写:{a : {b: {c: 5}}} == a: b: c: 5
  • 多种类型:a | b | c
  • 默认值:number | *1
  • 算术: 4 + 5
  • 变量引用:”Hello (person)”
  • 列表遍历:[ x for x in y ]
  • cue 执行 当前目录下的cue文件及父目录下同一个package的cue文件
  • cue ./… 以上目录 + 遍历当前目录的子目录下的cue文件
  • _开头的变量不会在输出结果中显示,作为局部变量
  • [Name=_] 可用来定义一个模板,其中Name匹配任意字段。例如:
    1
    2
    3
    application: [Name=_]: {
    name: string | *Name
    }
  • _|_ 可判断是否存在。例如:if _variable != _|_ { // … }
    1
    2
    3
    4
    5
    6
    7
    a ?: string
    if a == _|_ {
    b: "a"
    }
    // 结果为
    // cue export a.cue
    // b: "a"
    1
    2
    3
    4
    5
    6
    7
    8
    a: string
    if a == _|_ {
    b: "a"
    }
    // 结果为
    // cue eval a.cue
    // a: string
    // b: "a"
  • 定义映射:map: [string]: string
  • 定义切片:slice: […{name:string,value:string}]

实践

Go To Cue

  1. 使用 cue import 将已有的yaml转成Cue语言
    1
    $ cue import ./... -p kube -l '"\(strings.ToCamel(kind))" "\(metadata.name)"' -fR 
  2. 引入k8s资源的模块
    1
    2
    $ go mod init main
    $ cue get go k8s.io/api/extensions/v1beta1 -v
  3. 导入k8s资源模块,并创建资源
    1
    2
    3
    4
    5
    6
    7
    package kube
    import (
    "k8s.io/api/core/v1"
    "k8s.io/api/extensions/v1beta1"
    )
    service <Name>: v1.Service
    deployment <Name>: v1beta1.Deployment
  4. cue trim 可用来自动删除冗余字段,以简化配置文件

参考文档

cue torials
cue语法
cue语言入门