Version: v1.1

补丁型特征

在自定义运维特征中,使用补丁型特征是一种比较常用的形式。

它让我们可以修改、补丁某些属性给组件对象(一般是工作负载)来完成特定操作,比如更新 sidecar 和节点亲和性(node affinity)的规则(并且,这个操作一定是在资源往集群部署前就已经生效)。

当我们的组件是从第三方提供并自定义而来的时候,由于它们的模版往往是固定不可变的,所以能使用补丁型特征就显得尤为有用了。

尽管运维特征是由 CUE 来定义,它能打补丁的组件类型并不限,不管是来自 CUE、Helm 还是其余支持的模版格式

下面,我们通过一个节点亲和性(node affinity)的例子,讲解如何使用补丁型特征:

  1. apiVersion: core.oam.dev/v1beta1
  2. kind: TraitDefinition
  3. metadata:
  4. annotations:
  5. definition.oam.dev/description: "affinity specify node affinity and toleration"
  6. name: node-affinity
  7. spec:
  8. appliesToWorkloads:
  9. - deployments.apps
  10. podDisruptive: true
  11. schematic:
  12. cue:
  13. template: |
  14. patch: {
  15. spec: template: spec: {
  16. if parameter.affinity != _|_ {
  17. affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: [{
  18. matchExpressions: [
  19. for k, v in parameter.affinity {
  20. key: k
  21. operator: "In"
  22. values: v
  23. },
  24. ]}]
  25. }
  26. if parameter.tolerations != _|_ {
  27. tolerations: [
  28. for k, v in parameter.tolerations {
  29. effect: "NoSchedule"
  30. key: k
  31. operator: "Equal"
  32. value: v
  33. }]
  34. }
  35. }
  36. }
  37. parameter: {
  38. affinity?: [string]: [...string]
  39. tolerations?: [string]: string
  40. }

具体来说,我们上面的这个补丁型特征,假定了使用它的组件对象将会使用 spec.template.spec.affinity 这个字段。因此,我们需要用 appliesToWorkloads 来指明,让当前运维特征被应用到拥有这个字段的对应工作负载实例上。

另一个重要的字段是 podDisruptive,这个补丁型特征将修改 Pod 模板字段,因此对该运维特征的任何字段进行更改,都会导致 Pod 重启。我们应该增加 podDisruptive 并且设置它的值为 true,以此告诉用户这个运维特征生效后将导致 Pod 重新启动。

现在用户只需要,声明他们希望增加一个节点亲和性的规则到组件实例当中:

  1. apiVersion: core.oam.dev/v1alpha2
  2. kind: Application
  3. metadata:
  4. name: testapp
  5. spec:
  6. components:
  7. - name: express-server
  8. type: webservice
  9. properties:
  10. image: oamdev/testapp:v1
  11. traits:
  12. - type: "node-affinity"
  13. properties:
  14. affinity:
  15. server-owner: ["owner1","owner2"]
  16. resource-pool: ["pool1","pool2","pool3"]
  17. tolerations:
  18. resource-pool: "broken-pool1"
  19. server-owner: "old-owner"

待解决的短板

默认来说,补丁型特征是通过 CUE 的 merge 操作来实现的。它有以下限制:

  • 不能处理有冲突的字段名
    • 比方说,在一个组件实例中已经设置过这样的值 replicas=5,那一旦有运维特征实例,尝试给 replicas 字段的值打补丁就会失败。所以我们建议你提前规划好,不要在组件和运维特征之间使用重复的字段名。
  • 数组列表被补丁时,会按索引顺序进行合并。如果数组里出现了重复的值,将导致问题。为了规避这个风险,请查询后面的解决方案。

策略补丁

策略补丁,通过增加注解(annotation)而生效,并支持如下两种模式。

请注意,这里开始并不是 CUE 官方提供的功能, 而是 KubeVela 扩展开发而来

1. 使用 +patchKey=<key_name> 注解

这个注解,是给数组列表打补丁用的。它的执行方式也不遵循 CUE 官方的方式,而是将每一个数组列表视作对象,并执行如下的策略:

  • 如果发现重复的键名,补丁数据会直接替换掉它的值
  • 如果没有重复键名,补丁则会自动附加这些数据

下面来看看,一个使用 ‘patchKey’ 的策略补丁:

  1. apiVersion: core.oam.dev/v1beta1
  2. kind: TraitDefinition
  3. metadata:
  4. annotations:
  5. definition.oam.dev/description: "add sidecar to the app"
  6. name: sidecar
  7. spec:
  8. appliesToWorkloads:
  9. - deployments.apps
  10. podDisruptive: true
  11. schematic:
  12. cue:
  13. template: |
  14. patch: {
  15. // +patchKey=name
  16. spec: template: spec: containers: [parameter]
  17. }
  18. parameter: {
  19. name: string
  20. image: string
  21. command?: [...string]
  22. }

在上述的这个例子中,我们定义了要 patchKey 的字段 name,是来自容器的参数键名。如果工作负载中并没有同名的容器,那么一个 sidecar 容器就会被加到 spec.template.spec.containers 数组列表中。如果工作负载中有重名的 sidecar 运维特征,则会执行 merge 操作而不是附加。

如果 patchoutputs 同时存在于一个运维特征定义中,patch 会率先被执行然后再渲染 outputs

  1. apiVersion: core.oam.dev/v1beta1
  2. kind: TraitDefinition
  3. metadata:
  4. annotations:
  5. definition.oam.dev/description: "expose the app"
  6. name: expose
  7. spec:
  8. appliesToWorkloads:
  9. - deployments.apps
  10. podDisruptive: true
  11. schematic:
  12. cue:
  13. template: |
  14. patch: {spec: template: metadata: labels: app: context.name}
  15. outputs: service: {
  16. apiVersion: "v1"
  17. kind: "Service"
  18. metadata: name: context.name
  19. spec: {
  20. selector: app: context.name
  21. ports: [
  22. for k, v in parameter.http {
  23. port: v
  24. targetPort: v
  25. },
  26. ]
  27. }
  28. }
  29. parameter: {
  30. http: [string]: int
  31. }

在上面这个运维特征定义中,我们将会把一个 Service 添加到给定的组件实例上。同时会先去给工作负载类型打上补丁数据,然后基于模版里的 outputs 渲染余下的资源。

2. 使用 +patchStrategy=retainkeys 注解

这个注解的策略,与 Kubernetes 官方的 retainkeys 策略类似。

在一些场景下,整个对象需要被一起替换掉,使用 retainkeys 就是最适合的办法。

假定一个 Deployment 对象是这样编写的:

  1. apiVersion: apps/v1
  2. kind: Deployment
  3. metadata:
  4. name: retainkeys-demo
  5. spec:
  6. selector:
  7. matchLabels:
  8. app: nginx
  9. strategy:
  10. type: rollingUpdate
  11. rollingUpdate:
  12. maxSurge: 30%
  13. template:
  14. metadata:
  15. labels:
  16. app: nginx
  17. spec:
  18. containers:
  19. - name: retainkeys-demo-ctr
  20. image: nginx

现在如果我们想替换掉 rollingUpdate 策略,你可以这样写:

  1. apiVersion: core.oam.dev/v1alpha2
  2. kind: TraitDefinition
  3. metadata:
  4. name: recreate
  5. spec:
  6. appliesToWorkloads:
  7. - deployments.apps
  8. extension:
  9. template: |-
  10. patch: {
  11. spec: {
  12. // +patchStrategy=retainKeys
  13. strategy: type: "Recreate"
  14. }
  15. }

这个 YAML 资源将变更为:

  1. apiVersion: apps/v1
  2. kind: Deployment
  3. metadata:
  4. name: retainkeys-demo
  5. spec:
  6. selector:
  7. matchLabels:
  8. app: nginx
  9. strategy:
  10. type: Recreate
  11. template:
  12. metadata:
  13. labels:
  14. app: nginx
  15. spec:
  16. containers:
  17. - name: retainkeys-demo-ctr
  18. image: nginx

更多补丁型特征的使用场景

补丁型特征,针对组件层面做些整体操作时,非常有用。我们看看还可以满足哪些需求:

增加标签

比如说,我们要给组件实例打上 virtualgroup 的通用标签。

  1. apiVersion: core.oam.dev/v1alpha2
  2. kind: TraitDefinition
  3. metadata:
  4. annotations:
  5. definition.oam.dev/description: "Add virtual group labels"
  6. name: virtualgroup
  7. spec:
  8. appliesToWorkloads:
  9. - deployments.apps
  10. podDisruptive: true
  11. schematic:
  12. cue:
  13. template: |
  14. patch: {
  15. spec: template: {
  16. metadata: labels: {
  17. if parameter.scope == "namespace" {
  18. "app.namespace.virtual.group": parameter.group
  19. }
  20. if parameter.scope == "cluster" {
  21. "app.cluster.virtual.group": parameter.group
  22. }
  23. }
  24. }
  25. }
  26. parameter: {
  27. group: *"default" | string
  28. scope: *"namespace" | string
  29. }

然后这样用就可以了:

  1. apiVersion: core.oam.dev/v1beta1
  2. kind: Application
  3. spec:
  4. ...
  5. traits:
  6. - type: virtualgroup
  7. properties:
  8. group: "my-group1"
  9. scope: "cluster"

增加注解

与通用标签类似,你也可以给组件实例打补丁,增加一些注解。注解的格式,必须是 JSON。

  1. apiVersion: core.oam.dev/v1beta1
  2. kind: TraitDefinition
  3. metadata:
  4. annotations:
  5. definition.oam.dev/description: "Specify auto scale by annotation"
  6. name: kautoscale
  7. spec:
  8. appliesToWorkloads:
  9. - deployments.apps
  10. podDisruptive: false
  11. schematic:
  12. cue:
  13. template: |
  14. import "encoding/json"
  15. patch: {
  16. metadata: annotations: {
  17. "my.custom.autoscale.annotation": json.Marshal({
  18. "minReplicas": parameter.min
  19. "maxReplicas": parameter.max
  20. })
  21. }
  22. }
  23. parameter: {
  24. min: *1 | int
  25. max: *3 | int
  26. }

增加 Pod 环境变量

给 Pod 去注入环境变量也是非常常见的操作。

这种使用方式依赖策略补丁而生效, 所以记得加上 +patchKey=name

  1. apiVersion: core.oam.dev/v1beta1
  2. kind: TraitDefinition
  3. metadata:
  4. annotations:
  5. definition.oam.dev/description: "add env into your pods"
  6. name: env
  7. spec:
  8. appliesToWorkloads:
  9. - deployments.apps
  10. podDisruptive: true
  11. schematic:
  12. cue:
  13. template: |
  14. patch: {
  15. spec: template: spec: {
  16. // +patchKey=name
  17. containers: [{
  18. name: context.name
  19. // +patchKey=name
  20. env: [
  21. for k, v in parameter.env {
  22. name: k
  23. value: v
  24. },
  25. ]
  26. }]
  27. }
  28. }
  29. parameter: {
  30. env: [string]: string
  31. }

基于外部鉴权服务注入 ServiceAccount

在这个场景下,service-account 是从一个鉴权服务中动态获取、再通过打补丁给到应用的。

我们这里展示的是,将 UID token 放进 HTTP header 的例子。你也可以用 HTTP body 来完成需求。

  1. apiVersion: core.oam.dev/v1beta1
  2. kind: TraitDefinition
  3. metadata:
  4. annotations:
  5. definition.oam.dev/description: "dynamically specify service account"
  6. name: service-account
  7. spec:
  8. appliesToWorkloads:
  9. - deployments.apps
  10. podDisruptive: true
  11. schematic:
  12. cue:
  13. template: |
  14. processing: {
  15. output: {
  16. credentials?: string
  17. }
  18. http: {
  19. method: *"GET" | string
  20. url: parameter.serviceURL
  21. request: {
  22. header: {
  23. "authorization.token": parameter.uidtoken
  24. }
  25. }
  26. }
  27. }
  28. patch: {
  29. spec: template: spec: serviceAccountName: processing.output.credentials
  30. }
  31. parameter: {
  32. uidtoken: string
  33. serviceURL: string
  34. }

增加 InitContainer

InitContainer 常用于预定义镜像内的操作,并且在承载应用的容器运行前就跑起来。

看看示例:

  1. apiVersion: core.oam.dev/v1beta1
  2. kind: TraitDefinition
  3. metadata:
  4. annotations:
  5. definition.oam.dev/description: "add an init container and use shared volume with pod"
  6. name: init-container
  7. spec:
  8. appliesToWorkloads:
  9. - deployments.apps
  10. podDisruptive: true
  11. schematic:
  12. cue:
  13. template: |
  14. patch: {
  15. spec: template: spec: {
  16. // +patchKey=name
  17. containers: [{
  18. name: context.name
  19. // +patchKey=name
  20. volumeMounts: [{
  21. name: parameter.mountName
  22. mountPath: parameter.appMountPath
  23. }]
  24. }]
  25. initContainers: [{
  26. name: parameter.name
  27. image: parameter.image
  28. if parameter.command != _|_ {
  29. command: parameter.command
  30. }
  31. // +patchKey=name
  32. volumeMounts: [{
  33. name: parameter.mountName
  34. mountPath: parameter.initMountPath
  35. }]
  36. }]
  37. // +patchKey=name
  38. volumes: [{
  39. name: parameter.mountName
  40. emptyDir: {}
  41. }]
  42. }
  43. }
  44. parameter: {
  45. name: string
  46. image: string
  47. command?: [...string]
  48. mountName: *"workdir" | string
  49. appMountPath: string
  50. initMountPath: string
  51. }

用法像这样:

  1. apiVersion: core.oam.dev/v1beta1
  2. kind: Application
  3. metadata:
  4. name: testapp
  5. spec:
  6. components:
  7. - name: express-server
  8. type: webservice
  9. properties:
  10. image: oamdev/testapp:v1
  11. traits:
  12. - type: "init-container"
  13. properties:
  14. name: "install-container"
  15. image: "busybox"
  16. command:
  17. - wget
  18. - "-O"
  19. - "/work-dir/index.html"
  20. - http://info.cern.ch
  21. mountName: "workdir"
  22. appMountPath: "/usr/share/nginx/html"
  23. initMountPath: "/work-dir"