Skip to content

Commit

Permalink
feat(KONFLUX-5971): set intg test status in git according to PR bld PLR
Browse files Browse the repository at this point in the history
* set integration tests status to pending when build plr is triggered or
  retriggered
* set integration test status to failed/cancelled when build plr fails
* set integration test status to failed/cancelled with failure reason
  when snapshot is not created to show to users on git provider

Signed-off-by: Hongwei Liu<[email protected]>
  • Loading branch information
hongweiliu17 committed Jan 14, 2025
1 parent 7744cfb commit 96355e8
Show file tree
Hide file tree
Showing 16 changed files with 494 additions and 35 deletions.
8 changes: 8 additions & 0 deletions docs/build_pipeline_controller.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ new_pipeline_run_without_prgroup{PR group is added to pipelineRun metadata?}
get_pipeline_run{Pipeline updated?}
failed_pipeline_run{Pipeline failed?}
finalizer_exists{Does the finalizer already exist?}
need_to_set_integration_test{Build pipelineRun is newly triggered?<br>Or Build pipelineRun failed?<br> Or Failing to create snapshot?}
retrieve_associated_entity(Retrieve the entity <br> component/application)
determine_snapshot{Does a snapshot exist?}
prep_snapshot(Gather Application components<br> Add new component)
Expand All @@ -28,12 +29,15 @@ continue[Continue processing]
update_metadata(add PR group info to build pipelineRun metadata)
notify_pr_group_failure(annotate Snapshots and in-flight builds in PR group with failure message)
failed_group_pipeline_run{Pipeline failed?}
update_integrationTestStatus_in_git_provider(Create checkRun/commitStatus in<br>git provider)
update_build_plr_annotation(Update build pipelineRun annotation<br>test.appstudio.openshift.io/snapshot-creation-report<br>with the status)
%% Node connections
predicate --> get_pipeline_run
predicate --> new_pipeline_run
predicate --> new_pipeline_run_without_prgroup
predicate --> failed_pipeline_run
predicate --> need_to_set_integration_test
new_pipeline_run --Yes--> finalizer_exists
finalizer_exists --No--> add_finalizer
add_finalizer --> continue
Expand All @@ -55,6 +59,10 @@ prep_snapshot --> check_chains
check_chains --Yes --> annotate_pipelineRun
annotate_pipelineRun --Yes --> remove_finalizer
remove_finalizer --> continue
need_to_set_integration_test --Yes --> update_integrationTestStatus_in_git_provider
need_to_set_integration_test --No --> continue
update_integrationTestStatus_in_git_provider --> update_build_plr_annotation
update_build_plr_annotation --> continue
%% Assigning styles to nodes
class predicate Amber;
Expand Down
3 changes: 3 additions & 0 deletions gitops/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,9 @@ const (

//IntegrationTestStatusInProgressGithub is the status reported to github when integration test is in progress
IntegrationTestStatusInProgressGithub = "in_progress"

//IntegrationTestStatusCancelledGithub is the status reported to github when integration test is cancelled
IntegrationTestStatusCancelledGithub = "cancelled"
)

var (
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/konflux-ci/integration-service

go 1.22
go 1.22.0

require (
github.com/agiledragon/gomonkey/v2 v2.12.0
Expand Down
9 changes: 8 additions & 1 deletion helpers/build.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
package helpers

const CreateSnapshotAnnotationName = "test.appstudio.openshift.io/create-snapshot-status"
const (
// CreateSnapshotAnnotationName contains metadata of snapshot creation failure or success
CreateSnapshotAnnotationName = "test.appstudio.openshift.io/create-snapshot-status"

// SnapshotCreationReportAnnotation contains metadata of snapshot creation status reporting to git provider
// to initialize integration test or set it to cancelled or failed
SnapshotCreationReportAnnotation = "test.appstudio.openshift.io/snapshot-creation-report"
)
191 changes: 191 additions & 0 deletions internal/controller/buildpipeline/buildpipeline_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,18 @@ import (
"k8s.io/client-go/util/retry"

applicationapiv1alpha1 "github.com/konflux-ci/application-api/api/v1alpha1"
"github.com/konflux-ci/integration-service/api/v1beta2"
"github.com/konflux-ci/integration-service/gitops"
h "github.com/konflux-ci/integration-service/helpers"
"github.com/konflux-ci/integration-service/loader"
intgteststat "github.com/konflux-ci/integration-service/pkg/integrationteststatus"
"github.com/konflux-ci/integration-service/status"
"github.com/konflux-ci/integration-service/tekton"
"github.com/konflux-ci/operator-toolkit/controller"
"github.com/konflux-ci/operator-toolkit/metadata"
tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)
Expand All @@ -47,6 +51,7 @@ type Adapter struct {
logger h.IntegrationLogger
client client.Client
context context.Context
status status.StatusInterface
}

// NewAdapter creates and returns an Adapter instance.
Expand All @@ -61,6 +66,7 @@ func NewAdapter(context context.Context, pipelineRun *tektonv1.PipelineRun, comp
loader: loader,
client: client,
context: context,
status: status.NewStatus(logger.Logger, client),
}
}

Expand Down Expand Up @@ -189,6 +195,69 @@ func (a *Adapter) EnsurePRGroupAnnotated() (controller.OperationResult, error) {
return controller.ContinueProcessing()
}

// EnsureIntegrationTestReportedToGitProvider is an operation that will ensure that the integration test status is initialized as pending
// state when build PLR is triggered/retriggered or cancelled/failed when failing to create snapshot
// to prevent PR/MR from being automerged unexpectedly and also show the snapshot creation failure on PR/MR
func (a *Adapter) EnsureIntegrationTestReportedToGitProvider() (controller.OperationResult, error) {
if tekton.IsPLRCreatedByPACPushEvent(a.pipelineRun) {
a.logger.Info("build pipelineRun is not created by pull/merge request, no need to set integration test status in git provider")
return controller.ContinueProcessing()
}

if metadata.HasAnnotation(a.pipelineRun, tekton.SnapshotNameLabel) {
a.logger.Info("snapshot has been created for build pipelineRun, no need to report integration status from build pipelinerun status")
if err := tekton.AnnotateBuildPipelineRun(a.context, a.pipelineRun, h.SnapshotCreationReportAnnotation, "SnapshotCreated", a.client); err != nil {
a.logger.Error(err, "failed to annotate build pipelineRun")
return controller.RequeueWithError(err)
}
return controller.ContinueProcessing()
}

integrationTestStatus := getIntegrationTestStatusFromBuildPLR(a.pipelineRun)

if integrationTestStatus == intgteststat.IntegrationTestStatus(0) {
a.logger.Info("integration test has been set correctly or is being processed, no need to set integration test status from build pipelinerun")
return controller.ContinueProcessing()
}

a.logger.Info("try to set integration test status according to the build PLR status")
tempSnapshot := a.prepareTemporarySnapshot(a.pipelineRun)

allIntegrationTestScenarios, err := a.loader.GetAllIntegrationTestScenariosForApplication(a.context, a.client, a.application)
if err != nil {
a.logger.Error(err, "Failed to get integration test scenarios for the following application",
"Application.Namespace", a.application.Namespace, "Application.Name", a.application.Name)
return controller.RequeueWithError(err)
}

if allIntegrationTestScenarios != nil {
// Handle context in integrationTestScenario, but defer handling of 'group' for now
integrationTestScenarios := gitops.FilterIntegrationTestScenariosWithContext(allIntegrationTestScenarios, tempSnapshot)
a.logger.Info(
fmt.Sprintf("Found %d IntegrationTestScenarios for application", len(*integrationTestScenarios)),
"Application.Name", a.application.Name,
"IntegrationTestScenarios", len(*integrationTestScenarios))
if len(*integrationTestScenarios) == 0 {
a.logger.Info("no need to report integration test status since no integrationTestScenario can be applied to snapshot created for build pipelinerun")
return controller.ContinueProcessing()
}

err := a.ReportIntegrationTestStatusAccordingToBuildPLR(a.pipelineRun, tempSnapshot, integrationTestScenarios, integrationTestStatus, a.component)
if err != nil {
a.logger.Error(err, "failed to report snapshot creation status to git provider from build pipelineRun",
"pipelineRun.Namespace", a.pipelineRun.Namespace, "pipelineRun.Name", a.pipelineRun.Name)
return controller.RequeueWithError(err)
}

if err = tekton.AnnotateBuildPipelineRun(a.context, a.pipelineRun, h.SnapshotCreationReportAnnotation, integrationTestStatus.String(), a.client); err != nil {
a.logger.Error(err, fmt.Sprintf("failed to write build plr annotation %s", h.SnapshotCreationReportAnnotation))
return controller.RequeueWithError(fmt.Errorf("failed to write snapshot report status metadata for annotation %s: %w", h.SnapshotCreationReportAnnotation, err))
}
}
return controller.ContinueProcessing()

}

// getImagePullSpecFromPipelineRun gets the full image pullspec from the given build PipelineRun,
// In case the Image pullspec can't be composed, an error will be returned.
func (a *Adapter) getImagePullSpecFromPipelineRun(pipelineRun *tektonv1.PipelineRun) (string, error) {
Expand Down Expand Up @@ -454,3 +523,125 @@ func (a *Adapter) addPRGroupToBuildPLRMetadata(pipelineRun *tektonv1.PipelineRun
a.logger.Info("can't find source branch info in build PLR, not need to update build pipelineRun metadata")
return nil
}

// prepareTemporarySnapshot will create a temporary snapshot object to copy the labels/annotations from build pipelinerun
// and be used to communicate with git provider
func (a *Adapter) prepareTemporarySnapshot(pipelineRun *tektonv1.PipelineRun) *applicationapiv1alpha1.Snapshot {
tempSnapshot := &applicationapiv1alpha1.Snapshot{
ObjectMeta: metav1.ObjectMeta{
Name: "tempSnapshot",
Namespace: pipelineRun.Namespace,
},
}
prefixes := []string{gitops.BuildPipelineRunPrefix, gitops.TestLabelPrefix, gitops.CustomLabelPrefix}
gitops.CopySnapshotLabelsAndAnnotations(a.application, tempSnapshot, a.component.Name, &pipelineRun.ObjectMeta, prefixes)
return tempSnapshot
}

func (a *Adapter) ReportIntegrationTestStatusAccordingToBuildPLR(pipelineRun *tektonv1.PipelineRun, snapshot *applicationapiv1alpha1.Snapshot, integrationTestScenarios *[]v1beta2.IntegrationTestScenario,
integrationTestStatus intgteststat.IntegrationTestStatus, component *applicationapiv1alpha1.Component) error {
reporter := a.status.GetReporter(snapshot)
if reporter == nil {
a.logger.Info("No suitable reporter found, skipping report")
return nil
}
a.logger.Info(fmt.Sprintf("Detected reporter: %s", reporter.GetReporterName()))

if err := reporter.Initialize(a.context, snapshot); 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.iterateIntegrationTestInStatusReport(reporter, pipelineRun, snapshot, integrationTestScenarios, integrationTestStatus, component)
if err != nil {
a.logger.Error(err, fmt.Sprintf("failed to report integration test status according to build pipelinerun %s/%s",
pipelineRun.Namespace, pipelineRun.Name))
return fmt.Errorf("failed to report integration test status according to build pipelineRun %s/%s: %w",
pipelineRun.Namespace, pipelineRun.Name, err)
}

a.logger.Info("Successfully report integration test status for build pipelineRun",
"pipelineRun.Namespace", pipelineRun.Namespace,
"pipelineRun.Name", pipelineRun.Name,
"build pipelineRun Status", integrationTestStatus.String())

return err
})

if err != nil {
return fmt.Errorf("issue occurred during generating or updating report status: %w", err)
}

a.logger.Info(fmt.Sprintf("Successfully updated the %s annotation", gitops.SnapshotStatusReportAnnotation), "pipelinerun.Name", pipelineRun.Name)

return nil
}

// iterates integrationTestScenarios to report to integrationTestScenario and component
func (a *Adapter) iterateIntegrationTestInStatusReport(reporter status.ReporterInterface,
buildPLR *tektonv1.PipelineRun,
snapshot *applicationapiv1alpha1.Snapshot,
integrationTestScenarios *[]v1beta2.IntegrationTestScenario,
intgteststatus intgteststat.IntegrationTestStatus,
component *applicationapiv1alpha1.Component) error {

details := generateDetails(buildPLR, intgteststatus)
integrationTestStatusDetail := intgteststat.IntegrationTestStatusDetail{
Status: intgteststatus,
Details: details,
}
for _, integrationTestScenario := range *integrationTestScenarios {
integrationTestScenario := integrationTestScenario //G601
integrationTestStatusDetail.ScenarioName = integrationTestScenario.Name

testReport, reportErr := status.GenerateTestReport(a.context, a.client, integrationTestStatusDetail, snapshot, component.Name)
if reportErr != nil {
return fmt.Errorf("failed to generate test report: %w", reportErr)
}
if reportStatusErr := reporter.ReportStatus(a.context, *testReport); reportStatusErr != nil {
return fmt.Errorf("failed to report status to git provider: %w", reportStatusErr)
}
}
return nil
}

func getIntegrationTestStatusFromBuildPLR(plr *tektonv1.PipelineRun) intgteststat.IntegrationTestStatus {
var integrationTestStatus intgteststat.IntegrationTestStatus
// when build pipeline is triggered/retriggered, we need to set integration test status to pending
if !h.HasPipelineRunFinished(plr) && !metadata.HasAnnotationWithValue(plr, h.SnapshotCreationReportAnnotation, intgteststat.BuildPLRInProgress.String()) {
integrationTestStatus = intgteststat.BuildPLRInProgress
return integrationTestStatus
}
// when build pipeline fails, we need to set integreation test to canceled or failed
if h.HasPipelineRunFinished(plr) && !h.HasPipelineRunSucceeded(plr) && !metadata.HasAnnotationWithValue(plr, h.SnapshotCreationReportAnnotation, intgteststat.BuildPLRFailed.String()) {
integrationTestStatus = intgteststat.BuildPLRFailed
return integrationTestStatus
}
// when build pipeline succeeds but snapshot is not created, we need to set integration test to canceled or failed
if h.HasPipelineRunSucceeded(plr) && metadata.HasAnnotation(plr, h.CreateSnapshotAnnotationName) && !metadata.HasAnnotation(plr, tekton.SnapshotNameLabel) && !metadata.HasAnnotationWithValue(plr, h.SnapshotCreationReportAnnotation, intgteststat.SnapshotCreationFailed.String()) {
integrationTestStatus = intgteststat.SnapshotCreationFailed
return integrationTestStatus
}
return integrationTestStatus
}

// generateDetails generates details for integrationTestStatusDetail
func generateDetails(buildPLR *tektonv1.PipelineRun, integrationTestStatus intgteststat.IntegrationTestStatus) string {
details := ""
if integrationTestStatus == intgteststat.BuildPLRInProgress {
details = fmt.Sprintf("build pipelinerun %s/%s is still in progress", buildPLR.Namespace, buildPLR.Name)
}
if integrationTestStatus == intgteststat.SnapshotCreationFailed {
if failureReason, ok := buildPLR.Annotations[h.CreateSnapshotAnnotationName]; ok {
details = fmt.Sprintf("build Pipelinerun %s/%s succeeds but snapshot is not created due to error: %s", buildPLR.Namespace, buildPLR.Name, failureReason)
} else {
details = fmt.Sprintf("failed to create snapshot but can't find reason from build plr annotation %s", h.CreateSnapshotAnnotationName)
}
}
if integrationTestStatus == intgteststat.BuildPLRFailed {
details = fmt.Sprintf("build Pipelinerun %s/%s failed, so that snapshot is not created. Please fix build pipelinerun failure and then rerun build pipelinerun.", buildPLR.Namespace, buildPLR.Name)
}
return details
}
Loading

0 comments on commit 96355e8

Please sign in to comment.