Trait Definition

    A trait can be something similar to component, while they’re attached operational resources.

    • Kubernetes API resources like ingress, service, rollout.
    • The composition of these operational resources.
    • A patch of data, for example, patch sidecar to workload.

    Let’s use vela def init to create a basic trait scaffold:

    The content of the scaffold expected to be:

    1. // $ cat myroute.cue
    2. "my-route": {
    3. annotations: {}
    4. attributes: {
    5. appliesToWorkloads: []
    6. conflictsWith: []
    7. podDisruptive: false
    8. workloadRefPath: ""
    9. }
    10. description: "My ingress route trait."
    11. labels: {}
    12. type: "trait"
    13. }
    14. template: {
    15. patch: {}
    16. parameter: {}
    17. }

    caution

    There’s a bug in vela CLI(<=1.4.2), the vela def init command will generate definitionRef: "" in attributes which is wrong, please remove that line.

    Define a trait to compose resources

    Unlike component definition, KubeVela requires objects in traits MUST be defined in outputs section (not output) in CUE template with format as below:

    1. outputs: {
    2. <unique-name>: {
    3. <template of trait resource structural data>
    4. }
    5. }

    Trait Definition - 图2tip

    Actually the CUE template of trait here will be evaluated with component CUE template in the same context, so the name can’t be conflict. That also explain why the output can’t be defined in trait.

    Below is an example that we combine ingress and service of Kubernetes into our my-route trait.

    1. "my-route": {
    2. annotations: {}
    3. attributes: {
    4. appliesToWorkloads: []
    5. conflictsWith: []
    6. podDisruptive: false
    7. workloadRefPath: ""
    8. }
    9. description: "My ingress route trait."
    10. labels: {}
    11. type: "trait"
    12. }
    13. template: {
    14. parameter: {
    15. domain: string
    16. http: [string]: int
    17. }
    18. // trait template can have multiple outputs in one trait
    19. outputs: service: {
    20. apiVersion: "v1"
    21. kind: "Service"
    22. spec: {
    23. selector:
    24. app: context.name
    25. ports: [
    26. for k, v in parameter.http {
    27. port: v
    28. targetPort: v
    29. },
    30. ]
    31. }
    32. }
    33. outputs: ingress: {
    34. apiVersion: "networking.k8s.io/v1beta1"
    35. kind: "Ingress"
    36. metadata:
    37. name: context.name
    38. spec: {
    39. rules: [{
    40. host: parameter.domain
    41. http: {
    42. paths: [
    43. for k, v in parameter.http {
    44. path: k
    45. backend: {
    46. serviceName: context.name
    47. servicePort: v
    48. }
    49. },
    50. ]
    51. }
    52. }]
    53. }
    54. }
    55. }

    Apply to our control plane to make this trait work:

    1. vela def apply myroute.cue

    Then our end users can discover it immediately and attach this trait to any component instance in Application.

    After using vela up by the following command:

    1. cat <<EOF | vela up -f -
    2. apiVersion: core.oam.dev/v1beta1
    3. kind: Application
    4. metadata:
    5. name: testapp
    6. spec:
    7. components:
    8. - name: express-server
    9. type: webservice
    10. properties:
    11. cmd:
    12. - node
    13. - server.js
    14. image: oamdev/testapp:v1
    15. port: 8080
    16. traits:
    17. - type: my-route
    18. properties:
    19. domain: test.my.domain
    20. http:
    21. "/api": 8080
    22. EOF

    It will generate Kubernetes resources by KubeVela.

    With the help of CUE, we can achieve many advanced features in trait.

    You can define the for-loop inside the outputs.

    note

    In this case the type of parameter field used in the for-loop must be a map.

    Below is an example that will render multiple Kubernetes Services in one trait:

    1. apiVersion: core.oam.dev/v1beta1
    2. kind: Application
    3. metadata:
    4. name: testapp
    5. spec:
    6. components:
    7. - name: express-server
    8. properties:
    9. ...
    10. traits:
    11. - type: expose
    12. properties:
    13. http:
    14. myservice1: 8080
    15. myservice2: 8081

    The trait definition can send a HTTP request and capture the response to help you rendering the resource with keyword processing.

    You can define HTTP request method, url, body, header and trailer in the processing.http section, and the returned data will be stored in processing.output.

    Trait Definition - 图4tip

    Please ensure the target HTTP server returns a JSON data as output.

    Then you can reference the returned data from processing.output in or output/outputs.

    Below is an example:

    1. "auth-service": {
    2. type: "trait"
    3. }
    4. template: {
    5. parameter: {
    6. serviceURL: string
    7. }
    8. processing: {
    9. output: {
    10. token?: string
    11. }
    12. // The target server will return a JSON data with `token` as key.
    13. http: {
    14. method: *"GET" | string
    15. url: parameter.serviceURL
    16. request: {
    17. body?: bytes
    18. header: {}
    19. trailer: {}
    20. }
    21. }
    22. }
    23. patch: {
    24. data: token: processing.output.token
    25. }
    26. }

    In above example, this trait definition will send request to get the token data, and then patch the data to given component instance.

    A trait definition can read the generated resources (rendered from output and outputs) of given component definition.

    caution

    Generally, KubeVela will ensure the component definitions are evaluated before its traits. But there’re a that allow trait be deployed before component.

    Specifically, the context.output contains the rendered workload API resource (whose GVK is indicated by spec.workloadin component definition), and use context.outputs.<xx> to contain all the other rendered API resources.

    Let’s use an example to see this feature:

    1. Let’s define a component definition myworker like below:
    1. "myworker": {
    2. attributes: workload: definition: {
    3. apiVersion: "apps/v1"
    4. kind: "Deployment"
    5. }
    6. type: "component"
    7. }
    8. template: {
    9. output: {
    10. apiVersion: "apps/v1"
    11. kind: "Deployment"
    12. spec: {
    13. selector: matchLabels: {
    14. "app.oam.dev/component": context.name
    15. }
    16. template: {
    17. metadata: labels: {
    18. "app.oam.dev/component": context.name
    19. }
    20. spec: {
    21. containers: [{
    22. name: context.name
    23. image: parameter.image
    24. ports: [{containerPort: parameter.port}]
    25. envFrom: [{
    26. configMapRef: name: context.name + "game-config"
    27. }]
    28. if parameter["cmd"] != _|_ {
    29. command: parameter.cmd
    30. }
    31. }]
    32. }
    33. }
    34. }
    35. }
    36. outputs: gameconfig: {
    37. apiVersion: "v1"
    38. kind: "ConfigMap"
    39. metadata: {
    40. name: context.name + "game-config"
    41. }
    42. data: {
    43. enemies: parameter.enemies
    44. lives: parameter.lives
    45. }
    46. }
    47. parameter: {
    48. // +usage=Which image would you like to use for your service
    49. // +short=i
    50. image: string
    51. // +usage=Commands to run in the container
    52. cmd?: [...string]
    53. lives: string
    54. enemies: string
    55. port: int
    56. }
    57. }
    1. Define a new myingress trait that read the port.
    1. "myingress": {
    2. type: "trait"
    3. attributes: {
    4. appliesToWorkloads: ["myworker"]
    5. }
    6. }
    7. template: {
    8. parameter: {
    9. domain: string
    10. path: string
    11. exposePort: int
    12. }
    13. // trait template can have multiple outputs in one trait
    14. outputs: service: {
    15. apiVersion: "v1"
    16. kind: "Service"
    17. spec: {
    18. selector:
    19. app: context.name
    20. ports: [{
    21. port: parameter.exposePort
    22. targetPort: context.output.spec.template.spec.containers[0].ports[0].containerPort
    23. }]
    24. }
    25. }
    26. outputs: ingress: {
    27. kind: "Ingress"
    28. metadata:
    29. name: context.name
    30. labels: config: context.outputs.gameconfig.data.enemies
    31. spec: {
    32. rules: [{
    33. host: parameter.domain
    34. http: {
    35. paths: [{
    36. path: parameter.path
    37. backend: {
    38. serviceName: context.name
    39. servicePort: parameter.exposePort
    40. }
    41. }]
    42. }
    43. }]
    44. }
    45. }

    In detail, The trait myingress can only apply to the specific ComponentDefinition myworker:

    1. the rendered Kubernetes Deployment resource will be stored in the context.output,
    2. all other rendered resources will be stored in context.outputs.<xx>, with <xx> is the unique name in every template.outputs.

    Thus, in TraitDefinition myingress, it can read the rendered API resources (e.g. context.outputs.gameconfig.data.enemies) from the context and get the targetPort and labels.config.

    Patch Trait

    Besides generate objects from trait, one more important thing we can do in trait is to patch or override the configuration of Component. But why do we need to patch or override?

    There’re several reasons:

    1. The component could be defined by another person, for separation of concern, the operator can attach an operational trait to change that data.
    2. The component could be defined by third party which is not controlled by the one who use it.

    So KubeVela allow patch or override in this case, please refer to for more details. As trait and workflow step can both patch, so we write them together.

    You can also define health check policy and status message when a trait deployed and tell the real status to end users.

    If not defined, the health result will always be true, which means it will be marked as healthy immediately after resources applied to Kubernetes. You can define a CUE expression in it to notify if the trait is healthy or not.

    The keyword in CUE is isHealth, the result of CUE expression must be bool type.

    KubeVela runtime will evaluate the CUE expression periodically until it becomes healthy. Every time the controller will get all the Kubernetes resources and fill them into the context variables.

    So the context will contain following information:

    1. context:{
    2. name: <component name>
    3. appName: <app name>
    4. outputs: {
    5. <resource1>: <K8s trait resource1>
    6. <resource2>: <K8s trait resource2>
    7. }
    8. }

    The example of health check likes below:

    You can also use the parameter defined in the template like:

    1. mytrait: {
    2. type: "trait"
    3. ...
    4. attributes: {
    5. status: {
    6. healthPolicy: #"""
    7. isHealth: context.outputs."mytrait-\(parameter.name)".status.state == "Available"
    8. """#
    9. }
    10. }
    11. template: {
    12. parameter: {
    13. name: string
    14. }
    15. ...
    16. }

    The health check result will be recorded into the corresponding trait in .status.services of Application resource.

    1. apiVersion: core.oam.dev/v1beta1
    2. kind: Application
    3. status:
    4. ...
    5. services:
    6. - healthy: true
    7. ...
    8. name: myweb
    9. traits:
    10. - healthy: true
    11. type: my-ingress
    12. status: running

    The spec of custom status is <trait-type-name>.attributes.status.customStatus, it shares the same mechanism with the health check.

    The keyword in CUE is message, the result of CUE expression must be string type.

    Application CRD controller will evaluate the CUE expression after the health check succeed.

    The example of custom status likes below:

    1. my-service: {
    2. type: "trait"
    3. ...
    4. attributes: {
    5. status: {
    6. customStatus: #"""
    7. if context.outputs.ingress.status.loadBalancer.ingress != _|_ {
    8. let igs = context.outputs.ingress.status.loadBalancer.ingress
    9. if igs[0].ip != _|_ {
    10. if igs[0].host != _|_ {
    11. message: "Visiting URL: " + context.outputs.ingress.spec.rules[0].host + ", IP: " + igs[0].ip
    12. }
    13. if igs[0].host == _|_ {
    14. message: "Host not specified, visit the cluster or load balancer in front of the cluster with IP: " + igs[0].ip
    15. }
    16. }
    17. }
    18. """#
    19. }
    20. }
    21. }

    The message will be recorded into the corresponding trait in .status.services of Application resource.

    1. apiVersion: core.oam.dev/v1beta1
    2. kind: Application
    3. status:
    4. ...
    5. services:
    6. - healthy: true
    7. ...
    8. name: myweb
    9. traits:
    10. - healthy: true
    11. message: 'Visiting URL: www.example.com, IP: 47.111.233.220'
    12. type: my-ingress
    13. status: running

    Please refer to for more complete example.

    Full available context in Trait

    Context VariableDescriptionType
    context.clusterVersion.majorThe major version of the runtime Kubernetes cluster.string
    context.clusterVersion.gitVersionThe gitVersion of the runtime Kubernetes cluster.string
    context.clusterVersion.platformThe platform information of the runtime Kubernetes cluster.string
    context.clusterVersion.minorThe minor version of the runtime Kubernetes cluster.int

    The cluster version context info can be used for graceful upgrade of definition. For example, you can define different API according to the cluster version.

    1. outputs: ingress: {
    2. if context.clusterVersion.minor < 19 {
    3. apiVersion: "networking.k8s.io/v1beta1"
    4. }
    5. if context.clusterVersion.minor >= 19 {
    6. apiVersion: "networking.k8s.io/v1"
    7. }
    8. kind: "Ingress"
    9. }

    Or use string contain pattern for this usage:

    KubeVela is fully programmable via CUE, while it leverage Kubernetes as control plane and align with the API in yaml. As a result, the CUE definition will be converted as Kubernetes API when applied into cluster.

    The trait definition will be in the following API format:

    1. apiVersion: core.oam.dev/v1beta1
    2. kind: TraitDefinition
    3. metadata:
    4. name: <TraitDefinition name>
    5. annotations:
    6. definition.oam.dev/description: <function description>
    7. spec:
    8. definition:
    9. apiVersion: <corresponding Kubernetes resource group>
    10. kind: <corresponding Kubernetes resource type>
    11. workloadRefPath: <The path to the reference field of the Workload object in the Trait>
    12. podDisruptive: <whether the parameter update of Trait cause the underlying resource (pod) to restart>
    13. manageWorkload: <Whether the workload is managed by this Trait>
    14. skipRevisionAffect: <Whether this Trait is not included in the calculation of version changes>
    15. appliesToWorkloads:
    16. - <Workload that TraitDefinition can adapt to>
    17. conflictsWith:
    18. - <other Traits that conflict with this><>
    19. revisionEnabled: <whether Trait is aware of changes in component version>
    20. controlPlaneOnly: <Whether resources generated by trait are dispatched to the hubcluster (local)>
    21. schematic: # Abstract
    22. cue: # There are many abstracts
    23. template: <CUE format template>

    More examples to learn

    You can check the following resources for more examples: