From b332950936c821d491f84c7df128bc361bca3fa9 Mon Sep 17 00:00:00 2001 From: sean conroy Date: Fri, 20 Sep 2024 13:19:51 +0100 Subject: [PATCH] Draft: feat(RELEASE-1053): add support for final pipeline --- api/v1alpha1/release_conditions.go | 6 +- api/v1alpha1/release_types.go | 144 +++-- api/v1alpha1/release_types_test.go | 424 +++++++------- api/v1alpha1/releaseplan_types.go | 8 +- api/v1alpha1/zz_generated.deepcopy.go | 10 +- .../appstudio.redhat.com_releaseplans.yaml | 81 ++- .../bases/appstudio.redhat.com_releases.yaml | 26 + controllers/release/adapter.go | 193 ++++++- controllers/release/adapter_test.go | 542 +++++++++++++++++- controllers/release/controller.go | 2 + controllers/releaseplan/adapter_test.go | 3 +- loader/loader.go | 2 +- loader/loader_test.go | 23 + metadata/labels.go | 3 + 14 files changed, 1161 insertions(+), 306 deletions(-) diff --git a/api/v1alpha1/release_conditions.go b/api/v1alpha1/release_conditions.go index 2de7f0ea3..b678a1128 100644 --- a/api/v1alpha1/release_conditions.go +++ b/api/v1alpha1/release_conditions.go @@ -3,12 +3,12 @@ package v1alpha1 import "github.com/konflux-ci/operator-toolkit/conditions" const ( + // finalProcessedConditionType is the type used to track the status of a Release Final Pipeline processing + finalProcessedConditionType conditions.ConditionType = "FinalPipelineProcessed" + // managedProcessedConditionType is the type used to track the status of a Release Managed Pipeline processing managedProcessedConditionType conditions.ConditionType = "ManagedPipelineProcessed" - // postActionsExecutedConditionType is the type used to track the status of Release post-actions - postActionsExecutedConditionType conditions.ConditionType = "PostActionsExecuted" - // tenantProcessedConditionType is the type used to track the status of a Release Tenant Pipeline processing tenantProcessedConditionType conditions.ConditionType = "TenantPipelineProcessed" diff --git a/api/v1alpha1/release_types.go b/api/v1alpha1/release_types.go index 60f83c710..c97a9ea6d 100644 --- a/api/v1alpha1/release_types.go +++ b/api/v1alpha1/release_types.go @@ -67,6 +67,10 @@ type ReleaseStatus struct { // +optional Conditions []metav1.Condition `json:"conditions"` + // FinalProcessing contains information about the release final processing + // +optional + FinalProcessing PipelineInfo `json:"finalProcessing,omitempty"` + // ManagedProcessing contains information about the release managed processing // +optional ManagedProcessing PipelineInfo `json:"managedProcessing,omitempty"` @@ -165,10 +169,9 @@ type Release struct { Status ReleaseStatus `json:"status,omitempty"` } -// HasEveryPostActionExecutionFinished checks whether the Release post-actions execution has finished, -// regardless of the result. -func (r *Release) HasEveryPostActionExecutionFinished() bool { - return r.hasPhaseFinished(postActionsExecutedConditionType) +// Has FinalPipelineProcessingFinished checks whether the Release Final Pipeline processing has finished, regardless of the result. +func (r *Release) HasFinalPipelineProcessingFinished() bool { + return r.hasPhaseFinished(finalProcessedConditionType) } // HasManagedPipelineProcessingFinished checks whether the Release Managed Pipeline processing has finished, regardless of the result. @@ -196,14 +199,9 @@ func (r *Release) IsAutomated() bool { return r.Status.Automated } -// IsEveryPostActionExecuted checks whether the Release post-actions were successfully executed. -func (r *Release) IsEveryPostActionExecuted() bool { - return meta.IsStatusConditionTrue(r.Status.Conditions, postActionsExecutedConditionType.String()) -} - -// IsEachPostActionExecuting checks whether the Release post-actions are in progress. -func (r *Release) IsEachPostActionExecuting() bool { - return r.isPhaseProgressing(postActionsExecutedConditionType) +// IsFinalPipelineProcessed checks whether the Release Final Pipeline was successfully processed. +func (r *Release) IsFinalPipelineProcessed() bool { + return meta.IsStatusConditionTrue(r.Status.Conditions, finalProcessedConditionType.String()) } // IsManagedPipelineProcessed checks whether the Release Managed Pipeline was successfully processed. @@ -216,6 +214,11 @@ func (r *Release) IsTenantPipelineProcessed() bool { return meta.IsStatusConditionTrue(r.Status.Conditions, tenantProcessedConditionType.String()) } +// IsFinalPipelineProcessing checks whether the Release Final Pipeline processing is in progress. +func (r *Release) IsFinalPipelineProcessing() bool { + return r.isPhaseProgressing(finalProcessedConditionType) +} + // IsManagedPipelineProcessing checks whether the Release Managed Pipeline processing is in progress. func (r *Release) IsManagedPipelineProcessing() bool { return r.isPhaseProgressing(managedProcessedConditionType) @@ -259,6 +262,24 @@ func (r *Release) MarkManagedPipelineProcessed() { ) } +// MarkFinalPipelineProcessed marks the Release Final Pipeline as processed. +func (r *Release) MarkFinalPipelineProcessed() { + if !r.IsFinalPipelineProcessing() || r.HasFinalPipelineProcessingFinished() { + return + } + + r.Status.FinalProcessing.CompletionTime = &metav1.Time{Time: time.Now()} + conditions.SetCondition(&r.Status.Conditions, finalProcessedConditionType, metav1.ConditionTrue, SucceededReason) + + go metrics.RegisterCompletedReleasePipelineProcessing( + r.Status.FinalProcessing.StartTime, + r.Status.FinalProcessing.CompletionTime, + SucceededReason.String(), + r.Status.Target, + metadata.FinalPipelineType, + ) +} + // MarkTenantPipelineProcessed marks the Release Tenant Pipeline as processed. func (r *Release) MarkTenantPipelineProcessed() { if !r.IsTenantPipelineProcessing() || r.HasTenantPipelineProcessingFinished() { @@ -277,6 +298,27 @@ func (r *Release) MarkTenantPipelineProcessed() { ) } +// MarkFinalPipelineProcessing marks the Release Final Pipeline as processing. +func (r *Release) MarkFinalPipelineProcessing() { + if r.HasFinalPipelineProcessingFinished() { + return + } + + if !r.IsFinalPipelineProcessing() { + r.Status.FinalProcessing.StartTime = &metav1.Time{Time: time.Now()} + } + + conditions.SetCondition(&r.Status.Conditions, finalProcessedConditionType, metav1.ConditionFalse, ProgressingReason) + + go metrics.RegisterNewReleasePipelineProcessing( + r.Status.FinalProcessing.StartTime, + r.Status.StartTime, + ProgressingReason.String(), + r.Status.Target, + metadata.FinalPipelineType, + ) +} + // MarkManagedPipelineProcessing marks the Release Managed Pipeline as processing. func (r *Release) MarkManagedPipelineProcessing() { if r.HasManagedPipelineProcessingFinished() { @@ -319,6 +361,24 @@ func (r *Release) MarkTenantPipelineProcessing() { ) } +// MarkFinalPipelineProcessingFailed marks the Release Final Pipeline processing as failed. +func (r *Release) MarkFinalPipelineProcessingFailed(message string) { + if !r.IsFinalPipelineProcessing() || r.HasFinalPipelineProcessingFinished() { + return + } + + r.Status.FinalProcessing.CompletionTime = &metav1.Time{Time: time.Now()} + conditions.SetConditionWithMessage(&r.Status.Conditions, finalProcessedConditionType, metav1.ConditionFalse, FailedReason, message) + + go metrics.RegisterCompletedReleasePipelineProcessing( + r.Status.FinalProcessing.StartTime, + r.Status.FinalProcessing.CompletionTime, + FailedReason.String(), + r.Status.Target, + metadata.FinalPipelineType, + ) +} + // MarkManagedPipelineProcessingFailed marks the Release Managed Pipeline processing as failed. func (r *Release) MarkManagedPipelineProcessingFailed(message string) { if !r.IsManagedPipelineProcessing() || r.HasManagedPipelineProcessingFinished() { @@ -355,6 +415,15 @@ func (r *Release) MarkTenantPipelineProcessingFailed(message string) { ) } +// MarkFinalPipelineProcessingSkipped marks the Release Final Pipeline processing as skipped. +func (r *Release) MarkFinalPipelineProcessingSkipped() { + if r.HasFinalPipelineProcessingFinished() { + return + } + + conditions.SetCondition(&r.Status.Conditions, finalProcessedConditionType, metav1.ConditionTrue, SkippedReason) +} + // MarkManagedPipelineProcessingSkipped marks the Release Managed Pipeline processing as skipped. func (r *Release) MarkManagedPipelineProcessingSkipped() { if r.HasManagedPipelineProcessingFinished() { @@ -373,53 +442,6 @@ func (r *Release) MarkTenantPipelineProcessingSkipped() { conditions.SetCondition(&r.Status.Conditions, tenantProcessedConditionType, metav1.ConditionTrue, SkippedReason) } -// MarkPostActionsExecuted marks the Release post-actions as executed. -func (r *Release) MarkPostActionsExecuted() { - if !r.IsEachPostActionExecuting() || r.HasEveryPostActionExecutionFinished() { - return - } - - r.Status.PostActionsExecution.CompletionTime = &metav1.Time{Time: time.Now()} - conditions.SetCondition(&r.Status.Conditions, postActionsExecutedConditionType, metav1.ConditionTrue, SucceededReason) - - go metrics.RegisterCompletedReleasePostActionsExecuted( - r.Status.PostActionsExecution.StartTime, - r.Status.PostActionsExecution.CompletionTime, - SucceededReason.String(), - ) -} - -// MarkPostActionsExecuting marks the Release post-actions as executing. -func (r *Release) MarkPostActionsExecuting(message string) { - if r.HasEveryPostActionExecutionFinished() { - return - } - - if !r.IsEachPostActionExecuting() { - r.Status.PostActionsExecution.StartTime = &metav1.Time{Time: time.Now()} - } - - conditions.SetConditionWithMessage(&r.Status.Conditions, postActionsExecutedConditionType, metav1.ConditionFalse, ProgressingReason, message) - - go metrics.RegisterNewReleasePostActionsExecution() -} - -// MarkPostActionsExecutionFailed marks the Release post-actions execution as failed. -func (r *Release) MarkPostActionsExecutionFailed(message string) { - if !r.IsEachPostActionExecuting() || r.HasEveryPostActionExecutionFinished() { - return - } - - r.Status.PostActionsExecution.CompletionTime = &metav1.Time{Time: time.Now()} - conditions.SetConditionWithMessage(&r.Status.Conditions, postActionsExecutedConditionType, metav1.ConditionFalse, FailedReason, message) - - go metrics.RegisterCompletedReleasePostActionsExecuted( - r.Status.PostActionsExecution.StartTime, - r.Status.PostActionsExecution.CompletionTime, - FailedReason.String(), - ) -} - // MarkReleased marks the Release as released. func (r *Release) MarkReleased() { if !r.IsReleasing() || r.HasReleaseFinished() { @@ -433,10 +455,10 @@ func (r *Release) MarkReleased() { r.Status.StartTime, r.Status.CompletionTime, r.getPhaseReason(managedProcessedConditionType), - r.getPhaseReason(postActionsExecutedConditionType), SucceededReason.String(), r.Status.Target, r.getPhaseReason(tenantProcessedConditionType), + r.getPhaseReason(finalProcessedConditionType), r.getPhaseReason(validatedConditionType), ) } @@ -468,9 +490,9 @@ func (r *Release) MarkReleaseFailed(message string) { go metrics.RegisterCompletedRelease( r.Status.StartTime, r.Status.CompletionTime, - r.getPhaseReason(postActionsExecutedConditionType), r.getPhaseReason(tenantProcessedConditionType), r.getPhaseReason(managedProcessedConditionType), + r.getPhaseReason(finalProcessedConditionType), FailedReason.String(), r.Status.Target, r.getPhaseReason(validatedConditionType), diff --git a/api/v1alpha1/release_types_test.go b/api/v1alpha1/release_types_test.go index 378d9f54c..e597c5a1b 100644 --- a/api/v1alpha1/release_types_test.go +++ b/api/v1alpha1/release_types_test.go @@ -30,38 +30,6 @@ import ( var _ = Describe("Release type", func() { - When("HasEveryPostActionExecutionFinished method is called", func() { - var release *Release - - BeforeEach(func() { - release = &Release{} - }) - - It("should return false when the post-actions executed condition is missing", func() { - Expect(release.HasEveryPostActionExecutionFinished()).To(BeFalse()) - }) - - It("should return true when the post-actions executed condition status is True", func() { - conditions.SetCondition(&release.Status.Conditions, postActionsExecutedConditionType, metav1.ConditionTrue, SucceededReason) - Expect(release.HasEveryPostActionExecutionFinished()).To(BeTrue()) - }) - - It("should return false when the post-actions executed condition status is False and the reason is Progressing", func() { - conditions.SetCondition(&release.Status.Conditions, postActionsExecutedConditionType, metav1.ConditionFalse, ProgressingReason) - Expect(release.HasEveryPostActionExecutionFinished()).To(BeFalse()) - }) - - It("should return true when the post-actions executed condition status is False and the reason is not Progressing", func() { - conditions.SetCondition(&release.Status.Conditions, postActionsExecutedConditionType, metav1.ConditionFalse, FailedReason) - Expect(release.HasEveryPostActionExecutionFinished()).To(BeTrue()) - }) - - It("should return false when the post-actions executed condition status is Unknown", func() { - conditions.SetCondition(&release.Status.Conditions, postActionsExecutedConditionType, metav1.ConditionUnknown, ProgressingReason) - Expect(release.HasEveryPostActionExecutionFinished()).To(BeFalse()) - }) - }) - When("HasManagedPipelineProcessingFinished method is called", func() { var release *Release @@ -118,6 +86,34 @@ var _ = Describe("Release type", func() { }) }) + When("HasFinalPipelineProcessingFinshed method is called", func() { + var release *Release + + BeforeEach(func() { + release = &Release{} + }) + + It("should return true when the final pipeline processed condition status is True", func() { + conditions.SetCondition(&release.Status.Conditions, finalProcessedConditionType, metav1.ConditionTrue, SucceededReason) + Expect(release.HasFinalPipelineProcessingFinished()).To(BeTrue()) + }) + + It("should return false when the final pipeline processed condition status is False and the reason is Progressing", func() { + conditions.SetCondition(&release.Status.Conditions, finalProcessedConditionType, metav1.ConditionFalse, ProgressingReason) + Expect(release.HasFinalPipelineProcessingFinished()).To(BeFalse()) + }) + + It("Should return true when the final pipeline processed condition status is False and the reason is not Progressing", func() { + conditions.SetCondition(&release.Status.Conditions, finalProcessedConditionType, metav1.ConditionFalse, FailedReason) + Expect(release.HasFinalPipelineProcessingFinished()).To(BeTrue()) + }) + + It("should return false when the final pipeline processed condition status is Unknown", func() { + conditions.SetCondition(&release.Status.Conditions, finalProcessedConditionType, metav1.ConditionUnknown, ProgressingReason) + Expect(release.HasFinalPipelineProcessingFinished()).To(BeFalse()) + }) + }) + When("HasReleaseFinished method is called", func() { var release *Release @@ -189,65 +185,6 @@ var _ = Describe("Release type", func() { }) }) - When("IsEveryPostActionExecuted method is called", func() { - var release *Release - - BeforeEach(func() { - release = &Release{} - }) - - It("should return true when the post-actions executed condition status is True", func() { - conditions.SetCondition(&release.Status.Conditions, postActionsExecutedConditionType, metav1.ConditionTrue, SucceededReason) - Expect(release.IsEveryPostActionExecuted()).To(BeTrue()) - }) - - It("should return false when the post-actions executed condition status is False", func() { - conditions.SetCondition(&release.Status.Conditions, postActionsExecutedConditionType, metav1.ConditionFalse, SucceededReason) - Expect(release.IsEveryPostActionExecuted()).To(BeFalse()) - }) - - It("should return false when the post-actions executed condition status is Unknown", func() { - conditions.SetCondition(&release.Status.Conditions, postActionsExecutedConditionType, metav1.ConditionUnknown, SucceededReason) - Expect(release.IsEveryPostActionExecuted()).To(BeFalse()) - }) - - It("should return false when the post-actions executed condition is missing", func() { - Expect(release.IsEveryPostActionExecuted()).To(BeFalse()) - }) - }) - - When("IsEachPostActionExecuting method is called", func() { - var release *Release - - BeforeEach(func() { - release = &Release{} - }) - - It("should return false when the post-actions executed condition is missing", func() { - Expect(release.IsEachPostActionExecuting()).To(BeFalse()) - }) - - It("should return false when the post-actions executed condition status is True", func() { - conditions.SetCondition(&release.Status.Conditions, postActionsExecutedConditionType, metav1.ConditionTrue, SucceededReason) - Expect(release.IsEachPostActionExecuting()).To(BeFalse()) - }) - - It("should return true when the post-actions executed condition status is False and the reason is Progressing", func() { - conditions.SetCondition(&release.Status.Conditions, postActionsExecutedConditionType, metav1.ConditionFalse, ProgressingReason) - Expect(release.IsEachPostActionExecuting()).To(BeTrue()) - }) - - It("should return false when the post-actions executed condition status is False and the reason is not Progressing", func() { - conditions.SetCondition(&release.Status.Conditions, postActionsExecutedConditionType, metav1.ConditionFalse, FailedReason) - Expect(release.IsEachPostActionExecuting()).To(BeFalse()) - }) - - It("should return false when the post-actions executed condition status is Unknown", func() { - conditions.SetCondition(&release.Status.Conditions, postActionsExecutedConditionType, metav1.ConditionUnknown, ProgressingReason) - Expect(release.IsEachPostActionExecuting()).To(BeFalse()) - }) - }) - When("IsManagedPipelineProcessed method is called", func() { var release *Release @@ -302,6 +239,34 @@ var _ = Describe("Release type", func() { }) }) + When("IsFinalPipelineProcessed method is called", func() { + var release *Release + + BeforeEach(func() { + release = &Release{} + }) + + It("should return true when the final pipeline processed condition status is True", func() { + conditions.SetCondition(&release.Status.Conditions, finalProcessedConditionType, metav1.ConditionTrue, SucceededReason) + Expect(release.IsFinalPipelineProcessed()).To(BeTrue()) + }) + + It("should return false when the final pipeline processed condition status is False", func() { + conditions.SetCondition(&release.Status.Conditions, finalProcessedConditionType, metav1.ConditionFalse, SucceededReason) + Expect(release.IsFinalPipelineProcessed()).To(BeFalse()) + }) + + It("should return false when the final pipeline processed condition status is Unknown", func() { + conditions.SetCondition(&release.Status.Conditions, finalProcessedConditionType, metav1.ConditionUnknown, SucceededReason) + Expect(release.IsFinalPipelineProcessed()).To(BeFalse()) + }) + + It("should return false when the final pipeline processed condition is missing", func() { + Expect(release.IsFinalPipelineProcessed()).To(BeFalse()) + }) + + }) + When("IsManagedPipelineProcessing method is called", func() { var release *Release @@ -366,6 +331,34 @@ var _ = Describe("Release type", func() { }) }) + When("IsFinalPipelineProcessing method is called", func() { + var release *Release + + BeforeEach(func() { + release = &Release{} + }) + + It("should return false when the final pipeline processed condition is missing", func() { + Expect(release.IsFinalPipelineProcessing()).To(BeFalse()) + }) + + It("should return false when the final pipeline processed condition status is True", func() { + conditions.SetCondition(&release.Status.Conditions, finalProcessedConditionType, metav1.ConditionTrue, SucceededReason) + Expect(release.IsFinalPipelineProcessing()).To(BeFalse()) + }) + + It("should return true when the final pipeline processed condition status is False and the reason is Progressing", func() { + conditions.SetCondition(&release.Status.Conditions, finalProcessedConditionType, metav1.ConditionFalse, ProgressingReason) + Expect(release.IsFinalPipelineProcessing()).To(BeTrue()) + }) + + It("should return false when the final pipeline processed condition status is False and the reason is not Progressing", func() { + conditions.SetCondition(&release.Status.Conditions, finalProcessedConditionType, metav1.ConditionFalse, FailedReason) + Expect(release.IsFinalPipelineProcessing()).To(BeFalse()) + }) + + }) + When("IsReleased method is called", func() { var release *Release @@ -536,6 +529,48 @@ var _ = Describe("Release type", func() { }) }) + When("MarkFinalPipelineProcessed method is called", func() { + var release *Release + + BeforeEach(func() { + release = &Release{} + }) + + It("should do nothing if the Release final pipeline processing has not started", func() { + release.MarkFinalPipelineProcessed() + Expect(release.Status.FinalProcessing.CompletionTime).To(BeNil()) + }) + + It("should do nothing if the Release final pipeline processing finished", func() { + release.MarkFinalPipelineProcessing() + release.MarkFinalPipelineProcessed() + Expect(release.Status.FinalProcessing.CompletionTime.IsZero()).To(BeFalse()) + release.Status.FinalProcessing.CompletionTime = &metav1.Time{} + release.MarkFinalPipelineProcessed() + Expect(release.Status.FinalProcessing.CompletionTime.IsZero()).To(BeTrue()) + }) + + It("should register the completion time", func() { + release.MarkFinalPipelineProcessing() + Expect(release.Status.FinalProcessing.CompletionTime.IsZero()).To(BeTrue()) + release.MarkFinalPipelineProcessed() + Expect(release.Status.FinalProcessing.CompletionTime.IsZero()).To(BeFalse()) + }) + + It("should register the condition", func() { + Expect(release.Status.Conditions).To(HaveLen(0)) + release.MarkFinalPipelineProcessing() + release.MarkFinalPipelineProcessed() + + condition := meta.FindStatusCondition(release.Status.Conditions, finalProcessedConditionType.String()) + Expect(condition).NotTo(BeNil()) + Expect(*condition).To(MatchFields(IgnoreExtras, Fields{ + "Reason": Equal(SucceededReason.String()), + "Status": Equal(metav1.ConditionTrue), + })) + }) + }) + When("MarkManagedPipelineProcessing method is called", func() { var release *Release @@ -622,6 +657,50 @@ var _ = Describe("Release type", func() { }) }) + When("MarkFinalPipelineProcessing method is called", func() { + var release *Release + + BeforeEach(func() { + release = &Release{} + }) + + It("should do nothing if the Release final pipeline processing finished", func() { + release.MarkFinalPipelineProcessing() + release.MarkFinalPipelineProcessed() + Expect(release.IsFinalPipelineProcessing()).To(BeFalse()) + release.MarkFinalPipelineProcessing() + Expect(release.IsFinalPipelineProcessing()).To(BeFalse()) + }) + + It("should register the start time if the final pipeline is not processing", func() { + Expect(release.Status.FinalProcessing.StartTime).To(BeNil()) + release.MarkFinalPipelineProcessing() + Expect(release.Status.FinalProcessing.StartTime).NotTo(BeNil()) + }) + + It("should not register the start time if the final pipeline is processing already", func() { + Expect(release.Status.FinalProcessing.StartTime).To(BeNil()) + release.MarkFinalPipelineProcessing() + release.Status.FinalProcessing.StartTime = &metav1.Time{} + Expect(release.Status.FinalProcessing.StartTime.IsZero()).To(BeTrue()) + release.MarkFinalPipelineProcessing() + Expect(release.Status.FinalProcessing.StartTime.IsZero()).To(BeTrue()) + }) + + It("should register the condition", func() { + Expect(release.Status.Conditions).To(HaveLen(0)) + release.MarkFinalPipelineProcessing() + + condition := meta.FindStatusCondition(release.Status.Conditions, finalProcessedConditionType.String()) + Expect(condition).NotTo(BeNil()) + Expect(*condition).To(MatchFields(IgnoreExtras, Fields{ + "Reason": Equal(ProgressingReason.String()), + "Status": Equal(metav1.ConditionFalse), + })) + }) + + }) + When("MarkManagedPipelineProcessingFailed method is called", func() { var release *Release @@ -708,6 +787,50 @@ var _ = Describe("Release type", func() { }) }) + When("MarkFinalPipelineProcessingFailed method is called", func() { + var release *Release + + BeforeEach(func() { + release = &Release{} + }) + + It("should do nothing if the Release final pipeline processing has not started", func() { + release.MarkFinalPipelineProcessingFailed("") + Expect(release.Status.FinalProcessing.CompletionTime).To(BeNil()) + }) + + It("should do nothing if the Release final pipeline processing finished", func() { + release.MarkFinalPipelineProcessing() + release.MarkFinalPipelineProcessed() + Expect(release.Status.FinalProcessing.CompletionTime.IsZero()).To(BeFalse()) + release.Status.FinalProcessing.CompletionTime = &metav1.Time{} + release.MarkFinalPipelineProcessingFailed("") + Expect(release.Status.FinalProcessing.CompletionTime.IsZero()).To(BeTrue()) + }) + + It("should register the completion time", func() { + release.MarkFinalPipelineProcessing() + Expect(release.Status.FinalProcessing.CompletionTime.IsZero()).To(BeTrue()) + release.MarkFinalPipelineProcessingFailed("") + Expect(release.Status.FinalProcessing.CompletionTime.IsZero()).To(BeFalse()) + }) + + It("should register the condition", func() { + Expect(release.Status.Conditions).To(HaveLen(0)) + release.MarkFinalPipelineProcessing() + release.MarkFinalPipelineProcessingFailed("foo") + + condition := meta.FindStatusCondition(release.Status.Conditions, finalProcessedConditionType.String()) + Expect(condition).NotTo(BeNil()) + Expect(*condition).To(MatchFields(IgnoreExtras, Fields{ + "Message": Equal("foo"), + "Reason": Equal(FailedReason.String()), + "Status": Equal(metav1.ConditionFalse), + })) + }) + + }) + When("MarkManagedPipelineProcessingSkipped method is called", func() { var release *Release @@ -776,131 +899,36 @@ var _ = Describe("Release type", func() { }) }) - When("MarkPostActionsExecuted method is called", func() { - var release *Release - - BeforeEach(func() { - release = &Release{} - }) - - It("should do nothing if the Release post-actions execution has not started", func() { - release.MarkPostActionsExecuted() - Expect(release.Status.PostActionsExecution.CompletionTime).To(BeNil()) - }) - - It("should do nothing if the Release post-actions execution finished", func() { - release.MarkPostActionsExecuting("") - release.MarkPostActionsExecuted() - Expect(release.Status.PostActionsExecution.CompletionTime.IsZero()).To(BeFalse()) - release.Status.PostActionsExecution.CompletionTime = &metav1.Time{} - release.MarkPostActionsExecuted() - Expect(release.Status.PostActionsExecution.CompletionTime.IsZero()).To(BeTrue()) - }) - - It("should register the completion time", func() { - release.MarkPostActionsExecuting("") - Expect(release.Status.PostActionsExecution.CompletionTime.IsZero()).To(BeTrue()) - release.MarkPostActionsExecuted() - Expect(release.Status.PostActionsExecution.CompletionTime.IsZero()).To(BeFalse()) - }) - - It("should register the condition", func() { - Expect(release.Status.Conditions).To(HaveLen(0)) - release.MarkPostActionsExecuting("") - release.MarkPostActionsExecuted() - - condition := meta.FindStatusCondition(release.Status.Conditions, postActionsExecutedConditionType.String()) - Expect(condition).NotTo(BeNil()) - Expect(*condition).To(MatchFields(IgnoreExtras, Fields{ - "Reason": Equal(SucceededReason.String()), - "Status": Equal(metav1.ConditionTrue), - })) - }) - }) - - When("MarkPostActionsExecuting method is called", func() { + When("MarkFinalPipelineProcessingSkipped method is called", func() { var release *Release BeforeEach(func() { release = &Release{} }) - It("should do nothing if the Release post-actions execution finished", func() { - release.MarkPostActionsExecuting("") - release.MarkPostActionsExecuted() - Expect(release.IsEachPostActionExecuting()).To(BeFalse()) - release.MarkPostActionsExecuting("") - Expect(release.IsEachPostActionExecuting()).To(BeFalse()) - }) - - It("should register the start time if it's not executing post-actions", func() { - Expect(release.Status.PostActionsExecution.StartTime).To(BeNil()) - release.MarkPostActionsExecuting("") - Expect(release.Status.PostActionsExecution.StartTime).NotTo(BeNil()) - }) - - It("should not register the start time if it's executing post-actions already", func() { - Expect(release.Status.PostActionsExecution.StartTime).To(BeNil()) - release.MarkPostActionsExecuting("") - release.Status.PostActionsExecution.StartTime = &metav1.Time{} - Expect(release.Status.PostActionsExecution.StartTime.IsZero()).To(BeTrue()) - release.MarkPostActionsExecuting("") - Expect(release.Status.PostActionsExecution.StartTime.IsZero()).To(BeTrue()) - }) + It("should do nothing if the Release final pipeline processing finished already", func() { + release.MarkFinalPipelineProcessing() + release.MarkFinalPipelineProcessingFailed("error") + release.MarkFinalPipelineProcessingSkipped() - It("should register the condition", func() { - Expect(release.Status.Conditions).To(HaveLen(0)) - release.MarkPostActionsExecuting("foo") - - condition := meta.FindStatusCondition(release.Status.Conditions, postActionsExecutedConditionType.String()) + condition := meta.FindStatusCondition(release.Status.Conditions, finalProcessedConditionType.String()) Expect(condition).NotTo(BeNil()) Expect(*condition).To(MatchFields(IgnoreExtras, Fields{ - "Message": Equal("foo"), - "Reason": Equal(ProgressingReason.String()), + "Message": Equal("error"), + "Reason": Equal(FailedReason.String()), "Status": Equal(metav1.ConditionFalse), })) }) - }) - - When("MarkPostActionsExecutionFailed method is called", func() { - var release *Release - - BeforeEach(func() { - release = &Release{} - }) - - It("should do nothing if the Release post-actions execution has not started", func() { - release.MarkPostActionsExecutionFailed("") - Expect(release.Status.PostActionsExecution.CompletionTime).To(BeNil()) - }) - - It("should do nothing if the Release post-actions execution finished", func() { - release.MarkPostActionsExecuting("") - release.MarkPostActionsExecuted() - Expect(release.Status.PostActionsExecution.CompletionTime.IsZero()).To(BeFalse()) - release.Status.PostActionsExecution.CompletionTime = &metav1.Time{} - release.MarkPostActionsExecutionFailed("") - Expect(release.Status.PostActionsExecution.CompletionTime.IsZero()).To(BeTrue()) - }) - - It("should register the completion time", func() { - release.MarkPostActionsExecuting("") - Expect(release.Status.PostActionsExecution.CompletionTime.IsZero()).To(BeTrue()) - release.MarkPostActionsExecutionFailed("") - Expect(release.Status.PostActionsExecution.CompletionTime.IsZero()).To(BeFalse()) - }) It("should register the condition", func() { Expect(release.Status.Conditions).To(HaveLen(0)) - release.MarkPostActionsExecuting("") - release.MarkPostActionsExecutionFailed("foo") + release.MarkFinalPipelineProcessingSkipped() - condition := meta.FindStatusCondition(release.Status.Conditions, postActionsExecutedConditionType.String()) + condition := meta.FindStatusCondition(release.Status.Conditions, finalProcessedConditionType.String()) Expect(condition).NotTo(BeNil()) Expect(*condition).To(MatchFields(IgnoreExtras, Fields{ - "Message": Equal("foo"), - "Reason": Equal(FailedReason.String()), - "Status": Equal(metav1.ConditionFalse), + "Reason": Equal(SkippedReason.String()), + "Status": Equal(metav1.ConditionTrue), })) }) }) diff --git a/api/v1alpha1/releaseplan_types.go b/api/v1alpha1/releaseplan_types.go index 5f25aea74..d1ab96984 100644 --- a/api/v1alpha1/releaseplan_types.go +++ b/api/v1alpha1/releaseplan_types.go @@ -44,9 +44,13 @@ type ReleasePlanSpec struct { // +optional Data *runtime.RawExtension `json:"data,omitempty"` - // Pipeline contains all the information about the tenant Pipeline + // TenantPipeline contains all the information about the tenant Pipeline // +optional - Pipeline *tektonutils.ParameterizedPipeline `json:"pipeline,omitempty"` + TenantPipeline *tektonutils.ParameterizedPipeline `json:"tenantPipeline,omitempty"` + + // FinalPipeline contains all the information about the final Pipeline + // +optional + FinalPipeline *tektonutils.ParameterizedPipeline `json:"finalPipeline,omitempty"` // ReleaseGracePeriodDays is the number of days a Release should be kept // This value is used to define the Release ExpirationTime diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index ec2bbd571..34cacd232 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -385,8 +385,13 @@ func (in *ReleasePlanSpec) DeepCopyInto(out *ReleasePlanSpec) { *out = new(runtime.RawExtension) (*in).DeepCopyInto(*out) } - if in.Pipeline != nil { - in, out := &in.Pipeline, &out.Pipeline + if in.TenantPipeline != nil { + in, out := &in.TenantPipeline, &out.TenantPipeline + *out = new(utils.ParameterizedPipeline) + (*in).DeepCopyInto(*out) + } + if in.FinalPipeline != nil { + in, out := &in.FinalPipeline, &out.FinalPipeline *out = new(utils.ParameterizedPipeline) (*in).DeepCopyInto(*out) } @@ -551,6 +556,7 @@ func (in *ReleaseStatus) DeepCopyInto(out *ReleaseStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + in.FinalProcessing.DeepCopyInto(&out.FinalProcessing) in.ManagedProcessing.DeepCopyInto(&out.ManagedProcessing) in.PostActionsExecution.DeepCopyInto(&out.PostActionsExecution) in.TenantProcessing.DeepCopyInto(&out.TenantProcessing) diff --git a/config/crd/bases/appstudio.redhat.com_releaseplans.yaml b/config/crd/bases/appstudio.redhat.com_releaseplans.yaml index 515d36b29..a1b2cdd33 100644 --- a/config/crd/bases/appstudio.redhat.com_releaseplans.yaml +++ b/config/crd/bases/appstudio.redhat.com_releaseplans.yaml @@ -95,9 +95,9 @@ spec: the managed Release Pipeline type: object x-kubernetes-preserve-unknown-fields: true - pipeline: - description: Pipeline contains all the information about the tenant - Pipeline + finalPipeline: + description: FinalPipeline contains all the information about the + final Pipeline properties: params: description: Params is a slice of parameters for a given resolver @@ -180,6 +180,81 @@ spec: description: Target references where to send the release requests pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ type: string + tenantPipeline: + description: TenantPipeline contains all the information about the + tenant Pipeline + properties: + params: + description: Params is a slice of parameters for a given resolver + items: + description: Param defines the parameters for a given resolver + in PipelineRef + properties: + name: + description: Name is the name of the parameter + type: string + value: + description: Value is the value of the parameter + type: string + required: + - name + - value + type: object + type: array + pipelineRef: + description: PipelineRef is the reference to the Pipeline + properties: + params: + description: Params is a slice of parameters for a given resolver + items: + description: Param defines the parameters for a given resolver + in PipelineRef + properties: + name: + description: Name is the name of the parameter + type: string + value: + description: Value is the value of the parameter + type: string + required: + - name + - value + type: object + type: array + resolver: + description: Resolver is the name of a Tekton resolver to + be used (e.g. git) + type: string + required: + - params + - resolver + type: object + serviceAccountName: + description: ServiceAccountName is the ServiceAccount to use during + the execution of the Pipeline + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + timeouts: + description: Timeouts defines the different Timeouts to use in + the PipelineRun execution + properties: + finally: + description: Finally sets the maximum allowed duration of + this pipeline's finally + type: string + pipeline: + description: Pipeline sets the maximum allowed duration for + execution of the entire pipeline. The sum of individual + timeouts for tasks and finally must not exceed this value. + type: string + tasks: + description: Tasks sets the maximum allowed duration of this + pipeline's tasks + type: string + type: object + required: + - pipelineRef + type: object required: - application type: object diff --git a/config/crd/bases/appstudio.redhat.com_releases.yaml b/config/crd/bases/appstudio.redhat.com_releases.yaml index 4aa36bc5c..62f3b1f2f 100644 --- a/config/crd/bases/appstudio.redhat.com_releases.yaml +++ b/config/crd/bases/appstudio.redhat.com_releases.yaml @@ -180,6 +180,32 @@ spec: description: ExpirationTime is the time when a Release can be purged format: date-time type: string + finalProcessing: + description: FinalProcessing contains information about the release + final processing + properties: + completionTime: + description: CompletionTime is the time when the Release processing + was completed + format: date-time + type: string + pipelineRun: + description: PipelineRun contains the namespaced name of the managed + Release PipelineRun executed as part of this release + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?\/[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + roleBinding: + description: |- + RoleBinding contains the namespaced name of the roleBinding created for the managed Release PipelineRun + executed as part of this release + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?\/[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + startTime: + description: StartTime is the time when the Release processing + started + format: date-time + type: string + type: object managedProcessing: description: ManagedProcessing contains information about the release managed processing diff --git a/controllers/release/adapter.go b/controllers/release/adapter.go index 5ab000a0b..3281e229c 100644 --- a/controllers/release/adapter.go +++ b/controllers/release/adapter.go @@ -157,8 +157,8 @@ func (a *adapter) EnsureReleaseIsCompleted() (controller.OperationResult, error) return controller.ContinueProcessing() } - // The managed pipeline processing has to complete for a Release to be completed - if !a.release.HasManagedPipelineProcessingFinished() { + // The final pipeline processing has to complete for a Release to be completed + if !a.release.IsFinalPipelineProcessed() { return controller.ContinueProcessing() } @@ -201,7 +201,7 @@ func (a *adapter) EnsureTenantPipelineIsProcessed() (controller.OperationResult, return controller.RequeueWithError(err) } - if releasePlan.Spec.Pipeline == nil { + if releasePlan.Spec.TenantPipeline == nil { // no tenant pipeline to run patch := client.MergeFrom(a.release.DeepCopy()) a.release.MarkTenantPipelineProcessingSkipped() @@ -229,6 +229,52 @@ func (a *adapter) EnsureTenantPipelineIsProcessed() (controller.OperationResult, return controller.ContinueProcessing() } +// EnsureFinalPipelineIsProcessed is an operation that will ensure that a Final Release PipelineRun associated to the Release +// being processed exist. Otherwise, it will be created. +func (a *adapter) EnsureFinalPipelineIsProcessed() (controller.OperationResult, error) { + if a.release.HasFinalPipelineProcessingFinished() || !a.release.HasManagedPipelineProcessingFinished() { + return controller.ContinueProcessing() + } + + pipelineRun, err := a.loader.GetReleasePipelineRun(a.ctx, a.client, a.release, metadata.FinalPipelineType) + if err != nil && !errors.IsNotFound(err) { + return controller.RequeueWithError(err) + } + + if pipelineRun == nil || !a.release.IsFinalPipelineProcessing() { + releasePlan, err := a.loader.GetReleasePlan(a.ctx, a.client, a.release) + if err != nil { + return controller.RequeueWithError(err) + } + + if releasePlan.Spec.FinalPipeline == nil { + // no final pipeline to run in the ReleasePlan + patch := client.MergeFrom(a.release.DeepCopy()) + a.release.MarkFinalPipelineProcessingSkipped() + return controller.RequeueOnErrorOrContinue(a.client.Status().Patch(a.ctx, a.release, patch)) + } + + if pipelineRun == nil { + snapshot, err := a.loader.GetSnapshot(a.ctx, a.client, a.release) + if err != nil { + return controller.RequeueWithError(err) + } + + pipelineRun, err = a.createFinalPipelineRun(releasePlan, snapshot) + if err != nil { + return controller.RequeueWithError(err) + } + + a.logger.Info(fmt.Sprintf("Created %s Release PipelineRun", metadata.FinalPipelineType), + "PipelineRun.Name", pipelineRun.Name, "PipelineRun.Namespace", pipelineRun.Namespace) + } + + return controller.RequeueOnErrorOrContinue(a.registerFinalProcessingData(pipelineRun)) + } + + return controller.ContinueProcessing() +} + // EnsureManagedPipelineIsProcessed is an operation that will ensure that a managed Release PipelineRun associated to the Release // being processed and a RoleBinding to grant its serviceAccount permissions exist. Otherwise, it will create them. func (a *adapter) EnsureManagedPipelineIsProcessed() (controller.OperationResult, error) { @@ -353,7 +399,28 @@ func (a *adapter) EnsureTenantPipelineProcessingIsTracked() (controller.Operatio return controller.ContinueProcessing() } -// EnsureManagedPipelineProcessingIsTracked is an operation that will ensure that the Release Managed PipelineRun status +// EnsureReleaseFinalProcessingIsTracked is an operation that will ensure that the Release Final PipelineRun status +// is tracked in the Release being processed. +func (a *adapter) EnsureFinalPipelineProcessingIsTracked() (controller.OperationResult, error) { + if !a.release.IsFinalPipelineProcessing() || a.release.HasFinalPipelineProcessingFinished() { + return controller.ContinueProcessing() + } + + pipelineRun, err := a.loader.GetReleasePipelineRun(a.ctx, a.client, a.release, metadata.FinalPipelineType) + if err != nil { + return controller.RequeueWithError(err) + } + if pipelineRun != nil { + err = a.registerFinalProcessingStatus(pipelineRun) + if err != nil { + return controller.RequeueWithError(err) + } + } + + return controller.ContinueProcessing() +} + +// EnsureReleaseManagedProcessingIsTracked is an operation that will ensure that the Release Managed PipelineRun status // is tracked in the Release being processed. func (a *adapter) EnsureManagedPipelineProcessingIsTracked() (controller.OperationResult, error) { if !a.release.IsManagedPipelineProcessing() || a.release.HasManagedPipelineProcessingFinished() { @@ -378,7 +445,7 @@ func (a *adapter) EnsureManagedPipelineProcessingIsTracked() (controller.Operati // Processing step are cleaned up once processing is finished. This exists in conjunction with EnsureFinalizersAreCalled because // the finalizers should be removed from the pipelineRuns even if the Release is not marked for deletion for quota reasons. func (a *adapter) EnsureReleaseProcessingResourcesAreCleanedUp() (controller.OperationResult, error) { - if !a.release.HasTenantPipelineProcessingFinished() || !a.release.HasManagedPipelineProcessingFinished() { + if !a.release.HasTenantPipelineProcessingFinished() || !a.release.HasManagedPipelineProcessingFinished() || !a.release.HasFinalPipelineProcessingFinished() { return controller.ContinueProcessing() } @@ -469,11 +536,50 @@ func (a *adapter) createTenantPipelineRun(releasePlan *v1alpha1.ReleasePlan, sna metadata.ReleaseSnapshotLabel: a.release.Spec.Snapshot, }). WithObjectReferences(a.release, releasePlan, snapshot). - WithParams(releasePlan.Spec.Pipeline.GetTektonParams()...). + WithParams(releasePlan.Spec.TenantPipeline.GetTektonParams()...). + WithOwner(a.release). + WithPipelineRef(releasePlan.Spec.TenantPipeline.PipelineRef.ToTektonPipelineRef()). + WithServiceAccount(releasePlan.Spec.TenantPipeline.ServiceAccountName). + WithTimeouts(&releasePlan.Spec.TenantPipeline.Timeouts, &a.releaseServiceConfig.Spec.DefaultTimeouts). + WithWorkspaceFromVolumeTemplate( + os.Getenv("DEFAULT_RELEASE_WORKSPACE_NAME"), + os.Getenv("DEFAULT_RELEASE_WORKSPACE_SIZE"), + ). + Build() + + if err != nil { + return nil, err + } + + err = a.client.Create(a.ctx, pipelineRun) + if err != nil { + return nil, err + } + + return pipelineRun, nil +} + +// createFinalPipelineRun creates and returns a new Final Release PipelineRun. The new PipelineRun will include owner +// annotations, so it triggers Release reconciles whenever it changes. The Pipeline information and the parameters to it +// will be extracted from the given ReleasePlan. The Release's Snapshot will also be passed to the release +// PipelineRun. +func (a *adapter) createFinalPipelineRun(releasePlan *v1alpha1.ReleasePlan, snapshot *applicationapiv1alpha1.Snapshot) (*tektonv1.PipelineRun, error) { + pipelineRun, err := utils.NewPipelineRunBuilder(metadata.FinalPipelineType, releasePlan.Namespace). + WithAnnotations(metadata.GetAnnotationsWithPrefix(a.release, integrationgitops.PipelinesAsCodePrefix)). + WithFinalizer(metadata.ReleaseFinalizer). + WithLabels(map[string]string{ + metadata.ApplicationNameLabel: releasePlan.Spec.Application, + metadata.PipelinesTypeLabel: metadata.FinalPipelineType, + metadata.ReleaseNameLabel: a.release.Name, + metadata.ReleaseNamespaceLabel: a.release.Namespace, + metadata.ReleaseSnapshotLabel: a.release.Spec.Snapshot, + }). + WithObjectReferences(a.release, releasePlan, snapshot). + WithParams(releasePlan.Spec.FinalPipeline.GetTektonParams()...). WithOwner(a.release). - WithPipelineRef(releasePlan.Spec.Pipeline.PipelineRef.ToTektonPipelineRef()). - WithServiceAccount(releasePlan.Spec.Pipeline.ServiceAccountName). - WithTimeouts(&releasePlan.Spec.Pipeline.Timeouts, &a.releaseServiceConfig.Spec.DefaultTimeouts). + WithPipelineRef(releasePlan.Spec.FinalPipeline.PipelineRef.ToTektonPipelineRef()). + WithServiceAccount(releasePlan.Spec.FinalPipeline.ServiceAccountName). + WithTimeouts(&releasePlan.Spec.FinalPipeline.Timeouts, &a.releaseServiceConfig.Spec.DefaultTimeouts). WithWorkspaceFromVolumeTemplate( os.Getenv("DEFAULT_RELEASE_WORKSPACE_NAME"), os.Getenv("DEFAULT_RELEASE_WORKSPACE_SIZE"), @@ -575,6 +681,24 @@ func (a *adapter) finalizeRelease(delete bool) error { } } + // Cleanup Final Processing Resources + finalPipelineRun, err := a.loader.GetReleasePipelineRun(a.ctx, a.client, a.release, metadata.FinalPipelineType) + if err != nil && !errors.IsNotFound(err) { + return err + } + + err = a.cleanupProcessingResources(finalPipelineRun, nil) + if err != nil { + return err + } + + if delete && finalPipelineRun != nil { + err = a.client.Delete(a.ctx, finalPipelineRun) + if err != nil && !errors.IsNotFound(err) { + return err + } + } + a.logger.Info("Successfully finalized Release") return nil @@ -608,6 +732,22 @@ func (a *adapter) registerTenantProcessingData(releasePipelineRun *tektonv1.Pipe return a.client.Status().Patch(a.ctx, a.release, patch) } +// registerFinalProcessingData adds all the Release Final processing information to its Status and marks it as final processing. +func (a *adapter) registerFinalProcessingData(releasePipelineRun *tektonv1.PipelineRun) error { + if releasePipelineRun == nil { + return nil + } + + patch := client.MergeFrom(a.release.DeepCopy()) + + a.release.Status.FinalProcessing.PipelineRun = fmt.Sprintf("%s%c%s", + releasePipelineRun.Namespace, types.Separator, releasePipelineRun.Name) + + a.release.MarkFinalPipelineProcessing() + + return a.client.Status().Patch(a.ctx, a.release, patch) +} + // registerProcessingData adds all the Release Managed processing information to its Status and marks it as managed processing. func (a *adapter) registerManagedProcessingData(releasePipelineRun *tektonv1.PipelineRun, roleBinding *rbac.RoleBinding) error { if releasePipelineRun == nil { @@ -671,6 +811,27 @@ func (a *adapter) registerManagedProcessingStatus(pipelineRun *tektonv1.Pipeline return a.client.Status().Patch(a.ctx, a.release, patch) } +// registerFinalProcessingStatus updates the status of the Release being processed by monitoring the status of the +// associated final Release PipelineRun and setting the appropriate state in the Release. If the PipelineRun hasn't +// started/succeeded, no action will be taken. +func (a *adapter) registerFinalProcessingStatus(pipelineRun *tektonv1.PipelineRun) error { + if pipelineRun == nil || !pipelineRun.IsDone() { + return nil + } + + patch := client.MergeFrom(a.release.DeepCopy()) + + condition := pipelineRun.Status.GetCondition(apis.ConditionSucceeded) + if condition.IsTrue() { + a.release.MarkFinalPipelineProcessed() + } else { + a.release.MarkFinalPipelineProcessingFailed(condition.Message) + a.release.MarkReleaseFailed("Release processing failed on final pipelineRun") + } + + return a.client.Status().Patch(a.ctx, a.release, patch) +} + // validateAuthor will ensure that a valid author exists for the Release and add it to its status. If the Release // has the automated label but doesn't have automated set in its status, this function will return an error so the // operation knows to requeue the Release. @@ -729,7 +890,7 @@ func (a *adapter) validateProcessingResources() *controller.ValidationResult { return &controller.ValidationResult{Err: err} } - if releasePlan.Spec.Pipeline == nil { + if releasePlan.Spec.TenantPipeline == nil && releasePlan.Spec.FinalPipeline == nil { resources, err := a.loader.GetProcessingResources(a.ctx, a.client, a.release) if err != nil { if resources == nil || resources.ReleasePlan == nil || resources.ReleasePlanAdmission == nil || errors.IsNotFound(err) { @@ -751,8 +912,10 @@ func (a *adapter) validatePipelineSource() *controller.ValidationResult { return a.validationError(err) } - if releasePlan.Spec.Pipeline != nil { - pipelineRef = releasePlan.Spec.Pipeline.PipelineRef + if releasePlan.Spec.TenantPipeline != nil { + pipelineRef = releasePlan.Spec.TenantPipeline.PipelineRef + } else if releasePlan.Spec.FinalPipeline != nil { // TODO: Refactor to remove the else if + pipelineRef = releasePlan.Spec.FinalPipeline.PipelineRef } else { releasePlanAdmission, err := a.loader.GetActiveReleasePlanAdmissionFromRelease(a.ctx, a.client, a.release) if err != nil { @@ -788,9 +951,9 @@ func (a *adapter) validatePipelineDefined() *controller.ValidationResult { a.release.Status.Target = releasePlan.Spec.Target } - if releasePlan.Spec.Pipeline == nil { + if releasePlan.Spec.TenantPipeline == nil { if releasePlan.Spec.Target == "" { - errString := "releasePlan has no pipeline or target. Each Release should define a tenant pipeline, managed pipeline, or both" + errString := "releasePlan has no pipeline or target. Each Release should define a tenant pipeline, managed pipeline or both" a.release.MarkValidationFailed(errString) return &controller.ValidationResult{Valid: false} } @@ -804,7 +967,7 @@ func (a *adapter) validatePipelineDefined() *controller.ValidationResult { return &controller.ValidationResult{Err: err} } if releasePlanAdmission.Spec.Pipeline == nil { - errString := "releasePlan and releasePlanAdmission both have no pipeline. Each Release should define a tenant pipeline, managed pipeline, or both" + errString := "releasePlan has no pipeline or target. Each Release should define a tenant pipeline, managed pipeline or both" a.release.MarkValidationFailed(errString) return &controller.ValidationResult{Valid: false} } diff --git a/controllers/release/adapter_test.go b/controllers/release/adapter_test.go index 0fce4c69a..167f9a6b1 100644 --- a/controllers/release/adapter_test.go +++ b/controllers/release/adapter_test.go @@ -179,6 +179,10 @@ var _ = Describe("Release adapter", Ordered, func() { Expect(!result.RequeueRequest && result.CancelRequest).To(BeFalse()) Expect(err).NotTo(HaveOccurred()) + result, err = adapter.EnsureFinalPipelineIsProcessed() + Expect(!result.RequeueRequest && result.CancelRequest).To(BeFalse()) + Expect(err).NotTo(HaveOccurred()) + Expect(adapter.client.Delete(adapter.ctx, adapter.release)).To(Succeed()) adapter.release, err = adapter.loader.GetRelease(adapter.ctx, adapter.client, adapter.release.Name, adapter.release.Namespace) Expect(adapter.release).NotTo(BeNil()) @@ -197,6 +201,10 @@ var _ = Describe("Release adapter", Ordered, func() { Expect(pipelineRun).To(Or(BeNil(), HaveField("DeletionTimestamp", Not(BeNil())))) Expect(err).NotTo(HaveOccurred()) + pipelineRun, err = adapter.loader.GetReleasePipelineRun(adapter.ctx, adapter.client, adapter.release, metadata.FinalPipelineType) + Expect(pipelineRun).To(Or(BeNil(), HaveField("DeletionTimestamp", Not(BeNil())))) + Expect(err).NotTo(HaveOccurred()) + _, err = adapter.loader.GetRelease(adapter.ctx, adapter.client, adapter.release.Name, adapter.release.Namespace) Expect(err).To(HaveOccurred()) Expect(errors.IsNotFound(err)).To(BeTrue()) @@ -262,7 +270,7 @@ var _ = Describe("Release adapter", Ordered, func() { Expect(adapter.release.IsReleased()).To(BeFalse()) }) - It("should do nothing if the managed processing has not completed", func() { + It("should do nothing if the final processing has not completed", func() { result, err := adapter.EnsureReleaseIsCompleted() Expect(!result.RequeueRequest && !result.CancelRequest).To(BeTrue()) Expect(err).NotTo(HaveOccurred()) @@ -272,12 +280,12 @@ var _ = Describe("Release adapter", Ordered, func() { It("should complete the release if all the required phases have completed", func() { adapter.ctx = toolkit.GetMockedContext(ctx, []toolkit.MockData{ { - ContextKey: loader.ReleasePlanAdmissionContextKey, - Resource: releasePlanAdmission, + ContextKey: loader.ReleasePlanContextKey, + Resource: releasePlan, }, }) - adapter.release.MarkManagedPipelineProcessing() - adapter.release.MarkManagedPipelineProcessed() + adapter.release.MarkFinalPipelineProcessing() + adapter.release.MarkFinalPipelineProcessed() result, err := adapter.EnsureReleaseIsCompleted() Expect(!result.RequeueRequest && !result.CancelRequest).To(BeTrue()) Expect(err).NotTo(HaveOccurred()) @@ -686,9 +694,9 @@ var _ = Describe("Release adapter", Ordered, func() { Expect(adapter.release.IsTenantPipelineProcessing()).To(BeFalse()) }) - It("should continue and mark tenant processing as skipped if the ReleasePlan has no Pipeline set", func() { + It("should continue and mark tenant processing as skipped if the ReleasePlan has no Tenant Pipeline set", func() { newReleasePlan := releasePlan.DeepCopy() - newReleasePlan.Spec.Pipeline = nil + newReleasePlan.Spec.TenantPipeline = nil adapter.ctx = toolkit.GetMockedContext(ctx, []toolkit.MockData{ { ContextKey: loader.ProcessingResourcesContextKey, @@ -766,7 +774,8 @@ var _ = Describe("Release adapter", Ordered, func() { }, Spec: v1alpha1.ReleasePlanSpec{ Application: application.Name, - Pipeline: ¶meterizedPipeline, + TenantPipeline: ¶meterizedPipeline, + FinalPipeline: ¶meterizedPipeline, ReleaseGracePeriodDays: 6, }, } @@ -795,6 +804,192 @@ var _ = Describe("Release adapter", Ordered, func() { }) }) + When("EnsureFinalPipelineIsProcessed is called", func() { + var adapter *adapter + + AfterEach(func() { + _ = adapter.client.Delete(ctx, adapter.release) + }) + + BeforeEach(func() { + adapter = createReleaseAndAdapter() + adapter.releaseServiceConfig = releaseServiceConfig + }) + + It("should do nothing if the Release final pipeline is complete", func() { + adapter.release.MarkFinalPipelineProcessing() + adapter.release.MarkFinalPipelineProcessed() + + result, err := adapter.EnsureFinalPipelineIsProcessed() + Expect(!result.RequeueRequest && !result.CancelRequest).To(BeTrue()) + Expect(err).NotTo(HaveOccurred()) + Expect(adapter.release.IsFinalPipelineProcessing()).To(BeFalse()) + }) + + It("should do nothing if the Release managed pipeline processing has not yet completed", func() { + adapter.release.MarkManagedPipelineProcessing() + + result, err := adapter.EnsureFinalPipelineIsProcessed() + Expect(!result.RequeueRequest && !result.CancelRequest).To(BeTrue()) + Expect(err).NotTo(HaveOccurred()) + Expect(adapter.release.IsFinalPipelineProcessing()).To(BeFalse()) + }) + + It("should do nothing if the Release tenant pipeline processing has not yet completed", func() { + adapter.release.MarkTenantPipelineProcessed() + + result, err := adapter.EnsureFinalPipelineIsProcessed() + Expect(!result.RequeueRequest && !result.CancelRequest).To(BeTrue()) + Expect(err).NotTo(HaveOccurred()) + Expect(adapter.release.IsFinalPipelineProcessing()).To(BeFalse()) + }) + + It("should requeue with error if fetching the Release final pipeline returns an error besides not found", func() { + adapter.ctx = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ReleasePipelineRunContextKey, + Err: fmt.Errorf("some error"), + }, + }) + adapter.release.MarkManagedPipelineProcessingSkipped() + + result, err := adapter.EnsureFinalPipelineIsProcessed() + Expect(result.RequeueRequest && !result.CancelRequest).To(BeTrue()) + Expect(err).To(HaveOccurred()) + Expect(adapter.release.IsFinalPipelineProcessing()).To(BeFalse()) + }) + + It("should mark the Final Pipeline Processing as Skipped if the ReleasePlan isn't found", func() { + adapter.ctx = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ProcessingResourcesContextKey, + Err: fmt.Errorf("no ReleasePlan can be found"), + }, + }) + adapter.release.MarkManagedPipelineProcessingSkipped() + + result, err := adapter.EnsureFinalPipelineIsProcessed() + Expect(!result.RequeueRequest && !result.CancelRequest).To(BeTrue()) + Expect(err).NotTo(HaveOccurred()) + Expect(adapter.release.IsFinalPipelineProcessing()).To(BeFalse()) + Expect(adapter.release.IsFinalPipelineProcessed()).To(BeTrue()) + + }) + + It("should continue and mark final processing as skipped if the ReleasePlan has no Final Pipeline set", func() { + newReleasePlan := releasePlan.DeepCopy() + newReleasePlan.Spec.FinalPipeline = nil + adapter.ctx = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ProcessingResourcesContextKey, + Resource: &loader.ProcessingResources{ + ReleasePlan: newReleasePlan, + }, + }, + }) + adapter.release.MarkManagedPipelineProcessingSkipped() + + result, err := adapter.EnsureFinalPipelineIsProcessed() + Expect(!result.RequeueRequest && !result.CancelRequest).To(BeTrue()) + Expect(err).NotTo(HaveOccurred()) + Expect(adapter.release.IsFinalPipelineProcessing()).To(BeFalse()) + Expect(adapter.release.IsFinalPipelineProcessed()).To(BeTrue()) + }) + + It("should register the processing data if the PipelineRun already exists", func() { + adapter.ctx = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ReleasePipelineRunContextKey, + Resource: &tektonv1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipeline-run", + Namespace: "default", + }, + }, + }, + }) + adapter.release.MarkFinalPipelineProcessing() + + result, err := adapter.EnsureFinalPipelineIsProcessed() + Expect(!result.RequeueRequest && !result.CancelRequest).To(BeTrue()) + Expect(err).NotTo(HaveOccurred()) + Expect(adapter.release.IsFinalPipelineProcessing()).To(BeTrue()) + }) + + It("should requeue the Release if any of the resources is not found", func() { + adapter.ctx = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ReleasePlanContextKey, + Err: fmt.Errorf("Not Found"), + Resource: nil, + }, + }) + adapter.release.MarkManagedPipelineProcessingSkipped() + + result, err := adapter.EnsureFinalPipelineIsProcessed() + Expect(result.RequeueRequest && !result.CancelRequest).To(BeTrue()) + Expect(err).To(HaveOccurred()) + }) + + It("should create a pipelineRun and register the processing data if all the required resources are present", func() { + releasePlan := &v1alpha1.ReleasePlan{} + + parameterizedPipeline := tektonutils.ParameterizedPipeline{} + parameterizedPipeline.PipelineRef = tektonutils.PipelineRef{ + Resolver: "git", + Params: []tektonutils.Param{ + {Name: "url", Value: "my-url"}, + {Name: "revision", Value: "my-revision"}, + {Name: "pathInRepo", Value: "my-path"}, + }, + } + parameterizedPipeline.Params = []tektonutils.Param{ + {Name: "parameter1", Value: "value1"}, + {Name: "parameter2", Value: "value2"}, + } + parameterizedPipeline.Timeouts = tektonv1.TimeoutFields{ + Pipeline: &metav1.Duration{Duration: 1 * time.Hour}, + } + + releasePlan = &v1alpha1.ReleasePlan{ + ObjectMeta: metav1.ObjectMeta{ + Name: "release-plan", + Namespace: "default", + }, + Spec: v1alpha1.ReleasePlanSpec{ + Application: application.Name, + FinalPipeline: ¶meterizedPipeline, + ReleaseGracePeriodDays: 6, + }, + } + releasePlan.Kind = "ReleasePlan" + + adapter.ctx = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ReleasePlanContextKey, + Resource: releasePlan, + }, + { + ContextKey: loader.SnapshotContextKey, + Resource: snapshot, + }, + }) + + adapter.release.MarkManagedPipelineProcessingSkipped() + + result, err := adapter.EnsureFinalPipelineIsProcessed() + Expect(!result.RequeueRequest && !result.CancelRequest).To(BeTrue()) + Expect(err).NotTo(HaveOccurred()) + Expect(adapter.release.IsFinalPipelineProcessing()).To(BeTrue()) + + pipelineRun, err := adapter.loader.GetReleasePipelineRun(adapter.ctx, adapter.client, adapter.release, metadata.FinalPipelineType) + Expect(pipelineRun).NotTo(BeNil()) + Expect(err).NotTo(HaveOccurred()) + Expect(adapter.client.Delete(adapter.ctx, pipelineRun)).To(Succeed()) + }) + + }) + When("EnsureReleaseIsValid is called", func() { var adapter *adapter @@ -913,7 +1108,66 @@ var _ = Describe("Release adapter", Ordered, func() { }) }) - When("EnsureManagedPipelineProcessingIsTracked is called", func() { + When("EnsureReleaseFinalProcessingIsTracked is called", func() { + var adapter *adapter + + AfterEach(func() { + _ = adapter.client.Delete(ctx, adapter.release) + }) + + BeforeEach(func() { + adapter = createReleaseAndAdapter() + }) + + It("should continue if the Release final pipeline processing has not started", func() { + result, err := adapter.EnsureFinalPipelineProcessingIsTracked() + Expect(!result.RequeueRequest && !result.CancelRequest).To(BeTrue()) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should continue if the Release final pipeline processing has finished", func() { + adapter.release.MarkFinalPipelineProcessing() + adapter.release.MarkFinalPipelineProcessed() + + result, err := adapter.EnsureFinalPipelineProcessingIsTracked() + Expect(!result.RequeueRequest && !result.CancelRequest).To(BeTrue()) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should track the status if the PipelineRun exists", func() { + adapter.release.MarkFinalPipelineProcessing() + + pipelineRun := &tektonv1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipeline-run", + Namespace: "default", + }, + } + pipelineRun.Status.MarkSucceeded("", "") + adapter.ctx = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ReleasePipelineRunContextKey, + Resource: pipelineRun, + }, + }) + + result, err := adapter.EnsureFinalPipelineProcessingIsTracked() + Expect(!result.RequeueRequest && !result.CancelRequest).To(BeTrue()) + Expect(err).NotTo(HaveOccurred()) + Expect(adapter.release.HasFinalPipelineProcessingFinished()).To(BeTrue()) + }) + + It("should continue if the PipelineRun doesn't exist", func() { + adapter.release.MarkFinalPipelineProcessing() + + result, err := adapter.EnsureFinalPipelineProcessingIsTracked() + Expect(!result.RequeueRequest && !result.CancelRequest).To(BeTrue()) + Expect(err).NotTo(HaveOccurred()) + }) + + }) + + When("EnsureReleaseManagedProcessingIsTracked is called", func() { var adapter *adapter AfterEach(func() { @@ -1064,6 +1318,13 @@ var _ = Describe("Release adapter", Ordered, func() { Expect(err).NotTo(HaveOccurred()) }) + It("Should continure if the Release final processing has not finished", func() { + adapter.release.MarkFinalPipelineProcessingSkipped() + result, err := adapter.EnsureReleaseProcessingResourcesAreCleanedUp() + Expect(!result.RequeueRequest && !result.CancelRequest).To(BeTrue()) + Expect(err).NotTo(HaveOccurred()) + }) + It("should call finalizeRelease with false if all release processing is complete", func() { adapter.releaseServiceConfig = releaseServiceConfig resources := &loader.ProcessingResources{ @@ -1097,13 +1358,14 @@ var _ = Describe("Release adapter", Ordered, func() { }, Spec: v1alpha1.ReleasePlanSpec{ Application: application.Name, - Pipeline: ¶meterizedPipeline, + TenantPipeline: ¶meterizedPipeline, + FinalPipeline: ¶meterizedPipeline, ReleaseGracePeriodDays: 6, }, } newReleasePlan.Kind = "ReleasePlan" - // Create tenant and managed pipelineRuns + // Create tenant, managed, final and pipelineRuns pipelineRun, err := adapter.createTenantPipelineRun(newReleasePlan, snapshot) Expect(pipelineRun).NotTo(BeNil()) Expect(err).NotTo(HaveOccurred()) @@ -1112,12 +1374,18 @@ var _ = Describe("Release adapter", Ordered, func() { Expect(pipelineRun).NotTo(BeNil()) Expect(err).NotTo(HaveOccurred()) + pipelineRun, err = adapter.createFinalPipelineRun(newReleasePlan, snapshot) + Expect(pipelineRun).NotTo(BeNil()) + Expect(err).NotTo(HaveOccurred()) + adapter.release.MarkTenantPipelineProcessing() adapter.release.MarkTenantPipelineProcessed() adapter.release.MarkManagedPipelineProcessing() adapter.release.MarkManagedPipelineProcessed() + adapter.release.MarkFinalPipelineProcessing() + adapter.release.MarkFinalPipelineProcessed() - // Ensure both pipelineRuns have finalizers removed + // Ensure all pipelineRuns have finalizers removed result, err := adapter.EnsureReleaseProcessingResourcesAreCleanedUp() Expect(!result.RequeueRequest && !result.CancelRequest).To(BeTrue()) Expect(err).NotTo(HaveOccurred()) @@ -1133,6 +1401,12 @@ var _ = Describe("Release adapter", Ordered, func() { Expect(pipelineRun).NotTo(BeNil()) Expect(pipelineRun.Finalizers).To(HaveLen(0)) Expect(k8sClient.Delete(ctx, pipelineRun)).To(Succeed()) + + pipelineRun, err = adapter.loader.GetReleasePipelineRun(adapter.ctx, adapter.client, adapter.release, metadata.FinalPipelineType) + Expect(err).NotTo(HaveOccurred()) + Expect(pipelineRun).NotTo(BeNil()) + Expect(pipelineRun.Finalizers).To(HaveLen(0)) + Expect(k8sClient.Delete(ctx, pipelineRun)).To(Succeed()) }) }) @@ -1241,7 +1515,7 @@ var _ = Describe("Release adapter", Ordered, func() { }, Spec: v1alpha1.ReleasePlanSpec{ Application: application.Name, - Pipeline: ¶meterizedPipeline, + TenantPipeline: ¶meterizedPipeline, ReleaseGracePeriodDays: 6, }, } @@ -1323,7 +1597,7 @@ var _ = Describe("Release adapter", Ordered, func() { }) It("contains the proper timeout value", func() { - Expect(pipelineRun.Spec.Timeouts.Pipeline).To(Equal(newReleasePlan.Spec.Pipeline.Timeouts.Pipeline)) + Expect(pipelineRun.Spec.Timeouts.Pipeline).To(Equal(newReleasePlan.Spec.TenantPipeline.Timeouts.Pipeline)) }) }) @@ -1462,6 +1736,133 @@ var _ = Describe("Release adapter", Ordered, func() { }) }) + When("createFinalPipelineRun is called", func() { + var ( + adapter *adapter + pipelineRun *tektonv1.PipelineRun + newReleasePlan *v1alpha1.ReleasePlan + ) + + AfterEach(func() { + _ = adapter.client.Delete(ctx, adapter.release) + + Expect(k8sClient.Delete(ctx, pipelineRun)).To(Succeed()) + }) + + BeforeEach(func() { + adapter = createReleaseAndAdapter() + + parameterizedPipeline := tektonutils.ParameterizedPipeline{} + parameterizedPipeline.PipelineRef = tektonutils.PipelineRef{ + Resolver: "git", + Params: []tektonutils.Param{ + {Name: "url", Value: "my-url"}, + {Name: "revision", Value: "my-revision"}, + {Name: "pathInRepo", Value: "my-path"}, + }, + } + parameterizedPipeline.Params = []tektonutils.Param{ + {Name: "parameter1", Value: "value1"}, + {Name: "parameter2", Value: "value2"}, + } + parameterizedPipeline.Timeouts = tektonv1.TimeoutFields{ + Pipeline: &metav1.Duration{Duration: 1 * time.Hour}, + } + + newReleasePlan = &v1alpha1.ReleasePlan{ + ObjectMeta: metav1.ObjectMeta{ + Name: "release-plan", + Namespace: "default", + }, + Spec: v1alpha1.ReleasePlanSpec{ + Application: application.Name, + FinalPipeline: ¶meterizedPipeline, + ReleaseGracePeriodDays: 6, + }, + } + newReleasePlan.Kind = "ReleasePlan" + + adapter.ctx = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ReleasePlanContextKey, + Resource: newReleasePlan, + }, + }) + + var err error + pipelineRun, err = adapter.createFinalPipelineRun(newReleasePlan, snapshot) + Expect(pipelineRun).NotTo(BeNil()) + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns a PipelineRun with the right prefix", func() { + Expect(reflect.TypeOf(pipelineRun)).To(Equal(reflect.TypeOf(&tektonv1.PipelineRun{}))) + Expect(pipelineRun.Name).To(HavePrefix("final")) + }) + + It("has the release reference", func() { + Expect(pipelineRun.Spec.Params).Should(ContainElement(HaveField("Name", strings.ToLower(adapter.release.Kind)))) + Expect(pipelineRun.Spec.Params).Should(ContainElement(HaveField("Value.StringVal", + fmt.Sprintf("%s%c%s", adapter.release.Namespace, types.Separator, adapter.release.Name)))) + }) + + It("has the releasePlan reference", func() { + name := []rune(releasePlan.Kind) + name[0] = unicode.ToLower(name[0]) + + Expect(pipelineRun.Spec.Params).Should(ContainElement(HaveField("Name", string(name)))) + Expect(pipelineRun.Spec.Params).Should(ContainElement(HaveField("Value.StringVal", + fmt.Sprintf("%s%c%s", releasePlan.Namespace, types.Separator, releasePlan.Name)))) + }) + + It("has the snapshot reference", func() { + Expect(pipelineRun.Spec.Params).Should(ContainElement(HaveField("Name", strings.ToLower(snapshot.Kind)))) + Expect(pipelineRun.Spec.Params).Should(ContainElement(HaveField("Value.StringVal", + fmt.Sprintf("%s%c%s", snapshot.Namespace, types.Separator, snapshot.Name)))) + }) + + It("has owner annotations", func() { + Expect(pipelineRun.GetAnnotations()[handler.NamespacedNameAnnotation]).To(ContainSubstring(adapter.release.Name)) + Expect(pipelineRun.GetAnnotations()[handler.TypeAnnotation]).To(ContainSubstring("Release")) + }) + + It("has release labels", func() { + Expect(pipelineRun.GetLabels()[metadata.PipelinesTypeLabel]).To(Equal(metadata.FinalPipelineType)) + Expect(pipelineRun.GetLabels()[metadata.ReleaseNameLabel]).To(Equal(adapter.release.Name)) + Expect(pipelineRun.GetLabels()[metadata.ReleaseNamespaceLabel]).To(Equal(testNamespace)) + Expect(pipelineRun.GetLabels()[metadata.ReleaseSnapshotLabel]).To(Equal(adapter.release.Spec.Snapshot)) + }) + + It("contains a parameter with the taskGitUrl", func() { + Expect(pipelineRun.Spec.Params).Should(ContainElement(HaveField("Name", "taskGitUrl"))) + var url string + resolverParams := pipelineRun.Spec.PipelineRef.ResolverRef.Params + for i := range resolverParams { + if resolverParams[i].Name == "url" { + url = resolverParams[i].Value.StringVal + } + } + Expect(pipelineRun.Spec.Params).Should(ContainElement(HaveField("Value.StringVal", url))) + }) + + It("contains a parameter with the taskGitRevision", func() { + Expect(pipelineRun.Spec.Params).Should(ContainElement(HaveField("Name", "taskGitRevision"))) + var revision string + resolverParams := pipelineRun.Spec.PipelineRef.ResolverRef.Params + for i := range resolverParams { + if resolverParams[i].Name == "revision" { + revision = resolverParams[i].Value.StringVal + } + } + Expect(pipelineRun.Spec.Params).Should(ContainElement(HaveField("Value.StringVal", revision))) + }) + + It("contains the proper timeout value", func() { + Expect(pipelineRun.Spec.Timeouts.Pipeline).To(Equal(newReleasePlan.Spec.FinalPipeline.Timeouts.Pipeline)) + }) + + }) + When("createRoleBindingForClusterRole is called", func() { var adapter *adapter @@ -1552,7 +1953,8 @@ var _ = Describe("Release adapter", Ordered, func() { }, Spec: v1alpha1.ReleasePlanSpec{ Application: application.Name, - Pipeline: parameterizedPipeline, + TenantPipeline: parameterizedPipeline, + FinalPipeline: parameterizedPipeline, ReleaseGracePeriodDays: 6, }, } @@ -1593,6 +1995,19 @@ var _ = Describe("Release adapter", Ordered, func() { Expect(k8sClient.Delete(ctx, pipelineRun)).To(Succeed()) }) + It("finalizes the Release and removes the finalizer from the Final PipelineRun when called with false", func() { + pipelineRun, err := adapter.createFinalPipelineRun(newReleasePlan, snapshot) + Expect(pipelineRun).NotTo(BeNil()) + Expect(err).NotTo(HaveOccurred()) + + Expect(adapter.finalizeRelease(false)).To(Succeed()) + pipelineRun, err = adapter.loader.GetReleasePipelineRun(adapter.ctx, adapter.client, adapter.release, metadata.FinalPipelineType) + Expect(err).NotTo(HaveOccurred()) + Expect(pipelineRun).NotTo(BeNil()) + Expect(pipelineRun.Finalizers).To(HaveLen(0)) + Expect(k8sClient.Delete(ctx, pipelineRun)).To(Succeed()) + }) + It("finalizes the Release and deletes the Tenant PipelineRun when called with true", func() { pipelineRun, err := adapter.createTenantPipelineRun(newReleasePlan, snapshot) Expect(pipelineRun).NotTo(BeNil()) @@ -1622,6 +2037,17 @@ var _ = Describe("Release adapter", Ordered, func() { Expect(err).NotTo(HaveOccurred()) Expect(pipelineRun).To(BeNil()) }) + + It("finalizes the Release and deletes the Final PipelineRun when called with true", func() { + pipelineRun, err := adapter.createFinalPipelineRun(newReleasePlan, snapshot) + Expect(pipelineRun).NotTo(BeNil()) + Expect(err).NotTo(HaveOccurred()) + + Expect(adapter.finalizeRelease(true)).To(Succeed()) + pipelineRun, err = adapter.loader.GetReleasePipelineRun(adapter.ctx, adapter.client, adapter.release, metadata.FinalPipelineType) + Expect(err).NotTo(HaveOccurred()) + Expect(pipelineRun).To(BeNil()) + }) }) When("getEmptyReleaseServiceConfig is called", func() { @@ -1725,6 +2151,36 @@ var _ = Describe("Release adapter", Ordered, func() { }) }) + When("registerFinalProcessingData is called", func() { + var adapter *adapter + + AfterEach(func() { + _ = adapter.client.Delete(ctx, adapter.release) + }) + + BeforeEach(func() { + adapter = createReleaseAndAdapter() + }) + + It("does nothing if there is no PipelineRun", func() { + Expect(adapter.registerFinalProcessingData(nil)).To(Succeed()) + Expect(adapter.release.Status.FinalProcessing.PipelineRun).To(BeEmpty()) + }) + + It("registers the Release final processing data", func() { + pipelineRun := &tektonv1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipeline-run", + Namespace: "default", + }, + } + Expect(adapter.registerFinalProcessingData(pipelineRun)).To(Succeed()) + Expect(adapter.release.Status.FinalProcessing.PipelineRun).To(Equal(fmt.Sprintf("%s%c%s", + pipelineRun.Namespace, types.Separator, pipelineRun.Name))) + + }) + }) + When("registerTenantProcessingStatus is called", func() { var adapter *adapter @@ -1810,6 +2266,49 @@ var _ = Describe("Release adapter", Ordered, func() { }) }) + When("registerFinalProcessingStatus is called", func() { + var adapter *adapter + + AfterEach(func() { + _ = adapter.client.Delete(ctx, adapter.release) + }) + + BeforeEach(func() { + adapter = createReleaseAndAdapter() + }) + + It("does nothing if there is no PipelineRun", func() { + Expect(adapter.registerFinalProcessingStatus(nil)).To(Succeed()) + Expect(adapter.release.Status.FinalProcessing.CompletionTime).To(BeNil()) + }) + + It("does nothing if the PipelineRun is not done", func() { + pipelineRun := &tektonv1.PipelineRun{} + Expect(adapter.registerFinalProcessingStatus(pipelineRun)).To(Succeed()) + Expect(adapter.release.Status.FinalProcessing.CompletionTime).To(BeNil()) + }) + + It("sets the Release as Final Processed if the PipelineRun succeeded", func() { + pipelineRun := &tektonv1.PipelineRun{} + pipelineRun.Status.MarkSucceeded("", "") + adapter.release.MarkFinalPipelineProcessing() + + Expect(adapter.registerFinalProcessingStatus(pipelineRun)).To(Succeed()) + Expect(adapter.release.IsFinalPipelineProcessed()).To(BeTrue()) + }) + + It("sets the Release as Final Processing failed if the PipelineRun didn't succeed", func() { + pipelineRun := &tektonv1.PipelineRun{} + pipelineRun.Status.MarkFailed("", "") + adapter.release.MarkFinalPipelineProcessing() + + Expect(adapter.registerFinalProcessingStatus(pipelineRun)).To(Succeed()) + Expect(adapter.release.HasFinalPipelineProcessingFinished()).To(BeTrue()) + Expect(adapter.release.IsFinalPipelineProcessed()).To(BeFalse()) + }) + + }) + When("calling validateAuthor", func() { var adapter *adapter var conditionMsg string @@ -2210,7 +2709,8 @@ var _ = Describe("Release adapter", Ordered, func() { }, Spec: v1alpha1.ReleasePlanSpec{ Application: application.Name, - Pipeline: parameterizedPipeline, + TenantPipeline: parameterizedPipeline, + FinalPipeline: parameterizedPipeline, ReleaseGracePeriodDays: 6, Target: "default", }, @@ -2234,7 +2734,8 @@ var _ = Describe("Release adapter", Ordered, func() { }, Spec: v1alpha1.ReleasePlanSpec{ Application: application.Name, - Pipeline: parameterizedPipeline, + TenantPipeline: parameterizedPipeline, + FinalPipeline: parameterizedPipeline, ReleaseGracePeriodDays: 6, Target: "default", }, @@ -2247,7 +2748,7 @@ var _ = Describe("Release adapter", Ordered, func() { Expect(result.Err).NotTo(HaveOccurred()) }) - It("should return true if only ReleasePlan has Pipeline Set and it has no Target", func() { + It("should return true if only ReleasePlan has TenantPipeline Set and it has no Target", func() { adapter.ctx = toolkit.GetMockedContext(ctx, []toolkit.MockData{ { ContextKey: loader.ReleasePlanContextKey, @@ -2258,7 +2759,7 @@ var _ = Describe("Release adapter", Ordered, func() { }, Spec: v1alpha1.ReleasePlanSpec{ Application: application.Name, - Pipeline: parameterizedPipeline, + TenantPipeline: parameterizedPipeline, ReleaseGracePeriodDays: 6, }, }, @@ -2393,7 +2894,8 @@ var _ = Describe("Release adapter", Ordered, func() { }, Spec: v1alpha1.ReleasePlanSpec{ Application: application.Name, - Pipeline: parameterizedPipeline, + TenantPipeline: parameterizedPipeline, + FinalPipeline: parameterizedPipeline, ReleaseGracePeriodDays: 6, }, }, diff --git a/controllers/release/controller.go b/controllers/release/controller.go index 0c215c414..424f127b9 100644 --- a/controllers/release/controller.go +++ b/controllers/release/controller.go @@ -85,6 +85,8 @@ func (c *Controller) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu adapter.EnsureTenantPipelineProcessingIsTracked, adapter.EnsureManagedPipelineIsProcessed, adapter.EnsureManagedPipelineProcessingIsTracked, + adapter.EnsureFinalPipelineIsProcessed, + adapter.EnsureFinalPipelineProcessingIsTracked, adapter.EnsureReleaseProcessingResourcesAreCleanedUp, adapter.EnsureReleaseIsCompleted, }) diff --git a/controllers/releaseplan/adapter_test.go b/controllers/releaseplan/adapter_test.go index 18813d79d..2227f768d 100644 --- a/controllers/releaseplan/adapter_test.go +++ b/controllers/releaseplan/adapter_test.go @@ -214,7 +214,8 @@ var _ = Describe("ReleasePlan adapter", Ordered, func() { Spec: v1alpha1.ReleasePlanSpec{ Application: application.Name, Target: "default", - Pipeline: parameterizedPipeline, + TenantPipeline: parameterizedPipeline, + FinalPipeline: parameterizedPipeline, ReleaseGracePeriodDays: 6, }, } diff --git a/loader/loader.go b/loader/loader.go index bf06e3477..f4314f25c 100644 --- a/loader/loader.go +++ b/loader/loader.go @@ -245,7 +245,7 @@ func (l *loader) GetRoleBindingFromReleaseStatus(ctx context.Context, cli client // GetReleasePipelineRun returns the Release PipelineRun of the specified type referenced by the given Release // or nil if it's not found. In the case the List operation fails, an error will be returned. func (l *loader) GetReleasePipelineRun(ctx context.Context, cli client.Client, release *v1alpha1.Release, pipelineType string) (*tektonv1.PipelineRun, error) { - if pipelineType != metadata.ManagedPipelineType && pipelineType != metadata.TenantPipelineType { + if pipelineType != metadata.ManagedPipelineType && pipelineType != metadata.TenantPipelineType && pipelineType != metadata.FinalPipelineType { return nil, fmt.Errorf("cannot fetch Release PipelineRun with invalid type %s", pipelineType) } diff --git a/loader/loader_test.go b/loader/loader_test.go index b84c8e961..604d60393 100644 --- a/loader/loader_test.go +++ b/loader/loader_test.go @@ -34,6 +34,7 @@ var _ = Describe("Release Adapter", Ordered, func() { component *applicationapiv1alpha1.Component enterpriseContractConfigMap *corev1.ConfigMap enterpriseContractPolicy *ecapiv1alpha1.EnterpriseContractPolicy + finalPipelineRun *tektonv1.PipelineRun managedPipelineRun *tektonv1.PipelineRun tenantPipelineRun *tektonv1.PipelineRun release *v1alpha1.Release @@ -366,6 +367,14 @@ var _ = Describe("Release Adapter", Ordered, func() { }) When("calling GetReleasePipelineRun", func() { + + It("returns a Final PipelineRun if the labels match with the release data", func() { + returnedObject, err := loader.GetReleasePipelineRun(ctx, k8sClient, release, metadata.FinalPipelineType) + Expect(err).NotTo(HaveOccurred()) + Expect(returnedObject).NotTo(Equal(&tektonv1.PipelineRun{})) + Expect(returnedObject.Name).To(Equal(finalPipelineRun.Name)) + }) + It("returns a Managed PipelineRun if the labels match with the release data", func() { returnedObject, err := loader.GetReleasePipelineRun(ctx, k8sClient, release, metadata.ManagedPipelineType) Expect(err).NotTo(HaveOccurred()) @@ -569,6 +578,19 @@ var _ = Describe("Release Adapter", Ordered, func() { } Expect(k8sClient.Create(ctx, release)).To(Succeed()) + finalPipelineRun = &tektonv1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + metadata.ReleaseNameLabel: release.Name, + metadata.ReleaseNamespaceLabel: release.Namespace, + metadata.PipelinesTypeLabel: metadata.FinalPipelineType, + }, + Name: "final-pipeline-run", + Namespace: "default", + }, + } + Expect(k8sClient.Create(ctx, finalPipelineRun)).To(Succeed()) + managedPipelineRun = &tektonv1.PipelineRun{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ @@ -600,6 +622,7 @@ var _ = Describe("Release Adapter", Ordered, func() { Expect(k8sClient.Delete(ctx, application)).To(Succeed()) Expect(k8sClient.Delete(ctx, component)).To(Succeed()) Expect(k8sClient.Delete(ctx, enterpriseContractPolicy)).To(Succeed()) + Expect(k8sClient.Delete(ctx, finalPipelineRun)).To(Succeed()) Expect(k8sClient.Delete(ctx, managedPipelineRun)).To(Succeed()) Expect(k8sClient.Delete(ctx, tenantPipelineRun)).To(Succeed()) Expect(k8sClient.Delete(ctx, release)).To(Succeed()) diff --git a/metadata/labels.go b/metadata/labels.go index 81a89d36f..a78abb675 100644 --- a/metadata/labels.go +++ b/metadata/labels.go @@ -65,6 +65,9 @@ var ( // TenantPipelineType is the value to be used in the PipelinesTypeLabel for tenant Pipelines TenantPipelineType = "tenant" + // FinalPipelineType is the value to be used in the PipelinesTypeLabel for final Pipelines + FinalPipelineType = "final" + // PipelinesTypeLabel is the label used to describe the type of pipeline PipelinesTypeLabel = fmt.Sprintf("%s/%s", pipelinesLabelPrefix, "type")