From 42f7a428a67ee20495473881b83e26349eca09ba Mon Sep 17 00:00:00 2001 From: Hongwei Liu Date: Fri, 10 Jan 2025 16:59:27 +0800 Subject: [PATCH] feat(KONFLUX-5971): set intg test status in git according to PR bld PLR * set integration tests status to pending when build plr is triggered or retriggered * set integration test status to failed/cancelled when build plr fails * set integration test status to failed/cancelled with failure reason when snapshot is not created to show to users on git provider Signed-off-by: Hongwei Liu --- docs/build_pipeline_controller.md | 8 + gitops/snapshot.go | 3 + helpers/build.go | 9 +- .../buildpipeline/buildpipeline_adapter.go | 193 +++++++++++++++ .../buildpipeline_adapter_test.go | 220 +++++++++++++++++- .../buildpipeline/buildpipeline_controller.go | 2 + .../controller/snapshot/snapshot_adapter.go | 2 +- .../snapshot/snapshot_adapter_test.go | 2 +- .../statusreport/statusreport_adapter.go | 2 +- .../integration_test_status.go | 13 +- .../integrationteststatus_enumer.go | 23 +- status/reporter_github.go | 17 +- status/reporter_gitlab.go | 7 +- status/status.go | 12 +- status/status_test.go | 16 ++ 15 files changed, 495 insertions(+), 34 deletions(-) diff --git a/docs/build_pipeline_controller.md b/docs/build_pipeline_controller.md index a7806a1fa..e4b63619f 100644 --- a/docs/build_pipeline_controller.md +++ b/docs/build_pipeline_controller.md @@ -16,6 +16,7 @@ new_pipeline_run_without_prgroup{PR group is added to pipelineRun metadata?} get_pipeline_run{Pipeline updated?} failed_pipeline_run{Pipeline failed?} finalizer_exists{Does the finalizer already exist?} +need_to_set_integration_test{Build pipelineRun is newly triggered?
Or Build pipelineRun failed?
Or Failing to create snapshot?} retrieve_associated_entity(Retrieve the entity
component/application) determine_snapshot{Does a snapshot exist?} prep_snapshot(Gather Application components
Add new component) @@ -28,12 +29,15 @@ continue[Continue processing] update_metadata(add PR group info to build pipelineRun metadata) notify_pr_group_failure(annotate Snapshots and in-flight builds in PR group with failure message) failed_group_pipeline_run{Pipeline failed?} +update_integrationTestStatus_in_git_provider(Create checkRun/commitStatus in
git provider) +update_build_plr_annotation(Update build pipelineRun annotation
test.appstudio.openshift.io/snapshot-creation-report
with the status) %% Node connections predicate --> get_pipeline_run predicate --> new_pipeline_run predicate --> new_pipeline_run_without_prgroup predicate --> failed_pipeline_run +predicate --> need_to_set_integration_test new_pipeline_run --Yes--> finalizer_exists finalizer_exists --No--> add_finalizer add_finalizer --> continue @@ -55,6 +59,10 @@ prep_snapshot --> check_chains check_chains --Yes --> annotate_pipelineRun annotate_pipelineRun --Yes --> remove_finalizer remove_finalizer --> continue +need_to_set_integration_test --Yes --> update_integrationTestStatus_in_git_provider +need_to_set_integration_test --No --> continue +update_integrationTestStatus_in_git_provider --> update_build_plr_annotation +update_build_plr_annotation --> continue %% Assigning styles to nodes class predicate Amber; diff --git a/gitops/snapshot.go b/gitops/snapshot.go index 952e6c6bb..77fea560a 100644 --- a/gitops/snapshot.go +++ b/gitops/snapshot.go @@ -233,6 +233,9 @@ const ( //IntegrationTestStatusInProgressGithub is the status reported to github when integration test is in progress IntegrationTestStatusInProgressGithub = "in_progress" + + //IntegrationTestStatusCancelledGithub is the status reported to github when integration test is cancelled + IntegrationTestStatusCancelledGithub = "cancelled" ) var ( diff --git a/helpers/build.go b/helpers/build.go index b5a10a3a6..fe447907a 100644 --- a/helpers/build.go +++ b/helpers/build.go @@ -1,3 +1,10 @@ package helpers -const CreateSnapshotAnnotationName = "test.appstudio.openshift.io/create-snapshot-status" +const ( + // CreateSnapshotAnnotationName contains metadata of snapshot creation failure or success + CreateSnapshotAnnotationName = "test.appstudio.openshift.io/create-snapshot-status" + + // SnapshotCreationReportAnnotation contains metadata of snapshot creation status reporting to git provider + // to initialize integration test or set it to cancelled or failed + SnapshotCreationReportAnnotation = "test.appstudio.openshift.io/snapshot-creation-report" +) diff --git a/internal/controller/buildpipeline/buildpipeline_adapter.go b/internal/controller/buildpipeline/buildpipeline_adapter.go index acff16c05..cae384d3c 100644 --- a/internal/controller/buildpipeline/buildpipeline_adapter.go +++ b/internal/controller/buildpipeline/buildpipeline_adapter.go @@ -26,14 +26,18 @@ import ( "k8s.io/client-go/util/retry" applicationapiv1alpha1 "github.com/konflux-ci/application-api/api/v1alpha1" + "github.com/konflux-ci/integration-service/api/v1beta2" "github.com/konflux-ci/integration-service/gitops" h "github.com/konflux-ci/integration-service/helpers" "github.com/konflux-ci/integration-service/loader" + intgteststat "github.com/konflux-ci/integration-service/pkg/integrationteststatus" + "github.com/konflux-ci/integration-service/status" "github.com/konflux-ci/integration-service/tekton" "github.com/konflux-ci/operator-toolkit/controller" "github.com/konflux-ci/operator-toolkit/metadata" tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) @@ -47,6 +51,7 @@ type Adapter struct { logger h.IntegrationLogger client client.Client context context.Context + status status.StatusInterface } // NewAdapter creates and returns an Adapter instance. @@ -61,6 +66,7 @@ func NewAdapter(context context.Context, pipelineRun *tektonv1.PipelineRun, comp loader: loader, client: client, context: context, + status: status.NewStatus(logger.Logger, client), } } @@ -189,6 +195,69 @@ func (a *Adapter) EnsurePRGroupAnnotated() (controller.OperationResult, error) { return controller.ContinueProcessing() } +// EnsureIntegrationTestReportedToGitProvider is an operation that will ensure that the integration test status is initialized as pending +// state when build PLR is triggered/retriggered or cancelled/failed when failing to create snapshot +// to prevent PR/MR from being automerged unexpectedly and also show the snapshot creation failure on PR/MR +func (a *Adapter) EnsureIntegrationTestReportedToGitProvider() (controller.OperationResult, error) { + if tekton.IsPLRCreatedByPACPushEvent(a.pipelineRun) { + a.logger.Info("build pipelineRun is not created by pull/merge request, no need to set integration test status in git provider") + return controller.ContinueProcessing() + } + + if metadata.HasAnnotation(a.pipelineRun, tekton.SnapshotNameLabel) { + a.logger.Info("snapshot has been created for build pipelineRun, no need to report integration status from build pipelinerun status") + if err := tekton.AnnotateBuildPipelineRun(a.context, a.pipelineRun, h.SnapshotCreationReportAnnotation, "SnapshotCreated", a.client); err != nil { + a.logger.Error(err, "failed to annotate build pipelineRun") + return controller.RequeueWithError(err) + } + return controller.ContinueProcessing() + } + + integrationTestStatus := getIntegrationTestStatusFromBuildPLR(a.pipelineRun) + + if integrationTestStatus == intgteststat.IntegrationTestStatus(0) { + a.logger.Info("integration test has been set correctly or is being processed, no need to set integration test status from build pipelinerun") + return controller.ContinueProcessing() + } + + a.logger.Info("try to set integration test status according to the build PLR status") + tempSnapshot := a.prepareTemporarySnapshot(a.pipelineRun) + + allIntegrationTestScenarios, err := a.loader.GetAllIntegrationTestScenariosForApplication(a.context, a.client, a.application) + if err != nil { + a.logger.Error(err, "Failed to get integration test scenarios for the following application", + "Application.Namespace", a.application.Namespace, "Application.Name", a.application.Name) + return controller.RequeueWithError(err) + } + + if allIntegrationTestScenarios != nil { + // Handle context in integrationTestScenario, but defer handling of 'group' for now + integrationTestScenarios := gitops.FilterIntegrationTestScenariosWithContext(allIntegrationTestScenarios, tempSnapshot) + a.logger.Info( + fmt.Sprintf("Found %d IntegrationTestScenarios for application", len(*integrationTestScenarios)), + "Application.Name", a.application.Name, + "IntegrationTestScenarios", len(*integrationTestScenarios)) + if len(*integrationTestScenarios) == 0 { + a.logger.Info("no need to report integration test status since no integrationTestScenario can be applied to snapshot created for build pipelinerun") + return controller.ContinueProcessing() + } + + err := a.ReportIntegrationTestStatusAccordingToBuildPLR(a.pipelineRun, tempSnapshot, integrationTestScenarios, integrationTestStatus, a.component) + if err != nil { + a.logger.Error(err, "failed to report snapshot creation status to git provider from build pipelineRun", + "pipelineRun.Namespace", a.pipelineRun.Namespace, "pipelineRun.Name", a.pipelineRun.Name) + return controller.RequeueWithError(err) + } + + if err = tekton.AnnotateBuildPipelineRun(a.context, a.pipelineRun, h.SnapshotCreationReportAnnotation, integrationTestStatus.String(), a.client); err != nil { + a.logger.Error(err, fmt.Sprintf("failed to write build plr annotation %s", h.SnapshotCreationReportAnnotation)) + return controller.RequeueWithError(fmt.Errorf("failed to write snapshot report status metadata for annotation %s: %w", h.SnapshotCreationReportAnnotation, err)) + } + } + return controller.ContinueProcessing() + +} + // getImagePullSpecFromPipelineRun gets the full image pullspec from the given build PipelineRun, // In case the Image pullspec can't be composed, an error will be returned. func (a *Adapter) getImagePullSpecFromPipelineRun(pipelineRun *tektonv1.PipelineRun) (string, error) { @@ -454,3 +523,127 @@ func (a *Adapter) addPRGroupToBuildPLRMetadata(pipelineRun *tektonv1.PipelineRun a.logger.Info("can't find source branch info in build PLR, not need to update build pipelineRun metadata") return nil } + +// prepareTemporarySnapshot will create a temporary snapshot object to copy the labels/annotations from build pipelinerun +// and be used to communicate with git provider +func (a *Adapter) prepareTemporarySnapshot(pipelineRun *tektonv1.PipelineRun) *applicationapiv1alpha1.Snapshot { + tempSnapshot := &applicationapiv1alpha1.Snapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tempSnapshot", + Namespace: pipelineRun.Namespace, + }, + } + prefixes := []string{gitops.BuildPipelineRunPrefix, gitops.TestLabelPrefix, gitops.CustomLabelPrefix} + gitops.CopySnapshotLabelsAndAnnotations(a.application, tempSnapshot, a.component.Name, &pipelineRun.ObjectMeta, prefixes) + return tempSnapshot +} + +func (a *Adapter) ReportIntegrationTestStatusAccordingToBuildPLR(pipelineRun *tektonv1.PipelineRun, snapshot *applicationapiv1alpha1.Snapshot, integrationTestScenarios *[]v1beta2.IntegrationTestScenario, + integrationTestStatus intgteststat.IntegrationTestStatus, component *applicationapiv1alpha1.Component) error { + reporter := a.status.GetReporter(snapshot) + if reporter == nil { + a.logger.Info("No suitable reporter found, skipping report") + return nil + } + a.logger.Info(fmt.Sprintf("Detected reporter: %s", reporter.GetReporterName())) + + if err := reporter.Initialize(a.context, snapshot); err != nil { + a.logger.Error(err, "Failed to initialize reporter", "reporter", reporter.GetReporterName()) + return fmt.Errorf("failed to initialize reporter: %w", err) + } + a.logger.Info("Reporter initialized", "reporter", reporter.GetReporterName()) + + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + err := a.iterateIntegrationTestInStatusReport(reporter, pipelineRun, snapshot, integrationTestScenarios, integrationTestStatus, component) + if err != nil { + a.logger.Error(err, fmt.Sprintf("failed to report integration test status according to build pipelinerun %s/%s", + pipelineRun.Namespace, pipelineRun.Name)) + return fmt.Errorf("failed to report integration test status according to build pipelineRun %s/%s: %w", + pipelineRun.Namespace, pipelineRun.Name, err) + } + + a.logger.Info("Successfully report integration test status for build pipelineRun", + "pipelineRun.Namespace", pipelineRun.Namespace, + "pipelineRun.Name", pipelineRun.Name, + "build pipelineRun Status", integrationTestStatus.String()) + + return err + }) + + if err != nil { + return fmt.Errorf("issue occurred during generating or updating report status: %w", err) + } + + a.logger.Info(fmt.Sprintf("Successfully updated the %s annotation", gitops.SnapshotStatusReportAnnotation), "pipelinerun.Name", pipelineRun.Name) + + return nil +} + +// iterates integrationTestScenarios to set integration test status in PR/MR +func (a *Adapter) iterateIntegrationTestInStatusReport(reporter status.ReporterInterface, + buildPLR *tektonv1.PipelineRun, + snapshot *applicationapiv1alpha1.Snapshot, + integrationTestScenarios *[]v1beta2.IntegrationTestScenario, + intgteststatus intgteststat.IntegrationTestStatus, + component *applicationapiv1alpha1.Component) error { + + details := generateDetails(buildPLR, intgteststatus) + integrationTestStatusDetail := intgteststat.IntegrationTestStatusDetail{ + Status: intgteststatus, + Details: details, + } + for _, integrationTestScenario := range *integrationTestScenarios { + integrationTestScenario := integrationTestScenario //G601 + integrationTestStatusDetail.ScenarioName = integrationTestScenario.Name + + testReport, reportErr := status.GenerateTestReport(a.context, a.client, integrationTestStatusDetail, snapshot, component.Name) + if reportErr != nil { + return fmt.Errorf("failed to generate test report: %w", reportErr) + } + if reportStatusErr := reporter.ReportStatus(a.context, *testReport); reportStatusErr != nil { + return fmt.Errorf("failed to report status to git provider: %w", reportStatusErr) + } + } + return nil +} + +// getIntegrationTestStatusFromBuildPLR get the build PLR status to decide the integration test status +// reported to PR/MR when build PLR is triggered/retriggered, build PLR fails or snapshot can't be created +func getIntegrationTestStatusFromBuildPLR(plr *tektonv1.PipelineRun) intgteststat.IntegrationTestStatus { + var integrationTestStatus intgteststat.IntegrationTestStatus + // when build pipeline is triggered/retriggered, we need to set integration test status to pending + if !h.HasPipelineRunFinished(plr) && !metadata.HasAnnotationWithValue(plr, h.SnapshotCreationReportAnnotation, intgteststat.BuildPLRInProgress.String()) { + integrationTestStatus = intgteststat.BuildPLRInProgress + return integrationTestStatus + } + // when build pipeline fails, we need to set integreation test to canceled or failed + if h.HasPipelineRunFinished(plr) && !h.HasPipelineRunSucceeded(plr) && !metadata.HasAnnotationWithValue(plr, h.SnapshotCreationReportAnnotation, intgteststat.BuildPLRFailed.String()) { + integrationTestStatus = intgteststat.BuildPLRFailed + return integrationTestStatus + } + // when build pipeline succeeds but snapshot is not created, we need to set integration test to canceled or failed + if h.HasPipelineRunSucceeded(plr) && metadata.HasAnnotation(plr, h.CreateSnapshotAnnotationName) && !metadata.HasAnnotation(plr, tekton.SnapshotNameLabel) && !metadata.HasAnnotationWithValue(plr, h.SnapshotCreationReportAnnotation, intgteststat.SnapshotCreationFailed.String()) { + integrationTestStatus = intgteststat.SnapshotCreationFailed + return integrationTestStatus + } + return integrationTestStatus +} + +// generateDetails generates details for integrationTestStatusDetail +func generateDetails(buildPLR *tektonv1.PipelineRun, integrationTestStatus intgteststat.IntegrationTestStatus) string { + details := "" + if integrationTestStatus == intgteststat.BuildPLRInProgress { + details = fmt.Sprintf("build pipelinerun %s/%s is still in progress", buildPLR.Namespace, buildPLR.Name) + } + if integrationTestStatus == intgteststat.SnapshotCreationFailed { + if failureReason, ok := buildPLR.Annotations[h.CreateSnapshotAnnotationName]; ok { + details = fmt.Sprintf("build pipelinerun %s/%s succeeds but snapshot is not created due to error: %s", buildPLR.Namespace, buildPLR.Name, failureReason) + } else { + details = fmt.Sprintf("failed to create snapshot but can't find reason from build plr annotation %s", h.CreateSnapshotAnnotationName) + } + } + if integrationTestStatus == intgteststat.BuildPLRFailed { + details = fmt.Sprintf("build pipelinerun %s/%s failed, so that snapshot is not created. Please fix build pipelinerun failure and try again.", buildPLR.Namespace, buildPLR.Name) + } + return details +} diff --git a/internal/controller/buildpipeline/buildpipeline_adapter_test.go b/internal/controller/buildpipeline/buildpipeline_adapter_test.go index d635e6ef2..98841f497 100644 --- a/internal/controller/buildpipeline/buildpipeline_adapter_test.go +++ b/internal/controller/buildpipeline/buildpipeline_adapter_test.go @@ -26,14 +26,18 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "github.com/konflux-ci/integration-service/api/v1beta2" "github.com/konflux-ci/integration-service/gitops" "github.com/konflux-ci/integration-service/helpers" "github.com/konflux-ci/integration-service/loader" + intgteststat "github.com/konflux-ci/integration-service/pkg/integrationteststatus" + "github.com/konflux-ci/integration-service/status" "github.com/konflux-ci/integration-service/tekton" "github.com/konflux-ci/operator-toolkit/metadata" "knative.dev/pkg/apis" v1 "knative.dev/pkg/apis/duck/v1" + "go.uber.org/mock/gomock" k8serrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/strings/slices" @@ -54,16 +58,20 @@ var _ = Describe("Pipeline Adapter", Ordered, func() { var ( adapter *Adapter createAdapter func() *Adapter + buf bytes.Buffer logger helpers.IntegrationLogger - - successfulTaskRun *tektonv1.TaskRun - failedTaskRun *tektonv1.TaskRun - buildPipelineRun *tektonv1.PipelineRun - buildPipelineRun2 *tektonv1.PipelineRun - hasComp *applicationapiv1alpha1.Component - hasComp2 *applicationapiv1alpha1.Component - hasApp *applicationapiv1alpha1.Application - hasSnapshot *applicationapiv1alpha1.Snapshot + mockReporter *status.MockReporterInterface + mockStatus *status.MockStatusInterface + + successfulTaskRun *tektonv1.TaskRun + failedTaskRun *tektonv1.TaskRun + buildPipelineRun *tektonv1.PipelineRun + buildPipelineRun2 *tektonv1.PipelineRun + hasComp *applicationapiv1alpha1.Component + hasComp2 *applicationapiv1alpha1.Component + hasApp *applicationapiv1alpha1.Application + hasSnapshot *applicationapiv1alpha1.Snapshot + integrationTestScenario *v1beta2.IntegrationTestScenario ) const ( SampleRepoLink = "https://github.com/devfile-samples/devfile-sample-java-springboot-basic" @@ -251,6 +259,39 @@ var _ = Describe("Pipeline Adapter", Ordered, func() { }, } Expect(k8sClient.Status().Update(ctx, failedTaskRun)).Should(Succeed()) + + integrationTestScenario = &v1beta2.IntegrationTestScenario{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example-its", + Namespace: "default", + + Labels: map[string]string{ + "test.appstudio.openshift.io/optional": "false", + }, + }, + Spec: v1beta2.IntegrationTestScenarioSpec{ + Application: hasApp.Name, + ResolverRef: v1beta2.ResolverRef{ + Resolver: "git", + Params: []v1beta2.ResolverParameter{ + { + Name: "url", + Value: "https://github.com/redhat-appstudio/integration-examples.git", + }, + { + Name: "revision", + Value: "main", + }, + { + Name: "pathInRepo", + Value: "pipelineruns/integration_pipelinerun_pass.yaml", + }, + }, + }, + }, + } + + Expect(k8sClient.Create(ctx, integrationTestScenario)).Should(Succeed()) }) BeforeEach(func() { @@ -358,6 +399,8 @@ var _ = Describe("Pipeline Adapter", Ordered, func() { Expect(err == nil || k8serrors.IsNotFound(err)).To(BeTrue()) err = k8sClient.Delete(ctx, failedTaskRun) Expect(err == nil || k8serrors.IsNotFound(err)).To(BeTrue()) + err = k8sClient.Delete(ctx, integrationTestScenario) + Expect(err == nil || k8serrors.IsNotFound(err)).To(BeTrue()) }) When("NewAdapter is called", func() { @@ -1411,6 +1454,165 @@ var _ = Describe("Pipeline Adapter", Ordered, func() { }) }) + + When("a build PLR is triggered or retirggered, succeeded or failed", func() { + BeforeEach(func() { + ctrl := gomock.NewController(GinkgoT()) + mockReporter = status.NewMockReporterInterface(ctrl) + mockStatus = status.NewMockStatusInterface(ctrl) + mockReporter.EXPECT().GetReporterName().Return("mocked-reporter").AnyTimes() + mockStatus.EXPECT().GetReporter(gomock.Any()).Return(mockReporter) + mockStatus.EXPECT().GetReporter(gomock.Any()).AnyTimes() + mockReporter.EXPECT().GetReporterName().AnyTimes() + mockReporter.EXPECT().Initialize(gomock.Any(), gomock.Any()).Times(1) + mockReporter.EXPECT().ReportStatus(gomock.Any(), gomock.Any()).Times(1) + }) + It("ensure integration test is initialized from build PLR", func() { + buildPipelineRun.Status = tektonv1.PipelineRunStatus{ + Status: v1.Status{ + Conditions: v1.Conditions{ + apis.Condition{ + Reason: "Running", + Status: "Unknown", + Type: apis.ConditionSucceeded, + }, + }, + }, + } + Expect(k8sClient.Status().Update(ctx, buildPipelineRun)).Should(Succeed()) + + buf = bytes.Buffer{} + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + adapter = NewAdapter(ctx, buildPipelineRun, hasComp, hasApp, log, loader.NewMockLoader(), k8sClient) + adapter.status = mockStatus + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ApplicationContextKey, + Resource: hasApp, + }, + { + ContextKey: loader.AllIntegrationTestScenariosContextKey, + Resource: []v1beta2.IntegrationTestScenario{*integrationTestScenario}, + }, + }) + + result, err := adapter.EnsureIntegrationTestReportedToGitProvider() + Expect(!result.CancelRequest && err == nil).To(BeTrue()) + Expect(metadata.HasAnnotationWithValue(buildPipelineRun, helpers.SnapshotCreationReportAnnotation, intgteststat.BuildPLRInProgress.String())).To(BeTrue()) + + result, err = adapter.EnsureIntegrationTestReportedToGitProvider() + Expect(!result.CancelRequest && err == nil).To(BeTrue()) + expectedLogEntry := "integration test has been set correctly or is being processed, no need to set integration test status from build pipelinerun" + Expect(buf.String()).Should(ContainSubstring(expectedLogEntry)) + }) + + It("ensure integration test is set from build PLR when build PLR fails", func() { + buildPipelineRun.Status = tektonv1.PipelineRunStatus{ + Status: v1.Status{ + Conditions: v1.Conditions{ + apis.Condition{ + Reason: "Failed", + Status: "False", + Type: apis.ConditionSucceeded, + }, + }, + }, + } + Expect(k8sClient.Status().Update(ctx, buildPipelineRun)).Should(Succeed()) + + buf = bytes.Buffer{} + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + adapter = NewAdapter(ctx, buildPipelineRun, hasComp, hasApp, log, loader.NewMockLoader(), k8sClient) + adapter.status = mockStatus + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ApplicationContextKey, + Resource: hasApp, + }, + { + ContextKey: loader.AllIntegrationTestScenariosContextKey, + Resource: []v1beta2.IntegrationTestScenario{*integrationTestScenario}, + }, + }) + + result, err := adapter.EnsureIntegrationTestReportedToGitProvider() + Expect(!result.CancelRequest && err == nil).To(BeTrue()) + Expect(metadata.HasAnnotationWithValue(buildPipelineRun, helpers.SnapshotCreationReportAnnotation, intgteststat.BuildPLRFailed.String())).To(BeTrue()) + + result, err = adapter.EnsureIntegrationTestReportedToGitProvider() + Expect(!result.CancelRequest && err == nil).To(BeTrue()) + expectedLogEntry := "integration test has been set correctly or is being processed, no need to set integration test status from build pipelinerun" + Expect(buf.String()).Should(ContainSubstring(expectedLogEntry)) + }) + + It("ensure integration test is set from build PLR when build PLR succeeded but snapshot is not created", func() { + Expect(metadata.SetAnnotation(buildPipelineRun, helpers.CreateSnapshotAnnotationName, "failed to create snapshot due to error")).ShouldNot(HaveOccurred()) + buf = bytes.Buffer{} + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + adapter = NewAdapter(ctx, buildPipelineRun, hasComp, hasApp, log, loader.NewMockLoader(), k8sClient) + adapter.status = mockStatus + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ApplicationContextKey, + Resource: hasApp, + }, + { + ContextKey: loader.AllIntegrationTestScenariosContextKey, + Resource: []v1beta2.IntegrationTestScenario{*integrationTestScenario}, + }, + }) + + result, err := adapter.EnsureIntegrationTestReportedToGitProvider() + Expect(!result.CancelRequest && err == nil).To(BeTrue()) + Expect(metadata.HasAnnotationWithValue(buildPipelineRun, helpers.SnapshotCreationReportAnnotation, intgteststat.SnapshotCreationFailed.String())).To(BeTrue()) + + result, err = adapter.EnsureIntegrationTestReportedToGitProvider() + Expect(!result.CancelRequest && err == nil).To(BeTrue()) + expectedLogEntry := "integration test has been set correctly or is being processed, no need to set integration test status from build pipelinerun" + Expect(buf.String()).Should(ContainSubstring(expectedLogEntry)) + }) + }) + + When("integration status should not be set from build PLR", func() { + It("integration test will not be set from build PLR when build PLR succeeded and snapshot is created", func() { + Expect(metadata.SetAnnotation(buildPipelineRun, tekton.SnapshotNameLabel, "snashot-sample")).ShouldNot(HaveOccurred()) + buf = bytes.Buffer{} + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + adapter = NewAdapter(ctx, buildPipelineRun, hasComp, hasApp, log, loader.NewMockLoader(), k8sClient) + adapter.status = mockStatus + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ApplicationContextKey, + Resource: hasApp, + }, + }) + + result, err := adapter.EnsureIntegrationTestReportedToGitProvider() + Expect(!result.CancelRequest && err == nil).To(BeTrue()) + Expect(metadata.HasAnnotationWithValue(buildPipelineRun, helpers.SnapshotCreationReportAnnotation, "SnapshotCreated")).To(BeTrue()) + expectedLogEntry := "snapshot has been created for build pipelineRun, no need to report integration status from build pipelinerun status" + Expect(buf.String()).Should(ContainSubstring(expectedLogEntry)) + }) + + It("integration test will not be set from build PLR when build PLR is not from pac pull request event", func() { + Expect(metadata.DeleteLabel(buildPipelineRun, tekton.PipelineAsCodePullRequestLabel)).ShouldNot(HaveOccurred()) + buf = bytes.Buffer{} + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + adapter = NewAdapter(ctx, buildPipelineRun, hasComp, hasApp, log, loader.NewMockLoader(), k8sClient) + adapter.status = mockStatus + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ApplicationContextKey, + Resource: hasApp, + }, + }) + + result, err := adapter.EnsureIntegrationTestReportedToGitProvider() + Expect(!result.CancelRequest && err == nil).To(BeTrue()) + expectedLogEntry := "build pipelineRun is not created by pull/merge request, no need to set integration test status in git provider" + Expect(buf.String()).Should(ContainSubstring(expectedLogEntry)) + }) + }) createAdapter = func() *Adapter { adapter = NewAdapter(ctx, buildPipelineRun, hasComp, hasApp, logger, loader.NewMockLoader(), k8sClient) return adapter diff --git a/internal/controller/buildpipeline/buildpipeline_controller.go b/internal/controller/buildpipeline/buildpipeline_controller.go index e3a85bc04..8eb7e74af 100644 --- a/internal/controller/buildpipeline/buildpipeline_controller.go +++ b/internal/controller/buildpipeline/buildpipeline_controller.go @@ -119,6 +119,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu adapter.EnsurePipelineIsFinalized, adapter.EnsurePRGroupAnnotated, adapter.EnsureSnapshotExists, + adapter.EnsureIntegrationTestReportedToGitProvider, }) } @@ -127,6 +128,7 @@ type AdapterInterface interface { EnsurePipelineIsFinalized() (controller.OperationResult, error) EnsurePRGroupAnnotated() (controller.OperationResult, error) EnsureSnapshotExists() (controller.OperationResult, error) + EnsureIntegrationTestReportedToGitProvider() (controller.OperationResult, error) } // SetupController creates a new Integration controller and adds it to the Manager. diff --git a/internal/controller/snapshot/snapshot_adapter.go b/internal/controller/snapshot/snapshot_adapter.go index e0b1ad7ce..8250ee36c 100644 --- a/internal/controller/snapshot/snapshot_adapter.go +++ b/internal/controller/snapshot/snapshot_adapter.go @@ -221,7 +221,7 @@ func (a *Adapter) EnsureIntegrationPipelineRunsExist() (controller.OperationResu allIntegrationTestScenarios, err := a.loader.GetAllIntegrationTestScenariosForApplication(a.context, a.client, a.application) if err != nil { - a.logger.Error(err, "Failed to get Integration test scenarios for the following application", + a.logger.Error(err, "Failed to get integration test scenarios for the following application", "Application.Namespace", a.application.Namespace) } diff --git a/internal/controller/snapshot/snapshot_adapter_test.go b/internal/controller/snapshot/snapshot_adapter_test.go index b806e65dd..3d5025d6f 100644 --- a/internal/controller/snapshot/snapshot_adapter_test.go +++ b/internal/controller/snapshot/snapshot_adapter_test.go @@ -1076,7 +1076,7 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { }, }) result, err := adapter.EnsureIntegrationPipelineRunsExist() - Expect(buf.String()).Should(ContainSubstring("Failed to get Integration test scenarios for the following application")) + Expect(buf.String()).Should(ContainSubstring("Failed to get integration test scenarios for the following application")) Expect(buf.String()).Should(ContainSubstring("Failed to get all required IntegrationTestScenarios")) Expect(result.CancelRequest).To(BeTrue()) Expect(result.RequeueRequest).To(BeFalse()) diff --git a/internal/controller/statusreport/statusreport_adapter.go b/internal/controller/statusreport/statusreport_adapter.go index c62016e25..a2711cf99 100644 --- a/internal/controller/statusreport/statusreport_adapter.go +++ b/internal/controller/statusreport/statusreport_adapter.go @@ -291,7 +291,7 @@ func (a *Adapter) ReportSnapshotStatus(testedSnapshot *applicationapiv1alpha1.Sn } if err != nil { - return fmt.Errorf("issue occured during generating or updating report status: %w", err) + return fmt.Errorf("issue occurred during generating or updating report status: %w", err) } a.logger.Info(fmt.Sprintf("Successfully updated the %s annotation", gitops.SnapshotStatusReportAnnotation), "snapshot.Name", testedSnapshot.Name) diff --git a/pkg/integrationteststatus/integration_test_status.go b/pkg/integrationteststatus/integration_test_status.go index aa4c2d78b..d890681ed 100644 --- a/pkg/integrationteststatus/integration_test_status.go +++ b/pkg/integrationteststatus/integration_test_status.go @@ -45,6 +45,12 @@ const ( IntegrationTestStatusTestPassed // TestPassed // Integration PLR is invalid IntegrationTestStatusTestInvalid // TestInvalid + // Build PLR is in progress + BuildPLRInProgress // BuildPLRInProgress + // Snapshot is not created + SnapshotCreationFailed // SnapshotCreationFailed + // Build pipelinerun failed + BuildPLRFailed // BuildPLRFailed ) const integrationTestStatusesSchema = `{ @@ -168,7 +174,7 @@ func (sits *SnapshotIntegrationTestStatuses) UpdateTestStatusIfChanged(scenarioN detail.StartTime = ×tamp // null CompletionTime because testing started again detail.CompletionTime = nil - case IntegrationTestStatusPending: + case IntegrationTestStatusPending, BuildPLRInProgress: // null all timestamps as test is not inProgress neither in final state detail.StartTime = nil detail.CompletionTime = nil @@ -177,7 +183,10 @@ func (sits *SnapshotIntegrationTestStatuses) UpdateTestStatusIfChanged(scenarioN IntegrationTestStatusDeleted, IntegrationTestStatusTestFail, IntegrationTestStatusTestPassed, - IntegrationTestStatusTestInvalid: + IntegrationTestStatusTestInvalid, + SnapshotCreationFailed, + BuildPLRFailed: + detail.CompletionTime = ×tamp } } diff --git a/pkg/integrationteststatus/integrationteststatus_enumer.go b/pkg/integrationteststatus/integrationteststatus_enumer.go index 3038eb148..768da226e 100644 --- a/pkg/integrationteststatus/integrationteststatus_enumer.go +++ b/pkg/integrationteststatus/integrationteststatus_enumer.go @@ -7,9 +7,9 @@ import ( "fmt" ) -const _IntegrationTestStatusName = "PendingInProgressDeletedEnvironmentProvisionErrorDeploymentErrorTestFailTestPassedTestInvalid" +const _IntegrationTestStatusName = "PendingInProgressDeletedEnvironmentProvisionErrorDeploymentErrorTestFailTestPassedTestInvalidBuildPLRInProgressSnapshotCreationFailedBuildPLRFailed" -var _IntegrationTestStatusIndex = [...]uint8{0, 7, 17, 24, 49, 64, 72, 82, 93} +var _IntegrationTestStatusIndex = [...]uint8{0, 7, 17, 24, 49, 64, 72, 82, 93, 111, 133, 147} func (i IntegrationTestStatus) String() string { i -= 1 @@ -22,14 +22,17 @@ func (i IntegrationTestStatus) String() string { var _IntegrationTestStatusValues = []IntegrationTestStatus{1, 2, 3, 4, 5, 6, 7, 8} var _IntegrationTestStatusNameToValueMap = map[string]IntegrationTestStatus{ - _IntegrationTestStatusName[0:7]: 1, - _IntegrationTestStatusName[7:17]: 2, - _IntegrationTestStatusName[17:24]: 3, - _IntegrationTestStatusName[24:49]: 4, - _IntegrationTestStatusName[49:64]: 5, - _IntegrationTestStatusName[64:72]: 6, - _IntegrationTestStatusName[72:82]: 7, - _IntegrationTestStatusName[82:93]: 8, + _IntegrationTestStatusName[0:7]: 1, + _IntegrationTestStatusName[7:17]: 2, + _IntegrationTestStatusName[17:24]: 3, + _IntegrationTestStatusName[24:49]: 4, + _IntegrationTestStatusName[49:64]: 5, + _IntegrationTestStatusName[64:72]: 6, + _IntegrationTestStatusName[72:82]: 7, + _IntegrationTestStatusName[82:93]: 8, + _IntegrationTestStatusName[93:111]: 9, + _IntegrationTestStatusName[111:133]: 10, + _IntegrationTestStatusName[133:147]: 11, } // IntegrationTestStatusString retrieves an enum value from the enum constants string name. diff --git a/status/reporter_github.go b/status/reporter_github.go index 22f1bead5..a837665ed 100644 --- a/status/reporter_github.go +++ b/status/reporter_github.go @@ -518,7 +518,7 @@ func generateCheckRunTitle(state intgteststat.IntegrationTestStatus) (string, er var title string switch state { - case intgteststat.IntegrationTestStatusPending: + case intgteststat.IntegrationTestStatusPending, intgteststat.BuildPLRInProgress: title = "Pending" case intgteststat.IntegrationTestStatusInProgress: title = "In Progress" @@ -530,7 +530,9 @@ func generateCheckRunTitle(state intgteststat.IntegrationTestStatus) (string, er title = "Deleted" case intgteststat.IntegrationTestStatusTestPassed: title = "Succeeded" - case intgteststat.IntegrationTestStatusTestFail: + case intgteststat.IntegrationTestStatusTestFail, + intgteststat.SnapshotCreationFailed, + intgteststat.BuildPLRFailed: title = "Failed" default: return title, fmt.Errorf("unknown status") @@ -552,8 +554,11 @@ func generateCheckRunConclusion(state intgteststat.IntegrationTestStatus) (strin conclusion = gitops.IntegrationTestStatusFailureGithub case intgteststat.IntegrationTestStatusTestPassed: conclusion = gitops.IntegrationTestStatusSuccessGithub - case intgteststat.IntegrationTestStatusPending, intgteststat.IntegrationTestStatusInProgress: + case intgteststat.IntegrationTestStatusPending, intgteststat.IntegrationTestStatusInProgress, + intgteststat.BuildPLRInProgress: conclusion = "" + case intgteststat.SnapshotCreationFailed, intgteststat.BuildPLRFailed: + conclusion = gitops.IntegrationTestStatusCancelledGithub default: return conclusion, fmt.Errorf("unknown status") } @@ -568,14 +573,16 @@ func generateGithubCommitState(state intgteststat.IntegrationTestStatus) (string var commitState string switch state { - case intgteststat.IntegrationTestStatusTestFail: + case intgteststat.IntegrationTestStatusTestFail, intgteststat.SnapshotCreationFailed, + intgteststat.BuildPLRFailed: commitState = gitops.IntegrationTestStatusFailureGithub case intgteststat.IntegrationTestStatusEnvironmentProvisionError_Deprecated, intgteststat.IntegrationTestStatusDeploymentError_Deprecated, intgteststat.IntegrationTestStatusDeleted, intgteststat.IntegrationTestStatusTestInvalid: commitState = gitops.IntegrationTestStatusErrorGithub case intgteststat.IntegrationTestStatusTestPassed: commitState = gitops.IntegrationTestStatusSuccessGithub - case intgteststat.IntegrationTestStatusPending, intgteststat.IntegrationTestStatusInProgress: + case intgteststat.IntegrationTestStatusPending, intgteststat.IntegrationTestStatusInProgress, + intgteststat.BuildPLRInProgress: commitState = gitops.IntegrationTestStatusPendingGithub default: return commitState, fmt.Errorf("unknown status") diff --git a/status/reporter_gitlab.go b/status/reporter_gitlab.go index d97ef438b..cb8286760 100644 --- a/status/reporter_gitlab.go +++ b/status/reporter_gitlab.go @@ -284,7 +284,7 @@ func (r *GitLabReporter) ReportStatus(ctx context.Context, report TestReport) er // Create a note when integration test is neither pending nor inprogress since comment for pending/inprogress is less meaningful _, isMergeRequest := r.snapshot.GetAnnotations()[gitops.PipelineAsCodePullRequestAnnotation] - if report.Status != intgteststat.IntegrationTestStatusPending && report.Status != intgteststat.IntegrationTestStatusInProgress && isMergeRequest { + if report.Status != intgteststat.IntegrationTestStatusPending && report.Status != intgteststat.IntegrationTestStatusInProgress && report.Status != intgteststat.SnapshotCreationFailed && isMergeRequest { err := r.updateStatusInComment(report) if err != nil { return err @@ -299,7 +299,7 @@ func GenerateGitlabCommitState(state intgteststat.IntegrationTestStatus) (gitlab glState := gitlab.Failed switch state { - case intgteststat.IntegrationTestStatusPending: + case intgteststat.IntegrationTestStatusPending, intgteststat.BuildPLRInProgress: glState = gitlab.Pending case intgteststat.IntegrationTestStatusInProgress: glState = gitlab.Running @@ -307,7 +307,8 @@ func GenerateGitlabCommitState(state intgteststat.IntegrationTestStatus) (gitlab intgteststat.IntegrationTestStatusDeploymentError_Deprecated, intgteststat.IntegrationTestStatusTestInvalid: glState = gitlab.Failed - case intgteststat.IntegrationTestStatusDeleted: + case intgteststat.IntegrationTestStatusDeleted, + intgteststat.BuildPLRFailed, intgteststat.SnapshotCreationFailed: glState = gitlab.Canceled case intgteststat.IntegrationTestStatusTestPassed: glState = gitlab.Success diff --git a/status/status.go b/status/status.go index eb78c64ed..db70c4c2d 100644 --- a/status/status.go +++ b/status/status.go @@ -328,11 +328,21 @@ func GenerateSummary(state intgteststat.IntegrationTestStatus, snapshotName, sce statusDesc = "has failed" case intgteststat.IntegrationTestStatusTestInvalid: statusDesc = "is invalid" + case intgteststat.BuildPLRInProgress: + statusDesc = "is pending because build pipelinerun is still running and snapshot has not been created" + case intgteststat.SnapshotCreationFailed: + statusDesc = "has not run and is considered as failed because the snapshot was not created" + case intgteststat.BuildPLRFailed: + statusDesc = "has not run and is considered as failed because the build pipelinerun failed and snapshot was not created" default: return summary, fmt.Errorf("unknown status") } - summary = fmt.Sprintf("Integration test for snapshot %s and scenario %s %s", snapshotName, scenarioName, statusDesc) + if state == intgteststat.BuildPLRInProgress || state == intgteststat.SnapshotCreationFailed || state == intgteststat.BuildPLRFailed { + summary = fmt.Sprintf("Integration test for scenario %s %s", scenarioName, statusDesc) + } else { + summary = fmt.Sprintf("Integration test for snapshot %s and scenario %s %s", snapshotName, scenarioName, statusDesc) + } return summary, nil } diff --git a/status/status_test.go b/status/status_test.go index 447f6ad95..0d85b44a0 100644 --- a/status/status_test.go +++ b/status/status_test.go @@ -583,6 +583,22 @@ var _ = Describe("Status Adapter", func() { Entry("Invalid", integrationteststatus.IntegrationTestStatusTestInvalid, "is invalid"), ) + DescribeTable( + "report right summary per status", + func(expectedScenarioStatus integrationteststatus.IntegrationTestStatus, expectedTextEnding string) { + + integrationTestStatusDetail := newIntegrationTestStatusDetail(expectedScenarioStatus) + + expectedSummary := fmt.Sprintf("Integration test for scenario scenario1 %s", expectedTextEnding) + testReport, err := status.GenerateTestReport(context.Background(), mockK8sClient, integrationTestStatusDetail, hasSnapshot, "component-sample") + Expect(err).NotTo(HaveOccurred()) + Expect(testReport.Summary).To(Equal(expectedSummary)) + }, + Entry("BuildPLRInProgress", integrationteststatus.BuildPLRInProgress, "is pending because build pipelinerun is still running and snapshot has not been created"), + Entry("SnapshotCreationFailed", integrationteststatus.SnapshotCreationFailed, "has not run and is considered as failed because the snapshot was not created"), + Entry("BuildPLRFailed", integrationteststatus.BuildPLRFailed, "has not run and is considered as failed because the build pipelinerun failed and snapshot was not created"), + ) + It("check if GenerateSummary supports all integration test statuses", func() { for _, teststatus := range integrationteststatus.IntegrationTestStatusValues() { _, err := status.GenerateSummary(teststatus, "yolo", "yolo")