diff --git a/internal/controller/snapshot/snapshot_adapter.go b/internal/controller/snapshot/snapshot_adapter.go index 25449d6a1..825aaec17 100644 --- a/internal/controller/snapshot/snapshot_adapter.go +++ b/internal/controller/snapshot/snapshot_adapter.go @@ -91,73 +91,125 @@ func scenariosNamesToList(integrationTestScenarios *[]v1beta2.IntegrationTestSce return &result } -// EnsureRerunPipelineRunsExist is responsible for recreating integration test pipelines triggered by users +// EnsureRerunPipelineRunsExist is responsible for recreating integration test pipelineruns triggered by users func (a *Adapter) EnsureRerunPipelineRunsExist() (controller.OperationResult, error) { - - scenarioName, ok := gitops.GetIntegrationTestRunLabelValue(a.snapshot) + runLabelValue, ok := gitops.GetIntegrationTestRunLabelValue(a.snapshot) if !ok { - // no test rerun triggered return controller.ContinueProcessing() } - integrationTestScenario, err := a.loader.GetScenario(a.context, a.client, scenarioName, a.application.Namespace) + integrationTestScenarios, opResult, err := a.getScenariosToRerun(runLabelValue) + if integrationTestScenarios == nil { + return opResult, err + } + + testStatuses, err := gitops.NewSnapshotIntegrationTestStatusesFromSnapshot(a.snapshot) + if err != nil { + return controller.RequeueWithError(err) + } + + skipScenarioRerunCount, opResult, err := a.handleScenarioReruns(integrationTestScenarios, testStatuses) + if opResult.CancelRequest || err != nil { + return opResult, err + } + + if err = a.cleanupRerunLabelAndUpdateStatus(skipScenarioRerunCount, len(*integrationTestScenarios), runLabelValue, testStatuses); err != nil { + return controller.RequeueWithError(err) + } + + return controller.ContinueProcessing() +} + +// getScenariosToRerun fetches and filters the IntegrationTestScenarios based on runLabelValue. +func (a *Adapter) getScenariosToRerun(runLabelValue string) (*[]v1beta2.IntegrationTestScenario, controller.OperationResult, error) { + if runLabelValue == "all" { + scenarios, err := a.loader.GetAllIntegrationTestScenariosForSnapshot(a.context, a.client, a.application, a.snapshot) + if err != nil { + a.logger.Error(err, "Failed to get IntegrationTestScenarios", "Application.Namespace", a.application.Namespace) + opResult, err := controller.RequeueWithError(err) + return nil, opResult, err + } + if scenarios == nil { + a.logger.Info("None of the Scenarios' context are applicable to the Snapshot, nothing to re-run") + } + + return scenarios, controller.OperationResult{}, nil + } + + scenario, err := a.loader.GetScenario(a.context, a.client, runLabelValue, a.application.Namespace) if err != nil { if clienterrors.IsNotFound(err) { - a.logger.Error(err, "scenario for integration test re-run not found", "scenario", scenarioName) - // scenario doesn't exist just remove label and continue + a.logger.Error(err, "IntegrationTestScenario not found", "Scenario", runLabelValue) if err = gitops.RemoveIntegrationTestRerunLabel(a.context, a.client, a.snapshot); err != nil { - return controller.RequeueWithError(err) + opResult, err := controller.RequeueWithError(err) + return nil, opResult, err } - return controller.ContinueProcessing() + opResult, _ := controller.ContinueProcessing() + return nil, opResult, nil } - return controller.RequeueWithError(fmt.Errorf("failed to fetch requested scenario %s: %w", scenarioName, err)) + opResult, err := controller.RequeueWithError(fmt.Errorf("failed to fetch scenario %s: %w", runLabelValue, err)) + return nil, opResult, err } + return &[]v1beta2.IntegrationTestScenario{*scenario}, controller.OperationResult{}, nil +} - a.logger.Info("Re-running integration test for scenario", "scenario", scenarioName) - - testStatuses, err := gitops.NewSnapshotIntegrationTestStatusesFromSnapshot(a.snapshot) - if err != nil { - return controller.RequeueWithError(err) - } +// handleScenarioReruns iterates through scenarios, rerunning tests as needed and updating test statuses. +func (a *Adapter) handleScenarioReruns(scenarios *[]v1beta2.IntegrationTestScenario, testStatuses *intgteststat.SnapshotIntegrationTestStatuses) (int, controller.OperationResult, error) { + skipScenarioRerunCount := 0 + for _, scenario := range *scenarios { + scenario := scenario + status, found := testStatuses.GetScenarioStatus(scenario.Name) + if found && (status.Status == intgteststat.IntegrationTestStatusInProgress || status.Status == intgteststat.IntegrationTestStatusPending) { + a.logger.Info("Skipping re-run for IntegrationTestScenario since it's in 'InProgress' or 'Pending' state", "Scenario", scenario.Name) + skipScenarioRerunCount++ + continue + } - integrationTestScenarioStatus, ok := testStatuses.GetScenarioStatus(integrationTestScenario.Name) - if ok && (integrationTestScenarioStatus.Status == intgteststat.IntegrationTestStatusInProgress || - integrationTestScenarioStatus.Status == intgteststat.IntegrationTestStatusPending) { - a.logger.Info(fmt.Sprintf("Found existing test in %s status, skipping re-run", integrationTestScenarioStatus.Status), - "integrationTestScenario.Name", integrationTestScenario.Name) - if err = gitops.RemoveIntegrationTestRerunLabel(a.context, a.client, a.snapshot); err != nil { - return controller.RequeueWithError(err) + testStatuses.ResetStatus(scenario.Name) + if opResult, err := a.rerunIntegrationPipelinerunForScenario(&scenario, testStatuses); opResult.CancelRequest || err != nil { + a.logger.Error(err, "Failed to create rerun pipelinerun for IntegrationTestScenario", "Scenario", scenario.Name) + return -1, opResult, err } - return controller.ContinueProcessing() } - testStatuses.ResetStatus(scenarioName) + return skipScenarioRerunCount, controller.OperationResult{}, nil +} - pipelineRun, err := a.createIntegrationPipelineRun(a.application, integrationTestScenario, a.snapshot) +// rerunIntegrationPipelinerunForScenario creates a pipelinerun for the given scenario and updates its status. +func (a *Adapter) rerunIntegrationPipelinerunForScenario(scenario *v1beta2.IntegrationTestScenario, testStatuses *intgteststat.SnapshotIntegrationTestStatuses) (controller.OperationResult, error) { + pipelineRun, err := a.createIntegrationPipelineRun(a.application, scenario, a.snapshot) if err != nil { - return a.HandlePipelineCreationError(err, integrationTestScenario, testStatuses) + return a.HandlePipelineCreationError(err, scenario, testStatuses) } - testStatuses.UpdateTestStatusIfChanged( - integrationTestScenario.Name, intgteststat.IntegrationTestStatusInProgress, - fmt.Sprintf("IntegrationTestScenario pipeline '%s' has been created", pipelineRun.Name)) - if err = testStatuses.UpdateTestPipelineRunName(integrationTestScenario.Name, pipelineRun.Name); err != nil { + + testStatuses.UpdateTestStatusIfChanged(scenario.Name, intgteststat.IntegrationTestStatusInProgress, fmt.Sprintf("PipelineRun '%s' created", pipelineRun.Name)) + if err := testStatuses.UpdateTestPipelineRunName(scenario.Name, pipelineRun.Name); err != nil { // it doesn't make sense to restart reconciliation here, it will be eventually updated by integrationpipeline adapter - a.logger.Error(err, "Failed to update pipelinerun name in test status") + a.logger.Error(err, "Failed to update pipeline run name in test status") } + return controller.OperationResult{}, nil +} - if err = gitops.WriteIntegrationTestStatusesIntoSnapshot(a.context, a.snapshot, testStatuses, a.client); err != nil { - return controller.RequeueWithError(err) +// cleanupRerunLabelAndUpdateStatus removes rerun labels and updates snapshot status. +func (a *Adapter) cleanupRerunLabelAndUpdateStatus(skipCount, totalScenarios int, runLabelValue string, testStatuses *intgteststat.SnapshotIntegrationTestStatuses) error { + if err := gitops.RemoveIntegrationTestRerunLabel(a.context, a.client, a.snapshot); err != nil { + return err } - if err = gitops.ResetSnapshotStatusConditions(a.context, a.client, a.snapshot, "Integration test is being rerun for snapshot"); err != nil { - a.logger.Error(err, "Failed to reset snapshot status conditions") - return controller.RequeueWithError(err) + if skipCount == totalScenarios { + a.logger.Info(fmt.Sprintf("%[1]d out of %[1]d requested IntegrationTestScenario(s) are either in 'InProgress' or 'Pending' state, skipping their re-runs", totalScenarios), "Label", runLabelValue) + return nil } - if err = gitops.RemoveIntegrationTestRerunLabel(a.context, a.client, a.snapshot); err != nil { - return controller.RequeueWithError(err) + if err := gitops.WriteIntegrationTestStatusesIntoSnapshot(a.context, a.snapshot, testStatuses, a.client); err != nil { + return err } - return controller.ContinueProcessing() + if err := gitops.ResetSnapshotStatusConditions(a.context, a.client, a.snapshot, "Integration test re-run initiated for Snapshot"); err != nil { + a.logger.Error(err, "Failed to reset snapshot status conditions") + return err + } + + return nil } // EnsureIntegrationPipelineRunsExist is an operation that will ensure that all Integration pipeline runs diff --git a/internal/controller/snapshot/snapshot_adapter_test.go b/internal/controller/snapshot/snapshot_adapter_test.go index bdf7ca160..d28aa5919 100644 --- a/internal/controller/snapshot/snapshot_adapter_test.go +++ b/internal/controller/snapshot/snapshot_adapter_test.go @@ -78,6 +78,7 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { hasComSnapshot2 *applicationapiv1alpha1.Snapshot hasComSnapshot3 *applicationapiv1alpha1.Snapshot integrationTestScenario *v1beta2.IntegrationTestScenario + integrationTestScenario1 *v1beta2.IntegrationTestScenario integrationTestScenarioForInvalidSnapshot *v1beta2.IntegrationTestScenario buildPipelineRun1 *tektonv1.PipelineRun ) @@ -146,6 +147,43 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { Expect(k8sClient.Create(ctx, integrationTestScenario)).Should(Succeed()) helpers.SetScenarioIntegrationStatusAsValid(integrationTestScenario, "valid") + integrationTestScenario1 = &v1beta2.IntegrationTestScenario{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example-pass-1", + Namespace: "default", + + Labels: map[string]string{ + "test.appstudio.openshift.io/optional": "false", + }, + + Annotations: map[string]string{ + "test.appstudio.openshift.io/kind": "kind", + }, + }, + Spec: v1beta2.IntegrationTestScenarioSpec{ + Application: "application-sample", + ResolverRef: v1beta2.ResolverRef{ + Resolver: "git", + Params: []v1beta2.ResolverParameter{ + { + Name: "url", + Value: "https://github.com/redhat-appstudio/integration-examples.git", + }, + { + Name: "revision", + Value: sourceRepoRef, + }, + { + Name: "pathInRepo", + Value: "pipelineruns/integration_pipelinerun_pass.yaml", + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, integrationTestScenario1)).Should(Succeed()) + helpers.SetScenarioIntegrationStatusAsValid(integrationTestScenario1, "valid") + testReleasePlan = &releasev1alpha1.ReleasePlan{ ObjectMeta: metav1.ObjectMeta{ GenerateName: "test-releaseplan-", @@ -1521,6 +1559,340 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { }) }) + + When("the run label has the value 'all'", func() { + var ( + buf bytes.Buffer + ) + + const ( + fakePLRName string = "pipelinerun-test" + fakeDetails string = "Lorem ipsum sit dolor mit amet" + ) + + BeforeEach(func() { + err := gitops.MarkSnapshotAsPassed(ctx, k8sClient, hasSnapshot, "test passed") + Expect(err).To(Succeed()) + Expect(gitops.HaveAppStudioTestsSucceeded(hasSnapshot)).To(BeTrue()) + + // mock that test for scenario is already in progress by setting it in annotation + statuses, err := intgteststat.NewSnapshotIntegrationTestStatuses("") + Expect(err).To(Succeed()) + statuses.UpdateTestStatusIfChanged(integrationTestScenario.Name, intgteststat.IntegrationTestStatusInProgress, fakeDetails) + Expect(statuses.UpdateTestPipelineRunName(integrationTestScenario.Name, fakePLRName)).To(Succeed()) + Expect(gitops.WriteIntegrationTestStatusesIntoSnapshot(ctx, hasSnapshot, statuses, k8sClient)).Should(Succeed()) + + // add rerun label + // we cannot update it into k8s DB via patch, it would trigger reconciliation in background + // and test wouldn't test anything + hasSnapshot.Labels[gitops.SnapshotIntegrationTestRun] = "all" + + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + adapter = NewAdapter(ctx, hasSnapshot, hasApp, log, loader.NewMockLoader(), k8sClient) + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ApplicationContextKey, + Resource: hasApp, + }, + { + ContextKey: loader.ComponentContextKey, + Resource: hasComp, + }, + { + ContextKey: loader.SnapshotContextKey, + Resource: hasSnapshot, + }, + { + ContextKey: loader.SnapshotComponentsContextKey, + Resource: []applicationapiv1alpha1.Component{*hasComp}, + }, + { + ContextKey: loader.AllIntegrationTestScenariosForSnapshotContextKey, + Resource: []v1beta2.IntegrationTestScenario{*integrationTestScenario, *integrationTestScenario1}, + }, + }) + }) + + It("creates Integration PLR for the new ITS and skips for the one that's already InProgress", func() { + statuses, err := gitops.NewSnapshotIntegrationTestStatusesFromSnapshot(hasSnapshot) + Expect(err).To(Succeed()) + _, ok := statuses.GetScenarioStatus(integrationTestScenario1.Name) + Expect(ok).To(BeFalse()) // no entry for 'integrationTestScenario1' yet, because it wasn't run before + + result, err := adapter.EnsureRerunPipelineRunsExist() + Expect(err).To(Succeed()) + Expect(result.CancelRequest).To(BeFalse()) + + Expect(buf.String()).Should(ContainSubstring(fmt.Sprintf("Skipping re-run for IntegrationTestScenario since it's in 'InProgress' or 'Pending' state Scenario %s", integrationTestScenario.Name))) + + statuses, err = gitops.NewSnapshotIntegrationTestStatusesFromSnapshot(hasSnapshot) + Expect(err).To(Succeed()) + detail, ok := statuses.GetScenarioStatus(integrationTestScenario1.Name) + Expect(ok).To(BeTrue()) // 'integrationTestScenario1' has a status now + Expect(detail).ToNot(BeNil()) + Expect(detail.TestPipelineRunName).ToNot(BeEmpty()) + + m := MatchKeys(IgnoreExtras, Keys{ + gitops.SnapshotIntegrationTestRun: Equal("all"), + }) + Expect(hasSnapshot.GetLabels()).ShouldNot(m, "shouln't have 'run' label after running scenario") + + // We reset Snapshot's status since we created a new Intg PLR for the ITS + Expect(hasSnapshot.Status.Conditions).NotTo(BeNil()) + Expect(gitops.IsSnapshotStatusConditionSet(hasSnapshot, gitops.AppStudioTestSucceededCondition, + metav1.ConditionUnknown, "InProgress")).To(BeTrue()) + + // Verify that the message field of "AppStudioIntegrationStatusCondition" condition mentions the re-run initiation + condition := meta.FindStatusCondition(hasSnapshot.Status.Conditions, gitops.AppStudioIntegrationStatusCondition) + Expect(condition.Message).To(Equal("Integration test re-run initiated for Snapshot")) + }) + }) + + When("the run label has the value 'all' but with a component-type context on an ITS", func() { + var ( + buf bytes.Buffer + ) + + const ( + fakePLRName string = "pipelinerun-test" + fakeDetails string = "Lorem ipsum sit dolor mit amet" + ) + + BeforeEach(func() { + err := gitops.MarkSnapshotAsPassed(ctx, k8sClient, hasSnapshot, "test passed") + Expect(err).To(Succeed()) + Expect(gitops.HaveAppStudioTestsSucceeded(hasSnapshot)).To(BeTrue()) + + // Setting the context of 'integrationTestScenario1' to NOT match the current Component + integrationTestScenario1.Spec.Contexts = append(integrationTestScenario1.Spec.Contexts, v1beta2.TestContext{Name: "component_my-comp", Description: "Single component testing for 'my-comp' specifically"}) + Expect(k8sClient.Update(ctx, integrationTestScenario1)).Should(Succeed()) + + // mock that test for scenario is already in progress by setting it in annotation + statuses, err := intgteststat.NewSnapshotIntegrationTestStatuses("") + Expect(err).To(Succeed()) + statuses.UpdateTestStatusIfChanged(integrationTestScenario.Name, intgteststat.IntegrationTestStatusInProgress, fakeDetails) + Expect(statuses.UpdateTestPipelineRunName(integrationTestScenario.Name, fakePLRName)).To(Succeed()) + Expect(gitops.WriteIntegrationTestStatusesIntoSnapshot(ctx, hasSnapshot, statuses, k8sClient)).Should(Succeed()) + + // add rerun label + // we cannot update it into k8s DB via patch, it would trigger reconciliation in background + // and test wouldn't test anything + hasSnapshot.Labels[gitops.SnapshotIntegrationTestRun] = "all" + + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + adapter = NewAdapter(ctx, hasSnapshot, hasApp, log, loader.NewMockLoader(), k8sClient) + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ApplicationContextKey, + Resource: hasApp, + }, + { + ContextKey: loader.ComponentContextKey, + Resource: hasComp, + }, + { + ContextKey: loader.SnapshotContextKey, + Resource: hasSnapshot, + }, + { + ContextKey: loader.SnapshotComponentsContextKey, + Resource: []applicationapiv1alpha1.Component{*hasComp}, + }, + { + ContextKey: loader.AllIntegrationTestScenariosForSnapshotContextKey, + Resource: []v1beta2.IntegrationTestScenario{*integrationTestScenario}, + }, + }) + }) + + It("does not create Integration PLR for the new ITS since its context doesn't match with the Snapshot", func() { + statuses, err := gitops.NewSnapshotIntegrationTestStatusesFromSnapshot(hasSnapshot) + Expect(err).To(Succeed()) + _, ok := statuses.GetScenarioStatus(integrationTestScenario1.Name) + Expect(ok).To(BeFalse()) // no entry for 'integrationTestScenario1' yet, because it wasn't run before + + result, err := adapter.EnsureRerunPipelineRunsExist() + Expect(err).To(Succeed()) + Expect(result.CancelRequest).To(BeFalse()) + + Expect(buf.String()).Should(ContainSubstring(fmt.Sprintf("Skipping re-run for IntegrationTestScenario since it's in 'InProgress' or 'Pending' state Scenario %s", integrationTestScenario.Name))) + Expect(buf.String()).Should(ContainSubstring("1 out of 1 requested IntegrationTestScenario(s) are either in 'InProgress' or 'Pending' state, skipping their re-runs Label all")) + + statuses, err = gitops.NewSnapshotIntegrationTestStatusesFromSnapshot(hasSnapshot) + Expect(err).To(Succeed()) + _, ok = statuses.GetScenarioStatus(integrationTestScenario1.Name) + Expect(ok).To(BeFalse()) // 'integrationTestScenario1' does NOT has a status because it's context doesn't match with the 'hasSnapshot' + + m := MatchKeys(IgnoreExtras, Keys{ + gitops.SnapshotIntegrationTestRun: Equal("all"), + }) + Expect(hasSnapshot.GetLabels()).ShouldNot(m, "shouln't have 'run' label after running scenario") + + // Snapshot status is True because no new Integration PLRS were created + Expect(hasSnapshot.Status.Conditions).NotTo(BeNil()) + Expect(gitops.IsSnapshotStatusConditionSet(hasSnapshot, gitops.AppStudioTestSucceededCondition, + metav1.ConditionTrue, "Passed")).To(BeTrue()) // Because we didn't reset Snapshot's status since 1 ITS has mismatching context, other one is "InProgress" + }) + }) + + When("the run label has the name of an ITS with a component-type context", func() { + var ( + buf bytes.Buffer + ) + + BeforeEach(func() { + err := gitops.MarkSnapshotAsPassed(ctx, k8sClient, hasSnapshot, "test passed") + Expect(err).To(Succeed()) + Expect(gitops.HaveAppStudioTestsSucceeded(hasSnapshot)).To(BeTrue()) + + // Setting the context of 'integrationTestScenario1' to NOT match the current Component + integrationTestScenario1.Spec.Contexts = append(integrationTestScenario1.Spec.Contexts, v1beta2.TestContext{Name: "component_my-comp", Description: "Single component testing for 'my-comp' specifically"}) + Expect(k8sClient.Update(ctx, integrationTestScenario1)).Should(Succeed()) + + // add rerun label + // we cannot update it into k8s DB via patch, it would trigger reconciliation in background + // and test wouldn't test anything + hasSnapshot.Labels[gitops.SnapshotIntegrationTestRun] = integrationTestScenario1.Name + + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + adapter = NewAdapter(ctx, hasSnapshot, hasApp, log, loader.NewMockLoader(), k8sClient) + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ApplicationContextKey, + Resource: hasApp, + }, + { + ContextKey: loader.ComponentContextKey, + Resource: hasComp, + }, + { + ContextKey: loader.SnapshotContextKey, + Resource: hasSnapshot, + }, + { + ContextKey: loader.SnapshotComponentsContextKey, + Resource: []applicationapiv1alpha1.Component{*hasComp}, + }, + }) + }) + + It("does creates Integration PLR for the new ITS even though its context doesn't match with the Snapshot", func() { + statuses, err := gitops.NewSnapshotIntegrationTestStatusesFromSnapshot(hasSnapshot) + Expect(err).To(Succeed()) + _, ok := statuses.GetScenarioStatus(integrationTestScenario1.Name) + Expect(ok).To(BeFalse()) // no entry for 'integrationTestScenario1' yet, because it wasn't run before + + result, err := adapter.EnsureRerunPipelineRunsExist() + Expect(err).To(Succeed()) + Expect(result.CancelRequest).To(BeFalse()) + + statuses, err = gitops.NewSnapshotIntegrationTestStatusesFromSnapshot(hasSnapshot) + Expect(err).To(Succeed()) + detail, ok := statuses.GetScenarioStatus(integrationTestScenario1.Name) + // 'integrationTestScenario1' does have a status because the run was requested specifically with the ITS name. + // When users explicitly request run of a specific ITS, then we ignore the context of that ITS and process it. + Expect(ok).To(BeTrue()) + Expect(detail).ToNot(BeNil()) + Expect(detail.TestPipelineRunName).ToNot(BeEmpty()) + + m := MatchKeys(IgnoreExtras, Keys{ + gitops.SnapshotIntegrationTestRun: Equal(integrationTestScenario1.Name), + }) + Expect(hasSnapshot.GetLabels()).ShouldNot(m, "shouln't have 'run' label after running scenario") + + // We reset Snapshot's status since we created a new Intg PLR for the ITS + Expect(hasSnapshot.Status.Conditions).NotTo(BeNil()) + Expect(gitops.IsSnapshotStatusConditionSet(hasSnapshot, gitops.AppStudioTestSucceededCondition, + metav1.ConditionUnknown, "InProgress")).To(BeTrue()) + + // Verify that the message field of "AppStudioIntegrationStatusCondition" condition mentions the re-run initiation + condition := meta.FindStatusCondition(hasSnapshot.Status.Conditions, gitops.AppStudioIntegrationStatusCondition) + Expect(condition.Message).To(Equal("Integration test re-run initiated for Snapshot")) + }) + }) + + When("the run label has the name of an ITS that was already executed before", func() { + var ( + buf bytes.Buffer + ) + + const ( + fakePLRName string = "pipelinerun-test" + fakeDetails string = "Lorem ipsum sit dolor mit amet" + ) + + BeforeEach(func() { + err := gitops.MarkSnapshotAsPassed(ctx, k8sClient, hasSnapshot, "test passed") + Expect(err).To(Succeed()) + Expect(gitops.HaveAppStudioTestsSucceeded(hasSnapshot)).To(BeTrue()) + + // mock that test for scenario is already in progress by setting it in annotation + statuses, err := intgteststat.NewSnapshotIntegrationTestStatuses("") + Expect(err).To(Succeed()) + statuses.UpdateTestStatusIfChanged(integrationTestScenario.Name, intgteststat.IntegrationTestStatusTestPassed, fakeDetails) + Expect(statuses.UpdateTestPipelineRunName(integrationTestScenario.Name, fakePLRName)).To(Succeed()) + Expect(gitops.WriteIntegrationTestStatusesIntoSnapshot(ctx, hasSnapshot, statuses, k8sClient)).Should(Succeed()) + + // add rerun label + // we cannot update it into k8s DB via patch, it would trigger reconciliation in background + // and test wouldn't test anything + hasSnapshot.Labels[gitops.SnapshotIntegrationTestRun] = integrationTestScenario.Name + + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + adapter = NewAdapter(ctx, hasSnapshot, hasApp, log, loader.NewMockLoader(), k8sClient) + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ApplicationContextKey, + Resource: hasApp, + }, + { + ContextKey: loader.ComponentContextKey, + Resource: hasComp, + }, + { + ContextKey: loader.SnapshotContextKey, + Resource: hasSnapshot, + }, + { + ContextKey: loader.SnapshotComponentsContextKey, + Resource: []applicationapiv1alpha1.Component{*hasComp}, + }, + }) + }) + + It("does create a new Integration PLR for the ITS which was already executed before", func() { + statuses, err := gitops.NewSnapshotIntegrationTestStatusesFromSnapshot(hasSnapshot) + Expect(err).To(Succeed()) + detail, ok := statuses.GetScenarioStatus(integrationTestScenario.Name) + Expect(ok).To(BeTrue()) // Entry exists for 'integrationTestScenario' because it was run before + Expect(detail).ToNot(BeNil()) + Expect(detail.TestPipelineRunName).To(Equal(fakePLRName)) + + result, err := adapter.EnsureRerunPipelineRunsExist() + Expect(err).To(Succeed()) + Expect(result.CancelRequest).To(BeFalse()) + + Expect(buf.String()).Should(ContainSubstring(fmt.Sprintf("Creating new pipelinerun for integrationTestscenario integrationTestScenario.Name %s", integrationTestScenario.Name))) + + statuses, err = gitops.NewSnapshotIntegrationTestStatusesFromSnapshot(hasSnapshot) + Expect(err).To(Succeed()) + detail, ok = statuses.GetScenarioStatus(integrationTestScenario.Name) + Expect(ok).To(BeTrue()) + Expect(detail).ToNot(BeNil()) + // The name of the PLR is updated to match the latest one + Expect(detail.TestPipelineRunName).ToNot(Equal(fakePLRName)) + + m := MatchKeys(IgnoreExtras, Keys{ + gitops.SnapshotIntegrationTestRun: Equal(integrationTestScenario.Name), + }) + Expect(hasSnapshot.GetLabels()).ShouldNot(m, "shouln't have 'run' label after running scenario") + + // We reset Snapshot's status since we created a new Intg PLR for the ITS + Expect(hasSnapshot.Status.Conditions).NotTo(BeNil()) + Expect(gitops.IsSnapshotStatusConditionSet(hasSnapshot, gitops.AppStudioTestSucceededCondition, + metav1.ConditionUnknown, "InProgress")).To(BeTrue()) + }) + }) }) When("Adapter is created for override snapshot", func() { diff --git a/loader/loader.go b/loader/loader.go index 5ee58fbe0..1230985bb 100644 --- a/loader/loader.go +++ b/loader/loader.go @@ -48,6 +48,7 @@ type ObjectLoader interface { GetSnapshotFromPipelineRun(ctx context.Context, c client.Client, pipelineRun *tektonv1.PipelineRun) (*applicationapiv1alpha1.Snapshot, error) GetAllIntegrationTestScenariosForApplication(ctx context.Context, c client.Client, application *applicationapiv1alpha1.Application) (*[]v1beta2.IntegrationTestScenario, error) GetRequiredIntegrationTestScenariosForSnapshot(ctx context.Context, c client.Client, application *applicationapiv1alpha1.Application, snapshot *applicationapiv1alpha1.Snapshot) (*[]v1beta2.IntegrationTestScenario, error) + GetAllIntegrationTestScenariosForSnapshot(ctx context.Context, c client.Client, application *applicationapiv1alpha1.Application, snapshot *applicationapiv1alpha1.Snapshot) (*[]v1beta2.IntegrationTestScenario, error) GetAllPipelineRunsForSnapshotAndScenario(ctx context.Context, c client.Client, snapshot *applicationapiv1alpha1.Snapshot, integrationTestScenario *v1beta2.IntegrationTestScenario) (*[]tektonv1.PipelineRun, error) GetAllSnapshots(ctx context.Context, c client.Client, application *applicationapiv1alpha1.Application) (*[]applicationapiv1alpha1.Snapshot, error) GetAutoReleasePlansForApplication(ctx context.Context, c client.Client, application *applicationapiv1alpha1.Application) (*[]releasev1alpha1.ReleasePlan, error) @@ -248,6 +249,19 @@ func (l *loader) GetRequiredIntegrationTestScenariosForSnapshot(ctx context.Cont return integrationTestScenarios, nil } +// GetAllIntegrationTestScenariosForSnapshot returns the IntegrationTestScenarios used by the application and snapshot being processed. +// All the IntegrationTestScenarios will be returned regardless of whether it has the test.appstudio.openshift.io/optional +// label not set to true or if it is missing the label entirely, but they will have the correct context for the defined snapshot. +func (l *loader) GetAllIntegrationTestScenariosForSnapshot(ctx context.Context, c client.Client, application *applicationapiv1alpha1.Application, snapshot *applicationapiv1alpha1.Snapshot) (*[]v1beta2.IntegrationTestScenario, error) { + integrationList, err := l.GetAllIntegrationTestScenariosForApplication(ctx, c, application) + if err != nil { + return nil, err + } + + integrationTestScenarios := gitops.FilterIntegrationTestScenariosWithContext(integrationList, snapshot) + return integrationTestScenarios, nil +} + // GetAllPipelineRunsForSnapshotAndScenario returns all Integration PipelineRun for the // associated Snapshot and IntegrationTestScenario. In the case the List operation fails, // an error will be returned. diff --git a/loader/loader_mock.go b/loader/loader_mock.go index f90385a90..a75c21230 100644 --- a/loader/loader_mock.go +++ b/loader/loader_mock.go @@ -47,6 +47,7 @@ const ( PipelineRunsContextKey AllIntegrationTestScenariosContextKey RequiredIntegrationTestScenariosContextKey + AllIntegrationTestScenariosForSnapshotContextKey AllSnapshotsContextKey AutoReleasePlansContextKey GetScenarioContextKey @@ -150,6 +151,15 @@ func (l *mockLoader) GetRequiredIntegrationTestScenariosForSnapshot(ctx context. return &integrationTestScenarios, err } +// GetAllIntegrationTestScenariosForSnapshot returns the resource and error passed as values of the context. +func (l *mockLoader) GetAllIntegrationTestScenariosForSnapshot(ctx context.Context, c client.Client, application *applicationapiv1alpha1.Application, snapshot *applicationapiv1alpha1.Snapshot) (*[]v1beta2.IntegrationTestScenario, error) { + if ctx.Value(AllIntegrationTestScenariosForSnapshotContextKey) == nil { + return l.loader.GetAllIntegrationTestScenariosForSnapshot(ctx, c, application, snapshot) + } + integrationTestScenarios, err := toolkit.GetMockedResourceAndErrorFromContext(ctx, AllIntegrationTestScenariosForSnapshotContextKey, []v1beta2.IntegrationTestScenario{}) + return &integrationTestScenarios, err +} + // GetAllPipelineRunsForSnapshotAndScenario returns the resource and error passed as values of the context. func (l *mockLoader) GetAllPipelineRunsForSnapshotAndScenario(ctx context.Context, c client.Client, snapshot *applicationapiv1alpha1.Snapshot, integrationTestScenario *v1beta2.IntegrationTestScenario) (*[]tektonv1.PipelineRun, error) { if ctx.Value(PipelineRunsContextKey) == nil { diff --git a/loader/loader_mock_test.go b/loader/loader_mock_test.go index ba47b14a0..bfb0d3db4 100644 --- a/loader/loader_mock_test.go +++ b/loader/loader_mock_test.go @@ -201,6 +201,21 @@ var _ = Describe("Release Adapter", Ordered, func() { }) }) + Context("When calling GetAllIntegrationTestScenariosForSnapshot", func() { + It("returns all integrationTestScenario and error from the context", func() { + scenarios := []v1beta2.IntegrationTestScenario{} + mockContext := toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: AllIntegrationTestScenariosForSnapshotContextKey, + Resource: scenarios, + }, + }) + resource, err := loader.GetAllIntegrationTestScenariosForSnapshot(mockContext, nil, nil, nil) + Expect(resource).To(Equal(&scenarios)) + Expect(err).ToNot(HaveOccurred()) + }) + }) + Context("When calling GetAllPipelineRunsForSnapshotAndScenario", func() { It("returns pipelineRuns and error from the context", func() { prs := []tektonv1.PipelineRun{} diff --git a/loader/loader_test.go b/loader/loader_test.go index 1a63edec5..e955542bc 100644 --- a/loader/loader_test.go +++ b/loader/loader_test.go @@ -36,15 +36,16 @@ import ( var _ = Describe("Loader", Ordered, func() { var ( - loader ObjectLoader - hasSnapshot *applicationapiv1alpha1.Snapshot - hasApp *applicationapiv1alpha1.Application - hasComp *applicationapiv1alpha1.Component - integrationTestScenario *v1beta2.IntegrationTestScenario - successfulTaskRun *tektonv1.TaskRun - taskRunSample *tektonv1.TaskRun - buildPipelineRun *tektonv1.PipelineRun - integrationPipelineRun *tektonv1.PipelineRun + loader ObjectLoader + hasSnapshot *applicationapiv1alpha1.Snapshot + hasApp *applicationapiv1alpha1.Application + hasComp *applicationapiv1alpha1.Component + integrationTestScenario *v1beta2.IntegrationTestScenario + integrationTestScenarioOpt *v1beta2.IntegrationTestScenario + successfulTaskRun *tektonv1.TaskRun + taskRunSample *tektonv1.TaskRun + buildPipelineRun *tektonv1.PipelineRun + integrationPipelineRun *tektonv1.PipelineRun ) const ( @@ -209,6 +210,41 @@ var _ = Describe("Loader", Ordered, func() { } Expect(k8sClient.Create(ctx, integrationTestScenario)).Should(Succeed()) + integrationTestScenarioOpt = &v1beta2.IntegrationTestScenario{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example-pass-optional", + Namespace: "default", + + Labels: map[string]string{ + "test.appstudio.openshift.io/optional": "true", + }, + }, + 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", + }, + }, + }, + Contexts: []v1beta2.TestContext{ + {Name: "component_non-existent-component", Description: "Single component testing"}, + }, + }, + } + Expect(k8sClient.Create(ctx, integrationTestScenarioOpt)).Should(Succeed()) + buildPipelineRun = &tektonv1.PipelineRun{ ObjectMeta: metav1.ObjectMeta{ Name: "pipelinerun-sample", @@ -464,8 +500,7 @@ var _ = Describe("Loader", Ordered, func() { integrationTestScenarios, err := loader.GetAllIntegrationTestScenariosForApplication(ctx, k8sClient, hasApp) Expect(err).To(BeNil()) Expect(integrationTestScenarios).NotTo(BeNil()) - Expect(*integrationTestScenarios).To(HaveLen(1)) - Expect((*integrationTestScenarios)[0].Name).To(Equal(integrationTestScenario.Name)) + Expect(*integrationTestScenarios).To(HaveLen(2)) }) It("can fetch required integrationTestScenario for application", func() { @@ -476,6 +511,14 @@ var _ = Describe("Loader", Ordered, func() { Expect((*integrationTestScenarios)[0].Name).To(Equal(integrationTestScenario.Name)) }) + It("can fetch all integrationTestScenario for Snapshot, based on the context", func() { + integrationTestScenarios, err := loader.GetAllIntegrationTestScenariosForSnapshot(ctx, k8sClient, hasApp, hasSnapshot) + Expect(err).ToNot(HaveOccurred()) + Expect(integrationTestScenarios).NotTo(BeNil()) + Expect(*integrationTestScenarios).To(HaveLen(1)) + Expect((*integrationTestScenarios)[0].Name).To(Equal(integrationTestScenario.Name)) + }) + It("ensures that all Snapshots for a given application can be found", func() { snapshots, err := loader.GetAllSnapshots(ctx, k8sClient, hasApp) Expect(err).To(BeNil())