diff --git a/apis/core.oam.dev/v1alpha2/core_types.go b/apis/core.oam.dev/v1alpha2/core_types.go index 49118cf631d..48433a6000f 100644 --- a/apis/core.oam.dev/v1alpha2/core_types.go +++ b/apis/core.oam.dev/v1alpha2/core_types.go @@ -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"` diff --git a/apis/types/capability.go b/apis/types/capability.go index fa89e52a28c..3056535c4db 100644 --- a/apis/types/capability.go +++ b/apis/types/capability.go @@ -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 diff --git a/charts/vela-core/crds/core.oam.dev_traitdefinitions.yaml b/charts/vela-core/crds/core.oam.dev_traitdefinitions.yaml index 8a60ff7ede3..ac0ff3ac289 100644 --- a/charts/vela-core/crds/core.oam.dev_traitdefinitions.yaml +++ b/charts/vela-core/crds/core.oam.dev_traitdefinitions.yaml @@ -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 diff --git a/charts/vela-core/crds/core.oam.dev_workloaddefinitions.yaml b/charts/vela-core/crds/core.oam.dev_workloaddefinitions.yaml index 4fefb019e2e..15afb139b3f 100644 --- a/charts/vela-core/crds/core.oam.dev_workloaddefinitions.yaml +++ b/charts/vela-core/crds/core.oam.dev_workloaddefinitions.yaml @@ -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 diff --git a/charts/vela-core/templates/defwithtemplate/ingress.yaml b/charts/vela-core/templates/defwithtemplate/ingress.yaml index 89cfa60f98a..540b88f2d19 100644 --- a/charts/vela-core/templates/defwithtemplate/ingress.yaml +++ b/charts/vela-core/templates/defwithtemplate/ingress.yaml @@ -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 + } + }, + ] + } + }] + } + } + diff --git a/charts/vela-core/templates/defwithtemplate/manualscale.yaml b/charts/vela-core/templates/defwithtemplate/manualscale.yaml index 2d8c16a4446..3fe214c370d 100644 --- a/charts/vela-core/templates/defwithtemplate/manualscale.yaml +++ b/charts/vela-core/templates/defwithtemplate/manualscale.yaml @@ -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 + } + diff --git a/charts/vela-core/templates/defwithtemplate/task.yaml b/charts/vela-core/templates/defwithtemplate/task.yaml index 02b32fd36a3..98f93755979 100644 --- a/charts/vela-core/templates/defwithtemplate/task.yaml +++ b/charts/vela-core/templates/defwithtemplate/task.yaml @@ -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] + } + diff --git a/charts/vela-core/templates/defwithtemplate/webservice.yaml b/charts/vela-core/templates/defwithtemplate/webservice.yaml index 6830fb7cd71..f808db89adf 100644 --- a/charts/vela-core/templates/defwithtemplate/webservice.yaml +++ b/charts/vela-core/templates/defwithtemplate/webservice.yaml @@ -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 + } + diff --git a/charts/vela-core/templates/defwithtemplate/worker.yaml b/charts/vela-core/templates/defwithtemplate/worker.yaml index 77e3f6e0345..9ddeecb6231 100644 --- a/charts/vela-core/templates/defwithtemplate/worker.yaml +++ b/charts/vela-core/templates/defwithtemplate/worker.yaml @@ -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] + } + diff --git a/config/samples/app-with-status/template.yaml b/config/samples/app-with-status/template.yaml index 621ad3f3d25..c32773dcb84 100644 --- a/config/samples/app-with-status/template.yaml +++ b/config/samples/app-with-status/template.yaml @@ -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 - } - } - ] - } - }] - } - } \ No newline at end of file + 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 + } + }, + ] + } + }] + } + } diff --git a/hack/vela-templates/definitions/ingress.yaml b/hack/vela-templates/definitions/ingress.yaml index a458aa1696c..fd0bb9959e6 100644 --- a/hack/vela-templates/definitions/ingress.yaml +++ b/hack/vela-templates/definitions/ingress.yaml @@ -19,5 +19,4 @@ spec: appliesToWorkloads: - webservice - worker - extension: - template: | + template: | diff --git a/hack/vela-templates/definitions/manualscale.yaml b/hack/vela-templates/definitions/manualscale.yaml index ce5b7cc38c6..246291d4429 100644 --- a/hack/vela-templates/definitions/manualscale.yaml +++ b/hack/vela-templates/definitions/manualscale.yaml @@ -11,5 +11,4 @@ spec: definitionRef: name: manualscalertraits.core.oam.dev workloadRefPath: spec.workloadRef - extension: - template: |- + template: | diff --git a/hack/vela-templates/definitions/task.yaml b/hack/vela-templates/definitions/task.yaml index 0c45ef6573f..525794a318d 100644 --- a/hack/vela-templates/definitions/task.yaml +++ b/hack/vela-templates/definitions/task.yaml @@ -7,5 +7,4 @@ metadata: spec: definitionRef: name: jobs.batch - extension: - template: | + template: | diff --git a/hack/vela-templates/definitions/webservice.yaml b/hack/vela-templates/definitions/webservice.yaml index 6bc007ebc6f..ca03df0dc4e 100644 --- a/hack/vela-templates/definitions/webservice.yaml +++ b/hack/vela-templates/definitions/webservice.yaml @@ -8,5 +8,4 @@ metadata: spec: definitionRef: name: deployments.apps - extension: - template: | + template: | diff --git a/hack/vela-templates/definitions/worker.yaml b/hack/vela-templates/definitions/worker.yaml index 18e818b7968..9aea430c555 100644 --- a/hack/vela-templates/definitions/worker.yaml +++ b/hack/vela-templates/definitions/worker.yaml @@ -7,5 +7,4 @@ metadata: spec: definitionRef: name: deployments.apps - extension: - template: | + template: | diff --git a/hack/vela-templates/gen_definitions.sh b/hack/vela-templates/gen_definitions.sh index 2e5c55d2314..1ee5ece1e63 100755 --- a/hack/vela-templates/gen_definitions.sh +++ b/hack/vela-templates/gen_definitions.sh @@ -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%.*}" diff --git a/legacy/charts/vela-core-legacy/crds/core.oam.dev_traitdefinitions.yaml b/legacy/charts/vela-core-legacy/crds/core.oam.dev_traitdefinitions.yaml index 5581c7ac76b..a3093d35bfe 100644 --- a/legacy/charts/vela-core-legacy/crds/core.oam.dev_traitdefinitions.yaml +++ b/legacy/charts/vela-core-legacy/crds/core.oam.dev_traitdefinitions.yaml @@ -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 diff --git a/legacy/charts/vela-core-legacy/crds/core.oam.dev_workloaddefinitions.yaml b/legacy/charts/vela-core-legacy/crds/core.oam.dev_workloaddefinitions.yaml index 27e0866a07f..28ecc1a4e63 100644 --- a/legacy/charts/vela-core-legacy/crds/core.oam.dev_workloaddefinitions.yaml +++ b/legacy/charts/vela-core-legacy/crds/core.oam.dev_workloaddefinitions.yaml @@ -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 diff --git a/pkg/appfile/parser.go b/pkg/appfile/parser.go index a9278b8e0e8..c872dc97f1a 100644 --- a/pkg/appfile/parser.go +++ b/pkg/appfile/parser.go @@ -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 } diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/application_controller_test.go b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller_test.go index abe022e3c95..02a9fad4280 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/application_controller_test.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller_test.go @@ -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: { - 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 - } + 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 + 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 + } ` 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 { diff --git a/pkg/oam/util/template.go b/pkg/oam/util/template.go index 05a080e2373..6d16e7e0027 100644 --- a/pkg/oam/util/template.go +++ b/pkg/oam/util/template.go @@ -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 +} diff --git a/pkg/oam/util/template_test.go b/pkg/oam/util/template_test.go index 63ee0a81811..a667f2313c8 100644 --- a/pkg/oam/util/template_test.go +++ b/pkg/oam/util/template_test.go @@ -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) + } +} diff --git a/pkg/plugins/capcenter.go b/pkg/plugins/capcenter.go index f4b73925015..96b5f67bd25 100644 --- a/pkg/plugins/capcenter.go +++ b/pkg/plugins/capcenter.go @@ -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. } diff --git a/pkg/plugins/cluster.go b/pkg/plugins/cluster.go index 7bed52719b9..52e09e5eeee 100644 --- a/pkg/plugins/cluster.go +++ b/pkg/plugins/cluster.go @@ -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 } diff --git a/pkg/webhook/core.oam.dev/v1alpha2/traitdefinition/validating_handler.go b/pkg/webhook/core.oam.dev/v1alpha2/traitdefinition/validating_handler.go index eb5df83120e..8469d1ccec9 100644 --- a/pkg/webhook/core.oam.dev/v1alpha2/traitdefinition/validating_handler.go +++ b/pkg/webhook/core.oam.dev/v1alpha2/traitdefinition/validating_handler.go @@ -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