diff --git a/docs/en/end-user/definition-revision.md b/docs/en/end-user/definition-revision.md new file mode 100644 index 000000000..be09f394f --- /dev/null +++ b/docs/en/end-user/definition-revision.md @@ -0,0 +1,110 @@ +--- +title: Specify Definition Revision in Application +--- + +Each time the platform provider update ComponentDefinition/TraitDefinition, a corresponding DefinitionRevision will be generated. +And the DefinitionRevision can be regarded as a snapshot of ComponentDefinition/TraitDefinition. + +In this section, we will introduce how to specify a revision in Application. + +## Usage of Definition + +Suppose we need a `worker` to run a background service. And the platform provider has already provided a `worker` +ComponentDefinition for us(The ComponentDefinition `worker` may have been updated multiple times). + + +Assume the platform provider registered the `v1` version of the `worker` like below: + +**Click to see how to register the v1 version of the worker** +
+ +```shell +kubectl apply -f https://raw.githubusercontent.com/oam-dev/kubevela/master/docs/examples/definition-revision/worker-v1.yaml +``` + +
+ +We can use `kubectl vela show` to see the specification doc of the `worker`. + +```shell +$ kubectl vela show worker +# Properties ++-------+----------------------------------------------------+----------+----------+---------+ +| NAME | DESCRIPTION | TYPE | REQUIRED | DEFAULT | ++-------+----------------------------------------------------+----------+----------+---------+ +| cmd | Commands to run in the container | []string | false | | +| image | Which image would you like to use for your service | string | true | | ++-------+----------------------------------------------------+----------+----------+---------+ +``` + +Assume the platform provider has updated the version of the `worker` to `v2` like below. + +**Click to see how to update** +
+ +```shell +kubectl apply -f https://raw.githubusercontent.com/oam-dev/kubevela/master/docs/examples/definition-revision/worker-v2.yaml +``` + +
+ +The latest worker ComponentDefinition adds the `port` parameter, allowing users to specify the port exposed by the container. + +```shell +$ kubectl vela show worker +# Properties ++-------+----------------------------------------------------+----------+----------+---------+ +| NAME | DESCRIPTION | TYPE | REQUIRED | DEFAULT | ++-------+----------------------------------------------------+----------+----------+---------+ +| cmd | Commands to run in the container | []string | false | | +| image | Which image would you like to use for your service | string | true | | +| port | Which port do you want customer traffic sent to | int | true | | ++-------+----------------------------------------------------+----------+----------+---------+ +``` + +When the platform provider updated the two versions of the worker, the corresponding DefinitionRevision will +be generated to store the snapshot information. + +```shell +$ kubectl get definitionrevision -l="componentdefinition.oam.dev/name=worker" +NAME REVISION HASH TYPE +worker-v1 1 76486234845427dc Component +worker-v2 2 cb22fdc3b037702e Component +``` + +## Specify Definition Version in Application + +We can specify the Component to use a specific version of the Definition in Application, +By default, the application will always use the latest Definition to render the Component. + +But the latest Definition may not be compatible with user's application, so we can specify the old version like `v1` of the worker to render the Component. +we can specify the version of Definition in format `definitionName@version`. + +```yaml +# testapp.yaml +apiVersion: core.oam.dev/v1beta1 +kind: Application +metadata: + name: testapp +spec: + components: + - name: backend + type: worker@v1 + properties: + image: crccheck/hello-world +``` + +```shell +kubectl apply -f testapp.yaml +``` + +The `v1` version of the `worker` only allows the container to expose port `8000`. + +```shell +$ kubectl get deployment +NAME READY UP-TO-DATE AVAILABLE AGE +backend 1/1 1 1 3m7s + +$ kubectl get deployment backend -o jsonpath="{.spec.template.spec.containers[0].ports[0].containerPort}" +8000 +``` \ No newline at end of file diff --git a/docs/examples/definition-revision/worker-v1.yaml b/docs/examples/definition-revision/worker-v1.yaml new file mode 100644 index 000000000..113ddcde3 --- /dev/null +++ b/docs/examples/definition-revision/worker-v1.yaml @@ -0,0 +1,48 @@ +apiVersion: core.oam.dev/v1beta1 +kind: ComponentDefinition +metadata: + name: worker + namespace: default + 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: + workload: + definition: + apiVersion: apps/v1 + kind: Deployment + schematic: + cue: + 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 + } + ports: [{ + containerPort: 8000 + }] + }] + } + } + } + } + parameter: { + // +usage=Which image would you like to use for your service + image: string + // +usage=Commands to run in the container + cmd?: [...string] + } \ No newline at end of file diff --git a/docs/examples/definition-revision/worker-v2.yaml b/docs/examples/definition-revision/worker-v2.yaml new file mode 100644 index 000000000..130502e74 --- /dev/null +++ b/docs/examples/definition-revision/worker-v2.yaml @@ -0,0 +1,50 @@ +apiVersion: core.oam.dev/v1beta1 +kind: ComponentDefinition +metadata: + name: worker + namespace: default + 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: + workload: + definition: + apiVersion: apps/v1 + kind: Deployment + schematic: + cue: + 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 + } + ports: [{ + containerPort: parameter.port + }] + }] + } + } + } + } + parameter: { + // +usage=Which image would you like to use for your service + image: string + // +usage=Commands to run in the container + cmd?: [...string] + // +usage=Which port do you want customer traffic sent to + port: int + } diff --git a/docs/sidebars.js b/docs/sidebars.js index 4c5319175..8d73e2911 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -43,6 +43,7 @@ module.exports = { 'end-user/traits/more', ] }, + 'end-user/definition-revision', 'end-user/scopes/appdeploy', 'end-user/scopes/rollout-plan', { diff --git a/pkg/appfile/parser.go b/pkg/appfile/parser.go index 09fc971fe..9a22d7810 100644 --- a/pkg/appfile/parser.go +++ b/pkg/appfile/parser.go @@ -112,10 +112,15 @@ func (p *Parser) parseWorkload(ctx context.Context, comp v1beta1.ApplicationComp if err != nil { return nil, errors.WithMessagef(err, "fail to parse settings for %s", comp.Name) } + + wlType, err := util.ConvertDefinitionRevName(comp.Type) + if err != nil { + wlType = comp.Type + } workload := &Workload{ Traits: []*Trait{}, Name: comp.Name, - Type: comp.Type, + Type: wlType, CapabilityCategory: templ.CapabilityCategory, FullTemplate: templ, Params: settings, diff --git a/pkg/appfile/template.go b/pkg/appfile/template.go index c448299e9..22e7a25f4 100644 --- a/pkg/appfile/template.go +++ b/pkg/appfile/template.go @@ -70,7 +70,7 @@ func LoadTemplate(ctx context.Context, dm discoverymapper.DiscoveryMapper, cli c switch capType { case types.TypeComponentDefinition: cd := new(v1beta1.ComponentDefinition) - err := oamutil.GetDefinition(ctx, cli, cd, capName) + err := oamutil.GetCapabilityDefinition(ctx, cli, cd, capName) if err != nil { if kerrors.IsNotFound(err) { wd := new(v1beta1.WorkloadDefinition) @@ -101,7 +101,7 @@ func LoadTemplate(ctx context.Context, dm discoverymapper.DiscoveryMapper, cli c case types.TypeTrait: td := new(v1beta1.TraitDefinition) - err := oamutil.GetDefinition(ctx, cli, td, capName) + err := oamutil.GetCapabilityDefinition(ctx, cli, td, capName) if err != nil { return nil, errors.WithMessagef(err, "LoadTemplate [%s] ", capName) } diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/apply.go b/pkg/controller/core.oam.dev/v1alpha2/application/apply.go index 8c84ab3af..a88567dc4 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/apply.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/apply.go @@ -721,7 +721,7 @@ func (h *appHandler) handleRollout(ctx context.Context) (reconcile.Result, error // targetRevision should always points to LatestRevison targetRevision := h.app.Status.LatestRevision.Name var srcRevision string - target, _ := oamutil.ExtractRevisionNum(targetRevision) + target, _ := oamutil.ExtractRevisionNum(targetRevision, "-") // if target == 1 this is a initial scale operation, sourceRevision should be empty // otherwise source revision always is targetRevision - 1 if target > 1 { diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/revision.go b/pkg/controller/core.oam.dev/v1alpha2/application/revision.go index 03259068a..68b0dea0a 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/revision.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/revision.go @@ -369,7 +369,7 @@ func (h historiesByRevision) Len() int { return len(h) } func (h historiesByRevision) Swap(i, j int) { h[i], h[j] = h[j], h[i] } func (h historiesByRevision) Less(i, j int) bool { // the appRevision is generated by vela, the error always is nil, so ignore it - ir, _ := util.ExtractRevisionNum(h[i].Name) - ij, _ := util.ExtractRevisionNum(h[j].Name) + ir, _ := util.ExtractRevisionNum(h[i].Name, "-") + ij, _ := util.ExtractRevisionNum(h[j].Name, "-") return ir < ij } diff --git a/pkg/dsl/process/handle.go b/pkg/dsl/process/handle.go index cadae997a..0f23098f7 100644 --- a/pkg/dsl/process/handle.go +++ b/pkg/dsl/process/handle.go @@ -163,7 +163,7 @@ func (ctx *templateContext) BaseContextFile() string { buff += fmt.Sprintf(ContextAppName+": \"%s\"\n", ctx.appName) buff += fmt.Sprintf(ContextAppRevision+": \"%s\"\n", ctx.appRevision) // the appRevision is generated by vela, the error always is nil, so ignore it - revNum, _ := util.ExtractRevisionNum(ctx.appRevision) + revNum, _ := util.ExtractRevisionNum(ctx.appRevision, "-") buff += fmt.Sprintf(ContextAppRevisionNum+": %d\n", revNum) buff += fmt.Sprintf(ContextNamespace+": \"%s\"\n", ctx.namespace) diff --git a/pkg/oam/util/helper.go b/pkg/oam/util/helper.go index f2e49917b..a36b6a24d 100644 --- a/pkg/oam/util/helper.go +++ b/pkg/oam/util/helper.go @@ -145,6 +145,9 @@ type ConditionedObject interface { oam.Conditioned } +// ErrBadRevisionName represents an error when the revision name is not standardized +var ErrBadRevisionName = fmt.Errorf("bad revision name") + // LocateParentAppConfig locate the parent application configuration object func LocateParentAppConfig(ctx context.Context, client client.Client, oamObject oam.Object) (oam.Object, error) { @@ -333,6 +336,55 @@ func GetDefinition(ctx context.Context, cli client.Reader, definition runtime.Ob return nil } +// GetCapabilityDefinition can get different versions of ComponentDefinition/TraitDefinition +func GetCapabilityDefinition(ctx context.Context, cli client.Reader, definition runtime.Object, + definitionName string) error { + isLatestRevision, defRev, err := fetchDefinitionRev(ctx, cli, definitionName) + if err != nil { + return err + } + if isLatestRevision { + return GetDefinition(ctx, cli, definition, definitionName) + } + switch def := definition.(type) { + case *v1beta1.ComponentDefinition: + *def = defRev.Spec.ComponentDefinition + case *v1beta1.TraitDefinition: + *def = defRev.Spec.TraitDefinition + default: + } + return nil +} + +func fetchDefinitionRev(ctx context.Context, cli client.Reader, definitionName string) (bool, *v1beta1.DefinitionRevision, error) { + defRevName, err := ConvertDefinitionRevName(definitionName) + if err != nil { + if errors.As(err, &ErrBadRevisionName) { + return true, nil, nil + } + return false, nil, err + } + defRev := new(v1beta1.DefinitionRevision) + if err = GetDefinition(ctx, cli, defRev, defRevName); err != nil { + return false, nil, err + } + return false, defRev, err +} + +// ConvertDefinitionRevName can help convert definition type defined in Application to DefinitionRevision Name +// e.g., worker@v2 will be convert to worker-v2 +func ConvertDefinitionRevName(definitionName string) (string, error) { + revNum, err := ExtractRevisionNum(definitionName, "@") + if err != nil { + return "", err + } + defName := strings.TrimSuffix(definitionName, fmt.Sprintf("@v%d", revNum)) + if defName == "" { + return "", fmt.Errorf("invalid definition defName %s", definitionName) + } + return fmt.Sprintf("%s-v%d", defName, revNum), nil +} + // when get a namespaced scope object without namespace, would get an error request namespace func checkRequestNamespaceError(err error) bool { return err != nil && err.Error() == "an empty namespace may not be set when a resource name is provided" @@ -732,16 +784,16 @@ func ConvertComponentDef2WorkloadDef(dm discoverymapper.DiscoveryMapper, compone return nil } -// ExtractRevisionNum extract revision number from appRevision name -func ExtractRevisionNum(appRevision string) (int, error) { - splits := strings.Split(appRevision, "-") +// ExtractRevisionNum extract revision number +func ExtractRevisionNum(appRevision string, delimiter string) (int, error) { + splits := strings.Split(appRevision, delimiter) // check some bad appRevision name, eg:v1, appv2 if len(splits) == 1 { - return 0, fmt.Errorf("bad revison name") + return 0, ErrBadRevisionName } // check some bad appRevision name, eg:myapp-a1 if !strings.HasPrefix(splits[len(splits)-1], "v") { - return 0, fmt.Errorf("bad revison name") + return 0, ErrBadRevisionName } return strconv.Atoi(strings.TrimPrefix(splits[len(splits)-1], "v")) } diff --git a/pkg/oam/util/helper_test.go b/pkg/oam/util/helper_test.go index 8b4d73e99..f9e1ab990 100644 --- a/pkg/oam/util/helper_test.go +++ b/pkg/oam/util/helper_test.go @@ -2057,47 +2057,110 @@ spec: func TestExtractRevisionNum(t *testing.T) { testcases := []struct { - appRevision string + revName string wantRevisionNum int + delimiter string hasError bool }{{ - appRevision: "myapp-v1", + revName: "myapp-v1", wantRevisionNum: 1, + delimiter: "-", hasError: false, }, { - appRevision: "new-app-v2", + revName: "new-app-v2", wantRevisionNum: 2, + delimiter: "-", hasError: false, }, { - appRevision: "v1-v10", + revName: "v1-v10", wantRevisionNum: 10, + delimiter: "-", hasError: false, }, { - appRevision: "v10-v1-v1", + revName: "v10-v1-v1", wantRevisionNum: 1, + delimiter: "-", hasError: false, }, { - appRevision: "myapp-v1-v2", + revName: "myapp-v1-v2", wantRevisionNum: 2, + delimiter: "-", hasError: false, }, { - appRevision: "myapp-v1-vv", + revName: "myapp-v1-vv", wantRevisionNum: 0, + delimiter: "-", hasError: true, }, { - appRevision: "v1", + revName: "v1", wantRevisionNum: 0, + delimiter: "-", hasError: true, }, { - appRevision: "myapp-a1", + revName: "myapp-a1", wantRevisionNum: 0, + delimiter: "-", + hasError: true, + }, { + revName: "worker@v1", + wantRevisionNum: 1, + delimiter: "@", + hasError: false, + }, { + revName: "worke@10r@v1", + wantRevisionNum: 1, + delimiter: "@", + hasError: false, + }, { + revName: "webservice@a10", + wantRevisionNum: 0, + delimiter: "@", hasError: true, }} for _, tt := range testcases { - revision, err := util.ExtractRevisionNum(tt.appRevision) + revision, err := util.ExtractRevisionNum(tt.revName, tt.delimiter) hasError := err != nil assert.Equal(t, tt.wantRevisionNum, revision) assert.Equal(t, tt.hasError, hasError) } } + +func TestExtractDefinitionRevName(t *testing.T) { + testcases := []struct { + defName string + wantRevName string + hasError bool + }{{ + defName: "worker@v2", + wantRevName: "worker-v2", + hasError: false, + }, { + defName: "worker@v10", + wantRevName: "worker-v10", + hasError: false, + }, { + defName: "worker", + wantRevName: "", + hasError: true, + }, { + defName: "webservice@@v2", + wantRevName: "webservice@-v2", + hasError: false, + }, { + defName: "webservice@v10@v3", + wantRevName: "webservice@v10-v3", + hasError: false, + }, { + defName: "@v10", + wantRevName: "", + hasError: true, + }} + + for _, tt := range testcases { + revName, err := util.ConvertDefinitionRevName(tt.defName) + hasError := err != nil + assert.Equal(t, tt.wantRevName, revName) + assert.Equal(t, tt.hasError, hasError) + } +} diff --git a/pkg/oam/util/test_utils.go b/pkg/oam/util/test_utils.go index cd8e91bac..d19c1f7e2 100644 --- a/pkg/oam/util/test_utils.go +++ b/pkg/oam/util/test_utils.go @@ -197,7 +197,7 @@ func CheckAppRevision(revs []v1beta1.ApplicationRevision, collection []int) (boo } var revNums []int for _, rev := range revs { - num, err := ExtractRevisionNum(rev.Name) + num, err := ExtractRevisionNum(rev.Name, "-") if err != nil { return false, err } diff --git a/test/e2e-test/definition_revision_test.go b/test/e2e-test/definition_revision_test.go new file mode 100644 index 000000000..3f4798b64 --- /dev/null +++ b/test/e2e-test/definition_revision_test.go @@ -0,0 +1,1040 @@ +/* + Copyright 2021. The KubeVela Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package controllers_test + +import ( + "context" + "fmt" + "time" + + "github.com/ghodss/yaml" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/common" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/oam" + "github.com/oam-dev/kubevela/pkg/oam/util" +) + +var _ = Describe("Test application of the specified definition version", func() { + ctx := context.Background() + + var namespace string + var ns corev1.Namespace + + BeforeEach(func() { + namespace = randomNamespaceName("defrev-e2e-test") + ns = corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}} + + Eventually(func() error { + return k8sClient.Create(ctx, &ns) + }, time.Second*3, time.Microsecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + + labelV1 := labelWithNoTemplate.DeepCopy() + labelV1.Spec.Schematic.CUE.Template = labelV1Template + labelV1.SetNamespace(namespace) + Expect(k8sClient.Create(ctx, labelV1)).Should(Succeed()) + Eventually(func() error { + err := k8sClient.Get(ctx, client.ObjectKey{Name: "label", Namespace: namespace}, labelV1) + if err != nil { + return err + } + labelV1.Spec.Schematic.CUE.Template = labelV2Template + return k8sClient.Update(ctx, labelV1) + }, 15*time.Second, time.Second).Should(BeNil()) + labelDefRevList := new(v1beta1.DefinitionRevisionList) + labelDefRevListOpts := []client.ListOption{ + client.InNamespace(namespace), + client.MatchingLabels{ + oam.LabelTraitDefinitionName: "label", + }, + } + Eventually(func() error { + err := k8sClient.List(ctx, labelDefRevList, labelDefRevListOpts...) + if err != nil { + return err + } + if len(labelDefRevList.Items) != 2 { + return fmt.Errorf("error defRevison number wants %d, actually %d", 2, len(labelDefRevList.Items)) + } + return nil + }, 20*time.Second, time.Second).Should(BeNil()) + + }) + + AfterEach(func() { + By("Clean up resources after a test") + k8sClient.DeleteAllOf(ctx, &v1beta1.Application{}, client.InNamespace(namespace)) + k8sClient.DeleteAllOf(ctx, &v1beta1.ComponentDefinition{}, client.InNamespace(namespace)) + k8sClient.DeleteAllOf(ctx, &v1beta1.WorkloadDefinition{}, client.InNamespace(namespace)) + k8sClient.DeleteAllOf(ctx, &v1beta1.TraitDefinition{}, client.InNamespace(namespace)) + k8sClient.DeleteAllOf(ctx, &v1beta1.DefinitionRevision{}, client.InNamespace(namespace)) + + By(fmt.Sprintf("Delete the entire namespaceName %s", ns.Name)) + Expect(k8sClient.Delete(ctx, &ns, client.PropagationPolicy(metav1.DeletePropagationForeground))).Should(Succeed()) + }) + + It("Test deploy application which containing cue rendering module", func() { + var ( + appName = "test-website-app" + comp1Name = "front" + comp2Name = "backend" + ) + + workerV1 := workerWithNoTemplate.DeepCopy() + workerV1.Spec.Workload = common.WorkloadTypeDescriptor{ + Definition: common.WorkloadGVK{ + APIVersion: "batch/v1", + Kind: "Job", + }, + } + workerV1.Spec.Schematic.CUE.Template = workerV1Template + workerV1.SetNamespace(namespace) + Expect(k8sClient.Create(ctx, workerV1)).Should(Succeed()) + Eventually(func() error { + err := k8sClient.Get(ctx, client.ObjectKey{Name: "worker", Namespace: namespace}, workerV1) + if err != nil { + return err + } + workerV1.Spec.Workload = common.WorkloadTypeDescriptor{ + Definition: common.WorkloadGVK{ + APIVersion: "apps/v1", + Kind: "Deployment", + }, + } + workerV1.Spec.Schematic.CUE.Template = workerV2Template + return k8sClient.Update(ctx, workerV1) + }, 15*time.Second, time.Second).Should(BeNil()) + workerDefRevList := new(v1beta1.DefinitionRevisionList) + workerDefRevListOpts := []client.ListOption{ + client.InNamespace(namespace), + client.MatchingLabels{ + oam.LabelComponentDefinitionName: "worker", + }, + } + Eventually(func() error { + err := k8sClient.List(ctx, workerDefRevList, workerDefRevListOpts...) + if err != nil { + return err + } + if len(workerDefRevList.Items) != 2 { + return fmt.Errorf("error defRevison number wants %d, actually %d", 2, len(workerDefRevList.Items)) + } + return nil + }, 20*time.Second, time.Second).Should(BeNil()) + + webserviceV1 := webServiceWithNoTemplate.DeepCopy() + webserviceV1.Spec.Schematic.CUE.Template = webServiceV1Template + webserviceV1.SetNamespace(namespace) + Expect(k8sClient.Create(ctx, webserviceV1)).Should(Succeed()) + Eventually(func() error { + err := k8sClient.Get(ctx, client.ObjectKey{Name: "webservice", Namespace: namespace}, webserviceV1) + if err != nil { + return err + } + webserviceV1.Spec.Schematic.CUE.Template = webServiceV2Template + return k8sClient.Update(ctx, webserviceV1) + }, 15*time.Second, time.Second).Should(BeNil()) + + webserviceDefRevList := new(v1beta1.DefinitionRevisionList) + webserviceDefRevListOpts := []client.ListOption{ + client.InNamespace(namespace), + client.MatchingLabels{ + oam.LabelComponentDefinitionName: "webservice", + }, + } + Eventually(func() error { + err := k8sClient.List(ctx, webserviceDefRevList, webserviceDefRevListOpts...) + if err != nil { + return err + } + if len(webserviceDefRevList.Items) != 2 { + return fmt.Errorf("error defRevison number wants %d, actually %d", 2, len(webserviceDefRevList.Items)) + } + return nil + }, 20*time.Second, time.Second).Should(BeNil()) + + app := v1beta1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: appName, + Namespace: namespace, + }, + Spec: v1beta1.ApplicationSpec{ + Components: []v1beta1.ApplicationComponent{ + { + Name: comp1Name, + Type: "webservice", + Properties: util.Object2RawExtension(map[string]interface{}{ + "image": "nginx", + }), + Traits: []v1beta1.ApplicationTrait{ + { + Type: "label", + Properties: util.Object2RawExtension(map[string]interface{}{ + "labels": map[string]string{ + "hello": "world", + }, + }), + }, + }, + }, + { + Name: comp2Name, + Type: "worker", + Properties: util.Object2RawExtension(map[string]interface{}{ + "image": "busybox", + "cmd": []string{"sleep", "1000"}, + }), + }, + }, + }, + } + + By("Create application") + Expect(k8sClient.Create(ctx, &app)).Should(Succeed()) + + ac := &v1alpha2.ApplicationContext{} + acName := appName + By("Verify the ApplicationContext is created & reconciled successfully") + Eventually(func() bool { + if err := k8sClient.Get(ctx, client.ObjectKey{Name: acName, Namespace: namespace}, ac); err != nil { + return false + } + return len(ac.Status.Workloads) > 0 + }, 60*time.Second, time.Second).Should(BeTrue()) + + By("Verify the workload(deployment) is created successfully") + Expect(len(ac.Status.Workloads)).Should(Equal(len(app.Spec.Components))) + webServiceDeploy := &appsv1.Deployment{} + deployName := ac.Status.Workloads[0].Reference.Name + Eventually(func() error { + return k8sClient.Get(ctx, client.ObjectKey{Name: deployName, Namespace: namespace}, webServiceDeploy) + }, 30*time.Second, 3*time.Second).Should(Succeed()) + + workerDeploy := &appsv1.Deployment{} + deployName = ac.Status.Workloads[1].Reference.Name + Eventually(func() error { + return k8sClient.Get(ctx, client.ObjectKey{Name: deployName, Namespace: namespace}, workerDeploy) + }, 30*time.Second, 3*time.Second).Should(Succeed()) + + By("Verify trait is applied to the workload") + webserviceLabels := webServiceDeploy.GetLabels() + Expect(webserviceLabels["hello"]).Should(Equal("world")) + + By("Update Application and Specify the Definition version in Application") + app = v1beta1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: appName, + Namespace: namespace, + }, + Spec: v1beta1.ApplicationSpec{ + Components: []v1beta1.ApplicationComponent{ + { + Name: comp1Name, + Type: "webservice@v1", + Properties: util.Object2RawExtension(map[string]interface{}{ + "image": "nginx", + }), + Traits: []v1beta1.ApplicationTrait{ + { + Type: "label@v1", + Properties: util.Object2RawExtension(map[string]interface{}{ + "labels": map[string]string{ + "hello": "kubevela", + }, + }), + }, + }, + }, + { + Name: comp2Name, + Type: "worker@v1", + Properties: util.Object2RawExtension(map[string]interface{}{ + "image": "busybox", + "cmd": []string{"sleep", "1000"}, + }), + }, + }, + }, + } + Expect(k8sClient.Patch(ctx, &app, client.Merge)).Should(Succeed()) + + By("Verify the ApplicationContext is update successfully") + Eventually(func() bool { + if err := k8sClient.Get(ctx, client.ObjectKey{Name: acName, Namespace: namespace}, ac); err != nil { + return false + } + return ac.Generation == 2 + }, 10*time.Second, time.Second).Should(BeTrue()) + + By("Verify the workload(deployment) is created successfully") + Expect(len(ac.Status.Workloads)).Should(Equal(len(app.Spec.Components))) + webServiceV1Deploy := &appsv1.Deployment{} + deployName = ac.Status.Workloads[0].Reference.Name + Eventually(func() error { + return k8sClient.Get(ctx, client.ObjectKey{Name: deployName, Namespace: namespace}, webServiceV1Deploy) + }, 30*time.Second, 3*time.Second).Should(Succeed()) + + By("Verify the workload(job) is created successfully") + workerJob := &batchv1.Job{} + jobName := ac.Status.Workloads[1].Reference.Name + Eventually(func() error { + return k8sClient.Get(ctx, client.ObjectKey{Name: jobName, Namespace: namespace}, workerJob) + }, 30*time.Second, 3*time.Second).Should(Succeed()) + + By("Verify trait is applied to the workload") + webserviceV1Labels := webServiceV1Deploy.GetLabels() + Expect(webserviceV1Labels["hello"]).Should(Equal("kubevela")) + + By("Check Application is rendered by the specified version of the Definition") + Expect(webServiceV1Deploy.Labels["componentdefinition.oam.dev/version"]).Should(Equal("v1")) + Expect(webServiceV1Deploy.Labels["traitdefinition.oam.dev/version"]).Should(Equal("v1")) + + By("Application specifies the wrong version of the Definition, it will raise an error") + app = v1beta1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: appName, + Namespace: namespace, + }, + Spec: v1beta1.ApplicationSpec{ + Components: []v1beta1.ApplicationComponent{ + { + Name: comp1Name, + Type: "webservice@v10", + Properties: util.Object2RawExtension(map[string]interface{}{ + "image": "nginx", + }), + }, + }, + }, + } + Expect(k8sClient.Patch(ctx, &app, client.Merge)).Should(Succeed()) + + apprev := &v1beta1.ApplicationRevision{} + Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: fmt.Sprintf("%s-v3", appName)}, apprev)).Should(HaveOccurred()) + }) + + It("Test deploy application which containing helm module", func() { + var ( + appName = "test-helm" + compName = "worker" + ) + + helmworkerV1 := HELMWorker.DeepCopy() + helmworkerV1.SetNamespace(namespace) + helmworkerV1.Spec.Workload.Definition = common.WorkloadGVK{ + APIVersion: "batch/v1beta1", + Kind: "CronJob", + } + helmworkerV1.Spec.Schematic = &common.Schematic{ + HELM: &common.Helm{ + Release: util.Object2RawExtension(map[string]interface{}{ + "chart": map[string]interface{}{ + "spec": map[string]interface{}{ + "chart": "podinfo", + "version": "5.1.4", + }, + }, + }), + Repository: util.Object2RawExtension(map[string]interface{}{ + "url": "https://stefanprodan.github.io/podinfo", + }), + }, + } + Expect(k8sClient.Create(ctx, helmworkerV1)).Should(Succeed()) + Eventually(func() error { + err := k8sClient.Get(ctx, client.ObjectKey{Name: "helm-worker", Namespace: namespace}, helmworkerV1) + if err != nil { + return err + } + helmworkerV1.Spec.Workload.Definition = common.WorkloadGVK{ + APIVersion: "apps/v1", + Kind: "Deployment", + } + helmworkerV1.Spec.Schematic = &common.Schematic{ + HELM: &common.Helm{ + Release: util.Object2RawExtension(map[string]interface{}{ + "chart": map[string]interface{}{ + "spec": map[string]interface{}{ + "chart": "podinfo", + "version": "5.2.0", + }, + }, + }), + Repository: util.Object2RawExtension(map[string]interface{}{ + "url": "https://stefanprodan.github.io/podinfo", + }), + }, + } + return k8sClient.Update(ctx, helmworkerV1) + }, 15*time.Second, time.Second).Should(BeNil()) + + helmworkerDefRevList := new(v1beta1.DefinitionRevisionList) + helmworkerDefRevListOpts := []client.ListOption{ + client.InNamespace(namespace), + client.MatchingLabels{ + oam.LabelComponentDefinitionName: "helm-worker", + }, + } + Eventually(func() error { + err := k8sClient.List(ctx, helmworkerDefRevList, helmworkerDefRevListOpts...) + if err != nil { + return err + } + if len(helmworkerDefRevList.Items) != 2 { + return fmt.Errorf("error defRevison number wants %d, actually %d", 2, len(helmworkerDefRevList.Items)) + } + return nil + }, 20*time.Second, time.Second).Should(BeNil()) + + app := v1beta1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: appName, + Namespace: namespace, + }, + Spec: v1beta1.ApplicationSpec{ + Components: []v1beta1.ApplicationComponent{ + { + Name: compName, + Type: "helm-worker", + Properties: util.Object2RawExtension(map[string]interface{}{ + "image": map[string]interface{}{ + "tag": "5.1.2", + }, + }), + Traits: []v1beta1.ApplicationTrait{ + { + Type: "label", + Properties: util.Object2RawExtension(map[string]interface{}{ + "labels": map[string]string{ + "hello": "world", + }, + }), + }, + }, + }, + }, + }, + } + + By("Create application") + Expect(k8sClient.Create(ctx, &app)).Should(Succeed()) + + ac := &v1alpha2.ApplicationContext{} + acName := appName + By("Verify the ApplicationContext is created successfully") + Eventually(func() error { + return k8sClient.Get(ctx, client.ObjectKey{Name: acName, Namespace: namespace}, ac) + }, 30*time.Second, time.Second).Should(Succeed()) + + By("Verify the workload(deployment) is created successfully by Helm") + deploy := &appsv1.Deployment{} + deployName := fmt.Sprintf("%s-%s-podinfo", appName, compName) + Eventually(func() bool { + err := k8sClient.Get(ctx, client.ObjectKey{Name: deployName, Namespace: namespace}, deploy) + if err != nil { + return false + } + DeployLabels := deploy.GetLabels() + return DeployLabels["helm.sh/chart"] == "podinfo-5.2.0" + }, 120*time.Second, 5*time.Second).Should(BeTrue()) + + By("Verify trait is applied to the workload") + Eventually(func() bool { + requestReconcileNow(ctx, ac) + deploy := &appsv1.Deployment{} + if err := k8sClient.Get(ctx, client.ObjectKey{Name: deployName, Namespace: namespace}, deploy); err != nil { + return false + } + By("Verify patch trait is applied") + templateLabels := deploy.GetLabels() + return templateLabels["hello"] != "world" + }, 120*time.Second, 10*time.Second).Should(BeTrue()) + + app = v1beta1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: appName, + Namespace: namespace, + }, + Spec: v1beta1.ApplicationSpec{ + Components: []v1beta1.ApplicationComponent{ + { + Name: compName, + Type: "helm-worker@v1", + Properties: util.Object2RawExtension(map[string]interface{}{ + "image": map[string]interface{}{ + "tag": "5.1.2", + }, + }), + }, + }, + }, + } + + By("Create application") + Expect(k8sClient.Patch(ctx, &app, client.Merge)).Should(Succeed()) + + By("Verify the ApplicationContext is updated") + Eventually(func() bool { + ac = &v1alpha2.ApplicationContext{} + if err := k8sClient.Get(ctx, client.ObjectKey{Name: acName, Namespace: namespace}, ac); err != nil { + return false + } + return ac.GetGeneration() == 2 + }, 15*time.Second, 3*time.Second).Should(BeTrue()) + + By("Verify the workload(deployment) is update successfully by Helm") + deploy = &appsv1.Deployment{} + Eventually(func() bool { + err := k8sClient.Get(ctx, client.ObjectKey{Name: deployName, Namespace: namespace}, deploy) + if err != nil { + return false + } + DeployLabels := deploy.GetLabels() + return DeployLabels["helm.sh/chart"] != "podinfo-5.1.4" + }, 120*time.Second, 5*time.Second).Should(BeTrue()) + }) + + It("Test deploy application which containing kube module", func() { + var ( + appName = "test-kube-app" + compName = "worker" + ) + + kubeworkerV1 := KUBEWorker.DeepCopy() + kubeworkerV1.Spec.Workload.Definition = common.WorkloadGVK{ + APIVersion: "apps/v1", + Kind: "Deployment", + } + kubeworkerV1.Spec.Schematic = &common.Schematic{ + KUBE: &common.Kube{ + Template: generateTemplate(KUBEWorkerV1Template), + Parameters: []common.KubeParameter{ + { + Name: "image", + ValueType: common.StringType, + FieldPaths: []string{"spec.template.spec.containers[0].image"}, + Required: pointer.BoolPtr(true), + Description: pointer.StringPtr("test description"), + }, + }, + }, + } + kubeworkerV1.SetNamespace(namespace) + Expect(k8sClient.Create(ctx, kubeworkerV1)).Should(Succeed()) + Eventually(func() error { + err := k8sClient.Get(ctx, client.ObjectKey{Name: "kube-worker", Namespace: namespace}, kubeworkerV1) + if err != nil { + return err + } + kubeworkerV1.Spec.Workload.Definition = common.WorkloadGVK{ + APIVersion: "batch/v1", + Kind: "Job", + } + kubeworkerV1.Spec.Schematic = &common.Schematic{ + KUBE: &common.Kube{ + Template: generateTemplate(KUBEWorkerV2Template), + Parameters: []common.KubeParameter{ + { + Name: "image", + ValueType: common.StringType, + FieldPaths: []string{"spec.template.spec.containers[0].image"}, + Required: pointer.BoolPtr(true), + Description: pointer.StringPtr("test description"), + }, + }, + }, + } + return k8sClient.Update(ctx, kubeworkerV1) + }, 15*time.Second, time.Second).Should(BeNil()) + + kubeworkerDefRevList := new(v1beta1.DefinitionRevisionList) + kubeworkerDefRevListOpts := []client.ListOption{ + client.InNamespace(namespace), + client.MatchingLabels{ + oam.LabelComponentDefinitionName: "kube-worker", + }, + } + Eventually(func() error { + err := k8sClient.List(ctx, kubeworkerDefRevList, kubeworkerDefRevListOpts...) + if err != nil { + return err + } + if len(kubeworkerDefRevList.Items) != 2 { + return fmt.Errorf("error defRevison number wants %d, actually %d", 2, len(kubeworkerDefRevList.Items)) + } + return nil + }, 20*time.Second, time.Second).Should(BeNil()) + + app := v1beta1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: appName, + Namespace: namespace, + }, + Spec: v1beta1.ApplicationSpec{ + Components: []v1beta1.ApplicationComponent{ + { + Name: compName, + Type: "kube-worker", + Properties: util.Object2RawExtension(map[string]interface{}{ + "image": "busybox", + }), + Traits: []v1beta1.ApplicationTrait{ + { + Type: "label", + Properties: util.Object2RawExtension(map[string]interface{}{ + "labels": map[string]string{ + "hello": "world", + }, + }), + }, + }, + }, + }, + }, + } + + By("Create application") + Expect(k8sClient.Create(ctx, &app)).Should(Succeed()) + + ac := &v1alpha2.ApplicationContext{} + acName := appName + By("Verify the ApplicationContext is created & reconciled successfully") + Eventually(func() bool { + if err := k8sClient.Get(ctx, client.ObjectKey{Name: acName, Namespace: namespace}, ac); err != nil { + return false + } + return len(ac.Status.Workloads) > 0 + }, 60*time.Second, time.Second).Should(BeTrue()) + + By("Verify the workload(job) is created successfully") + job := &batchv1.Job{} + jobName := ac.Status.Workloads[0].Reference.Name + Eventually(func() error { + return k8sClient.Get(ctx, client.ObjectKey{Name: jobName, Namespace: namespace}, job) + }, 30*time.Second, 3*time.Second).Should(Succeed()) + + By("Verify trait is applied to the workload") + Labels := job.GetLabels() + Expect(Labels["hello"]).Should(Equal("world")) + + By("Update Application and Specify the Definition version in Application") + app = v1beta1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: appName, + Namespace: namespace, + }, + Spec: v1beta1.ApplicationSpec{ + Components: []v1beta1.ApplicationComponent{ + { + Name: compName, + Type: "kube-worker@v1", + Properties: util.Object2RawExtension(map[string]interface{}{ + "image": "nginx", + }), + Traits: []v1beta1.ApplicationTrait{ + { + Type: "label", + Properties: util.Object2RawExtension(map[string]interface{}{ + "labels": map[string]string{ + "hello": "kubevela", + }, + }), + }, + }, + }, + }, + }, + } + Expect(k8sClient.Patch(ctx, &app, client.Merge)).Should(Succeed()) + + By("Verify the ApplicationContext is update successfully") + Eventually(func() bool { + if err := k8sClient.Get(ctx, client.ObjectKey{Name: acName, Namespace: namespace}, ac); err != nil { + return false + } + return ac.Generation == 2 + }, 10*time.Second, time.Second).Should(BeTrue()) + + By("Verify the workload(deployment) is created successfully") + Expect(len(ac.Status.Workloads)).Should(Equal(len(app.Spec.Components))) + deploy := &appsv1.Deployment{} + deployName := ac.Status.Workloads[0].Reference.Name + Eventually(func() error { + return k8sClient.Get(ctx, client.ObjectKey{Name: deployName, Namespace: namespace}, deploy) + }, 30*time.Second, 3*time.Second).Should(Succeed()) + + By("Verify trait is applied to the workload") + webserviceV1Labels := deploy.GetLabels() + Expect(webserviceV1Labels["hello"]).Should(Equal("kubevela")) + + By("Application specifies the wrong version of the Definition, it will raise an error") + app = v1beta1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: appName, + Namespace: namespace, + }, + Spec: v1beta1.ApplicationSpec{ + Components: []v1beta1.ApplicationComponent{ + { + Name: compName, + Type: "kube-worker@a1", + Properties: util.Object2RawExtension(map[string]interface{}{ + "image": "nginx", + }), + }, + }, + }, + } + Expect(k8sClient.Patch(ctx, &app, client.Merge)).Should(Succeed()) + + apprev := &v1beta1.ApplicationRevision{} + Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: fmt.Sprintf("%s-v3", appName)}, apprev)).Should(HaveOccurred()) + }) + +}) + +var webServiceWithNoTemplate = &v1beta1.ComponentDefinition{ + TypeMeta: metav1.TypeMeta{ + Kind: "ComponentDefinition", + APIVersion: "core.oam.dev/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "webservice", + }, + Spec: v1beta1.ComponentDefinitionSpec{ + Workload: common.WorkloadTypeDescriptor{ + Definition: common.WorkloadGVK{ + APIVersion: "apps/v1", + Kind: "Deployment", + }, + }, + Schematic: &common.Schematic{ + CUE: &common.CUE{ + Template: "", + }, + }, + }, +} + +var workerWithNoTemplate = &v1beta1.ComponentDefinition{ + TypeMeta: metav1.TypeMeta{ + Kind: "ComponentDefinition", + APIVersion: "core.oam.dev/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "worker", + }, + Spec: v1beta1.ComponentDefinitionSpec{ + Schematic: &common.Schematic{ + CUE: &common.CUE{ + Template: "", + }, + }, + }, +} + +var KUBEWorker = &v1beta1.ComponentDefinition{ + TypeMeta: metav1.TypeMeta{ + Kind: "ComponentDefinition", + APIVersion: "core.oam.dev/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-worker", + }, +} + +var HELMWorker = &v1beta1.ComponentDefinition{ + TypeMeta: metav1.TypeMeta{ + Kind: "ComponentDefinition", + APIVersion: "core.oam.dev/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "helm-worker", + }, +} + +var labelWithNoTemplate = &v1beta1.TraitDefinition{ + TypeMeta: metav1.TypeMeta{ + Kind: "TraitDefinition", + APIVersion: "core.oam.dev/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "label", + }, + Spec: v1beta1.TraitDefinitionSpec{ + Schematic: &common.Schematic{ + CUE: &common.CUE{ + Template: "", + }, + }, + }, +} + +func generateTemplate(template string) runtime.RawExtension { + b, _ := yaml.YAMLToJSON([]byte(template)) + return runtime.RawExtension{Raw: b} +} + +var webServiceV1Template = `output: { + apiVersion: "apps/v1" + kind: "Deployment" + metadata: labels: { + "componentdefinition.oam.dev/version": "v1" + } + 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: { + image: string + cmd?: [...string] + port: *80 | int + env?: [...{ + name: string + value?: string + valueFrom?: { + secretKeyRef: { + name: string + key: string + } + } + }] + cpu?: string +} +` + +var webServiceV2Template = `output: { + apiVersion: "apps/v1" + kind: "Deployment" + spec: { + selector: matchLabels: { + "app.oam.dev/component": context.name + if parameter.addRevisionLabel { + "app.oam.dev/appRevision": context.appRevision + } + } + template: { + metadata: labels: { + "app.oam.dev/component": context.name + if parameter.addRevisionLabel { + "app.oam.dev/appRevision": context.appRevision + } + } + 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: { + image: string + cmd?: [...string] + port: *80 | int + env?: [...{ + name: string + value?: string + valueFrom?: { + secretKeyRef: { + name: string + key: string + } + } + }] + cpu?: string + addRevisionLabel: *false | bool +} +` + +var workerV1Template = `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: { + count: *1 | int + image: string + restart: *"Never" | string + cmd?: [...string] +} +` + +var workerV2Template = `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: { + image: string + cmd?: [...string] +} +` + +var labelV1Template = `patch: { + metadata: labels: { + for k, v in parameter.labels { + "\(k)": v + } + "traitdefinition.oam.dev/version": "v1" + } +} +parameter: { + labels: [string]: string +} +` + +var labelV2Template = `patch: { + metadata: labels: { + for k, v in parameter.labels { + "\(k)": v + } + } +} +parameter: { + labels: [string]: string +} +` + +var KUBEWorkerV1Template = `apiVersion: apps/v1 +kind: Deployment +spec: + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + ports: + - containerPort: 80 +` + +var KUBEWorkerV2Template = `apiVersion: "batch/v1" +kind: "Job" +spec: + parallelism: 1 + completions: 1 + template: + spec: + restartPolicy: "Never" + containers: + - name: "job" + image: "busybox" + command: + - "sleep" + - "1000" +`