diff --git a/.github/actions/deploy-keptn-on-cluster/values.yaml b/.github/actions/deploy-keptn-on-cluster/values.yaml index 5293db1cb2..77aa11988e 100644 --- a/.github/actions/deploy-keptn-on-cluster/values.yaml +++ b/.github/actions/deploy-keptn-on-cluster/values.yaml @@ -11,6 +11,7 @@ metricsOperator: tag: $TAG lifecycleOperator: + promotionTasksEnabled: true lifecycleOperator: imagePullPolicy: Never image: diff --git a/lifecycle-operator/apis/lifecycle/v1beta1/common/common.go b/lifecycle-operator/apis/lifecycle/v1beta1/common/common.go index d94ca7ce2a..3939a989a8 100644 --- a/lifecycle-operator/apis/lifecycle/v1beta1/common/common.go +++ b/lifecycle-operator/apis/lifecycle/v1beta1/common/common.go @@ -144,6 +144,7 @@ type CheckType string const PreDeploymentCheckType CheckType = "pre" const PostDeploymentCheckType CheckType = "post" +const PromotionCheckType CheckType = "promotion" const PreDeploymentEvaluationCheckType CheckType = "pre-eval" const PostDeploymentEvaluationCheckType CheckType = "post-eval" diff --git a/lifecycle-operator/apis/lifecycle/v1beta1/keptnworkloadversion_types.go b/lifecycle-operator/apis/lifecycle/v1beta1/keptnworkloadversion_types.go index ef554bf93c..56a8c99337 100644 --- a/lifecycle-operator/apis/lifecycle/v1beta1/keptnworkloadversion_types.go +++ b/lifecycle-operator/apis/lifecycle/v1beta1/keptnworkloadversion_types.go @@ -337,6 +337,16 @@ func (w KeptnWorkloadVersion) GetPostDeploymentEvaluationTaskStatus() []ItemStat return w.Status.PostDeploymentEvaluationTaskStatus } +func (w KeptnWorkloadVersion) GetPromotionTasks() []string { + // promotion tasks are not included in Workloads, but we need the implementation of this method to fulfil the PhaseItem interface + return []string{} +} + +func (w KeptnWorkloadVersion) GetPromotionTaskStatus() []ItemStatus { + // promotion tasks are not included in Workloads, but we need the implementation of this method to fulfil the PhaseItem interface + return []ItemStatus{} +} + func (w KeptnWorkloadVersion) GetAppName() string { return w.Spec.AppName } diff --git a/lifecycle-operator/apis/lifecycle/v1beta1/keptnworkloadversion_types_test.go b/lifecycle-operator/apis/lifecycle/v1beta1/keptnworkloadversion_types_test.go index 8688babd1a..715af0f2b5 100644 --- a/lifecycle-operator/apis/lifecycle/v1beta1/keptnworkloadversion_types_test.go +++ b/lifecycle-operator/apis/lifecycle/v1beta1/keptnworkloadversion_types_test.go @@ -259,6 +259,16 @@ func TestKeptnWorkloadVersion(t *testing.T) { "workloadVersion": "version", "workloadVersionName": "workload", }, workload.GetEventAnnotations()) + + require.Equal(t, + []string{}, + workload.GetPromotionTasks(), + ) + + require.Equal(t, + []ItemStatus{}, + workload.GetPromotionTaskStatus(), + ) } //nolint:dupl diff --git a/lifecycle-operator/controllers/common/task/handler.go b/lifecycle-operator/controllers/common/task/handler.go index 29e6ebabba..e700eb86e7 100644 --- a/lifecycle-operator/controllers/common/task/handler.go +++ b/lifecycle-operator/controllers/common/task/handler.go @@ -179,6 +179,9 @@ func (r Handler) setupTasks(taskCreateAttributes CreateTaskAttributes, piWrapper case apicommon.PostDeploymentCheckType: tasks = piWrapper.GetPostDeploymentTasks() statuses = piWrapper.GetPostDeploymentTaskStatus() + case apicommon.PromotionCheckType: + tasks = piWrapper.GetPromotionTasks() + statuses = piWrapper.GetPromotionTaskStatus() } return tasks, statuses } diff --git a/lifecycle-operator/controllers/common/task/handler_test.go b/lifecycle-operator/controllers/common/task/handler_test.go index a361951c8c..f22114b624 100644 --- a/lifecycle-operator/controllers/common/task/handler_test.go +++ b/lifecycle-operator/controllers/common/task/handler_test.go @@ -376,6 +376,60 @@ func TestTaskHandler(t *testing.T) { getSpanCalls: 1, unbindSpanCalls: 1, }, + { + name: "succeeded promotion task", + object: &v1beta1.KeptnAppVersion{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "namespace", + }, + Spec: v1beta1.KeptnAppVersionSpec{ + KeptnAppContextSpec: v1beta1.KeptnAppContextSpec{ + DeploymentTaskSpec: v1beta1.DeploymentTaskSpec{ + PromotionTasks: []string{"task-def"}, + }, + }, + }, + Status: v1beta1.KeptnAppVersionStatus{ + PromotionStatus: apicommon.StateSucceeded, + PromotionTaskStatus: []v1beta1.ItemStatus{ + { + DefinitionName: "task-def", + Status: apicommon.StateProgressing, + Name: "prom-task-def-", + }, + }, + }, + }, + taskObj: v1beta1.KeptnTask{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "namespace", + Name: "prom-task-def-", + }, + Status: v1beta1.KeptnTaskStatus{ + Status: apicommon.StateSucceeded, + }, + }, + createAttr: CreateTaskAttributes{ + SpanName: "", + Definition: v1beta1.KeptnTaskDefinition{ + ObjectMeta: v1.ObjectMeta{ + Name: "task-def", + }, + }, + CheckType: apicommon.PromotionCheckType, + }, + wantStatus: []v1beta1.ItemStatus{ + { + DefinitionName: "task-def", + Status: apicommon.StateSucceeded, + Name: "prom-task-def-", + }, + }, + wantSummary: apicommon.StatusSummary{Total: 1, Succeeded: 1}, + wantErr: nil, + getSpanCalls: 1, + unbindSpanCalls: 1, + }, } config.Instance().SetDefaultNamespace(testcommon.KeptnNamespace) diff --git a/lifecycle-operator/controllers/lifecycle/interfaces/fake/phaseitem_mock.go b/lifecycle-operator/controllers/lifecycle/interfaces/fake/phaseitem_mock.go index 33d0e2a175..eaef9bc740 100644 --- a/lifecycle-operator/controllers/lifecycle/interfaces/fake/phaseitem_mock.go +++ b/lifecycle-operator/controllers/lifecycle/interfaces/fake/phaseitem_mock.go @@ -72,6 +72,12 @@ import ( // GetPreviousVersionFunc: func() string { // panic("mock out the GetPreviousVersion method") // }, +// GetPromotionTaskStatusFunc: func() []klcv1beta1.ItemStatus { +// panic("mock out the GetPromotionTaskStatus method") +// }, +// GetPromotionTasksFunc: func() []string { +// panic("mock out the GetPromotionTasks method") +// }, // GetSpanAttributesFunc: func() []attribute.KeyValue { // panic("mock out the GetSpanAttributes method") // }, @@ -157,6 +163,12 @@ type PhaseItemMock struct { // GetPreviousVersionFunc mocks the GetPreviousVersion method. GetPreviousVersionFunc func() string + // GetPromotionTaskStatusFunc mocks the GetPromotionTaskStatus method. + GetPromotionTaskStatusFunc func() []klcv1beta1.ItemStatus + + // GetPromotionTasksFunc mocks the GetPromotionTasks method. + GetPromotionTasksFunc func() []string + // GetSpanAttributesFunc mocks the GetSpanAttributes method. GetSpanAttributesFunc func() []attribute.KeyValue @@ -247,6 +259,12 @@ type PhaseItemMock struct { // GetPreviousVersion holds details about calls to the GetPreviousVersion method. GetPreviousVersion []struct { } + // GetPromotionTaskStatus holds details about calls to the GetPromotionTaskStatus method. + GetPromotionTaskStatus []struct { + } + // GetPromotionTasks holds details about calls to the GetPromotionTasks method. + GetPromotionTasks []struct { + } // GetSpanAttributes holds details about calls to the GetSpanAttributes method. GetSpanAttributes []struct { } @@ -296,6 +314,8 @@ type PhaseItemMock struct { lockGetPreDeploymentTaskStatus sync.RWMutex lockGetPreDeploymentTasks sync.RWMutex lockGetPreviousVersion sync.RWMutex + lockGetPromotionTaskStatus sync.RWMutex + lockGetPromotionTasks sync.RWMutex lockGetSpanAttributes sync.RWMutex lockGetStartTime sync.RWMutex lockGetState sync.RWMutex @@ -815,6 +835,60 @@ func (mock *PhaseItemMock) GetPreviousVersionCalls() []struct { return calls } +// GetPromotionTaskStatus calls GetPromotionTaskStatusFunc. +func (mock *PhaseItemMock) GetPromotionTaskStatus() []klcv1beta1.ItemStatus { + if mock.GetPromotionTaskStatusFunc == nil { + panic("PhaseItemMock.GetPromotionTaskStatusFunc: method is nil but PhaseItem.GetPromotionTaskStatus was just called") + } + callInfo := struct { + }{} + mock.lockGetPromotionTaskStatus.Lock() + mock.calls.GetPromotionTaskStatus = append(mock.calls.GetPromotionTaskStatus, callInfo) + mock.lockGetPromotionTaskStatus.Unlock() + return mock.GetPromotionTaskStatusFunc() +} + +// GetPromotionTaskStatusCalls gets all the calls that were made to GetPromotionTaskStatus. +// Check the length with: +// +// len(mockedPhaseItem.GetPromotionTaskStatusCalls()) +func (mock *PhaseItemMock) GetPromotionTaskStatusCalls() []struct { +} { + var calls []struct { + } + mock.lockGetPromotionTaskStatus.RLock() + calls = mock.calls.GetPromotionTaskStatus + mock.lockGetPromotionTaskStatus.RUnlock() + return calls +} + +// GetPromotionTasks calls GetPromotionTasksFunc. +func (mock *PhaseItemMock) GetPromotionTasks() []string { + if mock.GetPromotionTasksFunc == nil { + panic("PhaseItemMock.GetPromotionTasksFunc: method is nil but PhaseItem.GetPromotionTasks was just called") + } + callInfo := struct { + }{} + mock.lockGetPromotionTasks.Lock() + mock.calls.GetPromotionTasks = append(mock.calls.GetPromotionTasks, callInfo) + mock.lockGetPromotionTasks.Unlock() + return mock.GetPromotionTasksFunc() +} + +// GetPromotionTasksCalls gets all the calls that were made to GetPromotionTasks. +// Check the length with: +// +// len(mockedPhaseItem.GetPromotionTasksCalls()) +func (mock *PhaseItemMock) GetPromotionTasksCalls() []struct { +} { + var calls []struct { + } + mock.lockGetPromotionTasks.RLock() + calls = mock.calls.GetPromotionTasks + mock.lockGetPromotionTasks.RUnlock() + return calls +} + // GetSpanAttributes calls GetSpanAttributesFunc. func (mock *PhaseItemMock) GetSpanAttributes() []attribute.KeyValue { if mock.GetSpanAttributesFunc == nil { diff --git a/lifecycle-operator/controllers/lifecycle/interfaces/phaseitem.go b/lifecycle-operator/controllers/lifecycle/interfaces/phaseitem.go index 14578f09c1..49028c83ff 100644 --- a/lifecycle-operator/controllers/lifecycle/interfaces/phaseitem.go +++ b/lifecycle-operator/controllers/lifecycle/interfaces/phaseitem.go @@ -30,8 +30,10 @@ type PhaseItem interface { GetAppName() string GetPreDeploymentTasks() []string GetPostDeploymentTasks() []string + GetPromotionTasks() []string GetPreDeploymentTaskStatus() []klcv1beta1.ItemStatus GetPostDeploymentTaskStatus() []klcv1beta1.ItemStatus + GetPromotionTaskStatus() []klcv1beta1.ItemStatus GetPreDeploymentEvaluations() []string GetPostDeploymentEvaluations() []string GetPreDeploymentEvaluationTaskStatus() []klcv1beta1.ItemStatus @@ -158,3 +160,11 @@ func (pw PhaseItemWrapper) GetSpanAttributes() []attribute.KeyValue { func (pw PhaseItemWrapper) DeprecateRemainingPhases(phase apicommon.KeptnPhaseType) { pw.Obj.DeprecateRemainingPhases(phase) } + +func (pw PhaseItemWrapper) GetPromotionTasks() []string { + return pw.Obj.GetPromotionTasks() +} + +func (pw PhaseItemWrapper) GetPromotionTaskStatus() []klcv1beta1.ItemStatus { + return pw.Obj.GetPromotionTaskStatus() +} diff --git a/lifecycle-operator/controllers/lifecycle/interfaces/phaseitem_test.go b/lifecycle-operator/controllers/lifecycle/interfaces/phaseitem_test.go index b1dd6ef9d9..b51e5e7a46 100644 --- a/lifecycle-operator/controllers/lifecycle/interfaces/phaseitem_test.go +++ b/lifecycle-operator/controllers/lifecycle/interfaces/phaseitem_test.go @@ -95,6 +95,12 @@ func TestPhaseItem(t *testing.T) { GetPostDeploymentEvaluationTaskStatusFunc: func() []v1beta1.ItemStatus { return nil }, + GetPromotionTasksFunc: func() []string { + return []string{} + }, + GetPromotionTaskStatusFunc: func() []v1beta1.ItemStatus { + return []v1beta1.ItemStatus{} + }, GenerateTaskFunc: func(taskDefinition v1beta1.KeptnTaskDefinition, checkType apicommon.CheckType) v1beta1.KeptnTask { return v1beta1.KeptnTask{} }, @@ -187,4 +193,10 @@ func TestPhaseItem(t *testing.T) { wrapper.DeprecateRemainingPhases(apicommon.PhaseAppDeployment) require.Len(t, phaseItemMock.DeprecateRemainingPhasesCalls(), 1) + _ = wrapper.GetPromotionTaskStatus() + require.Len(t, phaseItemMock.GetPromotionTaskStatusCalls(), 1) + + _ = wrapper.GetPromotionTasks() + require.Len(t, phaseItemMock.GetPromotionTasksCalls(), 1) + } diff --git a/lifecycle-operator/controllers/lifecycle/keptnappversion/controller.go b/lifecycle-operator/controllers/lifecycle/keptnappversion/controller.go index 8cc3c102a1..1e2b80ee31 100644 --- a/lifecycle-operator/controllers/lifecycle/keptnappversion/controller.go +++ b/lifecycle-operator/controllers/lifecycle/keptnappversion/controller.go @@ -52,13 +52,14 @@ const traceComponentName = "keptn/lifecycle-operator/appversion" type KeptnAppVersionReconciler struct { Scheme *runtime.Scheme client.Client - Log logr.Logger - EventSender eventsender.IEvent - TracerFactory telemetry.TracerFactory - Meters apicommon.KeptnMeters - SpanHandler telemetry.ISpanHandler - EvaluationHandler evaluation.IEvaluationHandler - PhaseHandler phase.IHandler + Log logr.Logger + EventSender eventsender.IEvent + TracerFactory telemetry.TracerFactory + Meters apicommon.KeptnMeters + SpanHandler telemetry.ISpanHandler + EvaluationHandler evaluation.IEvaluationHandler + PhaseHandler phase.IHandler + PromotionTasksEnabled bool } // +kubebuilder:rbac:groups=lifecycle.keptn.sh,resources=keptnappversions,verbs=get;list;watch;create;update;patch;delete @@ -76,7 +77,7 @@ type KeptnAppVersionReconciler struct { // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.13.0/pkg/reconcile // -//nolint:gocyclo +//nolint:gocyclo,gocognit func (r *KeptnAppVersionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { requestInfo := controllercommon.GetRequestInfo(req) r.Log.Info("Searching for Keptn App Version", "requestInfo", requestInfo) @@ -114,7 +115,7 @@ func (r *KeptnAppVersionReconciler) Reconcile(ctx context.Context, req ctrl.Requ if !appVersion.IsPreDeploymentSucceeded() { reconcilePreDep := func(phaseCtx context.Context) (apicommon.KeptnState, error) { - return r.reconcilePrePostDeployment(ctx, phaseCtx, appVersion, apicommon.PreDeploymentCheckType) + return r.reconcilePhase(ctx, phaseCtx, appVersion, apicommon.PreDeploymentCheckType) } result, err := r.PhaseHandler.HandlePhase(ctx, ctxAppTrace, r.getTracer(), appVersion, currentPhase, reconcilePreDep) if !result.Continue { @@ -147,7 +148,7 @@ func (r *KeptnAppVersionReconciler) Reconcile(ctx context.Context, req ctrl.Requ currentPhase = apicommon.PhaseAppPostDeployment if !appVersion.IsPostDeploymentSucceeded() { reconcilePostDep := func(phaseCtx context.Context) (apicommon.KeptnState, error) { - return r.reconcilePrePostDeployment(ctx, phaseCtx, appVersion, apicommon.PostDeploymentCheckType) + return r.reconcilePhase(ctx, phaseCtx, appVersion, apicommon.PostDeploymentCheckType) } result, err := r.PhaseHandler.HandlePhase(ctx, ctxAppTrace, r.getTracer(), appVersion, currentPhase, reconcilePostDep) if !result.Continue { @@ -166,6 +167,17 @@ func (r *KeptnAppVersionReconciler) Reconcile(ctx context.Context, req ctrl.Requ } } + if r.PromotionTasksEnabled && !appVersion.IsPromotionCompleted() { + currentPhase = apicommon.PhasePromotion + reconcilePromotionFunc := func(phaseCtx context.Context) (apicommon.KeptnState, error) { + return r.reconcilePhase(ctx, phaseCtx, appVersion, apicommon.PromotionCheckType) + } + result, err := r.PhaseHandler.HandlePhase(ctx, ctxAppTrace, r.getTracer(), appVersion, currentPhase, reconcilePromotionFunc) + if !result.Continue { + return result.Result, err + } + } + // AppVersion is completed at this place return r.finishKeptnAppVersionReconcile(ctx, appVersion, spanAppTrace) } diff --git a/lifecycle-operator/controllers/lifecycle/keptnappversion/controller_test.go b/lifecycle-operator/controllers/lifecycle/keptnappversion/controller_test.go index 676704d989..3fdbb71cf9 100644 --- a/lifecycle-operator/controllers/lifecycle/keptnappversion/controller_test.go +++ b/lifecycle-operator/controllers/lifecycle/keptnappversion/controller_test.go @@ -2,6 +2,7 @@ package keptnappversion import ( "context" + "errors" "fmt" "reflect" "strings" @@ -181,6 +182,8 @@ func TestKeptnAppVersionReconciler_ReconcileReachCompletion(t *testing.T) { app := testcommon.ReturnAppVersion("default", "myfinishedapp", "1.0.0", nil, createFinishedAppVersionStatus()) r, eventChannel, _ := setupReconciler(app) + + r.PromotionTasksEnabled = true req := ctrl.Request{ NamespacedName: types.NamespacedName{ Namespace: "default", @@ -219,6 +222,151 @@ func TestKeptnAppVersionReconciler_ReconcileReachCompletion(t *testing.T) { require.False(t, result.Requeue) } +func TestKeptnAppVersionReconciler_ReconcilePromotionPhase(t *testing.T) { + + appVersionStatus := lfcv1beta1.KeptnAppVersionStatus{ + CurrentPhase: apicommon.PhaseCompleted.ShortName, + PreDeploymentStatus: apicommon.StateSucceeded, + PostDeploymentStatus: apicommon.StateSucceeded, + PreDeploymentEvaluationStatus: apicommon.StateSucceeded, + PostDeploymentEvaluationStatus: apicommon.StateSucceeded, + PromotionStatus: apicommon.StatePending, + PreDeploymentTaskStatus: []lfcv1beta1.ItemStatus{{Status: apicommon.StateSucceeded}}, + PostDeploymentTaskStatus: []lfcv1beta1.ItemStatus{{Status: apicommon.StateSucceeded}}, + PreDeploymentEvaluationTaskStatus: []lfcv1beta1.ItemStatus{{Status: apicommon.StateSucceeded}}, + PostDeploymentEvaluationTaskStatus: []lfcv1beta1.ItemStatus{{Status: apicommon.StateSucceeded}}, + WorkloadOverallStatus: apicommon.StateSucceeded, + WorkloadStatus: []lfcv1beta1.WorkloadStatus{{Status: apicommon.StateSucceeded}}, + Status: apicommon.StateSucceeded, + } + + appVersionName := fmt.Sprintf("%s-%s", "myapp", "1.0.0") + appVersion := &lfcv1beta1.KeptnAppVersion{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: appVersionName, + Namespace: "default", + Generation: 1, + }, + Spec: lfcv1beta1.KeptnAppVersionSpec{ + KeptnAppSpec: lfcv1beta1.KeptnAppSpec{ + Version: "1.0.0", + }, + KeptnAppContextSpec: lfcv1beta1.KeptnAppContextSpec{ + DeploymentTaskSpec: lfcv1beta1.DeploymentTaskSpec{ + PromotionTasks: []string{"my-promotion-task"}, + }, + }, + AppName: "myapp", + }, + Status: appVersionStatus, + } + + r, eventChannel, _ := setupReconciler(appVersion) + + mockPhaseHandler := &phasefake.MockHandler{HandlePhaseFunc: func(ctx context.Context, ctxTrace context.Context, tracer telemetry.ITracer, reconcileObject client.Object, phaseMoqParam apicommon.KeptnPhaseType, reconcilePhase func(phaseCtx context.Context) (apicommon.KeptnState, error)) (phase.PhaseResult, error) { + return phase.PhaseResult{Continue: true, Result: ctrl.Result{}}, nil + }} + r.PhaseHandler = mockPhaseHandler + + r.PromotionTasksEnabled = true + req := ctrl.Request{ + NamespacedName: types.NamespacedName{ + Namespace: "default", + Name: "myapp-1.0.0", + }, + } + + result, err := r.Reconcile(context.WithValue(context.TODO(), CONTEXTID, req.Name), req) + require.Nil(t, err) + + expectedEvents := []string{ + "CompletedFinished", + } + + for _, e := range expectedEvents { + event := <-eventChannel + require.Equal(t, strings.Contains(event, req.Name), true, "wrong appversion") + require.Equal(t, strings.Contains(event, req.Namespace), true, "wrong namespace") + require.Equal(t, strings.Contains(event, e), true, fmt.Sprintf("no %s found in %s", e, event)) + } + + require.Nil(t, err) + + // do not requeue since we reached completion + require.False(t, result.Requeue) + + // verify that the phase handler was invoked for the promotion phase + require.Len(t, mockPhaseHandler.HandlePhaseCalls(), 1) + require.Equal(t, apicommon.PhasePromotion, mockPhaseHandler.HandlePhaseCalls()[0].PhaseMoqParam) +} + +func TestKeptnAppVersionReconciler_ReconcilePromotionPhaseFails(t *testing.T) { + + appVersionStatus := lfcv1beta1.KeptnAppVersionStatus{ + CurrentPhase: apicommon.PhaseCompleted.ShortName, + PreDeploymentStatus: apicommon.StateSucceeded, + PostDeploymentStatus: apicommon.StateSucceeded, + PreDeploymentEvaluationStatus: apicommon.StateSucceeded, + PostDeploymentEvaluationStatus: apicommon.StateSucceeded, + PromotionStatus: apicommon.StatePending, + PreDeploymentTaskStatus: []lfcv1beta1.ItemStatus{{Status: apicommon.StateSucceeded}}, + PostDeploymentTaskStatus: []lfcv1beta1.ItemStatus{{Status: apicommon.StateSucceeded}}, + PreDeploymentEvaluationTaskStatus: []lfcv1beta1.ItemStatus{{Status: apicommon.StateSucceeded}}, + PostDeploymentEvaluationTaskStatus: []lfcv1beta1.ItemStatus{{Status: apicommon.StateSucceeded}}, + WorkloadOverallStatus: apicommon.StateSucceeded, + WorkloadStatus: []lfcv1beta1.WorkloadStatus{{Status: apicommon.StateSucceeded}}, + Status: apicommon.StateSucceeded, + } + + appVersionName := fmt.Sprintf("%s-%s", "myapp", "1.0.0") + appVersion := &lfcv1beta1.KeptnAppVersion{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: appVersionName, + Namespace: "default", + Generation: 1, + }, + Spec: lfcv1beta1.KeptnAppVersionSpec{ + KeptnAppSpec: lfcv1beta1.KeptnAppSpec{ + Version: "1.0.0", + }, + KeptnAppContextSpec: lfcv1beta1.KeptnAppContextSpec{ + DeploymentTaskSpec: lfcv1beta1.DeploymentTaskSpec{ + PromotionTasks: []string{"my-promotion-task"}, + }, + }, + AppName: "myapp", + }, + Status: appVersionStatus, + } + + r, _, _ := setupReconciler(appVersion) + + mockPhaseHandler := &phasefake.MockHandler{HandlePhaseFunc: func(ctx context.Context, ctxTrace context.Context, tracer telemetry.ITracer, reconcileObject client.Object, phaseMoqParam apicommon.KeptnPhaseType, reconcilePhase func(phaseCtx context.Context) (apicommon.KeptnState, error)) (phase.PhaseResult, error) { + return phase.PhaseResult{Continue: false, Result: ctrl.Result{Requeue: true}}, errors.New("unexpected error") + }} + r.PhaseHandler = mockPhaseHandler + + r.PromotionTasksEnabled = true + req := ctrl.Request{ + NamespacedName: types.NamespacedName{ + Namespace: "default", + Name: "myapp-1.0.0", + }, + } + + result, err := r.Reconcile(context.WithValue(context.TODO(), CONTEXTID, req.Name), req) + require.NotNil(t, err) + + // requeue since we could not finish the promotion phase + require.True(t, result.Requeue) + + // verify that the phase handler was invoked for the promotion phase + require.Len(t, mockPhaseHandler.HandlePhaseCalls(), 1) + require.Equal(t, apicommon.PhasePromotion, mockPhaseHandler.HandlePhaseCalls()[0].PhaseMoqParam) +} + func createFinishedAppVersionStatus() lfcv1beta1.KeptnAppVersionStatus { return lfcv1beta1.KeptnAppVersionStatus{ CurrentPhase: apicommon.PhaseCompleted.ShortName, @@ -226,6 +374,7 @@ func createFinishedAppVersionStatus() lfcv1beta1.KeptnAppVersionStatus { PostDeploymentStatus: apicommon.StateSucceeded, PreDeploymentEvaluationStatus: apicommon.StateSucceeded, PostDeploymentEvaluationStatus: apicommon.StateSucceeded, + PromotionStatus: apicommon.StateSucceeded, PreDeploymentTaskStatus: []lfcv1beta1.ItemStatus{{Status: apicommon.StateSucceeded}}, PostDeploymentTaskStatus: []lfcv1beta1.ItemStatus{{Status: apicommon.StateSucceeded}}, PreDeploymentEvaluationTaskStatus: []lfcv1beta1.ItemStatus{{Status: apicommon.StateSucceeded}}, diff --git a/lifecycle-operator/controllers/lifecycle/keptnappversion/reconcile_prepostdeployment.go b/lifecycle-operator/controllers/lifecycle/keptnappversion/reconcile_phase.go similarity index 80% rename from lifecycle-operator/controllers/lifecycle/keptnappversion/reconcile_prepostdeployment.go rename to lifecycle-operator/controllers/lifecycle/keptnappversion/reconcile_phase.go index e0a8eeb115..b45a61b940 100644 --- a/lifecycle-operator/controllers/lifecycle/keptnappversion/reconcile_prepostdeployment.go +++ b/lifecycle-operator/controllers/lifecycle/keptnappversion/reconcile_phase.go @@ -10,7 +10,7 @@ import ( "github.com/keptn/lifecycle-toolkit/lifecycle-operator/controllers/common/task" ) -func (r *KeptnAppVersionReconciler) reconcilePrePostDeployment(ctx context.Context, phaseCtx context.Context, appVersion *klcv1beta1.KeptnAppVersion, checkType apicommon.CheckType) (apicommon.KeptnState, error) { +func (r *KeptnAppVersionReconciler) reconcilePhase(ctx context.Context, phaseCtx context.Context, appVersion *klcv1beta1.KeptnAppVersion, checkType apicommon.CheckType) (apicommon.KeptnState, error) { taskHandler := task.Handler{ Client: r.Client, EventSender: r.EventSender, @@ -38,6 +38,9 @@ func (r *KeptnAppVersionReconciler) reconcilePrePostDeployment(ctx context.Conte case apicommon.PostDeploymentCheckType: appVersion.Status.PostDeploymentStatus = overallState appVersion.Status.PostDeploymentTaskStatus = newStatus + case apicommon.PromotionCheckType: + appVersion.Status.PromotionStatus = overallState + appVersion.Status.PromotionTaskStatus = newStatus } // Write Status Field diff --git a/lifecycle-operator/main.go b/lifecycle-operator/main.go index 56e2afa121..724c296f92 100644 --- a/lifecycle-operator/main.go +++ b/lifecycle-operator/main.go @@ -327,15 +327,16 @@ func main() { spanHandler, ) appVersionReconciler := &keptnappversion.KeptnAppVersionReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Log: appVersionLogger, - EventSender: appVersionEventSender, - TracerFactory: telemetry.GetOtelInstance(), - Meters: keptnMeters, - SpanHandler: spanHandler, - EvaluationHandler: appVersionEvaluationHandler, - PhaseHandler: appVersionPhaseHandler, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Log: appVersionLogger, + EventSender: appVersionEventSender, + TracerFactory: telemetry.GetOtelInstance(), + Meters: keptnMeters, + SpanHandler: spanHandler, + EvaluationHandler: appVersionEvaluationHandler, + PhaseHandler: appVersionPhaseHandler, + PromotionTasksEnabled: env.PromotionTasksEnabled, } if err = (appVersionReconciler).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "KeptnAppVersion") diff --git a/test/chainsaw/integration/podtato-head-application/00-assert.yaml b/test/chainsaw/integration/podtato-head-application/00-assert.yaml index b10a425345..7928aae2a4 100644 --- a/test/chainsaw/integration/podtato-head-application/00-assert.yaml +++ b/test/chainsaw/integration/podtato-head-application/00-assert.yaml @@ -16,6 +16,10 @@ status: postDeploymentStatus: Succeeded preDeploymentEvaluationStatus: Succeeded preDeploymentStatus: Succeeded + promotionStatus: Succeeded + promotionTaskStatus: + - status: Succeeded + definitionName: post-deployment-hello status: Succeeded workloadOverallStatus: Succeeded --- diff --git a/test/chainsaw/integration/podtato-head-application/00-install.yaml b/test/chainsaw/integration/podtato-head-application/00-install.yaml index 338b637c3e..3ef76c52e4 100644 --- a/test/chainsaw/integration/podtato-head-application/00-install.yaml +++ b/test/chainsaw/integration/podtato-head-application/00-install.yaml @@ -13,6 +13,8 @@ spec: - post-deployment-hello postDeploymentEvaluations: - available-cpus + promotionTasks: + - post-deployment-hello --- apiVersion: apps/v1 kind: Deployment