Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(STONEINTG-999): report test status for group snapshot #860

Merged
merged 2 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading