diff --git a/docs/examples/advanced-cue/app4.yaml b/docs/examples/advanced-cue/app4.yaml new file mode 100644 index 000000000..8dedb82be --- /dev/null +++ b/docs/examples/advanced-cue/app4.yaml @@ -0,0 +1,14 @@ +apiVersion: core.oam.dev/v1alpha2 +kind: Application +metadata: + name: testapp4 +spec: + components: + - name: express-server4 + type: webserver + settings: + cmd: + - node + - server.js + image: oamdev/testapp:v1 + port: 8080 \ No newline at end of file diff --git a/docs/examples/advanced-cue/webserver.yaml b/docs/examples/advanced-cue/webserver.yaml new file mode 100644 index 000000000..0666cda58 --- /dev/null +++ b/docs/examples/advanced-cue/webserver.yaml @@ -0,0 +1,89 @@ +apiVersion: core.oam.dev/v1alpha2 +kind: WorkloadDefinition +metadata: + name: webserver + annotations: + definition.oam.dev/description: "webserver was composed by deployment and service" +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 + } + } + }] + } + } + } + } + // workload can have extra object composition by using 'outputs' keyword + outputs: service: { + apiVersion: "v1" + kind: "Service" + spec: { + selector: { + "app.oam.dev/component": context.name + } + ports: [ + { + port: parameter.port + targetPort: parameter.port + }, + ] + } + } + parameter: { + image: string + cmd?: [...string] + port: *80 | int + env?: [...{ + name: string + value?: string + valueFrom?: { + secretKeyRef: { + name: string + key: string + } + } + }] + cpu?: string + } + diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/appfile_parser.go b/pkg/controller/core.oam.dev/v1alpha2/application/appfile_parser.go new file mode 100644 index 000000000..f59957d27 --- /dev/null +++ b/pkg/controller/core.oam.dev/v1alpha2/application/appfile_parser.go @@ -0,0 +1,123 @@ +package application + +import ( + "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2" + "github.com/oam-dev/kubevela/pkg/appfile/config" + "github.com/oam-dev/kubevela/pkg/dsl/definition" + "github.com/oam-dev/kubevela/pkg/dsl/process" + "github.com/oam-dev/kubevela/pkg/oam" +) + +const ( + // OAMApplicationLabel is application's metadata label + OAMApplicationLabel = "application.oam.dev" +) + +// GenerateApplicationConfiguration converts an appFile to applicationConfig & Components +func (p *Parser) GenerateApplicationConfiguration(app *Appfile, ns string) (*v1alpha2.ApplicationConfiguration, + []*v1alpha2.Component, error) { + appconfig := &v1alpha2.ApplicationConfiguration{} + appconfig.SetGroupVersionKind(v1alpha2.ApplicationConfigurationGroupVersionKind) + appconfig.Name = app.Name + appconfig.Namespace = ns + appconfig.Spec.Components = []v1alpha2.ApplicationConfigurationComponent{} + + if appconfig.Labels == nil { + appconfig.Labels = map[string]string{} + } + appconfig.Labels[OAMApplicationLabel] = app.Name + + var components []*v1alpha2.Component + for _, wl := range app.Workloads { + + pCtx := process.NewContext(wl.Name) + userConfig := wl.GetUserConfigName() + if userConfig != "" { + cg := config.Configmap{Client: p.client} + + // TODO(wonderflow): envName should not be namespace when we have serverside env + var envName = ns + + data, err := cg.GetConfigData(config.GenConfigMapName(app.Name, wl.Name, userConfig), envName) + if err != nil { + return nil, nil, err + } + pCtx.SetConfigs(data) + } + + 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 := evalWorkloadWithContext(pCtx, wl) + if err != nil { + return nil, nil, err + } + comp.Name = wl.Name + acComp.ComponentName = comp.Name + + for _, sc := range wl.Scopes { + acComp.Scopes = append(acComp.Scopes, v1alpha2.ComponentScope{ScopeReference: v1alpha1.TypedReference{ + APIVersion: sc.GVK.GroupVersion().String(), + Kind: sc.GVK.Kind, + Name: sc.Name, + }}) + } + + comp.Namespace = ns + if comp.Labels == nil { + comp.Labels = map[string]string{} + } + comp.Labels[OAMApplicationLabel] = app.Name + comp.SetGroupVersionKind(v1alpha2.ComponentGroupVersionKind) + + components = append(components, comp) + appconfig.Spec.Components = append(appconfig.Spec.Components, *acComp) + } + return appconfig, components, nil +} + +// evalWorkloadWithContext evaluate the workload's template to generate component and ACComponent +func evalWorkloadWithContext(pCtx process.Context, wl *Workload) (*v1alpha2.Component, *v1alpha2.ApplicationConfigurationComponent, error) { + base, assists := pCtx.Output() + componentWorkload, err := base.Unstructured() + if err != nil { + return nil, nil, err + } + workloadType := wl.Type + labels := componentWorkload.GetLabels() + if labels == nil { + labels = map[string]string{oam.WorkloadTypeLabel: workloadType} + } else { + labels[oam.WorkloadTypeLabel] = workloadType + } + componentWorkload.SetLabels(labels) + + component := &v1alpha2.Component{} + component.Spec.Workload.Object = componentWorkload + + acComponent := &v1alpha2.ApplicationConfigurationComponent{} + acComponent.Traits = []v1alpha2.ComponentTrait{} + for _, assist := range assists { + tr, err := assist.Ins.Unstructured() + if err != nil { + return nil, nil, err + } + if assist.Type != "" && assist.Type != definition.ExtraWorkloadObj { + tr.SetLabels(map[string]string{oam.TraitTypeLabel: assist.Type}) + } + acComponent.Traits = append(acComponent.Traits, v1alpha2.ComponentTrait{ + Trait: runtime.RawExtension{ + Object: tr, + }, + }) + } + return component, acComponent, 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 6f5938ad1..8278191b6 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 @@ -25,6 +25,8 @@ import ( "net/http/httptest" "time" + "github.com/stretchr/testify/assert" + "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1" "github.com/google/go-cmp/cmp" . "github.com/onsi/ginkgo" @@ -154,6 +156,9 @@ var _ = Describe("Test Application Controller", func() { wd := &v1alpha2.WorkloadDefinition{} wDDefJson, _ := yaml.YAMLToJSON([]byte(wDDefYaml)) + webserverwd := &v1alpha2.WorkloadDefinition{} + webserverwdJson, _ := yaml.YAMLToJSON([]byte(webserverYaml)) + td := &v1alpha2.TraitDefinition{} tDDefJson, _ := yaml.YAMLToJSON([]byte(tDDefYaml)) @@ -176,6 +181,9 @@ var _ = Describe("Test Application Controller", func() { Expect(json.Unmarshal(sdDefJson, sd)).Should(BeNil()) Expect(k8sClient.Create(ctx, sd.DeepCopy())).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + Expect(json.Unmarshal(webserverwdJson, webserverwd)).Should(BeNil()) + Expect(k8sClient.Create(ctx, webserverwd.DeepCopy())).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + }) AfterEach(func() { }) @@ -326,6 +334,94 @@ var _ = Describe("Test Application Controller", func() { Expect(k8sClient.Delete(ctx, app)).Should(BeNil()) }) + It("app-with-composedworkload-trait will create workload and trait", func() { + compName := "myweb-composed-3" + expDeployment := getExpDeployment(compName) + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vela-test-with-composedworkload-trait", + }, + } + var appname = "app-with-composedworkload-trait" + appWithComposedWorkload := appwithNoTrait.DeepCopy() + appWithComposedWorkload.Spec.Components[0].WorkloadType = "webserver" + appWithComposedWorkload.SetName(appname) + appWithComposedWorkload.Spec.Components[0].Traits = []v1alpha2.ApplicationTrait{ + { + Name: "scaler", + Properties: runtime.RawExtension{Raw: []byte(`{"replicas":2}`)}, + }, + } + appWithComposedWorkload.Spec.Components[0].Name = compName + appWithComposedWorkload.SetNamespace(ns.Name) + Expect(k8sClient.Create(ctx, ns)).Should(BeNil()) + app := appWithComposedWorkload.DeepCopy() + Expect(k8sClient.Create(ctx, app)).Should(BeNil()) + + appKey := client.ObjectKey{ + Name: app.Name, + Namespace: app.Namespace, + } + reconcileRetry(reconciler, reconcile.Request{NamespacedName: appKey}) + + By("Check App running successfully") + checkApp := &v1alpha2.Application{} + Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil()) + Expect(checkApp.Status.Phase).Should(Equal(v1alpha2.ApplicationRunning)) + + By("Check AppConfig and trait created as expected") + appConfig := &v1alpha2.ApplicationConfiguration{} + Expect(k8sClient.Get(ctx, client.ObjectKey{ + Namespace: app.Namespace, + Name: app.Name, + }, appConfig)).Should(BeNil()) + + Expect(len(appConfig.Spec.Components[0].Traits)).Should(BeEquivalentTo(2)) + + gotTrait := unstructured.Unstructured{} + By("Check the first trait should be service") + expectServiceTrait := unstructured.Unstructured{Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Service", + "spec": map[string]interface{}{ + "ports": []interface{}{ + map[string]interface{}{"port": int64(80), "targetPort": int64(80)}, + }, + "selector": map[string]interface{}{ + "app.oam.dev/component": compName, + }, + }, + }} + Expect(json.Unmarshal(appConfig.Spec.Components[0].Traits[0].Trait.Raw, &gotTrait)).Should(BeNil()) + fmt.Println(cmp.Diff(expectServiceTrait, gotTrait)) + Expect(assert.ObjectsAreEqual(expectServiceTrait, gotTrait)).Should(BeTrue()) + + By("Check the second trait should be scaler") + gotTrait = unstructured.Unstructured{} + Expect(json.Unmarshal(appConfig.Spec.Components[0].Traits[1].Trait.Raw, &gotTrait)).Should(BeNil()) + Expect(gotTrait).Should(BeEquivalentTo(expectScalerTrait)) + + By("Check component created as expected") + component := &v1alpha2.Component{} + Expect(k8sClient.Get(ctx, client.ObjectKey{ + Namespace: app.Namespace, + Name: compName, + }, component)).Should(BeNil()) + Expect(component.ObjectMeta.Labels).Should(BeEquivalentTo(map[string]string{"application.oam.dev": appname})) + Expect(component.ObjectMeta.OwnerReferences[0].Name).Should(BeEquivalentTo(appname)) + Expect(component.ObjectMeta.OwnerReferences[0].Kind).Should(BeEquivalentTo("Application")) + Expect(component.ObjectMeta.OwnerReferences[0].APIVersion).Should(BeEquivalentTo("core.oam.dev/v1alpha2")) + Expect(component.ObjectMeta.OwnerReferences[0].Controller).Should(BeEquivalentTo(pointer.BoolPtr(true))) + gotD := &v1.Deployment{} + expDeployment.ObjectMeta.Labels["workload.oam.dev/type"] = "webserver" + expDeployment.Spec.Template.Spec.Containers[0].Ports = []corev1.ContainerPort{{ContainerPort: 80}} + Expect(json.Unmarshal(component.Spec.Workload.Raw, gotD)).Should(BeNil()) + fmt.Println(cmp.Diff(expDeployment, gotD)) + Expect(gotD).Should(BeEquivalentTo(expDeployment)) + + Expect(k8sClient.Delete(ctx, app)).Should(BeNil()) + }) + It("app-with-trait-and-scope will create workload, trait and scope", func() { expDeployment := getExpDeployment("myweb4") ns := &corev1.Namespace{ @@ -767,6 +863,97 @@ spec: cmd?: [...string] } +` + + webserverYaml = `apiVersion: core.oam.dev/v1alpha2 +kind: WorkloadDefinition +metadata: + name: webserver + annotations: + definition.oam.dev/description: "webserver was composed by deployment and service" +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 + } + } + }] + } + } + } + } + // workload can have extra object composition by using 'outputs' keyword + outputs: service: { + apiVersion: "v1" + kind: "Service" + spec: { + selector: { + "app.oam.dev/component": context.name + } + ports: [ + { + port: parameter.port + targetPort: parameter.port + }, + ] + } + } + parameter: { + image: string + cmd?: [...string] + port: *80 | int + env?: [...{ + name: string + value?: string + valueFrom?: { + secretKeyRef: { + name: string + key: string + } + } + }] + cpu?: string + } + ` wDDefWithHealthYaml = ` apiVersion: core.oam.dev/v1alpha2 diff --git a/pkg/dsl/definition/template.go b/pkg/dsl/definition/template.go index 32d8c174c..747c29745 100644 --- a/pkg/dsl/definition/template.go +++ b/pkg/dsl/definition/template.go @@ -29,6 +29,12 @@ const ( PatchFieldName = "patch" ) +const ( + // ExtraWorkloadObj defines the extra workload obj from a workloadDefinition, + // e.g. a workload composed by deployment and service, the service will be marked as ExtraWorkloadObj + ExtraWorkloadObj = "ExtraWorkloadObj" +) + var ( metadataAccessor = meta.NewAccessor() ) @@ -99,6 +105,24 @@ func (wd *workloadDef) Complete(ctx process.Context) error { return errors.WithMessagef(err, "workloadDef %s new base", wd.name) } ctx.SetBase(base) + + // we will support outputs for workload composition, and it will become trait in AppConfig. + outputs := inst.Lookup(OutputsFieldName) + 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, "parse WorkloadDefinition %s outputs(%s)", wd.name, fieldInfo.Name) + } + // extra workload CR object will not have type + ctx.PutAssistants(process.Assistant{Ins: other, Type: ExtraWorkloadObj}) + } + } } return nil } diff --git a/pkg/dsl/process/handle.go b/pkg/dsl/process/handle.go index 5fb3f6ba8..ef2d91edd 100644 --- a/pkg/dsl/process/handle.go +++ b/pkg/dsl/process/handle.go @@ -20,7 +20,9 @@ type Context interface { // Assistant are objects rendered by definition template. type Assistant struct { - Ins model.Instance + Ins model.Instance + // Type will be used to mark definition label for OAM runtime to get the CRD + // It's now required for trait and main workload object. Extra workload CR object will not have the type. Type string }