From 79819a86cdfa09eaaef6a6697998b3f56f7f0907 Mon Sep 17 00:00:00 2001 From: corinnesollows Date: Mon, 3 Oct 2022 14:07:52 -0300 Subject: [PATCH] Revert "PIPE-2315/new branch to iteratively test" (#790) --- .circleci/config.yml | 6 +- Dockerfile | 2 +- api/api.go | 120 +++++ api/rest/client.go | 37 +- cmd/config.go | 27 +- cmd/config_test.go | 467 +++++++++++++++--- .../deprecated-images.go | 7 +- cmd/root.go | 14 +- config/config.go | 94 ---- go.mod | 2 +- local/local.go | 13 +- settings/settings.go | 1 - 12 files changed, 545 insertions(+), 245 deletions(-) rename config/deprecated_images.go => cmd/deprecated-images.go (93%) delete mode 100644 config/config.go diff --git a/.circleci/config.yml b/.circleci/config.yml index 3f8297e46..1d7c27638 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,7 +7,7 @@ orbs: executors: go: docker: - - image: cimg/go:1.19 + - image: cimg/go:1.18 environment: CGO_ENABLED: 0 mac: @@ -105,8 +105,8 @@ jobs: - checkout - run: | brew update - brew install go@1.19 - echo 'export PATH="/usr/local/opt/go@1.19/bin:$PATH"' >> ~/.bash_profile + brew install go@1.18 + echo 'export PATH="/usr/local/opt/go@1.18/bin:$PATH"' >> ~/.bash_profile - gomod - run: make test build: diff --git a/Dockerfile b/Dockerfile index 0d6d09f17..c94a4659f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM cimg/go:1.19.1 +FROM cimg/go:1.18.3 ENV CIRCLECI_CLI_SKIP_UPDATE_CHECK true diff --git a/api/api.go b/api/api.go index eabbc5ef0..674ae112c 100644 --- a/api/api.go +++ b/api/api.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/CircleCI-Public/circleci-cli/api/graphql" + "github.com/CircleCI-Public/circleci-cli/pipeline" "github.com/CircleCI-Public/circleci-cli/references" "github.com/CircleCI-Public/circleci-cli/settings" "github.com/Masterminds/semver" @@ -512,6 +513,125 @@ func WhoamiQuery(cl *graphql.Client) (*WhoamiResponse, error) { return &response, nil } +// 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 + 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 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 values != nil { + request.Var("pipelineValues", pipeline.PrepareForGraphQL(values)) + } + if params != nil { + pipelineParameters, err := json.Marshal(params) + if err != nil { + 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!) { + buildConfig(configYaml: $config, pipelineValues: $pipelineValues%s) { + valid, + errors { message }, + sourceYaml, + outputYaml + } + }`, + fieldAddendums) + + request := graphql.NewRequest(query) + request.Var("config", config) + + if values != nil { + request.Var("pipelineValues", pipeline.PrepareForGraphQL(values)) + } + if params != nil { + pipelineParameters, err := json.Marshal(params) + if err != nil { + return nil, fmt.Errorf("unable to serialize pipeline values: %s", err.Error()) + } + request.Var("pipelineParametersJson", string(pipelineParameters)) + } + + if orgId != "" { + request.Var("orgId", orgId) + } + 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 +} + // OrbQuery validated and processes an orb. func OrbQuery(cl *graphql.Client, configPath string) (*ConfigResponse, error) { var response OrbConfigResponse diff --git a/api/rest/client.go b/api/rest/client.go index e8a4a4a68..d745b887e 100644 --- a/api/rest/client.go +++ b/api/rest/client.go @@ -18,7 +18,6 @@ import ( type Client struct { baseURL *url.URL - apiURL *url.URL circleToken string client *http.Client } @@ -30,14 +29,13 @@ func New(host string, config *settings.Config) *Client { endpoint += "/" } - baseURL, _ := url.Parse(host) - apiURL, _ := url.Parse(config.ConfigAPIHost) + u, _ := url.Parse(host) + client := config.HTTPClient client.Timeout = 10 * time.Second return &Client{ - apiURL: apiURL.ResolveReference(&url.URL{Path: endpoint}), - baseURL: baseURL.ResolveReference(&url.URL{Path: endpoint}), + baseURL: u.ResolveReference(&url.URL{Path: endpoint}), circleToken: config.Token, client: client, } @@ -72,35 +70,6 @@ func (c *Client) NewRequest(method string, u *url.URL, payload interface{}) (req return req, nil } -func (c *Client) NewAPIRequest(method string, u *url.URL, payload interface{}) (req *http.Request, err error) { - var r io.Reader - if payload != nil { - buf := &bytes.Buffer{} - r = buf - err = json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, err - } - } - - req, err = http.NewRequest(method, c.apiURL.ResolveReference(u).String(), r) - if err != nil { - return nil, err - } - - req.Header.Set("Circle-Token", c.circleToken) - req.Header.Set("Accept", "application/json") - req.Header.Set("User-Agent", version.UserAgent()) - commandStr := header.GetCommandStr() - if commandStr != "" { - req.Header.Set("Circleci-Cli-Command", commandStr) - } - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - return req, nil -} - func (c *Client) DoRequest(req *http.Request, resp interface{}) (statusCode int, err error) { httpResp, err := c.client.Do(req) if err != nil { diff --git a/cmd/config.go b/cmd/config.go index c3f8a4831..40344a079 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -5,8 +5,8 @@ import ( "io/ioutil" "strings" - "github.com/CircleCI-Public/circleci-cli/api/rest" - "github.com/CircleCI-Public/circleci-cli/config" + "github.com/CircleCI-Public/circleci-cli/api" + "github.com/CircleCI-Public/circleci-cli/api/graphql" "github.com/CircleCI-Public/circleci-cli/filetree" "github.com/CircleCI-Public/circleci-cli/local" "github.com/CircleCI-Public/circleci-cli/pipeline" @@ -20,7 +20,7 @@ import ( type configOptions struct { cfg *settings.Config - rest *rest.Client + cl *graphql.Client args []string } @@ -63,7 +63,7 @@ func newConfigCommand(config *settings.Config) *cobra.Command { Short: "Check that the config file is well formed.", PreRun: func(cmd *cobra.Command, args []string) { opts.args = args - opts.rest = rest.New(config.Host, config) + opts.cl = graphql.NewClient(config.HTTPClient, config.Host, config.Endpoint, config.Token, config.Debug) }, RunE: func(cmd *cobra.Command, _ []string) error { return validateConfig(opts, cmd.Flags()) @@ -85,7 +85,7 @@ func newConfigCommand(config *settings.Config) *cobra.Command { Short: "Validate config and display expanded configuration.", PreRun: func(cmd *cobra.Command, args []string) { opts.args = args - opts.rest = rest.New(config.Host, config) + opts.cl = graphql.NewClient(config.HTTPClient, config.Host, config.Endpoint, config.Token, config.Debug) }, RunE: func(cmd *cobra.Command, _ []string) error { return processConfig(opts, cmd.Flags()) @@ -125,7 +125,7 @@ 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 *config.ConfigResponse + var response *api.ConfigResponse path := local.DefaultConfigPath // First, set the path to configPath set by --path flag for compatibility if configPath != "" { @@ -140,16 +140,13 @@ func validateConfig(opts configOptions, flags *pflag.FlagSet) error { //if no orgId provided use org slug orgID, _ := flags.GetString("org-id") if strings.TrimSpace(orgID) != "" { - - response, err = config.ConfigQuery(opts.rest, path, orgID, nil, pipeline.LocalPipelineValues()) + 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()) - response, err = config.ConfigQuery(opts.rest, path, orgSlug, nil, pipeline.LocalPipelineValues()) + response, err = api.ConfigQueryLegacy(opts.cl, path, orgSlug, nil, pipeline.LocalPipelineValues()) if err != nil { return err } @@ -159,7 +156,7 @@ func validateConfig(opts configOptions, flags *pflag.FlagSet) error { // link here to blog post when available // returns an error if a deprecated image is used if !ignoreDeprecatedImages { - err := config.DeprecatedImageCheck(response) + err := deprecatedImageCheck(response) if err != nil { return err } @@ -176,7 +173,7 @@ func validateConfig(opts configOptions, flags *pflag.FlagSet) error { func processConfig(opts configOptions, flags *pflag.FlagSet) error { paramsYaml, _ := flags.GetString("pipeline-parameters") - var response *config.ConfigResponse + var response *api.ConfigResponse var params pipeline.Parameters var err error @@ -197,13 +194,13 @@ 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 = config.ConfigQuery(opts.rest, opts.args[0], orgID, params, pipeline.LocalPipelineValues()) + 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 = config.ConfigQuery(opts.rest, opts.args[0], orgSlug, params, pipeline.LocalPipelineValues()) + response, err = api.ConfigQueryLegacy(opts.cl, opts.args[0], orgSlug, params, pipeline.LocalPipelineValues()) if err != nil { return err } diff --git a/cmd/config_test.go b/cmd/config_test.go index 6c622f564..e03177f8d 100644 --- a/cmd/config_test.go +++ b/cmd/config_test.go @@ -1,13 +1,20 @@ package cmd_test import ( + "encoding/json" "fmt" + "io" + "net/http" "os/exec" "path/filepath" + "time" + "github.com/CircleCI-Public/circleci-cli/api/graphql" "github.com/CircleCI-Public/circleci-cli/clitest" + "github.com/CircleCI-Public/circleci-cli/pipeline" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" "github.com/onsi/gomega/gexec" "gotest.tools/v3/golden" ) @@ -18,7 +25,7 @@ var _ = Describe("Config", func() { command *exec.Cmd results []byte tempSettings *clitest.TempSettings - // token string = "testtoken" + token string = "testtoken" ) BeforeEach(func() { @@ -29,95 +36,395 @@ var _ = Describe("Config", func() { tempSettings.Close() }) - It("packs all YAML contents as expected", func() { - command = exec.Command(pathCLI, - "config", "pack", - "--skip-update-check", - "testdata/hugo-pack/.circleci") - results = golden.Get(GinkgoT(), filepath.FromSlash("hugo-pack/result.yml")) - session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) - session.Wait() - Expect(err).ShouldNot(HaveOccurred()) - Eventually(session.Err.Contents()).Should(BeEmpty()) - Eventually(session.Out.Contents()).Should(MatchYAML(results)) - Eventually(session).Should(gexec.Exit(0)) + Describe("a .circleci folder with config.yml and local orbs folder containing the hugo orb", func() { + BeforeEach(func() { + command = exec.Command(pathCLI, + "config", "pack", + "--skip-update-check", + "testdata/hugo-pack/.circleci") + results = golden.Get(GinkgoT(), filepath.FromSlash("hugo-pack/result.yml")) + }) + + It("pack all YAML contents as expected", func() { + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + session.Wait() + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Err.Contents()).Should(BeEmpty()) + Eventually(session.Out.Contents()).Should(MatchYAML(results)) + Eventually(session).Should(gexec.Exit(0)) + }) + }) + + Describe("local orbs folder with mixed inline and local commands, jobs, etc", func() { + BeforeEach(func() { + var path string = "nested-orbs-and-local-commands-etc" + command = exec.Command(pathCLI, + "config", "pack", + "--skip-update-check", + filepath.Join("testdata", path, "test")) + results = golden.Get(GinkgoT(), filepath.FromSlash(fmt.Sprintf("%s/result.yml", path))) + }) + + It("pack all YAML contents as expected", func() { + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + session.Wait() + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Err.Contents()).Should(BeEmpty()) + Eventually(session.Out.Contents()).Should(MatchYAML(results)) + Eventually(session).Should(gexec.Exit(0)) + }) }) - It("given a .circleci folder with config.ymla nd local orb, packs all YAML contents as expected", func() { - command = exec.Command(pathCLI, - "config", "pack", - "--skip-update-check", - "testdata/hugo-pack/.circleci") - results = golden.Get(GinkgoT(), filepath.FromSlash("hugo-pack/result.yml")) - session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) - session.Wait() - Expect(err).ShouldNot(HaveOccurred()) - Eventually(session.Err.Contents()).Should(BeEmpty()) - Eventually(session.Out.Contents()).Should(MatchYAML(results)) - Eventually(session).Should(gexec.Exit(0)) + Describe("an orb containing local executors and commands in folder", func() { + BeforeEach(func() { + command = exec.Command(pathCLI, + "config", "pack", + "--skip-update-check", + "testdata/myorb/test") + + results = golden.Get(GinkgoT(), filepath.FromSlash("myorb/result.yml")) + }) + + It("pack all YAML contents as expected", func() { + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + session.Wait() + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Err.Contents()).Should(BeEmpty()) + Eventually(session.Out.Contents()).Should(MatchYAML(results)) + Eventually(session).Should(gexec.Exit(0)) + }) }) - It("given a local orbs folder with mixed inline and local commands, jobs, etc, packs all YAML contents as expected", func() { - var path string = "nested-orbs-and-local-commands-etc" - command = exec.Command(pathCLI, - "config", "pack", - "--skip-update-check", - filepath.Join("testdata", path, "test")) - results = golden.Get(GinkgoT(), filepath.FromSlash(fmt.Sprintf("%s/result.yml", path))) - session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) - session.Wait() - Expect(err).ShouldNot(HaveOccurred()) - Eventually(session.Err.Contents()).Should(BeEmpty()) - Eventually(session.Out.Contents()).Should(MatchYAML(results)) - Eventually(session).Should(gexec.Exit(0)) + Describe("with a large nested config including rails orb", func() { + BeforeEach(func() { + var path string = "test-with-large-nested-rails-orb" + command = exec.Command(pathCLI, + "config", "pack", + "--skip-update-check", + filepath.Join("testdata", path, "test")) + results = golden.Get(GinkgoT(), filepath.FromSlash(fmt.Sprintf("%s/result.yml", path))) + }) + + It("pack all YAML contents as expected", func() { + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + session.Wait() + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Err.Contents()).Should(BeEmpty()) + Eventually(session.Out.Contents()).Should(MatchYAML(results)) + Eventually(session).Should(gexec.Exit(0)) + }) + }) + + Context("config is a list and not a map", func() { + var config *clitest.TmpFile + + BeforeEach(func() { + config = clitest.OpenTmpFile(filepath.Join(tempSettings.Home, "myorb"), "config.yaml") + + command = exec.Command(pathCLI, + "config", "pack", + "--skip-update-check", + config.RootDir, + ) + }) + + AfterEach(func() { + config.Close() + }) + + It("prints an error about invalid YAML", func() { + config.Write([]byte(`[]`)) + + expected := fmt.Sprintf("Error: Failed trying to marshal the tree to YAML : expected a map, got a `[]interface {}` which is not supported at this time for \"%s\"\n", config.Path) + + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + + Expect(err).ShouldNot(HaveOccurred()) + + stderr := session.Wait().Err.Contents() + Expect(string(stderr)).To(Equal(expected)) + Eventually(session).Should(clitest.ShouldFail()) + }) + }) + + Describe("validating configs", func() { + config := "version: 2.1" + var expReq string + + BeforeEach(func() { + command = exec.Command(pathCLI, + "config", "validate", + "--skip-update-check", + "--token", token, + "--host", tempSettings.TestServer.URL(), + "-", + ) + + 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!], $orgSlug: String) { + buildConfig(configYaml: $config, pipelineValues: $pipelineValues) { + valid, + errors { message }, + sourceYaml, + outputYaml + } + }` + + r := graphql.NewRequest(query) + r.Variables["config"] = config + 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", func() { + expResp := `{ + "buildConfig": { + "errors": [ + {"message": "error1"} + ] + } + }` + + 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: error1")) + Eventually(session).Should(clitest.ShouldFail()) + }) + + It("returns successfully when validating a config", func() { + expResp := `{ + "buildConfig": {} + }` + + 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.Out, time.Second*3).Should(gbytes.Say("Config input is valid.")) + Eventually(session).Should(gexec.Exit(0)) + }) }) - It("packs successfully given an orb containing local executors and commands in folder", func() { - command = exec.Command(pathCLI, - "config", "pack", - "--skip-update-check", - "testdata/myorb/test") - - results = golden.Get(GinkgoT(), filepath.FromSlash("myorb/result.yml")) - session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) - session.Wait() - Expect(err).ShouldNot(HaveOccurred()) - Eventually(session.Err.Contents()).Should(BeEmpty()) - Eventually(session.Out.Contents()).Should(MatchYAML(results)) - Eventually(session).Should(gexec.Exit(0)) + Describe("validating configs with pipeline parameters", func() { + config := "version: 2.1" + var expReq string + + BeforeEach(func() { + command = exec.Command(pathCLI, + "config", "process", + "--skip-update-check", + "--token", token, + "--host", tempSettings.TestServer.URL(), + "--pipeline-parameters", `{"foo": "test", "bar": true, "baz": 10}`, + "-", + ) + + 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!], $orgSlug: String) { + buildConfig(configYaml: $config, pipelineValues: $pipelineValues, pipelineParametersJson: $pipelineParametersJson) { + valid, + errors { message }, + sourceYaml, + outputYaml + } + }` + + r := graphql.NewRequest(query) + r.Variables["config"] = config + r.Variables["pipelineValues"] = pipeline.PrepareForGraphQL(pipeline.LocalPipelineValues()) + + pipelineParams, err := json.Marshal(pipeline.Parameters{ + "foo": "test", + "bar": true, + "baz": 10, + }) + Expect(err).ToNot(HaveOccurred()) + r.Variables["pipelineParametersJson"] = string(pipelineParams) + + req, err := r.Encode() + Expect(err).ShouldNot(HaveOccurred()) + expReq = req.String() + }) + + It("returns successfully when validating a config", func() { + expResp := `{ + "buildConfig": {} + }` + + 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).Should(gexec.Exit(0)) + }) }) - It("packs as expected given a large nested config including rails orbs", func() { - var path string = "test-with-large-nested-rails-orb" - command = exec.Command(pathCLI, - "config", "pack", - "--skip-update-check", - filepath.Join("testdata", path, "test")) - results = golden.Get(GinkgoT(), filepath.FromSlash(fmt.Sprintf("%s/result.yml", path))) - session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) - session.Wait() - Expect(err).ShouldNot(HaveOccurred()) - Eventually(session.Err.Contents()).Should(BeEmpty()) - Eventually(session.Out.Contents()).Should(MatchYAML(results)) - Eventually(session).Should(gexec.Exit(0)) + 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()) + }) }) - It("prints an error given a config which is a list and not a map", func() { - config := clitest.OpenTmpFile(filepath.Join(tempSettings.Home, "myorb"), "config.yaml") - command = exec.Command(pathCLI, - "config", "pack", - "--skip-update-check", - config.RootDir, - ) - config.Write([]byte(`[]`)) - - expected := fmt.Sprintf("Error: Failed trying to marshal the tree to YAML : expected a map, got a `[]interface {}` which is not supported at this time for \"%s\"\n", config.Path) - session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) - Expect(err).ShouldNot(HaveOccurred()) - stderr := session.Wait().Err.Contents() - Expect(string(stderr)).To(Equal(expected)) - Eventually(session).Should(clitest.ShouldFail()) - config.Close() + Describe("validating configs with private orbs Legacy", func() { + config := "version: 2.1" + orgSlug := "circleci" + var expReq string + + BeforeEach(func() { + command = exec.Command(pathCLI, + "config", "validate", + "--skip-update-check", + "--token", token, + "--host", tempSettings.TestServer.URL(), + "--org-slug", orgSlug, + "-", + ) + + 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!], $orgSlug: String) { + buildConfig(configYaml: $config, pipelineValues: $pipelineValues, orgSlug: $orgSlug) { + valid, + errors { message }, + sourceYaml, + outputYaml + } + }` + + r := graphql.NewRequest(query) + r.Variables["config"] = config + r.Variables["orgSlug"] = orgSlug + 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()) + }) + + It("returns successfully when validating a config with private orbs", func() { + expResp := `{ + "buildConfig": {} + }` + + 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.Out, time.Second*3).Should(gbytes.Say("Config input is valid.")) + Eventually(session).Should(gexec.Exit(0)) + }) }) }) }) diff --git a/config/deprecated_images.go b/cmd/deprecated-images.go similarity index 93% rename from config/deprecated_images.go rename to cmd/deprecated-images.go index 1579af3de..607edfc88 100644 --- a/config/deprecated_images.go +++ b/cmd/deprecated-images.go @@ -1,6 +1,8 @@ -package config +package cmd import ( + "github.com/CircleCI-Public/circleci-cli/api" + "github.com/pkg/errors" "gopkg.in/yaml.v3" ) @@ -38,7 +40,8 @@ type processedConfig struct { } // Processes the config down to v2.0, then checks image used against the block list -func DeprecatedImageCheck(response *ConfigResponse) error { +func deprecatedImageCheck(response *api.ConfigResponse) error { + aConfig := processedConfig{} err := yaml.Unmarshal([]byte(response.OutputYaml), &aConfig) if err != nil { diff --git a/cmd/root.go b/cmd/root.go index a51f10ee4..f47ff96ea 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -20,7 +20,6 @@ import ( var defaultEndpoint = "graphql-unstable" var defaultHost = "https://circleci.com" -var defaultConfigApiHost = "https://api.circleci.com" var defaultRestEndpoint = "api/v2" var trueString = "true" @@ -97,13 +96,12 @@ Use "{{.CommandPath}} [command] --help" for more information about a command.{{e // MakeCommands creates the top level commands func MakeCommands() *cobra.Command { rootOptions = &settings.Config{ - Debug: false, - Token: "", - Host: defaultHost, - ConfigAPIHost: defaultConfigApiHost, - RestEndpoint: defaultRestEndpoint, - Endpoint: defaultEndpoint, - GitHubAPI: "https://api.github.com/", + Debug: false, + Token: "", + Host: defaultHost, + RestEndpoint: defaultRestEndpoint, + Endpoint: defaultEndpoint, + GitHubAPI: "https://api.github.com/", } if err := rootOptions.Load(); err != nil { diff --git a/config/config.go b/config/config.go deleted file mode 100644 index 3db3a4f94..000000000 --- a/config/config.go +++ /dev/null @@ -1,94 +0,0 @@ -package config - -import ( - "fmt" - "io/ioutil" - "net/url" - "os" - - "github.com/CircleCI-Public/circleci-cli/api/rest" - "github.com/CircleCI-Public/circleci-cli/pipeline" - "github.com/pkg/errors" -) - -type ConfigError struct { - Message string `json:"message"` -} - -type ConfigResponse struct { - Valid bool `json:"valid"` - SourceYaml string `json:"source-yaml"` - OutputYaml string `json:"output-yaml"` - Errors []ConfigError `json:"errors"` -} - -type CompileConfigRequest struct { - ConfigYaml string `json:"config_yaml"` - Options Options `json:"options"` -} - -type Options struct { - PipelineValues pipeline.Values `json:"pipeline_values"` -} - -// #nosec -func loadYaml(path string) (string, error) { - var err error - var config []byte - if path == "-" { - config, err = ioutil.ReadAll(os.Stdin) - } else { - config, err = ioutil.ReadFile(path) - } - - if err != nil { - return "", errors.Wrapf(err, "Could not load config file at %s", path) - } - - return string(config), nil -} - -func ConfigQuery( - rest *rest.Client, - configPath string, - orgID string, - params pipeline.Parameters, - values pipeline.Values, -) (*ConfigResponse, error) { - - configString, err := loadYaml(configPath) - if err != nil { - return nil, err - } - - req, err := rest.NewAPIRequest( - "POST", - &url.URL{ - Path: "compile-config-with-defaults", - }, - CompileConfigRequest{ - ConfigYaml: configString, - Options: Options{ - PipelineValues: values, - }, - }, - ) - if err != nil { - return nil, err - } - - configCompilationResp := &ConfigResponse{} - statusCode, err := rest.DoRequest(req, configCompilationResp) - if err != nil { - return nil, err - } - if statusCode != 200 { - return nil, errors.New("non 200 status code") - } - - if len(configCompilationResp.Errors) > 0 { - return nil, errors.New(fmt.Sprintf("config compilation contains errors: %s", configCompilationResp.Errors)) - } - - return configCompilationResp, nil -} diff --git a/go.mod b/go.mod index fc76cc3ef..d515fd50c 100644 --- a/go.mod +++ b/go.mod @@ -90,4 +90,4 @@ require ( // fix vulnerability: CVE-2020-15114 in etcd v3.3.10+incompatible replace github.com/coreos/etcd => github.com/coreos/etcd v3.3.24+incompatible -go 1.19 +go 1.18 diff --git a/local/local.go b/local/local.go index 0177ba7a1..e139be8f4 100644 --- a/local/local.go +++ b/local/local.go @@ -10,8 +10,8 @@ import ( "strings" "syscall" - "github.com/CircleCI-Public/circleci-cli/api/rest" - "github.com/CircleCI-Public/circleci-cli/config" + "github.com/CircleCI-Public/circleci-cli/api" + "github.com/CircleCI-Public/circleci-cli/api/graphql" "github.com/CircleCI-Public/circleci-cli/pipeline" "github.com/CircleCI-Public/circleci-cli/settings" "github.com/pkg/errors" @@ -24,20 +24,21 @@ const DefaultConfigPath = ".circleci/config.yml" func Execute(flags *pflag.FlagSet, cfg *settings.Config) error { var err error - var configResponse *config.ConfigResponse - restClient := rest.New(cfg.Host, cfg) + var configResponse *api.ConfigResponse + cl := graphql.NewClient(cfg.HTTPClient, cfg.Host, cfg.Endpoint, cfg.Token, cfg.Debug) + processedArgs, configPath := buildAgentArguments(flags) //if no orgId provided use org slug orgID, _ := flags.GetString("org-id") if strings.TrimSpace(orgID) != "" { - configResponse, err = config.ConfigQuery(restClient, configPath, orgID, nil, pipeline.LocalPipelineValues()) + configResponse, err = api.ConfigQuery(cl, configPath, orgID, nil, pipeline.LocalPipelineValues()) if err != nil { return err } } else { orgSlug, _ := flags.GetString("org-slug") - configResponse, err = config.ConfigQuery(restClient, configPath, orgSlug, nil, pipeline.LocalPipelineValues()) + configResponse, err = api.ConfigQueryLegacy(cl, configPath, orgSlug, nil, pipeline.LocalPipelineValues()) if err != nil { return err } diff --git a/settings/settings.go b/settings/settings.go index 03cd4099c..d9f0953b4 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -35,7 +35,6 @@ type Config struct { GitHubAPI string `yaml:"-"` SkipUpdateCheck bool `yaml:"-"` OrbPublishing OrbPublishingInfo `yaml:"orb_publishing"` - ConfigAPIHost string `yaml:"-"` } type OrbPublishingInfo struct {