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..6fecf61f9 --- /dev/null +++ b/api/collaborators/collaborators.go @@ -0,0 +1,14 @@ +package collaborators + +type CollaborationResult struct { + VcsType string `json:"vcs_type"` + OrgSlug string `json:"slug"` + OrgName string `json:"name"` + OrgId string `json:"id"` + AvatarUrl string `json:"avatar_url"` +} + +type CollaboratorsClient interface { + GetCollaborationBySlug(slug string) (*CollaborationResult, error) + GetOrgCollaborations() ([]CollaborationResult, error) +} diff --git a/api/collaborators/collaborators_rest.go b/api/collaborators/collaborators_rest.go new file mode 100644 index 000000000..e15f7774e --- /dev/null +++ b/api/collaborators/collaborators_rest.go @@ -0,0 +1,64 @@ +package collaborators + +import ( + "net/url" + "strings" + + "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 +} + +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..333539c68 --- /dev/null +++ b/api/collaborators/collaborators_rest_test.go @@ -0,0 +1,380 @@ +package collaborators_test + +import ( + "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 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, "/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, "/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, "/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, "/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, "/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, "/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 b0724c18c..326b0a616 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,14 @@ 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()) + } + + response, err := api.OrbQuery(opts.cl, opts.args[0], orgId) if err != nil { return err @@ -1746,3 +1792,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 strings.TrimSpace(orgInfo.OrgID) != "" { + return orgInfo.OrgID, nil + } + + if strings.TrimSpace(orgInfo.OrgSlug) == "" { + return "", nil + } + + coll, err := o.collaborators.GetCollaborationBySlug(orgInfo.OrgSlug) + + 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 coll.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" } 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)