From 0f878f49157041938222d2221373da373ebfa10a Mon Sep 17 00:00:00 2001 From: zbenhadi Date: Tue, 25 Apr 2023 14:03:41 +0200 Subject: [PATCH 1/5] Add org-slug and org-id flags to orb validate & process commands --- api/api.go | 11 ++- api/collaborators/collaborators.go | 23 ++++++ api/collaborators/collaborators_rest.go | 36 +++++++++ cmd/orb.go | 100 +++++++++++++++++++++--- cmd/orb_test.go | 14 ++-- 5 files changed, 161 insertions(+), 23 deletions(-) create mode 100644 api/collaborators/collaborators.go create mode 100644 api/collaborators/collaborators_rest.go diff --git a/api/api.go b/api/api.go index ac29d7c1f..f94c64401 100644 --- a/api/api.go +++ b/api/api.go @@ -513,7 +513,7 @@ func WhoamiQuery(cl *graphql.Client) (*WhoamiResponse, error) { } // OrbQuery validated and processes an orb. -func OrbQuery(cl *graphql.Client, configPath string) (*ConfigResponse, error) { +func OrbQuery(cl *graphql.Client, configPath string, ownerId string) (*ConfigResponse, error) { var response OrbConfigResponse config, err := loadYaml(configPath) @@ -522,8 +522,8 @@ func OrbQuery(cl *graphql.Client, configPath string) (*ConfigResponse, error) { } query := ` - query ValidateOrb ($config: String!) { - orbConfig(orbYaml: $config) { + query ValidateOrb ($config: String!, $owner: UUID) { + orbConfig(orbYaml: $config, ownerId: $owner) { valid, errors { message }, sourceYaml, @@ -533,6 +533,11 @@ func OrbQuery(cl *graphql.Client, configPath string) (*ConfigResponse, error) { request := graphql.NewRequest(query) request.Var("config", config) + + if ownerId != "" { + request.Var("owner", ownerId) + } + request.SetToken(cl.Token) err = cl.Run(request, &response) diff --git a/api/collaborators/collaborators.go b/api/collaborators/collaborators.go new file mode 100644 index 000000000..94e06e42d --- /dev/null +++ b/api/collaborators/collaborators.go @@ -0,0 +1,23 @@ +package collaborators + +type CollaborationResult struct { + VcsTye string `json:"vcs_type"` + OrgSlug string `json:"slug"` + OrgName string `json:"name"` + OrgId string `json:"id"` + AvatarUrl string `json:"avatar_url"` +} + +type CollaboratorsClient interface { + GetOrgCollaborations() ([]CollaborationResult, error) +} + +// GetOrgIdFromSlug - converts a slug into an orgID. +func GetOrgIdFromSlug(slug string, collaborations []CollaborationResult) string { + for _, v := range collaborations { + if v.OrgSlug == slug { + return v.OrgId + } + } + return "" +} diff --git a/api/collaborators/collaborators_rest.go b/api/collaborators/collaborators_rest.go new file mode 100644 index 000000000..a15eea80b --- /dev/null +++ b/api/collaborators/collaborators_rest.go @@ -0,0 +1,36 @@ +package collaborators + +import ( + "net/url" + + "github.com/CircleCI-Public/circleci-cli/api/rest" + "github.com/CircleCI-Public/circleci-cli/settings" +) + +var ( + CollaborationsPath = "me/collaborations" +) + +type collaboratorsRestClient struct { + client *rest.Client +} + +// NewCollaboratorsRestClient returns a new collaboratorsRestClient satisfying the api.CollaboratorsClient +// interface via the REST API. +func NewCollaboratorsRestClient(config settings.Config) (*collaboratorsRestClient, error) { + client := &collaboratorsRestClient{ + client: rest.NewFromConfig(config.Host, &config), + } + return client, nil +} + +func (c *collaboratorsRestClient) GetOrgCollaborations() ([]CollaborationResult, error) { + req, err := c.client.NewRequest("GET", &url.URL{Path: CollaborationsPath}, nil) + if err != nil { + return nil, err + } + + var resp []CollaborationResult + _, err = c.client.DoRequest(req, &resp) + return resp, err +} diff --git a/cmd/orb.go b/cmd/orb.go index b0724c18c..c244283be 100644 --- a/cmd/orb.go +++ b/cmd/orb.go @@ -18,6 +18,7 @@ import ( "time" "github.com/CircleCI-Public/circleci-cli/api" + "github.com/CircleCI-Public/circleci-cli/api/collaborators" "github.com/CircleCI-Public/circleci-cli/api/graphql" "github.com/CircleCI-Public/circleci-cli/filetree" "github.com/CircleCI-Public/circleci-cli/process" @@ -43,9 +44,10 @@ import ( ) type orbOptions struct { - cfg *settings.Config - cl *graphql.Client - args []string + cfg *settings.Config + cl *graphql.Client + args []string + collaborators collaborators.CollaboratorsClient color string @@ -62,6 +64,11 @@ type orbOptions struct { integrationTesting bool } +type orbOrgOptions struct { + OrgID string + OrgSlug string +} + var orbAnnotations = map[string]string{ "": "The path to your orb (use \"-\" for STDIN)", "": "The namespace used for the orb (i.e. circleci)", @@ -93,9 +100,16 @@ func (ui createOrbTestUI) askUserToConfirm(message string) bool { } func newOrbCommand(config *settings.Config) *cobra.Command { + collaborators, err := collaborators.NewCollaboratorsRestClient(*config) + + if err != nil { + panic(err) + } + opts := orbOptions{ - cfg: config, - tty: createOrbInteractiveUI{}, + cfg: config, + tty: createOrbInteractiveUI{}, + collaborators: collaborators, } listCommand := &cobra.Command{ @@ -121,13 +135,23 @@ func newOrbCommand(config *settings.Config) *cobra.Command { validateCommand := &cobra.Command{ Use: "validate ", Short: "Validate an orb.yml", - RunE: func(_ *cobra.Command, _ []string) error { - return validateOrb(opts) + RunE: func(cmd *cobra.Command, _ []string) error { + orgID, _ := cmd.Flags().GetString("org-id") + orgSlug, _ := cmd.Flags().GetString("org-slug") + + org := orbOrgOptions{ + OrgID: orgID, + OrgSlug: orgSlug, + } + + return validateOrb(opts, org) }, Args: cobra.ExactArgs(1), Annotations: make(map[string]string), } validateCommand.Annotations[""] = orbAnnotations[""] + validateCommand.Flags().String("org-slug", "", "organization slug (for example: github/example-org), used when an orb depends on private orbs belonging to that org") + validateCommand.Flags().String("org-id", "", "organization id used when an orb depends on private orbs belonging to that org") processCommand := &cobra.Command{ Use: "process ", @@ -141,14 +165,24 @@ func newOrbCommand(config *settings.Config) *cobra.Command { opts.args = args opts.cl = graphql.NewClient(config.HTTPClient, config.Host, config.Endpoint, config.Token, config.Debug) }, - RunE: func(_ *cobra.Command, _ []string) error { - return processOrb(opts) + RunE: func(cmd *cobra.Command, _ []string) error { + orgID, _ := cmd.Flags().GetString("org-id") + orgSlug, _ := cmd.Flags().GetString("org-slug") + + org := orbOrgOptions{ + OrgID: orgID, + OrgSlug: orgSlug, + } + + return processOrb(opts, org) }, Args: cobra.ExactArgs(1), Annotations: make(map[string]string), } processCommand.Example = ` circleci orb process src/my-orb/@orb.yml` processCommand.Annotations[""] = orbAnnotations[""] + processCommand.Flags().String("org-slug", "", "organization slug (for example: github/example-org), used when an orb depends on private orbs belonging to that org") + processCommand.Flags().String("org-id", "", "organization id used when an orb depends on private orbs belonging to that org") publishCommand := &cobra.Command{ Use: "publish ", @@ -691,8 +725,14 @@ func listNamespaceOrbs(opts orbOptions) error { return logOrbs(*orbs, opts) } -func validateOrb(opts orbOptions) error { - _, err := api.OrbQuery(opts.cl, opts.args[0]) +func validateOrb(opts orbOptions, org orbOrgOptions) error { + orgId, err := (&opts).getOrgId(org) + + if err != nil { + return fmt.Errorf("failed to get the appropriate org-id: %s", err.Error()) + } + + _, err = api.OrbQuery(opts.cl, opts.args[0], orgId) if err != nil { return err @@ -707,8 +747,16 @@ func validateOrb(opts orbOptions) error { return nil } -func processOrb(opts orbOptions) error { - response, err := api.OrbQuery(opts.cl, opts.args[0]) +func processOrb(opts orbOptions, org orbOrgOptions) error { + orgId, err := (&opts).getOrgId(org) + + if err != nil { + return fmt.Errorf("failed to get the appropriate org-id: %s", err.Error()) + } + + _, err = api.OrbQuery(opts.cl, opts.args[0], orgId) + + response, err := api.OrbQuery(opts.cl, opts.args[0], orgId) if err != nil { return err @@ -1746,3 +1794,29 @@ func orbInformThatOrbCannotBeDeletedMessage() { fmt.Println("Once an orb is created it cannot be deleted. Orbs are semver compliant, and each published version is immutable. Publicly released orbs are potential dependencies for other projects.") fmt.Println("Therefore, allowing orb deletion would make users susceptible to unexpected loss of functionality.") } + +func (o *orbOptions) getOrgId(orgInfo orbOrgOptions) (string, error) { + if orgInfo.OrgID == "" && orgInfo.OrgSlug == "" { + return "", nil + } + + var orgID string + if strings.TrimSpace(orgInfo.OrgID) != "" { + orgID = orgInfo.OrgID + } else if strings.TrimSpace(orgInfo.OrgSlug) != "" { + orgs, err := o.collaborators.GetOrgCollaborations() + if err != nil { + return "", err + } + + orgID = collaborators.GetOrgIdFromSlug(orgInfo.OrgSlug, orgs) + + if orgID == "" { + fmt.Println("Could not fetch a valid org-id from collaborators endpoint.") + fmt.Println("Check if you have access to this org by hitting https://circleci.com/api/v2/me/collaborations") + fmt.Println("Continuing on - private orb resolution will not work as intended") + } + } + + return orgID, nil +} diff --git a/cmd/orb_test.go b/cmd/orb_test.go index e5a5f42b3..ded63dbe3 100644 --- a/cmd/orb_test.go +++ b/cmd/orb_test.go @@ -124,8 +124,8 @@ See a full explanation and documentation on orbs here: https://circleci.com/docs } `json:"variables"` }{ Query: ` - query ValidateOrb ($config: String!) { - orbConfig(orbYaml: $config) { + query ValidateOrb ($config: String!, $owner: UUID) { + orbConfig(orbYaml: $config, ownerId: $owner) { valid, errors { message }, sourceYaml, @@ -184,7 +184,7 @@ See a full explanation and documentation on orbs here: https://circleci.com/docs }` expectedRequestJson := ` { - "query": "\n\t\tquery ValidateOrb ($config: String!) {\n\t\t\torbConfig(orbYaml: $config) {\n\t\t\t\tvalid,\n\t\t\t\terrors { message },\n\t\t\t\tsourceYaml,\n\t\t\t\toutputYaml\n\t\t\t}\n\t\t}", + "query": "\n\t\tquery ValidateOrb ($config: String!, $owner: UUID) {\n\t\t\torbConfig(orbYaml: $config, ownerId: $owner) {\n\t\t\t\tvalid,\n\t\t\t\terrors { message },\n\t\t\t\tsourceYaml,\n\t\t\t\toutputYaml\n\t\t\t}\n\t\t}", "variables": { "config": "{}" } @@ -231,7 +231,7 @@ See a full explanation and documentation on orbs here: https://circleci.com/docs }` expectedRequestJson := ` { - "query": "\n\t\tquery ValidateOrb ($config: String!) {\n\t\t\torbConfig(orbYaml: $config) {\n\t\t\t\tvalid,\n\t\t\t\terrors { message },\n\t\t\t\tsourceYaml,\n\t\t\t\toutputYaml\n\t\t\t}\n\t\t}", + "query": "\n\t\tquery ValidateOrb ($config: String!, $owner: UUID) {\n\t\t\torbConfig(orbYaml: $config, ownerId: $owner) {\n\t\t\t\tvalid,\n\t\t\t\terrors { message },\n\t\t\t\tsourceYaml,\n\t\t\t\toutputYaml\n\t\t\t}\n\t\t}", "variables": { "config": "some orb" } @@ -266,7 +266,7 @@ See a full explanation and documentation on orbs here: https://circleci.com/docs }` expectedRequestJson := ` { - "query": "\n\t\tquery ValidateOrb ($config: String!) {\n\t\t\torbConfig(orbYaml: $config) {\n\t\t\t\tvalid,\n\t\t\t\terrors { message },\n\t\t\t\tsourceYaml,\n\t\t\t\toutputYaml\n\t\t\t}\n\t\t}", + "query": "\n\t\tquery ValidateOrb ($config: String!, $owner: UUID) {\n\t\t\torbConfig(orbYaml: $config, ownerId: $owner) {\n\t\t\t\tvalid,\n\t\t\t\terrors { message },\n\t\t\t\tsourceYaml,\n\t\t\t\toutputYaml\n\t\t\t}\n\t\t}", "variables": { "config": "some orb" } @@ -309,7 +309,7 @@ See a full explanation and documentation on orbs here: https://circleci.com/docs }` expectedRequestJson := ` { - "query": "\n\t\tquery ValidateOrb ($config: String!) {\n\t\t\torbConfig(orbYaml: $config) {\n\t\t\t\tvalid,\n\t\t\t\terrors { message },\n\t\t\t\tsourceYaml,\n\t\t\t\toutputYaml\n\t\t\t}\n\t\t}", + "query": "\n\t\tquery ValidateOrb ($config: String!, $owner: UUID) {\n\t\t\torbConfig(orbYaml: $config, ownerId: $owner) {\n\t\t\t\tvalid,\n\t\t\t\terrors { message },\n\t\t\t\tsourceYaml,\n\t\t\t\toutputYaml\n\t\t\t}\n\t\t}", "variables": { "config": "some orb" } @@ -344,7 +344,7 @@ See a full explanation and documentation on orbs here: https://circleci.com/docs }` expectedRequestJson := ` { - "query": "\n\t\tquery ValidateOrb ($config: String!) {\n\t\t\torbConfig(orbYaml: $config) {\n\t\t\t\tvalid,\n\t\t\t\terrors { message },\n\t\t\t\tsourceYaml,\n\t\t\t\toutputYaml\n\t\t\t}\n\t\t}", + "query": "\n\t\tquery ValidateOrb ($config: String!, $owner: UUID) {\n\t\t\torbConfig(orbYaml: $config, ownerId: $owner) {\n\t\t\t\tvalid,\n\t\t\t\terrors { message },\n\t\t\t\tsourceYaml,\n\t\t\t\toutputYaml\n\t\t\t}\n\t\t}", "variables": { "config": "some orb" } From 395a504c5d72725ed439b1f47e8a9d54dfcf155a Mon Sep 17 00:00:00 2001 From: zbenhadi Date: Wed, 26 Apr 2023 07:09:20 +0200 Subject: [PATCH 2/5] fixed double OrbQuery call --- cmd/orb.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/cmd/orb.go b/cmd/orb.go index c244283be..141e50464 100644 --- a/cmd/orb.go +++ b/cmd/orb.go @@ -754,8 +754,6 @@ func processOrb(opts orbOptions, org orbOrgOptions) error { return fmt.Errorf("failed to get the appropriate org-id: %s", err.Error()) } - _, err = api.OrbQuery(opts.cl, opts.args[0], orgId) - response, err := api.OrbQuery(opts.cl, opts.args[0], orgId) if err != nil { From 34ea5bf531b00e605f6b938d587eb4a3e3833bb1 Mon Sep 17 00:00:00 2001 From: zbenhadi Date: Thu, 27 Apr 2023 11:13:09 +0200 Subject: [PATCH 3/5] org-slug flags for orb validate & config validate can have full vcs-name --- api/collaborators/collaborators.go | 13 +- api/collaborators/collaborators_rest.go | 28 ++ api/collaborators/collaborators_rest_test.go | 381 +++++++++++++++++++ cmd/orb.go | 32 +- config/collaborators.go | 39 -- config/collaborators_test.go | 43 --- config/commands.go | 34 +- config/config.go | 21 +- 8 files changed, 459 insertions(+), 132 deletions(-) create mode 100644 api/collaborators/collaborators_rest_test.go delete mode 100644 config/collaborators.go delete mode 100644 config/collaborators_test.go diff --git a/api/collaborators/collaborators.go b/api/collaborators/collaborators.go index 94e06e42d..6fecf61f9 100644 --- a/api/collaborators/collaborators.go +++ b/api/collaborators/collaborators.go @@ -1,7 +1,7 @@ package collaborators type CollaborationResult struct { - VcsTye string `json:"vcs_type"` + VcsType string `json:"vcs_type"` OrgSlug string `json:"slug"` OrgName string `json:"name"` OrgId string `json:"id"` @@ -9,15 +9,6 @@ type CollaborationResult struct { } type CollaboratorsClient interface { + GetCollaborationBySlug(slug string) (*CollaborationResult, error) GetOrgCollaborations() ([]CollaborationResult, error) } - -// GetOrgIdFromSlug - converts a slug into an orgID. -func GetOrgIdFromSlug(slug string, collaborations []CollaborationResult) string { - for _, v := range collaborations { - if v.OrgSlug == slug { - return v.OrgId - } - } - return "" -} diff --git a/api/collaborators/collaborators_rest.go b/api/collaborators/collaborators_rest.go index a15eea80b..e15f7774e 100644 --- a/api/collaborators/collaborators_rest.go +++ b/api/collaborators/collaborators_rest.go @@ -2,6 +2,7 @@ package collaborators import ( "net/url" + "strings" "github.com/CircleCI-Public/circleci-cli/api/rest" "github.com/CircleCI-Public/circleci-cli/settings" @@ -34,3 +35,30 @@ func (c *collaboratorsRestClient) GetOrgCollaborations() ([]CollaborationResult, _, err = c.client.DoRequest(req, &resp) return resp, err } + +func (c *collaboratorsRestClient) GetCollaborationBySlug(slug string) (*CollaborationResult, error) { + // Support for / as well as / for the slug + // requires splitting + collaborations, err := c.GetOrgCollaborations() + + if err != nil { + return nil, err + } + + slugParts := strings.Split(slug, "/") + + for _, v := range collaborations { + // The rest-api allways returns / as a slug + if v.OrgSlug == slug { + return &v, nil + } + + // Compare first part of argument slug with the VCSType + splitted := strings.Split(v.OrgSlug, "/") + if len(slugParts) >= 2 && len(splitted) >= 2 && slugParts[0] == v.VcsType && slugParts[1] == splitted[1] { + return &v, nil + } + } + + return nil, nil +} diff --git a/api/collaborators/collaborators_rest_test.go b/api/collaborators/collaborators_rest_test.go new file mode 100644 index 000000000..cc617c218 --- /dev/null +++ b/api/collaborators/collaborators_rest_test.go @@ -0,0 +1,381 @@ +package collaborators_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "github.com/CircleCI-Public/circleci-cli/api/collaborators" + "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/version" + "gotest.tools/v3/assert" +) + +func getCollaboratorsRestClient(server *httptest.Server) (collaborators.CollaboratorsClient, error) { + client := &http.Client{} + + return collaborators.NewCollaboratorsRestClient(settings.Config{ + RestEndpoint: "api/v2", + Host: server.URL, + HTTPClient: client, + Token: "token", + }) +} + +func Test_collaboratorsRestClient_GetOrgCollaborations(t *testing.T) { + tests := []struct { + name string + handler http.HandlerFunc + want []collaborators.CollaborationResult + wantErr bool + }{ + { + name: "Should handle a successfull request", + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Header.Get("circle-token"), "token") + assert.Equal(t, r.Header.Get("accept"), "application/json") + assert.Equal(t, r.Header.Get("user-agent"), version.UserAgent()) + + assert.Equal(t, r.Method, "GET") + assert.Equal(t, r.URL.Path, fmt.Sprintf("/api/v2/me/collaborations")) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte(` + [ + { + "vcs_type": "github", + "slug": "gh/example", + "name": "Example Org", + "id": "some-uuid-123", + "avatar_url": "http://placekitten.com/200/300" + }, + { + "vcs_type": "bitbucket", + "slug": "bb/other", + "name": "Other Org", + "id": "some-uuid-789", + "avatar_url": "http://placekitten.com/200/300" + } + ] + `)) + + assert.NilError(t, err) + }, + want: []collaborators.CollaborationResult{ + { + VcsType: "github", + OrgSlug: "gh/example", + OrgName: "Example Org", + OrgId: "some-uuid-123", + AvatarUrl: "http://placekitten.com/200/300", + }, + { + VcsType: "bitbucket", + OrgSlug: "bb/other", + OrgName: "Other Org", + OrgId: "some-uuid-789", + AvatarUrl: "http://placekitten.com/200/300", + }, + }, + wantErr: false, + }, + { + name: "Should handle an error request", + handler: func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("content-type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + _, err := w.Write([]byte(`{"message": "error"}`)) + assert.NilError(t, err) + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(tt.handler) + defer server.Close() + + c, err := getCollaboratorsRestClient(server) + assert.NilError(t, err) + + got, err := c.GetOrgCollaborations() + if (err != nil) != tt.wantErr { + t.Errorf("collaboratorsRestClient.GetOrgCollaborations() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("collaboratorsRestClient.GetOrgCollaborations() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_collaboratorsRestClient_GetCollaborationBySlug(t *testing.T) { + type args struct { + slug string + } + tests := []struct { + name string + handler http.HandlerFunc + args args + want *collaborators.CollaborationResult + wantErr bool + }{ + { + name: "Should work with short-vcs notation (github)", + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Header.Get("circle-token"), "token") + assert.Equal(t, r.Header.Get("accept"), "application/json") + assert.Equal(t, r.Header.Get("user-agent"), version.UserAgent()) + + assert.Equal(t, r.Method, "GET") + assert.Equal(t, r.URL.Path, fmt.Sprintf("/api/v2/me/collaborations")) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte(` + [ + { + "vcs_type": "github", + "slug": "gh/example", + "name": "Example Org", + "id": "some-uuid-123", + "avatar_url": "http://placekitten.com/200/300" + }, + { + "vcs_type": "bitbucket", + "slug": "bb/other", + "name": "Other Org", + "id": "some-uuid-789", + "avatar_url": "http://placekitten.com/200/300" + } + ] + `)) + + assert.NilError(t, err) + }, + args: args{ + slug: "gh/example", + }, + want: &collaborators.CollaborationResult{ + VcsType: "github", + OrgSlug: "gh/example", + OrgName: "Example Org", + OrgId: "some-uuid-123", + AvatarUrl: "http://placekitten.com/200/300", + }, + wantErr: false, + }, + { + name: "Should work with vcs-name notation (github)", + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Header.Get("circle-token"), "token") + assert.Equal(t, r.Header.Get("accept"), "application/json") + assert.Equal(t, r.Header.Get("user-agent"), version.UserAgent()) + + assert.Equal(t, r.Method, "GET") + assert.Equal(t, r.URL.Path, fmt.Sprintf("/api/v2/me/collaborations")) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte(` + [ + { + "vcs_type": "github", + "slug": "gh/example", + "name": "Example Org", + "id": "some-uuid-123", + "avatar_url": "http://placekitten.com/200/300" + }, + { + "vcs_type": "bitbucket", + "slug": "bb/other", + "name": "Other Org", + "id": "some-uuid-789", + "avatar_url": "http://placekitten.com/200/300" + } + ] + `)) + + assert.NilError(t, err) + }, + args: args{ + slug: "github/example", + }, + want: &collaborators.CollaborationResult{ + VcsType: "github", + OrgSlug: "gh/example", + OrgName: "Example Org", + OrgId: "some-uuid-123", + AvatarUrl: "http://placekitten.com/200/300", + }, + wantErr: false, + }, + { + name: "Should work with vcs-short notation (bitbucket)", + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Header.Get("circle-token"), "token") + assert.Equal(t, r.Header.Get("accept"), "application/json") + assert.Equal(t, r.Header.Get("user-agent"), version.UserAgent()) + + assert.Equal(t, r.Method, "GET") + assert.Equal(t, r.URL.Path, fmt.Sprintf("/api/v2/me/collaborations")) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte(` + [ + { + "vcs_type": "github", + "slug": "gh/example", + "name": "Example Org", + "id": "some-uuid-123", + "avatar_url": "http://placekitten.com/200/300" + }, + { + "vcs_type": "bitbucket", + "slug": "bb/other", + "name": "Other Org", + "id": "some-uuid-789", + "avatar_url": "http://placekitten.com/200/300" + } + ] + `)) + + assert.NilError(t, err) + }, + args: args{ + slug: "bb/other", + }, + want: &collaborators.CollaborationResult{ + VcsType: "bitbucket", + OrgSlug: "bb/other", + OrgName: "Other Org", + OrgId: "some-uuid-789", + AvatarUrl: "http://placekitten.com/200/300", + }, + wantErr: false, + }, + { + name: "Should work with vcs-name notation (bitbucket)", + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Header.Get("circle-token"), "token") + assert.Equal(t, r.Header.Get("accept"), "application/json") + assert.Equal(t, r.Header.Get("user-agent"), version.UserAgent()) + + assert.Equal(t, r.Method, "GET") + assert.Equal(t, r.URL.Path, fmt.Sprintf("/api/v2/me/collaborations")) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte(` + [ + { + "vcs_type": "github", + "slug": "gh/example", + "name": "Example Org", + "id": "some-uuid-123", + "avatar_url": "http://placekitten.com/200/300" + }, + { + "vcs_type": "bitbucket", + "slug": "bb/other", + "name": "Other Org", + "id": "some-uuid-789", + "avatar_url": "http://placekitten.com/200/300" + } + ] + `)) + + assert.NilError(t, err) + }, + args: args{ + slug: "bitbucket/other", + }, + want: &collaborators.CollaborationResult{ + VcsType: "bitbucket", + OrgSlug: "bb/other", + OrgName: "Other Org", + OrgId: "some-uuid-789", + AvatarUrl: "http://placekitten.com/200/300", + }, + wantErr: false, + }, + { + name: "Should return nil if not found", + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Header.Get("circle-token"), "token") + assert.Equal(t, r.Header.Get("accept"), "application/json") + assert.Equal(t, r.Header.Get("user-agent"), version.UserAgent()) + + assert.Equal(t, r.Method, "GET") + assert.Equal(t, r.URL.Path, fmt.Sprintf("/api/v2/me/collaborations")) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte(` + [ + { + "vcs_type": "github", + "slug": "gh/example", + "name": "Example Org", + "id": "some-uuid-123", + "avatar_url": "http://placekitten.com/200/300" + }, + { + "vcs_type": "bitbucket", + "slug": "bb/other", + "name": "Other Org", + "id": "some-uuid-789", + "avatar_url": "http://placekitten.com/200/300" + } + ] + `)) + + assert.NilError(t, err) + }, + args: args{ + slug: "bad-slug", + }, + want: nil, + wantErr: false, + }, + { + name: "Should error if request errors", + handler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("content-type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + _, err := w.Write([]byte(`{"message": "error"}`)) + assert.NilError(t, err) + }, + args: args{ + slug: "bad-slug", + }, + want: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(tt.handler) + defer server.Close() + + c, err := getCollaboratorsRestClient(server) + assert.NilError(t, err) + + got, err := c.GetCollaborationBySlug(tt.args.slug) + if (err != nil) != tt.wantErr { + t.Errorf("collaboratorsRestClient.GetCollaborationBySlug() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("collaboratorsRestClient.GetCollaborationBySlug() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cmd/orb.go b/cmd/orb.go index 141e50464..326b0a616 100644 --- a/cmd/orb.go +++ b/cmd/orb.go @@ -1794,27 +1794,27 @@ func orbInformThatOrbCannotBeDeletedMessage() { } func (o *orbOptions) getOrgId(orgInfo orbOrgOptions) (string, error) { - if orgInfo.OrgID == "" && orgInfo.OrgSlug == "" { + if strings.TrimSpace(orgInfo.OrgID) != "" { + return orgInfo.OrgID, nil + } + + if strings.TrimSpace(orgInfo.OrgSlug) == "" { return "", nil } - var orgID string - if strings.TrimSpace(orgInfo.OrgID) != "" { - orgID = orgInfo.OrgID - } else if strings.TrimSpace(orgInfo.OrgSlug) != "" { - orgs, err := o.collaborators.GetOrgCollaborations() - if err != nil { - return "", err - } + coll, err := o.collaborators.GetCollaborationBySlug(orgInfo.OrgSlug) - orgID = collaborators.GetOrgIdFromSlug(orgInfo.OrgSlug, orgs) + if err != nil { + return "", err + } - if orgID == "" { - fmt.Println("Could not fetch a valid org-id from collaborators endpoint.") - fmt.Println("Check if you have access to this org by hitting https://circleci.com/api/v2/me/collaborations") - fmt.Println("Continuing on - private orb resolution will not work as intended") - } + if coll == nil { + fmt.Println("Could not fetch a valid org-id from collaborators endpoint.") + fmt.Println("Check if you have access to this org by hitting https://circleci.com/api/v2/me/collaborations") + fmt.Println("Continuing on - private orb resolution will not work as intended") + + return "", nil } - return orgID, nil + return coll.OrgId, nil } diff --git a/config/collaborators.go b/config/collaborators.go deleted file mode 100644 index 30da51d17..000000000 --- a/config/collaborators.go +++ /dev/null @@ -1,39 +0,0 @@ -package config - -import ( - "net/url" -) - -var ( - CollaborationsPath = "me/collaborations" -) - -type CollaborationResult struct { - VcsTye string `json:"vcs_type"` - OrgSlug string `json:"slug"` - OrgName string `json:"name"` - OrgId string `json:"id"` - AvatarUrl string `json:"avatar_url"` -} - -// GetOrgCollaborations - fetches all the collaborations for a given user. -func (c *ConfigCompiler) GetOrgCollaborations() ([]CollaborationResult, error) { - req, err := c.collaboratorRestClient.NewRequest("GET", &url.URL{Path: CollaborationsPath}, nil) - if err != nil { - return nil, err - } - - var resp []CollaborationResult - _, err = c.collaboratorRestClient.DoRequest(req, &resp) - return resp, err -} - -// GetOrgIdFromSlug - converts a slug into an orgID. -func GetOrgIdFromSlug(slug string, collaborations []CollaborationResult) string { - for _, v := range collaborations { - if v.OrgSlug == slug { - return v.OrgId - } - } - return "" -} diff --git a/config/collaborators_test.go b/config/collaborators_test.go deleted file mode 100644 index 115c4f8c9..000000000 --- a/config/collaborators_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package config - -import ( - "fmt" - "net/http" - "net/http/httptest" - "testing" - - "github.com/CircleCI-Public/circleci-cli/settings" - "github.com/stretchr/testify/assert" -) - -func TestGetOrgCollaborations(t *testing.T) { - svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - fmt.Fprintf(w, `[{"vcs_type":"circleci","slug":"gh/test","id":"2345"}]`) - })) - defer svr.Close() - compiler := New(&settings.Config{Host: svr.URL, HTTPClient: http.DefaultClient}) - - t.Run("assert compiler has correct host", func(t *testing.T) { - assert.Equal(t, "http://"+compiler.collaboratorRestClient.BaseURL.Host, svr.URL) - }) - - t.Run("getOrgCollaborations can parse response correctly", func(t *testing.T) { - collabs, err := compiler.GetOrgCollaborations() - assert.NoError(t, err) - assert.Equal(t, 1, len(collabs)) - assert.Equal(t, "circleci", collabs[0].VcsTye) - }) - - t.Run("can fetch orgID from a slug", func(t *testing.T) { - expected := "1234" - actual := GetOrgIdFromSlug("gh/test", []CollaborationResult{{OrgSlug: "gh/test", OrgId: "1234"}}) - assert.Equal(t, expected, actual) - }) - - t.Run("returns empty if no slug match", func(t *testing.T) { - expected := "" - actual := GetOrgIdFromSlug("gh/doesntexist", []CollaborationResult{{OrgSlug: "gh/test", OrgId: "1234"}}) - assert.Equal(t, expected, actual) - }) -} diff --git a/config/commands.go b/config/commands.go index 64504939f..ef3682145 100644 --- a/config/commands.go +++ b/config/commands.go @@ -39,27 +39,29 @@ func (c *ConfigCompiler) getOrgID( optsOrgID string, optsOrgSlug string, ) (string, error) { - if optsOrgID == "" && optsOrgSlug == "" { + if strings.TrimSpace(optsOrgID) != "" { + return optsOrgID, nil + } + + if strings.TrimSpace(optsOrgSlug) == "" { return "", nil } - var orgID string - if strings.TrimSpace(optsOrgID) != "" { - orgID = optsOrgID - } else { - orgs, err := c.GetOrgCollaborations() - if err != nil { - return "", err - } - orgID = GetOrgIdFromSlug(optsOrgSlug, orgs) - if orgID == "" { - fmt.Println("Could not fetch a valid org-id from collaborators endpoint.") - fmt.Println("Check if you have access to this org by hitting https://circleci.com/api/v2/me/collaborations") - fmt.Println("Continuing on - private orb resolution will not work as intended") - } + coll, err := c.collaborators.GetCollaborationBySlug(optsOrgSlug) + + if err != nil { + return "", err + } + + if coll == nil { + fmt.Println("Could not fetch a valid org-id from collaborators endpoint.") + fmt.Println("Check if you have access to this org by hitting https://circleci.com/api/v2/me/collaborations") + fmt.Println("Continuing on - private orb resolution will not work as intended") + + return "", nil } - return orgID, nil + return coll.OrgId, nil } func (c *ConfigCompiler) ProcessConfig(opts ProcessConfigOpts) error { diff --git a/config/config.go b/config/config.go index b8cb5921b..d76dcad51 100644 --- a/config/config.go +++ b/config/config.go @@ -6,6 +6,7 @@ import ( "net/url" "os" + "github.com/CircleCI-Public/circleci-cli/api/collaborators" "github.com/CircleCI-Public/circleci-cli/api/graphql" "github.com/CircleCI-Public/circleci-cli/api/rest" "github.com/CircleCI-Public/circleci-cli/settings" @@ -21,9 +22,9 @@ var ( ) type ConfigCompiler struct { - host string - compileRestClient *rest.Client - collaboratorRestClient *rest.Client + host string + compileRestClient *rest.Client + collaborators collaborators.CollaboratorsClient cfg *settings.Config legacyGraphQLClient *graphql.Client @@ -31,11 +32,17 @@ type ConfigCompiler struct { func New(cfg *settings.Config) *ConfigCompiler { hostValue := getCompileHost(cfg.Host) + collaboratorsClient, err := collaborators.NewCollaboratorsRestClient(*cfg) + + if err != nil { + panic(err) + } + configCompiler := &ConfigCompiler{ - host: hostValue, - compileRestClient: rest.NewFromConfig(hostValue, cfg), - collaboratorRestClient: rest.NewFromConfig(cfg.Host, cfg), - cfg: cfg, + host: hostValue, + compileRestClient: rest.NewFromConfig(hostValue, cfg), + collaborators: collaboratorsClient, + cfg: cfg, } configCompiler.legacyGraphQLClient = graphql.NewClient(cfg.HTTPClient, cfg.Host, cfg.Endpoint, cfg.Token, cfg.Debug) From 2572186f21657f8cea80e788d69d19af762e53d7 Mon Sep 17 00:00:00 2001 From: zbenhadi Date: Thu, 27 Apr 2023 17:17:38 +0200 Subject: [PATCH 4/5] fixed linting errors --- api/collaborators/collaborators_rest_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/api/collaborators/collaborators_rest_test.go b/api/collaborators/collaborators_rest_test.go index cc617c218..3832db981 100644 --- a/api/collaborators/collaborators_rest_test.go +++ b/api/collaborators/collaborators_rest_test.go @@ -32,14 +32,14 @@ func Test_collaboratorsRestClient_GetOrgCollaborations(t *testing.T) { wantErr bool }{ { - name: "Should handle a successfull request", + name: "Should handle a successful request", handler: func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, r.Header.Get("circle-token"), "token") assert.Equal(t, r.Header.Get("accept"), "application/json") assert.Equal(t, r.Header.Get("user-agent"), version.UserAgent()) assert.Equal(t, r.Method, "GET") - assert.Equal(t, r.URL.Path, fmt.Sprintf("/api/v2/me/collaborations")) + assert.Equal(t, r.URL.Path, "/api/v2/me/collaborations") w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) @@ -133,7 +133,7 @@ func Test_collaboratorsRestClient_GetCollaborationBySlug(t *testing.T) { assert.Equal(t, r.Header.Get("user-agent"), version.UserAgent()) assert.Equal(t, r.Method, "GET") - assert.Equal(t, r.URL.Path, fmt.Sprintf("/api/v2/me/collaborations")) + assert.Equal(t, r.URL.Path, "/api/v2/me/collaborations") w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) @@ -178,7 +178,7 @@ func Test_collaboratorsRestClient_GetCollaborationBySlug(t *testing.T) { assert.Equal(t, r.Header.Get("user-agent"), version.UserAgent()) assert.Equal(t, r.Method, "GET") - assert.Equal(t, r.URL.Path, fmt.Sprintf("/api/v2/me/collaborations")) + assert.Equal(t, r.URL.Path, "/api/v2/me/collaborations") w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) @@ -223,7 +223,7 @@ func Test_collaboratorsRestClient_GetCollaborationBySlug(t *testing.T) { assert.Equal(t, r.Header.Get("user-agent"), version.UserAgent()) assert.Equal(t, r.Method, "GET") - assert.Equal(t, r.URL.Path, fmt.Sprintf("/api/v2/me/collaborations")) + assert.Equal(t, r.URL.Path, "/api/v2/me/collaborations") w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) @@ -313,7 +313,7 @@ func Test_collaboratorsRestClient_GetCollaborationBySlug(t *testing.T) { assert.Equal(t, r.Header.Get("user-agent"), version.UserAgent()) assert.Equal(t, r.Method, "GET") - assert.Equal(t, r.URL.Path, fmt.Sprintf("/api/v2/me/collaborations")) + assert.Equal(t, r.URL.Path, "/api/v2/me/collaborations") w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) From 0f726195a58d9c5513752664aeaf202c21158a36 Mon Sep 17 00:00:00 2001 From: zbenhadi Date: Thu, 27 Apr 2023 17:24:30 +0200 Subject: [PATCH 5/5] fixed linting errors --- api/collaborators/collaborators_rest_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/collaborators/collaborators_rest_test.go b/api/collaborators/collaborators_rest_test.go index 3832db981..333539c68 100644 --- a/api/collaborators/collaborators_rest_test.go +++ b/api/collaborators/collaborators_rest_test.go @@ -1,7 +1,6 @@ package collaborators_test import ( - "fmt" "net/http" "net/http/httptest" "reflect" @@ -268,7 +267,7 @@ func Test_collaboratorsRestClient_GetCollaborationBySlug(t *testing.T) { assert.Equal(t, r.Header.Get("user-agent"), version.UserAgent()) assert.Equal(t, r.Method, "GET") - assert.Equal(t, r.URL.Path, fmt.Sprintf("/api/v2/me/collaborations")) + assert.Equal(t, r.URL.Path, "/api/v2/me/collaborations") w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK)