diff --git a/docs/build_pipeline_controller.md b/docs/build_pipeline_controller.md index fa1d3f899d..5cb8fc55cc 100644 --- a/docs/build_pipeline_controller.md +++ b/docs/build_pipeline_controller.md @@ -17,9 +17,9 @@ 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?
Or Build pipelineRun failed?
Or Failing to create snapshot?} -retrieve_associated_entity(Retrieve the entity
component/application) +retrieve_associated_entity(Retrieve the entity
component/application or componentGroup) determine_snapshot{Does a snapshot exist?} -prep_snapshot(Gather Application components
Add new component) +prep_snapshot(Gather Application or ComponentGroup components
Add new component) check_chains{Chains annotation present?} annotate_pipelineRun(Annotate pipeline with
name of Snapshot) add_finalizer(Add finalizer to build PLR) diff --git a/docs/snapshot-controller.md b/docs/snapshot-controller.md index 5b75b70aaa..ab2fa34439 100644 --- a/docs/snapshot-controller.md +++ b/docs/snapshot-controller.md @@ -14,10 +14,10 @@ flowchart TD %% Node definitions ensure1(Process further if: Snapshot testing
is not finished yet) - are_there_any_ITS{"Are there any
IntegrationTestScenario
present for the given
Application?"} + are_there_any_ITS{"Are there any
IntegrationTestScenario
present for the given
Application/ComponentGroup?"} create_new_test_PLR(Create a new Test PipelineRun for each
of the above ITS, if it doesn't exists already) mark_snapshot_InProgress(Mark Snapshot's Integration-testing
status as 'InProgress') - fetch_all_required_ITS("Fetch all the required
(non-optional) IntegrationTestScenario
for the given Application
filtered by ITS context(s)") + fetch_all_required_ITS("Fetch all the required
(non-optional) IntegrationTestScenario
for the given Application/ComponentGroup
filtered by ITS context(s)") encountered_error1{Encountered error?} mark_snapshot_Invalid1(Mark the Snapshot as Invalid) is_atleast_1_required_ITS{Is there atleast
1 required ITS?} @@ -60,7 +60,7 @@ flowchart TD %% Node definitions ensure3(Process further if: Snapshot is valid &
Snapshot testing succeeded &
Snapshot was not created by
PAC Pull Request Event &
Snapshot wasn't auto-released) - fetch_all_ReleasePlans("Fetch ALL the ReleasePlan CRs
for the given Application, that have the
'release.appstudio.openshift.io/auto-release'
label set to 'True'") + fetch_all_ReleasePlans("Fetch ALL the ReleasePlan CRs
for the given Application/ComponentGroup, that have the
'release.appstudio.openshift.io/auto-release'
label set to 'True'") encountered_error31{Encountered error?} create_Release(Create a Release for each of the above
ReleasePlan if it doesn't exists already) encountered_error32{Encountered error?} @@ -120,7 +120,7 @@ flowchart TD ensure5(Process further if: Snapshot has neither push event type label
nor PRGroupCreation annotation) validate_build_pipelinerun{Did all gotten build pipelineRun
under the same group
succeed and
component snapshot are already created?} annotate_component_snapshot(Annotate component snapshot) - get_component_snapshots_and_sort(Iterate all application components and
get all component snapshots
for each component under the same pr group sha
then sort snapshots) + get_component_snapshots_and_sort(Iterate all application or componentGroup components and
get all component snapshots
for each component under the same pr group sha
then sort snapshots) can_find_snapshotComponent_from_latest_snapshot(Can find the latest snapshot with open pull/merge request?) add_snapshot_to_group_snapshot_candidate(Add snapshotComponent of component
to group snapshot components candidate) get_snapshotComponent_from_gcl(Get snapshotComponent from
Global Candidate List) diff --git a/gitops/snapshot.go b/gitops/snapshot.go index c67a662731..67a800a529 100644 --- a/gitops/snapshot.go +++ b/gitops/snapshot.go @@ -359,6 +359,8 @@ type ComponentSnapshotInfo struct { RepoUrl string `json:"repoUrl"` // Pull/Merge request number for updated component PullRequestNumber string `json:"pullRequestNumber"` + // Version of the component + Version string `json:"version"` } // AddedToGlobalCandidateListStatus contains the information which will be added to build PLR or override snapshot about updating GCL @@ -394,6 +396,9 @@ const componentSnapshotInfosSchema = `{ }, "pullRequestNumber": { "type": "string" + }, + "version": { + "type": "string" } }, "required": ["namespace", "component", "buildPipelineRun", "snapshot"] @@ -783,6 +788,7 @@ func CanSnapshotBePromoted(snapshot *applicationapiv1alpha1.Snapshot) (bool, []s } // NewSnapshot creates a new snapshot based on the supplied application and components +// TODO: Remove this function once application model is fully deprecated [APPLICATION] - a matching function which supports ComponentGroups is located in the snapshot package func NewSnapshot(application *applicationapiv1alpha1.Application, snapshotComponents *[]applicationapiv1alpha1.SnapshotComponent) *applicationapiv1alpha1.Snapshot { // Use fallback timestamp (current time) - will be overridden in prepareSnapshotForPipelineRun // if BuildPipelineRunStartTime is available @@ -1045,7 +1051,7 @@ func PrepareSnapshot(ctx context.Context, adapterClient client.Client, applicati return snapshot, nil } -// FindMatchingSnapshot tries to finds the expected Snapshot with the same set of images. +// FindMatchingSnapshot tries to find the expected Snapshot with the same set of images. func FindMatchingSnapshot(application *applicationapiv1alpha1.Application, allSnapshots *[]applicationapiv1alpha1.Snapshot, expectedSnapshot *applicationapiv1alpha1.Snapshot) *applicationapiv1alpha1.Snapshot { for _, foundSnapshot := range *allSnapshots { foundSnapshot := foundSnapshot @@ -1228,7 +1234,7 @@ func CopyBuildPipelineRunResultsToSnapshot(pipelineRun *tektonv1.PipelineRun, sn // CopyTempGroupSnapshotLabelsAndAnnotations coppies labels and annotations from build pipelineRun or tested snapshot // into regular snapshot -func CopyTempGroupSnapshotLabelsAndAnnotations(application *applicationapiv1alpha1.Application, snapshot *applicationapiv1alpha1.Snapshot, componentName string, source *metav1.ObjectMeta, prefixes []string) { +func CopyTempGroupSnapshotLabelsAndAnnotations(object *metav1.ObjectMeta, snapshot *applicationapiv1alpha1.Snapshot, componentName string, source *metav1.ObjectMeta, prefixes []string, objectIsApplication bool) { if snapshot.Labels == nil { snapshot.Labels = map[string]string{} } @@ -1238,7 +1244,12 @@ func CopyTempGroupSnapshotLabelsAndAnnotations(application *applicationapiv1alph } snapshot.Labels[SnapshotTypeLabel] = SnapshotGroupType - snapshot.Labels[ApplicationNameLabel] = application.Name + if objectIsApplication { + snapshot.Labels[ApplicationNameLabel] = object.Name + } else { + // the object is a ComponentGroup + snapshot.Labels[ComponentGroupNameLabel] = object.Name + } // Copy PAC annotations/labels from source(tested snapshot or pipelinerun) to snapshot. _ = metadata.CopyLabelsWithPrefixReplacement(source, &snapshot.ObjectMeta, "pipelinesascode.tekton.dev", PipelinesAsCodePrefix) @@ -1366,9 +1377,9 @@ func GetPRGroup(object client.Object) (string, string) { } // FindMatchingSnapshotComponent find the snapshot component from the given snapshot according to the name of the given component name -func FindMatchingSnapshotComponent(snapshot *applicationapiv1alpha1.Snapshot, component *applicationapiv1alpha1.Component) applicationapiv1alpha1.SnapshotComponent { +func FindMatchingSnapshotComponent(snapshot *applicationapiv1alpha1.Snapshot, componentName string) applicationapiv1alpha1.SnapshotComponent { for _, snapshotComponent := range snapshot.Spec.Components { - if snapshotComponent.Name == component.Name { + if snapshotComponent.Name == componentName { return snapshotComponent } } @@ -1460,7 +1471,11 @@ func SetAnnotationAndLabelForGroupSnapshot(groupSnapshot *applicationapiv1alpha1 } groupSnapshot.Labels[SnapshotTypeLabel] = SnapshotGroupType groupSnapshot.Labels[PRGroupHashLabel] = componentSnapshot.Labels[PRGroupHashLabel] - groupSnapshot.Labels[ApplicationNameLabel] = componentSnapshot.Spec.Application + if componentSnapshot.Spec.Application != "" { + groupSnapshot.Labels[ApplicationNameLabel] = componentSnapshot.Spec.Application + } else { + groupSnapshot.Labels[ComponentGroupNameLabel] = componentSnapshot.Spec.ComponentGroup + } return groupSnapshot, nil } @@ -1522,6 +1537,7 @@ func GetShaFromSnapshot(ctx context.Context, snapshot *applicationapiv1alpha1.Sn } // PrepareTempGroupSnapshot will prepare a temp group snapshot used to check the integration test scenario that should be applied to the group snapshot under that application +// TODO: Remove this function once application model is fully deprecated [APPLICATION] - a matching function which supports ComponentGroups is located in the snapshot package func PrepareTempGroupSnapshot(application *applicationapiv1alpha1.Application, snapshot *applicationapiv1alpha1.Snapshot) *applicationapiv1alpha1.Snapshot { tempGroupSnapshot := NewSnapshot(application, &[]applicationapiv1alpha1.SnapshotComponent{}) tempGroupSnapshot, _ = SetAnnotationAndLabelForGroupSnapshot(tempGroupSnapshot, snapshot, []ComponentSnapshotInfo{}) diff --git a/gitops/snapshot_test.go b/gitops/snapshot_test.go index 21e7226d52..3ae85a08f9 100644 --- a/gitops/snapshot_test.go +++ b/gitops/snapshot_test.go @@ -548,7 +548,7 @@ var _ = Describe("Gitops functions for managing Snapshots", Ordered, func() { Expect(createdSnapshot.Name).To(MatchRegexp(`^application-sample-\d{8}-\d{6}-\d{3}$`)) }) - It("ensures NewSnapshot truncates application name if longer than 43 characters", func() { + It("ensures NewApplicationSnapshot truncates application name if longer than 43 characters [APPLICATION]", func() { longAppName := "this-is-a-very-long-application-name-that-exceeds-43-chars" longApp := &applicationapiv1alpha1.Application{ ObjectMeta: metav1.ObjectMeta{ @@ -572,7 +572,7 @@ var _ = Describe("Gitops functions for managing Snapshots", Ordered, func() { Expect(snapshot.Spec.Application).To(Equal(longAppName)) }) - It("ensures NewSnapshot does not truncate application name at 43 characters", func() { + It("ensures NewApplicationSnapshot does not truncate application name at 43 characters [APPLICATION]", func() { exactAppName := "this-is-application-name-exactly-43-chars" // 43 chars exactApp := &applicationapiv1alpha1.Application{ ObjectMeta: metav1.ObjectMeta{ @@ -1243,7 +1243,7 @@ var _ = Describe("Gitops functions for managing Snapshots", Ordered, func() { }) It("Can find the correct snapshotComponent for the given component name", func() { - FoundSnapshotComponent := gitops.FindMatchingSnapshotComponent(hasComSnapshot1, hasComp) + FoundSnapshotComponent := gitops.FindMatchingSnapshotComponent(hasComSnapshot1, hasComp.Name) Expect(FoundSnapshotComponent.Name).To(Equal(hasComp.Name)) }) @@ -1353,7 +1353,7 @@ var _ = Describe("Gitops functions for managing Snapshots", Ordered, func() { }, } prefixes := []string{gitops.BuildPipelineRunPrefix} - gitops.CopyTempGroupSnapshotLabelsAndAnnotations(hasApp, tempGroupSnapshot, hasComp.Name, &buildPipelineRun.ObjectMeta, prefixes) + gitops.CopyTempGroupSnapshotLabelsAndAnnotations(&hasApp.ObjectMeta, tempGroupSnapshot, hasComp.Name, &buildPipelineRun.ObjectMeta, prefixes, true) Expect(metadata.HasLabel(tempGroupSnapshot, "pac.test.appstudio.openshift.io/event-type")).To(BeTrue()) Expect(metadata.HasLabel(tempGroupSnapshot, "appstudio.openshift.io/component")).To(BeFalse()) }) diff --git a/helpers/component_group.go b/helpers/component_group.go index 1899c33819..d1eb403b50 100644 --- a/helpers/component_group.go +++ b/helpers/component_group.go @@ -22,8 +22,20 @@ import ( func GetComponentGroupNames(componentGroups *[]v1beta2.ComponentGroup) []string { names := []string{} - for _, componentGroup := range *componentGroups { - names = append(names, componentGroup.Name) + if componentGroups != nil { + for _, componentGroup := range *componentGroups { + names = append(names, componentGroup.Name) + } + } + return names +} + +func GetComponentNamesFromComponentGroup(componentGroup *v1beta2.ComponentGroup) []string { + names := []string{} + if componentGroup != nil && componentGroup.Spec.Components != nil { + for _, component := range componentGroup.Spec.Components { + names = append(names, component.Name) + } } return names } diff --git a/helpers/component_group_test.go b/helpers/component_group_test.go index e4d0a04708..43af22ec3d 100644 --- a/helpers/component_group_test.go +++ b/helpers/component_group_test.go @@ -1,6 +1,5 @@ /* -Copyright 2022 Red Hat Inc. - +Copyright 2026 Red Hat Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -14,11 +13,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -package helpers +package helpers_test import ( "fmt" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/konflux-ci/integration-service/api/v1beta2" + "github.com/konflux-ci/integration-service/helpers" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -28,18 +30,81 @@ const ( version = "v1" ) -var _ = Describe("Utility functions", Ordered, func() { +var _ = Describe("Helpers for component groups", func() { + Context("Validating component version strings", func() { It("Can generate a valid component version string", func() { expectedResult := fmt.Sprintf("%s/%s", name, version) - result := GetComponentVersionString(name, version) + result := helpers.GetComponentVersionString(name, version) Expect(result).To(Equal(expectedResult)) }) It("Can generate a valid component version log string", func() { expectedResult := fmt.Sprintf("%s (version %s)", name, version) - result := GetComponentVersionLogString(name, version) + result := helpers.GetComponentVersionLogString(name, version) Expect(result).To(Equal(expectedResult)) }) }) + + Context("GetComponentGroupNames", func() { + + It("returns an empty slice when componentGroups is nil", func() { + Expect(helpers.GetComponentGroupNames(nil)).To(BeEmpty()) + }) + + It("returns an empty slice when there are no component groups", func() { + groups := []v1beta2.ComponentGroup{} + Expect(helpers.GetComponentGroupNames(&groups)).To(BeEmpty()) + }) + + It("returns the name of each component group in order", func() { + groups := []v1beta2.ComponentGroup{ + { + ObjectMeta: metav1.ObjectMeta{Name: "frontend"}, + Spec: v1beta2.ComponentGroupSpec{}, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "backend"}, + Spec: v1beta2.ComponentGroupSpec{}, + }, + } + Expect(helpers.GetComponentGroupNames(&groups)).To(Equal([]string{"frontend", "backend"})) + }) + }) + + Context("GetComponentNamesFromComponentGroup", func() { + + It("returns an empty slice when the component group is nil", func() { + Expect(helpers.GetComponentNamesFromComponentGroup(nil)).To(BeEmpty()) + }) + + It("returns an empty slice when Spec.Components is nil", func() { + group := &v1beta2.ComponentGroup{ + ObjectMeta: metav1.ObjectMeta{Name: "my-group"}, + Spec: v1beta2.ComponentGroupSpec{}, + } + Expect(helpers.GetComponentNamesFromComponentGroup(group)).To(BeEmpty()) + }) + + It("returns an empty slice when Spec.Components is empty", func() { + group := &v1beta2.ComponentGroup{ + ObjectMeta: metav1.ObjectMeta{Name: "my-group"}, + Spec: v1beta2.ComponentGroupSpec{Components: []v1beta2.ComponentReference{}}, + } + Expect(helpers.GetComponentNamesFromComponentGroup(group)).To(BeEmpty()) + }) + + It("returns each component Name from Spec.Components in order", func() { + group := &v1beta2.ComponentGroup{ + ObjectMeta: metav1.ObjectMeta{Name: "my-group"}, + Spec: v1beta2.ComponentGroupSpec{ + Components: []v1beta2.ComponentReference{ + {Name: "api-service", ComponentVersion: v1beta2.ComponentVersionReference{Name: "main"}}, + {Name: "worker", ComponentVersion: v1beta2.ComponentVersionReference{Name: "release-1"}}, + }, + }, + } + Expect(helpers.GetComponentNamesFromComponentGroup(group)).To(Equal([]string{"api-service", "worker"})) + }) + }) }) diff --git a/internal/controller/buildpipeline/buildpipeline_adapter.go b/internal/controller/buildpipeline/buildpipeline_adapter.go index 0e9030359b..fd290d6c8e 100644 --- a/internal/controller/buildpipeline/buildpipeline_adapter.go +++ b/internal/controller/buildpipeline/buildpipeline_adapter.go @@ -487,66 +487,109 @@ func (a *Adapter) EnsureIntegrationTestReportedToGitProvider() (controller.Opera return controller.ContinueProcessing() } - a.logger.Info(fmt.Sprintf("try to set integration test status according to the build PLR status %s", integrationTestStatus.String())) - + var numComponentSnapshotScenarios, numGroupSnapshotScenarios int // TODO: remove application section after migration - var allIntegrationTestScenarios *[]v1beta2.IntegrationTestScenario if a.application != nil { - allIntegrationTestScenarios, err = a.loader.GetAllIntegrationTestScenariosForApplication(a.context, a.client, a.application) + numComponentSnapshotScenarios, numGroupSnapshotScenarios, err = a.reportIntegrationStatusAndHandleGroupsForApplication(&integrationTestStatus) 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) + a.logger.Error(err, "failed to report status and handle groups") return controller.RequeueWithError(err) } } else { - allIntegrationTestScenarios, err = a.loader.GetAllIntegrationTestScenariosForComponentGroups(a.context, a.client, a.componentGroups) + numComponentSnapshotScenarios, numGroupSnapshotScenarios, err = a.reportIntegrationStatusAndHandleGroups(&integrationTestStatus) if err != nil { - // PipelineRun, ComponentGroups, and IntegrationTestScenarios must be in same namespace - ns := a.pipelineRun.Namespace - componentGroupNames := h.GetComponentGroupNames(a.componentGroups) - a.logger.Error(err, "Failed to get integration test scenarios for the following componentGroups", - "ComponentGroup.Namespace", ns, "ComponentGroups", componentGroupNames) + a.logger.Error(err, "failed to report status and handle groups") return controller.RequeueWithError(err) } } - if allIntegrationTestScenarios == nil { - return controller.ContinueProcessing() + if numComponentSnapshotScenarios > 0 || numGroupSnapshotScenarios > 0 { + 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() - var numComponentSnapshotScenarios, numGroupSnapshotScenarios int - tempComponentSnapshot := a.prepareTempComponentSnapshot(a.pipelineRun) - numComponentSnapshotScenarios, err = a.reportStatusForExpectedSnapshot(a.pipelineRun, tempComponentSnapshot, allIntegrationTestScenarios, integrationTestStatus, a.component.Name) +} + +func (a *Adapter) reportIntegrationStatusAndHandleGroupsForApplication(integrationTestStatus *intgteststat.IntegrationTestStatus) (int, int, error) { + numComponentSnapshotScenarios := 0 + numGroupSnapshotScenarios := 0 + var tempComponentSnapshot *applicationapiv1alpha1.Snapshot + + allIntegrationTestScenarios, err := a.loader.GetAllIntegrationTestScenariosForApplication(a.context, a.client, a.application) if err != nil { - a.logger.Error(err, "failed to report status for expected group Snapshot") - return controller.RequeueWithError(err) + 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 0, 0, err + } + + if allIntegrationTestScenarios == nil { + return 0, 0, nil + } + a.logger.Info(fmt.Sprintf("try to set integration test status according to the build PLR status %s", integrationTestStatus.String())) + tempComponentSnapshot = a.prepareTempComponentSnapshot(a.pipelineRun, &a.application.ObjectMeta, true) + numComponentSnapshotScenarios, err = a.reportStatusForExpectedSnapshot(a.pipelineRun, tempComponentSnapshot, allIntegrationTestScenarios, *integrationTestStatus, a.component.Name) + if err != nil { + return 0, 0, fmt.Errorf("failed to report status for expected group Snapshot: %w", err) } a.logger.Info("try to check if group snapshot is expected for build PLR") - isGroupSnapshotExpected, err := a.isGroupSnapshotExpectedForBuildPLR(a.pipelineRun, tempComponentSnapshot) + isGroupSnapshotExpected, err := a.isGroupSnapshotExpectedForBuildPLR(a.pipelineRun, tempComponentSnapshot, a.application.Name, gitops.ApplicationNameLabel) if err != nil { a.logger.Error(err, "failed to check if group snapshot is expected") - return controller.RequeueWithError(err) + return 0, 0, fmt.Errorf("failed to check if group snapshot is expected: %w", err) } - if isGroupSnapshotExpected { a.logger.Info("group snapshot is expected to be created for build pipelinerun, group integration test should be set for found context scenario", "pipelineRun.Name", a.pipelineRun.Name) - tempGroupSnapshot := a.prepareTempGroupSnapshot(a.pipelineRun) - numGroupSnapshotScenarios, err = a.reportStatusForExpectedSnapshot(a.pipelineRun, tempGroupSnapshot, allIntegrationTestScenarios, integrationTestStatus, gitops.ComponentNameForGroupSnapshot) + tempGroupSnapshot := a.prepareTempGroupSnapshot(a.pipelineRun, &a.application.ObjectMeta, true) + numGroupSnapshotScenarios, err = a.reportStatusForExpectedSnapshot(a.pipelineRun, tempGroupSnapshot, allIntegrationTestScenarios, *integrationTestStatus, gitops.ComponentNameForGroupSnapshot) if err != nil { - a.logger.Error(err, "failed to report status for expected group Snapshot") - return controller.RequeueWithError(err) + return 0, 0, fmt.Errorf("failed to report status for expected group Snapshot: %w", err) } } + return numComponentSnapshotScenarios, numGroupSnapshotScenarios, nil +} - if numComponentSnapshotScenarios > 0 || numGroupSnapshotScenarios > 0 { - 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)) +func (a *Adapter) reportIntegrationStatusAndHandleGroups(integrationTestStatus *intgteststat.IntegrationTestStatus) (int, int, error) { + numComponentSnapshotScenarios := 0 + numGroupSnapshotScenarios := 0 + + for _, componentGroup := range *a.componentGroups { + integrationTestScenariosForGroup, err := a.loader.GetAllIntegrationTestScenariosForComponentGroup(a.context, a.client, &componentGroup) + if err != nil { + return 0, 0, fmt.Errorf("failed to get integration test scenarios for the componentGroup %s: %w", componentGroup.Name, err) } - } - return controller.ContinueProcessing() + if integrationTestScenariosForGroup == nil || len(*integrationTestScenariosForGroup) == 0 { + continue + } + a.logger.Info(fmt.Sprintf("try to set integration test status according to the build PLR status %s", integrationTestStatus.String())) + tempComponentSnapshot := a.prepareTempComponentSnapshot(a.pipelineRun, &componentGroup.ObjectMeta, false) + num, err := a.reportStatusForExpectedSnapshot(a.pipelineRun, tempComponentSnapshot, integrationTestScenariosForGroup, *integrationTestStatus, a.component.Name) + if err != nil { + return 0, 0, fmt.Errorf("failed to report status for expected group Snapshot: %w", err) + } + numComponentSnapshotScenarios += num + a.logger.Info("try to check if group snapshot is expected for build PLR") + isGroupSnapshotExpected, err := a.isGroupSnapshotExpectedForBuildPLR(a.pipelineRun, tempComponentSnapshot, componentGroup.Name, gitops.ComponentGroupNameLabel) + if err != nil { + return 0, 0, fmt.Errorf("failed to check if group snapshot is expected: %w", err) + } + if isGroupSnapshotExpected { + a.logger.Info("group snapshot is expected to be created for build pipelinerun, group integration test should be set for found context scenario", "pipelineRun.Name", a.pipelineRun.Name) + tempGroupSnapshot := a.prepareTempGroupSnapshot(a.pipelineRun, &componentGroup.ObjectMeta, false) + groupName := fmt.Sprintf("%s %s", gitops.ComponentNameForGroupSnapshot, componentGroup.Name) + numGroup, err := a.reportStatusForExpectedSnapshot(a.pipelineRun, tempGroupSnapshot, integrationTestScenariosForGroup, *integrationTestStatus, groupName) + if err != nil { + a.logger.Error(err, "failed to report status for expected group Snapshot") + return 0, 0, fmt.Errorf("failed to report status for expected group Snapshot: %w", err) + } + numGroupSnapshotScenarios += numGroup + } + } + return numComponentSnapshotScenarios, numGroupSnapshotScenarios, nil } // EnsureComponentSnapshotAnnotatedForMergedPR annotates all component snapshots when push build PLR is triggered for the same PR since the PR/MR are merged @@ -669,38 +712,73 @@ func (a *Adapter) EnsureSupercededSnapshotsCanceled() (result controller.Operati func (a *Adapter) notifySnapshotsInGroupAboutBuild(pipelineRun *tektonv1.PipelineRun, message string) error { prGroupHash := pipelineRun.Labels[gitops.PRGroupHashLabel] - buildPipelineRuns, err := a.loader.GetPipelineRunsWithPRGroupHash(a.context, a.client, a.pipelineRun.Namespace, prGroupHash, a.application.Name) - if err != nil { - a.logger.Error(err, fmt.Sprintf("Failed to get build pipelineRuns for given pr group hash %s", prGroupHash)) - return err - } - - // Don't do anything if the build pipelineRun isn't the latest for its component - if !tekton.IsLatestBuildPipelineRunInComponent(pipelineRun, buildPipelineRuns) { - a.logger.Info("not the latest pipelineRun, skipping notifying the group about the failure") - return nil - } + var allComponentSnapshotsInGroup *[]applicationapiv1alpha1.Snapshot + var buildPipelineRuns *[]tektonv1.PipelineRun + if a.application != nil { + var err error + buildPipelineRuns, err = a.loader.GetPipelineRunsWithPRGroupHashForApplication(a.context, a.client, a.pipelineRun.Namespace, prGroupHash, a.application.Name) + if err != nil { + return fmt.Errorf("failed to get build pipelineRuns for given pr group hash %s: %w", prGroupHash, err) + } - applicationComponents, err := a.loader.GetAllApplicationComponents(a.context, a.client, a.application) - if err != nil { - return err - } + // Don't do anything if the build pipelineRun isn't the latest for its component + if !tekton.IsLatestBuildPipelineRunInComponent(pipelineRun, buildPipelineRuns) { + a.logger.Info("not the latest pipelineRun, skipping notifying the group about the failure") + return nil + } - // Annotate all latest component Snapshots that are part of the PR group - for _, applicationComponent := range *applicationComponents { - applicationComponent := applicationComponent // G601 - allComponentSnapshotsInGroup, err := a.loader.GetMatchingComponentSnapshotsForComponentAndPRGroupHash(a.context, a.client, a.pipelineRun.Namespace, applicationComponent.Name, prGroupHash, a.application.Name) + applicationComponents, err := a.loader.GetAllApplicationComponents(a.context, a.client, a.application) if err != nil { - a.logger.Error(err, "Failed to fetch Snapshots for component", "component.Name", applicationComponent.Name) - return err + return fmt.Errorf("failed to get all application components for application %s: %w", a.application.Name, err) } - if len(*allComponentSnapshotsInGroup) > 0 { - latestSnapshot := gitops.SortSnapshots(*allComponentSnapshotsInGroup)[0] - err = gitops.AnnotateSnapshot(a.context, &latestSnapshot, gitops.PRGroupCreationAnnotation, - message, a.client) + // Annotate all latest component Snapshots that are part of the PR group + for _, applicationComponent := range *applicationComponents { + applicationComponent := applicationComponent // G601 + allComponentSnapshotsInGroup, err = a.loader.GetMatchingComponentSnapshotsForComponentAndPRGroupHash(a.context, a.client, + a.pipelineRun.Namespace, applicationComponent.Name, prGroupHash, a.application.Name, gitops.ApplicationNameLabel) if err != nil { - return err + return fmt.Errorf("failed to fetch Snapshots for component %s: %w", applicationComponent.Name, err) + } + if allComponentSnapshotsInGroup != nil && len(*allComponentSnapshotsInGroup) > 0 { + latestSnapshot := gitops.SortSnapshots(*allComponentSnapshotsInGroup)[0] + err = gitops.AnnotateSnapshot(a.context, &latestSnapshot, gitops.PRGroupCreationAnnotation, + message, a.client) + if err != nil { + return fmt.Errorf("failed to annotate latest snapshot %s of component %s: %w", latestSnapshot.Name, applicationComponent.Name, err) + } + } + } + } else { + allBuildPipelineRunsWithPRGroupHash, err := a.loader.GetPipelineRunsWithPRGroupHash(a.context, a.client, a.pipelineRun.Namespace, prGroupHash) + if err != nil { + return fmt.Errorf("failed to get build pipelineRuns for given pr group hash %s: %w", prGroupHash, err) + } + buildPipelineRuns = a.filterPipelineRunsForComponentGroups(allBuildPipelineRunsWithPRGroupHash, a.componentGroups) + + // Don't do anything if the build pipelineRun isn't the latest for its component + if !tekton.IsLatestBuildPipelineRunInComponent(pipelineRun, buildPipelineRuns) { + a.logger.Info("not the latest pipelineRun, skipping notifying the group about the failure") + return nil + } + for _, componentGroup := range *a.componentGroups { + componentGroup := componentGroup + for _, groupComponent := range componentGroup.Spec.Components { + groupComponent := groupComponent + allComponentSnapshotsInGroup, err = a.loader.GetMatchingComponentSnapshotsForComponentAndPRGroupHash(a.context, a.client, + a.pipelineRun.Namespace, groupComponent.Name, prGroupHash, componentGroup.Name, gitops.ComponentGroupNameLabel) + if err != nil { + return fmt.Errorf("failed to fetch Snapshots for component %s and pr group hash %s: %w", groupComponent.Name, prGroupHash, err) + } + if allComponentSnapshotsInGroup != nil && len(*allComponentSnapshotsInGroup) > 0 { + latestSnapshot := gitops.SortSnapshots(*allComponentSnapshotsInGroup)[0] + err = gitops.AnnotateSnapshot(a.context, &latestSnapshot, gitops.PRGroupCreationAnnotation, + message, a.client) + if err != nil { + return fmt.Errorf("failed to annotate latest snapshot %s of component %s with PR group creation annotation: %w", + latestSnapshot.Name, groupComponent.Name, err) + } + } } } } @@ -713,7 +791,7 @@ func (a *Adapter) notifySnapshotsInGroupAboutBuild(pipelineRun *tektonv1.Pipelin if !h.HasPipelineRunFinished(&buildPipelineRun) && buildPipelineRun.Labels[tektonconsts.ComponentNameLabel] != a.component.Name { err := tekton.AnnotateBuildPipelineRun(a.context, &buildPipelineRun, gitops.PRGroupCreationAnnotation, message, a.client) if err != nil { - return err + return fmt.Errorf("failed to annotate build pipelineRun %s with PR group creation annotation: %w", buildPipelineRun.Name, err) } } } @@ -723,6 +801,22 @@ func (a *Adapter) notifySnapshotsInGroupAboutBuild(pipelineRun *tektonv1.Pipelin return nil } +func (a *Adapter) filterPipelineRunsForComponentGroups(allBuildPipelineRuns *[]tektonv1.PipelineRun, componentGroups *[]v1beta2.ComponentGroup) *[]tektonv1.PipelineRun { + var filteredBuildPipelineRuns []tektonv1.PipelineRun + for _, buildPipelineRun := range *allBuildPipelineRuns { + if builtComponentName, found := buildPipelineRun.Labels[tektonconsts.PipelineRunComponentLabel]; found { + for _, componentGroup := range *componentGroups { + componentNames := h.GetComponentNamesFromComponentGroup(&componentGroup) + if slices.Contains(componentNames, builtComponentName) { + filteredBuildPipelineRuns = append(filteredBuildPipelineRuns, buildPipelineRun) + break + } + } + } + } + return &filteredBuildPipelineRuns +} + // prepareSnapshotForPipelineRun prepares the Snapshot for a given PipelineRun, // component and application. In case the Snapshot can't be created, an error will be returned. func (a *Adapter) prepareSnapshotForPipelineRun(pipelineRun *tektonv1.PipelineRun, component *applicationapiv1alpha1.Component, application *applicationapiv1alpha1.Application) (*applicationapiv1alpha1.Snapshot, error) { @@ -1042,7 +1136,7 @@ func (a *Adapter) addPRGroupToBuildPLRMetadata(pipelineRun *tektonv1.PipelineRun // prepareTempComponentSnapshot will create a temporary component snapshot object to copy the labels/annotations from build pipelinerun // and be used to communicate with git provider -func (a *Adapter) prepareTempComponentSnapshot(pipelineRun *tektonv1.PipelineRun) *applicationapiv1alpha1.Snapshot { +func (a *Adapter) prepareTempComponentSnapshot(pipelineRun *tektonv1.PipelineRun, object *metav1.ObjectMeta, isApplication bool) *applicationapiv1alpha1.Snapshot { tempComponentSnapshot := &applicationapiv1alpha1.Snapshot{ ObjectMeta: metav1.ObjectMeta{ Name: "tempComponentSnapshot", @@ -1050,13 +1144,13 @@ func (a *Adapter) prepareTempComponentSnapshot(pipelineRun *tektonv1.PipelineRun }, } prefixes := []string{gitops.BuildPipelineRunPrefix, gitops.TestLabelPrefix, gitops.CustomLabelPrefix, tektonconsts.ResourceLabelSuffix} - gitops.CopySnapshotLabelsAndAnnotations(&a.application.ObjectMeta, tempComponentSnapshot, a.component.Name, &pipelineRun.ObjectMeta, prefixes, true) + gitops.CopySnapshotLabelsAndAnnotations(object, tempComponentSnapshot, a.component.Name, &pipelineRun.ObjectMeta, prefixes, isApplication) return tempComponentSnapshot } // prepareTempGroupSnapshot will create a temporary group snapshot object to copy the labels/annotations from build pipelinerun // and be used to communicate with git provider -func (a *Adapter) prepareTempGroupSnapshot(pipelineRun *tektonv1.PipelineRun) *applicationapiv1alpha1.Snapshot { +func (a *Adapter) prepareTempGroupSnapshot(pipelineRun *tektonv1.PipelineRun, object *metav1.ObjectMeta, isApplication bool) *applicationapiv1alpha1.Snapshot { tempGroupSnapshot := &applicationapiv1alpha1.Snapshot{ ObjectMeta: metav1.ObjectMeta{ Name: "tempGroupSnapshot", @@ -1064,7 +1158,7 @@ func (a *Adapter) prepareTempGroupSnapshot(pipelineRun *tektonv1.PipelineRun) *a }, } prefixes := []string{gitops.BuildPipelineRunPrefix} - gitops.CopyTempGroupSnapshotLabelsAndAnnotations(a.application, tempGroupSnapshot, a.component.Name, &pipelineRun.ObjectMeta, prefixes) + gitops.CopyTempGroupSnapshotLabelsAndAnnotations(object, tempGroupSnapshot, a.component.Name, &pipelineRun.ObjectMeta, prefixes, isApplication) return tempGroupSnapshot } @@ -1190,10 +1284,20 @@ func generateIntgTestStatusDetails(buildPLR *tektonv1.PipelineRun, integrationTe // getComponentFromLatestFlightBuildPLR get the components from the build pipelineruns which have not snapshot created for the given pr group // according to the given pr group func (a *Adapter) getComponentsFromLatestFlightBuildPLR(prGroup, prGroupHash string) ([]string, error) { - pipelineRuns, err := a.loader.GetPipelineRunsWithPRGroupHash(a.context, a.client, a.pipelineRun.Namespace, prGroupHash, a.application.Name) - if err != nil { - a.logger.Error(err, fmt.Sprintf("Failed to get build pipelineRuns for given pr group hash %s", prGroupHash)) - return nil, err + var pipelineRuns *[]tektonv1.PipelineRun + var err error + if a.application != nil { + pipelineRuns, err = a.loader.GetPipelineRunsWithPRGroupHashForApplication(a.context, a.client, a.pipelineRun.Namespace, prGroupHash, a.application.Name) + if err != nil { + a.logger.Error(err, fmt.Sprintf("Failed to get build pipelineRuns for given pr group hash %s and application %s", prGroupHash, a.application.Name)) + return nil, err + } + } else { + pipelineRuns, err = a.loader.GetPipelineRunsWithPRGroupHash(a.context, a.client, a.pipelineRun.Namespace, prGroupHash) + if err != nil { + a.logger.Error(err, fmt.Sprintf("Failed to get build pipelineRuns for given pr group hash %s", prGroupHash)) + return nil, err + } } var componentsFromPipelineRun []string @@ -1216,7 +1320,7 @@ func (a *Adapter) getComponentsFromLatestFlightBuildPLR(prGroup, prGroupHash str } // isGroupSnapshotExpectedForBuildPLR return group context ITS if group snapshot is expected for the same pr group with the given build PLR -func (a *Adapter) isGroupSnapshotExpectedForBuildPLR(pipelineRun *tektonv1.PipelineRun, tempComponentSnapshot *applicationapiv1alpha1.Snapshot) (bool, error) { +func (a *Adapter) isGroupSnapshotExpectedForBuildPLR(pipelineRun *tektonv1.PipelineRun, tempComponentSnapshot *applicationapiv1alpha1.Snapshot, ownerName, ownerLabel string) (bool, error) { prGroupHash, prGroup := gitops.GetPRGroup(pipelineRun) if prGroupHash == "" || prGroup == "" { a.logger.Error(fmt.Errorf("NotFound"), fmt.Sprintf("Failed to get PR group label/annotation from pipelineRun %s/%s", pipelineRun.Namespace, pipelineRun.Name)) @@ -1229,11 +1333,11 @@ func (a *Adapter) isGroupSnapshotExpectedForBuildPLR(pipelineRun *tektonv1.Pipel return false, err } if len(componentsWithOpenPRMR) > 1 { - a.logger.Info(fmt.Sprintf("there is more than 1 component with open pr or mr found, so group snapshot is expected: %s", componentsWithOpenPRMR)) + a.logger.Info(fmt.Sprintf("there is more than 1 component with open pr or mr found, so group snapshot is expected for %s: %s", ownerName, componentsWithOpenPRMR)) return true, nil } - componentsFromSnapshot, err := a.loader.GetComponentsFromSnapshotForPRGroup(a.context, a.client, pipelineRun.Namespace, prGroup, prGroupHash, a.application.Name) + componentsFromSnapshot, err := a.loader.GetComponentsFromSnapshotForPRGroup(a.context, a.client, pipelineRun.Namespace, prGroupHash, ownerName, ownerLabel) if err != nil { a.logger.Error(err, fmt.Sprintf("failed to get component from component snapshot for pr group %s", prGroup)) return false, err @@ -1245,7 +1349,7 @@ func (a *Adapter) isGroupSnapshotExpectedForBuildPLR(pipelineRun *tektonv1.Pipel continue } - snapshots, err := a.loader.GetMatchingComponentSnapshotsForComponentAndPRGroupHash(a.context, a.client, pipelineRun.Namespace, componentName, prGroupHash, a.application.Name) + snapshots, err := a.loader.GetMatchingComponentSnapshotsForComponentAndPRGroupHash(a.context, a.client, pipelineRun.Namespace, componentName, prGroupHash, ownerName, ownerLabel) if err != nil { a.logger.Error(err, "Failed to fetch Snapshots for component", "component.Name", componentName) return false, err @@ -1263,10 +1367,10 @@ func (a *Adapter) isGroupSnapshotExpectedForBuildPLR(pipelineRun *tektonv1.Pipel } if len(componentsWithOpenPRMR) < 2 { - a.logger.Info(fmt.Sprintf("The number %d of components affected by this PR group %s is less than 2, skipping group snapshot status report", len(componentsWithOpenPRMR), prGroup)) + a.logger.Info(fmt.Sprintf("The number %d of components affected by this PR group %s is less than 2, skipping group snapshot status report for %s", len(componentsWithOpenPRMR), prGroup, ownerName)) return false, nil } - a.logger.Info(fmt.Sprintf("there is more than 1 component with open pr or mr found, so group snapshot is expected: %s", componentsWithOpenPRMR)) + a.logger.Info(fmt.Sprintf("there is more than 1 component with open pr or mr found, so group snapshot is expected for %s: %s", ownerName, componentsWithOpenPRMR)) return true, nil } @@ -1349,7 +1453,7 @@ func isBuildPLROlderThanLastBuild(pipelineRun *tektonv1.PipelineRun, component * func (a *Adapter) IsLatestBuildPipelineRunInComponentWithPRGroupHash(buildPlr *tektonv1.PipelineRun) (bool, error) { prGroupHash := buildPlr.Labels[gitops.PRGroupHashLabel] prGroupName := buildPlr.Annotations[gitops.PRGroupAnnotation] - pipelineRuns, err := a.loader.GetPipelineRunsWithPRGroupHash(a.context, a.client, buildPlr.Namespace, prGroupHash, a.application.Name) + pipelineRuns, err := a.loader.GetPipelineRunsWithPRGroupHash(a.context, a.client, buildPlr.Namespace, prGroupHash) if err != nil { a.logger.Error(err, fmt.Sprintf("Failed to get build pipelineRuns for given pr group hash %s", prGroupHash)) return false, err diff --git a/internal/controller/buildpipeline/buildpipeline_adapter_test.go b/internal/controller/buildpipeline/buildpipeline_adapter_test.go index 9a913f2722..96b14dfa5b 100644 --- a/internal/controller/buildpipeline/buildpipeline_adapter_test.go +++ b/internal/controller/buildpipeline/buildpipeline_adapter_test.go @@ -539,6 +539,87 @@ var _ = Describe("Pipeline Adapter", Ordered, func() { }) }) + When("filterPipelineRunsForComponentGroups is called", func() { + + BeforeEach(func() { + adapter = NewAdapter(ctx, buildPipelineRun, hasComp, &[]v1beta2.ComponentGroup{*hasCompGroup}, logger, loader.NewMockLoader(), k8sClient) + }) + + It("returns an empty filtered list when there are no build PipelineRuns", func() { + empty := []tektonv1.PipelineRun{} + groups := &[]v1beta2.ComponentGroup{*hasCompGroup} + + result := adapter.filterPipelineRunsForComponentGroups(&empty, groups) + Expect(result).NotTo(BeNil()) + Expect(*result).To(BeEmpty()) + }) + + It("keeps PipelineRuns whose component label matches a component listed in some ComponentGroup, and skips others", func() { + plrMatching := tektonv1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "build-plr-matching", + Labels: map[string]string{ + tektonconsts.PipelineRunComponentLabel: "component-sample", + }, + }, + } + plrUnknownComponent := tektonv1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "build-plr-unknown", + Labels: map[string]string{ + tektonconsts.PipelineRunComponentLabel: "not-in-any-group", + }, + }, + } + plrNoComponentLabel := tektonv1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{Name: "build-plr-no-label"}, + } + + stack := []tektonv1.PipelineRun{plrMatching, plrUnknownComponent, plrNoComponentLabel} + groups := &[]v1beta2.ComponentGroup{*hasCompGroup} + + result := adapter.filterPipelineRunsForComponentGroups(&stack, groups) + Expect(result).NotTo(BeNil()) + Expect(*result).To(HaveLen(1)) + Expect((*result)[0].Name).To(Equal("build-plr-matching")) + Expect((*result)[0].Labels[tektonconsts.PipelineRunComponentLabel]).To(Equal("component-sample")) + }) + + It("includes a PipelineRun when the component matches any ComponentGroup in order", func() { + groupAlpha := v1beta2.ComponentGroup{ + ObjectMeta: metav1.ObjectMeta{Name: "group-alpha"}, + Spec: v1beta2.ComponentGroupSpec{ + Components: []v1beta2.ComponentReference{ + {Name: "only-alpha", ComponentVersion: v1beta2.ComponentVersionReference{Name: "v1"}}, + }, + }, + } + groupBeta := v1beta2.ComponentGroup{ + ObjectMeta: metav1.ObjectMeta{Name: "group-beta"}, + Spec: v1beta2.ComponentGroupSpec{ + Components: []v1beta2.ComponentReference{ + {Name: "only-beta", ComponentVersion: v1beta2.ComponentVersionReference{Name: "v1"}}, + }, + }, + } + plr := tektonv1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "build-plr-only-beta", + Labels: map[string]string{ + tektonconsts.PipelineRunComponentLabel: "only-beta", + }, + }, + } + stack := []tektonv1.PipelineRun{plr} + groups := &[]v1beta2.ComponentGroup{groupAlpha, groupBeta} + + result := adapter.filterPipelineRunsForComponentGroups(&stack, groups) + Expect(result).NotTo(BeNil()) + Expect(*result).To(HaveLen(1)) + Expect((*result)[0].Name).To(Equal("build-plr-only-beta")) + }) + }) + When("NewAdapter is created", func() { BeforeEach(func() { adapter = createAdapter() @@ -678,7 +759,7 @@ var _ = Describe("Pipeline Adapter", Ordered, func() { Eventually(func() bool { result, err := adapter.EnsureSnapshotExists() return !result.CancelRequest && err != nil - }, time.Second*10).Should(BeTrue()) + }, time.Second*15).Should(BeTrue()) expectedLogEntry := "Exceeded timeout waiting for Chains signing" Expect(buf.String()).Should(ContainSubstring(expectedLogEntry)) @@ -1814,8 +1895,8 @@ var _ = Describe("Pipeline Adapter", Ordered, func() { }) }) - // NOTE: PR group support will be addressed in STONEINTG-1519 - When("add pr group to the build pipelineRun annotations and labels", func() { + // TODO: Remove after the application model is deprecated + When("add pr group to the build pipelineRun annotations and labels [APPLICATION]", func() { BeforeEach(func() { adapter = NewAdapterWithApplication(ctx, buildPipelineRun, hasComp, hasApp, logger, loader.NewMockLoader(), k8sClient) }) @@ -1851,6 +1932,43 @@ var _ = Describe("Pipeline Adapter", Ordered, func() { }) }) + When("add pr group to the build pipelineRun annotations and labels", func() { + BeforeEach(func() { + componentGroups := []v1beta2.ComponentGroup{*hasCompGroup} + adapter = NewAdapter(ctx, buildPipelineRun, hasComp, &componentGroups, logger, loader.NewMockLoader(), k8sClient) + }) + It("add pr group to the build pipelineRun annotations and labels", func() { + existingBuildPLR := new(tektonv1.PipelineRun) + Eventually(func() bool { + err := k8sClient.Get(ctx, types.NamespacedName{ + Namespace: buildPipelineRun.Namespace, + Name: buildPipelineRun.Name, + }, existingBuildPLR) + return err == nil + }, time.Second*10).Should(BeTrue()) + + Expect(metadata.HasAnnotation(existingBuildPLR, gitops.PRGroupAnnotation)).To(BeFalse()) + Expect(metadata.HasLabel(existingBuildPLR, gitops.PRGroupHashLabel)).To(BeFalse()) + + // Add label and annotation to PLR + result, err := adapter.EnsurePRGroupAnnotated() + Expect(err).NotTo(HaveOccurred()) + Expect(result.CancelRequest).To(BeFalse()) + Expect(result.RequeueRequest).To(BeFalse()) + + Eventually(func() bool { + _ = adapter.client.Get(adapter.context, types.NamespacedName{ + Namespace: buildPipelineRun.Namespace, + Name: buildPipelineRun.Name, + }, existingBuildPLR) + return metadata.HasAnnotation(existingBuildPLR, gitops.PRGroupAnnotation) && metadata.HasLabel(existingBuildPLR, gitops.PRGroupHashLabel) + }, time.Second*10).Should(BeTrue()) + + Expect(existingBuildPLR.Annotations).Should(HaveKeyWithValue(Equal(gitops.PRGroupAnnotation), Equal("sourceBranch"))) + Expect(existingBuildPLR.Labels[gitops.PRGroupHashLabel]).NotTo(BeNil()) + }) + }) + When("running pipeline with deletion timestamp is processed", func() { var runningDeletingBuildPipeline *tektonv1.PipelineRun @@ -2209,8 +2327,8 @@ var _ = Describe("Pipeline Adapter", Ordered, func() { }, time.Second*20).Should(BeTrue()) }) - // NOTE: PR group support will be addressed in STONEINTG-1519 - When("add pr group to the build pipelineRun annotations and labels", func() { + // TODO: Remove after application model is deprecated + When("add pr group to the build pipelineRun annotations and labels [APPLICATION]", func() { BeforeEach(func() { // Mock an in-flight component build PLR that belongs to the same PR group otherComp := hasComp.DeepCopy() @@ -2286,6 +2404,92 @@ var _ = Describe("Pipeline Adapter", Ordered, func() { return err == nil && metadata.HasAnnotation(hasSnapshot, gitops.PRGroupCreationAnnotation) }, time.Second*10).Should(BeTrue()) + Eventually(func() bool { + err = adapter.client.Get(adapter.context, types.NamespacedName{ + Namespace: buildPipelineRun.Namespace, + Name: buildPipelineRun.Name, + }, buildPipelineRun) + return err == nil && metadata.HasAnnotation(buildPipelineRun, gitops.PRGroupCreationAnnotation) + }, time.Second*10).Should(BeTrue()) + }) + }) + When("add pr group to the build pipelineRun annotations and labels", func() { + BeforeEach(func() { + // Mock an in-flight component build PLR that belongs to the same PR group + otherComp := hasComp.DeepCopy() + otherComp.Name = "other-component" + + buildPipelineRun2 = buildPipelineRun.DeepCopy() + buildPipelineRun2.Name = "incoming-build-pipeline-run" + buildPipelineRun2.Labels[tektonconsts.ComponentNameLabel] = otherComp.Name + delete(buildPipelineRun2.Annotations, gitops.PRGroupAnnotation) + delete(buildPipelineRun2.Labels, gitops.PRGroupHashLabel) + buildPipelineRun2.ResourceVersion = "" + + Expect(k8sClient.Create(ctx, buildPipelineRun2)).Should(Succeed()) + + buildPipelineRun2.Status = tektonv1.PipelineRunStatus{ + PipelineRunStatusFields: tektonv1.PipelineRunStatusFields{ + Results: []tektonv1.PipelineRunResult{}, + }, + Status: v1.Status{ + Conditions: v1.Conditions{ + apis.Condition{ + Reason: "Running", + Status: "Unknown", + Type: apis.ConditionSucceeded, + }, + }, + }, + } + Expect(k8sClient.Status().Update(ctx, buildPipelineRun2)).Should(Succeed()) + + // Set the timestamp in the future, so it's newer than the original buildPipelineRun + buildPipelineRun2.CreationTimestamp = metav1.NewTime(time.Now().Add(time.Hour * 12)) + + buf = bytes.Buffer{} + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + componentGroups := &[]v1beta2.ComponentGroup{*hasCompGroup} + adapter = NewAdapter(ctx, buildPipelineRun2, otherComp, componentGroups, log, loader.NewMockLoader(), k8sClient) + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ComponentGroupComponentsContextKey, + Resource: []applicationapiv1alpha1.Component{*hasComp}, + }, + { + ContextKey: loader.GetComponentSnapshotsKey, + Resource: []applicationapiv1alpha1.Snapshot{*hasSnapshot}, + }, + { + ContextKey: loader.GetBuildPLRContextKey, + Resource: []tektonv1.PipelineRun{*buildPipelineRun, *buildPipelineRun2}, + }, + }) + }) + AfterEach(func() { + Expect(k8sClient.Delete(ctx, buildPipelineRun2)).Should(Succeed()) + }) + It("notifies the latest Snapshots and in-flight builds in the PR group about the incoming new build pipelineRun", func() { + result, err := adapter.EnsurePRGroupAnnotated() + Expect(err).NotTo(HaveOccurred()) + Expect(result.CancelRequest).To(BeFalse()) + Expect(result.RequeueRequest).To(BeFalse()) + + expectedLogEntry := "pr group info is updated to build pipelineRun metadata" + Expect(buf.String()).Should(ContainSubstring(expectedLogEntry)) + expectedLogEntry = "notified all component snapshots and build pipelines in the pr group about the build pipeline status" + Expect(buf.String()).Should(ContainSubstring(expectedLogEntry)) + expectedLogEntry = "build pipelineRun has had pr group info in metadata, no need to update" + Expect(buf.String()).ShouldNot(ContainSubstring(expectedLogEntry)) + + Eventually(func() bool { + err := adapter.client.Get(adapter.context, types.NamespacedName{ + Namespace: hasSnapshot.Namespace, + Name: hasSnapshot.Name, + }, hasSnapshot) + return err == nil && metadata.HasAnnotation(hasSnapshot, gitops.PRGroupCreationAnnotation) + }, time.Second*10).Should(BeTrue()) + Eventually(func() bool { err = adapter.client.Get(adapter.context, types.NamespacedName{ Namespace: buildPipelineRun.Namespace, @@ -2345,8 +2549,8 @@ var _ = Describe("Pipeline Adapter", Ordered, func() { }, time.Second*20).Should(BeTrue()) }) - // NOTE: PR group support will be addressed in STONEINTG-1519 - When("add pr group to the build pipelineRun annotations and labels", func() { + // TODO: Remove after application model is deprecated + When("add pr group to the build pipelineRun annotations and labels [APPLICATION]", func() { BeforeEach(func() { // Add label and annotation to PLR err := metadata.AddLabels(buildPipelineRun, map[string]string{gitops.PRGroupHashLabel: prGroupSha}) @@ -2431,9 +2635,95 @@ var _ = Describe("Pipeline Adapter", Ordered, func() { }, time.Second*10).Should(BeTrue()) }) }) - - // NOTE: PR group support will be addressed in STONEINTG-1519 When("add pr group to the build pipelineRun annotations and labels", func() { + BeforeEach(func() { + // Add label and annotation to PLR + err := metadata.AddLabels(buildPipelineRun, map[string]string{gitops.PRGroupHashLabel: prGroupSha}) + Expect(err).NotTo(HaveOccurred()) + err = metadata.AddAnnotations(buildPipelineRun, map[string]string{gitops.PRGroupAnnotation: prGroup}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient.Update(ctx, buildPipelineRun)).Should(Succeed()) + + Eventually(func() bool { + _ = k8sClient.Get(ctx, types.NamespacedName{ + Namespace: buildPipelineRun.Namespace, + Name: buildPipelineRun.Name, + }, buildPipelineRun) + return metadata.HasAnnotation(buildPipelineRun, gitops.PRGroupAnnotation) && metadata.HasLabel(buildPipelineRun, gitops.PRGroupHashLabel) + }, time.Second*10).Should(BeTrue()) + + Expect(helpers.HasPipelineRunFinished(buildPipelineRun)).Should(BeTrue()) + Expect(helpers.HasPipelineRunSucceeded(buildPipelineRun)).Should(BeFalse()) + + // Mock an in-flight component build PLR that belongs to the same PR group and component and is newer + inFlightBuildPLR := buildPipelineRun.DeepCopy() + inFlightBuildPLR.Name = "in-flight-build-plr" + inFlightBuildPLR.CreationTimestamp = metav1.NewTime(time.Now().Add(time.Hour * 12)) + inFlightBuildPLR.Status = tektonv1.PipelineRunStatus{ + PipelineRunStatusFields: tektonv1.PipelineRunStatusFields{ + Results: []tektonv1.PipelineRunResult{}, + }, + Status: v1.Status{ + Conditions: v1.Conditions{ + apis.Condition{ + Reason: "Running", + Status: "Unknown", + Type: apis.ConditionSucceeded, + }, + }, + }, + } + + buf = bytes.Buffer{} + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + componentGroups := &[]v1beta2.ComponentGroup{*hasCompGroup} + adapter = NewAdapter(ctx, buildPipelineRun, hasComp, componentGroups, log, loader.NewMockLoader(), k8sClient) + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ComponentGroupComponentsContextKey, + Resource: []applicationapiv1alpha1.Component{*hasComp}, + }, + { + ContextKey: loader.GetComponentSnapshotsKey, + Resource: []applicationapiv1alpha1.Snapshot{*hasSnapshot}, + }, + { + ContextKey: loader.GetBuildPLRContextKey, + Resource: []tektonv1.PipelineRun{*inFlightBuildPLR, *buildPipelineRun}, + }, + }) + }) + It("doesn't notify latest Snapshots and in-flight builds in the PR group about the build pipeline failure because it's not the latest build", func() { + result, err := adapter.EnsurePRGroupAnnotated() + Expect(err).NotTo(HaveOccurred()) + Expect(result.CancelRequest).To(BeFalse()) + Expect(result.RequeueRequest).To(BeFalse()) + + expectedLogEntry := "not the latest pipelineRun, skipping notifying the group about the failure" + Expect(buf.String()).Should(ContainSubstring(expectedLogEntry)) + expectedLogEntry = "notified all component snapshots and build pipelines in the pr group about the build pipeline status" + Expect(buf.String()).ShouldNot(ContainSubstring(expectedLogEntry)) + + Eventually(func() bool { + err := adapter.client.Get(adapter.context, types.NamespacedName{ + Namespace: hasSnapshot.Namespace, + Name: hasSnapshot.Name, + }, hasSnapshot) + return err == nil && !metadata.HasAnnotation(hasSnapshot, gitops.PRGroupCreationAnnotation) + }, time.Second*10).Should(BeTrue()) + + Eventually(func() bool { + err = adapter.client.Get(adapter.context, types.NamespacedName{ + Namespace: buildPipelineRun.Namespace, + Name: buildPipelineRun.Name, + }, buildPipelineRun) + return err == nil && !metadata.HasAnnotation(buildPipelineRun, gitops.PRGroupCreationAnnotation) + }, time.Second*10).Should(BeTrue()) + }) + }) + + // TODO: Remove after the application model is deprecated + When("add pr group to the build pipelineRun annotations and labels [APPLICATION]", func() { BeforeEach(func() { // Remove the PR group creation Annotation from the group Snapshot delete(hasSnapshot.Annotations, gitops.PRGroupCreationAnnotation) @@ -2518,11 +2808,97 @@ var _ = Describe("Pipeline Adapter", Ordered, func() { Expect(buildPipelineRun.Annotations[gitops.PRGroupCreationAnnotation]).Should(ContainSubstring(expectedBuildFailureMsg)) }) }) + When("add pr group to the build pipelineRun annotations and labels", func() { + BeforeEach(func() { + // Remove the PR group creation Annotation from the group Snapshot + delete(hasSnapshot.Annotations, gitops.PRGroupCreationAnnotation) + Expect(k8sClient.Update(ctx, hasSnapshot)).Should(Succeed()) + // Add label and annotation to PLR + err := metadata.AddLabels(buildPipelineRun, map[string]string{gitops.PRGroupHashLabel: prGroupSha}) + Expect(err).NotTo(HaveOccurred()) + err = metadata.AddAnnotations(buildPipelineRun, map[string]string{gitops.PRGroupAnnotation: prGroup}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient.Update(ctx, buildPipelineRun)).Should(Succeed()) + + Eventually(func() bool { + _ = k8sClient.Get(ctx, types.NamespacedName{ + Namespace: buildPipelineRun.Namespace, + Name: buildPipelineRun.Name, + }, buildPipelineRun) + return metadata.HasAnnotation(buildPipelineRun, gitops.PRGroupAnnotation) && metadata.HasLabel(buildPipelineRun, gitops.PRGroupHashLabel) + }, time.Second*10).Should(BeTrue()) + + Expect(helpers.HasPipelineRunFinished(buildPipelineRun)).Should(BeTrue()) + Expect(helpers.HasPipelineRunSucceeded(buildPipelineRun)).Should(BeFalse()) + + // Mock an in-flight component build PLR that belongs to the same PR group + inFlightBuildPLR := buildPipelineRun.DeepCopy() + inFlightBuildPLR.Labels[tektonconsts.ComponentNameLabel] = "another-component-sample" + inFlightBuildPLR.Status = tektonv1.PipelineRunStatus{ + PipelineRunStatusFields: tektonv1.PipelineRunStatusFields{ + Results: []tektonv1.PipelineRunResult{}, + }, + Status: v1.Status{ + Conditions: v1.Conditions{ + apis.Condition{ + Reason: "Running", + Status: "Unknown", + Type: apis.ConditionSucceeded, + }, + }, + }, + } + + componentGroups := []v1beta2.ComponentGroup{*hasCompGroup} + adapter = NewAdapter(ctx, buildPipelineRun, hasComp, &componentGroups, logger, loader.NewMockLoader(), k8sClient) + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ComponentGroupComponentsContextKey, + Resource: []applicationapiv1alpha1.Component{*hasComp}, + }, + { + ContextKey: loader.GetComponentSnapshotsKey, + Resource: []applicationapiv1alpha1.Snapshot{*hasSnapshot}, + }, + { + ContextKey: loader.GetBuildPLRContextKey, + Resource: []tektonv1.PipelineRun{*inFlightBuildPLR}, + }, + }) + }) + It("notifies all latest Snapshots and in-flight builds in the PR group about the build pipeline failure", func() { + result, err := adapter.EnsurePRGroupAnnotated() + Expect(err).NotTo(HaveOccurred()) + Expect(result.CancelRequest).To(BeFalse()) + Expect(result.RequeueRequest).To(BeFalse()) + + Eventually(func() bool { + err = adapter.client.Get(adapter.context, types.NamespacedName{ + Namespace: hasSnapshot.Namespace, + Name: hasSnapshot.Name, + }, hasSnapshot) + return err == nil && metadata.HasAnnotation(hasSnapshot, gitops.PRGroupCreationAnnotation) + }, time.Second*10).Should(BeTrue()) + + expectedBuildFailureMsg := fmt.Sprintf("build PLR %s failed for component %s so it can't be added to the group Snapshot for PR group %s", buildPipelineRun.Name, hasComp.Name, prGroup) + Expect(hasSnapshot.Annotations[gitops.PRGroupCreationAnnotation]).Should(ContainSubstring(expectedBuildFailureMsg)) + + Eventually(func() bool { + err = adapter.client.Get(adapter.context, types.NamespacedName{ + Namespace: buildPipelineRun.Namespace, + Name: buildPipelineRun.Name, + }, buildPipelineRun) + return err == nil && metadata.HasAnnotation(buildPipelineRun, gitops.PRGroupCreationAnnotation) + }, time.Second*10).Should(BeTrue()) + + Expect(buildPipelineRun.Annotations[gitops.PRGroupCreationAnnotation]).Should(ContainSubstring(expectedBuildFailureMsg)) + }) + }) }) - // NOTE: PR group support will be addressed in STONEINTG-1519 - When("a build PLR is triggered or retirggered, succeeded or failed", func() { + // TODO: Remove after the application model is deprecated + When("a build PLR is triggered or retriggered, succeeded or failed [APPLICATION]", func() { BeforeEach(func() { patch := client.MergeFrom(buildPipelineRun.DeepCopy()) _ = metadata.SetAnnotation(&buildPipelineRun.ObjectMeta, gitops.PRGroupAnnotation, prGroup) @@ -2800,7 +3176,7 @@ var _ = Describe("Pipeline Adapter", Ordered, func() { Expect(buf.String()).Should(ContainSubstring(expectedLogEntry)) expectedLogEntry = "group snapshot is expected to be created for build pipelinerun" Expect(buf.String()).Should(ContainSubstring(expectedLogEntry)) - expectedLogEntry = "there is more than 1 component with open pr or mr found, so group snapshot is expected: [component-sample another-component-sample]" + expectedLogEntry = "there is more than 1 component with open pr or mr found, so group snapshot is expected for application-sample: [component-sample another-component-sample]" Expect(buf.String()).Should(ContainSubstring(expectedLogEntry)) Expect(result.CancelRequest).To(BeFalse()) Expect(result.RequeueRequest).To(BeFalse()) @@ -2808,8 +3184,352 @@ var _ = Describe("Pipeline Adapter", Ordered, func() { }) }) - // NOTE: PR group support will be addressed in STONEINTG-1519 - When("integration status should not be set from build PLR", func() { + When("a build PLR is triggered or retriggered, succeeded or failed", func() { + BeforeEach(func() { + patch := client.MergeFrom(buildPipelineRun.DeepCopy()) + _ = metadata.SetAnnotation(&buildPipelineRun.ObjectMeta, gitops.PRGroupAnnotation, prGroup) + _ = metadata.SetLabel(&buildPipelineRun.ObjectMeta, gitops.PRGroupHashLabel, prGroupSha) + Expect(k8sClient.Patch(ctx, buildPipelineRun, patch)).Should(Succeed()) + 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).AnyTimes() + mockStatus.EXPECT().FindSnapshotWithOpenedPR(gomock.Any(), gomock.Any(), gomock.Any()).Return(hasSnapshot, 0, nil).AnyTimes() + mockReporter.EXPECT().GetReporterName().AnyTimes() + mockReporter.EXPECT().Initialize(gomock.Any(), gomock.Any()).AnyTimes() + mockReporter.EXPECT().ReportStatus(gomock.Any(), gomock.Any()).AnyTimes() + }) + It("ensure integration test is initialized from build PLR", func() { + buildPipelineRun.Status = tektonv1.PipelineRunStatus{ + Status: v1.Status{ + Conditions: v1.Conditions{ + apis.Condition{ + Reason: "Running", + Status: "Unknown", + Type: apis.ConditionSucceeded, + }, + }, + }, + } + Expect(k8sClient.Status().Update(ctx, buildPipelineRun)).Should(Succeed()) + + buf = bytes.Buffer{} + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + componentGroups := []v1beta2.ComponentGroup{*hasCompGroup} + adapter = NewAdapter(ctx, buildPipelineRun, hasComp, &componentGroups, log, loader.NewMockLoader(), k8sClient) + adapter.status = mockStatus + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ComponentGroupsContextKey, + Resource: hasCompGroup, + }, + { + ContextKey: loader.AllIntegrationTestScenariosForComponentGroupsContextKey, + Resource: []v1beta2.IntegrationTestScenario{*integrationTestScenario}, + }, + { + ContextKey: loader.AllIntegrationTestScenariosForComponentGroupContextKey, + Resource: []v1beta2.IntegrationTestScenario{*integrationTestScenario}, + }, + }) + + result, err := adapter.EnsureIntegrationTestReportedToGitProvider() + Expect(!result.CancelRequest && err == nil).To(BeTrue()) + Expect(metadata.HasAnnotationWithValue(buildPipelineRun, helpers.SnapshotCreationReportAnnotation, intgteststat.BuildPLRInProgress.String())).To(BeTrue()) + + result, err = adapter.EnsureIntegrationTestReportedToGitProvider() + Expect(!result.CancelRequest && err == nil).To(BeTrue()) + expectedLogEntry := "integration test has been set correctly or is being processed, no need to set integration test status from build pipelinerun" + Expect(buf.String()).Should(ContainSubstring(expectedLogEntry)) + }) + + It("ensure integration test is set from build PLR when build PLR fails", func() { + buildPipelineRun.Status = tektonv1.PipelineRunStatus{ + Status: v1.Status{ + Conditions: v1.Conditions{ + apis.Condition{ + Reason: "Failed", + Status: "False", + Type: apis.ConditionSucceeded, + }, + }, + }, + } + Expect(k8sClient.Status().Update(ctx, buildPipelineRun)).Should(Succeed()) + + buf = bytes.Buffer{} + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + componentGroups := []v1beta2.ComponentGroup{*hasCompGroup} + adapter = NewAdapter(ctx, buildPipelineRun, hasComp, &componentGroups, log, loader.NewMockLoader(), k8sClient) + adapter.status = mockStatus + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ComponentGroupsContextKey, + Resource: hasCompGroup, + }, + { + ContextKey: loader.AllIntegrationTestScenariosForComponentGroupsContextKey, + Resource: []v1beta2.IntegrationTestScenario{*integrationTestScenario}, + }, + { + ContextKey: loader.AllIntegrationTestScenariosForComponentGroupContextKey, + Resource: []v1beta2.IntegrationTestScenario{*integrationTestScenario}, + }, + }) + + result, err := adapter.EnsureIntegrationTestReportedToGitProvider() + Expect(!result.CancelRequest && err == nil).To(BeTrue()) + Expect(metadata.HasAnnotationWithValue(buildPipelineRun, helpers.SnapshotCreationReportAnnotation, intgteststat.BuildPLRFailed.String())).To(BeTrue()) + + result, err = adapter.EnsureIntegrationTestReportedToGitProvider() + Expect(!result.CancelRequest && err == nil).To(BeTrue()) + expectedLogEntry := "integration test has been set correctly or is being processed, no need to set integration test status from build pipelinerun" + Expect(buf.String()).Should(ContainSubstring(expectedLogEntry)) + }) + + It("ensure integration test is set from build PLR when build PLR succeeded but snapshot is not created", func() { + Expect(metadata.SetAnnotation(buildPipelineRun, helpers.CreateSnapshotAnnotationName, "failed to create snapshot due to error")).ShouldNot(HaveOccurred()) + buf = bytes.Buffer{} + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + componentGroups := []v1beta2.ComponentGroup{*hasCompGroup} + adapter = NewAdapter(ctx, buildPipelineRun, hasComp, &componentGroups, log, loader.NewMockLoader(), k8sClient) + adapter.status = mockStatus + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ComponentGroupsContextKey, + Resource: hasCompGroup, + }, + { + ContextKey: loader.AllIntegrationTestScenariosForComponentGroupsContextKey, + Resource: []v1beta2.IntegrationTestScenario{*integrationTestScenario}, + }, + { + ContextKey: loader.AllIntegrationTestScenariosForComponentGroupContextKey, + Resource: []v1beta2.IntegrationTestScenario{*integrationTestScenario}, + }, + }) + + result, err := adapter.EnsureIntegrationTestReportedToGitProvider() + Expect(!result.CancelRequest && err == nil).To(BeTrue()) + Expect(metadata.HasAnnotationWithValue(buildPipelineRun, helpers.SnapshotCreationReportAnnotation, intgteststat.SnapshotCreationFailed.String())).To(BeTrue()) + + result, err = adapter.EnsureIntegrationTestReportedToGitProvider() + Expect(!result.CancelRequest && err == nil).To(BeTrue()) + expectedLogEntry := "integration test has been set correctly or is being processed, no need to set integration test status from build pipelinerun" + Expect(buf.String()).Should(ContainSubstring(expectedLogEntry)) + }) + + It("add annotation to snapshot when no git provider is found", func() { + // Create a snapshot WITHOUT git provider info but WITH proper component labels + snapshotWithoutGitProvider := &applicationapiv1alpha1.Snapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-snapshot-no-git", + Namespace: "test-namespace", + Labels: map[string]string{ + gitops.SnapshotTypeLabel: gitops.SnapshotComponentType, + gitops.SnapshotComponentLabel: "test-component", + }, + Annotations: map[string]string{ + "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\"}]", + }, + // NO git provider labels! + }, + } + + // Create a buffer for logging + var buf bytes.Buffer + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + + // Set up mocks + ctrl := gomock.NewController(GinkgoT()) + mockReporter = status.NewMockReporterInterface(ctrl) + mockStatus := status.NewMockStatusInterface(ctrl) + + // Mock GetReporter to return nil (no git provider found) + mockStatus.EXPECT().GetReporter(gomock.Any()).Return(nil) + + // Create adapter + componentGroups := []v1beta2.ComponentGroup{*hasCompGroup} + adapter = NewAdapter(ctx, buildPipelineRun, hasComp, &componentGroups, log, loader.NewMockLoader(), k8sClient) + adapter.status = mockStatus + + // Create the required test data + integrationTestScenarios := []v1beta2.IntegrationTestScenario{*integrationTestScenario} + testStatus := intgteststat.IntegrationTestStatusInProgress + componentName := "test-component" + + // Call the method + statusCode, err := adapter.ReportIntegrationTestStatusAccordingToBuildPLR( + buildPipelineRun, + snapshotWithoutGitProvider, + &integrationTestScenarios, + testStatus, + componentName) + + // Check results + Expect(err).ToNot(HaveOccurred()) + Expect(statusCode).To(BeTrue()) + Expect(snapshotWithoutGitProvider.Annotations).To(HaveKey(gitops.GitReportingFailureAnnotation)) + + // Check that the error was logged + Expect(buf.String()).Should(ContainSubstring("Failed to get git reporter for snapshot - missing required labels/annotations")) + }) + + It("Ensure group context integration test can be initialized", func() { + var buf bytes.Buffer + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + buildPipelineRun = &tektonv1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipelinerun-build-sample", + Namespace: "default", + Labels: map[string]string{ + "pipelines.appstudio.openshift.io/type": "build", + "pipelines.openshift.io/used-by": "build-cloud", + "pipelines.openshift.io/runtime": "nodejs", + "pipelines.openshift.io/strategy": "s2i", + "appstudio.openshift.io/component": "component-sample", + "build.appstudio.redhat.com/target_branch": "main", + "pipelinesascode.tekton.dev/event-type": "pull_request", + "pipelinesascode.tekton.dev/pull-request": "1", + "pipelinesascode.tekton.dev/git-provider": "github", + customLabel: "custom-label", + gitops.PRGroupHashLabel: prGroupSha, + }, + Annotations: map[string]string{ + "appstudio.redhat.com/updateComponentOnSuccess": "false", + "pipelinesascode.tekton.dev/on-target-branch": "[main,master]", + "build.appstudio.openshift.io/repo": "https://github.com/devfile-samples/devfile-sample-go-basic?rev=c713067b0e65fb3de50d1f7c457eb51c2ab0dbb0", + "chains.tekton.dev/signed": "true", + "pipelinesascode.tekton.dev/source-branch": "sourceBranch", + "pipelinesascode.tekton.dev/url-org": "redhat", + "pipelinesascode.tekton.dev/git-provider": "github", + gitops.PRGroupAnnotation: prGroup, + }, + CreationTimestamp: metav1.NewTime(time.Now().Add(time.Hour * 1)), + }, + Spec: tektonv1.PipelineRunSpec{}, + } + + buildPipelineRun2 = &tektonv1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipelinerun-build-sample-2", + Namespace: "default", + Labels: map[string]string{ + "pipelines.appstudio.openshift.io/type": "build", + "pipelines.openshift.io/used-by": "build-cloud", + "pipelines.openshift.io/runtime": "nodejs", + "pipelines.openshift.io/strategy": "s2i", + "appstudio.openshift.io/component": "component-sample", + "pipelinesascode.tekton.dev/event-type": "pull_request", + "pipelinesascode.tekton.dev/git-provider": "github", + gitops.PRGroupHashLabel: prGroupSha, + }, + Annotations: map[string]string{ + "appstudio.redhat.com/updateComponentOnSuccess": "false", + "pipelinesascode.tekton.dev/on-target-branch": "[main,master]", + gitops.PRGroupAnnotation: prGroup, + "pipelinesascode.tekton.dev/git-provider": "github", + }, + CreationTimestamp: metav1.NewTime(time.Now()), + }, + Spec: tektonv1.PipelineRunSpec{}, + } + + //mockStatus.EXPECT().IsPRMRInSnapshotOpened(gomock.Any(), hasComSnapshot2).Return(true, nil) + mockStatus.EXPECT().IsPRMRInSnapshotOpened(gomock.Any(), gomock.Any()).Return(true, 0, nil).AnyTimes() + + // Add another componentGroup with only one component + hasCompGroup2 := hasCompGroup.DeepCopy() + hasCompGroup2.Name = "component-group-sample2" + hasCompGroup2.Spec.Components = []v1beta2.ComponentReference{ + { + Name: "component-sample", + ComponentVersion: v1beta2.ComponentVersionReference{ + Name: "v1", + Revision: "main", + }, + }, + } + // Add another componentGroup with two components + hasCompGroup3 := hasCompGroup.DeepCopy() + hasCompGroup3.Name = "component-group-sample3" + hasCompGroup3.Spec.Components = []v1beta2.ComponentReference{ + { + Name: "component-sample", + ComponentVersion: v1beta2.ComponentVersionReference{ + Name: "v1", + Revision: "main", + }, + }, + { + Name: "another-component-sample", + ComponentVersion: v1beta2.ComponentVersionReference{ + Name: "v1", + Revision: "main", + }, + }, + } + componentGroups := []v1beta2.ComponentGroup{*hasCompGroup, *hasCompGroup2, *hasCompGroup3} + adapter = NewAdapter(ctx, buildPipelineRun, hasComp, &componentGroups, log, loader.NewMockLoader(), k8sClient) + + adapter.status = mockStatus + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ComponentGroupsContextKey, + Resource: *hasCompGroup, + }, + { + ContextKey: loader.GetPRSnapshotsKey, + Resource: []applicationapiv1alpha1.Snapshot{*hasSnapshot, *hasComSnapshot2}, + }, + { + ContextKey: loader.GetComponentSnapshotsKey, + Resource: []applicationapiv1alpha1.Snapshot{*hasSnapshot, *hasComSnapshot2}, + }, + { + ContextKey: loader.GetBuildPLRContextKey, + Resource: []tektonv1.PipelineRun{*buildPipelineRun, *buildPipelineRun2}, + }, + { + ContextKey: loader.ComponentGroupComponentsContextKey, + Resource: []applicationapiv1alpha1.Component{*hasComp, *hasComp2}, + }, + { + ContextKey: loader.AllIntegrationTestScenariosForComponentGroupContextKey, + Resource: []v1beta2.IntegrationTestScenario{*integrationTestScenario}, + }, + { + ContextKey: loader.GetComponentsFromSnapshotForPRGroupKey, + Resource: []string{hasComp.Name, hasComp2.Name}, + }, + { + ContextKey: loader.AllIntegrationTestScenariosForComponentGroupsContextKey, + Resource: []v1beta2.IntegrationTestScenario{*integrationTestScenario, *groupIntegrationTestScenario}, + }, + }) + + result, err := adapter.EnsureIntegrationTestReportedToGitProvider() + expectedLogEntry := "Opened PR/MR in snapshot is found" + Expect(buf.String()).Should(ContainSubstring(expectedLogEntry)) + expectedLogEntry = "group snapshot is expected to be created for build pipelinerun" + Expect(buf.String()).Should(ContainSubstring(expectedLogEntry)) + expectedLogEntry = "there is more than 1 component with open pr or mr found, so group snapshot is expected for component-group-sample: [component-sample another-component-sample]" + Expect(buf.String()).Should(ContainSubstring(expectedLogEntry)) + // Since the second componentGroup contains only one component, it should not have an expected group Snapshot + expectedLogEntry = "there is more than 1 component with open pr or mr found, so group snapshot is expected for component-group-sample2: [component-sample]" + Expect(buf.String()).ShouldNot(ContainSubstring(expectedLogEntry)) + // Since the thirt componentGroup contains both components, it should have an expected group Snapshot + expectedLogEntry = "there is more than 1 component with open pr or mr found, so group snapshot is expected for component-group-sample3: [component-sample another-component-sample]" + Expect(buf.String()).Should(ContainSubstring(expectedLogEntry)) + Expect(result.CancelRequest).To(BeFalse()) + Expect(result.RequeueRequest).To(BeFalse()) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + // TODO: Remove after the application model is deprecated + When("integration status should not be set from build PLR [APPLICATION]", func() { BeforeAll(func() { patch := client.MergeFrom(buildPipelineRun.DeepCopy()) _ = metadata.SetAnnotation(&buildPipelineRun.ObjectMeta, gitops.PRGroupAnnotation, prGroup) @@ -2875,6 +3595,75 @@ var _ = Describe("Pipeline Adapter", Ordered, func() { }) }) + When("integration status should not be set from build PLR", func() { + BeforeAll(func() { + patch := client.MergeFrom(buildPipelineRun.DeepCopy()) + _ = metadata.SetAnnotation(&buildPipelineRun.ObjectMeta, gitops.PRGroupAnnotation, prGroup) + _ = metadata.SetLabel(&buildPipelineRun.ObjectMeta, gitops.PRGroupHashLabel, prGroupSha) + Expect(k8sClient.Patch(ctx, buildPipelineRun, patch)).Should(Succeed()) + }) + It("integration test will not be set from build PLR when build PLR succeeded and snapshot is created", func() { + Expect(metadata.SetAnnotation(buildPipelineRun, tektonconsts.SnapshotNamesLabel, "snashot-sample")).ShouldNot(HaveOccurred()) + buf = bytes.Buffer{} + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + componentGroups := []v1beta2.ComponentGroup{*hasCompGroup} + adapter = NewAdapter(ctx, buildPipelineRun, hasComp, &componentGroups, log, loader.NewMockLoader(), k8sClient) + adapter.status = mockStatus + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ComponentGroupContextKey, + Resource: hasCompGroup, + }, + }) + + result, err := adapter.EnsureIntegrationTestReportedToGitProvider() + Expect(!result.CancelRequest && err == nil).To(BeTrue()) + Expect(metadata.HasAnnotationWithValue(buildPipelineRun, helpers.SnapshotCreationReportAnnotation, "SnapshotCreated")).To(BeTrue()) + expectedLogEntry := "snapshot has been created for build pipelineRun, no need to report integration status from build pipelinerun status" + Expect(buf.String()).Should(ContainSubstring(expectedLogEntry)) + }) + + It("integration test will not be set from build PLR when build PLR is not from pac pull request event", func() { + Expect(metadata.DeleteLabel(buildPipelineRun, tektonconsts.PipelineAsCodePullRequestLabel)).ShouldNot(HaveOccurred()) + buf = bytes.Buffer{} + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + componentGroups := []v1beta2.ComponentGroup{*hasCompGroup} + adapter = NewAdapter(ctx, buildPipelineRun, hasComp, &componentGroups, log, loader.NewMockLoader(), k8sClient) + adapter.status = mockStatus + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ComponentGroupContextKey, + Resource: hasCompGroup, + }, + }) + + result, err := adapter.EnsureIntegrationTestReportedToGitProvider() + Expect(!result.CancelRequest && err == nil).To(BeTrue()) + expectedLogEntry := "build pipelineRun is not created by pull/merge request, no need to set integration test status in git provider" + Expect(buf.String()).Should(ContainSubstring(expectedLogEntry)) + }) + + It("integration test will not be set from build PLR when pr group is not annotated to build PLR", func() { + Expect(metadata.DeleteLabel(buildPipelineRun, gitops.PRGroupHashLabel)).ShouldNot(HaveOccurred()) + Expect(metadata.DeleteAnnotation(buildPipelineRun, gitops.PRGroupAnnotation)).ShouldNot(HaveOccurred()) + buf = bytes.Buffer{} + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + componentGroups := []v1beta2.ComponentGroup{*hasCompGroup} + adapter = NewAdapter(ctx, buildPipelineRun, hasComp, &componentGroups, log, loader.NewMockLoader(), k8sClient) + adapter.status = mockStatus + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ComponentGroupContextKey, + Resource: hasCompGroup, + }, + }) + result, err := adapter.EnsureIntegrationTestReportedToGitProvider() + expectedLogEntry := "pr group info has not been added to build pipelineRun metadata, skipping reporting tests for the build pipelineRun" + Expect(buf.String()).Should(ContainSubstring(expectedLogEntry)) + Expect(!result.RequeueRequest && err == nil).To(BeTrue()) + }) + }) + // TODO: Duplicate for ComponentGroup (maybe) When("build pipelineRun succdeds and is signed", func() { BeforeEach(func() { @@ -2929,7 +3718,7 @@ var _ = Describe("Pipeline Adapter", Ordered, func() { }) }) - When("build pipelineRun succdeds and is signed [APPLICATION]", func() { + When("build pipelineRun succeeds and is signed [APPLICATION]", func() { BeforeEach(func() { Expect(metadata.DeleteLabel(buildPipelineRun, tektonconsts.PipelineAsCodePullRequestLabel)).ShouldNot(HaveOccurred()) adapter = createAdapterApplication() diff --git a/internal/controller/snapshot/snapshot_adapter.go b/internal/controller/snapshot/snapshot_adapter.go index e587722d61..3f55a3597a 100644 --- a/internal/controller/snapshot/snapshot_adapter.go +++ b/internal/controller/snapshot/snapshot_adapter.go @@ -21,6 +21,7 @@ import ( "encoding/json" "errors" "fmt" + "slices" "strconv" "strings" "time" @@ -734,7 +735,7 @@ func (a *Adapter) validateOverrideSnapshotComponents() error { } // EnsureGroupSnapshotExist is an operation that ensure the group snapshot is created for component snapshots -// once a new component snapshot is created for an pull request and there are multiple existing PRs belonging to the same PR group +// once a new component snapshot is created for a pull request and there are multiple existing PRs belonging to the same PR group func (a *Adapter) EnsureGroupSnapshotExist() (controller.OperationResult, error) { if gitops.IsSnapshotCreatedByPACPushEvent(a.snapshot) { a.logger.Info("The snapshot is not created by PAC pull request, no need to create group snapshot") @@ -775,8 +776,14 @@ func (a *Adapter) EnsureGroupSnapshotExist() (controller.OperationResult, error) return controller.ContinueProcessing() } - // TODO: handle group snapshots in STONEINTG-1519 - groupSnapshot, componentSnapshotInfos, err := a.prepareGroupSnapshot(a.application, prGroup, prGroupHash) + var groupSnapshot *applicationapiv1alpha1.Snapshot + var componentSnapshotInfos []gitops.ComponentSnapshotInfo + // TODO: Remove application branch once the application model is deprecated + if a.application != nil { + groupSnapshot, componentSnapshotInfos, err = a.prepareGroupSnapshotApplication(prGroup, prGroupHash) + } else { + groupSnapshot, componentSnapshotInfos, err = a.prepareGroupSnapshot(prGroup, prGroupHash) + } if err != nil { a.logger.Error(err, "failed to prepare group snapshot") if h.IsUnrecoverableMetadataError(err) || clienterrors.IsNotFound(err) { @@ -994,90 +1001,160 @@ func (a *Adapter) HandlePipelineCreationError(err error, integrationTestScenario return controller.RequeueWithError(err) } -// TODO: update in STONEINTG-1519 -func (a *Adapter) prepareGroupSnapshot(application *applicationapiv1alpha1.Application, prGroup, prGroupHash string) (*applicationapiv1alpha1.Snapshot, []gitops.ComponentSnapshotInfo, error) { - componentsToCheck, err := a.loader.GetComponentsFromSnapshotForPRGroup(a.context, a.client, application.Namespace, prGroup, prGroupHash, application.Name) +// prepareGroupSnapshot prepares a Group Snapshot based on the existing component Snapshots which belong to the same PR group +// It contains all of the updated components from each of the component Snapshots, while the rest of the components are taken from the Global Candidate List +func (a *Adapter) prepareGroupSnapshot(prGroup, prGroupHash string) (*applicationapiv1alpha1.Snapshot, []gitops.ComponentSnapshotInfo, error) { + snapshotComponents := make([]applicationapiv1alpha1.SnapshotComponent, 0) + componentSnapshotInfos := make([]gitops.ComponentSnapshotInfo, 0) + ownerName := a.componentGroup.Name + ownerLabel := gitops.ComponentGroupNameLabel + namespace := a.componentGroup.Namespace + snapshotComponentsFromGCL, invalidComponents := snapshot.GetSnapshotComponentsFromGCL(a.componentGroup, a.logger.Logger) + + componentsToCheck, err := a.loader.GetComponentsFromSnapshotForPRGroup(a.context, a.client, namespace, prGroupHash, ownerName, ownerLabel) if err != nil { return nil, nil, err } if len(componentsToCheck) < 2 { a.logger.Info(fmt.Sprintf("The number %d of components affected by this PR group %s is less than 2, skipping group snapshot creation", len(componentsToCheck), prGroup)) - return nil, nil, err + return nil, nil, nil } - applicationComponents, err := a.loader.GetAllApplicationComponents(a.context, a.client, application) - if err != nil { - return nil, nil, err - } - - snapshotComponents := make([]applicationapiv1alpha1.SnapshotComponent, 0) - componentSnapshotInfos := make([]gitops.ComponentSnapshotInfo, 0) - for _, applicationComponent := range *applicationComponents { - applicationComponent := applicationComponent // G601 + for _, groupComponent := range a.componentGroup.Spec.Components { + groupComponent := groupComponent // G601 - var foundSnapshotWithOpenedPR *applicationapiv1alpha1.Snapshot - var statusCode int componentInCheck := false for _, c := range componentsToCheck { - if c == applicationComponent.Name { + if c == groupComponent.Name { componentInCheck = true break } } if componentInCheck { - snapshots, err := a.loader.GetMatchingComponentSnapshotsForComponentAndPRGroupHash(a.context, a.client, application.Namespace, applicationComponent.Name, prGroupHash, application.Name) + snapshots, err := a.loader.GetMatchingComponentSnapshotsForComponentAndPRGroupHash(a.context, a.client, namespace, groupComponent.Name, prGroupHash, ownerName, ownerLabel) if err != nil { - a.logger.Error(err, "Failed to fetch Snapshots for component", "component.Name", applicationComponent.Name) + a.logger.Error(err, "Failed to fetch Snapshots for component", "component.Name", groupComponent.Name) return nil, nil, err } - foundSnapshotWithOpenedPR, statusCode, err = a.status.FindSnapshotWithOpenedPR(a.context, snapshots, a.snapshot) + foundSnapshotWithOpenedPR, statusCode, err := a.status.FindSnapshotWithOpenedPR(a.context, snapshots, a.snapshot) if err != nil { a.logger.Error(err, "failed to find snapshot with open PR or MR", "statusCode", statusCode) return nil, nil, err } if foundSnapshotWithOpenedPR != nil { a.logger.Info("PR/MR in snapshot is opened, will find snapshotComponent and add to groupSnapshot") - snapshotComponent := gitops.FindMatchingSnapshotComponent(foundSnapshotWithOpenedPR, &applicationComponent) + snapshotComponent := gitops.FindMatchingSnapshotComponent(foundSnapshotWithOpenedPR, groupComponent.Name) componentSnapshotInfos = append(componentSnapshotInfos, gitops.ComponentSnapshotInfo{ - Component: applicationComponent.Name, + Component: groupComponent.Name, BuildPipelineRun: foundSnapshotWithOpenedPR.Labels[gitops.BuildPipelineRunNameLabel], Snapshot: foundSnapshotWithOpenedPR.Name, Namespace: a.snapshot.Namespace, RepoUrl: foundSnapshotWithOpenedPR.Annotations[gitops.PipelineAsCodeRepoUrlAnnotation], PullRequestNumber: foundSnapshotWithOpenedPR.Annotations[gitops.PipelineAsCodePullRequestAnnotation], + Version: groupComponent.ComponentVersion.Name, }) snapshotComponents = append(snapshotComponents, snapshotComponent) continue } } - a.logger.Info("can't find snapshot with open pull/merge request for component, try to find snapshotComponent from Global Candidate List", "component", applicationComponent.Name) + a.logger.Info("can't find snapshot with open pull/merge request for component, try to find snapshotComponent from Global Candidate List", "component", groupComponent.Name) // if there is no component snapshot found for open PR/MR, we get snapshotComponent from gcl - componentSource, err := gitops.GetComponentSourceFromComponent(&applicationComponent) + snapshotComponent, err := snapshot.FetchSnapshotComponentFromGCL(groupComponent.Name, snapshotComponentsFromGCL, invalidComponents) if err != nil { - a.logger.Error(err, "component cannot be added to snapshot for application due to missing git source", "component.Name", applicationComponent.Name) + a.logger.Error(err, "component cannot be added to snapshot", "component.Name", groupComponent.Name) continue } - containerImage := applicationComponent.Status.LastPromotedImage - if containerImage == "" { - a.logger.Info("component cannot be added to snapshot for application due to missing containerImage", "component.Name", applicationComponent.Name) - continue + if snapshotComponent != nil { + a.logger.Info("component with containerImage from Global Candidate List will be added to group snapshot", "component.Name", snapshotComponent.Name) + snapshotComponents = append(snapshotComponents, *snapshotComponent) } else { - // if the containerImage doesn't have a valid digest, the component - // will not be added to snapshot - err := gitops.ValidateImageDigest(containerImage) + a.logger.Info("component cannot be added to snapshot, no GCL entries found for it", "component.Name", groupComponent.Name) + } + } + + // if the valid component snapshot from open MR/PR is less than 2, won't create group snapshot + if len(componentSnapshotInfos) < 2 { + return nil, componentSnapshotInfos, nil + } + + groupSnapshot := snapshot.NewSnapshot(a.componentGroup, &snapshotComponents) + err = ctrl.SetControllerReference(a.componentGroup, groupSnapshot, a.client.Scheme()) + if err != nil { + a.logger.Error(err, "failed to set owner reference to group snapshot") + return nil, nil, err + } + + groupSnapshot, err = gitops.SetAnnotationAndLabelForGroupSnapshot(groupSnapshot, a.snapshot, componentSnapshotInfos) + if err != nil { + a.logger.Error(err, "failed to annotate group snapshot") + return nil, nil, err + } + + return groupSnapshot, componentSnapshotInfos, nil +} + +// prepareGroupSnapshotApplication prepares a Group Snapshot based on the existing component Snapshots which belong to the same PR group +// It contains all of the updated components from each of the component Snapshots, while the rest of the components are taken from the Global Candidate List +func (a *Adapter) prepareGroupSnapshotApplication(prGroup, prGroupHash string) (*applicationapiv1alpha1.Snapshot, []gitops.ComponentSnapshotInfo, error) { + snapshotComponents := make([]applicationapiv1alpha1.SnapshotComponent, 0) + componentSnapshotInfos := make([]gitops.ComponentSnapshotInfo, 0) + ownerName := a.application.Name + ownerLabel := gitops.ApplicationNameLabel + namespace := a.application.Namespace + components, err := a.loader.GetAllApplicationComponents(a.context, a.client, a.application) + if err != nil { + return nil, nil, err + } + + componentsToCheck, err := a.loader.GetComponentsFromSnapshotForPRGroup(a.context, a.client, namespace, prGroupHash, ownerName, ownerLabel) + if err != nil { + return nil, nil, err + } + if len(componentsToCheck) < 2 { + a.logger.Info(fmt.Sprintf("The number %d of components affected by this PR group %s is less than 2, skipping group snapshot creation", len(componentsToCheck), prGroup)) + return nil, nil, nil + } + for _, applicationComponent := range *components { + applicationComponent := applicationComponent // G601 + + //nolint:govet // Allow parameter inference + if slices.Contains(componentsToCheck, applicationComponent.Name) { + snapshots, err := a.loader.GetMatchingComponentSnapshotsForComponentAndPRGroupHash(a.context, a.client, namespace, applicationComponent.Name, prGroupHash, ownerName, ownerLabel) if err != nil { - a.logger.Error(err, "component cannot be added to snapshot for application due to invalid digest in containerImage", "component.Name", applicationComponent.Name) - continue + a.logger.Error(err, "Failed to fetch Snapshots for component", "component.Name", applicationComponent.Name) + return nil, nil, err } - snapshotComponent := applicationapiv1alpha1.SnapshotComponent{ - Name: applicationComponent.Name, - ContainerImage: containerImage, - Source: *componentSource, + foundSnapshotWithOpenedPR, statusCode, err := a.status.FindSnapshotWithOpenedPR(a.context, snapshots, a.snapshot) + if err != nil { + a.logger.Error(err, "failed to find snapshot with open PR or MR", "statusCode", statusCode) + return nil, nil, err } - a.logger.Info("component with containerImage from Global Candidate List will be added to group snapshot", "component.Name", snapshotComponent.Name) - snapshotComponents = append(snapshotComponents, snapshotComponent) + if foundSnapshotWithOpenedPR != nil { + a.logger.Info("PR/MR in snapshot is opened, will find snapshotComponent and add to groupSnapshot") + snapshotComponent := gitops.FindMatchingSnapshotComponent(foundSnapshotWithOpenedPR, applicationComponent.Name) + componentSnapshotInfos = append(componentSnapshotInfos, gitops.ComponentSnapshotInfo{ + Component: applicationComponent.Name, + BuildPipelineRun: foundSnapshotWithOpenedPR.Labels[gitops.BuildPipelineRunNameLabel], + Snapshot: foundSnapshotWithOpenedPR.Name, + Namespace: a.snapshot.Namespace, + RepoUrl: foundSnapshotWithOpenedPR.Annotations[gitops.PipelineAsCodeRepoUrlAnnotation], + PullRequestNumber: foundSnapshotWithOpenedPR.Annotations[gitops.PipelineAsCodePullRequestAnnotation], + }) + snapshotComponents = append(snapshotComponents, snapshotComponent) + continue + } + } + + a.logger.Info("can't find snapshot with open pull/merge request for component, try to find snapshotComponent from Global Candidate List", "component", applicationComponent.Name) + // if there is no component snapshot found for open PR/MR, we get snapshotComponent from gcl + snapshotComponent, err := a.fetchSnapshotComponentFromApplicationGCL(&applicationComponent) + if err != nil { + a.logger.Error(err, "component cannot be added to snapshot", "component.Name", applicationComponent.Name) + continue } + a.logger.Info("component with containerImage from Global Candidate List will be added to group snapshot", "component.Name", snapshotComponent.Name) + snapshotComponents = append(snapshotComponents, *snapshotComponent) } // if the valid component snapshot from open MR/PR is less than 2, won't create group snapshot @@ -1085,8 +1162,10 @@ func (a *Adapter) prepareGroupSnapshot(application *applicationapiv1alpha1.Appli return nil, componentSnapshotInfos, nil } - groupSnapshot := gitops.NewSnapshot(application, &snapshotComponents) - err = ctrl.SetControllerReference(application, groupSnapshot, a.client.Scheme()) + var groupSnapshot *applicationapiv1alpha1.Snapshot + + groupSnapshot = gitops.NewSnapshot(a.application, &snapshotComponents) + err = ctrl.SetControllerReference(a.application, groupSnapshot, a.client.Scheme()) if err != nil { a.logger.Error(err, "failed to set owner reference to group snapshot") return nil, nil, err @@ -1101,13 +1180,52 @@ func (a *Adapter) prepareGroupSnapshot(application *applicationapiv1alpha1.Appli return groupSnapshot, componentSnapshotInfos, nil } +// fetchSnapshotComponentFromApplicationGCL fetches a Snapshot component from the Application's GCL +// TODO: Remove once the application model is fully deprecated [APPLICATION] +func (a *Adapter) fetchSnapshotComponentFromApplicationGCL(component *applicationapiv1alpha1.Component) (*applicationapiv1alpha1.SnapshotComponent, error) { + var snapshotComponent applicationapiv1alpha1.SnapshotComponent + componentSource, err := gitops.GetComponentSourceFromComponent(component) + if err != nil { + return nil, fmt.Errorf("component cannot be added to snapshot for application due to missing git source") + } + containerImage := component.Status.LastPromotedImage + + if containerImage == "" { + return nil, fmt.Errorf("component cannot be added to snapshot for application due to missing containerImage") + } + + // if the containerImage doesn't have a valid digest, the component + // will not be added to snapshot + err = gitops.ValidateImageDigest(containerImage) + if err != nil { + return nil, fmt.Errorf("component cannot be added to snapshot for application due to invalid digest in containerImage") + } + snapshotComponent = applicationapiv1alpha1.SnapshotComponent{ + Name: component.Name, + ContainerImage: containerImage, + Source: *componentSource, + } + return &snapshotComponent, nil +} + // haveAllPipelineRunProcessedForPrGroup checks if all build plr has been processed for the given pr group -// TODO: update in STONEINTG-1519 func (a *Adapter) haveAllPipelineRunProcessedForPrGroup(prGroup, prGroupHash string) (bool, error) { - pipelineRuns, err := a.loader.GetPipelineRunsWithPRGroupHash(a.context, a.client, a.snapshot.Namespace, prGroupHash, a.application.Name) - if err != nil { - a.logger.Error(err, fmt.Sprintf("Failed to get build pipelineRuns for given pr group hash %s", prGroupHash)) - return false, err + var pipelineRuns *[]tektonv1.PipelineRun + var err error + if a.application != nil { + pipelineRuns, err = a.loader.GetPipelineRunsWithPRGroupHashForApplication(a.context, a.client, a.snapshot.Namespace, prGroupHash, a.application.Name) + if err != nil { + a.logger.Error(err, fmt.Sprintf("Failed to get build pipelineRuns for given pr group hash %s", prGroupHash)) + return false, err + } + + } else { + pipelineRuns, err = a.loader.GetPipelineRunsWithPRGroupHash(a.context, a.client, a.snapshot.Namespace, prGroupHash) + if err != nil { + a.logger.Error(err, fmt.Sprintf("Failed to get build pipelineRuns for given pr group hash %s", prGroupHash)) + return false, err + } + pipelineRuns = a.filterPipelineRunsForComponentGroup(pipelineRuns, a.componentGroup) } for _, pipelineRun := range *pipelineRuns { @@ -1148,6 +1266,21 @@ func (a *Adapter) haveAllPipelineRunProcessedForPrGroup(prGroup, prGroupHash str return true, nil } +// filterPipelineRunsForComponentGroup filters pipelineRuns to only ones that have components that belong to the componentGroup +func (a *Adapter) filterPipelineRunsForComponentGroup(allPipelineRuns *[]tektonv1.PipelineRun, componentGroup *v1beta2.ComponentGroup) *[]tektonv1.PipelineRun { + var filteredPipelineRuns []tektonv1.PipelineRun + componentNames := h.GetComponentNamesFromComponentGroup(componentGroup) + for _, pipelineRun := range *allPipelineRuns { + if builtComponentName, found := pipelineRun.Labels[tektonconsts.PipelineRunComponentLabel]; found { + if slices.Contains(componentNames, builtComponentName) { + filteredPipelineRuns = append(filteredPipelineRuns, pipelineRun) + continue + } + } + } + return &filteredPipelineRuns +} + // checkAndCancelOldSnapshotsPipelineRun sorts all snapshots for component group and cancels all running integrationTest pipelineruns within component group // removes finalizer before the pipelinerun is set as CancelledRunFinally to be gracefully cancelled func (a *Adapter) checkAndCancelOldSnapshotsPipelineRun() error { @@ -1179,9 +1312,9 @@ func (a *Adapter) checkAndCancelOldSnapshotsPipelineRun() error { } // TODO: remove if statement when we deprecated old application model if a.application != nil { - snapshots, err = a.loader.GetMatchingGroupSnapshotsForPRGroupHash(a.context, a.client, a.application.Namespace, prGroupHash, a.application.Name) + snapshots, err = a.loader.GetMatchingGroupSnapshotsForPRGroupHash(a.context, a.client, a.application.Namespace, prGroupHash, a.application.Name, gitops.ApplicationNameLabel) } else { - snapshots, err = a.loader.GetMatchingGroupSnapshotsForPRGroupHash(a.context, a.client, a.componentGroup.Namespace, prGroupHash, a.componentGroup.Name) + snapshots, err = a.loader.GetMatchingGroupSnapshotsForPRGroupHash(a.context, a.client, a.componentGroup.Namespace, prGroupHash, a.componentGroup.Name, gitops.ComponentGroupNameLabel) } if err != nil { a.logger.Error(fmt.Errorf("failed to get group snapshot for pr group from group snapshot"), "snapshot.Namespace", a.snapshot.Namespace, "snapshot.Name", a.snapshot.Name) diff --git a/internal/controller/snapshot/snapshot_adapter_test.go b/internal/controller/snapshot/snapshot_adapter_test.go index 4c5635c1ad..91b03ebb85 100644 --- a/internal/controller/snapshot/snapshot_adapter_test.go +++ b/internal/controller/snapshot/snapshot_adapter_test.go @@ -770,6 +770,7 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { gitops.PipelineAsCodeSHALabel: "sha", gitops.PipelineAsCodePullRequestAnnotation: "1", gitops.ApplicationNameLabel: hasApp.Name, + gitops.ComponentGroupNameLabel: hasCompGroup.Name, }, Annotations: map[string]string{ "test.appstudio.openshift.io/pr-last-update": "2023-08-26T17:57:50+02:00", @@ -781,7 +782,7 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { }, }, Spec: applicationapiv1alpha1.SnapshotSpec{ - Application: hasApp.Name, + ComponentGroup: hasCompGroup.Name, Components: []applicationapiv1alpha1.SnapshotComponent{ { Name: "component1", @@ -817,7 +818,7 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { "pac.test.appstudio.openshift.io/url-repository": "testrepo", gitops.PipelineAsCodeSHALabel: "sha", gitops.PipelineAsCodePullRequestAnnotation: "1", - gitops.ApplicationNameLabel: hasApp.Name, + gitops.ComponentGroupNameLabel: hasCompGroup.Name, }, Annotations: map[string]string{ "test.appstudio.openshift.io/pr-last-update": "2023-08-26T17:57:50+02:00", @@ -828,7 +829,7 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { }, }, Spec: applicationapiv1alpha1.SnapshotSpec{ - Application: hasApp.Name, + ComponentGroup: hasCompGroup.Name, Components: []applicationapiv1alpha1.SnapshotComponent{ { Name: hasCom1.Name, @@ -855,7 +856,7 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { "pac.test.appstudio.openshift.io/url-repository": "testrepo", gitops.PipelineAsCodeSHALabel: "sha", gitops.PipelineAsCodePullRequestAnnotation: "1", - gitops.ApplicationNameLabel: hasApp.Name, + gitops.ComponentGroupNameLabel: hasCompGroup.Name, }, Annotations: map[string]string{ "test.appstudio.openshift.io/pr-last-update": "2023-08-26T17:57:50+02:00", @@ -867,7 +868,7 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { }, }, Spec: applicationapiv1alpha1.SnapshotSpec{ - Application: hasApp.Name, + ComponentGroup: hasCompGroup.Name, Components: []applicationapiv1alpha1.SnapshotComponent{ { Name: hasCom3.Name, @@ -890,7 +891,7 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { "pipelines.openshift.io/used-by": "build-cloud", "pipelines.openshift.io/runtime": "nodejs", "pipelines.openshift.io/strategy": "s2i", - "appstudio.openshift.io/component": "component-sample", + "appstudio.openshift.io/component": "component1-sample", "pipelinesascode.tekton.dev/event-type": "pull_request", "build.appstudio.redhat.com/target_branch": "main", "test.appstudio.openshift.io/pr-group-sha": prGroupSha, @@ -3366,12 +3367,38 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { }) }) - // NOTE: update in group snapshot ticket - When("Adapter is created for component snapshot with pr group", func() { - It("ensures component snapshot will not be processed if it is not from pull/merge request", func() { + // TODO: Remove after the Application model is deprecated + When("Adapter is created for component snapshot with pr group [APPLICATION]", func() { + BeforeEach(func() { + hasComSnapshot1.Spec.Application = hasApp.Name + hasComSnapshot1.Spec.ComponentGroup = "" + hasComSnapshot1.Labels[gitops.ApplicationNameLabel] = hasApp.Name + hasComSnapshot2.Spec.Application = hasApp.Name + hasComSnapshot2.Spec.ComponentGroup = "" + hasComSnapshot2.Labels[gitops.ApplicationNameLabel] = hasApp.Name + hasComSnapshot3.Spec.Application = hasApp.Name + hasComSnapshot3.Spec.ComponentGroup = "" + hasComSnapshot3.Labels[gitops.ApplicationNameLabel] = hasApp.Name + }) + It("Calling EnsureGroupSnapshotExist when there is running build PLR belonging to the same pr group sha", func() { + buildPipelineRun1.Status = tektonv1.PipelineRunStatus{ + PipelineRunStatusFields: tektonv1.PipelineRunStatusFields{ + Results: []tektonv1.PipelineRunResult{}, + }, + Status: v1.Status{ + Conditions: v1.Conditions{ + apis.Condition{ + Reason: "Running", + Status: "Unknown", + Type: apis.ConditionSucceeded, + }, + }, + }, + } + Expect(k8sClient.Status().Update(ctx, buildPipelineRun1)).Should(Succeed()) + var buf bytes.Buffer log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} - hasComSnapshot1.Labels[gitops.PipelineAsCodeEventTypeLabel] = gitops.PipelineAsCodePushType adapter = NewAdapterWithApplication(ctx, hasComSnapshot1, hasApp, log, loader.NewMockLoader(), k8sClient) adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ { @@ -3382,19 +3409,39 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { ContextKey: loader.SnapshotContextKey, Resource: hasComSnapshot1, }, + { + ContextKey: loader.GetBuildPLRContextKey, + Resource: []tektonv1.PipelineRun{*buildPipelineRun1}, + }, }) result, err := adapter.EnsureGroupSnapshotExist() Expect(result.CancelRequest).To(BeFalse()) Expect(result.RequeueRequest).To(BeFalse()) - Expect(buf.String()).Should(ContainSubstring("The snapshot is not created by PAC pull request")) + Expect(buf.String()).Should(ContainSubstring("is still running, won't create group snapshot")) Expect(err).ToNot(HaveOccurred()) + Expect(metadata.HasAnnotation(hasComSnapshot1, gitops.PRGroupCreationAnnotation)).To(BeTrue()) }) - It("ensures component snapshot will not be processed if it has been processed", func() { + It("Calling EnsureGroupSnapshotExist when there is failed build PLR belonging to the same pr group sha", func() { + buildPipelineRun1.Status = tektonv1.PipelineRunStatus{ + PipelineRunStatusFields: tektonv1.PipelineRunStatusFields{ + Results: []tektonv1.PipelineRunResult{}, + }, + Status: v1.Status{ + Conditions: v1.Conditions{ + apis.Condition{ + Reason: "failed", + Status: "False", + Type: apis.ConditionSucceeded, + }, + }, + }, + } + Expect(k8sClient.Status().Update(ctx, buildPipelineRun1)).Should(Succeed()) + var buf bytes.Buffer log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} - hasComSnapshot1.Annotations[gitops.PRGroupCreationAnnotation] = "processed" adapter = NewAdapterWithApplication(ctx, hasComSnapshot1, hasApp, log, loader.NewMockLoader(), k8sClient) adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ { @@ -3405,6 +3452,323 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { ContextKey: loader.SnapshotContextKey, Resource: hasComSnapshot1, }, + { + ContextKey: loader.GetBuildPLRContextKey, + Resource: []tektonv1.PipelineRun{*buildPipelineRun1}, + }, + }) + + result, err := adapter.EnsureGroupSnapshotExist() + Expect(result.CancelRequest).To(BeFalse()) + Expect(result.RequeueRequest).To(BeFalse()) + Expect(buf.String()).Should(ContainSubstring("failed, won't create group snapshot")) + Expect(err).ToNot(HaveOccurred()) + Expect(metadata.HasAnnotation(hasComSnapshot1, gitops.PRGroupCreationAnnotation)).To(BeTrue()) + }) + + It("Calling en when there is successful build PLR belonging to the same pr group sha but component snapshot is not created", func() { + buildPipelineRun1.Status = tektonv1.PipelineRunStatus{ + PipelineRunStatusFields: tektonv1.PipelineRunStatusFields{ + Results: []tektonv1.PipelineRunResult{}, + }, + Status: v1.Status{ + Conditions: v1.Conditions{ + apis.Condition{ + Reason: "succeeded", + Status: "True", + Type: apis.ConditionSucceeded, + }, + }, + }, + } + Expect(k8sClient.Status().Update(ctx, buildPipelineRun1)).Should(Succeed()) + + var buf bytes.Buffer + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + adapter = NewAdapterWithApplication(ctx, hasComSnapshot1, hasApp, log, loader.NewMockLoader(), k8sClient) + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ApplicationContextKey, + Resource: hasApp, + }, + { + ContextKey: loader.SnapshotContextKey, + Resource: hasComSnapshot1, + }, + { + ContextKey: loader.GetBuildPLRContextKey, + Resource: []tektonv1.PipelineRun{*buildPipelineRun1}, + }, + }) + + result, err := adapter.EnsureGroupSnapshotExist() + Expect(result.CancelRequest).To(BeFalse()) + Expect(result.RequeueRequest).To(BeFalse()) + Expect(buf.String()).Should(ContainSubstring("has succeeded but component snapshot has not been created now")) + Expect(err).ToNot(HaveOccurred()) + }) + + It("Stop processing when there is no annotationID in snapshot", func() { + var buf bytes.Buffer + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + adapter = NewAdapterWithApplication(ctx, hasComSnapshot1, hasApp, log, loader.NewMockLoader(), k8sClient) + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ApplicationContextKey, + Resource: hasApp, + }, + { + ContextKey: loader.SnapshotContextKey, + Resource: hasComSnapshot1, + }, + { + ContextKey: loader.GetBuildPLRContextKey, + Resource: []tektonv1.PipelineRun{}, + }, + { + ContextKey: loader.GetPRSnapshotsKey, + Resource: []applicationapiv1alpha1.Snapshot{*hasComSnapshot1, *hasComSnapshot3}, + }, + { + ContextKey: loader.ApplicationComponentsContextKey, + Resource: []applicationapiv1alpha1.Component{*hasCom1, *hasCom3}, + }, + }) + + // create 3 snapshots here because we need to get snapshot twice so that we can't use the mocked snapshot + Expect(k8sClient.Create(adapter.context, hasComSnapshot2)).Should(Succeed()) + Eventually(func() error { + err := k8sClient.Get(adapter.context, types.NamespacedName{ + Name: hasComSnapshot2.Name, + Namespace: "default", + }, hasComSnapshot2) + return err + }, time.Second*10).ShouldNot(HaveOccurred()) + Expect(k8sClient.Create(adapter.context, hasComSnapshot3)).Should(Succeed()) + Eventually(func() error { + err := k8sClient.Get(adapter.context, types.NamespacedName{ + Name: hasComSnapshot3.Name, + Namespace: "default", + }, hasComSnapshot3) + return err + }, time.Second*10).ShouldNot(HaveOccurred()) + + result, err := adapter.EnsureGroupSnapshotExist() + Expect(result.CancelRequest).To(BeFalse()) + Expect(result.RequeueRequest).To(BeFalse()) + Expect(buf.String()).Should(ContainSubstring("failed to get app credentials from Snapshot")) + Expect(err).ToNot(HaveOccurred()) + }) + + It("Stop processing when there is only one component affected for pr group", func() { + var buf bytes.Buffer + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + adapter = NewAdapterWithApplication(ctx, hasComSnapshot1, hasApp, log, loader.NewMockLoader(), k8sClient) + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ApplicationContextKey, + Resource: hasApp, + }, + { + ContextKey: loader.SnapshotContextKey, + Resource: hasComSnapshot1, + }, + { + ContextKey: loader.GetBuildPLRContextKey, + Resource: []tektonv1.PipelineRun{}, + }, + { + ContextKey: loader.ApplicationComponentsContextKey, + Resource: []applicationapiv1alpha1.Component{*hasCom1, *hasCom3}, + }, + }) + + result, err := adapter.EnsureGroupSnapshotExist() + Expect(result.CancelRequest).To(BeFalse()) + Expect(result.RequeueRequest).To(BeFalse()) + Expect(buf.String()).Should(ContainSubstring("The number 1 of components affected by this PR group feature1 is less than 2, skipping group snapshot creation")) + Expect(err).ToNot(HaveOccurred()) + }) + + It("Ensure group snapshot can be created", func() { + var buf bytes.Buffer + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + + ctrl := gomock.NewController(GinkgoT()) + mockStatus := status.NewMockStatusInterface(ctrl) + mockStatus.EXPECT().FindSnapshotWithOpenedPR(gomock.Any(), gomock.Any(), gomock.Any()).Return(hasComSnapshot2, 0, nil) + mockStatus.EXPECT().FindSnapshotWithOpenedPR(gomock.Any(), gomock.Any(), gomock.Any()).Return(hasComSnapshot3, 0, nil) + // mockStatus.EXPECT().IsPRMRInSnapshotOpened(gomock.Any(), gomock.Any()).Return(true, nil) + mockStatus.EXPECT().IsPRMRInSnapshotOpened(gomock.Any(), gomock.Any()).AnyTimes() + + adapter = NewAdapterWithApplication(ctx, hasComSnapshot1, hasApp, log, loader.NewMockLoader(), k8sClient) + + adapter.status = mockStatus + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ApplicationContextKey, + Resource: hasApp, + }, + { + ContextKey: loader.SnapshotContextKey, + Resource: hasComSnapshot1, + }, + { + ContextKey: loader.GetBuildPLRContextKey, + Resource: []tektonv1.PipelineRun{}, + }, + { + ContextKey: loader.ApplicationComponentsContextKey, + Resource: []applicationapiv1alpha1.Component{*hasComp, *hasCom1, *hasCom3, *hasCompMissingImageDigest, *hasCompWithValidImage}, + }, + }) + + // create 3 snapshots here because we need to get snapshot twice so that we can't use the mocked snapshot + Expect(k8sClient.Create(adapter.context, hasComSnapshot2)).Should(Succeed()) + Eventually(func() error { + err := k8sClient.Get(adapter.context, types.NamespacedName{ + Name: hasComSnapshot2.Name, + Namespace: "default", + }, hasComSnapshot2) + return err + }, time.Second*10).ShouldNot(HaveOccurred()) + Expect(k8sClient.Create(adapter.context, hasComSnapshot3)).Should(Succeed()) + Eventually(func() error { + err := k8sClient.Get(adapter.context, types.NamespacedName{ + Name: hasComSnapshot3.Name, + Namespace: "default", + }, hasComSnapshot3) + return err + }, time.Second*10).ShouldNot(HaveOccurred()) + + result, err := adapter.EnsureGroupSnapshotExist() + Expect(result.CancelRequest).To(BeFalse()) + Expect(result.RequeueRequest).To(BeFalse()) + Expect(buf.String()).Should(ContainSubstring("component cannot be added to snapshot for application due to missing containerImage")) + Expect(err).NotTo(HaveOccurred()) + Eventually(func() bool { + _ = k8sClient.Get(adapter.context, types.NamespacedName{ + Name: hasComSnapshot2.Name, + Namespace: "default", + }, hasComSnapshot2) + return metadata.HasAnnotation(hasComSnapshot2, gitops.PRGroupCreationAnnotation) && + Expect(hasComSnapshot2.Annotations[gitops.PRGroupCreationAnnotation]).Should(ContainSubstring("is created for pr group")) + }, time.Second*10).Should(BeTrue()) + Eventually(func() bool { + _ = k8sClient.Get(adapter.context, types.NamespacedName{ + Name: hasComSnapshot3.Name, + Namespace: "default", + }, hasComSnapshot3) + return metadata.HasAnnotation(hasComSnapshot3, gitops.PRGroupCreationAnnotation) && + Expect(hasComSnapshot3.Annotations[gitops.PRGroupCreationAnnotation]).Should(ContainSubstring("is created for pr group")) + }, time.Second*10).Should(BeTrue()) + }) + }) + + When("Adapter is created for component snapshot with pr group", func() { + BeforeAll(func() { + hasCompGroup.Spec.Components = []v1beta2.ComponentReference{ + v1beta2.ComponentReference{ + Name: hasComp.Name, + ComponentVersion: v1beta2.ComponentVersionReference{ + Name: "v1", + Revision: "main", + }, + }, + v1beta2.ComponentReference{ + Name: hasCom1.Name, + ComponentVersion: v1beta2.ComponentVersionReference{ + Name: "v1", + Revision: "main", + }, + }, + v1beta2.ComponentReference{ + Name: hasCom3.Name, + ComponentVersion: v1beta2.ComponentVersionReference{ + Name: "v1", + Revision: "main", + }, + }, + } + Expect(k8sClient.Update(ctx, hasCompGroup)).Should(Succeed()) + + hasCompGroup.Status = v1beta2.ComponentGroupStatus{ + Conditions: []metav1.Condition{ + metav1.Condition{ + Type: "Succeeded", + Status: metav1.ConditionTrue, + Reason: "testing", + Message: "test condition", + LastTransitionTime: metav1.Time{Time: time.Now()}, + }, + }, + GlobalCandidateList: []v1beta2.ComponentState{ + v1beta2.ComponentState{ + Name: hasComp.Name, + Version: "v1", + URL: SampleRepoLink, + LastPromotedImage: "", + LastPromotedCommit: "", + LastPromotedBuildTime: &metav1.Time{Time: time.Now()}, + }, + v1beta2.ComponentState{ + Name: hasCom1.Name, + Version: "v1", + URL: SampleRepoLink, + LastPromotedImage: fmt.Sprintf("%s@%s", sample_image, sampleDigest), + LastPromotedCommit: sample_commit, + LastPromotedBuildTime: &metav1.Time{Time: time.Now()}, + }, + v1beta2.ComponentState{ + Name: hasCom3.Name, + Version: "v1", + URL: SampleRepoLink, + LastPromotedImage: fmt.Sprintf("%s@%s", sample_image, sampleDigest), + LastPromotedCommit: sample_commit, + LastPromotedBuildTime: &metav1.Time{Time: time.Now()}, + }, + }, + } + Expect(k8sClient.Status().Update(ctx, hasCompGroup)).Should(Succeed()) + }) + + It("ensures component snapshot will not be processed if it is not from pull/merge request", func() { + var buf bytes.Buffer + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + hasComSnapshot1.Labels[gitops.PipelineAsCodeEventTypeLabel] = gitops.PipelineAsCodePushType + adapter = NewAdapter(ctx, hasComSnapshot1, hasCompGroup, log, loader.NewMockLoader(), k8sClient) + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ComponentGroupContextKey, + Resource: hasCompGroup, + }, + { + ContextKey: loader.SnapshotContextKey, + Resource: hasComSnapshot1, + }, + }) + + result, err := adapter.EnsureGroupSnapshotExist() + Expect(result.CancelRequest).To(BeFalse()) + Expect(result.RequeueRequest).To(BeFalse()) + Expect(buf.String()).Should(ContainSubstring("The snapshot is not created by PAC pull request")) + Expect(err).ToNot(HaveOccurred()) + }) + + It("ensures component snapshot will not be processed if it has been processed", func() { + var buf bytes.Buffer + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + hasComSnapshot1.Annotations[gitops.PRGroupCreationAnnotation] = "processed" + adapter = NewAdapter(ctx, hasComSnapshot1, hasCompGroup, log, loader.NewMockLoader(), k8sClient) + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ComponentGroupsContextKey, + Resource: hasCompGroup, + }, + { + ContextKey: loader.SnapshotContextKey, + Resource: hasComSnapshot1, + }, }) result, err := adapter.EnsureGroupSnapshotExist() @@ -3419,11 +3783,11 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} Expect(metadata.DeleteLabel(hasComSnapshot1, gitops.PRGroupHashLabel)).ShouldNot(HaveOccurred()) Expect(metadata.HasLabel(hasComSnapshot1, gitops.PRGroupHashLabel)).To(BeFalse()) - adapter = NewAdapterWithApplication(ctx, hasComSnapshot1, hasApp, log, loader.NewMockLoader(), k8sClient) + adapter = NewAdapter(ctx, hasComSnapshot1, hasCompGroup, log, loader.NewMockLoader(), k8sClient) adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ { - ContextKey: loader.ApplicationContextKey, - Resource: hasApp, + ContextKey: loader.ComponentGroupsContextKey, + Resource: hasCompGroup, }, { ContextKey: loader.SnapshotContextKey, @@ -3458,11 +3822,11 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { var buf bytes.Buffer log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} - adapter = NewAdapterWithApplication(ctx, hasComSnapshot1, hasApp, log, loader.NewMockLoader(), k8sClient) + adapter = NewAdapter(ctx, hasComSnapshot1, hasCompGroup, log, loader.NewMockLoader(), k8sClient) adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ { - ContextKey: loader.ApplicationContextKey, - Resource: hasApp, + ContextKey: loader.ComponentGroupsContextKey, + Resource: hasCompGroup, }, { ContextKey: loader.SnapshotContextKey, @@ -3501,11 +3865,11 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { var buf bytes.Buffer log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} - adapter = NewAdapterWithApplication(ctx, hasComSnapshot1, hasApp, log, loader.NewMockLoader(), k8sClient) + adapter = NewAdapter(ctx, hasComSnapshot1, hasCompGroup, log, loader.NewMockLoader(), k8sClient) adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ { - ContextKey: loader.ApplicationContextKey, - Resource: hasApp, + ContextKey: loader.ComponentGroupsContextKey, + Resource: hasCompGroup, }, { ContextKey: loader.SnapshotContextKey, @@ -3544,11 +3908,11 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { var buf bytes.Buffer log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} - adapter = NewAdapterWithApplication(ctx, hasComSnapshot1, hasApp, log, loader.NewMockLoader(), k8sClient) + adapter = NewAdapter(ctx, hasComSnapshot1, hasCompGroup, log, loader.NewMockLoader(), k8sClient) adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ { - ContextKey: loader.ApplicationContextKey, - Resource: hasApp, + ContextKey: loader.ComponentGroupsContextKey, + Resource: hasCompGroup, }, { ContextKey: loader.SnapshotContextKey, @@ -3570,11 +3934,11 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { It("Stop processing when there is no annotationID in snapshot", func() { var buf bytes.Buffer log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} - adapter = NewAdapterWithApplication(ctx, hasComSnapshot1, hasApp, log, loader.NewMockLoader(), k8sClient) + adapter = NewAdapter(ctx, hasComSnapshot1, hasCompGroup, log, loader.NewMockLoader(), k8sClient) adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ { - ContextKey: loader.ApplicationContextKey, - Resource: hasApp, + ContextKey: loader.ComponentGroupsContextKey, + Resource: hasCompGroup, }, { ContextKey: loader.SnapshotContextKey, @@ -3589,12 +3953,12 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { Resource: []applicationapiv1alpha1.Snapshot{*hasComSnapshot1, *hasComSnapshot3}, }, { - ContextKey: loader.ApplicationComponentsContextKey, + ContextKey: loader.ComponentGroupComponentsContextKey, Resource: []applicationapiv1alpha1.Component{*hasCom1, *hasCom3}, }, }) - // create 3 snasphots here because we need to get snapshot twice so that we can't use the mocked snapshot + // create 3 snapshots here because we need to get snapshot twice so that we can't use the mocked snapshot Expect(k8sClient.Create(adapter.context, hasComSnapshot2)).Should(Succeed()) Eventually(func() error { err := k8sClient.Get(adapter.context, types.NamespacedName{ @@ -3622,11 +3986,11 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { It("Stop processing when there is only one component affected for pr group", func() { var buf bytes.Buffer log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} - adapter = NewAdapterWithApplication(ctx, hasComSnapshot1, hasApp, log, loader.NewMockLoader(), k8sClient) + adapter = NewAdapter(ctx, hasComSnapshot1, hasCompGroup, log, loader.NewMockLoader(), k8sClient) adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ { - ContextKey: loader.ApplicationContextKey, - Resource: hasApp, + ContextKey: loader.ComponentGroupsContextKey, + Resource: hasCompGroup, }, { ContextKey: loader.SnapshotContextKey, @@ -3637,7 +4001,7 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { Resource: []tektonv1.PipelineRun{}, }, { - ContextKey: loader.ApplicationComponentsContextKey, + ContextKey: loader.ComponentGroupComponentsContextKey, Resource: []applicationapiv1alpha1.Component{*hasCom1, *hasCom3}, }, }) @@ -3649,7 +4013,7 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { Expect(err).ToNot(HaveOccurred()) }) - It("Ensure group snasphot can be created", func() { + It("Ensure group snapshot can be created", func() { var buf bytes.Buffer log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} @@ -3660,13 +4024,106 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { // mockStatus.EXPECT().IsPRMRInSnapshotOpened(gomock.Any(), gomock.Any()).Return(true, nil) mockStatus.EXPECT().IsPRMRInSnapshotOpened(gomock.Any(), gomock.Any()).AnyTimes() - adapter = NewAdapterWithApplication(ctx, hasComSnapshot1, hasApp, log, loader.NewMockLoader(), k8sClient) + tmpHasCompGroup := hasCompGroup.DeepCopy() + tmpHasCompGroup.Spec = v1beta2.ComponentGroupSpec{ + Components: []v1beta2.ComponentReference{ + { + Name: hasComp.Name, + ComponentVersion: v1beta2.ComponentVersionReference{ + Name: "v1", + Revision: "main", + }, + }, + { + Name: hasCom1.Name, + ComponentVersion: v1beta2.ComponentVersionReference{ + Name: "v1", + Revision: "main", + }, + }, + { + Name: hasCom3.Name, + ComponentVersion: v1beta2.ComponentVersionReference{ + Name: "v1", + Revision: "main", + }, + }, + { + Name: hasCompMissingImageDigest.Name, + ComponentVersion: v1beta2.ComponentVersionReference{ + Name: "v1", + Revision: "main", + }, + }, + { + Name: hasCompWithValidImage.Name, + ComponentVersion: v1beta2.ComponentVersionReference{ + Name: "v1", + Revision: "main", + }, + }, + }, + } + tmpHasCompGroup.Status = v1beta2.ComponentGroupStatus{ + Conditions: []metav1.Condition{ + metav1.Condition{ + Type: "Succeeded", + Status: metav1.ConditionTrue, + Reason: "testing", + Message: "test condition", + LastTransitionTime: metav1.Time{Time: time.Now()}, + }, + }, + GlobalCandidateList: []v1beta2.ComponentState{ + { + Name: hasComp.Name, + Version: "v1", + URL: SampleRepoLink, + LastPromotedImage: sample_image, + LastPromotedCommit: sample_commit, + LastPromotedBuildTime: &metav1.Time{Time: time.Now()}, + }, + { + Name: hasCom1.Name, + Version: "v1", + URL: SampleRepoLink, + LastPromotedImage: sample_image, + LastPromotedCommit: sample_commit, + LastPromotedBuildTime: &metav1.Time{Time: time.Now()}, + }, + { + Name: hasCom3.Name, + Version: "v1", + URL: SampleRepoLink, + LastPromotedImage: sample_image, + LastPromotedCommit: sample_commit, + LastPromotedBuildTime: &metav1.Time{Time: time.Now()}, + }, + { + Name: hasCompMissingImageDigest.Name, + Version: "v1", + URL: SampleRepoLink, + LastPromotedCommit: sample_commit, + LastPromotedBuildTime: &metav1.Time{Time: time.Now()}, + }, + { + Name: hasCompWithValidImage.Name, + Version: "v1", + URL: SampleRepoLink, + LastPromotedImage: sample_image, + LastPromotedCommit: sample_commit, + LastPromotedBuildTime: &metav1.Time{Time: time.Now()}, + }, + }, + } + + adapter = NewAdapter(ctx, hasComSnapshot1, tmpHasCompGroup, log, loader.NewMockLoader(), k8sClient) adapter.status = mockStatus adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ { - ContextKey: loader.ApplicationContextKey, - Resource: hasApp, + ContextKey: loader.ComponentGroupsContextKey, + Resource: hasCompGroup, }, { ContextKey: loader.SnapshotContextKey, @@ -3677,12 +4134,12 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { Resource: []tektonv1.PipelineRun{}, }, { - ContextKey: loader.ApplicationComponentsContextKey, + ContextKey: loader.ComponentGroupComponentsContextKey, Resource: []applicationapiv1alpha1.Component{*hasComp, *hasCom1, *hasCom3, *hasCompMissingImageDigest, *hasCompWithValidImage}, }, }) - // create 3 snasphots here because we need to get snapshot twice so that we can't use the mocked snapshot + // create 3 snapshots here because we need to get snapshot twice so that we can't use the mocked snapshot Expect(k8sClient.Create(adapter.context, hasComSnapshot2)).Should(Succeed()) Eventually(func() error { err := k8sClient.Get(adapter.context, types.NamespacedName{ diff --git a/internal/controller/statusreport/statusreport_adapter.go b/internal/controller/statusreport/statusreport_adapter.go index 2add5c27d2..301d2c67a7 100644 --- a/internal/controller/statusreport/statusreport_adapter.go +++ b/internal/controller/statusreport/statusreport_adapter.go @@ -24,6 +24,7 @@ import ( "time" applicationapiv1alpha1 "github.com/konflux-ci/application-api/api/v1alpha1" + "github.com/konflux-ci/integration-service/snapshot" "github.com/konflux-ci/operator-toolkit/controller" "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" @@ -231,43 +232,60 @@ func (a *Adapter) EnsureGroupSnapshotCreationStatusReportedToGitProvider() (cont return controller.ContinueProcessing() } - // TODO: remove when old application-specific code is removed (see STONEINTG-1519 for ComponentGroup path) - if a.application != nil { - integrationTestStatus := intgteststat.GroupSnapshotCreationFailed + var allIntegrationTestScenarios *[]v1beta2.IntegrationTestScenario + var tempGroupSnapshot *applicationapiv1alpha1.Snapshot + var resourceBlurb string + var err error + integrationTestStatus := intgteststat.GroupSnapshotCreationFailed + var groupName string - allIntegrationTestScenarios, err := a.loader.GetAllIntegrationTestScenariosForApplication(a.context, a.client, a.application) + // TODO: remove when old application-specific code is removed + if a.application != nil { + resourceBlurb = fmt.Sprintf("application %s in namespace %s", a.application.Name, a.application.Namespace) + groupName = gitops.ComponentNameForGroupSnapshot + tempGroupSnapshot = gitops.PrepareTempGroupSnapshot(a.application, a.snapshot) + 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) + a.logger.Error(err, fmt.Sprintf("Failed to get integration test scenarios for %s", resourceBlurb)) + return controller.RequeueWithError(err) + } + } else { + resourceBlurb = fmt.Sprintf("component group %s in namespace %s", a.componentGroup.Name, a.componentGroup.Namespace) + groupName = fmt.Sprintf("%s %s", gitops.ComponentNameForGroupSnapshot, a.componentGroup.Name) + tempGroupSnapshot = snapshot.PrepareTempGroupSnapshot(a.componentGroup, a.snapshot) + allIntegrationTestScenarios, err = a.loader.GetAllIntegrationTestScenariosForComponentGroup(a.context, a.client, a.componentGroup) + if err != nil { + a.logger.Error(err, fmt.Sprintf("Failed to get integration test scenarios for %s", resourceBlurb)) return controller.RequeueWithError(err) } - if allIntegrationTestScenarios != nil { - tempGroupSnapshot := gitops.PrepareTempGroupSnapshot(a.application, a.snapshot) - filterIntegrationTestScenarios := gitops.FilterIntegrationTestScenariosWithContext(allIntegrationTestScenarios, tempGroupSnapshot) - - a.logger.Info( - fmt.Sprintf("Found %d IntegrationTestScenarios for application", len(*filterIntegrationTestScenarios)), - "Application.Name", a.application.Name, - "IntegrationTestScenarios", len(*filterIntegrationTestScenarios)) - if len(*filterIntegrationTestScenarios) > 0 { - isErrorRecoverable, err := a.ReportGroupSnapshotCreationStatus(a.snapshot, filterIntegrationTestScenarios, integrationTestStatus, gitops.ComponentNameForGroupSnapshot) - - if err != nil { - a.logger.Error(err, "failed to report group snapshot createion status to git provider from component snapshot", - "snapshot.Namespace", a.snapshot.Namespace, "snapshot.Name", a.snapshot.Name, "isErrorRecoverable", isErrorRecoverable) - if helpers.IsObjectYoungerThanThreshold(a.snapshot, SnapshotRetryTimeout) && isErrorRecoverable { - return controller.RequeueWithError(err) - } else { - return controller.ContinueProcessing() - } - } - if err = gitops.AnnotateSnapshot(a.context, a.snapshot, gitops.PRGroupCreationAnnotation, gitops.GroupSnapshotCreationFailureReported, a.client); err != nil { - a.logger.Error(err, fmt.Sprintf("failed to write group snapshot creation status to annotation %s", gitops.PRGroupCreationAnnotation)) - return controller.RequeueWithError(fmt.Errorf("failed to write group snapshot creation status to annotation %s: %w", gitops.PRGroupCreationAnnotation, err)) - } + } + + if allIntegrationTestScenarios == nil { + return controller.ContinueProcessing() + } + + filterIntegrationTestScenarios := gitops.FilterIntegrationTestScenariosWithContext(allIntegrationTestScenarios, tempGroupSnapshot) + + a.logger.Info( + fmt.Sprintf("Found %d IntegrationTestScenarios for %s", len(*filterIntegrationTestScenarios), resourceBlurb), + "IntegrationTestScenarios", len(*filterIntegrationTestScenarios)) + if len(*filterIntegrationTestScenarios) > 0 { + isErrorRecoverable, err := a.ReportGroupSnapshotCreationStatus(a.snapshot, filterIntegrationTestScenarios, integrationTestStatus, groupName) + + if err != nil { + a.logger.Error(err, "failed to report group snapshot creation status to git provider from component snapshot", + "snapshot.Namespace", a.snapshot.Namespace, "snapshot.Name", a.snapshot.Name, "isErrorRecoverable", isErrorRecoverable) + if helpers.IsObjectYoungerThanThreshold(a.snapshot, SnapshotRetryTimeout) && isErrorRecoverable { + return controller.RequeueWithError(err) + } else { + return controller.ContinueProcessing() } } + if err = gitops.AnnotateSnapshot(a.context, a.snapshot, gitops.PRGroupCreationAnnotation, gitops.GroupSnapshotCreationFailureReported, a.client); err != nil { + a.logger.Error(err, fmt.Sprintf("failed to write group snapshot creation status to annotation %s", gitops.PRGroupCreationAnnotation)) + return controller.RequeueWithError(fmt.Errorf("failed to write group snapshot creation status to annotation %s: %w", gitops.PRGroupCreationAnnotation, err)) + } } return controller.ContinueProcessing() } @@ -476,7 +494,11 @@ func (a *Adapter) iterateIntegrationTestStatusDetailsInStatusReport(reporter sta // set componentName to component name of component snapshot or pr group name of group snapshot when reporting status to git provider componentNameOrPrGroup := "" if gitops.IsGroupSnapshot(testedSnapshot) { - componentNameOrPrGroup = gitops.ComponentNameForGroupSnapshot + if a.application != nil { + componentNameOrPrGroup = gitops.ComponentNameForGroupSnapshot + } else { + componentNameOrPrGroup = fmt.Sprintf("%s %s", gitops.ComponentNameForGroupSnapshot, a.componentGroup.Name) + } } else if gitops.IsComponentSnapshot(testedSnapshot) { componentNameOrPrGroup = testedSnapshot.Labels[gitops.SnapshotComponentLabel] } else { diff --git a/internal/controller/statusreport/statusreport_adapter_test.go b/internal/controller/statusreport/statusreport_adapter_test.go index 387d6f5e2b..ecc69cc2d3 100644 --- a/internal/controller/statusreport/statusreport_adapter_test.go +++ b/internal/controller/statusreport/statusreport_adapter_test.go @@ -1134,11 +1134,21 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { }) When("EnsureGroupSnapshotCreationStatusReportedToGitProvider is called with ComponentGroup adapter", func() { - var cgSnapshot *applicationapiv1alpha1.Snapshot var hasCompGroup *v1beta2.ComponentGroup BeforeEach(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) hasCompGroup = &v1beta2.ComponentGroup{ ObjectMeta: metav1.ObjectMeta{ @@ -1147,38 +1157,45 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { }, } - cgSnapshot = &applicationapiv1alpha1.Snapshot{ - ObjectMeta: metav1.ObjectMeta{ - Name: "cg-snapshot-noop", - Namespace: "default", - Labels: map[string]string{ - gitops.SnapshotTypeLabel: gitops.SnapshotComponentType, - gitops.SnapshotComponentLabel: "component-sample", - }, - Annotations: map[string]string{ - gitops.PRGroupCreationAnnotation: gitops.FailedToCreateGroupSnapshotMsg + " some error", - }, + hasPRSnapshot.Spec.Application = "" + hasPRSnapshot.Spec.ComponentGroup = hasCompGroup.Name + hasPRSnapshot.Labels[gitops.ComponentGroupNameLabel] = hasCompGroup.Name + Expect(k8sClient.Update(ctx, hasPRSnapshot)).Should(Succeed()) + + adapter = NewAdapter(ctx, hasPRSnapshot, hasCompGroup, log, loader.NewMockLoader(), k8sClient) + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ComponentGroupContextKey, + Resource: hasCompGroup, }, - Spec: applicationapiv1alpha1.SnapshotSpec{ - ComponentGroup: hasCompGroup.Name, - Components: []applicationapiv1alpha1.SnapshotComponent{ - { - Name: "component-sample", - ContainerImage: SampleImage, - }, - }, + { + ContextKey: loader.SnapshotContextKey, + Resource: hasPRSnapshot, }, - } - - log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} - adapter = NewAdapter(ctx, cgSnapshot, hasCompGroup, log, loader.NewMockLoader(), k8sClient) + { + ContextKey: loader.RequiredIntegrationTestScenariosForSnapshotContextKey, + Resource: []v1beta2.IntegrationTestScenario{*integrationTestScenario}, + }, + { + ContextKey: loader.AllIntegrationTestScenariosForComponentGroupContextKey, + Resource: []v1beta2.IntegrationTestScenario{*integrationTestScenario}, + }, + { + ContextKey: loader.AllIntegrationTestScenariosForComponentGroupsContextKey, + Resource: []v1beta2.IntegrationTestScenario{*integrationTestScenario}, + }, + }) + adapter.status = mockStatus + Expect(reflect.TypeOf(adapter)).To(Equal(reflect.TypeOf(&Adapter{}))) }) - It("should be a no-op for ComponentGroup path (deferred to STONEINTG-1519)", func() { + It("ensure group snapshot create failure is reported to git provider", func() { result, err := adapter.EnsureGroupSnapshotCreationStatusReportedToGitProvider() Expect(result.CancelRequest).To(BeFalse()) Expect(result.RequeueRequest).To(BeFalse()) - Expect(err).ToNot(HaveOccurred()) + Expect(buf.String()).Should(ContainSubstring("Successfully report group snapshot creation failure")) + Expect(buf.String()).Should(ContainSubstring("Successfully updated the test.appstudio.openshift.io/create-groupsnapshot-status")) + Expect(err).Should(Succeed()) }) }) }) diff --git a/loader/loader.go b/loader/loader.go index fd786e983c..839ff4d726 100644 --- a/loader/loader.go +++ b/loader/loader.go @@ -43,6 +43,7 @@ import ( type ObjectLoader interface { GetReleasesWithSnapshot(ctx context.Context, c client.Client, snapshot *applicationapiv1alpha1.Snapshot) (*[]releasev1alpha1.Release, error) GetAllApplicationComponents(ctx context.Context, c client.Client, application *applicationapiv1alpha1.Application) (*[]applicationapiv1alpha1.Component, error) + GetAllComponentGroupComponents(ctx context.Context, c client.Client, componentGroup *v1beta2.ComponentGroup) (*[]applicationapiv1alpha1.Component, error) GetApplicationFromSnapshot(ctx context.Context, c client.Client, snapshot *applicationapiv1alpha1.Snapshot) (*applicationapiv1alpha1.Application, error) GetComponentGroupFromSnapshot(ctx context.Context, c client.Client, snapshot *applicationapiv1alpha1.Snapshot) (*v1beta2.ComponentGroup, error) GetComponentFromSnapshot(ctx context.Context, c client.Client, snapshot *applicationapiv1alpha1.Snapshot) (*applicationapiv1alpha1.Component, error) @@ -71,12 +72,13 @@ type ObjectLoader interface { GetAllTaskRunsWithMatchingPipelineRunLabel(ctx context.Context, c client.Client, pipelineRun *tektonv1.PipelineRun) (*[]tektonv1.TaskRun, error) GetPipelineRun(ctx context.Context, c client.Client, name, namespace string) (*tektonv1.PipelineRun, error) GetComponent(ctx context.Context, c client.Client, name, namespace string) (*applicationapiv1alpha1.Component, error) - GetMatchingComponentSnapshotsForPRGroupHash(ctx context.Context, c client.Client, nameSpace, prGroupHash, applicationName string) (*[]applicationapiv1alpha1.Snapshot, error) - GetPipelineRunsWithPRGroupHash(ctx context.Context, c client.Client, namespace, prGroupHash, applicationName string) (*[]tektonv1.PipelineRun, error) - GetMatchingComponentSnapshotsForComponentAndPRGroupHash(ctx context.Context, c client.Client, snapshot, componentName, prGroupHash, applicationName string) (*[]applicationapiv1alpha1.Snapshot, error) + GetMatchingComponentSnapshotsForPRGroupHash(ctx context.Context, c client.Client, nameSpace, prGroupHash, ownerName string, ownerLabel string) (*[]applicationapiv1alpha1.Snapshot, error) + GetPipelineRunsWithPRGroupHash(ctx context.Context, c client.Client, namespace, prGroupHash string) (*[]tektonv1.PipelineRun, error) + GetPipelineRunsWithPRGroupHashForApplication(ctx context.Context, c client.Client, namespace, prGroupHash, applicationName string) (*[]tektonv1.PipelineRun, error) + GetMatchingComponentSnapshotsForComponentAndPRGroupHash(ctx context.Context, c client.Client, snapshot, componentName, prGroupHash, ownerName string, ownerLabel string) (*[]applicationapiv1alpha1.Snapshot, error) GetAllIntegrationPipelineRunsForSnapshot(ctx context.Context, adapterClient client.Client, snapshot *applicationapiv1alpha1.Snapshot) ([]tektonv1.PipelineRun, error) - GetComponentsFromSnapshotForPRGroup(ctx context.Context, c client.Client, namespace, prGroup, prGroupHash, applicationName string) ([]string, error) - GetMatchingGroupSnapshotsForPRGroupHash(ctx context.Context, c client.Client, namespace, prGroupHash, applicationName string) (*[]applicationapiv1alpha1.Snapshot, error) + GetComponentsFromSnapshotForPRGroup(ctx context.Context, c client.Client, namespace, prGroupHash, ownerName string, ownerLabel string) ([]string, error) + GetMatchingGroupSnapshotsForPRGroupHash(ctx context.Context, c client.Client, namespace, prGroupHash, ownerName string, ownerLabel string) (*[]applicationapiv1alpha1.Snapshot, error) GetResolutionRequest(ctx context.Context, c client.Client, namespace, name string) (resolutionv1beta1.ResolutionRequest, error) GetPRComponentSnapshotsForComponentApplication(ctx context.Context, c client.Client, namespace, applicationName, componentName, prNumber string) (*[]applicationapiv1alpha1.Snapshot, error) GetPRComponentSnapshotsForComponent(ctx context.Context, c client.Client, componentGroupNames []string, namespace, componentName, prNumber string) (*[]applicationapiv1alpha1.Snapshot, error) @@ -123,6 +125,26 @@ func (l *loader) GetAllApplicationComponents(ctx context.Context, c client.Clien return &applicationComponents.Items, nil } +func (l *loader) GetAllComponentGroupComponents(ctx context.Context, c client.Client, componentGroup *v1beta2.ComponentGroup) (*[]applicationapiv1alpha1.Component, error) { + componentGroupComponents := []applicationapiv1alpha1.Component{} + + for _, groupComponent := range componentGroup.Spec.Components { + component := &applicationapiv1alpha1.Component{} + err := c.Get(ctx, types.NamespacedName{ + Namespace: componentGroup.Namespace, + Name: groupComponent.Name, + }, component) + + if err != nil { + return nil, err + } + + componentGroupComponents = append(componentGroupComponents, *component) + } + + return &componentGroupComponents, nil +} + // GetApplicationFromSnapshot loads from the cluster the Application referenced in the given Snapshot. // If the Snapshot doesn't specify an Component or this is not found in the cluster, an error will be returned. // TODO: delete when we get rid of application model @@ -608,7 +630,6 @@ func (l *loader) GetAllSnapshotsForPR(ctx context.Context, c client.Client, obje // PipelineAsCodePullRequestAnnotation is also a label // Only snapshots with IntegrationWorkflowAnnotation set to "pull-request" are returned, // so on-push snapshots that carry the same PR label are excluded. -// TODO: make this function take ObjectMeta rather than application func (l *loader) GetAllPullSnapshotsForPR(ctx context.Context, c client.Client, object metav1.ObjectMeta, componentName, pullRequest string) (*[]applicationapiv1alpha1.Snapshot, error) { snapshots := &applicationapiv1alpha1.SnapshotList{} opts := []client.ListOption{ @@ -665,7 +686,36 @@ func (l *loader) GetComponent(ctx context.Context, c client.Client, name, namesp } // GetPipelineRunsWithPRGroupHash gets the build pipelineRun with the given pr group hash string and the same namespace with the given snapshot -func (l *loader) GetPipelineRunsWithPRGroupHash(ctx context.Context, adapterClient client.Client, namespace, prGroupHash, applicationName string) (*[]tektonv1.PipelineRun, error) { +func (l *loader) GetPipelineRunsWithPRGroupHash(ctx context.Context, adapterClient client.Client, namespace, prGroupHash string) (*[]tektonv1.PipelineRun, error) { + buildPipelineRuns := &tektonv1.PipelineRunList{} + + prGroupLabelRequirement, err := labels.NewRequirement("test.appstudio.openshift.io/pr-group-sha", selection.In, []string{prGroupHash}) + if err != nil { + return nil, err + } + plrTypeLabelRequirement, err := labels.NewRequirement("pipelines.appstudio.openshift.io/type", selection.In, []string{"build"}) + if err != nil { + return nil, err + } + + labelSelector := labels.NewSelector(). + Add(*prGroupLabelRequirement). + Add(*plrTypeLabelRequirement) + + opts := &client.ListOptions{ + Namespace: namespace, + LabelSelector: labelSelector, + } + + err = adapterClient.List(ctx, buildPipelineRuns, opts) + if err != nil { + return nil, err + } + return &buildPipelineRuns.Items, nil +} + +// GetPipelineRunsWithPRGroupHashForApplication gets the build pipelineRun with the given pr group hash string, application name and the same namespace with the given snapshot +func (l *loader) GetPipelineRunsWithPRGroupHashForApplication(ctx context.Context, adapterClient client.Client, namespace, prGroupHash, applicationName string) (*[]tektonv1.PipelineRun, error) { buildPipelineRuns := &tektonv1.PipelineRunList{} applicationLabelRequirement, err := labels.NewRequirement("appstudio.openshift.io/application", selection.In, []string{applicationName}) @@ -699,10 +749,10 @@ func (l *loader) GetPipelineRunsWithPRGroupHash(ctx context.Context, adapterClie } // GetMatchingComponentSnapshotsForComponentAndPRGroupHash gets the component snapshot with the given pr group hash string and the the same namespace with the given snapshot -func (l *loader) GetMatchingComponentSnapshotsForComponentAndPRGroupHash(ctx context.Context, c client.Client, namespace, componentName, prGroupHash, applicationName string) (*[]applicationapiv1alpha1.Snapshot, error) { +func (l *loader) GetMatchingComponentSnapshotsForComponentAndPRGroupHash(ctx context.Context, c client.Client, namespace, componentName, prGroupHash, ownerName, ownerLabel string) (*[]applicationapiv1alpha1.Snapshot, error) { snapshots := &applicationapiv1alpha1.SnapshotList{} - applicationLabelRequirement, err := labels.NewRequirement("appstudio.openshift.io/application", selection.In, []string{applicationName}) + ownerLabelRequirement, err := labels.NewRequirement(ownerLabel, selection.In, []string{ownerName}) if err != nil { return nil, err } @@ -720,7 +770,7 @@ func (l *loader) GetMatchingComponentSnapshotsForComponentAndPRGroupHash(ctx con } labelSelector := labels.NewSelector(). - Add(*applicationLabelRequirement). + Add(*ownerLabelRequirement). Add(*componentLabelRequirement). Add(*prGroupLabelRequirement). Add(*snapshotTypeLabelRequirement) @@ -737,14 +787,15 @@ func (l *loader) GetMatchingComponentSnapshotsForComponentAndPRGroupHash(ctx con return &snapshots.Items, nil } -// GetMatchingGroupSnapshotsForPRGroupHash gets the group snapshots with the given pr group hash string and the the same namespace -func (l *loader) GetMatchingGroupSnapshotsForPRGroupHash(ctx context.Context, c client.Client, namespace, prGroupHash, applicationName string) (*[]applicationapiv1alpha1.Snapshot, error) { +// GetMatchingGroupSnapshotsForPRGroupHash gets the group snapshots with the given pr group hash string and the same namespace +func (l *loader) GetMatchingGroupSnapshotsForPRGroupHash(ctx context.Context, c client.Client, namespace, prGroupHash, ownerName string, ownerLabel string) (*[]applicationapiv1alpha1.Snapshot, error) { snapshots := &applicationapiv1alpha1.SnapshotList{} - applicationLabelRequirement, err := labels.NewRequirement("appstudio.openshift.io/application", selection.In, []string{applicationName}) + ownerLabelRequirement, err := labels.NewRequirement(ownerLabel, selection.In, []string{ownerName}) if err != nil { return nil, err } + prGroupLabelRequirement, err := labels.NewRequirement("test.appstudio.openshift.io/pr-group-sha", selection.In, []string{prGroupHash}) if err != nil { return nil, err @@ -755,7 +806,7 @@ func (l *loader) GetMatchingGroupSnapshotsForPRGroupHash(ctx context.Context, c } labelSelector := labels.NewSelector(). - Add(*applicationLabelRequirement). + Add(*ownerLabelRequirement). Add(*prGroupLabelRequirement). Add(*snapshotTypeLabelRequirement) @@ -771,11 +822,11 @@ func (l *loader) GetMatchingGroupSnapshotsForPRGroupHash(ctx context.Context, c return &snapshots.Items, nil } -// GetMatchingComponentSnapshotsForPRGroupHash gets the component snapshot with the given pr group hash string and the the same namespace with the given snapshot -func (l *loader) GetMatchingComponentSnapshotsForPRGroupHash(ctx context.Context, c client.Client, namespace, prGroupHash, applicationName string) (*[]applicationapiv1alpha1.Snapshot, error) { +// GetMatchingComponentSnapshotsForPRGroupHash gets the component snapshot with the given pr group hash string and the same namespace with the given snapshot +func (l *loader) GetMatchingComponentSnapshotsForPRGroupHash(ctx context.Context, c client.Client, namespace, prGroupHash, ownerName string, ownerLabel string) (*[]applicationapiv1alpha1.Snapshot, error) { snapshots := &applicationapiv1alpha1.SnapshotList{} - applicationLabelRequirement, err := labels.NewRequirement("appstudio.openshift.io/application", selection.In, []string{applicationName}) + ownerLabelRequirement, err := labels.NewRequirement(ownerLabel, selection.In, []string{ownerName}) if err != nil { return nil, err } @@ -789,7 +840,7 @@ func (l *loader) GetMatchingComponentSnapshotsForPRGroupHash(ctx context.Context } labelSelector := labels.NewSelector(). - Add(*applicationLabelRequirement). + Add(*ownerLabelRequirement). Add(*prGroupLabelRequirement). Add(*snapshotTypeLabelRequirement) @@ -824,8 +875,8 @@ func (l *loader) GetAllIntegrationPipelineRunsForSnapshot(ctx context.Context, a } // GetComponentsFromSnapshotForPRGroup returns the component names affected by the given pr group hash -func (l *loader) GetComponentsFromSnapshotForPRGroup(ctx context.Context, client client.Client, namespace, prGroup, prGroupHash, applicationName string) ([]string, error) { - snapshots, err := l.GetMatchingComponentSnapshotsForPRGroupHash(ctx, client, namespace, prGroupHash, applicationName) +func (l *loader) GetComponentsFromSnapshotForPRGroup(ctx context.Context, client client.Client, namespace, prGroupHash, ownerName string, ownerLabel string) ([]string, error) { + snapshots, err := l.GetMatchingComponentSnapshotsForPRGroupHash(ctx, client, namespace, prGroupHash, ownerName, ownerLabel) if err != nil { return nil, err } diff --git a/loader/loader_mock.go b/loader/loader_mock.go index 7414d87a96..77aadf45bc 100644 --- a/loader/loader_mock.go +++ b/loader/loader_mock.go @@ -74,6 +74,7 @@ const ( ComponentGroupsContextKey RequiredIntegrationTestScenariosForSnapshotContextKey GetPushComponentSnapshotsForComponentContextKey + ComponentGroupComponentsContextKey ) func NewMockLoader() ObjectLoader { @@ -100,6 +101,14 @@ func (l *mockLoader) GetAllApplicationComponents(ctx context.Context, c client.C return &components, err } +func (l *mockLoader) GetAllComponentGroupComponents(ctx context.Context, c client.Client, componentGroup *v1beta2.ComponentGroup) (*[]applicationapiv1alpha1.Component, error) { + if ctx.Value(ComponentGroupComponentsContextKey) == nil { + return l.loader.GetAllComponentGroupComponents(ctx, c, componentGroup) + } + components, err := toolkit.GetMockedResourceAndErrorFromContext(ctx, ComponentGroupComponentsContextKey, []applicationapiv1alpha1.Component{}) + return &components, err +} + // GetApplicationFromSnapshot returns the resource and error passed as values of the context. func (l *mockLoader) GetApplicationFromSnapshot(ctx context.Context, c client.Client, snapshot *applicationapiv1alpha1.Snapshot) (*applicationapiv1alpha1.Application, error) { if ctx.Value(ApplicationContextKey) == nil { @@ -322,35 +331,44 @@ func (l *mockLoader) GetComponent(ctx context.Context, c client.Client, name, na } // GetPipelineRunsWithPRGroupHash returns the resource and error passed as values of the context. -func (l *mockLoader) GetPipelineRunsWithPRGroupHash(ctx context.Context, c client.Client, namespace, prGroupHash, applicationName string) (*[]tektonv1.PipelineRun, error) { +func (l *mockLoader) GetPipelineRunsWithPRGroupHash(ctx context.Context, c client.Client, namespace, prGroupHash string) (*[]tektonv1.PipelineRun, error) { + if ctx.Value(GetBuildPLRContextKey) == nil { + return l.loader.GetPipelineRunsWithPRGroupHash(ctx, c, namespace, prGroupHash) + } + pipelineRuns, err := toolkit.GetMockedResourceAndErrorFromContext(ctx, GetBuildPLRContextKey, []tektonv1.PipelineRun{}) + return &pipelineRuns, err +} + +// GetPipelineRunsWithPRGroupHashForApplication returns the resource and error passed as values of the context. +func (l *mockLoader) GetPipelineRunsWithPRGroupHashForApplication(ctx context.Context, c client.Client, namespace, prGroupHash, applicationName string) (*[]tektonv1.PipelineRun, error) { if ctx.Value(GetBuildPLRContextKey) == nil { - return l.loader.GetPipelineRunsWithPRGroupHash(ctx, c, namespace, prGroupHash, applicationName) + return l.loader.GetPipelineRunsWithPRGroupHashForApplication(ctx, c, namespace, prGroupHash, applicationName) } pipelineRuns, err := toolkit.GetMockedResourceAndErrorFromContext(ctx, GetBuildPLRContextKey, []tektonv1.PipelineRun{}) return &pipelineRuns, err } // GetMatchingComponentSnapshotsForComponentAndPRGroupHash returns the resource and error passed as values of the context -func (l *mockLoader) GetMatchingComponentSnapshotsForComponentAndPRGroupHash(ctx context.Context, c client.Client, namespace, componentName, prGroupHash, applicationName string) (*[]applicationapiv1alpha1.Snapshot, error) { +func (l *mockLoader) GetMatchingComponentSnapshotsForComponentAndPRGroupHash(ctx context.Context, c client.Client, namespace, componentName, prGroupHash, ownerName, ownerLabel string) (*[]applicationapiv1alpha1.Snapshot, error) { if ctx.Value(GetComponentSnapshotsKey) == nil { - return l.loader.GetMatchingComponentSnapshotsForComponentAndPRGroupHash(ctx, c, namespace, componentName, prGroupHash, applicationName) + return l.loader.GetMatchingComponentSnapshotsForComponentAndPRGroupHash(ctx, c, namespace, componentName, prGroupHash, ownerName, ownerLabel) } snapshots, err := toolkit.GetMockedResourceAndErrorFromContext(ctx, GetComponentSnapshotsKey, []applicationapiv1alpha1.Snapshot{}) return &snapshots, err } // GetMatchingComponentSnapshotsForPRGroupHash returns the resource and error passed as values of the context -func (l *mockLoader) GetMatchingComponentSnapshotsForPRGroupHash(ctx context.Context, c client.Client, nameSpace, prGroupHash, applicationName string) (*[]applicationapiv1alpha1.Snapshot, error) { +func (l *mockLoader) GetMatchingComponentSnapshotsForPRGroupHash(ctx context.Context, c client.Client, nameSpace, prGroupHash, ownerName, ownerLabel string) (*[]applicationapiv1alpha1.Snapshot, error) { if ctx.Value(GetPRSnapshotsKey) == nil { - return l.loader.GetMatchingComponentSnapshotsForPRGroupHash(ctx, c, nameSpace, prGroupHash, applicationName) + return l.loader.GetMatchingComponentSnapshotsForPRGroupHash(ctx, c, nameSpace, prGroupHash, ownerName, ownerLabel) } snapshots, err := toolkit.GetMockedResourceAndErrorFromContext(ctx, GetPRSnapshotsKey, []applicationapiv1alpha1.Snapshot{}) return &snapshots, err } -func (l *mockLoader) GetMatchingGroupSnapshotsForPRGroupHash(ctx context.Context, c client.Client, nameSpace, prGroupHash, applicationName string) (*[]applicationapiv1alpha1.Snapshot, error) { +func (l *mockLoader) GetMatchingGroupSnapshotsForPRGroupHash(ctx context.Context, c client.Client, nameSpace, prGroupHash, ownerName, ownerLabel string) (*[]applicationapiv1alpha1.Snapshot, error) { if ctx.Value(GetGroupSnapshotsKey) == nil { - return l.loader.GetMatchingGroupSnapshotsForPRGroupHash(ctx, c, nameSpace, prGroupHash, applicationName) + return l.loader.GetMatchingGroupSnapshotsForPRGroupHash(ctx, c, nameSpace, prGroupHash, ownerName, ownerLabel) } snapshots, err := toolkit.GetMockedResourceAndErrorFromContext(ctx, GetGroupSnapshotsKey, []applicationapiv1alpha1.Snapshot{}) return &snapshots, err @@ -365,9 +383,9 @@ func (l *mockLoader) GetAllIntegrationPipelineRunsForSnapshot(ctx context.Contex return pipelineRuns, err } -func (l *mockLoader) GetComponentsFromSnapshotForPRGroup(ctx context.Context, c client.Client, namespace, prGroup, prGroupHash, applicationName string) ([]string, error) { +func (l *mockLoader) GetComponentsFromSnapshotForPRGroup(ctx context.Context, c client.Client, namespace, prGroupHash, ownerName, ownerLabel string) ([]string, error) { if ctx.Value(GetComponentsFromSnapshotForPRGroupKey) == nil { - return l.loader.GetComponentsFromSnapshotForPRGroup(ctx, c, namespace, prGroup, prGroupHash, applicationName) + return l.loader.GetComponentsFromSnapshotForPRGroup(ctx, c, namespace, prGroupHash, ownerName, ownerLabel) } components, err := toolkit.GetMockedResourceAndErrorFromContext(ctx, GetComponentsFromSnapshotForPRGroupKey, []string{}) return components, err diff --git a/loader/loader_mock_test.go b/loader/loader_mock_test.go index 1cd40d68d0..03480c122d 100644 --- a/loader/loader_mock_test.go +++ b/loader/loader_mock_test.go @@ -52,7 +52,7 @@ var _ = Describe("Release Adapter", Ordered, func() { }) }) - Context("When calling GetAllApplicationComponents", func() { + Context("When calling GetAllApplicationComponents [APPLICATION]", func() { It("returns resource and error from the context", func() { applicationComponents := []applicationapiv1alpha1.Component{} mockContext := toolkit.GetMockedContext(ctx, []toolkit.MockData{ @@ -67,6 +67,21 @@ var _ = Describe("Release Adapter", Ordered, func() { }) }) + Context("When calling GetAllComponentGroupComponents", func() { + It("returns resource and error from the context", func() { + groupComponents := []applicationapiv1alpha1.Component{} + mockContext := toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: ComponentGroupComponentsContextKey, + Resource: groupComponents, + }, + }) + resource, err := loader.GetAllComponentGroupComponents(mockContext, nil, nil) + Expect(resource).To(Equal(&groupComponents)) + Expect(err).ToNot(HaveOccurred()) + }) + }) + Context("When calling GetApplicationFromSnapshot [APPLICATION]", func() { It("returns resource and error from the context", func() { application := &applicationapiv1alpha1.Application{} @@ -361,7 +376,22 @@ var _ = Describe("Release Adapter", Ordered, func() { Resource: plrs, }, }) - resource, err := loader.GetPipelineRunsWithPRGroupHash(mockContext, nil, "", "", "") + resource, err := loader.GetPipelineRunsWithPRGroupHash(mockContext, nil, "", "") + Expect(resource).To(Equal(&plrs)) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + Context("When calling GetPipelineRunsWithPRGroupHashForApplication", func() { + It("returns resource and error from the context", func() { + plrs := []tektonv1.PipelineRun{} + mockContext := toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: GetBuildPLRContextKey, + Resource: plrs, + }, + }) + resource, err := loader.GetPipelineRunsWithPRGroupHashForApplication(mockContext, nil, "", "", "") Expect(resource).To(Equal(&plrs)) Expect(err).ToNot(HaveOccurred()) }) @@ -376,7 +406,7 @@ var _ = Describe("Release Adapter", Ordered, func() { Resource: snapshots, }, }) - resource, err := loader.GetMatchingComponentSnapshotsForComponentAndPRGroupHash(mockContext, nil, "", "", "", "") + resource, err := loader.GetMatchingComponentSnapshotsForComponentAndPRGroupHash(mockContext, nil, "", "", "", "", "") Expect(resource).To(Equal(&snapshots)) Expect(err).ToNot(HaveOccurred()) }) @@ -391,7 +421,7 @@ var _ = Describe("Release Adapter", Ordered, func() { Resource: snapshots, }, }) - resource, err := loader.GetMatchingComponentSnapshotsForPRGroupHash(mockContext, nil, "", "", "") + resource, err := loader.GetMatchingComponentSnapshotsForPRGroupHash(mockContext, nil, "", "", "", "") Expect(resource).To(Equal(&snapshots)) Expect(err).ToNot(HaveOccurred()) }) @@ -421,7 +451,7 @@ var _ = Describe("Release Adapter", Ordered, func() { Resource: snapshots, }, }) - resource, err := loader.GetMatchingGroupSnapshotsForPRGroupHash(mockContext, nil, "", "", "") + resource, err := loader.GetMatchingGroupSnapshotsForPRGroupHash(mockContext, nil, "", "", "", "") Expect(resource).To(Equal(&snapshots)) Expect(err).ToNot(HaveOccurred()) }) diff --git a/loader/loader_test.go b/loader/loader_test.go index 0b68a8a69c..0b9f606566 100644 --- a/loader/loader_test.go +++ b/loader/loader_test.go @@ -61,6 +61,7 @@ var _ = Describe("Loader", Ordered, func() { const ( SampleRepoLink = "https://github.com/devfile-samples/devfile-sample-java-springboot-basic" applicationName = "application-sample" + componentGroupName = "component-group-sample" snapshotName = "snapshot-sample" cgSnapshotName = "componentgroup-snapshot-sample" groupSnapshotName = "group-snapshot-sample" @@ -670,6 +671,12 @@ var _ = Describe("Loader", Ordered, func() { Expect(applicationComponents).NotTo(BeNil()) }) + It("ensures the ComponentGroup Components can be found ", func() { + componentGroupComponents, err := loader.GetAllComponentGroupComponents(ctx, k8sClient, hasComponentGroup1) + Expect(err).ToNot(HaveOccurred()) + Expect(componentGroupComponents).NotTo(BeNil()) + }) + It("ensures we can get an Application from a Snapshot [APPLICATION]", func() { app, err := loader.GetApplicationFromSnapshot(ctx, k8sClient, hasSnapshot) Expect(err).ToNot(HaveOccurred()) @@ -943,7 +950,7 @@ var _ = Describe("Loader", Ordered, func() { }) It("ensures the merge queue build pipeline runs can be found", func() { - groupShaPipelineRuns, err := loader.GetPipelineRunsWithPRGroupHash(ctx, k8sClient, mergeQueueBuildPipelineRun.Namespace, mergeQueueHash, applicationName) + groupShaPipelineRuns, err := loader.GetPipelineRunsWithPRGroupHash(ctx, k8sClient, mergeQueueBuildPipelineRun.Namespace, mergeQueueHash) Expect(err).ToNot(HaveOccurred()) Expect(groupShaPipelineRuns).ToNot(BeNil()) Expect(*groupShaPipelineRuns).To(HaveLen(1)) @@ -1004,7 +1011,7 @@ var _ = Describe("Loader", Ordered, func() { }) It("ensures all merge queue snapshots can be found for a given PR group hash", func() { - groupShaComponentSnapshots, err := loader.GetMatchingComponentSnapshotsForPRGroupHash(ctx, k8sClient, mergeQueueSnapshot.Namespace, mergeQueueHash, applicationName) + groupShaComponentSnapshots, err := loader.GetMatchingComponentSnapshotsForPRGroupHash(ctx, k8sClient, mergeQueueSnapshot.Namespace, mergeQueueHash, applicationName, gitops.ApplicationNameLabel) Expect(err).ToNot(HaveOccurred()) Expect(groupShaComponentSnapshots).ToNot(BeNil()) Expect(*groupShaComponentSnapshots).To(HaveLen(3)) @@ -1014,7 +1021,7 @@ var _ = Describe("Loader", Ordered, func() { }) It("ensures all merge queue component snapshots can be found for a given component and PR group hash", func() { - groupShaComponentSnapshots, err := loader.GetMatchingComponentSnapshotsForComponentAndPRGroupHash(ctx, k8sClient, mergeQueueSnapshot.Namespace, hasComp.Name, mergeQueueHash, applicationName) + groupShaComponentSnapshots, err := loader.GetMatchingComponentSnapshotsForComponentAndPRGroupHash(ctx, k8sClient, mergeQueueSnapshot.Namespace, hasComp.Name, mergeQueueHash, applicationName, gitops.ApplicationNameLabel) Expect(err).ToNot(HaveOccurred()) Expect(groupShaComponentSnapshots).ToNot(BeNil()) Expect(*groupShaComponentSnapshots).To(HaveLen(2)) @@ -1025,7 +1032,7 @@ var _ = Describe("Loader", Ordered, func() { }) It("ensures all merge queue group snapshots can be found for a given component and PR group hash", func() { - groupShaComponentSnapshots, err := loader.GetMatchingGroupSnapshotsForPRGroupHash(ctx, k8sClient, mergeQueueSnapshot.Namespace, mergeQueueHash, applicationName) + groupShaComponentSnapshots, err := loader.GetMatchingGroupSnapshotsForPRGroupHash(ctx, k8sClient, mergeQueueSnapshot.Namespace, mergeQueueHash, applicationName, gitops.ApplicationNameLabel) Expect(err).ToNot(HaveOccurred()) Expect(groupShaComponentSnapshots).ToNot(BeNil()) Expect(*groupShaComponentSnapshots).To(HaveLen(1)) @@ -1219,7 +1226,15 @@ var _ = Describe("Loader", Ordered, func() { }) It("Can get build plr with pr group hash", func() { - fetchedBuildPLRs, err := loader.GetPipelineRunsWithPRGroupHash(ctx, k8sClient, hasSnapshot.Namespace, prGroupSha, hasApp.Name) + fetchedBuildPLRs, err := loader.GetPipelineRunsWithPRGroupHash(ctx, k8sClient, hasSnapshot.Namespace, prGroupSha) + Expect(err).To(Succeed()) + Expect((*fetchedBuildPLRs)[0].Name).To(Equal(buildPipelineRun.Name)) + Expect((*fetchedBuildPLRs)[0].Namespace).To(Equal(buildPipelineRun.Namespace)) + Expect((*fetchedBuildPLRs)[0].Spec).To(Equal(buildPipelineRun.Spec)) + }) + + It("Can get build plr with pr group hash [APPLICATION]", func() { + fetchedBuildPLRs, err := loader.GetPipelineRunsWithPRGroupHashForApplication(ctx, k8sClient, hasSnapshot.Namespace, prGroupSha, hasApp.Name) Expect(err).To(Succeed()) Expect((*fetchedBuildPLRs)[0].Name).To(Equal(buildPipelineRun.Name)) Expect((*fetchedBuildPLRs)[0].Namespace).To(Equal(buildPipelineRun.Namespace)) @@ -1227,7 +1242,7 @@ var _ = Describe("Loader", Ordered, func() { }) It("Can get matching snapshot for component and pr group hash", func() { - fetchedSnapshots, err := loader.GetMatchingComponentSnapshotsForComponentAndPRGroupHash(ctx, k8sClient, hasSnapshot.Namespace, hasComp.Name, prGroupSha, hasApp.Name) + fetchedSnapshots, err := loader.GetMatchingComponentSnapshotsForComponentAndPRGroupHash(ctx, k8sClient, hasSnapshot.Namespace, hasComp.Name, prGroupSha, hasApp.Name, gitops.ApplicationNameLabel) Expect(err).To(Succeed()) Expect((*fetchedSnapshots)[0].Name).To(Equal(hasSnapshot.Name)) Expect((*fetchedSnapshots)[0].Namespace).To(Equal(hasSnapshot.Namespace)) @@ -1235,7 +1250,7 @@ var _ = Describe("Loader", Ordered, func() { }) It("Can get matching snapshot for pr group hash", func() { - fetchedSnapshots, err := loader.GetMatchingComponentSnapshotsForPRGroupHash(ctx, k8sClient, "", prGroupSha, hasApp.Name) + fetchedSnapshots, err := loader.GetMatchingComponentSnapshotsForPRGroupHash(ctx, k8sClient, "", prGroupSha, hasApp.Name, gitops.ApplicationNameLabel) Expect(err).To(Succeed()) Expect((*fetchedSnapshots)[0].Name).To(Equal(hasSnapshot.Name)) Expect((*fetchedSnapshots)[0].Namespace).To(Equal(hasSnapshot.Namespace)) @@ -1243,7 +1258,7 @@ var _ = Describe("Loader", Ordered, func() { }) It("Can get matching group snapshot for pr group hash", func() { - fetchedSnapshots, err := loader.GetMatchingGroupSnapshotsForPRGroupHash(ctx, k8sClient, "", prGroupSha, hasApp.Name) + fetchedSnapshots, err := loader.GetMatchingGroupSnapshotsForPRGroupHash(ctx, k8sClient, "", prGroupSha, hasApp.Name, gitops.ApplicationNameLabel) Expect(err).To(Succeed()) Expect((*fetchedSnapshots)[0].Name).To(Equal(hasGroupSnapshot.Name)) Expect((*fetchedSnapshots)[0].Namespace).To(Equal(hasGroupSnapshot.Namespace)) @@ -1251,7 +1266,7 @@ var _ = Describe("Loader", Ordered, func() { }) It("Can get matching components from snapshots for pr group hash", func() { - components, err := loader.GetComponentsFromSnapshotForPRGroup(ctx, k8sClient, "", "", prGroupSha, hasApp.Name) + components, err := loader.GetComponentsFromSnapshotForPRGroup(ctx, k8sClient, "", prGroupSha, hasApp.Name, gitops.ApplicationNameLabel) Expect(err).To(Succeed()) Expect((components)[0]).To(Equal(hasComp.Name)) diff --git a/snapshot/create.go b/snapshot/create.go index 82db381530..51cf9c8241 100644 --- a/snapshot/create.go +++ b/snapshot/create.go @@ -154,7 +154,7 @@ func CreateSnapshotWithCollisionHandling(ctx context.Context, client client.Clie // In case the Snapshot can't be created, an error will be returned. func PrepareSnapshot(ctx context.Context, adapterClient client.Client, componentGroup *v1beta2.ComponentGroup, newSnapshotComponent applicationapiv1alpha1.SnapshotComponent, log logr.Logger) (*applicationapiv1alpha1.Snapshot, error) { - snapshotComponents, invalidComponents := getSnapshotComponentsFromGCL(componentGroup, log) + snapshotComponents, invalidComponents := GetSnapshotComponentsFromGCL(componentGroup, log) upsertNewComponentImage(&snapshotComponents, &invalidComponents, newSnapshotComponent, log) if len(snapshotComponents) == 0 { @@ -185,7 +185,7 @@ func PrepareSnapshot(ctx context.Context, adapterClient client.Client, component } // This prevents race conditions if EnsureGCLAlignedWithSpecComponents runs late -func getSnapshotComponentsFromGCL(componentGroup *v1beta2.ComponentGroup, log logr.Logger) ([]applicationapiv1alpha1.SnapshotComponent, []v1beta2.ComponentState) { +func GetSnapshotComponentsFromGCL(componentGroup *v1beta2.ComponentGroup, log logr.Logger) ([]applicationapiv1alpha1.SnapshotComponent, []v1beta2.ComponentState) { var snapshotComponents []applicationapiv1alpha1.SnapshotComponent var invalidComponents []v1beta2.ComponentState @@ -221,7 +221,7 @@ func getSnapshotComponentsFromGCL(componentGroup *v1beta2.ComponentGroup, log lo } // Get ComponentSource for the component which is not built in this pipeline - componentSource := getComponentSourceFromGCLComponent(gclComponent) + componentSource := GetComponentSourceFromGCLComponent(gclComponent) snapshotComponents = append(snapshotComponents, applicationapiv1alpha1.SnapshotComponent{ Name: name, @@ -307,7 +307,7 @@ func NewSnapshot(componentGroup *v1beta2.ComponentGroup, snapshotComponents *[]a return snapshot } -func getComponentSourceFromGCLComponent(gclComponent v1beta2.ComponentState) applicationapiv1alpha1.ComponentSource { +func GetComponentSourceFromGCLComponent(gclComponent v1beta2.ComponentState) applicationapiv1alpha1.ComponentSource { // NOTE: if we need to fall back on data from component CR we can do it in here componentSource := applicationapiv1alpha1.ComponentSource{ ComponentSourceUnion: applicationapiv1alpha1.ComponentSourceUnion{ @@ -346,3 +346,10 @@ func getSnapshotComponentFromBuildPLR(pipelineRun *tektonv1.PipelineRun, compone Source: *componentSource, }, nil } + +// PrepareTempGroupSnapshot will prepare a temp group snapshot used to check the integration test scenario that should be applied to the group snapshot under that componentGroup +func PrepareTempGroupSnapshot(componentGroup *v1beta2.ComponentGroup, snapshot *applicationapiv1alpha1.Snapshot) *applicationapiv1alpha1.Snapshot { + tempGroupSnapshot := NewSnapshot(componentGroup, &[]applicationapiv1alpha1.SnapshotComponent{}) + tempGroupSnapshot, _ = gitops.SetAnnotationAndLabelForGroupSnapshot(tempGroupSnapshot, snapshot, []gitops.ComponentSnapshotInfo{}) + return tempGroupSnapshot +} diff --git a/snapshot/create_test.go b/snapshot/create_test.go index 91c98b836d..4578b79619 100644 --- a/snapshot/create_test.go +++ b/snapshot/create_test.go @@ -537,7 +537,7 @@ var _ = Describe("Snapshot creation functions", Ordered, func() { It("Ensures valid and invalid snapshotComponents can be gathered from the GCL", func() { var buf bytes.Buffer readableLog := buflogr.NewWithBuffer(&buf) - snapshotComponents, invalidComponents := getSnapshotComponentsFromGCL(hasCompGroup, readableLog) + snapshotComponents, invalidComponents := GetSnapshotComponentsFromGCL(hasCompGroup, readableLog) Expect(snapshotComponents).To(HaveLen(1)) Expect(snapshotComponents[0].Name).To(Equal(componentName)) @@ -553,7 +553,7 @@ var _ = Describe("Snapshot creation functions", Ordered, func() { newSnapshotComponent, err := getSnapshotComponentFromBuildPLR(buildPipelineRun, componentName, logger) Expect(err).NotTo(HaveOccurred()) - snapshotComponents, invalidComponents := getSnapshotComponentsFromGCL(hasCompGroup, logger) + snapshotComponents, invalidComponents := GetSnapshotComponentsFromGCL(hasCompGroup, logger) Expect(snapshotComponents).To(HaveLen(1)) Expect(invalidComponents).To(HaveLen(1)) @@ -570,7 +570,7 @@ var _ = Describe("Snapshot creation functions", Ordered, func() { newSnapshotComponent, err := getSnapshotComponentFromBuildPLR(buildPipelineRun, componentName, logger) Expect(err).NotTo(HaveOccurred()) - snapshotComponents, invalidComponents := getSnapshotComponentsFromGCL(hasCompGroup, logger) + snapshotComponents, invalidComponents := GetSnapshotComponentsFromGCL(hasCompGroup, logger) Expect(snapshotComponents).To(HaveLen(1)) Expect(invalidComponents).To(HaveLen(1)) @@ -589,7 +589,7 @@ var _ = Describe("Snapshot creation functions", Ordered, func() { Expect(err).NotTo(HaveOccurred()) newSnapshotComponent.Name = componentName2 - snapshotComponents, invalidComponents := getSnapshotComponentsFromGCL(hasCompGroup, logger) + snapshotComponents, invalidComponents := GetSnapshotComponentsFromGCL(hasCompGroup, logger) Expect(snapshotComponents).To(HaveLen(1)) Expect(invalidComponents).To(HaveLen(1)) @@ -622,4 +622,51 @@ var _ = Describe("Snapshot creation functions", Ordered, func() { Expect(snapshot.Annotations[helpers.CreateSnapshotAnnotationName]).To(Equal("Component(s) 'another-component-sample (version v1)' is(are) not included in snapshot due to missing valid containerImage or git source")) }) }) + + It("ensures NewSnapshot truncates application name if longer than 43 characters", func() { + longGroupName := "this-is-a-very-long-application-name-that-exceeds-43-chars" + longGroup := &v1beta2.ComponentGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: longGroupName, + Namespace: "namespace", + }, + } + snapshotComponents := []applicationapiv1alpha1.SnapshotComponent{ + { + Name: "component-1", + ContainerImage: "registry.io/image1:v1.0.0", + }, + } + + snapshot := NewSnapshot(longGroup, &snapshotComponents) + + // Name should be truncated to 44 characters + "-" + 19 character timestamp + expectedPrefix := longGroupName[:43] + Expect(snapshot.Name).To(HavePrefix(expectedPrefix + "-")) + Expect(snapshot.Name).To(MatchRegexp(`^` + expectedPrefix + `-\d{8}-\d{6}-\d{3}$`)) + Expect(snapshot.Spec.ComponentGroup).To(Equal(longGroupName)) + }) + + It("ensures NewSnapshot does not truncate application name at 43 characters", func() { + exactGroupName := "this-is-application-name-exactly-43-chars" // 43 chars + exactGroup := &v1beta2.ComponentGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: exactGroupName, + Namespace: "namespace", + }, + } + snapshotComponents := []applicationapiv1alpha1.SnapshotComponent{ + { + Name: "component-1", + ContainerImage: "registry.io/image1:v1.0.0", + }, + } + + snapshot := NewSnapshot(exactGroup, &snapshotComponents) + + // Name should be exactAppName + "-" + 19 character timestamp + Expect(snapshot.Name).To(HavePrefix(exactGroupName + "-")) + Expect(snapshot.Name).To(MatchRegexp(`^` + exactGroupName + `-\d{8}-\d{6}-\d{3}$`)) + Expect(snapshot.Spec.ComponentGroup).To(Equal(exactGroupName)) + }) }) diff --git a/snapshot/gcl.go b/snapshot/gcl.go index bb2e3bf753..844fd389b6 100644 --- a/snapshot/gcl.go +++ b/snapshot/gcl.go @@ -163,3 +163,17 @@ func UpdateGCLForOverrideSnapshot(ctx context.Context, adapterClient client.Clie return err } + +func FetchSnapshotComponentFromGCL(componentName string, snapshotComponentsFromGCL []applicationapiv1alpha1.SnapshotComponent, invalidComponents []v1beta2.ComponentState) (*applicationapiv1alpha1.SnapshotComponent, error) { + for _, snapshotComponentFromGCL := range snapshotComponentsFromGCL { + if snapshotComponentFromGCL.Name == componentName { + return &snapshotComponentFromGCL, nil + } + } + for _, invalidComponent := range invalidComponents { + if invalidComponent.Name == componentName { + return nil, fmt.Errorf("component cannot be added to snapshot due to invalid digest in containerImage") + } + } + return nil, nil +} diff --git a/status/status.go b/status/status.go index 1a2c275a90..0ef9b58117 100644 --- a/status/status.go +++ b/status/status.go @@ -430,23 +430,21 @@ func GenerateSummaryForAllScenarios(state intgteststat.IntegrationTestStatus, co return summary, nil } -// function GenerateComponentNameWithPrefix to generate component name "pr group" or "component component-sample" +// GenerateComponentNameWithPrefix to generate component name "pr group" or "component component-sample" // to help search it in comment correctly to avoid component name is searched in pr group report func GenerateComponentNameWithPrefix(componentName string) string { - if componentName == gitops.ComponentNameForGroupSnapshot { - componentName = gitops.ComponentNameForGroupSnapshot - } else { + if !strings.HasPrefix(componentName, gitops.ComponentNameForGroupSnapshot) { componentName = "component " + componentName } return componentName } -// function GenerateTestSummaryPrefixForComponent to generate test summary prefix for component name "pr group" or "component component-sample" +// GenerateTestSummaryPrefixForComponent to generate test summary prefix for component name "pr group" or "component component-sample" // to help search it in comment correctly since all comments are posted together func GenerateTestSummaryPrefixForComponent(componentName string) string { var commentTitle string - if componentName == gitops.ComponentNameForGroupSnapshot { - commentTitle = "Integration test for " + gitops.ComponentNameForGroupSnapshot + if strings.HasPrefix(componentName, gitops.ComponentNameForGroupSnapshot) { + commentTitle = "Integration test for " + componentName } else { commentTitle = "Integration test for component " + componentName }