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 33a06c855..c5d7d895d 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 @@ -1734,3 +1782,29 @@ func stringifyDiff(diff gotextdiff.Unified, colorOpt string) string { color.NoColor = oldNoColor return strings.Join(lines, "\n") } + +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" }