From d41c3fcff4ea7d18e753977f5d63d5003becaa2f Mon Sep 17 00:00:00 2001 From: Yahav Itzhak Date: Thu, 26 Jan 2023 16:35:31 +0200 Subject: [PATCH] New Get Repository Environment Info API (#64) --- README.md | 31 +++++++++-- vcsclient/azurerepos.go | 5 ++ vcsclient/azurerepos_test.go | 8 +++ vcsclient/bitbucketcloud.go | 5 ++ vcsclient/bitbucketcloud_test.go | 9 +++ vcsclient/bitbucketcommon.go | 1 + vcsclient/bitbucketserver.go | 5 ++ vcsclient/bitbucketserver_test.go | 9 +++ vcsclient/common_test.go | 13 +++-- vcsclient/github.go | 55 +++++++++++++++++++ vcsclient/github_test.go | 34 ++++++++++++ vcsclient/gitlab.go | 6 ++ vcsclient/gitlab_test.go | 9 +++ vcsclient/gitlabcommon.go | 1 + .../repository_environment_response.json | 42 ++++++++++++++ vcsclient/vcsclient.go | 10 ++++ 16 files changed, 231 insertions(+), 12 deletions(-) create mode 100644 vcsclient/testdata/github/repository_environment_response.json diff --git a/README.md b/README.md index bf957d8f..7cc8f8b0 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ # Froggit-Go [![Frogbot](images/header.png)](#readme) + Froggit-Go is a Go library, allowing to perform actions on VCS providers. @@ -41,6 +42,7 @@ Currently supported providers are: [GitHub](#github), [Bitbucket Server](#bitbuc - [Get Commit By SHA](#get-commit-by-sha) - [Add Public SSH Key](#add-public-ssh-key) - [Get Repository Info](#get-repository-info) + - [Get Repository Environment Info](#get-repository-environment-info) - [Create a label](#create-a-label) - [Get a label](#get-a-label) - [List Pull Request Labels](#list-pull-request-labels) @@ -66,7 +68,7 @@ apiEndpoint := "https://github.example.com" token := "secret-github-token" // Logger // [Optional] -// Supported logger is a logger that implements the Log interface. +// Supported logger is a logger that implements the Log interface. // More information - https://github.com/jfrog/froggit-go/blob/master/vcsclient/logger.go logger := log.Default() @@ -86,7 +88,7 @@ apiEndpoint := "https://gitlab.example.com" token := "secret-gitlab-token" // Logger // [Optional] -// Supported logger is a logger that implements the Log interface. +// Supported logger is a logger that implements the Log interface. // More information - https://github.com/jfrog/froggit-go/blob/master/vcsclient/logger.go logger := logger @@ -106,7 +108,7 @@ apiEndpoint := "https://git.acme.com/rest" token := "secret-bitbucket-token" // Logger // [Optional] -// Supported logger is a logger that implements the Log interface. +// Supported logger is a logger that implements the Log interface. // More information - https://github.com/jfrog/froggit-go/blob/master/vcsclient/logger.go logger := log.Default() @@ -128,7 +130,7 @@ username := "bitbucket-user" token := "secret-bitbucket-token" // Logger // [Optional] -// Supported logger is a logger that implements the Log interface. +// Supported logger is a logger that implements the Log interface. // More information - https://github.com/jfrog/froggit-go/blob/master/vcsclient/logger.go logger := log.Default() @@ -148,7 +150,7 @@ apiEndpoint := "https://dev.azure.com/" token := "secret-azure-devops-token" // Logger // [Optional] -// Supported logger is a logger that implements the Log interface. +// Supported logger is a logger that implements the Log interface. // More information - https://github.com/jfrog/froggit-go/blob/master/vcsclient/logger.go logger := log.Default() // Project name @@ -420,6 +422,24 @@ repository := "jfrog-cli" repoInfo, err := client.GetRepositoryInfo(ctx, owner, repository) ``` +#### Get Repository Environment Info + +Notice - Get Repository Environment Info is currently supported on GitHub only. + +```go +// Go context +ctx := context.Background() +// Organization or username +owner := "jfrog" +// VCS repository +repository := "jfrog-cli" +// Environment name +name := "frogbot" + +// Get information about repository environment +repoEnvInfo, err := client.GetRepositoryEnvironmentInfo(ctx, owner, repository, name) +``` + #### Create a label Notice - Labels are not supported in Bitbucket @@ -552,4 +572,3 @@ provider := vcsutils.GitHub webhookInfo, err := webhookparser.ParseIncomingWebhook(provider, token, request) ``` - diff --git a/vcsclient/azurerepos.go b/vcsclient/azurerepos.go index 8b8200d3..01595a2e 100644 --- a/vcsclient/azurerepos.go +++ b/vcsclient/azurerepos.go @@ -360,3 +360,8 @@ func (client *AzureReposClient) SetCommitStatus(ctx context.Context, commitStatu func (client *AzureReposClient) DownloadFileFromRepo(ctx context.Context, owner, repository, branch, path string) ([]byte, int, error) { return nil, 0, getUnsupportedInAzureError("download file from repo") } + +// GetRepositoryEnvironmentInfo on GitLab +func (client *AzureReposClient) GetRepositoryEnvironmentInfo(ctx context.Context, owner, repository, name string) (RepositoryEnvironmentInfo, error) { + return RepositoryEnvironmentInfo{}, getUnsupportedInAzureError("get repository environment info") +} diff --git a/vcsclient/azurerepos_test.go b/vcsclient/azurerepos_test.go index 09b19520..423998e7 100644 --- a/vcsclient/azurerepos_test.go +++ b/vcsclient/azurerepos_test.go @@ -365,6 +365,14 @@ func TestAzureReposClient_GetLabel(t *testing.T) { assert.Error(t, err) } +func TestAzureReposClient_GetRepositoryEnvironmentInfo(t *testing.T) { + ctx := context.Background() + client, cleanUp := createServerAndClient(t, vcsutils.AzureRepos, true, "", "unsupportedTest", createAzureReposHandler) + defer cleanUp() + _, err := client.GetRepositoryEnvironmentInfo(ctx, owner, repo1, envName) + assert.Error(t, err) +} + func TestGetUnsupportedInAzureError(t *testing.T) { functionName := "foo" assert.Error(t, getUnsupportedInAzureError(functionName)) diff --git a/vcsclient/bitbucketcloud.go b/vcsclient/bitbucketcloud.go index f626f4a8..f013cbf2 100644 --- a/vcsclient/bitbucketcloud.go +++ b/vcsclient/bitbucketcloud.go @@ -466,6 +466,11 @@ func (client *BitbucketCloudClient) DownloadFileFromRepo(ctx context.Context, ow return nil, 0, errBitbucketDownloadFileFromRepoNotSupported } +// GetRepositoryEnvironmentInfo on Bitbucket cloud +func (client *BitbucketCloudClient) GetRepositoryEnvironmentInfo(ctx context.Context, owner, repository, name string) (RepositoryEnvironmentInfo, error) { + return RepositoryEnvironmentInfo{}, errBitbucketGetRepoEnvironmentInfoNotSupported +} + func extractCommitFromResponse(commits interface{}) (*commitResponse, error) { var res commitResponse err := extractStructFromResponse(commits, &res) diff --git a/vcsclient/bitbucketcloud_test.go b/vcsclient/bitbucketcloud_test.go index 51038fe3..63e88283 100644 --- a/vcsclient/bitbucketcloud_test.go +++ b/vcsclient/bitbucketcloud_test.go @@ -430,6 +430,15 @@ func TestBitbucketCloud_UnlabelPullRequest(t *testing.T) { assert.ErrorIs(t, err, errLabelsNotSupported) } +func TestBitbucketCloud_GetRepositoryEnvironmentInfo(t *testing.T) { + ctx := context.Background() + client, err := NewClientBuilder(vcsutils.BitbucketCloud).Build() + assert.NoError(t, err) + + _, err = client.GetRepositoryEnvironmentInfo(ctx, owner, repo1, envName) + assert.ErrorIs(t, err, errBitbucketGetRepoEnvironmentInfoNotSupported) +} + func TestBitbucketCloud_getRepositoryVisibility(t *testing.T) { assert.Equal(t, Private, getBitbucketCloudRepositoryVisibility(&bitbucket.Repository{Is_private: true})) assert.Equal(t, Public, getBitbucketCloudRepositoryVisibility(&bitbucket.Repository{Is_private: false})) diff --git a/vcsclient/bitbucketcommon.go b/vcsclient/bitbucketcommon.go index d9ebb59d..5ba8718a 100644 --- a/vcsclient/bitbucketcommon.go +++ b/vcsclient/bitbucketcommon.go @@ -8,6 +8,7 @@ var errLabelsNotSupported = errors.New("labels are not supported on Bitbucket") var errBitbucketCodeScanningNotSupported = errors.New("code scanning is not supported on Bitbucket") var errBitbucketDownloadFileFromRepoNotSupported = errors.New("download file from repo is currently not supported on Bitbucket") +var errBitbucketGetRepoEnvironmentInfoNotSupported = errors.New("get repository environment info is currently not supported on Bitbucket") func getBitbucketCommitState(commitState CommitStatus) string { switch commitState { diff --git a/vcsclient/bitbucketserver.go b/vcsclient/bitbucketserver.go index ea85ef3a..feeb83f2 100644 --- a/vcsclient/bitbucketserver.go +++ b/vcsclient/bitbucketserver.go @@ -523,6 +523,11 @@ func (client *BitbucketServerClient) UnlabelPullRequest(ctx context.Context, own return errLabelsNotSupported } +// GetRepositoryEnvironmentInfo on Bitbucket server +func (client *BitbucketServerClient) GetRepositoryEnvironmentInfo(ctx context.Context, owner, repository, name string) (RepositoryEnvironmentInfo, error) { + return RepositoryEnvironmentInfo{}, errBitbucketGetRepoEnvironmentInfoNotSupported +} + // Get all projects for which the authenticated user has the PROJECT_VIEW permission func (client *BitbucketServerClient) listProjects(bitbucketClient *bitbucketv1.DefaultApiService) ([]string, error) { var apiResponse *bitbucketv1.APIResponse diff --git a/vcsclient/bitbucketserver_test.go b/vcsclient/bitbucketserver_test.go index 6a865404..e64571dd 100644 --- a/vcsclient/bitbucketserver_test.go +++ b/vcsclient/bitbucketserver_test.go @@ -448,6 +448,15 @@ func TestBitbucketServer_UnlabelPullRequest(t *testing.T) { assert.ErrorIs(t, err, errLabelsNotSupported) } +func TestBitbucketServer_GetRepositoryEnvironmentInfo(t *testing.T) { + ctx := context.Background() + client, err := NewClientBuilder(vcsutils.BitbucketServer).Build() + assert.NoError(t, err) + + _, err = client.GetRepositoryEnvironmentInfo(ctx, owner, repo1, envName) + assert.ErrorIs(t, err, errBitbucketGetRepoEnvironmentInfoNotSupported) +} + func TestBitbucketServer_GetCommitBySha(t *testing.T) { ctx := context.Background() sha := "abcdef0123abcdef4567abcdef8987abcdef6543" diff --git a/vcsclient/common_test.go b/vcsclient/common_test.go index 72b70fb4..416ca7ac 100644 --- a/vcsclient/common_test.go +++ b/vcsclient/common_test.go @@ -19,12 +19,13 @@ const ( ) var ( - repo1 = "repo-1" - repo2 = "repo-2" - username = "frogger" - branch1 = "branch-1" - branch2 = "branch-2" - labelName = "🚀 label-name" + repo1 = "repo-1" + repo2 = "repo-2" + username = "frogger" + branch1 = "branch-1" + branch2 = "branch-2" + labelName = "🚀 label-name" + envName = "frogbot" ) type createHandlerFunc func(t *testing.T, expectedUri string, response []byte, expectedStatusCode int) http.HandlerFunc diff --git a/vcsclient/github.go b/vcsclient/github.go index 18ca8713..d4327397 100644 --- a/vcsclient/github.go +++ b/vcsclient/github.go @@ -13,6 +13,7 @@ import ( "github.com/google/go-github/v45/github" "github.com/grokify/mogo/encoding/base64" "github.com/jfrog/froggit-go/vcsutils" + "github.com/mitchellh/mapstructure" "golang.org/x/oauth2" ) @@ -533,6 +534,56 @@ func (client *GitHubClient) DownloadFileFromRepo(ctx context.Context, owner, rep return content, response.StatusCode, nil } +// GetRepositoryEnvironmentInfo on GitHub +func (client *GitHubClient) GetRepositoryEnvironmentInfo(ctx context.Context, owner, repository, name string) (RepositoryEnvironmentInfo, error) { + err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "name": name}) + if err != nil { + return RepositoryEnvironmentInfo{}, err + } + ghClient, err := client.buildGithubClient(ctx) + if err != nil { + return RepositoryEnvironmentInfo{}, err + } + + environment, resp, err := ghClient.Repositories.GetEnvironment(ctx, owner, repository, name) + if err != nil { + return RepositoryEnvironmentInfo{}, err + } + if err = vcsutils.CheckResponseStatusWithBody(resp.Response, http.StatusOK); err != nil { + return RepositoryEnvironmentInfo{}, err + } + + reviewers, err := extractGitHubEnvironmentReviewers(environment) + if err != nil { + return RepositoryEnvironmentInfo{}, err + } + + return RepositoryEnvironmentInfo{ + Name: *environment.Name, + Url: *environment.URL, + Reviewers: reviewers, + }, err +} + +// Extract code reviewers from environment +func extractGitHubEnvironmentReviewers(environment *github.Environment) ([]string, error) { + var reviewers []string + protectionRules := environment.ProtectionRules + if protectionRules == nil { + return reviewers, nil + } + reviewerStruct := repositoryEnvironmentReviewer{} + for _, rule := range protectionRules { + for _, reviewer := range rule.Reviewers { + if err := mapstructure.Decode(reviewer.Reviewer, &reviewerStruct); err != nil { + return []string{}, err + } + reviewers = append(reviewers, reviewerStruct.Login) + } + } + return reviewers, nil +} + func createGitHubHook(token, payloadURL string, webhookEvents ...vcsutils.WebhookEvent) *github.Hook { return &github.Hook{ Events: getGitHubWebhookEvents(webhookEvents...), @@ -636,3 +687,7 @@ func packScanningResult(data string) (string, error) { return compressedScan, err } + +type repositoryEnvironmentReviewer struct { + Login string `mapstructure:"login"` +} diff --git a/vcsclient/github_test.go b/vcsclient/github_test.go index 093d97e4..b849d27d 100644 --- a/vcsclient/github_test.go +++ b/vcsclient/github_test.go @@ -583,6 +583,40 @@ func TestGitHubClient_UploadScanningAnalysis(t *testing.T) { assert.Error(t, err) } +func TestGitHubClient_GetRepositoryEnvironmentInfo(t *testing.T) { + ctx := context.Background() + + response, err := os.ReadFile(filepath.Join("testdata", "github", "repository_environment_response.json")) + assert.NoError(t, err) + client, cleanUp := createServerAndClient(t, vcsutils.GitHub, false, response, fmt.Sprintf("/repos/jfrog/repo-1/environments/%s", envName), createGitHubHandler) + defer cleanUp() + + repositoryEnvironmentInfo, err := client.GetRepositoryEnvironmentInfo(ctx, owner, repo1, envName) + assert.NoError(t, err) + assert.Equal(t, envName, repositoryEnvironmentInfo.Name) + assert.Equal(t, "https://api.github.com/repos/superfrog/test-repo/environments/frogbot", repositoryEnvironmentInfo.Url) + assert.Equal(t, []string{"superfrog"}, repositoryEnvironmentInfo.Reviewers) + + _, err = createBadGitHubClient(t).GetRepositoryEnvironmentInfo(ctx, owner, repo1, envName) + assert.Error(t, err) +} + +func TestGitHubClient_ExtractGitHubEnvironmentReviewers(t *testing.T) { + reviewer1, reviewer2 := "reviewer-1", "reviewer-2" + environment := &github.Environment{ + ProtectionRules: []*github.ProtectionRule{{ + Reviewers: []*github.RequiredReviewer{ + {Reviewer: &repositoryEnvironmentReviewer{Login: reviewer1}}, + {Reviewer: &repositoryEnvironmentReviewer{Login: reviewer2}}, + }, + }}, + } + + actualReviewers, err := extractGitHubEnvironmentReviewers(environment) + assert.NoError(t, err) + assert.Equal(t, []string{reviewer1, reviewer2}, actualReviewers) +} + func createBadGitHubClient(t *testing.T) VcsClient { client, err := NewClientBuilder(vcsutils.GitHub).ApiEndpoint("https://bad^endpoint").Build() require.NoError(t, err) diff --git a/vcsclient/gitlab.go b/vcsclient/gitlab.go index 0ff40552..b6b639b8 100644 --- a/vcsclient/gitlab.go +++ b/vcsclient/gitlab.go @@ -386,10 +386,16 @@ func (client *GitLabClient) UnlabelPullRequest(ctx context.Context, owner, repos return err } +// UploadCodeScanning on GitLab func (client *GitLabClient) UploadCodeScanning(_ context.Context, _ string, _ string, _ string, _ string) (string, error) { return "", errGitLabCodeScanningNotSupported } +// GetRepositoryEnvironmentInfo on GitLab +func (client *GitLabClient) GetRepositoryEnvironmentInfo(ctx context.Context, owner, repository, name string) (RepositoryEnvironmentInfo, error) { + return RepositoryEnvironmentInfo{}, errGitLabGetRepoEnvironmentInfoNotSupported +} + // DownloadFileFromRepo on GitLab func (client *GitLabClient) DownloadFileFromRepo(_ context.Context, owner, repository, branch, path string) ([]byte, int, error) { file, response, err := client.glClient.RepositoryFiles.GetFile(getProjectID(owner, repository), path, &gitlab.GetFileOptions{Ref: &branch}) diff --git a/vcsclient/gitlab_test.go b/vcsclient/gitlab_test.go index dfa7b0ba..d7226705 100644 --- a/vcsclient/gitlab_test.go +++ b/vcsclient/gitlab_test.go @@ -462,6 +462,15 @@ func TestGitlabClient_UploadCodeScanning(t *testing.T) { assert.Error(t, err) } +func TestGitlabClient_GetRepositoryEnvironmentInfo(t *testing.T) { + ctx := context.Background() + client, cleanUp := createServerAndClient(t, vcsutils.GitLab, true, "", "unsupportedTest", createGitLabHandler) + defer cleanUp() + + _, err := client.GetRepositoryEnvironmentInfo(ctx, owner, repo1, envName) + assert.ErrorIs(t, err, errGitLabGetRepoEnvironmentInfoNotSupported) +} + func createGitLabHandler(t *testing.T, expectedURI string, response []byte, expectedStatusCode int) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.RequestURI == "/api/v4/" { diff --git a/vcsclient/gitlabcommon.go b/vcsclient/gitlabcommon.go index 72d2dee4..47d06e23 100644 --- a/vcsclient/gitlabcommon.go +++ b/vcsclient/gitlabcommon.go @@ -5,3 +5,4 @@ import ( ) var errGitLabCodeScanningNotSupported = errors.New("code scanning is not supported on Gitlab") +var errGitLabGetRepoEnvironmentInfoNotSupported = errors.New("get repository environment info is currently not supported on Bitbucket") diff --git a/vcsclient/testdata/github/repository_environment_response.json b/vcsclient/testdata/github/repository_environment_response.json new file mode 100644 index 00000000..c54200dc --- /dev/null +++ b/vcsclient/testdata/github/repository_environment_response.json @@ -0,0 +1,42 @@ +{ + "id": 458044593, + "node_id": "EN_kwDOFo0fF84bTTSx", + "name": "frogbot", + "url": "https://api.github.com/repos/superfrog/test-repo/environments/frogbot", + "html_url": "https://github.com/superfrog/test-repo/deployments/activity_log?environments_filter=frogbot", + "created_at": "2022-04-07T05:57:50Z", + "updated_at": "2022-04-07T05:57:50Z", + "protection_rules": [ + { + "id": 210348, + "node_id": "GA_kwDOFo0fF84AAzWs", + "type": "required_reviewers", + "reviewers": [ + { + "type": "User", + "reviewer": { + "login": "superfrog", + "id": 11367982, + "node_id": "MDQ6VXNlcjExMzY3OTgy", + "avatar_url": "https://avatars.githubusercontent.com/u/11367982?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/superfrog", + "html_url": "https://github.com/superfrog", + "followers_url": "https://api.github.com/users/superfrog/followers", + "following_url": "https://api.github.com/users/superfrog/following{/other_user}", + "gists_url": "https://api.github.com/users/superfrog/gists{/gist_id}", + "starred_url": "https://api.github.com/users/superfrog/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/superfrog/subscriptions", + "organizations_url": "https://api.github.com/users/superfrog/orgs", + "repos_url": "https://api.github.com/users/superfrog/repos", + "events_url": "https://api.github.com/users/superfrog/events{/privacy}", + "received_events_url": "https://api.github.com/users/superfrog/received_events", + "type": "User", + "site_admin": false + } + } + ] + } + ], + "deployment_branch_policy": null +} \ No newline at end of file diff --git a/vcsclient/vcsclient.go b/vcsclient/vcsclient.go index fa8c324c..3cae8d4d 100644 --- a/vcsclient/vcsclient.go +++ b/vcsclient/vcsclient.go @@ -54,6 +54,13 @@ type VcsInfo struct { Project string } +// RepositoryEnvironmentInfo is the environment details configured for a repository +type RepositoryEnvironmentInfo struct { + Name string + Url string + Reviewers []string +} + // VcsClient is a base class of all Vcs clients - GitHub, GitLab, Bitbucket server and cloud clients type VcsClient interface { // TestConnection Returns nil if connection and authorization established successfully @@ -199,6 +206,9 @@ type VcsClient interface { // branch - The name of the branch // path - The path to the requested file DownloadFileFromRepo(ctx context.Context, owner, repository, branch, path string) ([]byte, int, error) + + // GetRepositoryEnvironmentInfo Gets the environment info configured for a repository + GetRepositoryEnvironmentInfo(ctx context.Context, owner, repository, name string) (RepositoryEnvironmentInfo, error) } // CommitInfo contains the details of a commit