Version: v1.0

调试, 测试 以及 Dry-run

基于具有强大灵活抽象能力的 CUE 定义的模版来说,调试、测试以及 dry-run 非常重要。本教程将逐步介绍如何进行调试。

前提

请确保你的环境已经安装以下 CLI :

定义 Definition 和 Template

我们建议将 Definition Object 定义拆分为两个部分:CRD 部分和 CUE 模版部分。前面的拆分会帮忙我们对 CUE 模版进行调试、测试以及 dry-run 操作。

我们将 CRD 部分保存到 def.yaml 文件。

  1. apiVersion: core.oam.dev/v1beta1
  2. kind: ComponentDefinition
  3. metadata:
  4. name: microservice
  5. annotations:
  6. definition.oam.dev/description: "Describes a microservice combo Deployment with Service."
  7. spec:
  8. workload:
  9. definition:
  10. apiVersion: apps/v1
  11. kind: Deployment
  12. schematic:
  13. cue:
  14. template: |

同时将 CUE 模版部分保存到 def.cue 文件,随后我们可以使用 CUE 命令行(cue fmt / cue vet)格式化和校验 CUE 文件。

  1. output: {
  2. // Deployment
  3. apiVersion: "apps/v1"
  4. kind: "Deployment"
  5. metadata: {
  6. name: context.name
  7. namespace: "default"
  8. }
  9. spec: {
  10. selector: matchLabels: {
  11. "app": context.name
  12. }
  13. template: {
  14. metadata: {
  15. labels: {
  16. "app": context.name
  17. "version": parameter.version
  18. }
  19. }
  20. spec: {
  21. serviceAccountName: "default"
  22. terminationGracePeriodSeconds: parameter.podShutdownGraceSeconds
  23. containers: [{
  24. name: context.name
  25. image: parameter.image
  26. ports: [{
  27. if parameter.containerPort != _|_ {
  28. containerPort: parameter.containerPort
  29. }
  30. if parameter.containerPort == _|_ {
  31. containerPort: parameter.servicePort
  32. }
  33. }]
  34. if parameter.env != _|_ {
  35. env: [
  36. for k, v in parameter.env {
  37. name: k
  38. value: v
  39. },
  40. ]
  41. }
  42. resources: {
  43. requests: {
  44. if parameter.cpu != _|_ {
  45. cpu: parameter.cpu
  46. }
  47. if parameter.memory != _|_ {
  48. memory: parameter.memory
  49. }
  50. }
  51. }
  52. }]
  53. }
  54. }
  55. }
  56. }
  57. // Service
  58. outputs: service: {
  59. apiVersion: "v1"
  60. kind: "Service"
  61. metadata: {
  62. name: context.name
  63. labels: {
  64. "app": context.name
  65. }
  66. }
  67. spec: {
  68. type: "ClusterIP"
  69. selector: {
  70. "app": context.name
  71. }
  72. ports: [{
  73. port: parameter.servicePort
  74. if parameter.containerPort != _|_ {
  75. targetPort: parameter.containerPort
  76. }
  77. if parameter.containerPort == _|_ {
  78. targetPort: parameter.servicePort
  79. }
  80. }]
  81. }
  82. }
  83. parameter: {
  84. version: *"v1" | string
  85. image: string
  86. servicePort: int
  87. containerPort?: int
  88. // +usage=Optional duration in seconds the pod needs to terminate gracefully
  89. podShutdownGraceSeconds: *30 | int
  90. env: [string]: string
  91. cpu?: string
  92. memory?: string
  93. }

以上操作完成之后,使用该脚本 hack/vela-templates/mergedef.shdef.yamldef.cue 合并到完整的 Definition 对象中。

  1. $ ./hack/vela-templates/mergedef.sh def.yaml def.cue > microservice-def.yaml

调试 CUE 模版

使用 cue vet 进行校验

  1. $ cue vet def.cue
  2. output.metadata.name: reference "context" not found:
  3. ./def.cue:6:14
  4. output.spec.selector.matchLabels.app: reference "context" not found:
  5. ./def.cue:11:11
  6. output.spec.template.metadata.labels.app: reference "context" not found:
  7. ./def.cue:16:17
  8. output.spec.template.spec.containers.name: reference "context" not found:
  9. ./def.cue:24:13
  10. outputs.service.metadata.name: reference "context" not found:
  11. ./def.cue:62:9
  12. outputs.service.metadata.labels.app: reference "context" not found:
  13. ./def.cue:64:11
  14. outputs.service.spec.selector.app: reference "context" not found:
  15. ./def.cue:70:11

常见错误 reference "context" not found 主要发生在 context,该部分是仅在 KubeVela 控制器中存在的运行时信息。我们可以在 def.cue 中模拟 context ,从而对 CUE 模版进行 end-to-end 的校验操作。

注意,完成校验测试之后需要清除所有模拟数据。

  1. ... // existing template data
  2. context: {
  3. name: string
  4. }

随后执行命令:

  1. $ cue vet def.cue
  2. some instances are incomplete; use the -c flag to show errors or suppress this message

该错误 reference "context" not found 已经被解决,但是 cue vet 仅对数据类型进行校验,这还不能证明模版逻辑是准确对。因此,我们需要使用 cue vet -c 完成最终校验:

  1. $ cue vet def.cue -c
  2. context.name: incomplete value string
  3. output.metadata.name: incomplete value string
  4. output.spec.selector.matchLabels.app: incomplete value string
  5. output.spec.template.metadata.labels.app: incomplete value string
  6. output.spec.template.spec.containers.0.image: incomplete value string
  7. output.spec.template.spec.containers.0.name: incomplete value string
  8. output.spec.template.spec.containers.0.ports.0.containerPort: incomplete value int
  9. outputs.service.metadata.labels.app: incomplete value string
  10. outputs.service.metadata.name: incomplete value string
  11. outputs.service.spec.ports.0.port: incomplete value int
  12. outputs.service.spec.ports.0.targetPort: incomplete value int
  13. outputs.service.spec.selector.app: incomplete value string
  14. parameter.image: incomplete value string
  15. parameter.servicePort: incomplete value int

此时,命令行抛出运行时数据不完整的异常(主要因为 contextparameter 字段字段中还有设置值),现在我们填充更多的模拟数据到 def.cue 文件:

  1. context: {
  2. name: "test-app"
  3. }
  4. parameter: {
  5. version: "v2"
  6. image: "image-address"
  7. servicePort: 80
  8. containerPort: 8000
  9. env: {"PORT": "8000"}
  10. cpu: "500m"
  11. memory: "128Mi"
  12. }

此时,执行以下命令行没有抛出异常,说明逻辑校验通过:

  1. cue vet def.cue -c

使用 cue export 校验已渲染的资源

该命令行 cue export 将会渲染结果以 YAML 格式导出:

  1. $ cue export -e output def.cue --out yaml
  2. apiVersion: apps/v1
  3. kind: Deployment
  4. metadata:
  5. name: test-app
  6. namespace: default
  7. spec:
  8. selector:
  9. matchLabels:
  10. app: test-app
  11. template:
  12. metadata:
  13. labels:
  14. app: test-app
  15. version: v2
  16. spec:
  17. serviceAccountName: default
  18. terminationGracePeriodSeconds: 30
  19. containers:
  20. - name: test-app
  21. image: image-address
  1. $ cue export -e outputs.service def.cue --out yaml
  2. apiVersion: v1
  3. kind: Service
  4. metadata:
  5. name: test-app
  6. labels:
  7. app: test-app
  8. spec:
  9. selector:
  10. app: test-app
  11. type: ClusterIP

测试使用 Kube 包的 CUE 模版

KubeVela 将所有内置 Kubernetes API 资源以及 CRD 自动生成为内部 CUE 包。 你可以将它们导入CUE模板中,以简化模板以及帮助你进行验证。

目前有两种方式来导入内部 kube 包。

  1. 以固定方式导入: kube/<apiVersion> ,这样我们就可以直接引用 Kind 对应的结构体。

    1. import (
    2. apps "kube/apps/v1"
    3. corev1 "kube/v1"
    4. )
    5. // output is validated by Deployment.
    6. output: apps.#Deployment
    7. outputs: service: corev1.#Service

    这是比较好记易用的方式,主要因为它与 Kubernetes Object 的用法一致,只需要在 apiVersion 之前添加前缀 kube/。 当然,这个方式仅在 KubeVela 中被支持,所以你只能通过该方法 vela system dry-run 进行调试和测试。

  2. 以第三方包的方式导入。 你可以运行 vela system cue-packages 获取所有内置 kube 包,通过这个方式可以了解当前支持的 third-party packages

    1. $ vela system cue-packages
    2. DEFINITION-NAME IMPORT-PATH USAGE
    3. #Deployment k8s.io/apps/v1 Kube Object for apps/v1.Deployment
    4. #Service k8s.io/core/v1 Kube Object for v1.Service
    5. #Secret k8s.io/core/v1 Kube Object for v1.Secret
    6. #Node k8s.io/core/v1 Kube Object for v1.Node
    7. #PersistentVolume k8s.io/core/v1 Kube Object for v1.PersistentVolume
    8. #Endpoints k8s.io/core/v1 Kube Object for v1.Endpoints
    9. #Pod k8s.io/core/v1 Kube Object for v1.Pod

    其实,这些都是内置包,只是你可以像 third-party packages 一样使用 import-path 导入这些包。 当前方式你可以使用 cue 命令行进行调试。

使用 Kube 包的 CUE 模版调试流程

此部分主要介绍使用 cue 命令行对 CUE 模版调试和测试的流程,并且可以在 KubeVela中使用 完全相同的 CUE 模版

  1. 创建目录,初始化 CUE 模块
  1. mkdir cue-debug && cd cue-debug/
  2. cue mod init oam.dev
  3. go mod init oam.dev
  4. touch def.cue
  1. 使用 cue 命令行下载 third-party packages

其实在 KubeVela 中并不需要下载这些包,因为它们已经被从 Kubernetes API 自动生成。 但是在本地测试环境,我们需要使用 cue get go 来获取 Go 包并将其转换为 CUE 格式的文件。

所以,为了能够使用 Kubernetes 中 DeploymentSerivice 资源,我们需要下载并转换为 coreapps Kubernetes 模块的 CUE 定义,如下所示:

  1. cue get go k8s.io/api/core/v1
  2. cue get go k8s.io/api/apps/v1

随后,该模块目录下可以看到如下结构:

  1. ├── cue.mod
  2. ├── gen
  3. └── k8s.io
  4. ├── api
  5. ├── apps
  6. └── core
  7. └── apimachinery
  8. └── pkg
  9. ├── module.cue
  10. ├── pkg
  11. └── usr
  12. ├── def.cue
  13. ├── go.mod
  14. └── go.sum

该包在 CUE 模版中被导入的路径应该是:

  1. import (
  2. apps "k8s.io/api/apps/v1"
  3. corev1 "k8s.io/api/core/v1"
  4. )
  1. 重构目录结构

我们的目标是本地测试模版并在 KubeVela 中使用相同模版。 所以我们需要对我们本地 CUE 模块目录进行一些重构,并将目录与 KubeVela 提供的导入路径保持一致。

我们将 appscore 目录从 cue.mod/gen/k8s.io/api 复制到 cue.mod/gen/k8s.io。 请注意,我们应将源目录 appscore 保留在 gen/k8s.io/api 中,以避免出现包依赖性问题。

  1. cp -r cue.mod/gen/k8s.io/api/apps cue.mod/gen/k8s.io
  2. cp -r cue.mod/gen/k8s.io/api/core cue.mod/gen/k8s.io

合并过之后到目录结构如下:

  1. ├── cue.mod
  2. ├── gen
  3. └── k8s.io
  4. ├── api
  5. ├── apps
  6. └── core
  7. ├── apimachinery
  8. └── pkg
  9. ├── apps
  10. └── core
  11. ├── module.cue
  12. ├── pkg
  13. └── usr
  14. ├── def.cue
  15. ├── go.mod
  16. └── go.sum

因此,你可以使用与 KubeVela 对齐的路径导入包:

  1. import (
  2. apps "k8s.io/apps/v1"
  3. corev1 "k8s.io/core/v1"
  4. )
  1. 运行测试

最终,我们可以使用 Kube 包测试 CUE 模版。

  1. import (
  2. apps "k8s.io/apps/v1"
  3. corev1 "k8s.io/core/v1"
  4. )
  5. // output is validated by Deployment.
  6. output: apps.#Deployment
  7. output: {
  8. metadata: {
  9. name: context.name
  10. namespace: "default"
  11. }
  12. spec: {
  13. selector: matchLabels: {
  14. "app": context.name
  15. }
  16. template: {
  17. metadata: {
  18. labels: {
  19. "app": context.name
  20. "version": parameter.version
  21. }
  22. }
  23. spec: {
  24. terminationGracePeriodSeconds: parameter.podShutdownGraceSeconds
  25. containers: [{
  26. name: context.name
  27. image: parameter.image
  28. ports: [{
  29. if parameter.containerPort != _|_ {
  30. containerPort: parameter.containerPort
  31. }
  32. if parameter.containerPort == _|_ {
  33. containerPort: parameter.servicePort
  34. }
  35. }]
  36. if parameter.env != _|_ {
  37. env: [
  38. for k, v in parameter.env {
  39. name: k
  40. value: v
  41. },
  42. ]
  43. }
  44. resources: {
  45. requests: {
  46. if parameter.cpu != _|_ {
  47. cpu: parameter.cpu
  48. }
  49. if parameter.memory != _|_ {
  50. memory: parameter.memory
  51. }
  52. }
  53. }
  54. }]
  55. }
  56. }
  57. }
  58. }
  59. outputs:{
  60. service: corev1.#Service
  61. }
  62. // Service
  63. outputs: service: {
  64. metadata: {
  65. name: context.name
  66. labels: {
  67. "app": context.name
  68. }
  69. }
  70. spec: {
  71. //type: "ClusterIP"
  72. selector: {
  73. "app": context.name
  74. }
  75. ports: [{
  76. port: parameter.servicePort
  77. if parameter.containerPort != _|_ {
  78. targetPort: parameter.containerPort
  79. }
  80. if parameter.containerPort == _|_ {
  81. targetPort: parameter.servicePort
  82. }
  83. }]
  84. }
  85. }
  86. parameter: {
  87. version: *"v1" | string
  88. image: string
  89. servicePort: int
  90. containerPort?: int
  91. // +usage=Optional duration in seconds the pod needs to terminate gracefully
  92. podShutdownGraceSeconds: *30 | int
  93. env: [string]: string
  94. cpu?: string
  95. memory?: string
  96. }
  97. // mock context data
  98. context: {
  99. name: "test"
  100. }
  101. // mock parameter data
  102. parameter: {
  103. image: "test-image"
  104. servicePort: 8000
  105. env: {
  106. "HELLO": "WORLD"
  107. }
  108. }

使用 cue export 导出渲染结果。

  1. $ cue export def.cue --out yaml
  2. output:
  3. metadata:
  4. name: test
  5. namespace: default
  6. spec:
  7. selector:
  8. matchLabels:
  9. app: test
  10. template:
  11. metadata:
  12. labels:
  13. app: test
  14. version: v1
  15. spec:
  16. terminationGracePeriodSeconds: 30
  17. containers:
  18. - name: test
  19. image: test-image
  20. ports:
  21. - containerPort: 8000
  22. env:
  23. - name: HELLO
  24. value: WORLD
  25. resources:
  26. requests: {}
  27. outputs:
  28. service:
  29. metadata:
  30. name: test
  31. labels:
  32. app: test
  33. spec:
  34. selector:
  35. app: test
  36. ports:
  37. - port: 8000
  38. targetPort: 8000
  39. parameter:
  40. version: v1
  41. image: test-image
  42. servicePort: 8000
  43. podShutdownGraceSeconds: 30
  44. env:
  45. HELLO: WORLD
  46. context:
  47. name: test

Dry-Run Application

当 CUE 模版就绪,我们就可以使用 vela system dry-run 执行 dry-run 并检查在真实 Kubernetes 集群中被渲染的资源。该命令行背后的执行逻辑与 KubeVela 中 Application 控制器的逻辑是一致的。

首先,我们需要使用 mergedef.sh 合并 Definition 和 CUE 文件。

  1. $ mergedef.sh def.yaml def.cue > componentdef.yaml

随后,我们创建 test-app.yaml Application。

  1. apiVersion: core.oam.dev/v1beta1
  2. kind: Application
  3. metadata:
  4. name: boutique
  5. namespace: default
  6. spec:
  7. components:
  8. - name: frontend
  9. type: microservice
  10. properties:
  11. image: registry.cn-hangzhou.aliyuncs.com/vela-samples/frontend:v0.2.2
  12. servicePort: 80
  13. containerPort: 8080
  14. env:
  15. PORT: "8080"
  16. cpu: "100m"
  17. memory: "64Mi"

针对上面 Application 使用 vela system dry-run 命令执行 dry-run 操作。

  1. $ vela system dry-run -f test-app.yaml -d componentdef.yaml
  2. ---
  3. # Application(boutique) -- Comopnent(frontend)
  4. ---
  5. apiVersion: apps/v1
  6. kind: Deployment
  7. metadata:
  8. labels:
  9. app.oam.dev/component: frontend
  10. app.oam.dev/name: boutique
  11. workload.oam.dev/type: microservice
  12. name: frontend
  13. namespace: default
  14. spec:
  15. selector:
  16. matchLabels:
  17. app: frontend
  18. template:
  19. metadata:
  20. labels:
  21. app: frontend
  22. version: v1
  23. spec:
  24. containers:
  25. - env:
  26. - name: PORT
  27. value: "8080"
  28. image: registry.cn-hangzhou.aliyuncs.com/vela-samples/frontend:v0.2.2
  29. name: frontend
  30. ports:
  31. - containerPort: 8080
  32. resources:
  33. requests:
  34. cpu: 100m
  35. memory: 64Mi
  36. serviceAccountName: default
  37. terminationGracePeriodSeconds: 30
  38. ---
  39. apiVersion: v1
  40. kind: Service
  41. metadata:
  42. labels:
  43. app: frontend
  44. app.oam.dev/component: frontend
  45. app.oam.dev/name: boutique
  46. trait.oam.dev/resource: service
  47. trait.oam.dev/type: AuxiliaryWorkload
  48. name: frontend
  49. spec:
  50. ports:
  51. - port: 80
  52. targetPort: 8080
  53. selector:
  54. app: frontend
  55. type: ClusterIP
  56. ---