From 65401a93aff1ce226c09cefab960ce01117fd2ca Mon Sep 17 00:00:00 2001 From: Hongwei Liu Date: Wed, 14 Aug 2024 15:13:15 +0800 Subject: [PATCH] feat(STONEINTG-998): support group snapshot * Create group snapshot for the component snapshots with opened PR and belonging to the same pr group once a component snasphot is created for PR * Set group to snapshot type to group snapshot * Set event-type to pull-request to group snapshot * Copy pr-group annotation and pr-group-sha label from component snasphot to group snapshot Signed-off-by: Hongwei Liu --- docs/snapshot-controller.md | 28 + git/github/github.go | 32 + git/github/github_test.go | 35 +- gitops/snapshot.go | 127 +++- gitops/snapshot_test.go | 215 ++++++- .../buildpipeline/buildpipeline_adapter.go | 10 +- .../controller/snapshot/snapshot_adapter.go | 230 ++++++++ .../snapshot/snapshot_adapter_test.go | 557 +++++++++++++++++- .../snapshot/snapshot_controller.go | 2 + loader/loader.go | 75 +++ loader/loader_mock.go | 20 + loader/loader_mock_test.go | 30 + loader/loader_test.go | 42 +- status/mock_status.go | 47 +- status/reporter_github.go | 6 +- status/reporter_github_test.go | 7 + status/status.go | 141 +++++ tekton/build_pipeline.go | 4 +- tekton/build_pipeline_test.go | 6 +- 19 files changed, 1567 insertions(+), 47 deletions(-) diff --git a/docs/snapshot-controller.md b/docs/snapshot-controller.md index 55bd077b0..28254e6d8 100644 --- a/docs/snapshot-controller.md +++ b/docs/snapshot-controller.md @@ -114,6 +114,34 @@ flowchart TD mark_snapshot_invalid --> continue_processing4 + %%%%%%%%%%%%%%%%%%%%%%% Drawing EnsureGroupSnapshotExist() function + + %% Node definitions + 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) + 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) + create_group_snapshot(Create group snapshot for snasphotComponents) + annotate_component_snapshots_under_prgroupsha(Annotate component snapshots which have
snapshotComponent added to group snapshot) + continue_processing5(Controller continues processing...) + + %% Node connections + predicate ----> |"EnsureGroupSnapshotExist()"|ensure5 + ensure5 --> validate_build_pipelinerun + validate_build_pipelinerun --Yes--> get_component_snapshots_and_sort + validate_build_pipelinerun --No--> annotate_component_snapshot + get_component_snapshots_and_sort --> can_find_snapshotComponent_from_latest_snapshot + can_find_snapshotComponent_from_latest_snapshot --Yes--> add_snapshot_group_snapshot_candidate + can_find_snapshotComponent_from_latest_snapshot --No--> get_snapshotComponent_from_gcl + add_snapshot_to_group_snapshot_candidate --> create_group_snapshot + get_snapshotComponent_from_gcl --> create_group_snapshot + create_group_snapshot --> annotate_component_snapshots_under_prgroupsha + annotate_component_snapshots_under_prgroupsha --> continue_processing5 + annotate_component_snapshot --> continue_processing5 + %% Assigning styles to nodes class predicate Amber; class encountered_error1,encountered_error31,encountered_error32,encountered_error5 Red; diff --git a/git/github/github.go b/git/github/github.go index ebeb69352..65f705829 100644 --- a/git/github/github.go +++ b/git/github/github.go @@ -91,6 +91,11 @@ type RepositoriesService interface { ListStatuses(ctx context.Context, owner, repo, ref string, opts *ghapi.ListOptions) ([]*ghapi.RepoStatus, *ghapi.Response, error) } +// PullRequestsService defines the methods used in the github PullRequests service. +type PullRequestsService interface { + Get(ctx context.Context, owner string, repo string, prID int) (*ghapi.PullRequest, *ghapi.Response, error) +} + // ClientInterface defines the methods that should be implemented by a GitHub client type ClientInterface interface { CreateAppInstallationToken(ctx context.Context, appID int64, installationID int64, privateKey []byte) (string, error) @@ -107,6 +112,7 @@ type ClientInterface interface { CommitStatusExists(res []*ghapi.RepoStatus, commitStatus *CommitStatusAdapter) (bool, error) GetExistingCommentID(comments []*ghapi.IssueComment, snapshotName, scenarioName string) *int64 EditComment(ctx context.Context, owner string, repo string, commentID int64, body string) (int64, error) + GetPullRequest(ctx context.Context, owner string, repo string, prID int) (*ghapi.PullRequest, error) } // Client is an abstraction around the API client. @@ -117,6 +123,7 @@ type Client struct { checks ChecksService issues IssuesService repos RepositoriesService + pulls PullRequestsService } // GetAppsService returns either the default or custom Apps service. @@ -151,6 +158,14 @@ func (c *Client) GetRepositoriesService() RepositoriesService { return c.repos } +// GetPullRequestsService returns either the default or custom PullRequest service. +func (c *Client) GetPullRequestsService() PullRequestsService { + if c.pulls == nil { + return c.gh.PullRequests + } + return c.pulls +} + // ClientOption is used to extend Client with optional parameters. type ClientOption = func(c *Client) @@ -182,6 +197,13 @@ func WithRepositoriesService(svc RepositoriesService) ClientOption { } } +// WithPullRequestsService is an option which allows for overriding the github client's default PullRequests service. +func WithPullRequestsService(svc PullRequestsService) ClientOption { + return func(c *Client) { + c.pulls = svc + } +} + // NewClient constructs a new Client. func NewClient(logger logr.Logger, opts ...ClientOption) *Client { client := Client{ @@ -502,3 +524,13 @@ func (c *Client) CreateCommitStatus(ctx context.Context, owner string, repo stri return *status.ID, nil } + +// GetPullRequest returns pull request according to the owner, repo and pull request number +func (c *Client) GetPullRequest(ctx context.Context, owner string, repo string, prID int) (*ghapi.PullRequest, error) { + pr, _, err := c.GetPullRequestsService().Get(ctx, owner, repo, prID) + if err != nil { + return nil, fmt.Errorf("failed to get pull request for GitHub owner/repo/pull %s/%s/%d: %w", owner, repo, prID, err) + } + + return pr, err +} diff --git a/git/github/github_test.go b/git/github/github_test.go index 7d30c5edf..c4f7ee908 100644 --- a/git/github/github_test.go +++ b/git/github/github_test.go @@ -149,6 +149,20 @@ func (MockRepositoriesService) ListStatuses( return []*ghapi.RepoStatus{repoStatus}, nil, nil } +type MockPullRequestsService struct { + GetPullRequestResult *ghapi.PullRequest +} + +// MockPullRequestsService implements github.PullRequestsService +func (MockPullRequestsService) Get( + ctx context.Context, owner string, repo string, prID int, +) (*ghapi.PullRequest, *ghapi.Response, error) { + var id int64 = 60 + var state = "opened" + GetPullRequestResult := &ghapi.PullRequest{ID: &id, State: &state} + return GetPullRequestResult, nil, nil +} + var _ = Describe("CheckRunAdapter", func() { It("can compute status", func() { adapter := &github.CheckRunAdapter{Conclusion: "success", StartTime: time.Time{}} @@ -165,11 +179,12 @@ var _ = Describe("CheckRunAdapter", func() { var _ = Describe("Client", func() { var ( - client *github.Client - mockAppsSvc MockAppsService - mockChecksSvc MockChecksService - mockIssuesSvc MockIssuesService - mockReposSvc MockRepositoriesService + client *github.Client + mockAppsSvc MockAppsService + mockChecksSvc MockChecksService + mockIssuesSvc MockIssuesService + mockReposSvc MockRepositoriesService + mockPullRequestsSvc MockPullRequestsService ) var checkRunAdapter = &github.CheckRunAdapter{ @@ -202,12 +217,14 @@ var _ = Describe("Client", func() { mockChecksSvc = MockChecksService{} mockIssuesSvc = MockIssuesService{} mockReposSvc = MockRepositoriesService{} + mockPullRequestsSvc = MockPullRequestsService{} client = github.NewClient( logr.Discard(), github.WithAppsService(mockAppsSvc), github.WithChecksService(mockChecksSvc), github.WithIssuesService(mockIssuesSvc), github.WithRepositoriesService(mockReposSvc), + github.WithPullRequestsService(mockPullRequestsSvc), ) }) @@ -223,6 +240,7 @@ var _ = Describe("Client", func() { Expect(client.GetChecksService()).To(Equal(mockChecksSvc)) Expect(client.GetIssuesService()).To(Equal(mockIssuesSvc)) Expect(client.GetRepositoriesService()).To(Equal(mockReposSvc)) + Expect(client.GetPullRequestsService()).To(Equal(mockPullRequestsSvc)) client = github.NewClient(logr.Discard()) client.SetOAuthToken(context.TODO(), "example-token") @@ -230,6 +248,7 @@ var _ = Describe("Client", func() { Expect(client.GetChecksService()).ToNot(Equal(mockChecksSvc)) Expect(client.GetIssuesService()).ToNot(Equal(mockIssuesSvc)) Expect(client.GetRepositoriesService()).ToNot(Equal(mockReposSvc)) + Expect(client.GetPullRequestsService()).ToNot(Equal(mockPullRequestsSvc)) }) It("can create comments", func() { @@ -335,4 +354,10 @@ var _ = Describe("Client", func() { Expect(err).To(BeNil()) Expect(id).To(Equal(int64(1))) }) + + It("can get pull request", func() { + pullRequest, err := client.GetPullRequest(context.TODO(), "", "", 60) + Expect(err).ToNot(HaveOccurred()) + Expect(*pullRequest.State).To(Equal("opened")) + }) }) diff --git a/gitops/snapshot.go b/gitops/snapshot.go index 8528539a1..e89db1484 100644 --- a/gitops/snapshot.go +++ b/gitops/snapshot.go @@ -18,10 +18,12 @@ package gitops import ( "context" + "encoding/json" "errors" "fmt" "github.com/konflux-ci/integration-service/api/v1beta2" "reflect" + "sort" "strconv" "strings" "time" @@ -34,6 +36,7 @@ import ( applicationapiv1alpha1 "github.com/redhat-appstudio/application-api/api/v1alpha1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" @@ -82,6 +85,9 @@ const ( // PRGroupHashLabel contains the pr group name in sha format PRGroupHashLabel = "test.appstudio.openshift.io/pr-group-sha" + // PRGroupCreationAnnotation contains the info of groupsnapshot creation + PRGroupCreationAnnotation = "test.appstudio.openshift.io/create-groupsnapshot-status" + // BuildPipelineRunStartTime contains the start time of build pipelineRun BuildPipelineRunStartTime = "test.appstudio.openshift.io/pipelinerunstarttime" @@ -91,6 +97,9 @@ const ( // BuildPipelineRunFinishTimeLabel contains the build PipelineRun finish time of the Snapshot. BuildPipelineRunFinishTimeLabel = "test.appstudio.openshift.io/pipelinerunfinishtime" + // GroupSnapshotInfoAnnotation contains the component snapshot info included in group snapshot + GroupSnapshotInfoAnnotation = "test.appstudio.openshift.io/group-test-info" + // BuildPipelineRunNameLabel contains the build PipelineRun name BuildPipelineRunNameLabel = AppstudioLabelPrefix + "/build-pipelinerun" @@ -224,6 +233,18 @@ var ( SnapshotComponentLabel = tekton.ComponentNameLabel ) +// ComponentSnapshotInfo contains data about the component snapshots' info in group snapshot +type ComponentSnapshotInfo struct { + // Namespace + Namespace string `json:"namespace"` + // Component name + Component string `json:"component"` + // The build PLR name building the container image triggered by pull request + BuildPipelineRun string `json:"buildPipelineRun"` + // The built component snapshot from build PLR + Snapshot string `json:"snapshot"` +} + // IsSnapshotMarkedAsPassed returns true if snapshot is marked as passed func IsSnapshotMarkedAsPassed(snapshot *applicationapiv1alpha1.Snapshot) bool { return IsSnapshotStatusConditionSet(snapshot, AppStudioTestSucceededCondition, metav1.ConditionTrue, "") @@ -752,7 +773,7 @@ func PrepareSnapshot(ctx context.Context, adapterClient client.Client, applicati return snapshot, nil } -// FindMatchingSnapshot tries to find the expected Snapshot with the same set of images. +// FindMatchingSnapshot tries to finds 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 @@ -959,3 +980,107 @@ func FilterIntegrationTestScenariosWithContext(scenarios *[]v1beta2.IntegrationT } return &filteredScenarioList } + +// HasPRGroupProcessed checks if the pr group has been handled by snapshot adapter +// to avoid duplicate check, if yes, won't handle the snapshot again +func HasPRGroupProcessed(snapshot *applicationapiv1alpha1.Snapshot) bool { + return metadata.HasAnnotation(snapshot, PRGroupCreationAnnotation) +} + +// GetPRGroupHashFromSnapshot gets the value of label test.appstudio.openshift.io/pr-group-sha from component snapshot +func GetPRGroupHashFromSnapshot(snapshot *applicationapiv1alpha1.Snapshot) string { + if metadata.HasLabel(snapshot, PRGroupHashLabel) { + return snapshot.Labels[PRGroupHashLabel] + } + return "" +} + +// GetPRGroupFromSnapshot gets the value of annotation test.appstudio.openshift.io/pr-group from component snapshot +func GetPRGroupFromSnapshot(snapshot *applicationapiv1alpha1.Snapshot) string { + if metadata.HasAnnotation(snapshot, PRGroupAnnotation) { + return snapshot.Annotations[PRGroupAnnotation] + } + return "" +} + +// 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 { + for _, snapshotComponent := range snapshot.Spec.Components { + if snapshotComponent.Name == component.Name { + return snapshotComponent + } + } + return applicationapiv1alpha1.SnapshotComponent{} + +} + +// SortSnapshots sorts the snapshots according to the snapshot annotation BuildPipelineRunStartTime +func SortSnapshots(snapshots []applicationapiv1alpha1.Snapshot) []applicationapiv1alpha1.Snapshot { + sort.Slice(snapshots, func(i, j int) bool { + // sorting snapshots according to the annotation BuildPipelineRunStartTime which + // represents the start time of build PLR + // when BuildPipelineRunStartTime is not set, the value of Atoi is 0 + time_i, _ := strconv.Atoi(snapshots[i].Annotations[BuildPipelineRunStartTime]) + time_j, _ := strconv.Atoi(snapshots[j].Annotations[BuildPipelineRunStartTime]) + + return time_i > time_j + }) + return snapshots +} + +// AnnotateSnapshot sets annotation for a snapshot in defined context, return error if meeting it +func AnnotateSnapshot(ctx context.Context, snapshot *applicationapiv1alpha1.Snapshot, key, value string, cl client.Client) error { + patch := client.MergeFrom(snapshot.DeepCopy()) + + _ = metadata.SetAnnotation(&snapshot.ObjectMeta, key, value) + + err := cl.Patch(ctx, snapshot, patch) + if err != nil { + return err + } + return nil +} + +// NotifyComponentSnapshotsInGroupSnapshot annotate the msg to the given component snapshots in componentSnapshotInfos +func NotifyComponentSnapshotsInGroupSnapshot(ctx context.Context, cl client.Client, componentSnapshotInfos []ComponentSnapshotInfo, msg string) error { + log := log.FromContext(ctx) + for _, componentSnapshotInfo := range componentSnapshotInfos { + snapshot := &applicationapiv1alpha1.Snapshot{} + err := cl.Get(ctx, types.NamespacedName{ + Namespace: componentSnapshotInfo.Namespace, + Name: componentSnapshotInfo.Snapshot, + }, snapshot) + if err != nil { + log.Error(err, fmt.Sprintf("error while getting snapshot %s from namespace: %s", componentSnapshotInfo.Snapshot, componentSnapshotInfo.Namespace)) + return err + } + + err = AnnotateSnapshot(ctx, snapshot, PRGroupCreationAnnotation, msg, cl) + if err != nil { + log.Error(err, fmt.Sprintf("Failed to annotate group snapshot creation status to component snapshot %s/%s", componentSnapshotInfo.Namespace, componentSnapshotInfo.Snapshot)) + return err + } + } + return nil +} + +func SetAnnotationAndLabelForGroupSnapshot(groupSnapshot *applicationapiv1alpha1.Snapshot, componentSnapshot *applicationapiv1alpha1.Snapshot, componentSnapshotInfos []ComponentSnapshotInfo) (*applicationapiv1alpha1.Snapshot, error) { + err := metadata.SetAnnotation(groupSnapshot, PRGroupAnnotation, componentSnapshot.Annotations[PRGroupAnnotation]) + if err != nil { + return nil, err + } + annotationJson, err := json.Marshal(componentSnapshotInfos) + if err != nil { + return nil, err + } + groupSnapshot.Annotations[GroupSnapshotInfoAnnotation] = string(annotationJson) + + err = metadata.SetLabel(groupSnapshot, PipelineAsCodeEventTypeLabel, componentSnapshot.Labels[PipelineAsCodeEventTypeLabel]) + if err != nil { + return nil, err + } + groupSnapshot.Labels[SnapshotTypeLabel] = SnapshotGroupType + groupSnapshot.Labels[ApplicationNameLabel] = componentSnapshot.Spec.Application + + return groupSnapshot, nil +} diff --git a/gitops/snapshot_test.go b/gitops/snapshot_test.go index 1f6c699ad..c6de97de3 100644 --- a/gitops/snapshot_test.go +++ b/gitops/snapshot_test.go @@ -22,6 +22,7 @@ import ( . "github.com/onsi/gomega" . "github.com/onsi/gomega/gstruct" + "strconv" "time" "github.com/konflux-ci/integration-service/gitops" @@ -39,19 +40,26 @@ import ( var _ = Describe("Gitops functions for managing Snapshots", Ordered, func() { var ( - hasApp *applicationapiv1alpha1.Application - hasComp *applicationapiv1alpha1.Component - hasSnapshot *applicationapiv1alpha1.Snapshot - sampleImage string + hasApp *applicationapiv1alpha1.Application + hasComp *applicationapiv1alpha1.Component + hasSnapshot *applicationapiv1alpha1.Snapshot + hasComSnapshot1 *applicationapiv1alpha1.Snapshot + hasComSnapshot2 *applicationapiv1alpha1.Snapshot + hasComSnapshot3 *applicationapiv1alpha1.Snapshot + sampleImage string ) const ( - SampleRepoLink = "https://github.com/devfile-samples/devfile-sample-java-springboot-basic" - namespace = "default" - applicationName = "application-sample" - componentName = "component-sample" - snapshotName = "snapshot-sample" - SampleCommit = "a2ba645d50e471d5f084b" + SampleRepoLink = "https://github.com/devfile-samples/devfile-sample-java-springboot-basic" + namespace = "default" + applicationName = "application-sample" + componentName = "component-sample" + snapshotName = "snapshot-sample" + hasComSnapshot1Name = "hascomsnapshot1-sample" + hasComSnapshot2Name = "hascomsnapshot2-sample" + hasComSnapshot3Name = "hascomsnapshot3-sample" + SampleCommit = "a2ba645d50e471d5f084b" + plrstarttime = 1775992257 ) BeforeAll(func() { hasApp = &applicationapiv1alpha1.Application{ @@ -123,6 +131,120 @@ var _ = Describe("Gitops functions for managing Snapshots", Ordered, func() { }, hasSnapshot) return err }, time.Second*10).ShouldNot(HaveOccurred()) + + hasComSnapshot1 = &applicationapiv1alpha1.Snapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: hasComSnapshot1Name, + Namespace: namespace, + Labels: map[string]string{ + gitops.SnapshotTypeLabel: gitops.SnapshotComponentType, + gitops.SnapshotComponentLabel: hasComSnapshot1Name, + gitops.PipelineAsCodeEventTypeLabel: gitops.PipelineAsCodePullRequestType, + }, + Annotations: map[string]string{ + "test.appstudio.openshift.io/pr-last-update": "2023-08-26T17:57:50+02:00", + gitops.BuildPipelineRunStartTime: strconv.Itoa(plrstarttime), + }, + }, + Spec: applicationapiv1alpha1.SnapshotSpec{ + Application: hasApp.Name, + Components: []applicationapiv1alpha1.SnapshotComponent{ + { + Name: "component1", + ContainerImage: "test-image", + }, + { + Name: componentName, + ContainerImage: sampleImage, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, hasComSnapshot1)).Should(Succeed()) + + Eventually(func() error { + err := k8sClient.Get(ctx, types.NamespacedName{ + Name: hasComSnapshot1.Name, + Namespace: namespace, + }, hasComSnapshot1) + return err + }, time.Second*10).ShouldNot(HaveOccurred()) + + hasComSnapshot2 = &applicationapiv1alpha1.Snapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: hasComSnapshot2Name, + Namespace: namespace, + Labels: map[string]string{ + gitops.SnapshotTypeLabel: gitops.SnapshotComponentType, + gitops.SnapshotComponentLabel: hasComSnapshot2Name, + gitops.PipelineAsCodeEventTypeLabel: gitops.PipelineAsCodePullRequestType, + }, + Annotations: map[string]string{ + "test.appstudio.openshift.io/pr-last-update": "2023-08-26T17:57:50+02:00", + gitops.BuildPipelineRunStartTime: strconv.Itoa(plrstarttime + 100), + }, + }, + Spec: applicationapiv1alpha1.SnapshotSpec{ + Application: hasApp.Name, + Components: []applicationapiv1alpha1.SnapshotComponent{ + { + Name: "component1", + ContainerImage: "test-image", + }, + { + Name: componentName, + ContainerImage: sampleImage, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, hasComSnapshot2)).Should(Succeed()) + + Eventually(func() error { + err := k8sClient.Get(ctx, types.NamespacedName{ + Name: hasComSnapshot2.Name, + Namespace: namespace, + }, hasComSnapshot2) + return err + }, time.Second*10).ShouldNot(HaveOccurred()) + + hasComSnapshot3 = &applicationapiv1alpha1.Snapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: hasComSnapshot3Name, + Namespace: namespace, + Labels: map[string]string{ + gitops.SnapshotTypeLabel: gitops.SnapshotComponentType, + gitops.SnapshotComponentLabel: hasComSnapshot3Name, + gitops.PipelineAsCodeEventTypeLabel: gitops.PipelineAsCodePullRequestType, + }, + Annotations: map[string]string{ + "test.appstudio.openshift.io/pr-last-update": "2023-08-26T17:57:50+02:00", + gitops.BuildPipelineRunStartTime: strconv.Itoa(plrstarttime + 200), + }, + }, + Spec: applicationapiv1alpha1.SnapshotSpec{ + Application: hasApp.Name, + Components: []applicationapiv1alpha1.SnapshotComponent{ + { + Name: "component1", + ContainerImage: "test-image", + }, + { + Name: componentName, + ContainerImage: sampleImage, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, hasComSnapshot3)).Should(Succeed()) + + Eventually(func() error { + err := k8sClient.Get(ctx, types.NamespacedName{ + Name: hasComSnapshot3.Name, + Namespace: namespace, + }, hasComSnapshot3) + return err + }, time.Second*10).ShouldNot(HaveOccurred()) }) AfterEach(func() { @@ -130,6 +252,12 @@ var _ = Describe("Gitops functions for managing Snapshots", Ordered, func() { Expect(err == nil || errors.IsNotFound(err)).To(BeTrue()) err = k8sClient.Delete(ctx, hasSnapshot) Expect(err == nil || errors.IsNotFound(err)).To(BeTrue()) + err = k8sClient.Delete(ctx, hasComSnapshot1) + Expect(err == nil || errors.IsNotFound(err)).To(BeTrue()) + err = k8sClient.Delete(ctx, hasComSnapshot2) + Expect(err == nil || errors.IsNotFound(err)).To(BeTrue()) + err = k8sClient.Delete(ctx, hasComSnapshot3) + Expect(err == nil || errors.IsNotFound(err)).To(BeTrue()) }) AfterAll(func() { @@ -712,4 +840,71 @@ var _ = Describe("Gitops functions for managing Snapshots", Ordered, func() { }) }) + Context("Group snapshot creation tests", func() { + When("Snapshot has snapshot type label", func() { + prGroup := "feature1" + prGroupSha := "feature1hash" + BeforeEach(func() { + hasComSnapshot1.Labels[gitops.PRGroupHashLabel] = prGroupSha + hasComSnapshot1.Annotations[gitops.PRGroupAnnotation] = prGroup + hasComSnapshot1.Annotations[gitops.PRGroupCreationAnnotation] = "group snapshot is created" + }) + + It("make sure pr group annotation/label can be found in group", func() { + Expect(gitops.GetPRGroupFromSnapshot(hasComSnapshot1)).To(Equal(prGroup)) + Expect(gitops.GetPRGroupHashFromSnapshot(hasComSnapshot1)).To(Equal(prGroupSha)) + Expect(gitops.HasPRGroupProcessed(hasComSnapshot1)).To(BeTrue()) + }) + + It("Can find the correct snapshotComponent for the given component name", func() { + FoundSnapshotComponent := gitops.FindMatchingSnapshotComponent(hasComSnapshot1, hasComp) + Expect(FoundSnapshotComponent.Name).To(Equal(hasComp.Name)) + }) + + It("Can sort the snapshots according to annotation test.appstudio.openshift.io/pipelinerunstarttime", func() { + snapshots := []applicationapiv1alpha1.Snapshot{*hasComSnapshot1, *hasComSnapshot2, *hasComSnapshot3} + sortedSnapshots := gitops.SortSnapshots(snapshots) + Expect(sortedSnapshots[0].Name).To(Equal(hasComSnapshot3.Name)) + snapshots = []applicationapiv1alpha1.Snapshot{*hasComSnapshot2, *hasComSnapshot1, *hasComSnapshot3} + sortedSnapshots = gitops.SortSnapshots(snapshots) + Expect(sortedSnapshots[0].Name).To(Equal(hasComSnapshot3.Name)) + }) + + It("Can notify all component snapshots group snapshot creation status", func() { + Expect(metadata.HasAnnotation(hasComSnapshot2, gitops.PRGroupCreationAnnotation)).To(BeFalse()) + Expect(metadata.HasAnnotation(hasComSnapshot3, gitops.PRGroupCreationAnnotation)).To(BeFalse()) + componentSnapshotInfos := []gitops.ComponentSnapshotInfo{ + { + Namespace: "default", + Component: hasComSnapshot2Name, + BuildPipelineRun: "plr2", + Snapshot: hasComSnapshot2.Name, + }, + { + Namespace: "default", + Component: hasComSnapshot1Name, + BuildPipelineRun: "plr3", + Snapshot: hasComSnapshot3.Name, + }, + } + err := gitops.NotifyComponentSnapshotsInGroupSnapshot(ctx, k8sClient, componentSnapshotInfos, "group snapshot created") + + Eventually(func() bool { + _ = k8sClient.Get(ctx, types.NamespacedName{ + Name: hasComSnapshot2.Name, + Namespace: namespace, + }, hasComSnapshot2) + return metadata.HasAnnotationWithValue(hasComSnapshot2, gitops.PRGroupCreationAnnotation, "group snapshot created") + }, time.Second*10).Should(BeTrue()) + Eventually(func() bool { + _ = k8sClient.Get(ctx, types.NamespacedName{ + Name: hasComSnapshot3.Name, + Namespace: namespace, + }, hasComSnapshot3) + return metadata.HasAnnotationWithValue(hasComSnapshot3, gitops.PRGroupCreationAnnotation, "group snapshot created") + }, time.Second*10).Should(BeTrue()) + Expect(err).ShouldNot(HaveOccurred()) + }) + }) + }) }) diff --git a/internal/controller/buildpipeline/buildpipeline_adapter.go b/internal/controller/buildpipeline/buildpipeline_adapter.go index 887f5273b..51fdef2a8 100644 --- a/internal/controller/buildpipeline/buildpipeline_adapter.go +++ b/internal/controller/buildpipeline/buildpipeline_adapter.go @@ -369,9 +369,9 @@ func (a *Adapter) updatePipelineRunWithCustomizedError(canRemoveFinalizer *bool, // addPRGroupToBuildPLRMetadata will add pr-group info gotten from souce-branch to annotation // and also the string in sha format to metadata label func (a *Adapter) addPRGroupToBuildPLRMetadata(pipelineRun *tektonv1.PipelineRun) error { - prGroupName := tekton.GetPRGroupNameFromBuildPLR(pipelineRun) - if prGroupName != "" { - prGroupHashName := tekton.GenerateSHA(prGroupName) + prGroup := tekton.GetPRGroupFromBuildPLR(pipelineRun) + if prGroup != "" { + prGroupHash := tekton.GenerateSHA(prGroup) return retry.RetryOnConflict(retry.DefaultRetry, func() error { var err error @@ -382,8 +382,8 @@ func (a *Adapter) addPRGroupToBuildPLRMetadata(pipelineRun *tektonv1.PipelineRun patch := client.MergeFrom(pipelineRun.DeepCopy()) - _ = metadata.SetAnnotation(&pipelineRun.ObjectMeta, gitops.PRGroupAnnotation, prGroupName) - _ = metadata.SetLabel(&pipelineRun.ObjectMeta, gitops.PRGroupHashLabel, prGroupHashName) + _ = metadata.SetAnnotation(&pipelineRun.ObjectMeta, gitops.PRGroupAnnotation, prGroup) + _ = metadata.SetLabel(&pipelineRun.ObjectMeta, gitops.PRGroupHashLabel, prGroupHash) return a.client.Patch(a.context, pipelineRun, patch) }) diff --git a/internal/controller/snapshot/snapshot_adapter.go b/internal/controller/snapshot/snapshot_adapter.go index de7dd3d07..0f8a4b162 100644 --- a/internal/controller/snapshot/snapshot_adapter.go +++ b/internal/controller/snapshot/snapshot_adapter.go @@ -36,6 +36,7 @@ import ( intgteststat "github.com/konflux-ci/integration-service/pkg/integrationteststatus" "github.com/konflux-ci/integration-service/pkg/metrics" "github.com/konflux-ci/integration-service/release" + "github.com/konflux-ci/integration-service/status" "github.com/konflux-ci/integration-service/tekton" applicationapiv1alpha1 "github.com/redhat-appstudio/application-api/api/v1alpha1" @@ -62,6 +63,7 @@ type Adapter struct { loader loader.ObjectLoader client client.Client context context.Context + status status.StatusInterface } // NewAdapter creates and returns an Adapter instance. @@ -74,6 +76,7 @@ func NewAdapter(context context.Context, snapshot *applicationapiv1alpha1.Snapsh loader: loader, client: client, context: context, + status: status.NewStatus(logger.Logger, client), } } @@ -507,6 +510,119 @@ func (a *Adapter) EnsureOverrideSnapshotValid() (controller.OperationResult, err return controller.ContinueProcessing() } +// 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 +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") + return controller.ContinueProcessing() + } + + if !metadata.HasLabelWithValue(a.snapshot, gitops.SnapshotTypeLabel, gitops.SnapshotComponentType) { + a.logger.Info("The snapshot is not a component snapshot, no need to create group snapshot for it") + return controller.ContinueProcessing() + } + + if gitops.HasPRGroupProcessed(a.snapshot) { + a.logger.Info("The PR group info has been processed for this component snapshot, no need to process it again") + return controller.ContinueProcessing() + } + + prGroupHash := gitops.GetPRGroupHashFromSnapshot(a.snapshot) + if prGroupHash == "" { + a.logger.Error(fmt.Errorf("NotFound"), fmt.Sprintf("Failed to get PR group hash from snapshot %s/%s", a.snapshot.Namespace, a.snapshot.Name)) + err := gitops.AnnotateSnapshot(a.context, a.snapshot, gitops.PRGroupCreationAnnotation, fmt.Sprintf("Failed to get PR group hash from snapshot %s/%s", a.snapshot.Namespace, a.snapshot.Name), a.client) + if err != nil { + return controller.RequeueWithError(err) + } + return controller.ContinueProcessing() + } + + prGroup := gitops.GetPRGroupFromSnapshot(a.snapshot) + if prGroup == "" { + a.logger.Error(fmt.Errorf("NotFound"), fmt.Sprintf("Failed to get PR group from snapshot %s/%s", a.snapshot.Namespace, a.snapshot.Name)) + err := gitops.AnnotateSnapshot(a.context, a.snapshot, gitops.PRGroupCreationAnnotation, fmt.Sprintf("Failed to get PR group from snapshot %s/%s", a.snapshot.Namespace, a.snapshot.Name), a.client) + if err != nil { + return controller.RequeueWithError(err) + } + return controller.ContinueProcessing() + } + + pipelineRuns, err := a.loader.GetPipelineRunsWithPRGroupHash(a.context, a.client, a.snapshot, prGroupHash) + if err != nil { + a.logger.Error(err, fmt.Sprintf("Failed to get build pipelineRuns for given pr group hash %s", prGroupHash)) + return controller.RequeueWithError(err) + } + + for _, pipelineRun := range *pipelineRuns { + pipelineRun := pipelineRun + + // check if the build PLR is the latest existing one + if !isLatestBuildPipelineRunInComponent(&pipelineRun, pipelineRuns) { + a.logger.Info(fmt.Sprintf("The build pipelineRun %s/%s with pr group %s is not the latest for its component, skipped", pipelineRun.Namespace, pipelineRun.Name, prGroup)) + continue + } + + // check if build PLR finishes + if !h.HasPipelineRunFinished(&pipelineRun) { + a.logger.Info(fmt.Sprintf("The build pipelineRun %s/%s with pr group %s is still running, won't create group snapshot", pipelineRun.Namespace, pipelineRun.Name, prGroup)) + err := gitops.AnnotateSnapshot(a.context, a.snapshot, gitops.PRGroupCreationAnnotation, fmt.Sprintf("The build pipelineRun %s/%s with pr group %s is still running, won't create group snapshot", pipelineRun.Namespace, pipelineRun.Name, prGroup), a.client) + if err != nil { + return controller.RequeueWithError(err) + } + return controller.ContinueProcessing() + } + + // check if build PLR succeeds + if !h.HasPipelineRunSucceeded(&pipelineRun) { + a.logger.Info(fmt.Sprintf("The build pipelineRun %s/%s with pr group %s failed, won't create group snapshot", pipelineRun.Namespace, pipelineRun.Name, prGroup)) + err := gitops.AnnotateSnapshot(a.context, a.snapshot, gitops.PRGroupCreationAnnotation, fmt.Sprintf("The build pipelineRun %s/%s with pr group %s failed, won't create group snapshot", pipelineRun.Namespace, pipelineRun.Name, prGroup), a.client) + if err != nil { + return controller.RequeueWithError(err) + } + return controller.ContinueProcessing() + } + + // check if build PLR has component snapshot created except the build that snapshot is created from because the build plr has not been labeled with snapshot name + if !metadata.HasAnnotation(&pipelineRun, tekton.SnapshotNameLabel) && !metadata.HasLabelWithValue(a.snapshot, gitops.BuildPipelineRunNameLabel, pipelineRun.Name) { + a.logger.Info(fmt.Sprintf("The build pipelineRun %s/%s with pr group %s has succeeded but component snapshot has not been created now", pipelineRun.Namespace, pipelineRun.Name, prGroup)) + return controller.ContinueProcessing() + } + } + + groupSnapshot, componentSnapshotInfos, err := a.prepareGroupSnapshot(a.application, prGroupHash) + if err != nil { + a.logger.Error(err, "Failed to prepare group snapshot") + return controller.RequeueWithError(err) + } + + if groupSnapshot == nil { + a.logger.Info(fmt.Sprintf("The number %d of component snapshots belonging to this pr group hash %s is less than 2, skipping group snapshot creation", len(componentSnapshotInfos), prGroupHash)) + err = gitops.AnnotateSnapshot(a.context, a.snapshot, gitops.PRGroupCreationAnnotation, fmt.Sprintf("The number %d of component snapshots belonging to this pr group hash %s is less than 2, skipping group snapshot creation", len(componentSnapshotInfos), prGroupHash), a.client) + if err != nil { + return controller.RequeueWithError(err) + } + return controller.ContinueProcessing() + } + + err = a.client.Create(a.context, groupSnapshot) + if err != nil { + a.logger.Error(err, "Failed to create group snapshot") + if clienterrors.IsForbidden(err) { + return controller.StopProcessing() + } + return controller.RequeueWithError(err) + } + + // notify all component snapshots that group snapshot is created for them + err = gitops.NotifyComponentSnapshotsInGroupSnapshot(a.context, a.client, componentSnapshotInfos, fmt.Sprintf("Group snapshot %s/%s is created for pr group %s", groupSnapshot.Namespace, groupSnapshot.Name, prGroup)) + if err != nil { + a.logger.Error(err, fmt.Sprintf("Failed to annotate the component snapshots for group snapshot %s/%s", a.snapshot.Namespace, a.snapshot.Name)) + return controller.RequeueWithError(err) + } + return controller.ContinueProcessing() +} + // createMissingReleasesForReleasePlans checks if there's existing Releases for a given list of ReleasePlans and creates // new ones if they are missing. In case the Releases can't be created, an error will be returned. func (a *Adapter) createMissingReleasesForReleasePlans(application *applicationapiv1alpha1.Application, releasePlans *[]releasev1alpha1.ReleasePlan, snapshot *applicationapiv1alpha1.Snapshot) error { @@ -736,3 +852,117 @@ func (a *Adapter) updateComponentSource(ctx context.Context, c client.Client, co } return nil } + +func (a *Adapter) prepareGroupSnapshot(application *applicationapiv1alpha1.Application, prGroupHash string) (*applicationapiv1alpha1.Snapshot, []gitops.ComponentSnapshotInfo, error) { + 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 { + var isPRMROpened bool + applicationComponent := applicationComponent // G601 + snapshots, err := a.loader.GetMatchingComponentSnapshotsForComponentAndPRGroupHash(a.context, a.client, a.snapshot, applicationComponent.Name, prGroupHash) + if err != nil { + a.logger.Error(err, "Failed to fetch Snapshots for component", "component.Name", applicationComponent.Name) + return nil, nil, err + } + + sortedSnapshots := gitops.SortSnapshots(*snapshots) + // find the latest component snapshot created for open PR/MR + for _, snapshot := range sortedSnapshots { + snapshot := snapshot + // find the built image for pull/merge request build PLR from the latest opened pull request component snapshot + isPRMROpened, err = a.status.IsPRMRInSnapshotOpened(a.context, &snapshot) + if err != nil { + a.logger.Error(err, "Failed to fetch PR/MR status for component snapshot", "snapshot.Name", a.snapshot.Name) + return nil, nil, err + } + if isPRMROpened { + a.logger.Info("PR/MR in snapshot is opend, will find snapshotComponent and add to groupSnapshot") + snapshotComponent := gitops.FindMatchingSnapshotComponent(&snapshot, &applicationComponent) + componentSnapshotInfos = append(componentSnapshotInfos, gitops.ComponentSnapshotInfo{ + Component: applicationComponent.Name, + BuildPipelineRun: snapshot.Labels[gitops.BuildPipelineRunNameLabel], + Snapshot: snapshot.Name, + Namespace: a.snapshot.Namespace, + }) + snapshotComponents = append(snapshotComponents, snapshotComponent) + break + } + } + // isPRMROpened represents snapshotComponent can be gottent from PR component snapshot + // so continue next applicationComponent + if isPRMROpened { + 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 + componentSource := gitops.GetComponentSourceFromComponent(&applicationComponent) + containerImage := applicationComponent.Spec.ContainerImage + if containerImage == "" { + a.logger.Info("component cannot be added to snapshot for application due to missing containerImage", "component.Name", applicationComponent.Name) + continue + } else { + // if the containerImage doesn't have a valid digest, the component + // will not be added to snapshot + err := gitops.ValidateImageDigest(containerImage) + 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 + } + snapshotComponent := applicationapiv1alpha1.SnapshotComponent{ + Name: applicationComponent.Name, + ContainerImage: containerImage, + Source: *componentSource, + } + snapshotComponents = append(snapshotComponents, snapshotComponent) + } + } + + // 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 := gitops.NewSnapshot(application, &snapshotComponents) + err = ctrl.SetControllerReference(application, 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 +} + +// isLatestBuildPipelineRunInComponent return true if pipelineRun is the latest pipelineRun +// for its component and pr group sha. Pipeline start timestamp is used for comparison because we care about +// time when pipeline was created. +func isLatestBuildPipelineRunInComponent(pipelineRun *tektonv1.PipelineRun, pipelineRuns *[]tektonv1.PipelineRun) bool { + pipelineStartTime := pipelineRun.CreationTimestamp.Time + componentName := pipelineRun.Labels[tekton.PipelineRunComponentLabel] + for _, run := range *pipelineRuns { + if pipelineRun.Name == run.Name { + // it's the same pipeline + continue + } + if componentName != run.Labels[tekton.PipelineRunComponentLabel] { + continue + } + timestamp := run.CreationTimestamp.Time + if pipelineStartTime.Before(timestamp) { + // pipeline is not the latest + // 1 second is minimal granularity, if both pipelines started at the same second, we cannot decide + return false + } + } + return true +} diff --git a/internal/controller/snapshot/snapshot_adapter_test.go b/internal/controller/snapshot/snapshot_adapter_test.go index 4238b2a99..91c5d1f59 100644 --- a/internal/controller/snapshot/snapshot_adapter_test.go +++ b/internal/controller/snapshot/snapshot_adapter_test.go @@ -21,9 +21,11 @@ import ( "context" "fmt" "reflect" + "strconv" "time" "github.com/tonglil/buflogr" + "go.uber.org/mock/gomock" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" . "github.com/onsi/ginkgo/v2" @@ -32,12 +34,16 @@ import ( "github.com/konflux-ci/integration-service/api/v1beta2" "github.com/konflux-ci/integration-service/loader" + "github.com/konflux-ci/integration-service/status" "github.com/konflux-ci/integration-service/tekton" toolkit "github.com/konflux-ci/operator-toolkit/loader" + "github.com/konflux-ci/operator-toolkit/metadata" releasev1alpha1 "github.com/konflux-ci/release-service/api/v1alpha1" releasemetadata "github.com/konflux-ci/release-service/metadata" applicationapiv1alpha1 "github.com/redhat-appstudio/application-api/api/v1alpha1" tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + "knative.dev/pkg/apis" + v1 "knative.dev/pkg/apis/duck/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -60,22 +66,34 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { hasApp *applicationapiv1alpha1.Application hasComp *applicationapiv1alpha1.Component hasCompMissingImageDigest *applicationapiv1alpha1.Component + hasCom1 *applicationapiv1alpha1.Component + hasCom3 *applicationapiv1alpha1.Component hasSnapshot *applicationapiv1alpha1.Snapshot hasSnapshotPR *applicationapiv1alpha1.Snapshot hasOverRideSnapshot *applicationapiv1alpha1.Snapshot hasInvalidSnapshot *applicationapiv1alpha1.Snapshot hasInvalidOverrideSnapshot *applicationapiv1alpha1.Snapshot + hasComSnapshot1 *applicationapiv1alpha1.Snapshot + hasComSnapshot2 *applicationapiv1alpha1.Snapshot + hasComSnapshot3 *applicationapiv1alpha1.Snapshot integrationTestScenario *v1beta2.IntegrationTestScenario integrationTestScenarioForInvalidSnapshot *v1beta2.IntegrationTestScenario env *applicationapiv1alpha1.Environment + buildPipelineRun1 *tektonv1.PipelineRun ) const ( - SampleRepoLink = "https://github.com/devfile-samples/devfile-sample-java-springboot-basic" - sample_image = "quay.io/redhat-appstudio/sample-image" - sample_revision = "random-value" - sampleDigest = "sha256:841328df1b9f8c4087adbdcfec6cc99ac8308805dea83f6d415d6fb8d40227c1" - customLabel = "custom.appstudio.openshift.io/custom-label" - sourceRepoRef = "db2c043b72b3f8d292ee0e38768d0a94859a308b" + SampleRepoLink = "https://github.com/devfile-samples/devfile-sample-java-springboot-basic" + sample_image = "quay.io/redhat-appstudio/sample-image" + sample_revision = "random-value" + sampleDigest = "sha256:841328df1b9f8c4087adbdcfec6cc99ac8308805dea83f6d415d6fb8d40227c1" + customLabel = "custom.appstudio.openshift.io/custom-label" + sourceRepoRef = "db2c043b72b3f8d292ee0e38768d0a94859a308b" + hasComSnapshot1Name = "hascomsnapshot1-sample" + hasComSnapshot2Name = "hascomsnapshot2-sample" + hasComSnapshot3Name = "hascomsnapshot3-sample" + prGroup = "feature1" + prGroupSha = "feature1hash" + plrstarttime = 1775992257 ) BeforeAll(func() { @@ -166,6 +184,52 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { } Expect(k8sClient.Create(ctx, hasComp)).Should(Succeed()) + hasCom1 = &applicationapiv1alpha1.Component{ + ObjectMeta: metav1.ObjectMeta{ + Name: "component1-sample", + Namespace: "default", + }, + Spec: applicationapiv1alpha1.ComponentSpec{ + ComponentName: "component1-sample", + Application: "application-sample", + ContainerImage: sample_image + "@" + sampleDigest, + Source: applicationapiv1alpha1.ComponentSource{ + ComponentSourceUnion: applicationapiv1alpha1.ComponentSourceUnion{ + GitSource: &applicationapiv1alpha1.GitSource{ + URL: SampleRepoLink, + }, + }, + }, + }, + Status: applicationapiv1alpha1.ComponentStatus{ + LastBuiltCommit: "", + }, + } + Expect(k8sClient.Create(ctx, hasCom1)).Should(Succeed()) + + hasCom3 = &applicationapiv1alpha1.Component{ + ObjectMeta: metav1.ObjectMeta{ + Name: "component3-sample", + Namespace: "default", + }, + Spec: applicationapiv1alpha1.ComponentSpec{ + ComponentName: "component3-sample", + Application: "application-sample", + ContainerImage: sample_image + "@" + sampleDigest, + Source: applicationapiv1alpha1.ComponentSource{ + ComponentSourceUnion: applicationapiv1alpha1.ComponentSourceUnion{ + GitSource: &applicationapiv1alpha1.GitSource{ + URL: SampleRepoLink, + }, + }, + }, + }, + Status: applicationapiv1alpha1.ComponentStatus{ + LastBuiltCommit: "", + }, + } + Expect(k8sClient.Create(ctx, hasCom3)).Should(Succeed()) + hasCompMissingImageDigest = &applicationapiv1alpha1.Component{ ObjectMeta: metav1.ObjectMeta{ Name: "component-sample-missing-image", @@ -333,6 +397,174 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { }, hasSnapshot) return err }, time.Second*10).ShouldNot(HaveOccurred()) + + hasComSnapshot1 = &applicationapiv1alpha1.Snapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: hasComSnapshot1Name, + Namespace: "default", + Labels: map[string]string{ + gitops.SnapshotTypeLabel: gitops.SnapshotComponentType, + gitops.SnapshotComponentLabel: hasCom1.Name, + gitops.PipelineAsCodeEventTypeLabel: gitops.PipelineAsCodePullRequestType, + gitops.PRGroupHashLabel: prGroupSha, + "pac.test.appstudio.openshift.io/url-org": "testorg", + "pac.test.appstudio.openshift.io/url-repository": "testrepo", + gitops.PipelineAsCodeSHALabel: "sha", + }, + Annotations: map[string]string{ + "test.appstudio.openshift.io/pr-last-update": "2023-08-26T17:57:50+02:00", + gitops.BuildPipelineRunStartTime: strconv.Itoa(plrstarttime), + gitops.PRGroupAnnotation: prGroup, + gitops.PipelineAsCodeGitProviderAnnotation: "github", + gitops.PipelineAsCodePullRequestAnnotation: "1", + gitops.PipelineAsCodeInstallationIDAnnotation: "123", + }, + }, + Spec: applicationapiv1alpha1.SnapshotSpec{ + Application: hasApp.Name, + Components: []applicationapiv1alpha1.SnapshotComponent{ + { + Name: "component1", + ContainerImage: sample_image + "@" + sampleDigest, + }, + { + Name: hasComp.Name, + ContainerImage: sample_image + "@" + sampleDigest, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, hasComSnapshot1)).Should(Succeed()) + + Eventually(func() error { + err := k8sClient.Get(ctx, types.NamespacedName{ + Name: hasComSnapshot1.Name, + Namespace: "default", + }, hasComSnapshot1) + return err + }, time.Second*10).ShouldNot(HaveOccurred()) + + hasComSnapshot2 = &applicationapiv1alpha1.Snapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: hasComSnapshot2Name, + Namespace: "default", + Labels: map[string]string{ + gitops.SnapshotTypeLabel: gitops.SnapshotComponentType, + gitops.SnapshotComponentLabel: hasCom1.Name, + gitops.PipelineAsCodeEventTypeLabel: gitops.PipelineAsCodePullRequestType, + gitops.PRGroupHashLabel: prGroupSha, + "pac.test.appstudio.openshift.io/url-org": "testorg", + "pac.test.appstudio.openshift.io/url-repository": "testrepo", + gitops.PipelineAsCodeSHALabel: "sha", + }, + Annotations: map[string]string{ + "test.appstudio.openshift.io/pr-last-update": "2023-08-26T17:57:50+02:00", + gitops.BuildPipelineRunStartTime: strconv.Itoa(plrstarttime + 100), + gitops.PRGroupAnnotation: prGroup, + gitops.PipelineAsCodeGitProviderAnnotation: "github", + gitops.PipelineAsCodeInstallationIDAnnotation: "123", + }, + }, + Spec: applicationapiv1alpha1.SnapshotSpec{ + Application: hasApp.Name, + Components: []applicationapiv1alpha1.SnapshotComponent{ + { + Name: hasCom1.Name, + ContainerImage: sample_image + "@" + sampleDigest, + }, + { + Name: hasComp.Name, + ContainerImage: sample_image + "@" + sampleDigest, + }, + }, + }, + } + + hasComSnapshot3 = &applicationapiv1alpha1.Snapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: hasComSnapshot3Name, + Namespace: "default", + Labels: map[string]string{ + gitops.SnapshotTypeLabel: gitops.SnapshotComponentType, + gitops.SnapshotComponentLabel: hasCom3.Name, + gitops.PipelineAsCodeEventTypeLabel: gitops.PipelineAsCodePullRequestType, + gitops.PRGroupHashLabel: prGroupSha, + "pac.test.appstudio.openshift.io/url-org": "testorg", + "pac.test.appstudio.openshift.io/url-repository": "testrepo", + gitops.PipelineAsCodeSHALabel: "sha", + }, + Annotations: map[string]string{ + "test.appstudio.openshift.io/pr-last-update": "2023-08-26T17:57:50+02:00", + gitops.BuildPipelineRunStartTime: strconv.Itoa(plrstarttime + 200), + gitops.PRGroupAnnotation: prGroup, + gitops.PipelineAsCodeGitProviderAnnotation: "github", + gitops.PipelineAsCodePullRequestAnnotation: "1", + gitops.PipelineAsCodeInstallationIDAnnotation: "123", + }, + }, + Spec: applicationapiv1alpha1.SnapshotSpec{ + Application: hasApp.Name, + Components: []applicationapiv1alpha1.SnapshotComponent{ + { + Name: hasCom3.Name, + ContainerImage: sample_image + "@" + sampleDigest, + }, + { + Name: hasComp.Name, + ContainerImage: sample_image + "@" + sampleDigest, + }, + }, + }, + } + + buildPipelineRun1 = &tektonv1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipelinerun-build-running1", + 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", + "build.appstudio.redhat.com/target_branch": "main", + "test.appstudio.openshift.io/pr-group-sha": 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", + "test.appstudio.openshift.io/pr-group": prGroup, + }, + }, + Spec: tektonv1.PipelineRunSpec{ + PipelineRef: &tektonv1.PipelineRef{ + Name: "build-pipeline-pass", + ResolverRef: tektonv1.ResolverRef{ + Resolver: "bundle", + Params: tektonv1.Params{ + {Name: "bundle", + Value: tektonv1.ParamValue{Type: "string", StringVal: "quay.io/redhat-appstudio/example-tekton-bundle:test"}, + }, + {Name: "name", + Value: tektonv1.ParamValue{Type: "string", StringVal: "test-task"}, + }, + }, + }, + }, + Params: []tektonv1.Param{ + { + Name: "output-image", + Value: tektonv1.ParamValue{ + Type: tektonv1.ParamTypeString, + StringVal: sample_image + "@" + sampleDigest, + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, buildPipelineRun1)).Should(Succeed()) }) AfterEach(func() { @@ -344,6 +576,14 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { Expect(err == nil || errors.IsNotFound(err)).To(BeTrue()) err = k8sClient.Delete(ctx, hasInvalidOverrideSnapshot) Expect(err == nil || errors.IsNotFound(err)).To(BeTrue()) + err = k8sClient.Delete(ctx, hasComSnapshot1) + Expect(err == nil || errors.IsNotFound(err)).To(BeTrue()) + err = k8sClient.Delete(ctx, hasComSnapshot2) + Expect(err == nil || errors.IsNotFound(err)).To(BeTrue()) + err = k8sClient.Delete(ctx, hasComSnapshot3) + Expect(err == nil || errors.IsNotFound(err)).To(BeTrue()) + err = k8sClient.Delete(ctx, buildPipelineRun1) + Expect(err == nil || errors.IsNotFound(err)).To(BeTrue()) }) AfterAll(func() { @@ -357,6 +597,10 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { Expect(err == nil || errors.IsNotFound(err)).To(BeTrue()) err = k8sClient.Delete(ctx, testReleasePlan) Expect(err == nil || errors.IsNotFound(err)).To(BeTrue()) + err = k8sClient.Delete(ctx, hasCom1) + Expect(err == nil || errors.IsNotFound(err)).To(BeTrue()) + err = k8sClient.Delete(ctx, hasCom3) + Expect(err == nil || errors.IsNotFound(err)).To(BeTrue()) }) When("adapter is created for Snapshot hasSnapshot", func() { @@ -1296,6 +1540,307 @@ var _ = Describe("Snapshot Adapter", Ordered, func() { Expect(result.RequeueRequest).To(BeFalse()) }) }) + + 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() { + var buf bytes.Buffer + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + hasComSnapshot1.Labels[gitops.PipelineAsCodeEventTypeLabel] = gitops.PipelineAsCodePushType + adapter = NewAdapter(ctx, hasComSnapshot1, hasApp, log, loader.NewMockLoader(), k8sClient) + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ApplicationContextKey, + Resource: hasApp, + }, + { + 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, hasApp, log, loader.NewMockLoader(), k8sClient) + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ApplicationContextKey, + Resource: hasApp, + }, + { + 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 PR group info has been processed for this component snapshot")) + Expect(err).ToNot(HaveOccurred()) + }) + + It("ensures component snapshot will not be processed if it has no pr group sha label", func() { + var buf bytes.Buffer + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + Expect(metadata.DeleteLabel(hasComSnapshot1, gitops.PRGroupHashLabel)).ShouldNot(HaveOccurred()) + Expect(metadata.HasLabel(hasComSnapshot1, gitops.PRGroupHashLabel)).To(BeFalse()) + adapter = NewAdapter(ctx, hasComSnapshot1, hasApp, log, loader.NewMockLoader(), k8sClient) + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ApplicationContextKey, + Resource: hasApp, + }, + { + ContextKey: loader.SnapshotContextKey, + Resource: hasComSnapshot1, + }, + }) + + result, err := adapter.EnsureGroupSnapshotExist() + Expect(result.CancelRequest).To(BeFalse()) + Expect(result.RequeueRequest).To(BeFalse()) + Expect(buf.String()).Should(ContainSubstring("Failed to get PR group hash from snapshot")) + Expect(err).ToNot(HaveOccurred()) + Expect(metadata.HasAnnotation(hasComSnapshot1, gitops.PRGroupCreationAnnotation)).To(BeTrue()) + }) + + It("ensures component snapshot will not be processed if it has no pr group annotation", func() { + var buf bytes.Buffer + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + Expect(metadata.DeleteAnnotation(hasComSnapshot1, gitops.PRGroupAnnotation)).ShouldNot(HaveOccurred()) + Expect(metadata.HasAnnotation(hasComSnapshot1, gitops.PRGroupAnnotation)).To(BeFalse()) + adapter = NewAdapter(ctx, hasComSnapshot1, hasApp, log, loader.NewMockLoader(), k8sClient) + adapter.context = toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: loader.ApplicationContextKey, + Resource: hasApp, + }, + { + ContextKey: loader.SnapshotContextKey, + Resource: hasComSnapshot1, + }, + }) + + result, err := adapter.EnsureGroupSnapshotExist() + Expect(result.CancelRequest).To(BeFalse()) + Expect(result.RequeueRequest).To(BeFalse()) + Expect(buf.String()).Should(ContainSubstring("Failed to get PR group from snapshot")) + Expect(err).ToNot(HaveOccurred()) + Expect(metadata.HasAnnotation(hasComSnapshot1, gitops.PRGroupCreationAnnotation)).To(BeTrue()) + }) + + It("Calling en 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)} + adapter = NewAdapter(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("is still running, won't create group snapshot")) + Expect(err).ToNot(HaveOccurred()) + Expect(metadata.HasAnnotation(hasComSnapshot1, gitops.PRGroupCreationAnnotation)).To(BeTrue()) + }) + + It("Calling en 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)} + adapter = NewAdapter(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("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 = NewAdapter(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("Ensure group snasphot 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().IsPRMRInSnapshotOpened(gomock.Any(), hasComSnapshot2).Return(true, nil) + mockStatus.EXPECT().IsPRMRInSnapshotOpened(gomock.Any(), hasComSnapshot3).Return(true, nil) + // mockStatus.EXPECT().IsPRMRInSnapshotOpened(gomock.Any(), gomock.Any()).Return(true, nil) + mockStatus.EXPECT().IsPRMRInSnapshotOpened(gomock.Any(), gomock.Any()).AnyTimes() + + adapter = NewAdapter(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{*hasCom1, *hasCom3}, + }, + }) + + // create 3 snasphots 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("")) + Expect(err).ToNot(HaveOccurred()) + fmt.Fprintf(GinkgoWriter, "-------test: %v\n", buf.String()) + 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()) + }) + }) }) func getAllIntegrationPipelineRunsForSnapshot(ctx context.Context, snapshot *applicationapiv1alpha1.Snapshot) ([]tektonv1.PipelineRun, error) { diff --git a/internal/controller/snapshot/snapshot_controller.go b/internal/controller/snapshot/snapshot_controller.go index 70740af84..5e151edf4 100644 --- a/internal/controller/snapshot/snapshot_controller.go +++ b/internal/controller/snapshot/snapshot_controller.go @@ -117,6 +117,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu adapter := NewAdapter(ctx, snapshot, application, logger, loader, r.Client) return controller.ReconcileHandler([]controller.Operation{ + adapter.EnsureGroupSnapshotExist, adapter.EnsureOverrideSnapshotValid, adapter.EnsureAllReleasesExist, adapter.EnsureGlobalCandidateImageUpdated, @@ -127,6 +128,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu // AdapterInterface is an interface defining all the operations that should be defined in an Integration adapter. type AdapterInterface interface { + EnsureGroupSnapshotExist() (controller.OperationResult, error) EnsureAllReleasesExist() (controller.OperationResult, error) EnsureRerunPipelineRunsExist() (controller.OperationResult, error) EnsureIntegrationPipelineRunsExist() (controller.OperationResult, error) diff --git a/loader/loader.go b/loader/loader.go index 9187658e4..b47a48d06 100644 --- a/loader/loader.go +++ b/loader/loader.go @@ -56,6 +56,8 @@ 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) + GetPipelineRunsWithPRGroupHash(ctx context.Context, c client.Client, snapshot *applicationapiv1alpha1.Snapshot, prGroupHash string) (*[]tektonv1.PipelineRun, error) + GetMatchingComponentSnapshotsForComponentAndPRGroupHash(ctx context.Context, c client.Client, snapshot *applicationapiv1alpha1.Snapshot, componentName, prGroupHash string) (*[]applicationapiv1alpha1.Snapshot, error) } type loader struct{} @@ -381,3 +383,76 @@ func (l *loader) GetComponent(ctx context.Context, c client.Client, name, namesp component := &applicationapiv1alpha1.Component{} return component, toolkit.GetObject(name, namespace, c, ctx, component) } + +// GetPipelineRunsWithPRGroupHash gets the build pipelineRun with the given pr group hash string and the the same namespace with the given snapshot +func (l *loader) GetPipelineRunsWithPRGroupHash(ctx context.Context, adapterClient client.Client, snapshot *applicationapiv1alpha1.Snapshot, prGroupHash string) (*[]tektonv1.PipelineRun, error) { + buildPipelineRuns := &tektonv1.PipelineRunList{} + + evnentTypeLabelRequirement, err := labels.NewRequirement("pipelinesascode.tekton.dev/event-type", selection.NotIn, []string{"push", "Push"}) + 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 + } + plrTypeLabelRequirement, err := labels.NewRequirement("pipelines.appstudio.openshift.io/type", selection.In, []string{"build"}) + if err != nil { + return nil, err + } + + labelSelector := labels.NewSelector(). + Add(*evnentTypeLabelRequirement). + Add(*prGroupLabelRequirement). + Add(*plrTypeLabelRequirement) + + opts := &client.ListOptions{ + Namespace: snapshot.Namespace, + LabelSelector: labelSelector, + } + + err = adapterClient.List(ctx, buildPipelineRuns, opts) + if err != nil { + return nil, err + } + return &buildPipelineRuns.Items, nil +} + +// 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, snapshot *applicationapiv1alpha1.Snapshot, componentName, prGroupHash string) (*[]applicationapiv1alpha1.Snapshot, error) { + snapshots := &applicationapiv1alpha1.SnapshotList{} + + eventTypeLabelRequirement, err := labels.NewRequirement("pac.test.appstudio.openshift.io/event-type", selection.NotIn, []string{"push", "Push"}) + if err != nil { + return nil, err + } + componentLabelRequirement, err := labels.NewRequirement("appstudio.openshift.io/component", selection.In, []string{componentName}) + 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 + } + snapshotTypeLabelRequirement, err := labels.NewRequirement("test.appstudio.openshift.io/type", selection.In, []string{"component"}) + if err != nil { + return nil, err + } + + labelSelector := labels.NewSelector(). + Add(*eventTypeLabelRequirement). + Add(*componentLabelRequirement). + Add(*prGroupLabelRequirement). + Add(*snapshotTypeLabelRequirement) + + opts := &client.ListOptions{ + Namespace: snapshot.Namespace, + LabelSelector: labelSelector, + } + + err = c.List(ctx, snapshots, opts) + if err != nil { + return nil, err + } + return &snapshots.Items, nil +} diff --git a/loader/loader_mock.go b/loader/loader_mock.go index 04b8e9473..56739b245 100644 --- a/loader/loader_mock.go +++ b/loader/loader_mock.go @@ -55,6 +55,8 @@ const ( AllTaskRunsWithMatchingPipelineRunLabelContextKey GetPipelineRunContextKey GetComponentContextKey + GetBuildPLRContextKey + GetComponentSnapshotsKey ) func NewMockLoader() ObjectLoader { @@ -213,3 +215,21 @@ func (l *mockLoader) GetComponent(ctx context.Context, c client.Client, name, na } return toolkit.GetMockedResourceAndErrorFromContext(ctx, GetComponentContextKey, &applicationapiv1alpha1.Component{}) } + +// GetPipelineRunsWithPRGroupHash returns the resource and error passed as values of the context. +func (l *mockLoader) GetPipelineRunsWithPRGroupHash(ctx context.Context, c client.Client, snapshot *applicationapiv1alpha1.Snapshot, prGroupHash string) (*[]tektonv1.PipelineRun, error) { + if ctx.Value(GetBuildPLRContextKey) == nil { + return l.loader.GetPipelineRunsWithPRGroupHash(ctx, c, snapshot, prGroupHash) + } + 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, snapshot *applicationapiv1alpha1.Snapshot, componentName, prGroupHash string) (*[]applicationapiv1alpha1.Snapshot, error) { + if ctx.Value(GetComponentSnapshotsKey) == nil { + return l.loader.GetMatchingComponentSnapshotsForComponentAndPRGroupHash(ctx, c, snapshot, componentName, prGroupHash) + } + snapshots, err := toolkit.GetMockedResourceAndErrorFromContext(ctx, GetComponentSnapshotsKey, []applicationapiv1alpha1.Snapshot{}) + return &snapshots, err +} diff --git a/loader/loader_mock_test.go b/loader/loader_mock_test.go index 0da53abd0..4ecf5bf81 100644 --- a/loader/loader_mock_test.go +++ b/loader/loader_mock_test.go @@ -290,4 +290,34 @@ var _ = Describe("Release Adapter", Ordered, func() { Expect(err).ToNot(HaveOccurred()) }) }) + + Context("When calling GetPipelineRunsWithPRGroupHash", 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.GetPipelineRunsWithPRGroupHash(mockContext, nil, nil, "") + Expect(resource).To(Equal(&plrs)) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + Context("When calling GetMatchingComponentSnapshotsForComponentAndPRGroupHash", func() { + It("returns resource and error from the context", func() { + snapshots := []applicationapiv1alpha1.Snapshot{} + mockContext := toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: GetComponentSnapshotsKey, + Resource: snapshots, + }, + }) + resource, err := loader.GetMatchingComponentSnapshotsForComponentAndPRGroupHash(mockContext, nil, nil, "", "") + Expect(resource).To(Equal(&snapshots)) + Expect(err).ToNot(HaveOccurred()) + }) + }) }) diff --git a/loader/loader_test.go b/loader/loader_test.go index a9b2d59e6..0427e169d 100644 --- a/loader/loader_test.go +++ b/loader/loader_test.go @@ -99,9 +99,11 @@ var _ = Describe("Loader", Ordered, func() { Name: snapshotName, Namespace: "default", Labels: map[string]string{ - gitops.SnapshotTypeLabel: "component", - gitops.SnapshotComponentLabel: "component-sample", - gitops.BuildPipelineRunNameLabel: "pipelinerun-sample", + gitops.SnapshotTypeLabel: "component", + gitops.SnapshotComponentLabel: "component-sample", + gitops.BuildPipelineRunNameLabel: "pipelinerun-sample", + gitops.PRGroupHashLabel: "featuresha", + gitops.PipelineAsCodeEventTypeLabel: "pull_request", }, Annotations: map[string]string{ gitops.PipelineAsCodeInstallationIDAnnotation: "123", @@ -211,14 +213,16 @@ var _ = Describe("Loader", Ordered, func() { Name: "pipelinerun-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", - "appstudio.openshift.io/application": applicationName, - "appstudio.openshift.io/snapshot": snapshotName, - "test.appstudio.openshift.io/scenario": integrationTestScenario.Name, + "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", + "appstudio.openshift.io/application": applicationName, + "appstudio.openshift.io/snapshot": snapshotName, + "test.appstudio.openshift.io/scenario": integrationTestScenario.Name, + "pipelinesascode.tekton.dev/event-type": "pull_request", + "test.appstudio.openshift.io/pr-group-sha": "featuresha", }, Annotations: map[string]string{ "appstudio.redhat.com/updateComponentOnSuccess": "false", @@ -637,5 +641,21 @@ var _ = Describe("Loader", Ordered, func() { Expect(fetchedBuildComponent.Namespace).To(Equal(hasComp.Namespace)) Expect(fetchedBuildComponent.Spec).To(Equal(hasComp.Spec)) }) + + It("Can get build plr with pr group hash", func() { + fetchedBuildPLRs, err := loader.GetPipelineRunsWithPRGroupHash(ctx, k8sClient, hasSnapshot, "featuresha") + 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 matching snapshot for component and pr group hash", func() { + fetchedSnapshots, err := loader.GetMatchingComponentSnapshotsForComponentAndPRGroupHash(ctx, k8sClient, hasSnapshot, hasComp.Name, "featuresha") + Expect(err).To(Succeed()) + Expect((*fetchedSnapshots)[0].Name).To(Equal(hasSnapshot.Name)) + Expect((*fetchedSnapshots)[0].Namespace).To(Equal(hasSnapshot.Namespace)) + Expect((*fetchedSnapshots)[0].Spec).To(Equal(hasSnapshot.Spec)) + }) }) }) diff --git a/status/mock_status.go b/status/mock_status.go index 26f486b69..cfafcb030 100644 --- a/status/mock_status.go +++ b/status/mock_status.go @@ -54,6 +54,51 @@ func (mr *MockStatusInterfaceMockRecorder) GetReporter(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReporter", reflect.TypeOf((*MockStatusInterface)(nil).GetReporter), arg0) } +// IsMRInSnapshotOpened mocks base method. +func (m *MockStatusInterface) IsMRInSnapshotOpened(arg0 context.Context, arg1 ReporterInterface, arg2 *v1alpha1.Snapshot) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsMRInSnapshotOpened", arg0, arg1, arg2) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsMRInSnapshotOpened indicates an expected call of IsMRInSnapshotOpened. +func (mr *MockStatusInterfaceMockRecorder) IsMRInSnapshotOpened(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsMRInSnapshotOpened", reflect.TypeOf((*MockStatusInterface)(nil).IsMRInSnapshotOpened), arg0, arg1, arg2) +} + +// IsPRInSnapshotOpened mocks base method. +func (m *MockStatusInterface) IsPRInSnapshotOpened(arg0 context.Context, arg1 ReporterInterface, arg2 *v1alpha1.Snapshot) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsPRInSnapshotOpened", arg0, arg1, arg2) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsPRInSnapshotOpened indicates an expected call of IsPRInSnapshotOpened. +func (mr *MockStatusInterfaceMockRecorder) IsPRInSnapshotOpened(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsPRInSnapshotOpened", reflect.TypeOf((*MockStatusInterface)(nil).IsPRInSnapshotOpened), arg0, arg1, arg2) +} + +// IsPRMRInSnapshotOpened mocks base method. +func (m *MockStatusInterface) IsPRMRInSnapshotOpened(arg0 context.Context, arg1 *v1alpha1.Snapshot) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsPRMRInSnapshotOpened", arg0, arg1) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsPRMRInSnapshotOpened indicates an expected call of IsPRMRInSnapshotOpened. +func (mr *MockStatusInterfaceMockRecorder) IsPRMRInSnapshotOpened(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsPRMRInSnapshotOpened", reflect.TypeOf((*MockStatusInterface)(nil).IsPRMRInSnapshotOpened), arg0, arg1) +} + // ReportSnapshotStatus mocks base method. func (m *MockStatusInterface) ReportSnapshotStatus(arg0 context.Context, arg1 ReporterInterface, arg2 *v1alpha1.Snapshot) error { m.ctrl.T.Helper() @@ -62,7 +107,7 @@ func (m *MockStatusInterface) ReportSnapshotStatus(arg0 context.Context, arg1 Re return ret0 } -// ReportSnapshotStatus indicates an expected call of ReportSnapshotStatus. +// ReportSnapshotStatus indicates an expected call of ReportSnapshot func (mr *MockStatusInterfaceMockRecorder) ReportSnapshotStatus(arg0, arg1, arg2 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportSnapshotStatus", reflect.TypeOf((*MockStatusInterface)(nil).ReportSnapshotStatus), arg0, arg1, arg2) diff --git a/status/reporter_github.go b/status/reporter_github.go index dc31db673..49b5e8443 100644 --- a/status/reporter_github.go +++ b/status/reporter_github.go @@ -85,7 +85,7 @@ func NewCheckRunStatusUpdater( } } -func (cru *CheckRunStatusUpdater) getAppCredentials(ctx context.Context, object client.Object) (*appCredentials, error) { +func GetAppCredentials(ctx context.Context, k8sclient client.Client, object client.Object) (*appCredentials, error) { var err error var found bool appInfo := appCredentials{} @@ -97,7 +97,7 @@ func (cru *CheckRunStatusUpdater) getAppCredentials(ctx context.Context, object // Get the global pipelines as code secret pacSecret := v1.Secret{} - err = cru.k8sClient.Get(ctx, types.NamespacedName{Namespace: integrationNS, Name: PACSecret}, &pacSecret) + err = k8sclient.Get(ctx, types.NamespacedName{Namespace: integrationNS, Name: PACSecret}, &pacSecret) if err != nil { return nil, err } @@ -124,7 +124,7 @@ func (cru *CheckRunStatusUpdater) getAppCredentials(ctx context.Context, object // Authenticate Github Client with application credentials func (cru *CheckRunStatusUpdater) Authenticate(ctx context.Context, snapshot *applicationapiv1alpha1.Snapshot) error { - creds, err := cru.getAppCredentials(ctx, snapshot) + creds, err := GetAppCredentials(ctx, cru.k8sClient, snapshot) cru.creds = creds if err != nil { diff --git a/status/reporter_github_test.go b/status/reporter_github_test.go index 31e43a0fc..e8ac7ffaa 100644 --- a/status/reporter_github_test.go +++ b/status/reporter_github_test.go @@ -182,6 +182,13 @@ func (c *MockGitHubClient) GetAllCommitStatusesForRef( return []*ghapi.RepoStatus{repoStatus}, nil } +func (c *MockGitHubClient) GetPullRequest(ctx context.Context, owner string, repo string, prID int) (*ghapi.PullRequest, error) { + var id int64 = 60 + var state = "opened" + pullRequest := &ghapi.PullRequest{ID: &id, State: &state} + return pullRequest, nil +} + var _ = Describe("GitHubReporter", func() { var reporter *status.GitHubReporter diff --git a/status/status.go b/status/status.go index c22bf5142..ae50966bd 100644 --- a/status/status.go +++ b/status/status.go @@ -23,20 +23,25 @@ import ( "encoding/json" "errors" "fmt" + "net/url" "os" + "strconv" "time" "github.com/go-logr/logr" + "github.com/konflux-ci/integration-service/git/github" "github.com/konflux-ci/integration-service/gitops" "github.com/konflux-ci/integration-service/helpers" intgteststat "github.com/konflux-ci/integration-service/pkg/integrationteststatus" "github.com/konflux-ci/operator-toolkit/metadata" applicationapiv1alpha1 "github.com/redhat-appstudio/application-api/api/v1alpha1" tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + gitlab "github.com/xanzy/go-gitlab" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" ) // ScenarioReportStatus keep report status of git provider for the particular scenario @@ -183,6 +188,12 @@ func MigrateSnapshotToReportStatus(s *applicationapiv1alpha1.Snapshot, testStatu type StatusInterface interface { GetReporter(*applicationapiv1alpha1.Snapshot) ReporterInterface ReportSnapshotStatus(context.Context, ReporterInterface, *applicationapiv1alpha1.Snapshot) error + // Check if PR/MR is opened + IsPRMRInSnapshotOpened(context.Context, *applicationapiv1alpha1.Snapshot) (bool, error) + // Check if github PR is open + IsPRInSnapshotOpened(context.Context, ReporterInterface, *applicationapiv1alpha1.Snapshot) (bool, error) + // Check if gitlab MR is open + IsMRInSnapshotOpened(context.Context, ReporterInterface, *applicationapiv1alpha1.Snapshot) (bool, error) } type Status struct { @@ -392,3 +403,133 @@ func getConsoleName() string { } return consoleName } + +func (s Status) IsPRMRInSnapshotOpened(ctx context.Context, snapshot *applicationapiv1alpha1.Snapshot) (bool, error) { + // need to rework reporter.Detect() function and reuse it here + githubReporter := NewGitHubReporter(s.logger, s.client) + if githubReporter.Detect(snapshot) { + err := githubReporter.Initialize(ctx, snapshot) + if err != nil { + return false, err + } + return s.IsPRInSnapshotOpened(ctx, githubReporter, snapshot) + } + + gitlabReporter := NewGitLabReporter(s.logger, s.client) + if gitlabReporter.Detect(snapshot) { + err := gitlabReporter.Initialize(ctx, snapshot) + if err != nil { + return false, err + } + return s.IsMRInSnapshotOpened(ctx, gitlabReporter, snapshot) + } + + return false, fmt.Errorf("invalid git provier, valid git provider must be one of github and gitlab") +} + +// IsMRInSnapshotOpened check if the gitlab merge request triggering snapshot is opened +func (s Status) IsMRInSnapshotOpened(ctx context.Context, reporter ReporterInterface, snapshot *applicationapiv1alpha1.Snapshot) (bool, error) { + log := log.FromContext(ctx) + token, err := GetPACGitProviderToken(ctx, s.client, snapshot) + if err != nil { + log.Error(err, "failed to get token from snapshot", + "snapshot.NameSpace", snapshot.Namespace, "snapshot.Name", snapshot.Name) + return false, fmt.Errorf("failed to get PAC token for gitlab provider: %w", err) + } + + annotations := snapshot.GetAnnotations() + repoUrl, ok := annotations[gitops.PipelineAsCodeRepoURLAnnotation] + if !ok { + return false, fmt.Errorf("failed to get value of %s annotation from the snapshot %s", gitops.PipelineAsCodeRepoURLAnnotation, snapshot.Name) + } + + burl, err := url.Parse(repoUrl) + if err != nil { + return false, fmt.Errorf("failed to parse repo-url: %w", err) + } + apiURL := fmt.Sprintf("%s://%s", burl.Scheme, burl.Host) + + gitLabClient, err := gitlab.NewClient(token, gitlab.WithBaseURL(apiURL)) + if err != nil { + return false, fmt.Errorf("failed to create gitlab client: %w", err) + } + + targetProjectIDstr, found := annotations[gitops.PipelineAsCodeTargetProjectIDAnnotation] + if !found { + return false, fmt.Errorf("target project ID annotation not found %q", gitops.PipelineAsCodeTargetProjectIDAnnotation) + } + targetProjectID, err := strconv.Atoi(targetProjectIDstr) + if err != nil { + return false, fmt.Errorf("failed to convert project ID '%s' to integer: %w", targetProjectIDstr, err) + } + + mergeRequestStr, found := annotations[gitops.PipelineAsCodePullRequestAnnotation] + if !found { + return false, fmt.Errorf("pull-request annotation not found %q", gitops.PipelineAsCodePullRequestAnnotation) + } + mergeRequest, err := strconv.Atoi(mergeRequestStr) + if err != nil { + return false, fmt.Errorf("failed to convert merge request number '%s' to integer: %w", mergeRequestStr, err) + } + + log.Info(fmt.Sprintf("try to find the status of merge request projectID/pulls %d/%d", targetProjectID, mergeRequest)) + getOpts := gitlab.GetMergeRequestsOptions{} + mr, _, err := gitLabClient.MergeRequests.GetMergeRequest(targetProjectID, mergeRequest, &getOpts) + if mr != nil { + log.Info(fmt.Sprintf("found merge request projectID/pulls %d/%d", targetProjectID, mergeRequest)) + return mr.State == "opened", err + } + log.Info(fmt.Sprintf("can not find merge request projectID/pulls %d/%d", targetProjectID, mergeRequest)) + return false, err +} + +// IsPRInSnapshotOpened check if the github pull request triggering snapshot is opened +func (s Status) IsPRInSnapshotOpened(ctx context.Context, reporter ReporterInterface, snapshot *applicationapiv1alpha1.Snapshot) (bool, error) { + log := log.FromContext(ctx) + ghClient := github.NewClient(s.logger) + githubAppCreds, err := GetAppCredentials(ctx, s.client, snapshot) + + if err != nil { + log.Error(err, "failed to get app credentials from Snapshot", + "snapshot.NameSpace", snapshot.Namespace, "snapshot.Name", snapshot.Name) + return false, err + } + + token, err := ghClient.CreateAppInstallationToken(ctx, githubAppCreds.AppID, githubAppCreds.InstallationID, githubAppCreds.PrivateKey) + if err != nil { + log.Error(err, "failed to create app installation token", + "githubAppCreds.AppID", githubAppCreds.AppID, "githubAppCreds.InstallationID", githubAppCreds.InstallationID) + return false, err + } + + ghClient.SetOAuthToken(ctx, token) + + labels := snapshot.GetLabels() + + owner, found := labels[gitops.PipelineAsCodeURLOrgLabel] + if !found { + return false, fmt.Errorf("org label not found %q", gitops.PipelineAsCodeURLOrgLabel) + } + + repo, found := labels[gitops.PipelineAsCodeURLRepositoryLabel] + if !found { + return false, fmt.Errorf("repository label not found %q", gitops.PipelineAsCodeURLRepositoryLabel) + } + + pullRequestStr, found := labels[gitops.PipelineAsCodePullRequestAnnotation] + if !found { + return false, fmt.Errorf("pull request label not found %q", gitops.PipelineAsCodePullRequestAnnotation) + } + + pullRequest, err := strconv.Atoi(pullRequestStr) + if err != nil { + return false, fmt.Errorf("failed to convert pull request number '%s' to integer: %w", pullRequestStr, err) + } + + log.Info(fmt.Sprintf("try to find the status of pull request owner/repo/pulls %s/%s/%d", owner, repo, pullRequest)) + pr, err := ghClient.GetPullRequest(ctx, owner, repo, pullRequest) + if pr != nil { + return *pr.State == "open", nil + } + return false, err +} diff --git a/tekton/build_pipeline.go b/tekton/build_pipeline.go index 98b6d1903..c327d463d 100644 --- a/tekton/build_pipeline.go +++ b/tekton/build_pipeline.go @@ -106,9 +106,9 @@ func AnnotateBuildPipelineRunWithCreateSnapshotAnnotation(ctx context.Context, p return AnnotateBuildPipelineRun(ctx, pipelineRun, h.CreateSnapshotAnnotationName, string(jsonResult), cl) } -// GetPRGroupNameFromBuildPLR gets the PR group from the substring before @ from +// GetPRGroupFromBuildPLR gets the PR group from the substring before @ from // the source-branch pac annotation, for main, it generate PR group with {source-branch}-{url-org} -func GetPRGroupNameFromBuildPLR(pipelineRun *tektonv1.PipelineRun) string { +func GetPRGroupFromBuildPLR(pipelineRun *tektonv1.PipelineRun) string { if prGroup, found := pipelineRun.ObjectMeta.Annotations[PipelineAsCodeSourceBranchAnnotation]; found { if prGroup == MainBranch || prGroup == MasterBranch && metadata.HasAnnotation(pipelineRun, PipelineAsCodeSourceRepoOrg) { prGroup = prGroup + "-" + pipelineRun.ObjectMeta.Annotations[PipelineAsCodeSourceRepoOrg] diff --git a/tekton/build_pipeline_test.go b/tekton/build_pipeline_test.go index c5208678b..50f573979 100644 --- a/tekton/build_pipeline_test.go +++ b/tekton/build_pipeline_test.go @@ -112,21 +112,21 @@ var _ = Describe("build pipeline", func() { Context("When a build pipelineRun exists", func() { It("can get PR group from build pipelineRun", func() { Expect(tekton.IsPLRCreatedByPACPushEvent(buildPipelineRun)).To(BeFalse()) - prGroup := tekton.GetPRGroupNameFromBuildPLR(buildPipelineRun) + prGroup := tekton.GetPRGroupFromBuildPLR(buildPipelineRun) Expect(prGroup).To(Equal("sourceBranch")) Expect(tekton.GenerateSHA(prGroup)).NotTo(BeNil()) }) It("can get PR group from build pipelineRun is source branch is main", func() { buildPipelineRun.Annotations[tekton.PipelineAsCodeSourceBranchAnnotation] = "main" - prGroup := tekton.GetPRGroupNameFromBuildPLR(buildPipelineRun) + prGroup := tekton.GetPRGroupFromBuildPLR(buildPipelineRun) Expect(prGroup).To(Equal("main-redhat")) Expect(tekton.GenerateSHA(prGroup)).NotTo(BeNil()) }) It("can get PR group from build pipelineRun is source branch has @ charactor", func() { buildPipelineRun.Annotations[tekton.PipelineAsCodeSourceBranchAnnotation] = "myfeature@change1" - prGroup := tekton.GetPRGroupNameFromBuildPLR(buildPipelineRun) + prGroup := tekton.GetPRGroupFromBuildPLR(buildPipelineRun) Expect(prGroup).To(Equal("myfeature")) Expect(tekton.GenerateSHA(prGroup)).NotTo(BeNil()) })