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 184bdc55d..8ddc210c9 100644 --- a/gitops/snapshot.go +++ b/gitops/snapshot.go @@ -21,7 +21,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/konflux-ci/integration-service/api/v1beta2" "reflect" "sort" "strconv" @@ -30,10 +29,12 @@ import ( "github.com/google/go-containerregistry/pkg/name" applicationapiv1alpha1 "github.com/konflux-ci/application-api/api/v1alpha1" + "github.com/konflux-ci/integration-service/api/v1beta2" "github.com/konflux-ci/integration-service/helpers" "github.com/konflux-ci/integration-service/pkg/metrics" "github.com/konflux-ci/integration-service/tekton" "github.com/konflux-ci/operator-toolkit/metadata" + "github.com/santhosh-tekuri/jsonschema/v5" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -245,6 +246,29 @@ type ComponentSnapshotInfo struct { Snapshot string `json:"snapshot"` } +const componentSnapshotInfosSchema = `{ + "$schema": "http://json-schema.org/draft/2020-12/schema#", + "type": "array", + "items": { + "type": "object", + "properties": { + "namespace": { + "type": "string" + }, + "component": { + "type": "string" + }, + "buildPipelineRun": { + "type": "string" + }, + "snapshot": { + "type": "string" + } + }, + "required": ["namespace", "component", "buildPipelineRun", "snapshot"] + } + }` + // IsSnapshotMarkedAsPassed returns true if snapshot is marked as passed func IsSnapshotMarkedAsPassed(snapshot *applicationapiv1alpha1.Snapshot) bool { return IsSnapshotStatusConditionSet(snapshot, AppStudioTestSucceededCondition, metav1.ConditionTrue, "") @@ -912,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) } @@ -1090,3 +1115,26 @@ func SetAnnotationAndLabelForGroupSnapshot(groupSnapshot *applicationapiv1alpha1 return groupSnapshot, nil } + +// UnmarshalJSON load data from JSON +func UnmarshalJSON(b []byte) ([]*ComponentSnapshotInfo, error) { + var componentSnapshotInfos []*ComponentSnapshotInfo + + sch, err := jsonschema.CompileString("schema.json", componentSnapshotInfosSchema) + if err != nil { + return nil, fmt.Errorf("error while compiling json data for schema validation: %w", err) + } + var v interface{} + if err := json.Unmarshal(b, &v); err != nil { + return nil, fmt.Errorf("failed to unmarshal json data raw: %w", err) + } + if err = sch.Validate(v); err != nil { + return nil, fmt.Errorf("error validating snapshot info: %w", err) + } + err = json.Unmarshal(b, &componentSnapshotInfos) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal json data: %w", err) + } + + return componentSnapshotInfos, nil +} diff --git a/gitops/snapshot_test.go b/gitops/snapshot_test.go index 53e678d9e..38fcf8fc1 100644 --- a/gitops/snapshot_test.go +++ b/gitops/snapshot_test.go @@ -842,6 +842,35 @@ var _ = Describe("Gitops functions for managing Snapshots", Ordered, func() { filteredScenarios = gitops.FilterIntegrationTestScenariosWithContext(&allScenarios, hasSnapshot) Expect(*filteredScenarios).To(HaveLen(3)) }) + + It("Testing annotating snapshot", func() { + componentSnapshotInfos := []gitops.ComponentSnapshotInfo{ + { + Component: "com1", + Snapshot: "snapshot1", + BuildPipelineRun: "buildPLR1", + Namespace: "default", + }, + { + Component: "com2", + Snapshot: "snapshot2", + BuildPipelineRun: "buildPLR2", + Namespace: "default", + }, + } + snapshot, err := gitops.SetAnnotationAndLabelForGroupSnapshot(hasSnapshot, hasSnapshot, componentSnapshotInfos) + Expect(err).ToNot(HaveOccurred()) + Expect(componentSnapshotInfos).To(HaveLen(2)) + Expect(snapshot.Labels[gitops.SnapshotTypeLabel]).To(Equal("group")) + }) + + It("Testing UnmarshalJSON", func() { + infoString := "[{\"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\"}]" + componentSnapshotInfos, err := gitops.UnmarshalJSON([]byte(infoString)) + Expect(err).ToNot(HaveOccurred()) + Expect(componentSnapshotInfos[0].Namespace).To(Equal("default")) + Expect(componentSnapshotInfos).To(HaveLen(2)) + }) }) }) diff --git a/internal/controller/statusreport/statusreport_adapter.go b/internal/controller/statusreport/statusreport_adapter.go index 712180446..65ba5bf94 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,145 @@ 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 { + 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()), "destinationComponentSnapshot.Name", destinationComponentSnapshot.Name, "testedSnapshot", testedSnapshot.Name) + + 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 { + a.logger.Error(err, fmt.Sprintf("failed to report integration test status for snapshot %s/%s", + destinationComponentSnapshot.Namespace, destinationComponentSnapshot.Name)) + 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 { + a.logger.Error(err, "failed to write snapshot report status metadata") + 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, "destinationSnapshot.Name", destinationSnapshot.Name, "testedSnapshot", testedSnapshot.Name) + } else { + //integration test contains no changes + a.logger.Info("Integration Test doen't contain new status updates", "scenario.Name", integrationTestStatusDetail.ScenarioName) + 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) + } + a.logger.Info("Successfully report integration test status for snapshot", + "testedSnapshot.Name", testedSnapshot.Name, + "destinationSnapshot.Name", destinationSnapshot.Name, + "testStatus", integrationTestStatusDetail.Status) + 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..22362ea01 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("testing ReportSnapshotStatus", 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/format.go b/status/format.go index 7e485b914..46058309d 100644 --- a/status/format.go +++ b/status/format.go @@ -24,6 +24,7 @@ import ( "text/template" "github.com/go-logr/logr" + "github.com/konflux-ci/integration-service/gitops" "github.com/konflux-ci/integration-service/helpers" "knative.dev/pkg/apis" ) @@ -45,14 +46,23 @@ const summaryTemplate = ` | {{ formatTaskName $tr }} | {{ $tr.GetDuration.String }} | {{ formatNamespace $tr }} | {{ formatStatus $tr }} | {{ formatDetails $tr }} | {{- end }} -{{ formatFootnotes .TaskRuns }}` +{{ formatFootnotes .TaskRuns }} +{{ if .ComponentSnapshotInfos}} +The group snapshot is generated for the component snasphots as below: +| Component | Snapshot | BuildPipelineRun | +| --- | --- | --- | +{{- range $cs := .ComponentSnapshotInfos }} +| {{ $cs.Component }} | {{ $cs.Snapshot }} | {{ $cs.BuildPipelineRun }} | +{{- end }} +{{end}}` // SummaryTemplateData holds the data necessary to construct a PipelineRun summary. type SummaryTemplateData struct { - TaskRuns []*helpers.TaskRun - PipelineRunName string - Namespace string - Logger logr.Logger + TaskRuns []*helpers.TaskRun + PipelineRunName string + Namespace string + ComponentSnapshotInfos []*gitops.ComponentSnapshotInfo + Logger logr.Logger } // TaskLogTemplateData holds the data necessary to construct a Task log URL. @@ -69,7 +79,7 @@ type CommentTemplateData struct { } // FormatTestsSummary builds a markdown summary for a list of integration TaskRuns. -func FormatTestsSummary(taskRuns []*helpers.TaskRun, pipelineRunName string, namespace string, logger logr.Logger) (string, error) { +func FormatTestsSummary(taskRuns []*helpers.TaskRun, pipelineRunName string, namespace string, componentSnapshotInfos []*gitops.ComponentSnapshotInfo, logger logr.Logger) (string, error) { funcMap := template.FuncMap{ "formatTaskName": FormatTaskName, "formatNamespace": FormatNamespace, @@ -80,7 +90,7 @@ func FormatTestsSummary(taskRuns []*helpers.TaskRun, pipelineRunName string, nam "formatFootnotes": FormatFootnotes, } buf := bytes.Buffer{} - data := SummaryTemplateData{TaskRuns: taskRuns, PipelineRunName: pipelineRunName, Namespace: namespace, Logger: logger} + data := SummaryTemplateData{TaskRuns: taskRuns, PipelineRunName: pipelineRunName, Namespace: namespace, ComponentSnapshotInfos: componentSnapshotInfos, Logger: logger} t := template.Must(template.New("").Funcs(funcMap).Parse(summaryTemplate)) if err := t.Execute(&buf, data); err != nil { return "", err diff --git a/status/format_test.go b/status/format_test.go index 504e4cf08..d4303067c 100644 --- a/status/format_test.go +++ b/status/format_test.go @@ -21,6 +21,7 @@ import ( "time" "github.com/go-logr/logr" + "github.com/konflux-ci/integration-service/gitops" "github.com/konflux-ci/integration-service/helpers" "github.com/konflux-ci/integration-service/status" . "github.com/onsi/ginkgo/v2" @@ -46,11 +47,13 @@ const expectedSummary = `