From e882a650ae58784121ddc5a0132d211c7e8fba84 Mon Sep 17 00:00:00 2001 From: "Jian.Li" <74582607+leejanee@users.noreply.github.com> Date: Mon, 14 Dec 2020 12:00:06 +0800 Subject: [PATCH] Extend application's capabilities by trait definition (#742) * add dsl pkg * realize context capabilities for application * fix golint * fix check-diff * upgrade vela server sample * comment group * Definition Reference be Optional & fix three-part trait crd apply without namespace * Improve samples --- apis/core.oam.dev/v1alpha2/core_types.go | 2 +- .../crds/core.oam.dev_traitdefinitions.yaml | 2 - config/samples/vela-server/Demo.md | 144 ++++++ config/samples/vela-server/README.md | 436 +++++++++++++----- .../vela-server/application-sample.yaml | 8 +- config/samples/vela-server/template.yaml | 75 ++- .../crds/core.oam.dev_traitdefinitions.yaml | 2 - .../application/application_controller.go | 3 + .../v1alpha2/application/builder/build.go | 74 ++- .../v1alpha2/application/parser/service.go | 12 + .../applicationconfiguration/render.go | 1 + pkg/dsl/definition/template.go | 162 +++++++ pkg/dsl/definition/template_test.go | 124 +++++ pkg/dsl/model/instance.go | 195 ++++++++ pkg/dsl/model/instance_test.go | 63 +++ pkg/dsl/process/handle.go | 89 ++++ pkg/dsl/process/handle_test.go | 43 ++ 17 files changed, 1293 insertions(+), 142 deletions(-) create mode 100644 config/samples/vela-server/Demo.md create mode 100644 pkg/dsl/definition/template.go create mode 100644 pkg/dsl/definition/template_test.go create mode 100644 pkg/dsl/model/instance.go create mode 100644 pkg/dsl/model/instance_test.go create mode 100644 pkg/dsl/process/handle.go create mode 100644 pkg/dsl/process/handle_test.go diff --git a/apis/core.oam.dev/v1alpha2/core_types.go b/apis/core.oam.dev/v1alpha2/core_types.go index 00c6aebe9..e97708fb3 100644 --- a/apis/core.oam.dev/v1alpha2/core_types.go +++ b/apis/core.oam.dev/v1alpha2/core_types.go @@ -97,7 +97,7 @@ type WorkloadDefinitionList struct { // A TraitDefinitionSpec defines the desired state of a TraitDefinition. type TraitDefinitionSpec struct { // Reference to the CustomResourceDefinition that defines this trait kind. - Reference DefinitionReference `json:"definitionRef"` + Reference DefinitionReference `json:"definitionRef,omitempty"` // Revision indicates whether a trait is aware of component revision // +optional diff --git a/charts/vela-core/crds/core.oam.dev_traitdefinitions.yaml b/charts/vela-core/crds/core.oam.dev_traitdefinitions.yaml index a88b261e5..e9da8f49c 100644 --- a/charts/vela-core/crds/core.oam.dev_traitdefinitions.yaml +++ b/charts/vela-core/crds/core.oam.dev_traitdefinitions.yaml @@ -66,8 +66,6 @@ spec: workloadRefPath: description: WorkloadRefPath indicates where/if a trait accepts a workloadRef object type: string - required: - - definitionRef type: object type: object served: true diff --git a/config/samples/vela-server/Demo.md b/config/samples/vela-server/Demo.md new file mode 100644 index 000000000..16b1562c5 --- /dev/null +++ b/config/samples/vela-server/Demo.md @@ -0,0 +1,144 @@ +# Application Example + +In this Demo, Application application-sample will be converted to appconfig and component + +The fields in the application spec come from the parametes defined in the definition template +, so we must install Definition at first + +Step 1: Install Workload Definition & Trait Definition +``` +kubectl apply -f template.yaml +``` +Step 2: Create a sample application in the cluster +``` +kubectl apply -f application-sample.yaml +``` +Step 3: View the application status +``` +kubectl get -f application-sample.yaml -oyaml + +// You can see the following +apiVersion: core.oam.dev/v1alpha2 +kind: Application +metadata: + annotations: + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"core.oam.dev/v1alpha2","kind":"Application","metadata":{"annotations":{},"name":"application-sample","namespace":"oam-test"},"spec":{"template":"services:\n myweb:\n type: worker\n image: \"busybox\"\n cmd:\n - sleep\n - \"1000\"\n scaler:\n replicas: 10"}} + name: application-sample + namespace: oam-test +spec: + services: + myweb: + cmd: + - sleep + - "1000" + image: busybox + scaler: + replicas: 10 + service: + http: + server: 80 + sidecar: + command: + - sleep + - "1000" + image: busybox + name: test-sidecar + type: worker +status: + conditions: + - lastTransitionTime: "2020-12-02T12:12:52Z" + reason: Available + status: "True" + type: Parsed + - lastTransitionTime: "2020-12-02T12:12:52Z" + reason: Available + status: "True" + type: Built + - lastTransitionTime: "2020-12-02T12:12:52Z" + reason: Available + status: "True" + type: Applied + status: running + +``` + +Step 4: View the oam CR generated by application + +``` +kubectl get appconfig/application-sample -oyaml + +// appconfig is as follows +apiVersion: core.oam.dev/v1alpha2 +kind: ApplicationConfiguration +metadata: + labels: + application.oam.dev: application-sample + name: application-sample + namespace: oam-test + ownerReferences: + - apiVersion: core.oam.dev/v1alpha2 + controller: true + kind: Application + name: application-sample + uid: dca7acc3-664c-422b-aa52-4fe012e37974 +spec: + components: + - componentName: myweb + traits: + - trait: + apiVersion: v1 + kind: Service + metadata: + name: myweb + spec: + ports: + - port: 80 + targetPort: 80 + selector: + app: myweb + +kubectl get component/myweb -oyaml + +// component is as follows +apiVersion: core.oam.dev/v1alpha2 +kind: Component +metadata: + labels: + application.oam.dev: application-sample + name: myweb + namespace: oam-test + ownerReferences: + - apiVersion: core.oam.dev/v1alpha2 + controller: true + kind: Application + name: application-sample + uid: dca7acc3-664c-422b-aa52-4fe012e37974 +spec: + workload: + apiVersion: apps/v1 + kind: Deployment + spec: + replicas: 10 + selector: + matchLabels: + app.oam.dev/component: myweb + template: + metadata: + labels: + app: myweb + app.oam.dev/component: myweb + spec: + containers: + - command: + - sleep + - "1000" + image: busybox + name: myweb + - command: + - sleep + - "1000" + image: busybox + name: test-sidecar +``` + diff --git a/config/samples/vela-server/README.md b/config/samples/vela-server/README.md index c219be4eb..472fc0bf6 100644 --- a/config/samples/vela-server/README.md +++ b/config/samples/vela-server/README.md @@ -1,146 +1,332 @@ -# Vela Server -## example +# Definition Docs -In this Demo, Application application-sample will be converted to appconfig and component +## Reserved word +### patch +Perform the CUE AND operation with the content declared by 'patch' and workload cr -The fields in the application spec come from the parametes defined in the definition template -, so we must install Definition at first +### output +Generate a new cr, which is generally associated with workload cr -Step 1: Install Workload Definition & Trait Definition +## Workload Definition +The following workload definition is to generate a deployment ``` -kubectl apply -f template.yaml -``` -Step 2: Create a sample application in the cluster -``` -kubectl apply -f application-sample.yaml -``` -Step 3: View the application status -``` -kubectl get -f application-sample.yaml -oyaml +apiVersion: core.oam.dev/v1alpha2 +kind: WorkloadDefinition +metadata: + name: worker + annotations: + definition.oam.dev/description: "Long-running scalable backend worker without network endpoint" +spec: + definitionRef: + name: deployments.apps + extension: + template: | + output: { + apiVersion: "apps/v1" + kind: "Deployment" + spec: { + selector: matchLabels: { + "app.oam.dev/component": context.name + } -// You can see the following + 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 + + cmd?: [...string] + } +``` + +If defined an application as follows +``` apiVersion: core.oam.dev/v1alpha2 kind: Application metadata: - annotations: - kubectl.kubernetes.io/last-applied-configuration: | - {"apiVersion":"core.oam.dev/v1alpha2","kind":"Application","metadata":{"annotations":{},"name":"application-sample","namespace":"oam-test"},"spec":{"template":"services:\n myweb:\n type: worker\n image: \"busybox\"\n cmd:\n - sleep\n - \"1000\"\n scaler:\n replicas: 10"}} name: application-sample - namespace: oam-test spec: services: myweb: + type: worker + image: "busybox" cmd: - sleep - "1000" - image: busybox - scaler: - replicas: 10 - type: worker -status: - conditions: - - lastTransitionTime: "2020-12-02T12:12:52Z" - reason: Available - status: "True" - type: Parsed - - lastTransitionTime: "2020-12-02T12:12:52Z" - reason: Available - status: "True" - type: Built - - lastTransitionTime: "2020-12-02T12:12:52Z" - reason: Available - status: "True" - type: Applied - status: running - ``` - -Step 4: View the oam CR generated by application - +we will get a deployment ``` -kubectl get appconfig/application-sample -oyaml - -// appconfig is as follows -apiVersion: core.oam.dev/v1alpha2 -kind: ApplicationConfiguration -metadata: - labels: - application.oam.dev: application-sample - name: application-sample - namespace: oam-test - ownerReferences: - - apiVersion: core.oam.dev/v1alpha2 - controller: true - kind: Application - name: application-sample - uid: dca7acc3-664c-422b-aa52-4fe012e37974 +apiVersion: apps/v1 +kind: Deployment spec: - components: - - componentName: myweb - traits: - - trait: - apiVersion: core.oam.dev/v1alpha2 - kind: ManualScalerTrait - spec: - replicaCount: 10 -status: - conditions: - - lastTransitionTime: "2020-12-02T12:12:52Z" - reason: Successfully reconciled resource - status: "True" - type: Synced - dependency: {} - workloads: - - componentName: myweb - componentRevisionName: myweb-v1 - traits: - - traitRef: - apiVersion: core.oam.dev/v1alpha2 - kind: ManualScalerTrait - name: myweb-trait-78fdd467d6 - workloadRef: - apiVersion: apps/v1 - kind: Deployment - name: myweb - -kubectl get component/myweb -oyaml - -// component is as follows -apiVersion: core.oam.dev/v1alpha2 -kind: Component -metadata: - labels: - application.oam.dev: application-sample - name: myweb - namespace: oam-test - ownerReferences: - - apiVersion: core.oam.dev/v1alpha2 - controller: true - kind: Application - name: application-sample - uid: dca7acc3-664c-422b-aa52-4fe012e37974 -spec: - workload: - apiVersion: apps/v1 - kind: Deployment + selector: + matchLabels: + app.oam.dev/component: myweb + template: + metadata: + labels: + app.oam.dev/component: myweb spec: - selector: - matchLabels: - app.oam.dev/component: myweb - template: - metadata: - labels: - app.oam.dev/component: myweb - spec: - containers: - - command: - - sleep - - "1000" - image: busybox - name: myweb -status: - latestRevision: - name: myweb-v1 - revision: 1 + containers: + - command: + - sleep + - "1000" + image: busybox + name: myweb +``` +## Service Trait Definition + +Define a trait Definition that appends service to workload(worker) , as shown below +``` +apiVersion: core.oam.dev/v1alpha2 +kind: TraitDefinition +metadata: + annotations: + definition.oam.dev/description: "service the app" + name: service +spec: + appliesToWorkloads: + - webservice + - worker + definitionRef: + name: service.v1 + extension: + template: |- + patch: {spec: template: metadata: labels: app: context.name} + output: { + 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 + } ``` +If add service capability to the application, as follows +``` +apiVersion: core.oam.dev/v1alpha2 +kind: Application +metadata: + name: application-sample +spec: + services: + myweb: + type: worker + image: "busybox" + cmd: + - sleep + - "1000" + service: + http: + server: 80 +``` + +we will get a new deployment and service +``` +// origin deployment template add labels +apiVersion: apps/v1 +kind: Deployment +spec: + selector: + matchLabels: + app.oam.dev/component: myweb + template: + metadata: + labels: + // add label app + app: myweb + app.oam.dev/component: myweb + spec: + containers: + - command: + - sleep + - "1000" + image: busybox + name: myweb +--- +apiVersion: v1 +kind: Service +metadata: + name: myweb +spec: + ports: + - port: 80 + targetPort: 80 + selector: + app: myweb +``` + +## Scaler Trait Definition + +Define a trait Definition that scale workload(worker) replicas +``` +apiVersion: core.oam.dev/v1alpha2 +kind: TraitDefinition +metadata: + annotations: + definition.oam.dev/description: "Manually scale the app" + name: scaler +spec: + appliesToWorkloads: + - webservice + - worke + extension: + template: |- + patch: { + spec: replicas: parameter.replicas + } + parameter: { + //+short=r + replicas: *1 | int + } +``` +If add scaler capability to the application, as follows +``` +apiVersion: core.oam.dev/v1alpha2 +kind: Application +metadata: + name: application-sample +spec: + services: + myweb: + type: worker + image: "busybox" + cmd: + - sleep + - "1000" + service: + http: + server: 80 + scaler: + replicas: 10 +``` + +The deployment replicas will be scale to 10 +``` +apiVersion: apps/v1 +kind: Deployment +spec: + selector: + matchLabels: + app.oam.dev/component: myweb + // scale to 10 + replicas: 10 + template: + metadata: + labels: + // add label app + app: myweb + app.oam.dev/component: myweb + spec: + containers: + - command: + - sleep + - "1000" + image: busybox + name: myweb +``` + +## Sidecar Trait Definition + +Define a trait Definition that append containers to workload(worker) +``` +apiVersion: core.oam.dev/v1alpha2 +kind: TraitDefinition +metadata: + annotations: + definition.oam.dev/description: "add sidecar to the app" + name: sidecar +spec: + appliesToWorkloads: + - webservice + - worke + extension: + template: |- + _containers: context.input.spec.template.spec.containers+[parameter] + patch: { + spec: template: spec: containers: _containers + } + parameter: { + name: string + image: string + command?: [...string] + } +``` + +If add sidercar capability to the application, as follows +``` +apiVersion: core.oam.dev/v1alpha2 +kind: Application +metadata: + name: application-sample +spec: + services: + myweb: + type: worker + image: "busybox" + cmd: + - sleep + - "1000" + service: + http: + server: 80 + scaler: + replicas: 10 + sidercar: + name: "sidecar-test" + image: "nginx" +``` +The deployment updated as follows +``` +apiVersion: apps/v1 +kind: Deployment +spec: + selector: + matchLabels: + app.oam.dev/component: myweb + // scale to 10 + replicas: 10 + template: + metadata: + labels: + // add label app + app: myweb + app.oam.dev/component: myweb + spec: + containers: + - command: + - sleep + - "1000" + image: busybox + name: myweb + - name: sidecar-test + image: nginx +``` \ No newline at end of file diff --git a/config/samples/vela-server/application-sample.yaml b/config/samples/vela-server/application-sample.yaml index 3752f1540..f0f10ddda 100644 --- a/config/samples/vela-server/application-sample.yaml +++ b/config/samples/vela-server/application-sample.yaml @@ -11,4 +11,10 @@ spec: - sleep - "1000" scaler: - replicas: 10 \ No newline at end of file + replicas: 10 + sidercar: + name: "sidecar-test" + image: "nginx" + service: + http: + server: 80 \ No newline at end of file diff --git a/config/samples/vela-server/template.yaml b/config/samples/vela-server/template.yaml index f82fbcf49..43e827285 100644 --- a/config/samples/vela-server/template.yaml +++ b/config/samples/vela-server/template.yaml @@ -58,19 +58,76 @@ spec: appliesToWorkloads: - webservice - worker - definitionRef: - name: manualscalertraits.core.oam.dev - workloadRefPath: spec.workloadRef extension: template: |- - output: { - apiVersion: "core.oam.dev/v1alpha2" - kind: "ManualScalerTrait" - spec: { - replicaCount: parameter.replicas - } + patch: { + spec: replicas: parameter.replicas } parameter: { //+short=r replicas: *1 | int } +--- +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: |- + _containers: context.input.spec.template.spec.containers+[parameter] + patch: { + spec: template: spec: containers: _containers + } + parameter: { + name: string + image: string + command?: [...string] + } +--- +apiVersion: core.oam.dev/v1alpha2 +kind: TraitDefinition +metadata: + annotations: + definition.oam.dev/description: "service the app" + name: service +spec: + appliesToWorkloads: + - webservice + - worker + definitionRef: + name: services + extension: + template: |- + patch: {spec: template: metadata: labels: app: context.name} + output: { + 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 + } +--- +apiVersion: core.oam.dev/v1alpha2 +kind: TraitDefinition +metadata: + name: services + namespace: default +spec: + definitionRef: + name: services 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 d58604294..dcfe0dd5f 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 @@ -65,8 +65,6 @@ spec: workloadRefPath: description: WorkloadRefPath indicates where/if a trait accepts a workloadRef object type: string - required: - - definitionRef type: object type: object version: v1alpha2 diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go index 1ae46127d..641bda5ae 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go @@ -19,6 +19,7 @@ package application import ( "context" + "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1" "github.com/crossplane/crossplane-runtime/pkg/logging" "github.com/go-logr/logr" kerrors "k8s.io/apimachinery/pkg/api/errors" @@ -69,6 +70,8 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (result ctrl.Result, gerr error app.Status.Phase = v1alpha2.ApplicationRendering handler := &reter{r, app, applog} + app.Status.Conditions = []v1alpha1.Condition{} + applog.Info("parse template") // parse template appParser := parser.NewParser(template.GetHanler(fclient.NewDefinitionClient(r.Client))) diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/builder/build.go b/pkg/controller/core.oam.dev/v1alpha2/application/builder/build.go index ee8a64fc7..877bbc61c 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/builder/build.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/builder/build.go @@ -9,9 +9,11 @@ import ( "cuelang.org/go/cue/build" cueparser "cuelang.org/go/cue/parser" "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/runtime" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2" "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/application/parser" + "github.com/oam-dev/kubevela/pkg/dsl/process" ) type builder struct { @@ -26,7 +28,7 @@ const ( // Build template to applicationConfig & Component func Build(ns string, app *parser.Appfile) (*v1alpha2.ApplicationConfiguration, []*v1alpha2.Component, error) { b := &builder{app} - return b.Complete(ns) + return b.CompleteWithContext(ns) } // Complete: builder complete rendering @@ -44,7 +46,6 @@ func (b *builder) Complete(ns string) (*v1alpha2.ApplicationConfiguration, []*v1 componets := []*v1alpha2.Component{} for _, wl := range b.app.Services() { - compCtx := map[string]string{"name": wl.Name()} component, err := wl.Eval(newLoader(compCtx)) @@ -78,6 +79,75 @@ func (b *builder) Complete(ns string) (*v1alpha2.ApplicationConfiguration, []*v1 return appconfig, componets, nil } +func (b *builder) CompleteWithContext(ns string) (*v1alpha2.ApplicationConfiguration, []*v1alpha2.Component, error) { + appconfig := &v1alpha2.ApplicationConfiguration{} + appconfig.SetGroupVersionKind(v1alpha2.ApplicationConfigurationGroupVersionKind) + appconfig.Name = b.app.Name() + appconfig.Namespace = ns + appconfig.Spec.Components = []v1alpha2.ApplicationConfigurationComponent{} + + if appconfig.Labels == nil { + appconfig.Labels = map[string]string{} + } + appconfig.Labels[OamApplicationLabel] = b.app.Name() + + componets := []*v1alpha2.Component{} + for _, wl := range b.app.Services() { + pCtx := process.NewContext(wl.Name()) + if err := wl.EvalContext(pCtx); err != nil { + return nil, nil, err + } + for _, tr := range wl.Traits() { + if err := tr.EvalContext(pCtx); err != nil { + return nil, nil, err + } + } + comp, acComp, err := generateOAM(pCtx) + if err != nil { + return nil, nil, err + } + comp.Name = wl.Name() + acComp.ComponentName = comp.Name + + comp.Namespace = ns + if comp.Labels == nil { + comp.Labels = map[string]string{} + } + comp.Labels[OamApplicationLabel] = b.app.Name() + comp.SetGroupVersionKind(v1alpha2.ComponentGroupVersionKind) + + componets = append(componets, comp) + appconfig.Spec.Components = append(appconfig.Spec.Components, *acComp) + } + + return appconfig, componets, nil +} + +func generateOAM(pCtx process.Context) (*v1alpha2.Component, *v1alpha2.ApplicationConfigurationComponent, error) { + base, assists := pCtx.Output() + componetWorkload, err := base.Object(nil) + if err != nil { + return nil, nil, err + } + component := &v1alpha2.Component{} + component.Spec.Workload.Object = componetWorkload + + acComponent := &v1alpha2.ApplicationConfigurationComponent{} + acComponent.Traits = []v1alpha2.ComponentTrait{} + for _, assist := range assists { + traitRef, err := assist.Object(nil) + if err != nil { + return nil, nil, err + } + acComponent.Traits = append(acComponent.Traits, v1alpha2.ComponentTrait{ + Trait: runtime.RawExtension{ + Object: traitRef, + }, + }) + } + return component, acComponent, nil +} + type loader struct { files map[string]*ast.File err error diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/parser/service.go b/pkg/controller/core.oam.dev/v1alpha2/application/parser/service.go index c4666133b..5aa18762f 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/parser/service.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/parser/service.go @@ -11,6 +11,8 @@ import ( "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2" "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/application/template" + "github.com/oam-dev/kubevela/pkg/dsl/definition" + "github.com/oam-dev/kubevela/pkg/dsl/process" ) // Render is cue render @@ -67,6 +69,11 @@ func (wl *Workload) Eval(render Render) (*v1alpha2.Component, error) { return component, nil } +// EvalContext eval workload template and set result to context +func (wl *Workload) EvalContext(ctx process.Context) error { + return definition.NewWDTemplater("-", wl.template).Params(wl.params).Complete(ctx) +} + // Trait is ComponentTrait type Trait struct { name string @@ -120,6 +127,11 @@ func (trait *Trait) Eval(render Render) ([]v1alpha2.ComponentTrait, error) { return compTraits, nil } +// EvalContext eval trait template and set result to context +func (trait *Trait) EvalContext(ctx process.Context) error { + return definition.NewTDTemplater("-", trait.template).Params(trait.params).Complete(ctx) +} + // Appfile describle application type Appfile struct { name string diff --git a/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration/render.go b/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration/render.go index e1bc407ba..e786bc465 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration/render.go +++ b/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration/render.go @@ -229,6 +229,7 @@ func (r *components) renderTrait(ctx context.Context, ct v1alpha2.ComponentTrait traitDef, err := util.FetchTraitDefinition(ctx, r.client, r.dm, t) if err != nil { if apierrors.IsNotFound(err) { + t.SetNamespace(ac.GetNamespace()) return t, util.GetDummyTraitDefinition(t), nil } return nil, nil, errors.Wrapf(err, errFmtGetTraitDefinition, t.GetAPIVersion(), t.GetKind(), t.GetName()) diff --git a/pkg/dsl/definition/template.go b/pkg/dsl/definition/template.go new file mode 100644 index 000000000..2d8a98cb8 --- /dev/null +++ b/pkg/dsl/definition/template.go @@ -0,0 +1,162 @@ +package definition + +import ( + "encoding/json" + "fmt" + + "cuelang.org/go/cue" + "cuelang.org/go/cue/build" + "github.com/pkg/errors" + + "github.com/oam-dev/kubevela/pkg/dsl/model" + "github.com/oam-dev/kubevela/pkg/dsl/process" +) + +// Template defines Definition's Render interface +type Template interface { + Params(params interface{}) Template + Complete(ctx process.Context) error +} + +type def struct { + name string + templ string + params interface{} +} + +type workloadDef struct { + def +} + +// NewWDTemplater create Workload Definition templater +func NewWDTemplater(name, templ string) Template { + return &workloadDef{ + def: def{ + name: name, + templ: templ, + params: nil, + }, + } +} + +// Params set definition's params +func (wd *workloadDef) Params(params interface{}) Template { + wd.params = params + return wd +} + +// Complete do workload definition's rendering +func (wd *workloadDef) Complete(ctx process.Context) error { + bi := build.NewContext().NewInstance("", nil) + if err := bi.AddFile("-", wd.templ); err != nil { + return err + } + if wd.params != nil { + bt, _ := json.Marshal(wd.params) + if err := bi.AddFile("parameter", fmt.Sprintf("parameter: %s", string(bt))); err != nil { + return err + } + } + + if err := bi.AddFile("-", ctx.Compile("context")); err != nil { + return err + } + insts := cue.Build([]*build.Instance{bi}) + for _, inst := range insts { + if err := inst.Value().Err(); err != nil { + return errors.WithMessagef(err, "workloadDef %s eval", wd.name) + } + output := inst.Lookup("output") + base, err := model.NewBase(output) + if err != nil { + return errors.WithMessagef(err, "workloadDef %s new base", wd.name) + } + ctx.SetBase(base) + } + return nil +} + +type traitDef struct { + def +} + +// NewTDTemplater create Trait Definition templater +func NewTDTemplater(name, templ string) Template { + return &traitDef{ + def: def{ + name: name, + templ: templ, + }, + } +} + +// Params set definition's params +func (td *traitDef) Params(params interface{}) Template { + td.params = params + return td +} + +// Complete do trait definition's rendering +func (td *traitDef) Complete(ctx process.Context) error { + bi := build.NewContext().NewInstance("", nil) + if err := bi.AddFile("-", td.templ); err != nil { + return err + } + if td.params != nil { + bt, _ := json.Marshal(td.params) + if err := bi.AddFile("parameter", fmt.Sprintf("parameter: %s", string(bt))); err != nil { + return err + } + } + + if err := bi.AddFile("f", ctx.Compile("context")); err != nil { + return err + } + insts := cue.Build([]*build.Instance{bi}) + for _, inst := range insts { + + if err := inst.Value().Err(); err != nil { + return errors.WithMessagef(err, "traitDef %s build", td.name) + } + + output := inst.Lookup("output") + if output.Exists() { + other, err := model.NewOther(output) + if err != nil { + return errors.WithMessagef(err, "traitDef %s new Assist", td.name) + } + ctx.PutAssistants(other) + } + + outputs := inst.Lookup("outputs") + st, err := outputs.Struct() + if err == nil { + for i := 0; i < st.Len(); i++ { + fieldInfo := st.Field(i) + if fieldInfo.IsDefinition || fieldInfo.IsHidden || fieldInfo.IsOptional { + continue + } + other, err := model.NewOther(fieldInfo.Value) + if err != nil { + return errors.WithMessagef(err, "traitDef %s new Assists(%s)", td.name, fieldInfo.Name) + } + ctx.PutAssistants(other) + } + + } + + patcher := inst.Lookup("patch") + if patcher.Exists() { + base, _ := ctx.Output() + p, err := model.NewOther(patcher) + if err != nil { + return errors.WithMessagef(err, "traitDef %s patcher NewOther", td.name) + } + if err := base.Unity(p); err != nil { + return err + } + } + + } + return nil +} diff --git a/pkg/dsl/definition/template_test.go b/pkg/dsl/definition/template_test.go new file mode 100644 index 000000000..44b03a4d0 --- /dev/null +++ b/pkg/dsl/definition/template_test.go @@ -0,0 +1,124 @@ +package definition + +import ( + "testing" + + "github.com/bmizerany/assert" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/oam-dev/kubevela/pkg/dsl/process" +) + +func TestWDTemplate(t *testing.T) { + + testCases := []struct { + templ string + params map[string]interface{} + expectObj runtime.Object + }{ + { + templ: ` +output:{ + apiVersion: "apps/v1" + kind: "Deployment" + metadata: name: context.name + spec: replicas: parameter.replicas +} + +parameter: { + replicas: *1 | int +} +`, + params: map[string]interface{}{ + "replicas": 2, + }, + expectObj: &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "apps/v1", "kind": "Deployment", "metadata": map[string]interface{}{"name": "test"}, "spec": map[string]interface{}{"replicas": int64(2)}}}, + }, + } + + for _, v := range testCases { + ctx := process.NewContext("test") + wt := NewWDTemplater("-", v.templ) + if err := wt.Params(v.params).Complete(ctx); err != nil { + t.Error(err) + return + } + base, assists := ctx.Output() + assert.Equal(t, 0, len(assists)) + assert.Equal(t, false, base == nil) + baseObj, err := base.Object(nil) + assert.Equal(t, nil, err) + assert.Equal(t, v.expectObj, baseObj) + + } + +} + +func TestTDTemplate(t *testing.T) { + baseTemplate := ` +output:{ + apiVersion: "apps/v1" + kind: "Deployment" + metadata: name: context.name + spec: { + replicas: parameter.replicas + template: spec: { + containers: [{image: "website:0.1",name:"main"}] + } + } +} + +parameter: { + replicas: *1 | int +} +` + ctx := process.NewContext("test") + wt := NewWDTemplater("-", baseTemplate) + if err := wt.Params(map[string]interface{}{ + "replicas": 2, + }).Complete(ctx); err != nil { + t.Error(err) + return + } + + tds := []struct { + templ string + params map[string]interface{} + }{ + { + templ: ` +patch: { + _containers: context.input.spec.template.spec.containers+[parameter] + sepc: template: spec: containers: _containers +} + +parameter: { + name: string + image: string + command?: [...string] +} +`, + params: map[string]interface{}{ + "name": "sidecar", + "image": "metrics-agent:0.2", + }, + }, + } + + for _, v := range tds { + td := NewTDTemplater("-", v.templ) + if err := td.Params(v.params).Complete(ctx); err != nil { + t.Error(err) + return + } + } + + base, assists := ctx.Output() + assert.Equal(t, 0, len(assists)) + assert.Equal(t, false, base == nil) + obj, err := base.Object(nil) + assert.Equal(t, nil, err) + expect := &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "apps/v1", "kind": "Deployment", "metadata": map[string]interface{}{"name": "test"}, "sepc": map[string]interface{}{"template": map[string]interface{}{"spec": map[string]interface{}{"containers": []interface{}{map[string]interface{}{"image": "website:0.1", "name": "main"}, map[string]interface{}{"image": "metrics-agent:0.2", "name": "sidecar"}}}}}, "spec": map[string]interface{}{"replicas": int64(2), "template": map[string]interface{}{"spec": map[string]interface{}{"containers": []interface{}{map[string]interface{}{"image": "website:0.1", "name": "main"}}}}}}} + assert.Equal(t, expect, obj) +} diff --git a/pkg/dsl/model/instance.go b/pkg/dsl/model/instance.go new file mode 100644 index 000000000..8d922f591 --- /dev/null +++ b/pkg/dsl/model/instance.go @@ -0,0 +1,195 @@ +package model + +import ( + "bytes" + "encoding/json" + "fmt" + "path/filepath" + + "cuelang.org/go/cue" + "cuelang.org/go/cue/ast" + "cuelang.org/go/cue/format" + "cuelang.org/go/cue/token" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +// Instance defines Model Interface +type Instance interface { + String() string + Object(m *runtime.Scheme) (runtime.Object, error) + IsBase() bool + Unity(other Instance) error +} + +type instance struct { + v string + base bool +} + +// String return instance's cue format string +func (inst *instance) String() string { + return inst.v +} + +// IsBase indicate whether the instance is base model +func (inst *instance) IsBase() bool { + return inst.base +} + +// Object convert to runtime.Object +func (inst *instance) Object(m *runtime.Scheme) (runtime.Object, error) { + var r cue.Runtime + cueInst, err := r.Compile("-", inst.v) + if err != nil { + return nil, err + } + o := new(unstructured.Unstructured) + jsonv, err := cueInst.Value().MarshalJSON() + if err != nil { + return nil, err + } + if err := o.UnmarshalJSON(jsonv); err != nil { + return nil, err + } + + if m != nil { + object, err := m.New(o.GetObjectKind().GroupVersionKind()) + if err == nil { + if err := json.Unmarshal(jsonv, object); err != nil { + return nil, err + } + return object, nil + } + if !runtime.IsNotRegisteredError(err) { + return nil, err + } + } + + return o, nil +} + +// Unity implement unity operations between instances +func (inst *instance) Unity(other Instance) error { + var r cue.Runtime + raw, err := r.Compile("-", inst.v) + if err != nil { + return err + } + o, err := r.Compile("-", other.String()) + if err != nil { + return err + } + pv, err := print(raw.Value().Unify(o.Value())) + if err != nil { + return err + } + inst.v = pv + return nil +} + +// NewBase create a base instance +func NewBase(v cue.Value) (Instance, error) { + vs, err := openPrint(v) + if err != nil { + return nil, err + } + return &instance{ + v: vs, + base: true, + }, nil +} + +// NewOther create a non-base instance +func NewOther(v cue.Value) (Instance, error) { + vs, err := openPrint(v) + if err != nil { + return nil, err + } + return &instance{ + v: vs, + }, nil +} + +func print(v cue.Value) (string, error) { + v = v.Eval() + syopts := []cue.Option{cue.All(), cue.DisallowCycles(true), cue.ResolveReferences(true)} + + var w bytes.Buffer + useSep := false + format := func(name string, n ast.Node) error { + if name != "" { + // TODO: make this relative to DIR + fmt.Fprintf(&w, "// %s\n", filepath.Base(name)) + } else if useSep { + fmt.Println("// ---") + } + useSep = true + + b, err := format.Node(toFile(n)) + if err != nil { + return err + } + _, err = w.Write(b) + return err + } + + if err := format("", v.Syntax(syopts...)); err != nil { + return "", err + } + instStr := w.String() + return instStr, nil +} + +func toFile(n ast.Node) *ast.File { + switch x := n.(type) { + case nil: + return nil + case *ast.StructLit: + return &ast.File{Decls: x.Elts} + case ast.Expr: + ast.SetRelPos(x, token.NoSpace) + return &ast.File{Decls: []ast.Decl{&ast.EmbedDecl{Expr: x}}} + case *ast.File: + return x + default: + panic(fmt.Sprintf("Unsupported node type %T", x)) + } +} + +func openPrint(v cue.Value) (string, error) { + sysopts := []cue.Option{cue.All(), cue.DisallowCycles(true), cue.ResolveReferences(true)} + f := toFile(v.Syntax(sysopts...)) + for _, decl := range f.Decls { + listOpen(decl) + } + ret, err := format.Node(f) + return string(ret), err +} + +func listOpen(expr ast.Node) { + switch v := expr.(type) { + case *ast.Field: + listOpen(v.Value) + case *ast.StructLit: + for _, elt := range v.Elts { + listOpen(elt) + } + case *ast.BinaryExpr: + listOpen(v.X) + listOpen(v.Y) + case *ast.EmbedDecl: + listOpen(v.Expr) + case *ast.Comprehension: + listOpen(v.Value) + case *ast.ListLit: + for _, elt := range v.Elts { + listOpen(elt) + } + if len(v.Elts) > 0 { + if _, ok := v.Elts[len(v.Elts)-1].(*ast.Ellipsis); !ok { + v.Elts = append(v.Elts, &ast.Ellipsis{}) + } + } + } +} diff --git a/pkg/dsl/model/instance_test.go b/pkg/dsl/model/instance_test.go new file mode 100644 index 000000000..2cd5d8fd2 --- /dev/null +++ b/pkg/dsl/model/instance_test.go @@ -0,0 +1,63 @@ +package model + +import ( + "testing" + + "cuelang.org/go/cue" + "github.com/bmizerany/assert" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func TestInstance(t *testing.T) { + + testCases := []struct { + src string + gvk schema.GroupVersionKind + }{{ + src: `apiVersion: "apps/v1" +kind: "Deployment" +metadata: name: "test" +`, + gvk: schema.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + }}, + } + + for _, v := range testCases { + var r cue.Runtime + inst, err := r.Compile("-", v.src) + if err != nil { + t.Error(err) + return + } + base, err := NewBase(inst.Value()) + if err != nil { + t.Error(err) + return + } + baseObj, err := base.Object(nil) + if err != nil { + t.Error(err) + return + } + + assert.Equal(t, v.gvk, baseObj.GetObjectKind().GroupVersionKind()) + assert.Equal(t, true, base.IsBase()) + + other, err := NewOther(inst.Value()) + if err != nil { + t.Error(err) + return + } + otherObj, err := other.Object(nil) + if err != nil { + t.Error(err) + return + } + + assert.Equal(t, v.gvk, otherObj.GetObjectKind().GroupVersionKind()) + assert.Equal(t, false, other.IsBase()) + } +} diff --git a/pkg/dsl/process/handle.go b/pkg/dsl/process/handle.go new file mode 100644 index 000000000..2df080cc9 --- /dev/null +++ b/pkg/dsl/process/handle.go @@ -0,0 +1,89 @@ +package process + +import ( + "encoding/json" + "fmt" + "strings" + "unicode" + + "github.com/oam-dev/kubevela/pkg/dsl/model" +) + +// Context defines Rendering Context Interface +type Context interface { + SetBase(base model.Instance) + PutAssistants(insts ...model.Instance) + Output() (model.Instance, []model.Instance) + Compile(label string) string +} + +type context struct { + name string + configs map[string]interface{} + base model.Instance + assistants []model.Instance +} + +// NewContext create render context +func NewContext(name string) Context { + return &context{ + name: name, + configs: map[string]interface{}{}, + assistants: []model.Instance{}, + } +} + +// SetBase set context base model +func (ctx *context) SetBase(base model.Instance) { + ctx.base = base +} + +// PutAssistants add Assist model to context +func (ctx *context) PutAssistants(insts ...model.Instance) { + ctx.assistants = append(ctx.assistants, insts...) +} + +// Compile return cue format string of context +func (ctx *context) Compile(label string) string { + var buff string + buff += fmt.Sprintf("name: \"%s\"\n", ctx.name) + + if ctx.base != nil { + buff += fmt.Sprintf("input: %s\n", structMashal(ctx.base.String())) + } + + if len(ctx.configs) > 0 { + bt, _ := json.Marshal(ctx.configs) + buff += "configs: " + string(bt) + } + + if label != "" { + buff = fmt.Sprintf("%s: %s", label, structMashal(buff)) + } + + return buff +} + +// Output return models of context +func (ctx *context) Output() (model.Instance, []model.Instance) { + return ctx.base, ctx.assistants +} + +func structMashal(v string) string { + skip := false + v = strings.TrimFunc(v, func(r rune) bool { + if !skip { + if unicode.IsSpace(r) { + return true + } + skip = true + + } + return false + }) + + if strings.HasPrefix(v, "{") { + return v + } + return fmt.Sprintf("{%s}", v) +} diff --git a/pkg/dsl/process/handle_test.go b/pkg/dsl/process/handle_test.go new file mode 100644 index 000000000..2a4b2c3b4 --- /dev/null +++ b/pkg/dsl/process/handle_test.go @@ -0,0 +1,43 @@ +package process + +import ( + "testing" + + "cuelang.org/go/cue" + "github.com/bmizerany/assert" + + "github.com/oam-dev/kubevela/pkg/dsl/model" +) + +func TestContext(t *testing.T) { + baseTemplate := ` +image: "myserver" +` + + var r cue.Runtime + inst, err := r.Compile("-", baseTemplate) + if err != nil { + t.Error(err) + return + } + base, err := model.NewBase(inst.Value()) + if err != nil { + t.Error(err) + return + } + + ctx := NewContext("myctx") + ctx.SetBase(base) + ctxInst, err := r.Compile("-", ctx.Compile("context")) + if err != nil { + t.Error(err) + return + } + + gName, err := ctxInst.Lookup("context", "name").String() + assert.Equal(t, nil, err) + assert.Equal(t, "myctx", gName) + inputJs, err := ctxInst.Lookup("context", "input").MarshalJSON() + assert.Equal(t, nil, err) + assert.Equal(t, `{"image":"myserver"}`, string(inputJs)) +}