diff --git a/docs/statusreport-controller.md b/docs/statusreport-controller.md index 7a25824e2..3cd8d86aa 100644 --- a/docs/statusreport-controller.md +++ b/docs/statusreport-controller.md @@ -34,6 +34,7 @@ flowchart TD %% Node definitions ensure(Process further if: Snapshot has label
pac.test.appstudio.openshift.io/git-provider:github
defined) get_annotation_value(Get integration test status from annotation
test.appstudio.openshift.io/status
from Snapshot) + get_destination_snapshot(Get destination snapshots from
component snapshot or group snapshot
to collect git provider info) detect_git_provider{Detect git provider} @@ -70,7 +71,8 @@ flowchart TD %% Node connections predicate ----> |"EnsureSnapshotTestStatusReportedToGitProvider()"|ensure ensure --> get_annotation_value - get_annotation_value --> detect_git_provider + get_annotation_value --> get_destination_snapshot + get_destination_snapshot --> detect_git_provider detect_git_provider --github--> collect_commit_info_gh detect_git_provider --gitlab--> collect_commit_info_gl collect_commit_info_gh --> is_installation_defined diff --git a/gitops/snapshot.go b/gitops/snapshot.go index fedbbc556..8ddc210c9 100644 --- a/gitops/snapshot.go +++ b/gitops/snapshot.go @@ -936,6 +936,7 @@ func IsComponentSnapshot(snapshot *applicationapiv1alpha1.Snapshot) bool { return metadata.HasLabelWithValue(snapshot, SnapshotTypeLabel, SnapshotComponentType) } +// IsGroupSnapshot returns true if snapshot label 'test.appstudio.openshift.io/type' is 'group' func IsGroupSnapshot(snapshot *applicationapiv1alpha1.Snapshot) bool { return metadata.HasLabelWithValue(snapshot, SnapshotTypeLabel, SnapshotGroupType) } diff --git a/internal/controller/statusreport/statusreport_adapter.go b/internal/controller/statusreport/statusreport_adapter.go index 712180446..e195e088d 100644 --- a/internal/controller/statusreport/statusreport_adapter.go +++ b/internal/controller/statusreport/statusreport_adapter.go @@ -18,11 +18,13 @@ package statusreport import ( "context" + e "errors" "fmt" "time" applicationapiv1alpha1 "github.com/konflux-ci/application-api/api/v1alpha1" "github.com/konflux-ci/operator-toolkit/controller" + "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/konflux-ci/integration-service/api/v1beta2" @@ -66,19 +68,14 @@ func NewAdapter(context context.Context, snapshot *applicationapiv1alpha1.Snapsh // EnsureSnapshotTestStatusReportedToGitProvider will ensure that integration test status is reported to the git provider // which (indirectly) triggered its execution. +// The status is reported to git provider if it is a component snapshot +// Or reported to git providers which trigger component snapshots included in group snapshot if it is a group snapshot func (a *Adapter) EnsureSnapshotTestStatusReportedToGitProvider() (controller.OperationResult, error) { - if gitops.IsSnapshotCreatedByPACPushEvent(a.snapshot) { + if gitops.IsSnapshotCreatedByPACPushEvent(a.snapshot) && !gitops.IsGroupSnapshot(a.snapshot) { return controller.ContinueProcessing() } - reporter := a.status.GetReporter(a.snapshot) - if reporter == nil { - a.logger.Info("No suitable reporter found, skipping report") - return controller.ContinueProcessing() - } - a.logger.Info(fmt.Sprintf("Detected reporter: %s", reporter.GetReporterName())) - - err := a.status.ReportSnapshotStatus(a.context, reporter, a.snapshot) + err := a.ReportSnapshotStatus(a.snapshot) if err != nil { a.logger.Error(err, "failed to report test status to git provider for snapshot", "snapshot.Namespace", a.snapshot.Namespace, "snapshot.Name", a.snapshot.Name) @@ -238,3 +235,137 @@ func (a *Adapter) findUntriggeredIntegrationTestFromStatus(integrationTestScenar } return "" } + +// ReportSnapshotStatus reports status of all integration tests into Pull Requests from component snapshot or group snapshot +func (a *Adapter) ReportSnapshotStatus(testedSnapshot *applicationapiv1alpha1.Snapshot) error { + + statuses, err := gitops.NewSnapshotIntegrationTestStatusesFromSnapshot(testedSnapshot) + if err != nil { + a.logger.Error(err, "failed to get test status annotations from snapshot", + "snapshot.Namespace", testedSnapshot.Namespace, "snapshot.Name", testedSnapshot.Name) + return err + } + + integrationTestStatusDetails := statuses.GetStatuses() + if len(integrationTestStatusDetails) == 0 { + // no tests to report, skip + a.logger.Info("No test result to report to GitHub, skipping", + "snapshot.Namespace", testedSnapshot.Namespace, "snapshot.Name", testedSnapshot.Name) + return nil + } + + // get the component snapshot list that include the git provider info the report will be reported to + destinationSnapshots, err := a.getDestinationSnapshots(testedSnapshot) + if err != nil { + a.logger.Error(err, "failed to get component snapshots from group snapshot", + "snapshot.NameSpace", testedSnapshot.Namespace, "snapshot.Name", testedSnapshot.Name) + return fmt.Errorf("failed to get component snapshots from snapshot %s/%s", testedSnapshot.Namespace, testedSnapshot.Name) + } + + status.MigrateSnapshotToReportStatus(testedSnapshot, integrationTestStatusDetails) + + srs, err := status.NewSnapshotReportStatusFromSnapshot(testedSnapshot) + if err != nil { + a.logger.Error(err, "failed to get latest snapshot write metadata annotation for snapshot", + "snapshot.NameSpace", testedSnapshot.Namespace, "snapshot.Name", testedSnapshot.Name) + srs, _ = status.NewSnapshotReportStatus("") + } + + // Report the integration test status to pr/commit included in the tested component snapshot + // or the component snapshot included in group snapshot + for _, destinationComponentSnapshot := range destinationSnapshots { + destinationComponentSnapshot := destinationComponentSnapshot + reporter := a.status.GetReporter(destinationComponentSnapshot) + if reporter == nil { + a.logger.Info("No suitable reporter found, skipping report") + continue + } + a.logger.Info(fmt.Sprintf("Detected reporter: %s", reporter.GetReporterName())) + + if err := reporter.Initialize(a.context, destinationComponentSnapshot); 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.iterateIntegrationTestStatusDetailsInStatusReport(reporter, integrationTestStatusDetails, testedSnapshot, destinationComponentSnapshot, srs) + if err != nil { + return fmt.Errorf("failed to report integration test status for snapshot %s/%s: %w", + destinationComponentSnapshot.Namespace, destinationComponentSnapshot.Name, err) + } + if err := status.WriteSnapshotReportStatus(a.context, a.client, testedSnapshot, srs); err != nil { + return fmt.Errorf("failed to write snapshot report status metadata: %w", err) + } + return err + }) + } + + if err != nil { + return fmt.Errorf("issue occured during generating or updating report status: %w", err) + } + + a.logger.Info(fmt.Sprintf("Successfully updated the %s annotation", gitops.SnapshotStatusReportAnnotation), "snapshot.Name", testedSnapshot.Name) + + return nil +} + +// iterates iterateIntegrationTestStatusDetails to report to destination snapshot for them +func (a *Adapter) iterateIntegrationTestStatusDetailsInStatusReport(reporter status.ReporterInterface, + integrationTestStatusDetails []*intgteststat.IntegrationTestStatusDetail, + testedSnapshot *applicationapiv1alpha1.Snapshot, + destinationSnapshot *applicationapiv1alpha1.Snapshot, + srs *status.SnapshotReportStatus) error { + // set componentName to component name of component snapshot or pr group name of group snapshot when reporting status to git provider + componentName := "" + if gitops.IsGroupSnapshot(testedSnapshot) { + componentName = "pr group " + testedSnapshot.Annotations[gitops.PRGroupAnnotation] + } else if gitops.IsComponentSnapshot(testedSnapshot) { + componentName = testedSnapshot.Labels[gitops.SnapshotComponentLabel] + } else { + return fmt.Errorf("unsupported snapshot type: %s", testedSnapshot.Annotations[gitops.SnapshotTypeLabel]) + } + + for _, integrationTestStatusDetail := range integrationTestStatusDetails { + if srs.IsNewer(integrationTestStatusDetail.ScenarioName, integrationTestStatusDetail.LastUpdateTime) { + a.logger.Info("Integration Test contains new status updates", "scenario.Name", integrationTestStatusDetail.ScenarioName) + } else { + //integration test contains no changes + continue + } + testReport, reportErr := status.GenerateTestReport(a.context, a.client, *integrationTestStatusDetail, testedSnapshot, componentName) + if reportErr != nil { + if writeErr := status.WriteSnapshotReportStatus(a.context, a.client, testedSnapshot, srs); writeErr != nil { // try to write what was already written + return fmt.Errorf("failed to generate test report AND write snapshot report status metadata: %w", e.Join(reportErr, writeErr)) + } + return fmt.Errorf("failed to generate test report: %w", reportErr) + } + if reportStatusErr := reporter.ReportStatus(a.context, *testReport); reportStatusErr != nil { + if writeErr := status.WriteSnapshotReportStatus(a.context, a.client, testedSnapshot, srs); writeErr != nil { // try to write what was already written + return fmt.Errorf("failed to report status AND write snapshot report status metadata: %w", e.Join(reportStatusErr, writeErr)) + } + return fmt.Errorf("failed to update status: %w", reportStatusErr) + } + srs.SetLastUpdateTime(integrationTestStatusDetail.ScenarioName, integrationTestStatusDetail.LastUpdateTime) + } + return nil +} + +// getDestinationSnapshots gets the component snapshots that include the git provider info the report will be reported to +func (a *Adapter) getDestinationSnapshots(testedSnapshot *applicationapiv1alpha1.Snapshot) ([]*applicationapiv1alpha1.Snapshot, error) { + destinationSnapshots := make([]*applicationapiv1alpha1.Snapshot, 0) + if gitops.IsComponentSnapshot(testedSnapshot) { + destinationSnapshots = append(destinationSnapshots, testedSnapshot) + return destinationSnapshots, nil + } else if gitops.IsGroupSnapshot(testedSnapshot) { + // get component snapshots from group snapshot annotation GroupSnapshotInfoAnnotation + destinationSnapshots, err := status.GetComponentSnapshotsFromGroupSnapshot(a.context, a.client, testedSnapshot) + if err != nil { + a.logger.Error(err, "failed to get component snapshots included in group snapshot", + "snapshot.NameSpace", testedSnapshot.Namespace, "snapshot.Name", testedSnapshot.Name) + return nil, fmt.Errorf("failed to get component snapshots included in group snapshot %s/%s", testedSnapshot.Namespace, testedSnapshot.Name) + } + return destinationSnapshots, nil + } + return nil, fmt.Errorf("unsupported snapshot type in snapshot %s/%s", testedSnapshot.Namespace, testedSnapshot.Name) +} diff --git a/internal/controller/statusreport/statusreport_adapter_test.go b/internal/controller/statusreport/statusreport_adapter_test.go index 89997d2bd..14aa6d62a 100644 --- a/internal/controller/statusreport/statusreport_adapter_test.go +++ b/internal/controller/statusreport/statusreport_adapter_test.go @@ -19,7 +19,10 @@ package statusreport import ( "bytes" "fmt" + "os" "reflect" + "strconv" + "time" "github.com/konflux-ci/integration-service/api/v1beta2" "github.com/tonglil/buflogr" @@ -44,22 +47,34 @@ import ( var _ = Describe("Snapshot Adapter", Ordered, func() { var ( - adapter *Adapter - logger helpers.IntegrationLogger - buf bytes.Buffer + adapter *Adapter + logger helpers.IntegrationLogger + buf bytes.Buffer + mockReporter *status.MockReporterInterface + mockStatus *status.MockStatusInterface hasComp *applicationapiv1alpha1.Component hasComp2 *applicationapiv1alpha1.Component hasApp *applicationapiv1alpha1.Application hasSnapshot *applicationapiv1alpha1.Snapshot hasPRSnapshot *applicationapiv1alpha1.Snapshot + hasComSnapshot2 *applicationapiv1alpha1.Snapshot + hasComSnapshot3 *applicationapiv1alpha1.Snapshot + groupSnapshot *applicationapiv1alpha1.Snapshot + githubSnapshot *applicationapiv1alpha1.Snapshot integrationTestScenario *v1beta2.IntegrationTestScenario ) const ( - SampleRepoLink = "https://github.com/devfile-samples/devfile-sample-java-springboot-basic" - SampleImage = "quay.io/redhat-appstudio/sample-image@sha256:841328df1b9f8c4087adbdcfec6cc99ac8308805dea83f6d415d6fb8d40227c1" - SampleCommit = "a2ba645d50e471d5f084b" - SampleRevision = "random-value" + SampleRepoLink = "https://github.com/devfile-samples/devfile-sample-java-springboot-basic" + SampleImage = "quay.io/redhat-appstudio/sample-image@sha256:841328df1b9f8c4087adbdcfec6cc99ac8308805dea83f6d415d6fb8d40227c1" + SampleDigest = "sha256:841328df1b9f8c4087adbdcfec6cc99ac8308805dea83f6d415d6fb8d40227c1" + SampleCommit = "a2ba645d50e471d5f084b" + SampleRevision = "random-value" + hasComSnapshot2Name = "hascomsnapshot2-sample" + hasComSnapshot3Name = "hascomsnapshot3-sample" + prGroup = "feature1" + prGroupSha = "feature1hash" + plrstarttime = 1775992257 ) BeforeAll(func() { @@ -116,6 +131,114 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { }, } Expect(k8sClient.Create(ctx, hasComp2)).Should(Succeed()) + + hasComSnapshot2 = &applicationapiv1alpha1.Snapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: hasComSnapshot2Name, + Namespace: "default", + Labels: map[string]string{ + gitops.SnapshotTypeLabel: gitops.SnapshotComponentType, + gitops.SnapshotComponentLabel: hasComSnapshot2Name, + gitops.PipelineAsCodeEventTypeLabel: gitops.PipelineAsCodePullRequestType, + gitops.PRGroupHashLabel: prGroupSha, + "pac.test.appstudio.openshift.io/url-org": "testorg", + "pac.test.appstudio.openshift.io/url-repository": "testrepo", + gitops.PipelineAsCodeSHALabel: "sha", + gitops.PipelineAsCodePullRequestAnnotation: "1", + }, + Annotations: map[string]string{ + "test.appstudio.openshift.io/pr-last-update": "2023-08-26T17:57:50+02:00", + gitops.BuildPipelineRunStartTime: strconv.Itoa(plrstarttime + 100), + gitops.PRGroupAnnotation: prGroup, + gitops.PipelineAsCodeGitProviderAnnotation: "github", + gitops.PipelineAsCodeInstallationIDAnnotation: "123", + }, + }, + Spec: applicationapiv1alpha1.SnapshotSpec{ + Application: hasApp.Name, + Components: []applicationapiv1alpha1.SnapshotComponent{ + { + Name: hasComSnapshot3Name, + ContainerImage: SampleImage + "@" + SampleDigest, + }, + { + Name: hasComSnapshot2Name, + ContainerImage: SampleImage + "@" + SampleDigest, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, hasComSnapshot2)).Should(Succeed()) + + hasComSnapshot3 = &applicationapiv1alpha1.Snapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: hasComSnapshot3Name, + Namespace: "default", + Labels: map[string]string{ + gitops.SnapshotTypeLabel: gitops.SnapshotComponentType, + gitops.SnapshotComponentLabel: hasComSnapshot3Name, + gitops.PipelineAsCodeEventTypeLabel: gitops.PipelineAsCodePullRequestType, + gitops.PRGroupHashLabel: prGroupSha, + "pac.test.appstudio.openshift.io/url-org": "testorg", + "pac.test.appstudio.openshift.io/url-repository": "testrepo", + gitops.PipelineAsCodeSHALabel: "sha", + gitops.PipelineAsCodePullRequestAnnotation: "1", + }, + Annotations: map[string]string{ + "test.appstudio.openshift.io/pr-last-update": "2023-08-26T17:57:50+02:00", + gitops.BuildPipelineRunStartTime: strconv.Itoa(plrstarttime + 200), + gitops.PRGroupAnnotation: prGroup, + gitops.PipelineAsCodeGitProviderAnnotation: "github", + gitops.PipelineAsCodePullRequestAnnotation: "1", + gitops.PipelineAsCodeInstallationIDAnnotation: "123", + }, + }, + Spec: applicationapiv1alpha1.SnapshotSpec{ + Application: hasApp.Name, + Components: []applicationapiv1alpha1.SnapshotComponent{ + { + Name: hasComSnapshot2Name, + ContainerImage: SampleImage + "@" + SampleDigest, + }, + { + Name: hasComSnapshot3Name, + ContainerImage: SampleImage + "@" + SampleDigest, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, hasComSnapshot3)).Should(Succeed()) + + groupSnapshot = &applicationapiv1alpha1.Snapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "groupsnapshot", + Namespace: "default", + Labels: map[string]string{ + gitops.SnapshotTypeLabel: gitops.SnapshotGroupType, + gitops.PipelineAsCodeEventTypeLabel: gitops.PipelineAsCodePullRequestType, + gitops.PRGroupHashLabel: prGroupSha, + }, + Annotations: map[string]string{ + gitops.PRGroupAnnotation: prGroup, + gitops.GroupSnapshotInfoAnnotation: "[{\"namespace\":\"default\",\"component\":\"component1-sample\",\"buildPipelineRun\":\"\",\"snapshot\":\"hascomsnapshot2-sample\"},{\"namespace\":\"default\",\"component\":\"component3-sample\",\"buildPipelineRun\":\"\",\"snapshot\":\"hascomsnapshot3-sample\"}]", + gitops.SnapshotTestsStatusAnnotation: "[{\"scenario\":\"scenario-1\",\"status\":\"EnvironmentProvisionError\",\"startTime\":\"2023-07-26T16:57:49+02:00\",\"completionTime\":\"2023-07-26T17:57:49+02:00\",\"lastUpdateTime\":\"2023-08-26T17:57:49+02:00\",\"details\":\"Failed to find deploymentTargetClass with right provisioner for copy of existingEnvironment\"}]", + }, + }, + Spec: applicationapiv1alpha1.SnapshotSpec{ + Application: hasApp.Name, + Components: []applicationapiv1alpha1.SnapshotComponent{ + { + Name: hasComSnapshot2Name, + ContainerImage: SampleImage + "@" + SampleDigest, + }, + { + Name: hasComSnapshot3Name, + ContainerImage: SampleImage + "@" + SampleDigest, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, groupSnapshot)).Should(Succeed()) }) AfterAll(func() { @@ -125,6 +248,12 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { Expect(err == nil || errors.IsNotFound(err)).To(BeTrue()) err = k8sClient.Delete(ctx, hasComp2) Expect(err == nil || errors.IsNotFound(err)).To(BeTrue()) + err = k8sClient.Delete(ctx, groupSnapshot) + Expect(err == nil || errors.IsNotFound(err)).To(BeTrue()) + err = k8sClient.Delete(ctx, hasComSnapshot2) + Expect(err == nil || errors.IsNotFound(err)).To(BeTrue()) + err = k8sClient.Delete(ctx, hasComSnapshot3) + Expect(err == nil || errors.IsNotFound(err)).To(BeTrue()) }) BeforeEach(func() { @@ -151,10 +280,11 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { }, }, } + Expect(k8sClient.Create(ctx, hasSnapshot)).Should(Succeed()) hasPRSnapshot = &applicationapiv1alpha1.Snapshot{ ObjectMeta: metav1.ObjectMeta{ - Name: "snapshot-PR-sample", + Name: "snapshot-pr-sample", Namespace: "default", Labels: map[string]string{ gitops.SnapshotTypeLabel: "component", @@ -172,6 +302,7 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { "build.appstudio.redhat.com/commit_sha": "6c65b2fcaea3e1a0a92476c8b5dc89e92a85f025", "appstudio.redhat.com/updateComponentOnSuccess": "false", gitops.SnapshotTestsStatusAnnotation: "[{\"scenario\":\"scenario-1\",\"status\":\"EnvironmentProvisionError\",\"startTime\":\"2023-07-26T16:57:49+02:00\",\"completionTime\":\"2023-07-26T17:57:49+02:00\",\"lastUpdateTime\":\"2023-08-26T17:57:49+02:00\",\"details\":\"Failed to find deploymentTargetClass with right provisioner for copy of existingEnvironment\"}]", + gitops.PipelineAsCodeGitProviderAnnotation: gitops.PipelineAsCodeGitHubProviderType, }, }, Spec: applicationapiv1alpha1.SnapshotSpec{ @@ -191,7 +322,7 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { }, }, } - Expect(k8sClient.Create(ctx, hasSnapshot)).Should(Succeed()) + Expect(k8sClient.Create(ctx, hasPRSnapshot)).Should(Succeed()) integrationTestScenario = &v1beta2.IntegrationTestScenario{ ObjectMeta: metav1.ObjectMeta{ @@ -223,6 +354,7 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { }, }, } + Expect(k8sClient.Create(ctx, integrationTestScenario)).Should(Succeed()) }) @@ -240,17 +372,17 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { Expect(reflect.TypeOf(NewAdapter(ctx, hasSnapshot, hasApp, logger, loader.NewMockLoader(), k8sClient))).To(Equal(reflect.TypeOf(&Adapter{}))) }) - It("ensures the statusReport is called", func() { + It("ensures the statusReport is called for component snapshot", func() { ctrl := gomock.NewController(GinkgoT()) - mockReporter := status.NewMockReporterInterface(ctrl) + mockReporter = status.NewMockReporterInterface(ctrl) mockStatus := status.NewMockStatusInterface(ctrl) - - mockReporter.EXPECT().GetReporterName().Return("mocked_reporter") - + mockReporter.EXPECT().GetReporterName().Return("mocked-reporter").AnyTimes() mockStatus.EXPECT().GetReporter(gomock.Any()).Return(mockReporter) - // ReportSnapshotStatus must be called once - mockStatus.EXPECT().ReportSnapshotStatus(gomock.Any(), gomock.Any(), gomock.Any()).Times(1) + 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) mockScenarios := []v1beta2.IntegrationTestScenario{} adapter = NewAdapter(ctx, hasPRSnapshot, hasApp, logger, loader.NewMockLoader(), k8sClient) @@ -274,6 +406,37 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { fmt.Fprintf(GinkgoWriter, "-------result: %v\n", result) Expect(!result.CancelRequest && err == nil).To(BeTrue()) }) + + It("ensures the statusReport is called for group snapshot", func() { + buf = bytes.Buffer{} + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + 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) + + mockScenarios := []v1beta2.IntegrationTestScenario{} + adapter = NewAdapter(ctx, groupSnapshot, hasApp, log, loader.NewMockLoader(), k8sClient) + adapter.status = mockStatus + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ApplicationContextKey, + Resource: hasApp, + }, + { + ContextKey: loader.RequiredIntegrationTestScenariosContextKey, + Resource: mockScenarios, + }, + }) + result, err := adapter.EnsureSnapshotTestStatusReportedToGitProvider() + fmt.Fprintf(GinkgoWriter, "-------test: %v\n", buf.String()) + Expect(!result.CancelRequest && err == nil).To(BeTrue()) + }) }) When("New Adapter is created for a push-type Snapshot that passed all tests", func() { @@ -535,4 +698,113 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { }) }) + + When("", func() { + BeforeEach(func() { + buf = bytes.Buffer{} + + githubSnapshot = &applicationapiv1alpha1.Snapshot{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "pac.test.appstudio.openshift.io/git-provider": "github", + }, + }, + } + + ctrl := gomock.NewController(GinkgoT()) + mockReporter = status.NewMockReporterInterface(ctrl) + mockReporter.EXPECT().GetReporterName().Return("mocked-reporter").AnyTimes() + mockStatus = status.NewMockStatusInterface(ctrl) + }) + It("doesn't report anything when there are not test results", func() { + mockReporter.EXPECT().Initialize(gomock.Any(), gomock.Any()).Times(0) // without test results reporter shouldn't be initialized + mockReporter.EXPECT().ReportStatus(gomock.Any(), gomock.Any()).Times(0) // without test results reported shouldn't report status + + adapter = NewAdapter(ctx, githubSnapshot, hasApp, logger, loader.NewMockLoader(), k8sClient) + adapter.status = mockStatus + err := adapter.ReportSnapshotStatus(githubSnapshot) + Expect(err).NotTo(HaveOccurred()) + }) + + It("doesn't report anything when data are older", func() { + 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(0) // data are older, status shouldn't be reported + + hasPRSnapshot.Annotations["test.appstudio.openshift.io/status"] = "[{\"scenario\":\"scenario1\",\"status\":\"InProgress\",\"startTime\":\"2023-07-26T16:57:49+02:00\",\"lastUpdateTime\":\"2023-08-26T17:57:50+02:00\",\"details\":\"Test in progress\"}]" + hasPRSnapshot.Annotations["test.appstudio.openshift.io/git-reporter-status"] = "{\"scenarios\":{\"scenario1\":{\"lastUpdateTime\":\"2023-08-26T17:57:50+02:00\"}}}" + adapter = NewAdapter(ctx, hasPRSnapshot, hasApp, logger, loader.NewMockLoader(), k8sClient) + adapter.status = mockStatus + err := adapter.ReportSnapshotStatus(adapter.snapshot) + Expect(err).NotTo(HaveOccurred()) + }) + + It("Report new status if it was updated", func() { + 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) + + hasPRSnapshot.Annotations["test.appstudio.openshift.io/status"] = "[{\"scenario\":\"scenario1\",\"status\":\"InProgress\",\"startTime\":\"2023-07-26T16:57:49+02:00\",\"lastUpdateTime\":\"2023-08-26T17:57:50+02:00\",\"details\":\"Test in progress\"}]" + hasPRSnapshot.Annotations["test.appstudio.openshift.io/git-reporter-status"] = "{\"scenarios\":{\"scenario1\":{\"lastUpdateTime\":\"2023-08-26T17:57:49+02:00\"}}}" + hasPRSnapshot.Annotations["test.appstudio.openshift.io/group-test-info"] = "[{\"namespace\":\"default\",\"component\":\"devfile-sample-java-springboot-basic-8969\",\"buildPipelineRun\":\"build-plr-java-qjfxz\",\"snapshot\":\"app-8969-bbn7d\"},{\"namespace\":\"default\",\"component\":\"devfile-sample-go-basic-8969\",\"buildPipelineRun\":\"build-plr-go-jmsjq\",\"snapshot\":\"app-8969-kzq2l\"}]" + adapter = NewAdapter(ctx, hasPRSnapshot, hasApp, logger, loader.NewMockLoader(), k8sClient) + adapter.status = mockStatus + err := adapter.ReportSnapshotStatus(adapter.snapshot) + Expect(err).NotTo(HaveOccurred()) + }) + + It("Report new status if it was updated (old way - migration test)", func() { + 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) + + hasPRSnapshot.Annotations["test.appstudio.openshift.io/status"] = "[{\"scenario\":\"scenario1\",\"status\":\"InProgress\",\"startTime\":\"2023-07-26T16:57:49+02:00\",\"lastUpdateTime\":\"2023-08-26T17:57:50+02:00\",\"details\":\"Test in progress\"}]" + hasPRSnapshot.Annotations["test.appstudio.openshift.io/pr-last-update"] = "2023-08-26T17:57:49+02:00" + adapter = NewAdapter(ctx, hasPRSnapshot, hasApp, logger, loader.NewMockLoader(), k8sClient) + adapter.status = mockStatus + err := adapter.ReportSnapshotStatus(adapter.snapshot) + fmt.Fprintf(GinkgoWriter, "-------test: %v\n", "") + Expect(err).NotTo(HaveOccurred()) + }) + + It("report expected textual data for InProgress test scenario", func() { + os.Setenv("CONSOLE_NAME", "Konflux Staging") + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + + mockStatus.EXPECT().GetReporter(gomock.Any()).Return(mockReporter) + mockStatus.EXPECT().GetReporter(gomock.Any()).AnyTimes() + mockReporter.EXPECT().GetReporterName().AnyTimes() + + hasPRSnapshot.Annotations["test.appstudio.openshift.io/status"] = "[{\"scenario\":\"scenario1\",\"status\":\"InProgress\",\"testPipelineRunName\":\"test-pipelinerun\",\"startTime\":\"2023-07-26T16:57:49+02:00\",\"lastUpdateTime\":\"2023-08-26T17:57:50+02:00\",\"details\":\"Test in progress\"}]" + t, err := time.Parse(time.RFC3339, "2023-07-26T16:57:49+02:00") + Expect(err).NotTo(HaveOccurred()) + expectedTestReport := status.TestReport{ + FullName: "Konflux Staging / scenario1 / component-sample", + ScenarioName: "scenario1", + SnapshotName: "snapshot-pr-sample", + ComponentName: "component-sample", + Text: "Test in progress", + Summary: "Integration test for snapshot snapshot-pr-sample and scenario scenario1 is in progress", + Status: intgteststat.IntegrationTestStatusInProgress, + StartTime: &t, + TestPipelineRunName: "test-pipelinerun", + } + mockReporter.EXPECT().Initialize(gomock.Any(), gomock.Any()).Times(1) + mockReporter.EXPECT().ReportStatus(gomock.Any(), gomock.Eq(expectedTestReport)).Times(1) + adapter = NewAdapter(ctx, hasPRSnapshot, hasApp, log, loader.NewMockLoader(), k8sClient) + adapter.status = mockStatus + err = adapter.ReportSnapshotStatus(adapter.snapshot) + fmt.Fprintf(GinkgoWriter, "-------test: %v\n", buf.String()) + Expect(err).NotTo(HaveOccurred()) + }) + }) }) diff --git a/status/mock_status.go b/status/mock_status.go index fd1556eb5..0928aaba5 100644 --- a/status/mock_status.go +++ b/status/mock_status.go @@ -99,16 +99,16 @@ func (mr *MockStatusInterfaceMockRecorder) IsPRMRInSnapshotOpened(arg0, arg1 any return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsPRMRInSnapshotOpened", reflect.TypeOf((*MockStatusInterface)(nil).IsPRMRInSnapshotOpened), arg0, arg1) } -// ReportSnapshotStatus mocks base method. -func (m *MockStatusInterface) ReportSnapshotStatus(arg0 context.Context, arg1 ReporterInterface, arg2 *v1alpha1.Snapshot) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ReportSnapshotStatus", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// ReportSnapshotStatus indicates an expected call of ReportSnapshot -func (mr *MockStatusInterfaceMockRecorder) ReportSnapshotStatus(arg0, arg1, arg2 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportSnapshotStatus", reflect.TypeOf((*MockStatusInterface)(nil).ReportSnapshotStatus), arg0, arg1, arg2) -} +// // ReportSnapshotStatus mocks base method. +// func (m *MockStatusInterface) ReportSnapshotStatus(arg0 context.Context, arg1 *v1alpha1.Snapshot) error { +// m.ctrl.T.Helper() +// ret := m.ctrl.Call(m, "ReportSnapshotStatus", arg0, arg1) +// ret0, _ := ret[0].(error) +// return ret0 +// } + +// // ReportSnapshotStatus indicates an expected call of ReportSnapshot +// func (mr *MockStatusInterfaceMockRecorder) ReportSnapshotStatus(arg0, arg1 any) *gomock.Call { +// mr.mock.ctrl.T.Helper() +// return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportSnapshotStatus", reflect.TypeOf((*MockStatusInterface)(nil).ReportSnapshotStatus), arg0, arg1) +// } diff --git a/status/status.go b/status/status.go index 427640d5c..7f46d6204 100644 --- a/status/status.go +++ b/status/status.go @@ -21,7 +21,6 @@ package status import ( "context" "encoding/json" - "errors" "fmt" "net/url" "os" @@ -39,7 +38,6 @@ import ( gitlab "github.com/xanzy/go-gitlab" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" ) @@ -187,7 +185,6 @@ func MigrateSnapshotToReportStatus(s *applicationapiv1alpha1.Snapshot, testStatu type StatusInterface interface { GetReporter(*applicationapiv1alpha1.Snapshot) ReporterInterface - ReportSnapshotStatus(context.Context, ReporterInterface, *applicationapiv1alpha1.Snapshot) error // Check if PR/MR is opened IsPRMRInSnapshotOpened(context.Context, *applicationapiv1alpha1.Snapshot) (bool, error) // Check if github PR is open @@ -226,93 +223,23 @@ func (s *Status) GetReporter(snapshot *applicationapiv1alpha1.Snapshot) Reporter return nil } -// ReportSnapshotStatus reports status of all integration tests into Pull Request -func (s *Status) ReportSnapshotStatus(ctx context.Context, reporter ReporterInterface, snapshot *applicationapiv1alpha1.Snapshot) error { - - statuses, err := gitops.NewSnapshotIntegrationTestStatusesFromSnapshot(snapshot) - if err != nil { - s.logger.Error(err, "failed to get test status annotations from snapshot", - "snapshot.Namespace", snapshot.Namespace, "snapshot.Name", snapshot.Name) - return err - } - - integrationTestStatusDetails := statuses.GetStatuses() - if len(integrationTestStatusDetails) == 0 { - // no tests to report, skip - s.logger.Info("No test result to report to GitHub, skipping", - "snapshot.Namespace", snapshot.Namespace, "snapshot.Name", snapshot.Name) - return nil - } - - if err := reporter.Initialize(ctx, snapshot); err != nil { - s.logger.Error(err, "Failed to initialize reporter", "reporter", reporter.GetReporterName()) - return fmt.Errorf("failed to initialize reporter: %w", err) - } - s.logger.Info("Reporter initialized", "reporter", reporter.GetReporterName()) - - MigrateSnapshotToReportStatus(snapshot, integrationTestStatusDetails) - - srs, err := NewSnapshotReportStatusFromSnapshot(snapshot) - if err != nil { - s.logger.Error(err, "failed to get latest snapshot write metadata annotation for snapshot", - "snapshot.NameSpace", snapshot.Namespace, "snapshot.Name", snapshot.Name) - srs, _ = NewSnapshotReportStatus("") - } - - err = retry.RetryOnConflict(retry.DefaultRetry, func() error { - for _, integrationTestStatusDetail := range integrationTestStatusDetails { - if srs.IsNewer(integrationTestStatusDetail.ScenarioName, integrationTestStatusDetail.LastUpdateTime) { - s.logger.Info("Integration Test contains new status updates", "scenario.Name", integrationTestStatusDetail.ScenarioName) - } else { - //integration test contains no changes - continue - } - testReport, reportErr := s.generateTestReport(ctx, *integrationTestStatusDetail, snapshot) - if reportErr != nil { - if writeErr := WriteSnapshotReportStatus(ctx, s.client, snapshot, srs); writeErr != nil { // try to write what was already written - return fmt.Errorf("failed to generate test report AND write snapshot report status metadata: %w", errors.Join(reportErr, writeErr)) - } - return fmt.Errorf("failed to generate test report: %w", reportErr) - } - if reportStatusErr := reporter.ReportStatus(ctx, *testReport); reportStatusErr != nil { - if writeErr := WriteSnapshotReportStatus(ctx, s.client, snapshot, srs); writeErr != nil { // try to write what was already written - return fmt.Errorf("failed to report status AND write snapshot report status metadata: %w", errors.Join(reportStatusErr, writeErr)) - } - return fmt.Errorf("failed to update status: %w", reportStatusErr) - } - srs.SetLastUpdateTime(integrationTestStatusDetail.ScenarioName, integrationTestStatusDetail.LastUpdateTime) - } - if err := WriteSnapshotReportStatus(ctx, s.client, snapshot, srs); err != nil { - return fmt.Errorf("failed to write snapshot report status metadata: %w", err) - } - return err - }) - if err != nil { - return fmt.Errorf("issue occured during generating or updating report status: %w", err) - } - - s.logger.Info(fmt.Sprintf("Successfully updated the %s annotation", gitops.SnapshotStatusReportAnnotation), "snapshotReporterStatus.value", srs) - - return nil -} - -// generateTestReport generates TestReport to be used by all reporters -func (s *Status) generateTestReport(ctx context.Context, detail intgteststat.IntegrationTestStatusDetail, snapshot *applicationapiv1alpha1.Snapshot) (*TestReport, error) { +// GenerateTestReport generates TestReport to be used by all reporters +func GenerateTestReport(ctx context.Context, client client.Client, detail intgteststat.IntegrationTestStatusDetail, testedSnapshot *applicationapiv1alpha1.Snapshot, componentName string) (*TestReport, error) { var componentSnapshotInfos []*gitops.ComponentSnapshotInfo var err error - if componentSnapshotInfoString, ok := snapshot.Annotations[gitops.GroupSnapshotInfoAnnotation]; ok { + if componentSnapshotInfoString, ok := testedSnapshot.Annotations[gitops.GroupSnapshotInfoAnnotation]; ok { componentSnapshotInfos, err = gitops.UnmarshalJSON([]byte(componentSnapshotInfoString)) if err != nil { return nil, fmt.Errorf("failed to unmarshal JSON string: %w", err) } } - text, err := s.generateText(ctx, detail, snapshot.Namespace, componentSnapshotInfos) + text, err := generateText(ctx, client, detail, testedSnapshot.Namespace, componentSnapshotInfos) if err != nil { return nil, fmt.Errorf("failed to generate text message: %w", err) } - summary, err := GenerateSummary(detail.Status, snapshot.Name, detail.ScenarioName) + summary, err := GenerateSummary(detail.Status, testedSnapshot.Name, detail.ScenarioName) if err != nil { return nil, fmt.Errorf("failed to generate summary message: %w", err) } @@ -320,16 +247,16 @@ func (s *Status) generateTestReport(ctx context.Context, detail intgteststat.Int consoleName := getConsoleName() fullName := fmt.Sprintf("%s / %s", consoleName, detail.ScenarioName) - if snapshot.Labels[gitops.SnapshotComponentLabel] != "" { - fullName = fmt.Sprintf("%s / %s", fullName, snapshot.Labels[gitops.SnapshotComponentLabel]) + if componentName != "" { + fullName = fmt.Sprintf("%s / %s", fullName, componentName) } report := TestReport{ Text: text, FullName: fullName, ScenarioName: detail.ScenarioName, - SnapshotName: snapshot.Name, - ComponentName: snapshot.Labels[gitops.SnapshotComponentLabel], + SnapshotName: testedSnapshot.Name, + ComponentName: componentName, Status: detail.Status, Summary: summary, StartTime: detail.StartTime, @@ -340,18 +267,19 @@ func (s *Status) generateTestReport(ctx context.Context, detail intgteststat.Int } // generateText generates a text with details for the given state -func (s *Status) generateText(ctx context.Context, integrationTestStatusDetail intgteststat.IntegrationTestStatusDetail, namespace string, componentSnapshotInfos []*gitops.ComponentSnapshotInfo) (string, error) { +func generateText(ctx context.Context, client client.Client, integrationTestStatusDetail intgteststat.IntegrationTestStatusDetail, namespace string, componentSnapshotInfos []*gitops.ComponentSnapshotInfo) (string, error) { + log := log.FromContext(ctx) if integrationTestStatusDetail.Status == intgteststat.IntegrationTestStatusTestPassed || integrationTestStatusDetail.Status == intgteststat.IntegrationTestStatusTestFail { pipelineRunName := integrationTestStatusDetail.TestPipelineRunName pipelineRun := &tektonv1.PipelineRun{} - err := s.client.Get(ctx, types.NamespacedName{ + err := client.Get(ctx, types.NamespacedName{ Namespace: namespace, Name: pipelineRunName, }, pipelineRun) if err != nil { if apierrors.IsNotFound(err) { - s.logger.Error(err, "Failed to fetch pipelineRun", "pipelineRun.Name", pipelineRunName) + log.Error(err, "Failed to fetch pipelineRun", "pipelineRun.Name", pipelineRunName) text := fmt.Sprintf("%s\n\n\n(Failed to fetch test result details because pipelineRun %s/%s can not be found.)", integrationTestStatusDetail.Details, namespace, pipelineRunName) return text, nil } @@ -359,11 +287,11 @@ func (s *Status) generateText(ctx context.Context, integrationTestStatusDetail i return "", fmt.Errorf("error while getting the pipelineRun %s: %w", pipelineRunName, err) } - taskRuns, err := helpers.GetAllChildTaskRunsForPipelineRun(ctx, s.client, pipelineRun) + taskRuns, err := helpers.GetAllChildTaskRunsForPipelineRun(ctx, client, pipelineRun) if err != nil { return "", fmt.Errorf("error while getting all child taskRuns from pipelineRun %s: %w", pipelineRunName, err) } - text, err := FormatTestsSummary(taskRuns, pipelineRunName, namespace, componentSnapshotInfos, s.logger) + text, err := FormatTestsSummary(taskRuns, pipelineRunName, namespace, componentSnapshotInfos, log) if err != nil { return "", err } @@ -542,3 +470,32 @@ func (s Status) IsPRInSnapshotOpened(ctx context.Context, reporter ReporterInter } return false, err } + +// GetComponentSnapshotsFromGroupSnapshot return the component snapshot list which component snapshot is created from +func GetComponentSnapshotsFromGroupSnapshot(ctx context.Context, c client.Client, groupSnapshot *applicationapiv1alpha1.Snapshot) ([]*applicationapiv1alpha1.Snapshot, error) { + log := log.FromContext(ctx) + var componentSnapshotInfos []*gitops.ComponentSnapshotInfo + var componentSnapshots []*applicationapiv1alpha1.Snapshot + var err error + if componentSnapshotInfoString, ok := groupSnapshot.Annotations[gitops.GroupSnapshotInfoAnnotation]; ok { + componentSnapshotInfos, err = gitops.UnmarshalJSON([]byte(componentSnapshotInfoString)) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON string: %w", err) + } + } + + for _, componentSnapshotInfo := range componentSnapshotInfos { + componentSnapshot := &applicationapiv1alpha1.Snapshot{} + err = c.Get(ctx, types.NamespacedName{ + Namespace: groupSnapshot.Namespace, + Name: componentSnapshotInfo.Snapshot, + }, componentSnapshot) + if err != nil { + log.Error(err, fmt.Sprintf("failed to find component snapshot %s included in group snapshot %s/%s", componentSnapshotInfo.Snapshot, groupSnapshot.Namespace, groupSnapshot.Name)) + continue + } + componentSnapshots = append(componentSnapshots, componentSnapshot) + } + return componentSnapshots, nil + +} diff --git a/status/status_test.go b/status/status_test.go index 9c488525c..88f4f4dc8 100644 --- a/status/status_test.go +++ b/status/status_test.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "os" + "strconv" "time" "github.com/go-logr/logr" @@ -31,8 +32,10 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/konflux-ci/integration-service/gitops" "github.com/konflux-ci/integration-service/pkg/integrationteststatus" "github.com/konflux-ci/integration-service/status" + "k8s.io/apimachinery/pkg/api/errors" ) // Custom matcher for gomock, to match expected summary in TestReport @@ -59,18 +62,44 @@ func HasSummary(value string) gomock.Matcher { return hasSummary{expectedSummary: value} } +func newIntegrationTestStatusDetail(expectedScenarioStatus integrationteststatus.IntegrationTestStatus) integrationteststatus.IntegrationTestStatusDetail { + ts, _ := time.Parse(time.RFC3339, "2023-07-26T16:57:49+02:00") + tc, _ := time.Parse(time.RFC3339, "2023-07-26T17:57:49+02:00") + return integrationteststatus.IntegrationTestStatusDetail{ + ScenarioName: "scenario1", + Status: expectedScenarioStatus, + LastUpdateTime: time.Now().UTC(), + Details: "failed", + StartTime: &ts, + CompletionTime: &tc, + TestPipelineRunName: "test-pipelinerun", + } +} + var _ = Describe("Status Adapter", func() { var ( - githubSnapshot *applicationapiv1alpha1.Snapshot - hasSnapshot *applicationapiv1alpha1.Snapshot - mockReporter *status.MockReporterInterface + githubSnapshot *applicationapiv1alpha1.Snapshot + hasSnapshot *applicationapiv1alpha1.Snapshot + hasComSnapshot2 *applicationapiv1alpha1.Snapshot + hasComSnapshot3 *applicationapiv1alpha1.Snapshot + groupSnapshot *applicationapiv1alpha1.Snapshot + mockReporter *status.MockReporterInterface pipelineRun *tektonv1.PipelineRun successfulTaskRun *tektonv1.TaskRun failedTaskRun *tektonv1.TaskRun skippedTaskRun *tektonv1.TaskRun mockK8sClient *MockK8sClient + + hasComSnapshot2Name = "hascomsnapshot2-sample" + hasComSnapshot3Name = "hascomsnapshot3-sample" + + prGroup = "feature1" + prGroupSha = "feature1hash" + plrstarttime = 1775992257 + SampleImage = "quay.io/redhat-appstudio/sample-image@sha256:841328df1b9f8c4087adbdcfec6cc99ac8308805dea83f6d415d6fb8d40227c1" + SampleDigest = "sha256:841328df1b9f8c4087adbdcfec6cc99ac8308805dea83f6d415d6fb8d40227c1" ) BeforeEach(func() { @@ -279,6 +308,111 @@ var _ = Describe("Status Adapter", func() { }, } + hasComSnapshot2 = &applicationapiv1alpha1.Snapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: hasComSnapshot2Name, + Namespace: "default", + Labels: map[string]string{ + gitops.SnapshotTypeLabel: gitops.SnapshotComponentType, + gitops.SnapshotComponentLabel: hasComSnapshot2Name, + gitops.PipelineAsCodeEventTypeLabel: gitops.PipelineAsCodePullRequestType, + gitops.PRGroupHashLabel: prGroupSha, + "pac.test.appstudio.openshift.io/url-org": "testorg", + "pac.test.appstudio.openshift.io/url-repository": "testrepo", + gitops.PipelineAsCodeSHALabel: "sha", + gitops.PipelineAsCodePullRequestAnnotation: "1", + }, + Annotations: map[string]string{ + "test.appstudio.openshift.io/pr-last-update": "2023-08-26T17:57:50+02:00", + gitops.BuildPipelineRunStartTime: strconv.Itoa(plrstarttime + 100), + gitops.PRGroupAnnotation: prGroup, + gitops.PipelineAsCodeGitProviderAnnotation: "github", + gitops.PipelineAsCodeInstallationIDAnnotation: "123", + }, + }, + Spec: applicationapiv1alpha1.SnapshotSpec{ + Application: "application-sample", + Components: []applicationapiv1alpha1.SnapshotComponent{ + { + Name: hasComSnapshot2Name, + ContainerImage: SampleImage + "@" + SampleDigest, + }, + { + Name: hasComSnapshot3Name, + ContainerImage: SampleImage + "@" + SampleDigest, + }, + }, + }, + } + + hasComSnapshot3 = &applicationapiv1alpha1.Snapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: hasComSnapshot3Name, + Namespace: "default", + Labels: map[string]string{ + gitops.SnapshotTypeLabel: gitops.SnapshotComponentType, + gitops.SnapshotComponentLabel: hasComSnapshot3Name, + gitops.PipelineAsCodeEventTypeLabel: gitops.PipelineAsCodePullRequestType, + gitops.PRGroupHashLabel: prGroupSha, + "pac.test.appstudio.openshift.io/url-org": "testorg", + "pac.test.appstudio.openshift.io/url-repository": "testrepo", + gitops.PipelineAsCodeSHALabel: "sha", + gitops.PipelineAsCodePullRequestAnnotation: "1", + }, + Annotations: map[string]string{ + "test.appstudio.openshift.io/pr-last-update": "2023-08-26T17:57:50+02:00", + gitops.BuildPipelineRunStartTime: strconv.Itoa(plrstarttime + 200), + gitops.PRGroupAnnotation: prGroup, + gitops.PipelineAsCodeGitProviderAnnotation: "github", + gitops.PipelineAsCodePullRequestAnnotation: "1", + gitops.PipelineAsCodeInstallationIDAnnotation: "123", + }, + }, + Spec: applicationapiv1alpha1.SnapshotSpec{ + Application: "application-sample", + Components: []applicationapiv1alpha1.SnapshotComponent{ + { + Name: hasComSnapshot2Name, + ContainerImage: SampleImage + "@" + SampleDigest, + }, + { + Name: hasComSnapshot3Name, + ContainerImage: SampleImage + "@" + SampleDigest, + }, + }, + }, + } + + groupSnapshot = &applicationapiv1alpha1.Snapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "groupsnapshot", + Namespace: "default", + Labels: map[string]string{ + gitops.SnapshotTypeLabel: gitops.SnapshotGroupType, + gitops.PipelineAsCodeEventTypeLabel: gitops.PipelineAsCodePullRequestType, + gitops.PRGroupHashLabel: prGroupSha, + }, + Annotations: map[string]string{ + gitops.PRGroupAnnotation: prGroup, + gitops.GroupSnapshotInfoAnnotation: "[{\"namespace\":\"default\",\"component\":\"component1-sample\",\"buildPipelineRun\":\"\",\"snapshot\":\"hascomsnapshot2-sample\"},{\"namespace\":\"default\",\"component\":\"component3-sample\",\"buildPipelineRun\":\"\",\"snapshot\":\"hascomsnapshot3-sample\"}]", + gitops.SnapshotTestsStatusAnnotation: "[{\"scenario\":\"scenario-1\",\"status\":\"EnvironmentProvisionError\",\"startTime\":\"2023-07-26T16:57:49+02:00\",\"completionTime\":\"2023-07-26T17:57:49+02:00\",\"lastUpdateTime\":\"2023-08-26T17:57:49+02:00\",\"details\":\"Failed to find deploymentTargetClass with right provisioner for copy of existingEnvironment\"}]", + }, + }, + Spec: applicationapiv1alpha1.SnapshotSpec{ + Application: "application-sample", + Components: []applicationapiv1alpha1.SnapshotComponent{ + { + Name: hasComSnapshot2Name, + ContainerImage: SampleImage + "@" + SampleDigest, + }, + { + Name: hasComSnapshot3Name, + ContainerImage: SampleImage + "@" + SampleDigest, + }, + }, + }, + } + mockK8sClient = &MockK8sClient{ getInterceptor: func(key client.ObjectKey, obj client.Object) { if taskRun, ok := obj.(*tektonv1.TaskRun); ok { @@ -295,10 +429,56 @@ var _ = Describe("Status Adapter", func() { plr.Status = pipelineRun.Status } } + if snapshot, ok := obj.(*applicationapiv1alpha1.Snapshot); ok { + if key.Name == hasComSnapshot2.Name { + snapshot.Name = hasComSnapshot2.Name + } + if key.Name == hasComSnapshot3.Name { + snapshot.Name = hasComSnapshot3.Name + } + } }, listInterceptor: func(list client.ObjectList) {}, } + hasSnapshot = &applicationapiv1alpha1.Snapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "snapshot-sample", + Namespace: "default", + Labels: map[string]string{ + "test.appstudio.openshift.io/type": "component", + "appstudio.openshift.io/component": "component-sample", + "build.appstudio.redhat.com/pipeline": "enterprise-contract", + "pac.test.appstudio.openshift.io/git-provider": "github", + "pac.test.appstudio.openshift.io/url-org": "devfile-sample", + "pac.test.appstudio.openshift.io/url-repository": "devfile-sample-go-basic", + "pac.test.appstudio.openshift.io/sha": "12a4a35ccd08194595179815e4646c3a6c08bb77", + "pac.test.appstudio.openshift.io/event-type": "pull_request", + }, + Annotations: map[string]string{ + "build.appstudio.redhat.com/commit_sha": "6c65b2fcaea3e1a0a92476c8b5dc89e92a85f025", + "appstudio.redhat.com/updateComponentOnSuccess": "false", + "pac.test.appstudio.openshift.io/repo-url": "https://github.com/devfile-sample/devfile-sample-go-basic", + }, + }, + Spec: applicationapiv1alpha1.SnapshotSpec{ + Application: "application-sample", + Components: []applicationapiv1alpha1.SnapshotComponent{ + { + Name: "component-sample", + ContainerImage: "sample_image", + Source: applicationapiv1alpha1.ComponentSource{ + ComponentSourceUnion: applicationapiv1alpha1.ComponentSourceUnion{ + GitSource: &applicationapiv1alpha1.GitSource{ + Revision: "sample_revision", + }, + }, + }, + }, + }, + }, + } + githubSnapshot = &applicationapiv1alpha1.Snapshot{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ @@ -310,6 +490,7 @@ var _ = Describe("Status Adapter", func() { ctrl := gomock.NewController(GinkgoT()) mockReporter = status.NewMockReporterInterface(ctrl) mockReporter.EXPECT().GetReporterName().Return("mocked-reporter").AnyTimes() + mockReporter.EXPECT().Initialize(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() os.Setenv("CONSOLE_NAME", "Red Hat Konflux") }) @@ -325,91 +506,19 @@ var _ = Describe("Status Adapter", func() { Expect(reporter.GetReporterName()).To(Equal("GithubReporter")) }) - It("doesn't report anything when there are not test results", func() { - - mockReporter.EXPECT().Initialize(gomock.Any(), gomock.Any()).Times(0) // without test results reporter shouldn't be initialized - mockReporter.EXPECT().ReportStatus(gomock.Any(), gomock.Any()).Times(0) // without test results reported shouldn't report status - - st := status.NewStatus(logr.Discard(), nil) - err := st.ReportSnapshotStatus(context.Background(), mockReporter, githubSnapshot) - Expect(err).NotTo(HaveOccurred()) - }) - - It("doesn't report anything when data are older", func() { - - mockReporter.EXPECT().Initialize(gomock.Any(), gomock.Any()).Times(1) - mockReporter.EXPECT().ReportStatus(gomock.Any(), gomock.Any()).Times(0) // data are older, status shouldn't be reported - - hasSnapshot.Annotations["test.appstudio.openshift.io/status"] = "[{\"scenario\":\"scenario1\",\"status\":\"InProgress\",\"startTime\":\"2023-07-26T16:57:49+02:00\",\"lastUpdateTime\":\"2023-08-26T17:57:50+02:00\",\"details\":\"Test in progress\"}]" - hasSnapshot.Annotations["test.appstudio.openshift.io/git-reporter-status"] = "{\"scenarios\":{\"scenario1\":{\"lastUpdateTime\":\"2023-08-26T17:57:50+02:00\"}}}" - st := status.NewStatus(logr.Discard(), mockK8sClient) - err := st.ReportSnapshotStatus(context.Background(), mockReporter, hasSnapshot) - Expect(err).NotTo(HaveOccurred()) - }) - - It("doesn't report anything when data are older (old way - migration test)", func() { - - mockReporter.EXPECT().Initialize(gomock.Any(), gomock.Any()).Times(1) - mockReporter.EXPECT().ReportStatus(gomock.Any(), gomock.Any()).Times(0) // data are older, status shouldn't be reported - + It("can migrate snapshot to reportStatus in old way - migration test)", func() { hasSnapshot.Annotations["test.appstudio.openshift.io/status"] = "[{\"scenario\":\"scenario1\",\"status\":\"InProgress\",\"startTime\":\"2023-07-26T16:57:49+02:00\",\"lastUpdateTime\":\"2023-08-26T17:57:50+02:00\",\"details\":\"Test in progress\"}]" hasSnapshot.Annotations["test.appstudio.openshift.io/pr-last-update"] = "2023-08-26T17:57:50+02:00" - st := status.NewStatus(logr.Discard(), mockK8sClient) - err := st.ReportSnapshotStatus(context.Background(), mockReporter, hasSnapshot) - Expect(err).NotTo(HaveOccurred()) - }) - - It("Report new status if it was updated", func() { - - mockReporter.EXPECT().Initialize(gomock.Any(), gomock.Any()).Times(1) - mockReporter.EXPECT().ReportStatus(gomock.Any(), gomock.Any()).Times(1) - - hasSnapshot.Annotations["test.appstudio.openshift.io/status"] = "[{\"scenario\":\"scenario1\",\"status\":\"InProgress\",\"startTime\":\"2023-07-26T16:57:49+02:00\",\"lastUpdateTime\":\"2023-08-26T17:57:50+02:00\",\"details\":\"Test in progress\"}]" - hasSnapshot.Annotations["test.appstudio.openshift.io/git-reporter-status"] = "{\"scenarios\":{\"scenario1\":{\"lastUpdateTime\":\"2023-08-26T17:57:49+02:00\"}}}" - hasSnapshot.Annotations["test.appstudio.openshift.io/group-test-info"] = "[{\"namespace\":\"default\",\"component\":\"devfile-sample-java-springboot-basic-8969\",\"buildPipelineRun\":\"build-plr-java-qjfxz\",\"snapshot\":\"app-8969-bbn7d\"},{\"namespace\":\"default\",\"component\":\"devfile-sample-go-basic-8969\",\"buildPipelineRun\":\"build-plr-go-jmsjq\",\"snapshot\":\"app-8969-kzq2l\"}]" - st := status.NewStatus(logr.Discard(), mockK8sClient) - err := st.ReportSnapshotStatus(context.Background(), mockReporter, hasSnapshot) - Expect(err).NotTo(HaveOccurred()) - }) - - It("Report new status if it was updated (old way - migration test)", func() { - - mockReporter.EXPECT().Initialize(gomock.Any(), gomock.Any()).Times(1) - mockReporter.EXPECT().ReportStatus(gomock.Any(), gomock.Any()).Times(1) - - hasSnapshot.Annotations["test.appstudio.openshift.io/status"] = "[{\"scenario\":\"scenario1\",\"status\":\"InProgress\",\"startTime\":\"2023-07-26T16:57:49+02:00\",\"lastUpdateTime\":\"2023-08-26T17:57:50+02:00\",\"details\":\"Test in progress\"}]" - hasSnapshot.Annotations["test.appstudio.openshift.io/pr-last-update"] = "2023-08-26T17:57:49+02:00" - st := status.NewStatus(logr.Discard(), mockK8sClient) - err := st.ReportSnapshotStatus(context.Background(), mockReporter, hasSnapshot) - Expect(err).NotTo(HaveOccurred()) - }) - - It("report expected textual data for InProgress test scenario", func() { - os.Setenv("CONSOLE_NAME", "Konflux Staging") - hasSnapshot.Annotations["test.appstudio.openshift.io/status"] = "[{\"scenario\":\"scenario1\",\"status\":\"InProgress\",\"testPipelineRunName\":\"test-pipelinerun\",\"startTime\":\"2023-07-26T16:57:49+02:00\",\"lastUpdateTime\":\"2023-08-26T17:57:50+02:00\",\"details\":\"Test in progress\"}]" - t, err := time.Parse(time.RFC3339, "2023-07-26T16:57:49+02:00") - Expect(err).NotTo(HaveOccurred()) - expectedTestReport := status.TestReport{ - FullName: "Konflux Staging / scenario1 / component-sample", - ScenarioName: "scenario1", - SnapshotName: "snapshot-sample", - ComponentName: "component-sample", - Text: "Test in progress", - Summary: "Integration test for snapshot snapshot-sample and scenario scenario1 is in progress", - Status: integrationteststatus.IntegrationTestStatusInProgress, - StartTime: &t, - TestPipelineRunName: "test-pipelinerun", - } - mockReporter.EXPECT().Initialize(gomock.Any(), gomock.Any()).Times(1) - mockReporter.EXPECT().ReportStatus(gomock.Any(), gomock.Eq(expectedTestReport)).Times(1) - - st := status.NewStatus(logr.Discard(), mockK8sClient) - err = st.ReportSnapshotStatus(context.Background(), mockReporter, hasSnapshot) + statuses, err := gitops.NewSnapshotIntegrationTestStatusesFromSnapshot(hasSnapshot) Expect(err).NotTo(HaveOccurred()) + integrationTestStatusDetails := statuses.GetStatuses() + status.MigrateSnapshotToReportStatus(hasSnapshot, integrationTestStatusDetails) + Expect(hasSnapshot.Annotations[gitops.SnapshotStatusReportAnnotation]).Should(ContainSubstring("lastUpdateTime")) }) It("report status for TestPassed test scenario", func() { hasSnapshot.Annotations["test.appstudio.openshift.io/status"] = "[{\"scenario\":\"scenario1\",\"status\":\"TestPassed\",\"testPipelineRunName\":\"test-pipelinerun\",\"startTime\":\"2023-07-26T16:57:49+02:00\",\"completionTime\":\"2023-07-26T17:57:49+02:00\",\"lastUpdateTime\":\"2023-08-26T17:57:55+02:00\",\"details\":\"failed\"}]" + integrationTestStatusDetail := newIntegrationTestStatusDetail(integrationteststatus.IntegrationTestStatusTestPassed) delete(hasSnapshot.Labels, "appstudio.openshift.io/component") ts, err := time.Parse(time.RFC3339, "2023-07-26T16:57:49+02:00") Expect(err).NotTo(HaveOccurred()) @@ -439,28 +548,22 @@ var _ = Describe("Status Adapter", func() { CompletionTime: &tc, TestPipelineRunName: "test-pipelinerun", } - mockReporter.EXPECT().Initialize(gomock.Any(), gomock.Any()).Times(1) - mockReporter.EXPECT().ReportStatus(gomock.Any(), gomock.Eq(expectedTestReport)).Times(1) - st := status.NewStatus(logr.Discard(), mockK8sClient) - err = st.ReportSnapshotStatus(context.Background(), mockReporter, hasSnapshot) + testReport, err := status.GenerateTestReport(context.Background(), mockK8sClient, integrationTestStatusDetail, hasSnapshot, "") Expect(err).NotTo(HaveOccurred()) + Expect(testReport).To(Equal(&expectedTestReport)) }) DescribeTable( "report right summary per status", func(expectedScenarioStatus integrationteststatus.IntegrationTestStatus, expectedTextEnding string) { - statusAnnotationTempl := "[{\"scenario\":\"scenario1\",\"status\":\"%s\",\"testPipelineRunName\":\"test-pipelinerun\",\"startTime\":\"2023-07-26T16:57:49+02:00\",\"completionTime\":\"2023-07-26T17:57:49+02:00\",\"lastUpdateTime\":\"2023-08-26T17:57:55+02:00\",\"details\":\"failed\"}]" - hasSnapshot.Annotations["test.appstudio.openshift.io/status"] = fmt.Sprintf(statusAnnotationTempl, expectedScenarioStatus) + integrationTestStatusDetail := newIntegrationTestStatusDetail(expectedScenarioStatus) expectedSummary := fmt.Sprintf("Integration test for snapshot snapshot-sample and scenario scenario1 %s", expectedTextEnding) - mockReporter.EXPECT().Initialize(gomock.Any(), gomock.Any()).Times(1) - mockReporter.EXPECT().ReportStatus(gomock.Any(), HasSummary(expectedSummary)).Times(1) - - st := status.NewStatus(logr.Discard(), mockK8sClient) - err := st.ReportSnapshotStatus(context.Background(), mockReporter, hasSnapshot) + testReport, err := status.GenerateTestReport(context.Background(), mockK8sClient, integrationTestStatusDetail, hasSnapshot, "component-sample") Expect(err).NotTo(HaveOccurred()) + Expect(testReport.Summary).To(Equal(expectedSummary)) }, Entry("Passed", integrationteststatus.IntegrationTestStatusTestPassed, "has passed"), Entry("Failed", integrationteststatus.IntegrationTestStatusTestFail, "has failed"), @@ -479,6 +582,13 @@ var _ = Describe("Status Adapter", func() { } }) + It("check getting component snapshots from group snapshot", func() { + componentSnapshots, err := status.GetComponentSnapshotsFromGroupSnapshot(context.Background(), mockK8sClient, groupSnapshot) + Expect(err).NotTo(HaveOccurred()) + Expect(componentSnapshots).To(HaveLen(2)) + + }) + Describe("SnapshotReportStatus (SRS)", func() { const ( scenarioName = "test-scenario" @@ -502,6 +612,7 @@ var _ = Describe("Status Adapter", func() { }) It("Reseting dirty bit works", func() { + Expect(mockK8sClient.Create(context.Background(), hasSnapshot)).Should(Succeed()) hasSRS.SetLastUpdateTime(scenarioName, now) Expect(hasSRS.IsDirty()).To(BeTrue()) @@ -513,9 +624,16 @@ var _ = Describe("Status Adapter", func() { HaveKeyWithValue(scenarioName, &status.ScenarioReportStatus{ LastUpdateTime: &now, })) + Expect(hasSnapshot.Annotations[gitops.SnapshotStatusReportAnnotation]).To(Equal("")) + err := status.WriteSnapshotReportStatus(context.Background(), mockK8sClient, hasSnapshot, hasSRS) + Expect(err).ToNot(HaveOccurred()) + Expect(hasSnapshot.Annotations[gitops.SnapshotStatusReportAnnotation]).NotTo(BeNil()) + err = mockK8sClient.Delete(context.Background(), hasSnapshot) + Expect(err == nil || errors.IsNotFound(err)).To(BeTrue()) }) It("New scenario can be added to SRS", func() { + Expect(mockK8sClient.Create(context.Background(), hasSnapshot)).Should(Succeed()) hasSRS.SetLastUpdateTime(scenarioName, now) Expect(hasSRS.IsDirty()).To(BeTrue()) @@ -523,6 +641,13 @@ var _ = Describe("Status Adapter", func() { HaveKeyWithValue(scenarioName, &status.ScenarioReportStatus{ LastUpdateTime: &now, })) + + Expect(hasSnapshot.Annotations[gitops.SnapshotStatusReportAnnotation]).To(Equal("")) + err := status.WriteSnapshotReportStatus(context.Background(), mockK8sClient, hasSnapshot, hasSRS) + Expect(err).ToNot(HaveOccurred()) + Expect(hasSnapshot.Annotations[gitops.SnapshotStatusReportAnnotation]).NotTo(BeNil()) + err = mockK8sClient.Delete(context.Background(), hasSnapshot) + Expect(err == nil || errors.IsNotFound(err)).To(BeTrue()) }) It("Additional scenario can be added to SRS", func() {