Version: v1.2

Patch Traits

Patch is a very common pattern of trait definitions, i.e. the app operators can amend/patch attributes to the component instance (normally the workload) to enable certain operational features such as sidecar or node affinity rules (and this should be done before the resources applied to target cluster).

This pattern is extremely useful when the component definition is provided by third-party component provider (e.g. software distributor) so app operators do not have privilege to change its template.

Note that even patch trait itself is defined by CUE, it can patch any component regardless how its schematic is defined (i.e. CUE, Helm, and any other supported schematic approaches).

Below is an example for node-affinity trait:

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

The patch trait above assumes the target component instance have spec.template.spec.affinity field. Hence, we need to use appliesToWorkloads to enforce the trait only applies to those workload types have this field.

Another important field is podDisruptive, this patch trait will patch to the pod template field, so changes on any field of this trait will cause the pod to restart, We should add podDisruptive and make it to be true to tell users that applying this trait will cause the pod to restart.

Now the users could declare they want to add node affinity rules to the component instance as below:

  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"

Known Limitations

By default, patch trait in KubeVela leverages the CUE merge operation. It has following known constraints though:

  • Can not handle conflicts.
    • For example, if a component instance already been set with value replicas=5, then any patch trait to patch replicas field will fail, a.k.a you should not expose replicas field in its component definition schematic.
  • Array list in the patch will be merged following the order of index. It can not handle the duplication of the array list members. This could be fixed by another feature below.

Strategy Patch

Strategy Patch is effective by adding annotation, and supports the following two ways

Note that this is not a standard CUE feature, KubeVela enhanced CUE in this case.

1. With +patchKey=<key_name> annotation

This is useful for patching array list, merging logic of two array lists will not follow the CUE behavior. Instead, it will treat the list as object and use a strategy merge approach:

  • if a duplicated key is found, the patch data will be merge with the existing values;
  • if no duplication found, the patch will append into the array list.

The example of strategy patch trait with ‘patchKey’ will like below:

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

In above example we defined patchKey is name which is the parameter key of container name. In this case, if the workload don’t have the container with same name, it will be a sidecar container append into the spec.template.spec.containers array list. If the workload already has a container with the same name of this sidecar trait, then merge operation will happen instead of append (which leads to duplicated containers).

If patch and outputs both exist in one trait definition, the patch operation will be handled first and then render the 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. }

So the above trait which attaches a Service to given component instance will patch an corresponding label to the workload first and then render the Service resource based on template in outputs.

2. With +patchStrategy=retainkeys annotation

Similar to strategy retainkeys in K8s strategic merge patch

In some scenarios that the entire object needs to be replaced, retainkeys strategy is the best choice. the example as follows:

Assume the Deployment is the base resource

  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

Now want to replace rollingUpdate strategy with a new strategy, you can write the patch trait like below

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

Then the base resource becomes as follows

  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

More Use Cases of Patch Trait

Patch trait is in general pretty useful to separate operational concerns from the component definition, here are some more examples.

Add Labels

For example, patch common label (virtual group) to the component instance.

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

Then it could be used like:

  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"

Add Annotations

Similar to common labels, you could also patch the component instance with annotations. The annotation value should be a JSON string.

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

Add Pod Environments

Inject system environments into Pod is also very common use case.

This case relies on strategy merge patch, so don’t forget add +patchKey=name as below:

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

Inject ServiceAccount Based on External Auth Service

In this example, the service account was dynamically requested from an authentication service and patched into the service.

This example put UID token in HTTP header but you can also use request body if you prefer.

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

The processing.http section is an advanced feature that allow trait definition to send a HTTP request during rendering the resource. Please refer to Execute HTTP Request in Trait Definition section for more details.

Add InitContainer

InitContainer is useful to pre-define operations in an image and run it before app container.

Below is an example:

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

The usage could be:

  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"