Cue ,是一种开源语言,用于定义,生成和验证各种数据:配置,API,数据库模式,代码……。它能够将数据的结构、约束、数值作为同一层级成员,从而简化配置文件的生成。Cue教程
Cue格式说明
使用//进行单行注释
对象被称为结构体
对象成员称为结构字段
对于没有特殊字符的字段名,可以省略引号
结构字段后面无需,
在列表中的最后一个元素后放置,
最外层的{}可省略
例子:
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 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" ]
结构
结构并不会输出
它的值可能是不确认、不完整的
字段必须完全
使用#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 2 3 4 5 str1: #"avoid using \ to "escape""# str2: """ a nested multiline string goes here """
使用反引号(`)定义原始字符串
1 2 3 4 5 multiline: ` 这是一个 多行字符串 保留了换行和空格 `
使用#”…”# 定义原始字符串,可以避免转义
1 2 3 4 multiline: #" 这种写法可以包含 "引号" 而不需要转义 还可以包含 \反斜杠\ 等特殊字符 "#
使用 + 连接多个字符串
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 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)"
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)" } } } }
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 } ]
条件控制语句 没有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)。
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)
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")
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)
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依赖
模块定义
通过在项目根目录创建cue.mod/module.cue文件来定义模块。通过 cue mod init <模块名>来初始化。
模块名格式通常为domain.com/name或github.com/owner/repo
package组织
一个模块可以包含多个package
允许在一个目录中包含多个package
导入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> 例子:
脚本文件名为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为函数,
&{…}为函数参数
创建data.cue
1 2 3 package foo city: "Amsterdam"
运行: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?
使用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}]
实践
使用 cue import 将已有的yaml转成Cue语言
1 $ cue import ./... -p kube -l '"\(strings.ToCamel(kind))" "\(metadata.name)"' -fR
引入k8s资源的模块
1 2 $ go mod init main $ cue get go k8s.io/api/extensions/v1beta1 -v
导入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
cue trim 可用来自动删除冗余字段,以简化配置文件
参考文档 cue torials cue语法 cue语言入门