Version: v1.0

CUE 入门

本章节将详细介绍关于如何使用 CUE 封装和抽象 Kubernetes 中已有的能力。

开始阅读本章节前,请确保已经了解 Application 资源。

概述

KubeVela 将 CUE 作为抽象最优方案的主要原因如下:

  • CUE 本身就是为大规模配置而设计。 CUE 能够感知非常复杂的配置文件,并且能够安全地更改可修改配置中成千上万个对象的值。这非常符合 KubeVela 的最初目标,即以 web-scale 方式定义和交付生产级别的应用程序(web-scale,是一种软件设计方法,主要包含可扩展性、一致性、容忍度和版本控制等)。
  • CUE 支持一流的代码生成和自动化。 CUE 原生支持与现有工具以及工作流进行集成,反观其他工具则需要自定义复杂的方案才能实现。例如,需要手动使用 Go 代码生成 OpenAPI 模式。KubeVela 也是依赖 CUE 该特性进行构建开发工具和GUI界面。
  • CUE与Go完美集成。 KubeVela 像 Kubernetes 系统中的大多数项目一样使用 GO 进行开发。CUE 已经在 Go 中实现并提供了丰富的 API 。 KubeVela 以 CUE 为核心实现 Kubernetes 控制器。 借助 CUE KubeVela 可以轻松处理数据约束问题。

更多细节请查看 The Configuration Complexity Curse 以及 The Logic of CUE

前提

请确保你的环境中已经安装如下命令行:

CUE 命令行基础

我们可以使用几乎相同的格式在同一个文件中定义模型和数据,以下为 CUE 基础数据类型:

  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 是 JSON 的超集, 我们可以像使用 json 一样使用 CUE,同时具备以下便利性:

  • C 样式的注释,
  • 字段名称可以省略引号且不带特殊字符,
  • 字段末尾逗号可选,
  • 允许列表中最后一个元素末尾带逗号,
  • 外花括号可选。

CUE 拥有强大的命令行。请将数据保存到 first.cue 文件并尝试使用命令行。

  • 格式化 CUE 文件。如果你使用 Goland 或者类似 JetBrains IDE, 可以参考该文章配置自动格式化插件 使用 Goland 设置 cuelang 的自动格式化。 该命令不仅可以格式化 CUE 文件,还能指出错误的模型,相当好用的命令。

    1. cue fmt first.cue
  • 模型校验。 除了 cue fmt,你还可以使用 vue vet 来校验模型.

    1. cue vet first.cue
  • 计算/渲染结果。 cue eval 可以计算 CUE 文件并且渲染出最终结果。 我们看到最终结果中并不包含 a: floatb: int,这是因为这两个变量已经被计算填充。 其中 e: string 没有被明确的赋值, 故保持不变.

    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
  • 渲染指定结果。例如,我们仅想知道文件中 b 的渲染结果,则可以使用该参数 -e

    1. $ cue eval -e b first.cue
    2. 1
  • 导出渲染结果。 cue export 可以导出最终渲染结果。如果一些变量没有被定义执行该命令将会报错。

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

    我们可以通过给 e 赋值来完成赋值,例如:

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

    然后,该命令就可以正常工作。默认情况下, 渲染结果会被格式化为 json 格式。

    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. }
  • 导出 YAML 格式渲染结果。

    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
  • 导出指定变量的结果。

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

至此, 你已经学习完所有常用 CUE 命令行参数。

CUE 语言基础

  • 数据类型: 以下为 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
  • 自定义 CUE 类型。你可以使用 # 符号来指定一些表示 CUE 类型的变量。
  1. #abc: string

我们将上述内容保存到 second.cue 文件。 执行 cue export 不会报 #abc 是一个类型不完整的值。

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

你还可以定义更复杂的自定义结构,比如:

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

自定义结构在 KubeVela 中被广泛用于定义模板和进行验证。

CUE 模板和引用

我们开始尝试利用刚刚学习知识来定义 CUE 模版。

  1. 定义结构体变量 parameter.
  1. parameter: {
  2. name: string
  3. image: string
  4. }

保存上述变量到文件 deployment.cue.

  1. 定义更复杂的结构变量 template 同时引用变量 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. }

熟悉 Kubernetes 的人可能已经知道,这是 Kubernetes Deployment 的模板。 parameter 为模版的参数部分。

添加上述内容到文件 deployment.cue.

  1. 随后, 我们通过添加以下内容来完成变量赋值:
  1. parameter:{
  2. name: "mytest"
  3. image: "nginx:v1"
  4. }
  1. 最后, 导出渲染结果为 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

高级 CUE 设计

  • 开放的结构体和列表。在列表或者结构体中使用 ... 说明该对象为开放的。

    • 列表对象 [...string] ,说明该对象可以容纳多个字符串元素。 如果不添加 ..., 该对象 [string] 说明列表只能容纳一个类型为 string 的元素。
    • 如下所示的结构体说明可以包含未知字段。

      1. {
      2. abc: string
      3. ...
      4. }
  • 运算符 |, 它可以表示两种类型的值。如下所示,变量 a 表示类型可以是字符串或者整数类型。

  1. a: string | int
  • 默认值, 我们可以使用符号 * 定义变量的默认值。通常与符号 | 配合使用, 代表某种类型的默认值。如下所示,变量 a 类型为 int,默认值为 1
  1. a: *1 | int
  • 选填变量。 某些情况下,一些变量不一定被使用,这些变量就是可选变量,我们可以使用 ?: 定义此类变量。 如下所示, a 是可选变量, 自定义 #my 对象中 xz 为可选变量, 而 y 为必填字段。
  1. a ?: int
  2. #my: {
  3. x ?: string
  4. y : int
  5. z ?:float
  6. }

选填变量可以被跳过,这经常和条件判断逻辑一起使用。 具体来说,如果某些字段不存在,则 CUE 语法为 if _variable_!= _ | _ ,如下所示:

  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. }
  • 运算符 &,该运算符用来运算两个变量。
  1. a: *1 | int
  2. b: 3
  3. c: a & b

保存上述内容到 third.cue 文件。

你可以使用 cue eval 来验证结果:

  1. $ cue eval third.cue
  2. a: 1
  3. b: 3
  4. c: 3
  • 条件判断。 当你执行一些级联操作时,不同的值会影响不同的结果,条件判断就非常有用。 因此,你可以在模版中执行 if..else 的逻辑。
  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

保存上述内容到 fourth.cue 文件。

你可以使用 cue eval 来验证结果:

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

另一个示例是将布尔类型作为参数。

  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循环。 我们为了避免重复可以使用 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. }
    • 类型循环

      ```

      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. - 切片循环
  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. ```

备注, 可以使用 "\( _my-statement_ )" 进行字符串内部计算,比如上面类型循环示例中,获取值的长度等等操作。

导入 CUE 内部包

CUE 有很多 internal packages 可以被 KubeVela 使用。

如下所示,使用 strings.Join 方法将字符串列表拼接成字符串。

  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. }

导入 Kubernetes 包

KubeVela 会从 Kubernetes 集群中读取 OpenApi,并将 Kubernetes 所有资源自动构建为内部包。

你可以在 KubeVela 的 CUE 模版中通过 kube/<apiVersion> 导入这些包,就像使用 CUE 内部包一样。

比如,Deployment 可以这样使用:

  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 可以这样使用(无需使用别名导入软件包):

  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. }

甚至已经安装的 CRD 也可以正常使用:

  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. }