From 2c079f740f625740d9348f6445bc3d40567f01ed Mon Sep 17 00:00:00 2001 From: corinnesollows Date: Mon, 30 May 2022 11:21:55 -0300 Subject: [PATCH 1/4] Org UUID support --- api/api.go | 24 ++++++++++++++++++------ cmd/build.go | 1 + cmd/config.go | 9 +++++++-- local/local.go | 10 +++++++--- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/api/api.go b/api/api.go index 313b834c9..bf6f50488 100644 --- a/api/api.go +++ b/api/api.go @@ -15,6 +15,7 @@ import ( "github.com/CircleCI-Public/circleci-cli/references" "github.com/CircleCI-Public/circleci-cli/settings" "github.com/Masterminds/semver" + "github.com/google/uuid" "github.com/pkg/errors" "gopkg.in/yaml.v3" ) @@ -514,15 +515,14 @@ func WhoamiQuery(cl *graphql.Client) (*WhoamiResponse, error) { } // ConfigQuery calls the GQL API to validate and process config -func ConfigQuery(cl *graphql.Client, configPath string, orgSlug string, params pipeline.Parameters, values pipeline.Values) (*ConfigResponse, error) { +func ConfigQuery(cl *graphql.Client, configPath string, orgIDString, orgSlug string, params pipeline.Parameters, values pipeline.Values) (*ConfigResponse, error) { var response BuildConfigResponse var query string - + var orgId uuid.UUID config, err := loadYaml(configPath) if err != nil { return nil, err } - // GraphQL isn't forwards-compatible, so we are unusually selective here about // passing only non-empty fields on to the API, to minimize user impact if the // backend is out of date. @@ -530,11 +530,19 @@ func ConfigQuery(cl *graphql.Client, configPath string, orgSlug string, params p if orgSlug != "" { fieldAddendums += ", orgSlug: $orgSlug" } + + if orgIDString != "" { + orgId, err = uuid.Parse(orgIDString) + if err != nil { + return nil, err + } + fieldAddendums += ", orgId: $orgId" + } if len(params) > 0 { fieldAddendums += ", pipelineParametersJson: $pipelineParametersJson" } query = fmt.Sprintf( - `query ValidateConfig ($config: String!, $pipelineParametersJson: String, $pipelineValues: [StringKeyVal!], $orgSlug: String) { + `query ValidateConfig ($config: String!, $pipelineParametersJson: String, $pipelineValues: [StringKeyVal!], $orgId: UUID!, $orgSlug: String) { buildConfig(configYaml: $config, pipelineValues: $pipelineValues%s) { valid, errors { message }, @@ -546,6 +554,7 @@ func ConfigQuery(cl *graphql.Client, configPath string, orgSlug string, params p request := graphql.NewRequest(query) request.Var("config", config) + if values != nil { request.Var("pipelineValues", pipeline.PrepareForGraphQL(values)) } @@ -556,17 +565,20 @@ func ConfigQuery(cl *graphql.Client, configPath string, orgSlug string, params p } request.Var("pipelineParametersJson", string(pipelineParameters)) } + + if orgId != uuid.Nil { + request.Var("orgId", orgId) + } if orgSlug != "" { request.Var("orgSlug", orgSlug) } + request.SetToken(cl.Token) err = cl.Run(request, &response) - if err != nil { return nil, errors.Wrap(err, "Unable to validate config") } - if len(response.BuildConfig.ConfigResponse.Errors) > 0 { return nil, &response.BuildConfig.ConfigResponse.Errors } diff --git a/cmd/build.go b/cmd/build.go index 96fa77510..002e9682f 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -17,6 +17,7 @@ func newLocalExecuteCommand(config *settings.Config) *cobra.Command { local.AddFlagsForDocumentation(buildCommand.Flags()) buildCommand.Flags().StringP("org-slug", "o", "", "organization slug (for example: github/example-org), used when a config depends on private orbs belonging to that org") + buildCommand.Flags().String("org-id", "", "organization id, used when a config depends on private orbs belonging to that org") return buildCommand } diff --git a/cmd/config.go b/cmd/config.go index b293ff787..e18436871 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -77,6 +77,7 @@ func newConfigCommand(config *settings.Config) *cobra.Command { panic(err) } validateCommand.Flags().StringP("org-slug", "o", "", "organization slug (for example: github/example-org), used when a config depends on private orbs belonging to that org") + validateCommand.Flags().String("org-id", "", "organization id used when a config depends on private orbs belonging to that org") processCommand := &cobra.Command{ Use: "process ", @@ -121,6 +122,8 @@ func newConfigCommand(config *settings.Config) *cobra.Command { // The arg is actually optional, in order to support compatibility with the --path flag. func validateConfig(opts configOptions, flags *pflag.FlagSet) error { + var err error + var response *api.ConfigResponse path := local.DefaultConfigPath // First, set the path to configPath set by --path flag for compatibility if configPath != "" { @@ -133,8 +136,9 @@ func validateConfig(opts configOptions, flags *pflag.FlagSet) error { } orgSlug, _ := flags.GetString("org-slug") + orgID, _ := flags.GetString("org-id") - response, err := api.ConfigQuery(opts.cl, path, orgSlug, nil, pipeline.LocalPipelineValues()) + response, err = api.ConfigQuery(opts.cl, path, orgID, orgSlug, nil, pipeline.LocalPipelineValues()) if err != nil { return err } @@ -160,6 +164,7 @@ func validateConfig(opts configOptions, flags *pflag.FlagSet) error { func processConfig(opts configOptions, flags *pflag.FlagSet) error { orgSlug, _ := flags.GetString("org-slug") + orgID, _ := flags.GetString("org-id") paramsYaml, _ := flags.GetString("pipeline-parameters") var params pipeline.Parameters @@ -178,7 +183,7 @@ func processConfig(opts configOptions, flags *pflag.FlagSet) error { } } - response, err := api.ConfigQuery(opts.cl, opts.args[0], orgSlug, params, pipeline.LocalPipelineValues()) + response, err := api.ConfigQuery(opts.cl, opts.args[0], orgID, orgSlug, params, pipeline.LocalPipelineValues()) if err != nil { return err } diff --git a/local/local.go b/local/local.go index 4fda8b773..08a7ae75b 100644 --- a/local/local.go +++ b/local/local.go @@ -22,11 +22,15 @@ var picardRepo = "circleci/picard" const DefaultConfigPath = ".circleci/config.yml" func Execute(flags *pflag.FlagSet, cfg *settings.Config) error { + var err error + var configResponse *api.ConfigResponse + cl := graphql.NewClient(cfg.HTTPClient, cfg.Host, cfg.Endpoint, cfg.Token, cfg.Debug) + processedArgs, configPath := buildAgentArguments(flags) orgSlug, _ := flags.GetString("org-slug") - cl := graphql.NewClient(cfg.HTTPClient, cfg.Host, cfg.Endpoint, cfg.Token, cfg.Debug) - configResponse, err := api.ConfigQuery(cl, configPath, orgSlug, nil, pipeline.LocalPipelineValues()) + orgID, _ := flags.GetString("org-id") + configResponse, err = api.ConfigQuery(cl, configPath, orgID, orgSlug, nil, pipeline.LocalPipelineValues()) if err != nil { return err } @@ -118,7 +122,7 @@ func buildAgentArguments(flags *pflag.FlagSet) ([]string, string) { // build a list of all supplied flags, that we will pass on to build-agent flags.Visit(func(flag *pflag.Flag) { - if flag.Name != "org-slug" && flag.Name != "config" && flag.Name != "debug" { + if flag.Name != "org-slug" && flag.Name != "config" && flag.Name != "debug" && flag.Name != "org-id" { result = append(result, unparseFlag(flags, flag)...) } }) From d964cccd2427adc1ded984a07e8807067254961b Mon Sep 17 00:00:00 2001 From: corinnesollows Date: Mon, 30 May 2022 15:23:01 -0300 Subject: [PATCH 2/4] Made two methods --- api/api.go | 72 +++++++++++++++++++++++++++++++++++++++++--------- cmd/config.go | 37 ++++++++++++++++++-------- local/local.go | 18 +++++++++---- 3 files changed, 98 insertions(+), 29 deletions(-) diff --git a/api/api.go b/api/api.go index bf6f50488..674ae112c 100644 --- a/api/api.go +++ b/api/api.go @@ -15,7 +15,6 @@ import ( "github.com/CircleCI-Public/circleci-cli/references" "github.com/CircleCI-Public/circleci-cli/settings" "github.com/Masterminds/semver" - "github.com/google/uuid" "github.com/pkg/errors" "gopkg.in/yaml.v3" ) @@ -514,11 +513,10 @@ func WhoamiQuery(cl *graphql.Client) (*WhoamiResponse, error) { return &response, nil } -// ConfigQuery calls the GQL API to validate and process config -func ConfigQuery(cl *graphql.Client, configPath string, orgIDString, orgSlug string, params pipeline.Parameters, values pipeline.Values) (*ConfigResponse, error) { +// ConfigQueryLegacy calls the GQL API to validate and process config with the legacy orgSlug +func ConfigQueryLegacy(cl *graphql.Client, configPath string, orgSlug string, params pipeline.Parameters, values pipeline.Values) (*ConfigResponse, error) { var response BuildConfigResponse var query string - var orgId uuid.UUID config, err := loadYaml(configPath) if err != nil { return nil, err @@ -530,19 +528,71 @@ func ConfigQuery(cl *graphql.Client, configPath string, orgIDString, orgSlug str if orgSlug != "" { fieldAddendums += ", orgSlug: $orgSlug" } + if len(params) > 0 { + fieldAddendums += ", pipelineParametersJson: $pipelineParametersJson" + } + query = fmt.Sprintf( + `query ValidateConfig ($config: String!, $pipelineParametersJson: String, $pipelineValues: [StringKeyVal!], $orgSlug: String) { + buildConfig(configYaml: $config, pipelineValues: $pipelineValues%s) { + valid, + errors { message }, + sourceYaml, + outputYaml + } + }`, + fieldAddendums) + + request := graphql.NewRequest(query) + request.Var("config", config) - if orgIDString != "" { - orgId, err = uuid.Parse(orgIDString) + if values != nil { + request.Var("pipelineValues", pipeline.PrepareForGraphQL(values)) + } + if params != nil { + pipelineParameters, err := json.Marshal(params) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to serialize pipeline values: %s", err.Error()) } + request.Var("pipelineParametersJson", string(pipelineParameters)) + } + + if orgSlug != "" { + request.Var("orgSlug", orgSlug) + } + + request.SetToken(cl.Token) + + err = cl.Run(request, &response) + if err != nil { + return nil, errors.Wrap(err, "Unable to validate config") + } + if len(response.BuildConfig.ConfigResponse.Errors) > 0 { + return nil, &response.BuildConfig.ConfigResponse.Errors + } + + return &response.BuildConfig.ConfigResponse, nil +} + +// ConfigQuery calls the GQL API to validate and process config with the org id +func ConfigQuery(cl *graphql.Client, configPath string, orgId string, params pipeline.Parameters, values pipeline.Values) (*ConfigResponse, error) { + var response BuildConfigResponse + var query string + config, err := loadYaml(configPath) + if err != nil { + return nil, err + } + // GraphQL isn't forwards-compatible, so we are unusually selective here about + // passing only non-empty fields on to the API, to minimize user impact if the + // backend is out of date. + var fieldAddendums string + if orgId != "" { fieldAddendums += ", orgId: $orgId" } if len(params) > 0 { fieldAddendums += ", pipelineParametersJson: $pipelineParametersJson" } query = fmt.Sprintf( - `query ValidateConfig ($config: String!, $pipelineParametersJson: String, $pipelineValues: [StringKeyVal!], $orgId: UUID!, $orgSlug: String) { + `query ValidateConfig ($config: String!, $pipelineParametersJson: String, $pipelineValues: [StringKeyVal!], $orgId: UUID!) { buildConfig(configYaml: $config, pipelineValues: $pipelineValues%s) { valid, errors { message }, @@ -566,13 +616,9 @@ func ConfigQuery(cl *graphql.Client, configPath string, orgIDString, orgSlug str request.Var("pipelineParametersJson", string(pipelineParameters)) } - if orgId != uuid.Nil { + if orgId != "" { request.Var("orgId", orgId) } - if orgSlug != "" { - request.Var("orgSlug", orgSlug) - } - request.SetToken(cl.Token) err = cl.Run(request, &response) diff --git a/cmd/config.go b/cmd/config.go index e18436871..3574a4483 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" "io/ioutil" + "strings" "github.com/CircleCI-Public/circleci-cli/api" "github.com/CircleCI-Public/circleci-cli/api/graphql" @@ -135,12 +136,18 @@ func validateConfig(opts configOptions, flags *pflag.FlagSet) error { path = opts.args[0] } - orgSlug, _ := flags.GetString("org-slug") orgID, _ := flags.GetString("org-id") - - response, err = api.ConfigQuery(opts.cl, path, orgID, orgSlug, nil, pipeline.LocalPipelineValues()) - if err != nil { - return err + if strings.TrimSpace(orgID) != "" { + response, err = api.ConfigQuery(opts.cl, path, orgID, nil, pipeline.LocalPipelineValues()) + if err != nil { + return err + } + } else { + orgSlug, _ := flags.GetString("org-slug") + response, err = api.ConfigQueryLegacy(opts.cl, path, orgSlug, nil, pipeline.LocalPipelineValues()) + if err != nil { + return err + } } // check if a deprecated Linux VM image is being used @@ -163,11 +170,10 @@ func validateConfig(opts configOptions, flags *pflag.FlagSet) error { } func processConfig(opts configOptions, flags *pflag.FlagSet) error { - orgSlug, _ := flags.GetString("org-slug") - orgID, _ := flags.GetString("org-id") paramsYaml, _ := flags.GetString("pipeline-parameters") - + var response *api.ConfigResponse var params pipeline.Parameters + var err error if len(paramsYaml) > 0 { // The 'src' value can be a filepath, or a yaml string. If the file cannot be read sucessfully, @@ -183,9 +189,18 @@ func processConfig(opts configOptions, flags *pflag.FlagSet) error { } } - response, err := api.ConfigQuery(opts.cl, opts.args[0], orgID, orgSlug, params, pipeline.LocalPipelineValues()) - if err != nil { - return err + orgID, _ := flags.GetString("org-id") + if strings.TrimSpace(orgID) != "" { + response, err = api.ConfigQuery(opts.cl, opts.args[0], orgID, params, pipeline.LocalPipelineValues()) + if err != nil { + return err + } + } else { + orgSlug, _ := flags.GetString("org-slug") + response, err = api.ConfigQueryLegacy(opts.cl, opts.args[0], orgSlug, params, pipeline.LocalPipelineValues()) + if err != nil { + return err + } } fmt.Print(response.OutputYaml) diff --git a/local/local.go b/local/local.go index 08a7ae75b..7afea3df5 100644 --- a/local/local.go +++ b/local/local.go @@ -7,6 +7,7 @@ import ( "os" "os/exec" "regexp" + "strings" "syscall" "github.com/CircleCI-Public/circleci-cli/api" @@ -27,12 +28,19 @@ func Execute(flags *pflag.FlagSet, cfg *settings.Config) error { cl := graphql.NewClient(cfg.HTTPClient, cfg.Host, cfg.Endpoint, cfg.Token, cfg.Debug) processedArgs, configPath := buildAgentArguments(flags) - orgSlug, _ := flags.GetString("org-slug") - orgID, _ := flags.GetString("org-id") - configResponse, err = api.ConfigQuery(cl, configPath, orgID, orgSlug, nil, pipeline.LocalPipelineValues()) - if err != nil { - return err + orgID, _ := flags.GetString("org-id") + if strings.TrimSpace(orgID) != "" { + configResponse, err = api.ConfigQuery(cl, configPath, orgID, nil, pipeline.LocalPipelineValues()) + if err != nil { + return err + } + } else { + orgSlug, _ := flags.GetString("org-slug") + configResponse, err = api.ConfigQueryLegacy(cl, configPath, orgSlug, nil, pipeline.LocalPipelineValues()) + if err != nil { + return err + } } if !configResponse.Valid { From cb03086f8fe37b2b1b7b9e4b2bc78465ce7e3aa8 Mon Sep 17 00:00:00 2001 From: corinnesollows Date: Mon, 30 May 2022 15:30:14 -0300 Subject: [PATCH 3/4] Test for OrgId --- cmd/config_test.go | 62 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/cmd/config_test.go b/cmd/config_test.go index 7363145c4..e03177f8d 100644 --- a/cmd/config_test.go +++ b/cmd/config_test.go @@ -287,6 +287,68 @@ var _ = Describe("Config", func() { }) Describe("validating configs with private orbs", func() { + config := "version: 2.1" + orgId := "bb604b45-b6b0-4b81-ad80-796f15eddf87" + var expReq string + + BeforeEach(func() { + command = exec.Command(pathCLI, + "config", "validate", + "--skip-update-check", + "--token", token, + "--host", tempSettings.TestServer.URL(), + "--org-id", orgId, + "-", + ) + + stdin, err := command.StdinPipe() + Expect(err).ToNot(HaveOccurred()) + _, err = io.WriteString(stdin, config) + Expect(err).ToNot(HaveOccurred()) + stdin.Close() + + query := `query ValidateConfig ($config: String!, $pipelineParametersJson: String, $pipelineValues: [StringKeyVal!], $orgId: UUID!) { + buildConfig(configYaml: $config, pipelineValues: $pipelineValues, orgId: $orgId) { + valid, + errors { message }, + sourceYaml, + outputYaml + } + }` + + r := graphql.NewRequest(query) + r.Variables["config"] = config + r.Variables["orgId"] = orgId + r.Variables["pipelineValues"] = pipeline.PrepareForGraphQL(pipeline.LocalPipelineValues()) + + req, err := r.Encode() + Expect(err).ShouldNot(HaveOccurred()) + expReq = req.String() + }) + + It("returns an error when validating a config with a private orb", func() { + expResp := `{ + "buildConfig": { + "errors": [ + {"message": "permission denied"} + ] + } + }` + + tempSettings.AppendPostHandler(token, clitest.MockRequestResponse{ + Status: http.StatusOK, + Request: expReq, + Response: expResp, + }) + + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Err, time.Second*3).Should(gbytes.Say("Error: permission denied")) + Eventually(session).Should(clitest.ShouldFail()) + }) + }) + + Describe("validating configs with private orbs Legacy", func() { config := "version: 2.1" orgSlug := "circleci" var expReq string From e9914b61cc1e3253033ba8976f46f24bdf1b1b33 Mon Sep 17 00:00:00 2001 From: corinnesollows Date: Tue, 31 May 2022 13:33:01 -0300 Subject: [PATCH 4/4] Comments --- cmd/config.go | 3 +++ local/local.go | 1 + 2 files changed, 4 insertions(+) diff --git a/cmd/config.go b/cmd/config.go index 3574a4483..be7523c84 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -95,6 +95,7 @@ func newConfigCommand(config *settings.Config) *cobra.Command { } processCommand.Annotations[""] = configAnnotations[""] processCommand.Flags().StringP("org-slug", "o", "", "organization slug (for example: github/example-org), used when a config depends on private orbs belonging to that org") + processCommand.Flags().String("org-id", "", "organization id used when a config depends on private orbs belonging to that org") processCommand.Flags().StringP("pipeline-parameters", "", "", "YAML/JSON map of pipeline parameters, accepts either YAML/JSON directly or file path (for example: my-params.yml)") migrateCommand := &cobra.Command{ @@ -136,6 +137,7 @@ func validateConfig(opts configOptions, flags *pflag.FlagSet) error { path = opts.args[0] } + //if no orgId provided use org slug orgID, _ := flags.GetString("org-id") if strings.TrimSpace(orgID) != "" { response, err = api.ConfigQuery(opts.cl, path, orgID, nil, pipeline.LocalPipelineValues()) @@ -189,6 +191,7 @@ func processConfig(opts configOptions, flags *pflag.FlagSet) error { } } + //if no orgId provided use org slug orgID, _ := flags.GetString("org-id") if strings.TrimSpace(orgID) != "" { response, err = api.ConfigQuery(opts.cl, opts.args[0], orgID, params, pipeline.LocalPipelineValues()) diff --git a/local/local.go b/local/local.go index 7afea3df5..e139be8f4 100644 --- a/local/local.go +++ b/local/local.go @@ -29,6 +29,7 @@ func Execute(flags *pflag.FlagSet, cfg *settings.Config) error { processedArgs, configPath := buildAgentArguments(flags) + //if no orgId provided use org slug orgID, _ := flags.GetString("org-id") if strings.TrimSpace(orgID) != "" { configResponse, err = api.ConfigQuery(cl, configPath, orgID, nil, pipeline.LocalPipelineValues())