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/application_controller_test.go b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller_test.go index 6f5938ad1..7559c2572 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,97 @@ 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", + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{"trait.oam.dev/type": "AuxiliaryWorkload"}, + }, + "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 +866,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/controller/core.oam.dev/v1alpha2/applicationconfiguration/applicationconfiguration_without_traitdefinition_test.go b/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration/applicationconfiguration_without_traitdefinition_test.go index 688ff7d3e..28a2b9e8b 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration/applicationconfiguration_without_traitdefinition_test.go +++ b/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration/applicationconfiguration_without_traitdefinition_test.go @@ -175,7 +175,7 @@ spec: LabelSelector: selector, }) Expect(err).Should(BeNil()) - traitNamePrefix := fmt.Sprintf("%s-dummy-", componentName) + traitNamePrefix := fmt.Sprintf("%s-trait-", componentName) var traitExistFlag bool for _, t := range scaleList.Items { if strings.HasPrefix(t.Name, traitNamePrefix) { diff --git a/pkg/dsl/definition/template.go b/pkg/dsl/definition/template.go index 32d8c174c..4014072de 100644 --- a/pkg/dsl/definition/template.go +++ b/pkg/dsl/definition/template.go @@ -29,6 +29,12 @@ const ( PatchFieldName = "patch" ) +const ( + // AuxiliaryWorkload defines the extra workload obj from a workloadDefinition, + // e.g. a workload composed by deployment and service, the service will be marked as AuxiliaryWorkload + AuxiliaryWorkload = "AuxiliaryWorkload" +) + var ( metadataAccessor = meta.NewAccessor() ) @@ -99,6 +105,23 @@ 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) + } + ctx.PutAssistants(process.Assistant{Ins: other, Type: AuxiliaryWorkload}) + } + } } 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 } diff --git a/pkg/oam/util/helper.go b/pkg/oam/util/helper.go index 865106120..a79d85e26 100644 --- a/pkg/oam/util/helper.go +++ b/pkg/oam/util/helper.go @@ -51,7 +51,7 @@ const ( Dummy = "dummy" // DummyTraitMessage is a message for trait which don't have definition found - DummyTraitMessage = "No valid TraitDefinition found, all framework capabilities will work as default or disabled" + DummyTraitMessage = "No TraitDefinition found, all framework capabilities will work as default" // DefinitionNamespaceEnv is env key for specifying a namespace to fetch definition DefinitionNamespaceEnv = "DEFINITION_NAMESPACE" @@ -417,7 +417,7 @@ func RawExtension2Map(raw *runtime.RawExtension) (map[string]interface{}, error) // GenTraitName generate trait name func GenTraitName(componentName string, ct *v1alpha2.ComponentTrait, traitType string) string { var traitMiddleName = TraitPrefixKey - if traitType != "" { + if traitType != "" && traitType != Dummy { traitMiddleName = strings.ToLower(traitType) } return fmt.Sprintf("%s-%s-%s", componentName, traitMiddleName, ComputeHash(ct)) diff --git a/pkg/oam/util/helper_test.go b/pkg/oam/util/helper_test.go index 6621a48cf..b30c34ff7 100644 --- a/pkg/oam/util/helper_test.go +++ b/pkg/oam/util/helper_test.go @@ -851,6 +851,12 @@ func TestGenTraitName(t *testing.T) { definitionName: "", exp: "simple-trait-67b8949f8d", }, + { + name: "service", + template: &v1alpha2.ComponentTrait{}, + definitionName: "dummy", + exp: "service-trait-67b8949f8d", + }, { name: "simple", template: &v1alpha2.ComponentTrait{ diff --git a/pkg/serverlib/capability.go b/pkg/serverlib/capability.go index cc94885f3..3f2772586 100644 --- a/pkg/serverlib/capability.go +++ b/pkg/serverlib/capability.go @@ -16,12 +16,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2" - - "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" - "github.com/oam-dev/kubevela/pkg/oam/util" - "github.com/oam-dev/kubevela/apis/types" cmdutil "github.com/oam-dev/kubevela/pkg/commands/util" + "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" + "github.com/oam-dev/kubevela/pkg/oam/util" "github.com/oam-dev/kubevela/pkg/plugins" "github.com/oam-dev/kubevela/pkg/server/apis" "github.com/oam-dev/kubevela/pkg/utils/helm"