Skip to content

Commit

Permalink
Merge pull request #860 from hongweiliu17/STONEINTG-999-1
Browse files Browse the repository at this point in the history
feat(STONEINTG-999): report test status for group snapshot
  • Loading branch information
hongweiliu17 authored Sep 25, 2024
2 parents 93714e6 + 0d83137 commit 691989d
Show file tree
Hide file tree
Showing 11 changed files with 861 additions and 229 deletions.
4 changes: 3 additions & 1 deletion docs/statusreport-controller.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ flowchart TD
%% Node definitions
ensure(Process further if: Snapshot has label <br>pac.test.appstudio.openshift.io/git-provider:github <br>defined)
get_annotation_value(Get integration test status from annotation <br>test.appstudio.openshift.io/status <br>from Snapshot)
get_destination_snapshot(Get destination snapshots from <br>component snapshot or group snapshot <br>to collect git provider info)
detect_git_provider{Detect git provider}
Expand Down Expand Up @@ -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
Expand Down
50 changes: 49 additions & 1 deletion gitops/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/konflux-ci/integration-service/api/v1beta2"
"reflect"
"sort"
"strconv"
Expand All @@ -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"
Expand Down Expand Up @@ -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, "")
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
}
29 changes: 29 additions & 0 deletions gitops/snapshot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
})
})
})

Expand Down
157 changes: 148 additions & 9 deletions internal/controller/statusreport/statusreport_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
Loading

0 comments on commit 691989d

Please sign in to comment.