move template out from extension

This commit is contained in:
天元
2021-02-05 14:45:30 +08:00
parent b6f10bb1fa
commit 96a589b098
25 changed files with 681 additions and 474 deletions
+20
View File
@@ -68,6 +68,16 @@ type WorkloadDefinitionSpec struct {
// +optional
Status *Status `json:"status,omitempty"`
// Template defines the abstraction template data of the workload, it will replace the old template in extension field.
// the data format depends on templateType, by default it's CUE
// +optional
Template string `json:"template,omitempty"`
// TemplateType defines the data format of the template, by default it's CUE format
// Terraform HCL, Helm Chart will also be candidates in the near future.
// +optional
TemplateType string `json:"templateType,omitempty"`
// Extension is used for extension needs by OAM platform builders
// +optional
// +kubebuilder:pruning:PreserveUnknownFields
@@ -140,6 +150,16 @@ type TraitDefinitionSpec struct {
// +optional
ConflictsWith []string `json:"conflictsWith,omitempty"`
// Template defines the abstraction template data of the workload, it will replace the old template in extension field.
// the data format depends on templateType, by default it's CUE
// +optional
Template string `json:"template,omitempty"`
// TemplateType defines the data format of the template, by default it's CUE format
// Terraform HCL, Helm Chart will also be candidates in the near future.
// +optional
TemplateType string `json:"templateType,omitempty"`
// Status defines the custom health policy and status message for trait
// +optional
Status *Status `json:"status,omitempty"`
-16
View File
@@ -18,12 +18,10 @@ package types
import (
"encoding/json"
"fmt"
"cuelang.org/go/cue"
"github.com/google/go-cmp/cmp"
"github.com/spf13/pflag"
"k8s.io/apimachinery/pkg/runtime"
)
// Source record the source of Capability
@@ -107,20 +105,6 @@ type Parameter struct {
Alias string `json:"alias,omitempty"`
}
// ConvertTemplateJSON2Object convert spec.extension to object
func ConvertTemplateJSON2Object(in *runtime.RawExtension) (Capability, error) {
var t Capability
var extension Capability
if in == nil || in.Raw == nil {
return t, fmt.Errorf("no template found")
}
err := json.Unmarshal(in.Raw, &extension)
if err == nil {
t = extension
}
return t, err
}
// SetFlagBy set cli flag from Parameter
func SetFlagBy(flags *pflag.FlagSet, v Parameter) {
name := v.Name
@@ -78,6 +78,12 @@ spec:
description: HealthPolicy defines the health check policy for the abstraction
type: string
type: object
template:
description: Template defines the abstraction template data of the workload, it will replace the old template in extension field. the data format depends on templateType, by default it's CUE
type: string
templateType:
description: TemplateType defines the data format of the template, by default it's CUE format Terraform HCL, Helm Chart will also be candidates in the near future.
type: string
workloadRefPath:
description: WorkloadRefPath indicates where/if a trait accepts a workloadRef object
type: string
@@ -92,6 +92,12 @@ spec:
description: HealthPolicy defines the health check policy for the abstraction
type: string
type: object
template:
description: Template defines the abstraction template data of the workload, it will replace the old template in extension field. the data format depends on templateType, by default it's CUE
type: string
templateType:
description: TemplateType defines the data format of the template, by default it's CUE format Terraform HCL, Helm Chart will also be candidates in the near future.
type: string
required:
- definitionRef
type: object
@@ -20,51 +20,50 @@ spec:
appliesToWorkloads:
- webservice
- worker
extension:
template: |
parameter: {
domain: string
http: [string]: int
}
// trait template can have multiple outputs in one trait
outputs: service: {
apiVersion: "v1"
kind: "Service"
metadata:
name: context.name
spec: {
selector:
"app.oam.dev/component": 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
}
},
]
}
}]
}
}
template: |
parameter: {
domain: string
http: [string]: int
}
// trait template can have multiple outputs in one trait
outputs: service: {
apiVersion: "v1"
kind: "Service"
metadata:
name: context.name
spec: {
selector:
"app.oam.dev/component": 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
}
},
]
}
}]
}
}
@@ -12,18 +12,17 @@ spec:
definitionRef:
name: manualscalertraits.core.oam.dev
workloadRefPath: spec.workloadRef
extension:
template: |-
output: {
apiVersion: "core.oam.dev/v1alpha2"
kind: "ManualScalerTrait"
spec: {
replicaCount: parameter.replicas
}
}
parameter: {
//+short=r
//+usage=Replicas of the workload
replicas: *1 | int
}
template: |
output: {
apiVersion: "core.oam.dev/v1alpha2"
kind: "ManualScalerTrait"
spec: {
replicaCount: parameter.replicas
}
}
parameter: {
//+short=r
//+usage=Replicas of the workload
replicas: *1 | int
}
@@ -8,40 +8,39 @@ metadata:
spec:
definitionRef:
name: jobs.batch
extension:
template: |
output: {
apiVersion: "batch/v1"
kind: "Job"
spec: {
parallelism: parameter.count
completions: parameter.count
template: spec: {
restartPolicy: parameter.restart
containers: [{
name: context.name
image: parameter.image
if parameter["cmd"] != _|_ {
command: parameter.cmd
}
}]
}
}
}
parameter: {
// +usage=specify number of tasks to run in parallel
// +short=c
count: *1 | int
// +usage=Which image would you like to use for your service
// +short=i
image: string
// +usage=Define the job restart policy, the value can only be Never or OnFailure. By default, it's Never.
restart: *"Never" | string
// +usage=Commands to run in the container
cmd?: [...string]
}
template: |
output: {
apiVersion: "batch/v1"
kind: "Job"
spec: {
parallelism: parameter.count
completions: parameter.count
template: spec: {
restartPolicy: parameter.restart
containers: [{
name: context.name
image: parameter.image
if parameter["cmd"] != _|_ {
command: parameter.cmd
}
}]
}
}
}
parameter: {
// +usage=specify number of tasks to run in parallel
// +short=c
count: *1 | int
// +usage=Which image would you like to use for your service
// +short=i
image: string
// +usage=Define the job restart policy, the value can only be Never or OnFailure. By default, it's Never.
restart: *"Never" | string
// +usage=Commands to run in the container
cmd?: [...string]
}
@@ -9,84 +9,83 @@ metadata:
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
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
}
}
}]
}
}
}
}
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]
// +usage=Which port do you want customer traffic sent to
// +short=p
port: *80 | int
// +usage=Define arguments by using environment variables
env?: [...{
// +usage=Environment variable name
name: string
// +usage=The value of the environment variable
value?: string
// +usage=Specifies a source the value of this var should come from
valueFrom?: {
// +usage=Selects a key of a secret in the pod's namespace
secretKeyRef: {
// +usage=The name of the secret in the pod's namespace to select from
name: string
// +usage=The key of the secret to select from. Must be a valid secret key
key: string
}
}
}]
// +usage=Number of CPU units for the service, like `0.5` (0.5 CPU core), `1` (1 CPU core)
cpu?: string
}
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
}
}
}]
}
}
}
}
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]
// +usage=Which port do you want customer traffic sent to
// +short=p
port: *80 | int
// +usage=Define arguments by using environment variables
env?: [...{
// +usage=Environment variable name
name: string
// +usage=The value of the environment variable
value?: string
// +usage=Specifies a source the value of this var should come from
valueFrom?: {
// +usage=Selects a key of a secret in the pod's namespace
secretKeyRef: {
// +usage=The name of the secret in the pod's namespace to select from
name: string
// +usage=The key of the secret to select from. Must be a valid secret key
key: string
}
}
}]
// +usage=Number of CPU units for the service, like `0.5` (0.5 CPU core), `1` (1 CPU core)
cpu?: string
}
@@ -8,40 +8,39 @@ metadata:
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
if parameter["cmd"] != _|_ {
command: parameter.cmd
}
}]
}
}
}
}
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]
}
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
}
}]
}
}
}
}
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]
}
+90 -94
View File
@@ -1,7 +1,7 @@
apiVersion: core.oam.dev/v1alpha2
kind: WorkloadDefinition
metadata:
name: worker
name: nworker
annotations:
definition.oam.dev/description: "Describes long-running, scalable, containerized services that running at backend. They do NOT have network endpoint to receive external network traffic."
spec:
@@ -12,60 +12,57 @@ spec:
isHealth: (context.output.status.readyReplicas > 0) && (context.output.status.readyReplicas == context.output.status.replicas)
customStatus: |-
message: "type: " + context.output.spec.template.spec.containers[0].image + ",\t enemies:" + context.outputs.gameconfig.data.enemies
extension:
template: |
output: {
apiVersion: "apps/v1"
kind: "Deployment"
spec: {
selector: matchLabels: {
"app.oam.dev/component": context.name
}
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
}
template: {
metadata: labels: {
"app.oam.dev/component": context.name
}
spec: {
containers: [{
name: context.name
image: parameter.image
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
}
spec: {
containers: [{
name: context.name
image: parameter.image
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
}
---
apiVersion: core.oam.dev/v1alpha2
@@ -78,46 +75,45 @@ spec:
message: "type: "+ context.outputs.service.spec.type +",\t clusterIP:"+ context.outputs.service.spec.clusterIP+",\t ports:"+ "\(context.outputs.service.spec.ports[0].port)"+",\t domain"+context.outputs.ingress.spec.rules[0].host
healthPolicy: |
isHealth: len(context.outputs.service.spec.clusterIP) > 0
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
}
}
]
}
}]
}
}
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
}
},
]
}
}]
}
}
+1 -2
View File
@@ -19,5 +19,4 @@ spec:
appliesToWorkloads:
- webservice
- worker
extension:
template: |
template: |
@@ -11,5 +11,4 @@ spec:
definitionRef:
name: manualscalertraits.core.oam.dev
workloadRefPath: spec.workloadRef
extension:
template: |-
template: |
+1 -2
View File
@@ -7,5 +7,4 @@ metadata:
spec:
definitionRef:
name: jobs.batch
extension:
template: |
template: |
@@ -8,5 +8,4 @@ metadata:
spec:
definitionRef:
name: deployments.apps
extension:
template: |
template: |
+1 -2
View File
@@ -7,5 +7,4 @@ metadata:
spec:
definitionRef:
name: deployments.apps
extension:
template: |
template: |
+1 -1
View File
@@ -16,7 +16,7 @@ echo "# Code generated by KubeVela templates. DO NOT EDIT." >> tmpC
for filename in `ls cue`; do
cat "cue/${filename}" > tmp
echo "" >> tmp
sed -i.bak 's/^/ /' tmp
sed -i.bak 's/^/ /' tmp
nameonly="${filename%.*}"
@@ -77,6 +77,12 @@ spec:
description: HealthPolicy defines the health check policy for the abstraction
type: string
type: object
template:
description: Template defines the abstraction template data of the workload, it will replace the old template in extension field. the data format depends on templateType, by default it's CUE
type: string
templateType:
description: TemplateType defines the data format of the template, by default it's CUE format Terraform HCL, Helm Chart will also be candidates in the near future.
type: string
workloadRefPath:
description: WorkloadRefPath indicates where/if a trait accepts a workloadRef object
type: string
@@ -91,6 +91,12 @@ spec:
description: HealthPolicy defines the health check policy for the abstraction
type: string
type: object
template:
description: Template defines the abstraction template data of the workload, it will replace the old template in extension field. the data format depends on templateType, by default it's CUE
type: string
templateType:
description: TemplateType defines the data format of the template, by default it's CUE format Terraform HCL, Helm Chart will also be candidates in the near future.
type: string
required:
- definitionRef
type: object
+5 -5
View File
@@ -230,7 +230,7 @@ func (p *Parser) GenerateApplicationConfiguration(app *Appfile, ns string) (*v1a
}
for _, tr := range wl.Traits {
if err := tr.EvalContext(pCtx); err != nil {
return nil, nil, err
return nil, nil, errors.Wrapf(err, "evaluate template trait=%s app=%s", tr.Name, wl.Name)
}
}
comp, acComp, err := evalWorkloadWithContext(pCtx, wl, app.Name, wl.Name)
@@ -266,7 +266,7 @@ func evalWorkloadWithContext(pCtx process.Context, wl *Workload, appName, compNa
base, assists := pCtx.Output()
componentWorkload, err := base.Unstructured()
if err != nil {
return nil, nil, err
return nil, nil, errors.Wrapf(err, "evaluate base template component=%s app=%s", compName, appName)
}
labels := map[string]string{
@@ -284,7 +284,7 @@ func evalWorkloadWithContext(pCtx process.Context, wl *Workload, appName, compNa
for _, assist := range assists {
tr, err := assist.Ins.Unstructured()
if err != nil {
return nil, nil, err
return nil, nil, errors.Wrapf(err, "evaluate trait=%s template for component=%s app=%s", assist.Name, compName, appName)
}
labels := map[string]string{
oam.TraitTypeLabel: assist.Type,
@@ -314,12 +314,12 @@ func PrepareProcessContext(k8sClient client.Client, wl *Workload, applicationNam
var envName = namespace
data, err := cg.GetConfigData(config.GenConfigMapName(applicationName, wl.Name, userConfig), envName)
if err != nil {
return nil, err
return nil, errors.Wrapf(err, "get config=%s for app=%s in namespace=%s", userConfig, applicationName, namespace)
}
pCtx.SetConfigs(data)
}
if err := wl.EvalContext(pCtx); err != nil {
return nil, err
return nil, errors.Wrapf(err, "evaluate base template app=%s in namespace=%s", applicationName, namespace)
}
return pCtx, nil
}
@@ -1227,58 +1227,57 @@ spec:
isHealth: (context.output.status.readyReplicas > 0) && (context.output.status.readyReplicas == context.output.status.replicas)
customStatus: |-
message: "type: " + context.output.spec.template.spec.containers[0].image + ",\t enemies:" + context.outputs.gameconfig.data.enemies
extension:
template: |
output: {
apiVersion: "apps/v1"
kind: "Deployment"
spec: {
selector: matchLabels: {
"app.oam.dev/component": context.name
}
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
}
template: {
metadata: labels: {
"app.oam.dev/component": context.name
}
spec: {
containers: [{
name: context.name
image: parameter.image
envFrom: [{
configMapRef: name: context.name + "game-config"
}]
if parameter["cmd"] != _|_ {
command: parameter.cmd
}
}]
}
}
}
}
spec: {
containers: [{
name: context.name
image: parameter.image
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
}
}
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
}
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
}
`
tDDefYaml = `
apiVersion: core.oam.dev/v1alpha2
@@ -1394,49 +1393,49 @@ spec:
message: "type: "+ context.outputs.service.spec.type +",\t clusterIP:"+ context.outputs.service.spec.clusterIP+",\t ports:"+ "\(context.outputs.service.spec.ports[0].port)"+",\t domain"+context.outputs.ingress.spec.rules[0].host
healthPolicy: |
isHealth: len(context.outputs.service.spec.clusterIP) > 0
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
}
}
]
}
}]
}
}`
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
}
},
]
}
}]
}
}
`
)
func NewMock() *httptest.Server {
+35 -8
View File
@@ -48,7 +48,7 @@ func LoadTemplate(cli client.Reader, key string, kd types.CapType) (*Template, e
if wd.Annotations["type"] == string(types.TerraformCategory) {
capabilityCategory = types.TerraformCategory
}
tmpl, err := NewTemplate(wd.Spec.Extension, wd.Spec.Status)
tmpl, err := NewTemplate(wd.Spec.Template, wd.Spec.Status, wd.Spec.Extension)
if err != nil {
return nil, errors.WithMessagef(err, "LoadTemplate [%s] ", key)
}
@@ -67,7 +67,7 @@ func LoadTemplate(cli client.Reader, key string, kd types.CapType) (*Template, e
if td.Annotations["type"] == string(types.TerraformCategory) {
capabilityCategory = types.TerraformCategory
}
tmpl, err := NewTemplate(td.Spec.Extension, td.Spec.Status)
tmpl, err := NewTemplate(td.Spec.Template, td.Spec.Status, td.Spec.Extension)
if err != nil {
return nil, errors.WithMessagef(err, "LoadTemplate [%s] ", key)
}
@@ -79,18 +79,24 @@ func LoadTemplate(cli client.Reader, key string, kd types.CapType) (*Template, e
case types.TypeScope:
// TODO: add scope template support
}
return nil, fmt.Errorf("kind(%s) of %s not supported", kd, key)
}
// NewTemplate will create CUE template for inner AbstractEngine using.
func NewTemplate(raw *runtime.RawExtension, status *v1alpha2.Status) (*Template, error) {
func NewTemplate(template string, status *v1alpha2.Status, raw *runtime.RawExtension) (*Template, error) {
extension := map[string]interface{}{}
if err := json.Unmarshal(raw.Raw, &extension); err != nil {
return nil, err
}
tmp := &Template{
TemplateStr: fmt.Sprint(extension["template"]),
TemplateStr: template,
}
if tmp.TemplateStr == "" && raw != nil {
if err := json.Unmarshal(raw.Raw, &extension); err != nil {
return nil, err
}
if extTemplate, ok := extension["template"]; ok {
if tmpStr, ok := extTemplate.(string); ok {
tmp.TemplateStr = tmpStr
}
}
}
if status != nil {
tmp.CustomStatus = status.CustomStatus
@@ -98,3 +104,24 @@ func NewTemplate(raw *runtime.RawExtension, status *v1alpha2.Status) (*Template,
}
return tmp, nil
}
// ConvertTemplateJSON2Object convert spec.extension to object
func ConvertTemplateJSON2Object(in *runtime.RawExtension, specTemplate string) (types.Capability, error) {
var t types.Capability
capTemplate, err := NewTemplate(specTemplate, nil, in)
if err != nil {
return t, errors.Wrapf(err, "parse cue template")
}
var extension types.Capability
if in != nil && in.Raw != nil {
err := json.Unmarshal(in.Raw, &extension)
if err != nil {
return t, errors.Wrapf(err, "parse extension fail")
}
t = extension
}
if capTemplate.TemplateStr != "" {
t.CueTemplate = capTemplate.TemplateStr
}
return t, err
}
+173 -1
View File
@@ -4,6 +4,8 @@ import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"cuelang.org/go/cue"
"github.com/crossplane/crossplane-runtime/pkg/test"
"k8s.io/apimachinery/pkg/runtime"
@@ -13,7 +15,7 @@ import (
"github.com/oam-dev/kubevela/apis/types"
)
func TestTemplate(t *testing.T) {
func TestLoadWorkloadTemplate(t *testing.T) {
cueTemplate := `
context: {
name: "test"
@@ -110,3 +112,173 @@ spec:
t.Errorf("parsered template is not correct")
}
}
func TestLoadTraitTemplate(t *testing.T) {
cueTemplate := `
parameter: {
domain: string
http: [string]: int
}
context: {
name: "test"
}
// trait template can have multiple outputs in one trait
outputs: service: {
apiVersion: "v1"
kind: "Service"
metadata:
name: context.name
spec: {
selector:
"app.oam.dev/component": 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
}
},
]
}
}]
}
}
`
var traitDefintion = `
apiVersion: core.oam.dev/v1alpha2
kind: TraitDefinition
metadata:
annotations:
definition.oam.dev/description: "Configures K8s ingress and service to enable web traffic for your service.
Please use route trait in cap center for advanced usage."
name: ingress
spec:
status:
customStatus: |-
if len(context.outputs.ingress.status.loadBalancer.ingress) > 0 {
message: "Visiting URL: " + context.outputs.ingress.spec.rules[0].host + ", IP: " + context.outputs.ingress.status.loadBalancer.ingress[0].ip
}
if len(context.outputs.ingress.status.loadBalancer.ingress) == 0 {
message: "No loadBalancer found, visiting by using 'vela port-forward " + context.appName + " --route'\n"
}
healthPolicy: |
isHealth: len(context.outputs.service.spec.clusterIP) > 0
appliesToWorkloads:
- webservice
- worker
template: |
` + cueTemplate
// Create mock client
tclient := test.MockClient{
MockGet: func(ctx context.Context, key ktypes.NamespacedName, obj runtime.Object) error {
switch o := obj.(type) {
case *v1alpha2.TraitDefinition:
wd, err := UnMarshalStringToTraitDefinition(traitDefintion)
if err != nil {
return err
}
*o = *wd
}
return nil
},
}
temp, err := LoadTemplate(&tclient, "ingress", types.TypeTrait)
if err != nil {
t.Error(err)
return
}
var r cue.Runtime
inst, err := r.Compile("-", temp.TemplateStr)
if err != nil {
t.Error(err)
return
}
instDest, err := r.Compile("-", cueTemplate)
if err != nil {
t.Error(err)
return
}
s1, _ := inst.Value().String()
s2, _ := instDest.Value().String()
if s1 != s2 {
t.Errorf("parsered template is not correct")
}
}
func TestNewTemplate(t *testing.T) {
testCases := map[string]struct {
tmp string
status *v1alpha2.Status
ext *runtime.RawExtension
exp *Template
}{
"only tmp": {
tmp: "t1",
exp: &Template{
TemplateStr: "t1",
},
},
"no tmp,but has extension": {
ext: &runtime.RawExtension{Raw: []byte(`{"template":"t1"}`)},
exp: &Template{
TemplateStr: "t1",
},
},
"no tmp,but has extension without temp": {
ext: &runtime.RawExtension{Raw: []byte(`{"template":{"t1":"t2"}}`)},
exp: &Template{
TemplateStr: "",
},
},
"tmp with status": {
tmp: "t1",
status: &v1alpha2.Status{
CustomStatus: "s1",
HealthPolicy: "h1",
},
exp: &Template{
TemplateStr: "t1",
CustomStatus: "s1",
Health: "h1",
},
},
"no tmp only status": {
status: &v1alpha2.Status{
CustomStatus: "s1",
HealthPolicy: "h1",
},
exp: &Template{
CustomStatus: "s1",
Health: "h1",
},
},
}
for reason, casei := range testCases {
gtmp, err := NewTemplate(casei.tmp, casei.status, casei.ext)
assert.NoError(t, err, reason)
assert.Equal(t, gtmp, casei.exp, reason)
}
}
+2 -2
View File
@@ -175,14 +175,14 @@ func ParseAndSyncCapability(data []byte, syncDir string) (types.Capability, erro
if err != nil {
return types.Capability{}, err
}
return HandleDefinition(rd.Name, syncDir, rd.Spec.Reference.Name, rd.Annotations, rd.Spec.Extension, types.TypeWorkload, nil)
return HandleDefinition(rd.Name, syncDir, rd.Spec.Reference.Name, rd.Annotations, rd.Spec.Extension, types.TypeWorkload, nil, rd.Spec.Template)
case "TraitDefinition":
var td v1alpha2.TraitDefinition
err = yaml.Unmarshal(data, &td)
if err != nil {
return types.Capability{}, err
}
return HandleDefinition(td.Name, syncDir, td.Spec.Reference.Name, td.Annotations, td.Spec.Extension, types.TypeTrait, td.Spec.AppliesToWorkloads)
return HandleDefinition(td.Name, syncDir, td.Spec.Reference.Name, td.Annotations, td.Spec.Extension, types.TypeTrait, td.Spec.AppliesToWorkloads, td.Spec.Template)
case "ScopeDefinition":
// TODO(wonderflow): support scope definition here.
}
+18 -18
View File
@@ -61,7 +61,7 @@ func GetWorkloadsFromCluster(ctx context.Context, namespace string, c types.Args
var templateErrors []error
for _, wd := range workloadDefs.Items {
tmp, err := HandleDefinition(wd.Name, syncDir, wd.Spec.Reference.Name, wd.Annotations, wd.Spec.Extension, types.TypeWorkload, nil)
tmp, err := HandleDefinition(wd.Name, syncDir, wd.Spec.Reference.Name, wd.Annotations, wd.Spec.Extension, types.TypeWorkload, nil, wd.Spec.Template)
if err != nil {
templateErrors = append(templateErrors, errors.Wrapf(err, "handle workload template `%s` failed", wd.Name))
continue
@@ -93,7 +93,7 @@ func GetTraitsFromCluster(ctx context.Context, namespace string, c types.Args, s
var templateErrors []error
for _, td := range traitDefs.Items {
tmp, err := HandleDefinition(td.Name, syncDir, td.Spec.Reference.Name, td.Annotations, td.Spec.Extension, types.TypeTrait, td.Spec.AppliesToWorkloads)
tmp, err := HandleDefinition(td.Name, syncDir, td.Spec.Reference.Name, td.Annotations, td.Spec.Extension, types.TypeTrait, td.Spec.AppliesToWorkloads, td.Spec.Template)
if err != nil {
templateErrors = append(templateErrors, errors.Wrapf(err, "handle trait template `%s` failed", td.Name))
continue
@@ -134,9 +134,9 @@ func validateCapabilities(tmp types.Capability, dm discoverymapper.DiscoveryMapp
}
// HandleDefinition will handle definition to capability
func HandleDefinition(name, syncDir, crdName string, annotation map[string]string, extension *runtime.RawExtension, tp types.CapType, applyTo []string) (types.Capability, error) {
func HandleDefinition(name, syncDir, crdName string, annotation map[string]string, extension *runtime.RawExtension, tp types.CapType, applyTo []string, template string) (types.Capability, error) {
var tmp types.Capability
tmp, err := HandleTemplate(extension, name, syncDir)
tmp, err := HandleTemplate(extension, template, name, syncDir)
if err != nil {
return types.Capability{}, err
}
@@ -162,31 +162,31 @@ func GetDescription(annotation map[string]string) string {
}
// HandleTemplate will handle definition template to capability
func HandleTemplate(in *runtime.RawExtension, name, syncDir string) (types.Capability, error) {
tmp, err := types.ConvertTemplateJSON2Object(in)
func HandleTemplate(in *runtime.RawExtension, specTemplate, name, syncDir string) (types.Capability, error) {
tmp, err := util.ConvertTemplateJSON2Object(in, specTemplate)
if err != nil {
return types.Capability{}, err
}
tmp.Name = name
var cueTemplate string
// if spec.template is not empty it should has the highest priority
if specTemplate != "" {
tmp.CueTemplate = specTemplate
tmp.CueTemplateURI = ""
}
if tmp.CueTemplateURI != "" {
b, err := common.HTTPGet(context.Background(), tmp.CueTemplateURI)
if err != nil {
return types.Capability{}, err
}
cueTemplate = string(b)
tmp.CueTemplate = cueTemplate
} else {
if tmp.CueTemplate == "" {
return types.Capability{}, errors.New("template not exist in definition")
}
cueTemplate = tmp.CueTemplate
tmp.CueTemplate = string(b)
}
if tmp.CueTemplate == "" {
return types.Capability{}, errors.New("template not exist in definition")
}
_, _ = system.CreateIfNotExist(syncDir)
filePath := filepath.Join(syncDir, name+".cue")
//nolint:gosec
err = ioutil.WriteFile(filePath, []byte(cueTemplate), 0644)
err = ioutil.WriteFile(filePath, []byte(tmp.CueTemplate), 0644)
if err != nil {
return types.Capability{}, err
}
@@ -245,7 +245,7 @@ func SyncDefinitionToLocal(ctx context.Context, c types.Args, localDefinitionDir
}
if foundCapability {
template, err := HandleDefinition(capabilityName, localDefinitionDir, workloadDef.Spec.Reference.Name,
workloadDef.Annotations, workloadDef.Spec.Extension, types.TypeWorkload, nil)
workloadDef.Annotations, workloadDef.Spec.Extension, types.TypeWorkload, nil, workloadDef.Spec.Template)
if err == nil {
return &template, nil
}
@@ -259,7 +259,7 @@ func SyncDefinitionToLocal(ctx context.Context, c types.Args, localDefinitionDir
}
if foundCapability {
template, err := HandleDefinition(capabilityName, localDefinitionDir, traitDef.Spec.Reference.Name,
traitDef.Annotations, traitDef.Spec.Extension, types.TypeTrait, nil)
traitDef.Annotations, traitDef.Spec.Extension, types.TypeTrait, nil, workloadDef.Spec.Template)
if err == nil {
return &template, nil
}
@@ -2,10 +2,11 @@ package traitdefinition
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/oam-dev/kubevela/pkg/oam/util"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
"k8s.io/klog"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -123,17 +124,11 @@ func ValidateDefinitionReference(_ context.Context, td v1alpha2.TraitDefinition)
if len(td.Spec.Reference.Name) > 0 {
return nil
}
if td.Spec.Extension == nil || len(td.Spec.Extension.Raw) < 1 {
return errors.New(failInfoDefRefOmitted)
}
tmp := map[string]interface{}{}
if err := json.Unmarshal(td.Spec.Extension.Raw, &tmp); err != nil {
tmp, err := util.NewTemplate(td.Spec.Template, td.Spec.Status, td.Spec.Extension)
if err != nil {
return errors.Wrap(err, errValidateDefRef)
}
template, ok := tmp["template"]
if !ok || len(fmt.Sprint(template)) < 1 {
if len(tmp.TemplateStr) == 0 {
return errors.New(failInfoDefRefOmitted)
}
return nil