diff --git a/docs/examples/workflow/rollout/first-scale.yaml b/docs/examples/workflow/rollout/first-scale.yaml new file mode 100644 index 000000000..80a469d2f --- /dev/null +++ b/docs/examples/workflow/rollout/first-scale.yaml @@ -0,0 +1,53 @@ +apiVersion: core.oam.dev/v1beta1 +kind: Application +metadata: + name: rollout-multi-comp + namespace: default +spec: + components: + - name: back-end + externalRevision: backend-v1 + type: webservice + properties: + image: nginx:1.21 + port: 80 + traits: + - type: rollout + properties: + targetSize: 2 + # This means to rollout two more replicas in two batches. + rolloutBatches: + - replicas: 1 + - replicas: 1 + + - name: front-end + externalRevision: front-end-v1 + type: webservice + properties: + image: node:16.7-alpine3.12 + port: 80 + traits: + - type: rollout + properties: + targetSize: 4 + # This means to rollout two more replicas in two batches. + rolloutBatches: + - replicas: 2 + - replicas: 2 + + workflow: + steps: + # rollout back-end component first, workflow will block until rollout succeed + - name: rollout-back-end + type: rollout-wait-succeed + properties: + appName: rollout-multi-comp + component: back-end + + # rollout front-end component + - name: rollout-front-end + type: rollout-wait-succeed + properties: + appName: rollout-multi-comp + component: front-end + diff --git a/docs/examples/workflow/rollout/rollout-wait-succeed.yaml b/docs/examples/workflow/rollout/rollout-wait-succeed.yaml new file mode 100644 index 000000000..084408717 --- /dev/null +++ b/docs/examples/workflow/rollout/rollout-wait-succeed.yaml @@ -0,0 +1,33 @@ +# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file. +# Definition source cue file: vela-templates/definitions/internal/apply-component.cue +apiVersion: core.oam.dev/v1beta1 +kind: WorkflowStepDefinition +metadata: + annotations: + definition.oam.dev/description: Apply components and traits for your workflow steps + name: rollout-wait-succeed + namespace: vela-system +spec: + schematic: + cue: + template: | + import ( + "vela/op" + ) + + // apply components and traits + apply: op.#ApplyComponent & { + component: parameter.component + } + + parameter: { + // +usage=Declare the name of the application + appName: string + // +usage=Declare the name of the component + component: string + } + + wait: op.#ConditionalWait & { + continue: apply.traits["rollout"].value.status.rollingState=="rolloutSucceed" + } + 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 b4e8d23d6..52a490f9d 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go @@ -138,6 +138,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu r.Recorder.Event(app, event.Warning(velatypes.ReasonFailedRender, err)) return r.endWithNegativeCondition(ctx, app, condition.ErrorCondition("Render", err)) } + + handler.handleCheckManageWorkloadTrait(handler.currentAppRev.Spec.TraitDefinitions, comps) + if err := handler.HandleComponentsRevision(ctx, comps); err != nil { klog.ErrorS(err, "Failed to handle compoents revision", "application", klog.KObj(app)) r.Recorder.Event(app, event.Warning(velatypes.ReasonFailedRevision, err)) 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 d3adc7466..fb1deed03 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 @@ -1839,7 +1839,10 @@ spec: } Expect(k8sClient.Create(ctx, app)).Should(BeNil()) appKey := types.NamespacedName{Namespace: ns.Name, Name: app.Name} - testutil.ReconcileOnceAfterFinalizer(reconciler, reconcile.Request{NamespacedName: appKey}) + // first reconcile handle finalizer + testutil.ReconcileOnce(reconciler, reconcile.Request{NamespacedName: appKey}) + // second reconcile apply all resources + testutil.ReconcileOnce(reconciler, reconcile.Request{NamespacedName: appKey}) checkApp := &v1beta1.Application{} Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil()) Expect(checkApp.Status.Phase).Should(BeEquivalentTo(common.ApplicationRunning)) @@ -1848,6 +1851,81 @@ spec: By("verify targetRevision will be filled with real compRev by context.Revision") Expect(checkRollout.Spec.TargetRevisionName).Should(BeEquivalentTo(externalRevision)) }) + + It("Test rollout trait in workflow", func() { + rolloutTdDef, err := yaml.YAMLToJSON([]byte(rolloutTraitDefinition)) + Expect(err).Should(BeNil()) + rolloutTrait := &v1beta1.TraitDefinition{} + Expect(json.Unmarshal([]byte(rolloutTdDef), rolloutTrait)).Should(BeNil()) + + wfStepDef, err := yaml.YAMLToJSON([]byte(applyCompWfStepDefinition)) + Expect(err).Should(BeNil()) + wfStep := &v1beta1.WorkflowStepDefinition{} + Expect(json.Unmarshal([]byte(wfStepDef), wfStep)).Should(BeNil()) + + ns := corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app-with-rollout-workflow", + }, + } + rolloutTrait.SetNamespace(ns.Name) + wfStep.SetNamespace(ns.Name) + Expect(k8sClient.Create(ctx, &ns)).Should(BeNil()) + Expect(k8sClient.Create(ctx, rolloutTrait)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + Expect(k8sClient.Create(ctx, wfStep)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + + app := &v1beta1.Application{ + TypeMeta: metav1.TypeMeta{ + Kind: "Application", + APIVersion: "core.oam.dev/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "app-with-rollout-workflow", + Namespace: ns.Name, + }, + Spec: v1beta1.ApplicationSpec{ + Components: []common.ApplicationComponent{ + { + Name: "myweb1", + Type: "worker", + Properties: runtime.RawExtension{Raw: []byte(`{"cmd":["sleep","1000"],"image":"busybox"}`)}, + Traits: []common.ApplicationTrait{ + { + Type: "rollout", + Properties: runtime.RawExtension{Raw: []byte(`{}`)}, + }, + }, + }, + }, + Workflow: &v1beta1.Workflow{ + Steps: []v1beta1.WorkflowStep{ + { + Name: "apply", + Type: "apply-component", + Properties: runtime.RawExtension{Raw: []byte(`{"component" : "myweb1"}`)}, + }, + }, + }, + }, + } + + Expect(k8sClient.Create(ctx, app)).Should(BeNil()) + appKey := types.NamespacedName{Namespace: ns.Name, Name: app.Name} + testutil.ReconcileOnceAfterFinalizer(reconciler, reconcile.Request{NamespacedName: appKey}) + checkApp := &v1beta1.Application{} + Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil()) + Expect(checkApp.Status.Phase).Should(BeEquivalentTo(common.ApplicationRunning)) + + By("verify workflow apply component had apply rollout") + checkRollout := &stdv1alpha1.Rollout{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "myweb1", Namespace: ns.Name}, checkRollout)).Should(BeNil()) + By("verify targetRevision will be filled with real compRev by context.ComponentRevName") + Expect(checkRollout.Spec.TargetRevisionName).Should(BeEquivalentTo("myweb1-v1")) + + By("verify workflow apply component didn't apply workload") + deploy := &v1.Deployment{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "myweb1", Namespace: ns.Name}, deploy)).Should(util.NotFoundMatcher{}) + }) }) const ( @@ -2732,6 +2810,31 @@ spec: parameter: { targetRevision: *context.revision|string } +` + applyCompWfStepDefinition = ` +apiVersion: core.oam.dev/v1beta1 +kind: WorkflowStepDefinition +metadata: + annotations: + definition.oam.dev/description: Apply components and traits for your workflow steps + name: apply-component + namespace: vela-system +spec: + schematic: + cue: + template: | + import ( + "vela/op" + ) + + // apply components and traits + apply: op.#ApplyComponent & { + component: parameter.component + } + parameter: { + // +usage=Declare the name of the component + component: string + } ` ) diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/apply.go b/pkg/controller/core.oam.dev/v1alpha2/application/apply.go index 6b08c82bb..74ff95e3b 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/apply.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/apply.go @@ -235,6 +235,26 @@ func (h *AppHandler) aggregateHealthStatus(appFile *appfile.Appfile) ([]common.A return appStatus, healthy, nil } +func (h *AppHandler) handleCheckManageWorkloadTrait(traitDefs map[string]v1beta1.TraitDefinition, comps []*types.ComponentManifest) { + manageWorkloadTrait := map[string]bool{} + for traitName, definition := range traitDefs { + if definition.Spec.ManageWorkload { + manageWorkloadTrait[traitName] = true + } + } + if len(manageWorkloadTrait) == 0 { + return + } + for _, comp := range comps { + for _, trait := range comp.Traits { + traitType := trait.GetLabels()[oam.TraitTypeLabel] + if manageWorkloadTrait[traitType] { + trait.SetLabels(oamutil.MergeMapOverrideWithDst(trait.GetLabels(), map[string]string{oam.LabelManageWorkloadTrait: "true"})) + } + } + } +} + func generateScopeReference(scopes []appfile.Scope) []corev1.ObjectReference { var references []corev1.ObjectReference for _, scope := range scopes { diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/apply_test.go b/pkg/controller/core.oam.dev/v1alpha2/application/apply_test.go index 22d1fd9af..dd7d23432 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/apply_test.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/apply_test.go @@ -30,6 +30,7 @@ import ( . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" @@ -40,6 +41,7 @@ import ( "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" velatypes "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/appfile" + "github.com/oam-dev/kubevela/pkg/oam" ) const workloadDefinition = ` @@ -215,3 +217,37 @@ var _ = Describe("Test statusAggregate", func() { Expect(err).Should(BeNil()) }) }) + +var _ = Describe("Test handleCheckManageWorkloadTrait func", func() { + It("Test every situation", func() { + traitDefs := map[string]v1beta1.TraitDefinition{ + "rollout": v1beta1.TraitDefinition{ + Spec: v1beta1.TraitDefinitionSpec{ + ManageWorkload: true, + }, + }, + "normal": v1beta1.TraitDefinition{ + Spec: v1beta1.TraitDefinitionSpec{}, + }, + } + rolloutTrait := &unstructured.Unstructured{} + rolloutTrait.SetLabels(map[string]string{oam.TraitTypeLabel: "rollout"}) + + normalTrait := &unstructured.Unstructured{} + normalTrait.SetLabels(map[string]string{oam.TraitTypeLabel: "normal"}) + comps := []*velatypes.ComponentManifest{ + { + Traits: []*unstructured.Unstructured{ + rolloutTrait, + normalTrait, + }, + }, + } + h := AppHandler{} + h.handleCheckManageWorkloadTrait(traitDefs, comps) + Expect(len(rolloutTrait.GetLabels())).Should(BeEquivalentTo(2)) + Expect(rolloutTrait.GetLabels()[oam.LabelManageWorkloadTrait]).Should(BeEquivalentTo("true")) + Expect(len(normalTrait.GetLabels())).Should(BeEquivalentTo(1)) + Expect(normalTrait.GetLabels()[oam.LabelManageWorkloadTrait]).Should(BeEquivalentTo("")) + }) +}) diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/assemble/assemble.go b/pkg/controller/core.oam.dev/v1alpha2/application/assemble/assemble.go index c1af7515e..ffd8031a8 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/assemble/assemble.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/assemble/assemble.go @@ -108,19 +108,22 @@ func (am *AppManifests) AssembledManifests() ([]*unstructured.Unstructured, erro if am.err != nil { return nil, am.err } - am.CheckSkipApplyWorkloadComp() r := make([]*unstructured.Unstructured, 0) for compName, wl := range am.assembledWorkloads { - if !am.skipWorkloadApplyComp[compName] { + skipApplyWorkload := false + ts := am.assembledTraits[compName] + for _, t := range ts { + r = append(r, t.DeepCopy()) + if v := t.GetLabels()[oam.LabelManageWorkloadTrait]; v == "true" { + skipApplyWorkload = true + } + } + if !skipApplyWorkload { r = append(r, wl.DeepCopy()) } else { klog.InfoS("assemble meet a managedByTrait workload, so skip apply it", "namespace", am.AppRevision.Namespace, "appRev", am.AppRevision.Name) } - ts := am.assembledTraits[compName] - for _, t := range ts { - r = append(r, t.DeepCopy()) - } } return r, nil } @@ -414,25 +417,3 @@ func (am *AppManifests) setWorkloadRefToTrait(wlRef corev1.ObjectReference, trai } return nil } - -// CheckSkipApplyWorkloadComp check component's workload is manage by trait, if yes skip apply it -func (am *AppManifests) CheckSkipApplyWorkloadComp() { - app := am.AppRevision.Spec.Application - traitDefs := am.AppRevision.Spec.TraitDefinitions - manageWorkloadTrait := map[string]bool{} - for traitName, definition := range traitDefs { - if definition.Spec.ManageWorkload { - manageWorkloadTrait[traitName] = true - } - } - if len(manageWorkloadTrait) == 0 { - return - } - for _, component := range app.Spec.Components { - for _, trait := range component.Traits { - if manageWorkloadTrait[trait.Type] { - am.skipWorkloadApplyComp[component.Name] = true - } - } - } -} diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/assemble/assemble_suite_test.go b/pkg/controller/core.oam.dev/v1alpha2/application/assemble/assemble_suite_test.go index 3c50c0c62..9376317c6 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/assemble/assemble_suite_test.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/assemble/assemble_suite_test.go @@ -26,7 +26,6 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/yaml" - "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/pkg/oam" ) @@ -207,58 +206,3 @@ var _ = Describe("Test Assemble Options", func() { Expect(annotationKeys).Should(ContainElements("canPassAnno")) }) }) - -var _ = Describe("Test CheckSkipApplyWorkloadComp func", func() { - It("Test every situation", func() { - a := AppManifests{ - AppRevision: &v1beta1.ApplicationRevision{ - Spec: v1beta1.ApplicationRevisionSpec{ - Application: v1beta1.Application{ - Spec: v1beta1.ApplicationSpec{ - Components: []common.ApplicationComponent{ - { - Name: "comp1", - Traits: []common.ApplicationTrait{ - { - Type: "rollout", - }, - { - Type: "normal", - }, - }, - }, - { - Name: "comp2", - Traits: []common.ApplicationTrait{ - { - Type: "normal", - }, - }, - }, - { - Name: "comp3", - }, - }, - }, - }, - TraitDefinitions: map[string]v1beta1.TraitDefinition{ - "rollout": v1beta1.TraitDefinition{ - Spec: v1beta1.TraitDefinitionSpec{ - ManageWorkload: true, - }, - }, - "normal": v1beta1.TraitDefinition{ - Spec: v1beta1.TraitDefinitionSpec{}, - }, - }, - }, - }, - skipWorkloadApplyComp: make(map[string]bool), - } - a.CheckSkipApplyWorkloadComp() - Expect(len(a.skipWorkloadApplyComp)).Should(BeEquivalentTo(1)) - Expect(a.skipWorkloadApplyComp["comp1"]).Should(BeTrue()) - Expect(a.skipWorkloadApplyComp["comp2"]).Should(BeFalse()) - Expect(a.skipWorkloadApplyComp["comp3"]).Should(BeFalse()) - }) -}) diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/revision.go b/pkg/controller/core.oam.dev/v1alpha2/application/revision.go index 1301b3fd4..864c186af 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/revision.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/revision.go @@ -395,6 +395,14 @@ func (h *AppHandler) handleComponentRevisionNameSpecified(ctx context.Context, c if err := h.createControllerRevision(ctx, comp); err != nil { return err } + + // when controllerRevision not exist handle replace context.RevisionName + for _, trait := range comp.Traits { + if err := replaceComponentRevisionContext(trait, comp.RevisionName); err != nil { + return err + } + } + return nil } diff --git a/pkg/oam/labels.go b/pkg/oam/labels.go index 2ecfd6eb2..5539c9dce 100644 --- a/pkg/oam/labels.go +++ b/pkg/oam/labels.go @@ -47,6 +47,8 @@ const ( LabelComponentDefinitionName = "componentdefinition.oam.dev/name" // LabelTraitDefinitionName records the name of TraitDefinition LabelTraitDefinitionName = "trait.oam.dev/name" + // LabelManageWorkloadTrait indicates if the trait will manage the lifecycle of the workload + LabelManageWorkloadTrait = "trait.oam.dev/manage-workload" // LabelPolicyDefinitionName records the name of PolicyDefinition LabelPolicyDefinitionName = "policydefinition.oam.dev/name" // LabelWorkflowStepDefinitionName records the name of WorkflowStepDefinition diff --git a/pkg/stdlib/op.cue b/pkg/stdlib/op.cue index ead27f051..35da8eb98 100644 --- a/pkg/stdlib/op.cue +++ b/pkg/stdlib/op.cue @@ -16,26 +16,36 @@ import ( #Apply: kube.#Apply #ApplyComponent: #Steps & { - component: string - _componentName: component - load: ws.#Load & { - component: _componentName - } @step(1) - workload: workload__.value - workload__: kube.#Apply & { - value: load.value.workload - ... - } @step(2) + component: string + _componentName: component + load: ws.#Load & { + component: _componentName + } @step(1) + + traits: #Steps & { + _key: "trait.oam.dev/resource" + _manWlKey: "trait.oam.dev/manage-workload" + skipApplyWorkload: *false | bool + if load.value.auxiliaries != _|_ { + for o in load.value.auxiliaries { + "\(o.metadata.labels[_key])": kube.#Apply & {value: o} + if o.metadata.labels[_manWlKey] != _|_ { + skipApplyWorkload: true + } + } + } + } @step(2) + + workload__: { + if !traits.skipApplyWorkload { + kube.#Apply & { + value: load.value.workload + ... + } + } + } @step(3) - traits: #Steps & { - _key: "trait.oam.dev/resource" - if load.value.auxiliaries != _|_ { - for o in load.value.auxiliaries { - "\(o.metadata.labels[_key])": kube.#Apply & {value: o} - } - } - } @step(3) } #ApplyRemaining: #Steps & {