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 = `