Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(STONEINTG-998): support group snapshot #842

Merged
merged 1 commit into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

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