738 lines
18 KiB
Markdown
738 lines
18 KiB
Markdown
---
|
|
title: Defining Traits
|
|
---
|
|
|
|
In this section we will introduce how to define a Trait with CUE template.
|
|
|
|
## Composition
|
|
|
|
Defining a *Trait* with CUE template is a bit different from *Workload Type*: a trait MUST use `outputs` keyword instead of `output` in template.
|
|
|
|
With the help of CUE template, it is very nature to compose multiple Kubernetes resources in one trait.
|
|
Similarly, the format MUST be `outputs:<unique-name>:<full template>`.
|
|
|
|
Below is an example for `ingress` trait.
|
|
|
|
```yaml
|
|
apiVersion: core.oam.dev/v1alpha2
|
|
kind: TraitDefinition
|
|
metadata:
|
|
name: ingress
|
|
spec:
|
|
extension:
|
|
template: |
|
|
parameter: {
|
|
domain: string
|
|
http: [string]: int
|
|
}
|
|
|
|
// trait template can have multiple outputs in one trait
|
|
outputs: service: {
|
|
apiVersion: "v1"
|
|
kind: "Service"
|
|
spec: {
|
|
selector:
|
|
app: context.name
|
|
ports: [
|
|
for k, v in parameter.http {
|
|
port: v
|
|
targetPort: v
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
outputs: ingress: {
|
|
apiVersion: "networking.k8s.io/v1beta1"
|
|
kind: "Ingress"
|
|
metadata:
|
|
name: context.name
|
|
spec: {
|
|
rules: [{
|
|
host: parameter.domain
|
|
http: {
|
|
paths: [
|
|
for k, v in parameter.http {
|
|
path: k
|
|
backend: {
|
|
serviceName: context.name
|
|
servicePort: v
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}]
|
|
}
|
|
}
|
|
```
|
|
|
|
It can be used in the application object like below:
|
|
|
|
```yaml
|
|
apiVersion: core.oam.dev/v1alpha2
|
|
kind: Application
|
|
metadata:
|
|
name: testapp
|
|
spec:
|
|
components:
|
|
- name: express-server
|
|
type: webservice
|
|
settings:
|
|
cmd:
|
|
- node
|
|
- server.js
|
|
image: oamdev/testapp:v1
|
|
port: 8080
|
|
traits:
|
|
- name: ingress
|
|
properties:
|
|
domain: test.my.domain
|
|
http:
|
|
"/api": 8080
|
|
```
|
|
|
|
### Generate Multiple Resources with Loop
|
|
|
|
You can define the for-loop inside the `outputs`, the type of `parameter` field used in the for-loop must be a map.
|
|
|
|
Below is an example that will generate multiple Kubernetes Services in one trait:
|
|
|
|
```yaml
|
|
apiVersion: core.oam.dev/v1alpha2
|
|
kind: TraitDefinition
|
|
metadata:
|
|
name: expose
|
|
spec:
|
|
extension:
|
|
template: |
|
|
parameter: {
|
|
http: [string]: int
|
|
}
|
|
|
|
outputs: {
|
|
for k, v in parameter.http {
|
|
"\(k)": {
|
|
apiVersion: "v1"
|
|
kind: "Service"
|
|
spec: {
|
|
selector:
|
|
app: context.name
|
|
ports: [{
|
|
port: v
|
|
targetPort: v
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
The usage of this trait could be:
|
|
|
|
```yaml
|
|
apiVersion: core.oam.dev/v1alpha2
|
|
kind: Application
|
|
metadata:
|
|
name: testapp
|
|
spec:
|
|
components:
|
|
- name: express-server
|
|
type: webservice
|
|
settings:
|
|
...
|
|
traits:
|
|
- name: expose
|
|
properties:
|
|
http:
|
|
myservice1: 8080
|
|
myservice2: 8081
|
|
```
|
|
|
|
## Patch Trait
|
|
|
|
You could also use keyword `patch` to patch data to the component instance (before the resource applied) and claim this behavior as a trait.
|
|
|
|
Below is an example for `node-affinity` trait:
|
|
|
|
```yaml
|
|
apiVersion: core.oam.dev/v1alpha2
|
|
kind: TraitDefinition
|
|
metadata:
|
|
annotations:
|
|
definition.oam.dev/description: "affinity specify node affinity and toleration"
|
|
name: node-affinity
|
|
spec:
|
|
appliesToWorkloads:
|
|
- webservice
|
|
- worker
|
|
extension:
|
|
template: |-
|
|
patch: {
|
|
spec: template: spec: {
|
|
if parameter.affinity != _|_ {
|
|
affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: [{
|
|
matchExpressions: [
|
|
for k, v in parameter.affinity {
|
|
key: k
|
|
operator: "In"
|
|
values: v
|
|
},
|
|
]}]
|
|
}
|
|
if parameter.tolerations != _|_ {
|
|
tolerations: [
|
|
for k, v in parameter.tolerations {
|
|
effect: "NoSchedule"
|
|
key: k
|
|
operator: "Equal"
|
|
value: v
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
|
|
parameter: {
|
|
affinity?: [string]: [...string]
|
|
tolerations?: [string]: string
|
|
}
|
|
```
|
|
|
|
You can use it like:
|
|
|
|
```yaml
|
|
apiVersion: core.oam.dev/v1alpha2
|
|
kind: Application
|
|
metadata:
|
|
name: testapp
|
|
spec:
|
|
components:
|
|
- name: express-server
|
|
type: webservice
|
|
settings:
|
|
image: oamdev/testapp:v1
|
|
traits:
|
|
- name: "node-affinity"
|
|
properties:
|
|
affinity:
|
|
server-owner: ["owner1","owner2"]
|
|
resource-pool: ["pool1","pool2","pool3"]
|
|
tolerations:
|
|
resource-pool: "broken-pool1"
|
|
server-owner: "old-owner"
|
|
```
|
|
|
|
The patch trait above assumes the component instance have `spec.template.spec.affinity` schema. Hence we need to use it with the field `appliesToWorkloads` which can enforce the trait only to be used by these specified workload types.
|
|
|
|
By default, the patch trait in KubeVela relies on the CUE `merge` operation. It has following known constraints:
|
|
|
|
* Can not handle conflicts. For example, if a field already has a final value `replicas=5`, then the patch trait will conflict when patches `replicas=1` and fail. It only works when `replica` is not finalized before patch.
|
|
* Array list in the patch will be merged following the order of index. It can not handle the duplication of the array list members.
|
|
|
|
|
|
### Strategy Patch Trait
|
|
|
|
The `strategy patch` is a special patch logic for patching array list. This is supported **only** in KubeVela (i.e. not a standard CUE feature).
|
|
|
|
In order to make it work, you need to use annotation `//+patchKey=<key_name>` in the template.
|
|
|
|
With this annotation, 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 the value of the key name equal, then the patch data will merge into that, if no equal found, the patch will append into the array list.
|
|
|
|
The example of strategy patch trait will like below:
|
|
|
|
```yaml
|
|
apiVersion: core.oam.dev/v1alpha2
|
|
kind: TraitDefinition
|
|
metadata:
|
|
annotations:
|
|
definition.oam.dev/description: "add sidecar to the app"
|
|
name: sidecar
|
|
spec:
|
|
appliesToWorkloads:
|
|
- webservice
|
|
- worker
|
|
extension:
|
|
template: |-
|
|
patch: {
|
|
// +patchKey=name
|
|
spec: template: spec: containers: [parameter]
|
|
}
|
|
parameter: {
|
|
name: string
|
|
image: string
|
|
command?: [...string]
|
|
}
|
|
```
|
|
|
|
The patchKey is `name` which represents the container name in this example. In this case, if the workload already has a container with the same name of this `sidecar` trait, it will be a merge operation. 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.
|
|
|
|
### Patch The Trait
|
|
|
|
If patch and outputs both exist in one trait, the patch part will execute first and then the output object will be rendered out.
|
|
|
|
```yaml
|
|
apiVersion: core.oam.dev/v1alpha2
|
|
kind: TraitDefinition
|
|
metadata:
|
|
annotations:
|
|
definition.oam.dev/description: "service the app"
|
|
name: kservice
|
|
spec:
|
|
appliesToWorkloads:
|
|
- webservice
|
|
- worker
|
|
extension:
|
|
template: |-
|
|
patch: {spec: template: metadata: labels: app: context.name}
|
|
outputs: service: {
|
|
apiVersion: "v1"
|
|
kind: "Service"
|
|
metadata: name: context.name
|
|
spec: {
|
|
selector: app: context.name
|
|
ports: [
|
|
for k, v in parameter.http {
|
|
port: v
|
|
targetPort: v
|
|
}
|
|
]
|
|
}
|
|
}
|
|
parameter: {
|
|
http: [string]: int
|
|
}
|
|
```
|
|
|
|
## Processing Trait
|
|
|
|
A trait can also help you to do some processing job. Currently, we have supported http request.
|
|
|
|
The keyword is `processing`, inside the `processing`, there are two keywords `output` and `http`.
|
|
|
|
You can define http request `method`, `url`, `body`, `header` and `trailer` in the `http` section.
|
|
KubeVela will send a request using this information, the requested server shall output a **json result**.
|
|
|
|
The `output` section will used to match with the `json result`, correlate fields by name will be automatically filled into it.
|
|
Then you can use the requested data from `processing.output` into `patch` or `output/outputs`.
|
|
|
|
Below is an example:
|
|
|
|
```yaml
|
|
apiVersion: core.oam.dev/v1alpha1
|
|
kind: TraitDefinition
|
|
metadata:
|
|
name: auth-service
|
|
spec:
|
|
schematic:
|
|
cue:
|
|
template: |
|
|
parameter: {
|
|
serviceURL: string
|
|
}
|
|
|
|
processing: {
|
|
output: {
|
|
token?: string
|
|
}
|
|
// task shall output a json result and output will correlate fields by name.
|
|
http: {
|
|
method: *"GET" | string
|
|
url: parameter.serviceURL
|
|
request: {
|
|
body?: bytes
|
|
header: {}
|
|
trailer: {}
|
|
}
|
|
}
|
|
}
|
|
|
|
patch: {
|
|
data: token: processing.output.token
|
|
}
|
|
```
|
|
|
|
## Simple data passing
|
|
|
|
The trait can use the data of workload output and outputs to fill itself.
|
|
|
|
There are two keywords `output` and `outputs` in the rendering context.
|
|
You can use `context.output` refer to the workload object, and use `context.outputs.<xx>` refer to the trait object.
|
|
please make sure the trait resource name is unique, or the former data will be covered by the latter one.
|
|
|
|
Below is an example
|
|
1. the main workload object(Deployment) in this example will render into the context.output before rendering traits.
|
|
2. the `context.outputs.<xx>` will keep all these rendered trait data and can be used in the traits after them.
|
|
|
|
```yaml
|
|
apiVersion: core.oam.dev/v1alpha2
|
|
kind: WorkloadDefinition
|
|
metadata:
|
|
name: worker
|
|
spec:
|
|
definitionRef:
|
|
name: deployments.apps
|
|
extension:
|
|
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
|
|
ports: [{containerPort: parameter.port}]
|
|
envFrom: [{
|
|
configMapRef: name: context.name + "game-config"
|
|
}]
|
|
if parameter["cmd"] != _|_ {
|
|
command: parameter.cmd
|
|
}
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
outputs: gameconfig: {
|
|
apiVersion: "v1"
|
|
kind: "ConfigMap"
|
|
metadata: {
|
|
name: context.name + "game-config"
|
|
}
|
|
data: {
|
|
enemies: parameter.enemies
|
|
lives: parameter.lives
|
|
}
|
|
}
|
|
|
|
parameter: {
|
|
// +usage=Which image would you like to use for your service
|
|
// +short=i
|
|
image: string
|
|
// +usage=Commands to run in the container
|
|
cmd?: [...string]
|
|
lives: string
|
|
enemies: string
|
|
port: int
|
|
}
|
|
|
|
---
|
|
apiVersion: core.oam.dev/v1alpha2
|
|
kind: TraitDefinition
|
|
metadata:
|
|
name: ingress
|
|
spec:
|
|
extension:
|
|
template: |
|
|
parameter: {
|
|
domain: string
|
|
path: string
|
|
exposePort: int
|
|
}
|
|
// trait template can have multiple outputs in one trait
|
|
outputs: service: {
|
|
apiVersion: "v1"
|
|
kind: "Service"
|
|
spec: {
|
|
selector:
|
|
app: context.name
|
|
ports: [{
|
|
port: parameter.exposePort
|
|
targetPort: context.output.spec.template.spec.containers[0].ports[0].containerPort
|
|
}]
|
|
}
|
|
}
|
|
outputs: ingress: {
|
|
apiVersion: "networking.k8s.io/v1beta1"
|
|
kind: "Ingress"
|
|
metadata:
|
|
name: context.name
|
|
labels: config: context.outputs.gameconfig.data.enemies
|
|
spec: {
|
|
rules: [{
|
|
host: parameter.domain
|
|
http: {
|
|
paths: [{
|
|
path: parameter.path
|
|
backend: {
|
|
serviceName: context.name
|
|
servicePort: parameter.exposePort
|
|
}
|
|
}]
|
|
}
|
|
}]
|
|
}
|
|
}
|
|
```
|
|
|
|
## More Use Cases for Patch Trait
|
|
|
|
Patch trait could be very powerful, here are some more advanced use cases.
|
|
|
|
### Add Labels
|
|
|
|
For example, patch common label (virtual group) to the component workload.
|
|
|
|
```yaml
|
|
apiVersion: core.oam.dev/v1alpha2
|
|
kind: TraitDefinition
|
|
metadata:
|
|
annotations:
|
|
definition.oam.dev/description: "Add virtual group labels"
|
|
name: virtualgroup
|
|
spec:
|
|
appliesToWorkloads:
|
|
- webservice
|
|
- worker
|
|
extension:
|
|
template: |-
|
|
patch: {
|
|
spec: template: {
|
|
metadata: labels: {
|
|
if parameter.type == "namespace" {
|
|
"app.namespace.virtual.group": parameter.group
|
|
}
|
|
if parameter.type == "cluster" {
|
|
"app.cluster.virtual.group": parameter.group
|
|
}
|
|
}
|
|
}
|
|
}
|
|
parameter: {
|
|
group: *"default" | string
|
|
type: *"namespace" | string
|
|
}
|
|
```
|
|
|
|
Then it could be used like:
|
|
|
|
```yaml
|
|
apiVersion: core.oam.dev/v1alpha2
|
|
kind: Application
|
|
spec:
|
|
...
|
|
traits:
|
|
- name: virtualgroup
|
|
properties:
|
|
group: "my-group1"
|
|
type: "cluster"
|
|
```
|
|
|
|
In this example, different type will use different label key.
|
|
|
|
### Add Annotations
|
|
|
|
Similar to common labels, you could also patch the component workload with annotations. The annotation value will be a JSON string.
|
|
|
|
```yaml
|
|
apiVersion: core.oam.dev/v1alpha2
|
|
kind: TraitDefinition
|
|
metadata:
|
|
annotations:
|
|
definition.oam.dev/description: "Specify auto scale by annotation"
|
|
name: kautoscale
|
|
spec:
|
|
appliesToWorkloads:
|
|
- webservice
|
|
- worker
|
|
extension:
|
|
template: |-
|
|
import "encoding/json"
|
|
|
|
patch: {
|
|
metadata: annotations: {
|
|
"my.custom.autoscale.annotation": json.Marshal({
|
|
"minReplicas": parameter.min
|
|
"maxReplicas": parameter.max
|
|
})
|
|
}
|
|
}
|
|
parameter: {
|
|
min: *1 | int
|
|
max: *3 | int
|
|
}
|
|
```
|
|
|
|
### Add Pod ENV
|
|
|
|
Inject some system environments into pod is also very common use case.
|
|
|
|
The example could be like below, this case rely on strategy merge patch, so don't forget add `+patchKey=name` like below:
|
|
|
|
```yaml
|
|
apiVersion: core.oam.dev/v1alpha2
|
|
kind: TraitDefinition
|
|
metadata:
|
|
annotations:
|
|
definition.oam.dev/description: "add env into your pods"
|
|
name: env
|
|
spec:
|
|
appliesToWorkloads:
|
|
- webservice
|
|
- worker
|
|
extension:
|
|
template: |-
|
|
patch: {
|
|
spec: template: spec: {
|
|
// +patchKey=name
|
|
containers: [{
|
|
name: context.name
|
|
// +patchKey=name
|
|
env: [
|
|
for k, v in parameter.env {
|
|
name: k
|
|
value: v
|
|
},
|
|
]
|
|
}]
|
|
}
|
|
}
|
|
|
|
parameter: {
|
|
env: [string]: string
|
|
}
|
|
```
|
|
|
|
### Dynamically Pod Service Account
|
|
|
|
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, you can also use request body. You may refer to [processing](#Processing-Trait) section for more details.
|
|
|
|
```yaml
|
|
apiVersion: core.oam.dev/v1alpha2
|
|
kind: TraitDefinition
|
|
metadata:
|
|
annotations:
|
|
definition.oam.dev/description: "dynamically specify service account"
|
|
name: service-account
|
|
spec:
|
|
appliesToWorkloads:
|
|
- webservice
|
|
- worker
|
|
extension:
|
|
template: |-
|
|
processing: {
|
|
output: {
|
|
credentials?: string
|
|
}
|
|
http: {
|
|
method: *"GET" | string
|
|
url: parameter.serviceURL
|
|
request: {
|
|
header: {
|
|
"authorization.token": parameter.uidtoken
|
|
}
|
|
}
|
|
}
|
|
}
|
|
patch: {
|
|
spec: template: spec: serviceAccountName: processing.output.credentials
|
|
}
|
|
|
|
parameter: {
|
|
uidtoken: string
|
|
serviceURL: string
|
|
}
|
|
```
|
|
|
|
### Add Init Container
|
|
|
|
Init container is useful to pre-define operations in an image and run it before app container.
|
|
|
|
> Please check [Kubernetes documentation](https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-initialization/#create-a-pod-that-has-an-init-container) for more detail about Init Container.
|
|
|
|
Below is an example of init container trait:
|
|
|
|
```yaml
|
|
apiVersion: core.oam.dev/v1alpha2
|
|
kind: TraitDefinition
|
|
metadata:
|
|
annotations:
|
|
definition.oam.dev/description: "add an init container and use shared volume with pod"
|
|
name: init-container
|
|
spec:
|
|
appliesToWorkloads:
|
|
- webservice
|
|
- worker
|
|
extension:
|
|
template: |-
|
|
patch: {
|
|
spec: template: spec: {
|
|
// +patchKey=name
|
|
containers: [{
|
|
name: context.name
|
|
// +patchKey=name
|
|
volumeMounts: [{
|
|
name: parameter.mountName
|
|
mountPath: parameter.appMountPath
|
|
}]
|
|
}]
|
|
initContainers: [{
|
|
name: parameter.name
|
|
image: parameter.image
|
|
if parameter.command != _|_ {
|
|
command: parameter.command
|
|
}
|
|
// +patchKey=name
|
|
volumeMounts: [{
|
|
name: parameter.mountName
|
|
mountPath: parameter.initMountPath
|
|
}]
|
|
}]
|
|
// +patchKey=name
|
|
volumes: [{
|
|
name: parameter.mountName
|
|
emptyDir: {}
|
|
}]
|
|
}
|
|
}
|
|
|
|
parameter: {
|
|
name: string
|
|
image: string
|
|
command?: [...string]
|
|
mountName: *"workdir" | string
|
|
appMountPath: string
|
|
initMountPath: string
|
|
}
|
|
```
|
|
|
|
This case must rely on the strategy merge patch, for every array list, we add a `// +patchKey=name` annotation to avoid conflict.
|
|
|
|
The usage could be:
|
|
|
|
```yaml
|
|
apiVersion: core.oam.dev/v1alpha2
|
|
kind: Application
|
|
metadata:
|
|
name: testapp
|
|
spec:
|
|
components:
|
|
- name: express-server
|
|
type: webservice
|
|
settings:
|
|
image: oamdev/testapp:v1
|
|
traits:
|
|
- name: "init-container"
|
|
properties:
|
|
name: "install-container"
|
|
image: "busybox"
|
|
command:
|
|
- wget
|
|
- "-O"
|
|
- "/work-dir/index.html"
|
|
- http://info.cern.ch
|
|
mountName: "workdir"
|
|
appMountPath: "/usr/share/nginx/html"
|
|
initMountPath: "/work-dir"
|
|
```
|