Skip to content

Commit

Permalink
feat(STONEINTG-998): support group snapshot
Browse files Browse the repository at this point in the history
* 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 <hongliu@redhat.com>
  • Loading branch information
hongweiliu17 committed Sep 2, 2024
1 parent de7f46e commit f62f3cd
Show file tree
Hide file tree
Showing 19 changed files with 1,531 additions and 47 deletions.
28 changes: 28 additions & 0 deletions docs/snapshot-controller.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,34 @@ flowchart TD
mark_snapshot_invalid --> continue_processing4
%%%%%%%%%%%%%%%%%%%%%%% Drawing EnsureGroupSnapshotExist() function
%% Node definitions
ensure5(Process further if: Snapshot has <b>neither</b> push event type label <br><b>nor</b> PRGroupCreation annotation)
validate_build_pipelinerun{Did all gotten build pipelineRun <br>under the same group <br>succeed <b>and</b> <br>component snapshot are already created?}
annotate_component_snapshot(<b>Annotate</b> component snapshot)
get_component_snapshots_and_sort(<b>Iterate</b> all application components and <br><b>get<b/> all component snapshots <br>for each component under the same pr group sha <br>then <b>sort</b> snapshots)
can_find_snapshotComponent_from_latest_snapshot(<b>Can</b> find the latest snapshot with open pull/merge request?)
add_snapshot_to_group_snapshot_candidate(<b>Add</b> snapshotComponent of component <br>to group snapshot components candidate)
get_snapshotComponent_from_gcl(<b>Get</b> snapshotComponent from <br>Global Candidate List)
create_group_snapshot(<b>Create</b> group snapshot for snasphotComponents)
annotate_component_snapshots_under_prgroupsha(<b>Annotate<b> component snapshots which <b>have</b> <br>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;
Expand Down
32 changes: 32 additions & 0 deletions git/github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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.
Expand All @@ -117,6 +123,7 @@ type Client struct {
checks ChecksService
issues IssuesService
repos RepositoriesService
pulls PullRequestsService
}

// GetAppsService returns either the default or custom Apps service.
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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
}
35 changes: 30 additions & 5 deletions git/github/github_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}}
Expand All @@ -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{
Expand Down Expand Up @@ -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),
)
})

Expand All @@ -223,13 +240,15 @@ 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")
Expect(client.GetAppsService()).ToNot(Equal(mockAppsSvc))
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() {
Expand Down Expand Up @@ -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"))
})
})
127 changes: 126 additions & 1 deletion gitops/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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"

Expand All @@ -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"

Expand Down Expand Up @@ -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, "")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -959,3 +980,107 @@ func FilterIntegrationTestScenariosWithContext(scenarios *[]v1beta2.IntegrationT
}
return &filteredScenarioList
}

// HasPRGroupProcessed check 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
}
Loading

0 comments on commit f62f3cd

Please sign in to comment.