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

756 lines
22 KiB
Markdown

---
title: Component Definition
---
In this section, we will introduce how to use [CUE](../cue/basic.md) to customize components via `ComponentDefinition`. Make sure you've learned the basic knowledge about [Definition Concept](../../getting-started/definition.md) and [how to manage definition](../cue/definition-edit.md).
## Declare `ComponentDefinition`
First, generate `ComponentDefinition` scaffolds via `vela def init` with existed YAML file.
The YAML file:
```yaml title="stateless.yaml"
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:
```shell
vela def init stateless -t component --template-yaml ./stateless.yaml -o stateless.cue
```
It generates a file:
```cue title="stateless.cue"
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 :
```cue
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:
```shell
vela def vet stateless.cue
```
<details>
<summary>expected output</summary>
```
Validation succeed.
```
</details>
Apply above `ComponentDefinition` to your Kubernetes cluster to make it work:
```shell
vela def apply stateless.cue
```
<details>
<summary>expected output</summary>
```
ComponentDefinition stateless created in namespace vela-system.
```
</details>
Then the end user can check the schema and use it in an application now:
```
vela show stateless
```
<details>
<summary>expected output</summary>
```
# Specification
+-------+-------------+--------+----------+---------+
| NAME | DESCRIPTION | TYPE | REQUIRED | DEFAULT |
+-------+-------------+--------+----------+---------+
| name | | string | true | |
| image | | string | true | |
+-------+-------------+--------+----------+---------+
```
</details>
Declare another component named `task` which is an abstraction for run-to-completion workload works the same.
<details>
<summary>Check the details for another example to define a task based component.</summary>
```shell
vela def init task -t component -o task.cue
```
It generates a file:
```cue
// $ 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:
```cue
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:
```shell
$ vela def apply task.cue
ComponentDefinition task created in namespace vela-system.
```
</details>
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:
```yaml
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"
```
<details>
<summary> Learn Details Under The Hood </summary>
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.
```yaml
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
```
</details>
You can also use [dry run](../debug/dry-run.md) 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`).
```cue
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:
```cue
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](#full-available-context-in-component) 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](../../tutorials/helm.md), 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:
```cue
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:
```cue title="webserver.cue"
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:
```shell
vela def apply webserver.cue
```
<details>
<summary>expected output</summary>
```
ComponentDefinition webserver created in namespace vela-system.
```
</details>
The user could now declare an `Application` with it:
```yaml title="webserver-app.yaml"
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:
```shell
vela status webserver-demo --tree --detail
```
<details>
<summary>expected output</summary>
```console
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
```
</details>
## 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:
```cue
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:
```cue
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.
```yaml
apiVersion: core.oam.dev/v1beta1
kind: Application
status:
...
services:
- healthy: true
name: myweb
...
status: running
```
> Please refer to [this doc](https://github.com/kubevela/kubevela/blob/master/vela-templates/definitions/internal/component/webservice.cue#L29-L50) 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:
```cue
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.
```yaml
apiVersion: core.oam.dev/v1beta1
kind: Application
status:
...
services:
- healthy: false
message: Ready:1/1
name: express-server
```
> Please refer to [this doc](https://github.com/kubevela/kubevela/blob/master/vela-templates/definitions/internal/component/webservice.cue#L29-L50) 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:
```yaml
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](../oam/x-definition.md).
## More examples to learn
You can check the following resources for more examples:
- Builtin component definitions in the [KubeVela github repo](https://github.com/kubevela/kubevela/tree/master/vela-templates/definitions/internal/component).
- Definitions defined in addons in the [catalog repo](https://github.com/kubevela/catalog/tree/master/addons).