kubevela.github.io/docs/platform-engineers/components/custom-component.md

22 KiB

title
Component Definition

In this section, we will introduce how to use CUE to customize components via ComponentDefinition. Make sure you've learned the basic knowledge about Definition Concept and how to manage definition.

Declare ComponentDefinition

First, generate ComponentDefinition scaffolds via vela def init with existed YAML file.

The YAML file:

apiVersion: "apps/v1"
kind: "Deployment"
spec:
  selector:
    matchLabels:
      "app.oam.dev/component": "name"
  template:
    metadata:
      labels:
        "app.oam.dev/component": "name"
    spec:
      containers: 
      - name: "name"
        image: "image"

Generate ComponentDefinition based on the YAML file:

vela def init stateless -t component --template-yaml ./stateless.yaml -o stateless.cue

It generates a file:

stateless: {
	annotations: {}
	attributes: workload: definition: {
		apiVersion: "<change me> apps/v1"
		kind:       "<change me> Deployment"
	}
	description: ""
	labels: {}
	type: "component"
}

template: {
	output: {
		spec: {
			selector: matchLabels: "app.oam.dev/component": "name"
			template: {
				metadata: labels: "app.oam.dev/component": "name"
				spec: containers: [{
					name:  "name"
					image: "image"
				}]
			}
		}
		apiVersion: "apps/v1"
		kind:       "Deployment"
	}
	outputs: {}
	parameter: {}
}

In detail:

  • The stateless is the name of component definition, it can be defined by yourself when initialize the component.
  • stateless.attributes.workload indicates the workload type of this component, it can help integrate with traits that apply to this kind of workload.
  • template is a CUE template, specifically:
    • The output and outputs fields define the resources that the component will be composed.
    • The parameter field defines the parameters of the component, i.e. the configurable properties exposed in the Application (and schema will be automatically generated based on them for end users to learn this component).

Add parameters in this auto-generated custom component file :

stateless: {
	annotations: {}
	attributes: workload: definition: {
		apiVersion: "apps/v1"
		kind:       "Deployment"
	}
	description: ""
	labels: {}
	type: "component"
}

template: {
	output: {
		spec: {
			selector: matchLabels: "app.oam.dev/component": parameter.name
			template: {
				metadata: labels: "app.oam.dev/component": parameter.name
				spec: containers: [{
					name:  parameter.name
					image: parameter.image
				}]
			}
		}
		apiVersion: "apps/v1"
		kind:       "Deployment"
	}
	outputs: {}
	parameter: {
    name: string
    image: string
  }
}

You can use vela def vet to validate the format:

vela def vet stateless.cue
expected output
Validation succeed.

Apply above ComponentDefinition to your Kubernetes cluster to make it work:

vela def apply stateless.cue
expected output
ComponentDefinition stateless created in namespace vela-system.

Then the end user can check the schema and use it in an application now:

vela show stateless
expected output
# Specification
+-------+-------------+--------+----------+---------+
| NAME  | DESCRIPTION |  TYPE  | REQUIRED | DEFAULT |
+-------+-------------+--------+----------+---------+
| name  |             | string | true     |         |
| image |             | string | true     |         |
+-------+-------------+--------+----------+---------+

Declare another component named task which is an abstraction for run-to-completion workload works the same.

Check the details for another example to define a task based component.
vela def init task -t component -o task.cue

It generates a file:

// $ cat task.cue
task: {
	annotations: {}
	attributes: workload: definition: {
		apiVersion: "<change me> apps/v1"
		kind:       "<change me> Deployment"
	}
	description: ""
	labels: {}
	type: "component"
}

template: {
	output: {}
	parameter: {}
}

Edit the generated component file:

task: {
	annotations: {}
	attributes: workload: definition: {
		apiVersion: "batch/v1"
		kind:       "Job"
	}
	description: ""
	labels: {}
	type: "component"
}

template: {
  output: {
    apiVersion: "batch/v1"
    kind:       "Job"
    spec: {
      parallelism: parameter.count
      completions: parameter.count
      template: spec: {
        restartPolicy: parameter.restart
        containers: [{
          image: parameter.image
          if parameter["cmd"] != _|_ {
            command: parameter.cmd
          }
        }]
      }
    }
  }
	parameter: {
    count:   *1 | int
    image:   string
    restart: *"Never" | string
    cmd?: [...string]
  }
}

Apply above ComponentDefinition files to your Kubernetes cluster to make it work:

$ vela def apply task.cue
ComponentDefinition task created in namespace vela-system.

Now let's use the stateless and task component type.

Declare an Application using this component

The ComponentDefinition can be instantiated in Application abstraction as below:

apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
  name: website
spec:
  components:
    - name: hello
      type: stateless
      properties:
        image: oamdev/hello-world
        name: mysvc
    - name: countdown
      type: task
      properties:
        image: centos:7
        cmd:
          - "bin/bash"
          - "-c"
          - "for i in 9 8 7 6 5 4 3 2 1 ; do echo $i ; done"
Learn Details Under The Hood

Above application resource will generate and manage following Kubernetes resources in your target cluster based on the output in CUE template and user input in Application properties.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
  ... # skip tons of metadata info
spec:
  template:
    spec:
      containers:
        - name: mysvc
          image: oamdev/hello-world
    metadata:
      labels:
        app.oam.dev/component: mysvc
  selector:
    matchLabels:
      app.oam.dev/component: mysvc
---
apiVersion: batch/v1
kind: Job
metadata:
  name: countdown
  ... # skip tons of metadata info
spec:
  parallelism: 1
  completions: 1
  template:
    metadata:
      name: countdown
    spec:
      containers:
        - name: countdown
          image: 'centos:7'
          command:
            - bin/bash
            - '-c'
            - for i in 9 8 7 6 5 4 3 2 1 ; do echo $i ; done
      restartPolicy: Never

You can also use dry run to show what the yaml results will be rendered for debugging.

CUE Context for runtime information

KubeVela allows you to reference the runtime information of your application via context keyword.

The most widely used context is application name(context.appName) component name(context.name).

context: {
  appName: string
  name: string
}

For example, let's say you want to use the component name filled in by users as the container name in the workload instance:

parameter: {
    image: string
}
output: {
  ...
    spec: {
        containers: [{
            name:  context.name
            image: parameter.image
        }]
    }
  ...
}

:::tip Note that context information are auto-injected before resources are applied to target cluster. :::

The list of all available context variables are listed at last of this doc.

Compose resources in one component

It's common that a component definition is composed by multiple API resources, for example, a webserver component that is composed by a Deployment and a Service. CUE is a great solution to achieve this in simplified primitives.

:::tip Compare to using Helm, this approach gives your more flexibility as you can control the abstraction any time and integrate with traits, workflows in KubeVela better. :::

KubeVela requires you to define the template of main workload in output section, and leave all the other resource templates in outputs section with format as below:

output: {
  <template of main workload structural data>
}
outputs: {
  <unique-name>: {
    <template of auxiliary resource structural data>
  }
}

:::note The reason for this requirement is KubeVela needs to know it is currently rendering a workload so it could do some "magic" by traits such like patching annotations/labels or other data during it. :::

Below is the example for webserver definition, let's use a demo to show how to use it:

webserver: {
	annotations: {}
	attributes: workload: definition: {
		apiVersion: "apps/v1"
		kind:       "Deployment"
	}
	description: ""
	labels: {}
	type: "component"
}

template: {
  output: {
    apiVersion: "apps/v1"
    kind:       "Deployment"
    spec: {
      selector: matchLabels: {
        "app.oam.dev/component": context.name
      }
      template: {
        metadata: labels: {
          "app.oam.dev/component": context.name
        }
        spec: {
          containers: [{
            name:  context.name
            image: parameter.image

            if parameter["cmd"] != _|_ {
              command: parameter.cmd
            }

            if parameter["env"] != _|_ {
              env: parameter.env
            }

            if context["config"] != _|_ {
              env: context.config
            }

            ports: [{
              containerPort: parameter.port
            }]

            if parameter["cpu"] != _|_ {
              resources: {
                limits:
                  cpu: parameter.cpu
                requests:
                  cpu: parameter.cpu
              }
            }
          }]
        }
      }
    }
  }
  // an extra template
  outputs: service: {
    apiVersion: "v1"
    kind:       "Service"
    spec: {
      selector: {
        "app.oam.dev/component": context.name
      }
      ports: [
        {
          port:       parameter.port
          targetPort: parameter.port
        },
      ]
    }
  }
	parameter: {
    image: string
    cmd?: [...string]
    port: *80 | int
    env?: [...{
      name:   string
      value?: string
      valueFrom?: {
        secretKeyRef: {
          name: string
          key:  string
        }
      }
    }]
    cpu?: string
  }
}

Apply to your Kubernetes cluster:

vela def apply webserver.cue
expected output
ComponentDefinition webserver created in namespace vela-system.

The user could now declare an Application with it:

apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
  name: webserver-demo
  namespace: default
spec:
  components:
    - name: hello-webserver
      type: webserver
      properties:
        image: oamdev/hello-world
        port: 8000
        env:
        - name: "foo"
          value: "bar"
        cpu: "100m"

Create this application by:

vela up -f webserver-app.yaml

Then you can check the resources and find the results:

vela status webserver-demo --tree --detail
expected output
CLUSTER       NAMESPACE     RESOURCE                                             STATUS    APPLY_TIME          DETAIL
local     ─── default   ─┬─ Service/hello-webserver-auxiliaryworkload-685d98b6d9 updated   2022-10-15 21:58:35 Type: ClusterIP
                         │                                                                                     Cluster-IP: 10.43.255.55
                         │                                                                                     External-IP: <none>
                         │                                                                                     Port(s): 8000/TCP
                         │                                                                                     Age: 66s
                         └─ Deployment/hello-webserver                           updated   2022-10-15 21:58:35 Ready: 1/1  Up-to-date: 1
                                                                                                               Available: 1  Age: 66s

Define health check and status message for component

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

Health check

The spec of health check is <component-type-name>.attributes.status.healthPolicy.

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 component is healthy or not.

The keyword 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:

context:{
  name: <component name>
  appName: <app name>
  output: <K8s workload resource>
  outputs: {
    <resource1>: <K8s trait resource1>
    <resource2>: <K8s trait resource2>
  }
}

The example of health check likes below:

webserver: {
	type: "component"
  ...
	attributes: {
		status: {
			healthPolicy: #"""
        isHealth: (context.output.status.readyReplicas > 0) && (context.output.status.readyReplicas == context.output.status.replicas)
        """#
    }
  }
}

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

webserver: {
	type: "component"
  ...
	attributes: {
		status: {
			healthPolicy: #"""
        isHealth: (context.output.status.readyReplicas > 0) && (context.output.status.readyReplicas == parameter.replicas)
        """#
    }
  }
template: {
	parameter: {
    replicas: int
  } 
  ...
}

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

apiVersion: core.oam.dev/v1beta1
kind: Application
status:
  ...
  services:
  - healthy: true
    name: myweb
    ...
  status: running

Please refer to this doc for more examples.

Custom Status

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

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

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

The example of custom status likes below:

webserver: {
	type: "component"
  ...
	attributes: {
		status: {
			customStatus: #"""
				ready: {
					readyReplicas: *0 | int
				} & {
					if context.output.status.readyReplicas != _|_ {
						readyReplicas: context.output.status.readyReplicas
					}
				}
				message: "Ready:\(ready.readyReplicas)/\(context.output.spec.replicas)"
				"""#
    }
  }
}

The message will be recorded into the corresponding component in .status.services of Application resource like below.

apiVersion: core.oam.dev/v1beta1
kind: Application
status:
  ...
  services:
  - healthy: false
    message: Ready:1/1
    name: express-server

Please refer to this doc for more examples.

Full available context in Component

Context Variable Description Type
context.appName The app name corresponding to the current instance of the application. string
context.namespace The target namespace of the current resource is going to be deployed, it can be different with the namespace of application if overridden by some policies. string
context.cluster The target cluster of the current resource is going to be deployed, it can be different with the namespace of application if overridden by some policies. string
context.appRevision The app version name corresponding to the current instance of the application. string
context.appRevisionNum The app version number corresponding to the current instance of the application. int
context.name The component name corresponding to the current instance of the application. string
context.revision The version name of the current component instance. string
context.output The object structure after instantiation of current component. Object Map
context.outputs.<resourceName> Structure after instantiation of current component auxiliary resources. Object Map
context.workflowName The workflow name specified in annotation. string
context.publishVersion The version of application instance specified in annotation. string
context.appLabels The labels of the current application instance. Object Map
context.appAnnotations The annotations of the current application instance. Object Map
context.replicaKey The key of replication in context. Replication is an internal policy, it will replicate resources with different keys specified. (This feature will be introduced in v1.6+.) string

Cluster Version

Context Variable Description Type
context.clusterVersion.major The major version of the runtime Kubernetes cluster. string
context.clusterVersion.gitVersion The gitVersion of the runtime Kubernetes cluster. string
context.clusterVersion.platform The platform information of the runtime Kubernetes cluster. string
context.clusterVersion.minor The 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.

 outputs: ingress: {
	if context.clusterVersion.minor < 19 {
		apiVersion: "networking.k8s.io/v1beta1"
	}
	if context.clusterVersion.minor >= 19 {
		apiVersion: "networking.k8s.io/v1"
	}
	kind: "Ingress"
}

Or use string contain pattern for this usage:

import "strings"

if strings.Contains(context.clusterVersion.gitVersion, "k3s") {
     provider: "k3s"
}
if strings.Contains(context.clusterVersion.gitVersion, "aliyun") {
     provider: "aliyun"
}

Component definition in Kubernetes

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 component definition will be in the following API format:

apiVersion: core.oam.dev/v1beta1
kind: ComponentDefinition
metadata:
  name: <ComponentDefinition name>
  annotations:
    definition.oam.dev/description: <Function description>
spec:
  workload: # Workload Capability Indicator
    definition:
      apiVersion: <Kubernetes Workload resource group>
      kind: <Kubernetes Workload types>
  schematic:  # Component description
    cue: # Details of components defined by CUE language
      template: <CUE format template>

You can check the detail of this format here.

More examples to learn

You can check the following resources for more examples: