CUE 是一门开源的数据约束语言,专门用于处理配置、结构、数据并执行它们。大多数人开始使用 CUE 是因为要做数据验证和生成配置。然后他们继续将 CUE 用于数据模板、运行时输入验证、代码生成、脚本、管道等等。首先你要明白 CUE 并不是一种通常意义的程序语言,它是图灵非完备的编程语言。
CUE 延续了 JSON 超集的思路,额外提供了丰富的类型、表达式、import 语句等常用能力;与 JSONNET 不同,CUE 不支持自定义 function,支持基于 typed feature structure 思路的外置 schema,并通过显式的合一化、分离化操作支持类型和数据的融合,但这样的设定及外置类型推导同样增加了理解难度和编写复杂性。
CUE 项目完全由 Golang 编写,同时背靠 Golang,允许通过 “import” 引入 CUE 提供的必需能力协助用户完成如 encoding、strings、math 等配置编写常用功能。可以说 CUE 既属于 JSON 系的模板语言,同时也带有了很多 Configuration Language 的思考,提供了良好的样本,无论是其语言设计思路,还是基于成熟高级编程语言引入能力的工程方式都值得深入学习。
通过官方二进制安装 CUE
安装包支持多个操作系统,包括 Linux、Window、macOS,可以在 CUE 官方网站 https://cuelang.org/releases 下载。
使用 homebrew 安装
另外,CUE 在 MacOS 和 Linux 上通过 brew 安装。
brew install cue-lang/tap/cue
通过源码安装
首先需要保证安装 Go 1.16 或更高版本,然后执行下面命令即可安装:
go install cuelang.org/go/cmd/cue@latest
安装完成后即可执行 cue
命令:
cue version
cue version v0.4.3-beta.1 darwin/amd64
CUE 是 JSON 的超集, 我们可以像使用 JSON 一样使用 CUE,并具备以下特性:
请先复制以下信息,保存为一个 first.cue
文件:
a: 1.5
a: float
b: 1
b: int
d: [1, 2, 3]
g: {
h: "abc"
}
e: string
接下来,我们以上面这个文件为例子,来学习 CUE 命令行的相关指令:
如何格式化 CUE 文件。下面的命令不仅可以格式化 CUE 文件,还能提示错误的模型,相当好用的命令。
cue fmt first.cue
如何校验模型。除了 cue fmt
,你还可以使用 cue vet
来校验模型。
$ 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
提示我们:这个文件里的 e 这个变量,有数据类型 string
但并没有赋值。
如何计算/渲染结果。cue eval
可以计算 CUE 文件并且渲染出最终结果。我们看到最终结果中并不包含 a: float
和 b: int
,这是因为这两个变量已经被计算填充。其中 e: string
没有被明确的赋值, 故保持不变.
$ cue eval first.cue
a: 1.5
b: 1
d: [1, 2, 3]
g: {
h: "abc"
}
e: string
如何指定渲染的结果。例如,我们仅想知道文件中 b
的渲染结果,则可以使用该参数 -e
。
$ cue eval -e b first.cue
1
如何导出渲染结果。cue export
可以导出最终渲染结果。如果一些变量没有被定义执行该命令将会报错。
$ cue export first.cue
e: incomplete value string
我们更新一下 first.cue
文件,给 e
赋值:
a: 1.5
a: float
b: 1
b: int
d: [1, 2, 3]
g: {
h: "abc"
}
e: string
e: "abc"
然后,该命令就可以正常工作。默认情况下, 渲染结果会被格式化为 JSON 格式。
$ cue export first.cue
{
"a": 1.5,
"b": 1,
"d": [
1,
2,
3
],
"g": {
"h": "abc"
},
"e": "abc"
}
如何导出 YAML 格式的渲染结果。
$ cue export first.cue --out yaml
a: 1.5
b: 1
d:
- 1
- 2
- 3
g:
h: abc
e: abc
如何导出指定变量的结果。
$ cue export -e g first.cue
{
"h": "abc"
}
以上, 你已经学习完所有常用的 CUE 命令行指令。
在熟悉完常用 CUE 命令行指令后,我们来进一步学习 CUE 语言。
先了解 CUE 的数据类型。以下是它的基础数据类型:
// 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
如何自定义 CUE 类型?使用 #
符号来指定一些表示 CUE 类型的变量。
#abc: string
我们将上述内容保存到 second.cue
文件。执行 cue export
不会报 #abc
是一个类型不完整的值。
$ cue export second.cue
{}
你还可以定义更复杂的自定义结构,比如:
#abc: {
x: int
y: string
z: {
a: float
b: bool
}
}
下面,我们开始尝试利用刚刚学习到的知识,来定义 CUE 模版。
parameter
.parameter: {
name: string
image: string
}
保存上述变量到文件 deployment.cue
.
template
同时引用变量 parameter
.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
}]
}}}
}
熟悉 Kubernetes 的你可能已经知道,这是 Kubernetes Deployment 的模板。parameter
为模版的参数部分。
添加上述内容到文件 deployment.cue
.
parameter:{
name: "mytest"
image: "nginx:v1"
}
$ 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
以上,你就得到了一个 Kubernetes Deployment 类型的模板。
设计开放的结构体和数组。如果在数组或者结构体中使用 ...
,则说明该对象为开放的。
[...string]
,说明该对象可以容纳多个字符串元素。如果不添加 ...
, 该对象 [string]
说明数组只能容纳一个类型为 string
的元素。使用运算符 |
来表示两种类型的值。如下所示,变量 a
表示类型可以是字符串或者整数类型。
a: string | int
*
定义变量的默认值。通常它与符号 |
配合使用, 代表某种类型的默认值。如下所示,变量 a
类型为 int
,默认值为 1
。a: *1 | int
?:
定义此类变量。如下所示, a
是可选变量, 自定义 #my
对象中 x
和 z
为可选变量, 而 y
为必填字段。a ?: int
#my: {
x ?: string
y : int
z ?:float
}
选填变量可以被跳过,这经常和条件判断逻辑一起使用。具体来说,如果某些字段不存在,则 CUE 语法为 if _variable_!= _ | _
,如下所示:
parameter: {
name: string
image: string
config?: [...#Config]
}
output: {
...
spec: {
containers: [{
name: parameter.name
image: parameter.image
if parameter.config != _|_ {
config: parameter.config
}
}]
}
...
}
&
来运算两个变量。a: *1 | int
b: 3
c: a & b
保存上述内容到 third.cue
文件。
你可以使用 cue eval
来验证结果:
$ cue eval third.cue
a: 1
b: 3
c: 3
if..else
的逻辑。price: number
feel: *"good" | string
// Feel bad if price is too high
if price > 100 {
feel: "bad"
}
price: 200
保存上述内容到 fourth.cue
文件。
你可以使用 cue eval
来验证结果:
$ cue eval fourth.cue
price: 200
feel: "bad"
另一个示例是将布尔类型作为参数。
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"}]
}
}]
}
...
}
使用 For 循环。我们为了避免减少重复代码,常常使用 For 循环。
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
},
]
}]
}
}
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
},
]
}]
}
}
另外,可以使用 "\( _my-statement_ )"
进行字符串内部计算,比如上面类型循环示例中,获取值的长度等等操作。
比如,使用 strings.Join
方法将字符串数组拼接成字符串。
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, "")
}
}
}
你可以在 CUE 模版中通过 kube/<apiVersion>
导入 kubernetes 的包,就像使用 CUE 内部包一样。
比如,Deployment
可以这样使用:
import (
apps "kube/apps/v1"
)
parameter: {
name: string
}
output: apps.#Deployment
output: {
metadata: name: parameter.name
}
Service
可以这样使用(无需使用别名导入软件包):
import ("kube/v1")
output: v1.#Service
output: {
metadata: {
"name": parameter.name
}
spec: type: "ClusterIP",
}
parameter: {
name: "myapp"
}
甚至已经安装的 CRD 也可以导入使用:
import (
oam "kube/core.oam.dev/v1alpha2"
)
output: oam.#Application
output: {
metadata: {
"name": parameter.name
}
}
parameter: {
name: "myapp"
}