From e96f9cb8ef0689f4865191f5c969736dcafcac79 Mon Sep 17 00:00:00 2001 From: Brady Lill Date: Thu, 21 Jun 2018 10:08:47 -0600 Subject: [PATCH] [Circle-11767] config expand command (#27) * Adding most basic version of expand. * Moving expand under the config command. * Setting the token when calling expand config. * Adding config file to expand config. * Breaking out query and load for expand command. * Using load and query in validate. * Using the same response type for validate and expand. * Inlining variables map initialization. * Pulling out error processing for config commands. * Fixing linting errors. * Cleaning up config_test * Actually testing the graphql request is correct. * Testing gql errors. * Testing console output. * Using persistent flags to pass the config path to validate and expand. * Adding tests around validate. * Documenting verify json handler. * Updating with PR feedback. * Dep ensure --- .gitignore | 1 + Gopkg.lock | 2 +- cmd/config.go | 132 +++++++++++++++++------ cmd/config_test.go | 261 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 361 insertions(+), 35 deletions(-) create mode 100644 cmd/config_test.go diff --git a/.gitignore b/.gitignore index b7ecc2c10..7a26eb4d9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ TODO circleci-cli coverage.txt dist/ +.vscode diff --git a/Gopkg.lock b/Gopkg.lock index e27cff859..d5a90bdc9 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -247,6 +247,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "0918dd6d626b6bc967d88ec42515ca4e30fa1890caafbf7f74472bf85a3dea7b" + inputs-digest = "f962eb3583db4a6bf85610971d2e59921e827a46813904524f97749a428e7581" solver-name = "gps-cdcl" solver-version = 1 diff --git a/cmd/config.go b/cmd/config.go index 3d208f833..9f82ebedf 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -3,7 +3,6 @@ package cmd import ( "bytes" "context" - "fmt" "io/ioutil" "github.com/pkg/errors" @@ -28,68 +27,133 @@ var validateCommand = &cobra.Command{ RunE: validateConfig, } +var expandCommand = &cobra.Command{ + Use: "expand", + Short: "Expand the config.", + RunE: expandConfig, +} + func init() { - validateCommand.Flags().StringVarP(&configPath, "path", "p", ".circleci/config.yml", "path to build config") + configCmd.PersistentFlags().StringVarP(&configPath, "path", "p", ".circleci/config.yml", "path to build config") configCmd.AddCommand(validateCommand) + configCmd.AddCommand(expandCommand) } -func validateConfig(cmd *cobra.Command, args []string) error { +// Define a structure that matches the result of the GQL +// query, so that we can use mapstructure to convert from +// nested maps to a strongly typed struct. +type buildConfigResponse struct { + BuildConfig struct { + Valid bool + SourceYaml string + OutputYaml string + + Errors []struct { + Message string + } + } +} +func queryAPI(query string, variables map[string]string, response interface{}) error { ctx := context.Background() - // Define a structure that matches the result of the GQL - // query, so that we can use mapstructure to convert from - // nested maps to a strongly typed struct. - type validateResult struct { - BuildConfig struct { - Valid bool - SourceYaml string - Errors []struct { - Message string - } - } + request := graphql.NewRequest(query) + request.Header.Set("Authorization", viper.GetString("token")) + for varName, varValue := range variables { + request.Var(varName, varValue) + } + + client := graphql.NewClient(viper.GetString("endpoint")) + + return client.Run(ctx, request, response) +} + +func loadYaml(path string) (string, error) { + 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 (response buildConfigResponse) processErrors() error { + var buffer bytes.Buffer + + buffer.WriteString("\n") + for i := range response.BuildConfig.Errors { + buffer.WriteString("-- ") + buffer.WriteString(response.BuildConfig.Errors[i].Message) + buffer.WriteString(",\n") } - request := graphql.NewRequest(` + return errors.New(buffer.String()) +} + +func validateConfig(cmd *cobra.Command, args []string) error { + query := ` query ValidateConfig ($config: String!) { buildConfig(configYaml: $config) { valid, errors { message }, sourceYaml } - }`) - - config, err := ioutil.ReadFile(configPath) + }` + config, err := loadYaml(configPath) if err != nil { - return errors.Wrapf(err, "Could not load config file at %s", configPath) + return err } - request.Var("config", string(config)) + variables := map[string]string{ + "config": config, + } - client := graphql.NewClient(viper.GetString("endpoint")) + var response buildConfigResponse + err = queryAPI(query, variables, &response) + if err != nil { + return errors.New("Unable to validate config") + } - var result validateResult + if !response.BuildConfig.Valid { + return response.processErrors() + } - err = client.Run(ctx, request, &result) + Logger.Infoln("Config is valid") + return nil +} +func expandConfig(cmd *cobra.Command, args []string) error { + query := ` + query ExpandConfig($config: String!) { + buildConfig(configYaml: $config) { + outputYaml + valid + errors { message } + } + } + ` + + config, err := loadYaml(configPath) if err != nil { - return errors.Wrap(err, "GraphQL query failed") + return err } - if !result.BuildConfig.Valid { - - var buffer bytes.Buffer + variables := map[string]string{ + "config": config, + } - for i := range result.BuildConfig.Errors { - buffer.WriteString(result.BuildConfig.Errors[i].Message) - buffer.WriteString("\n") - } + var response buildConfigResponse + err = queryAPI(query, variables, &response) + if err != nil { + return errors.New("Unable to expand config") + } - return fmt.Errorf("config file is invalid:\n%s", buffer.String()) + if !response.BuildConfig.Valid { + return response.processErrors() } - fmt.Println("Config is valid") + Logger.Info(response.BuildConfig.OutputYaml) return nil - } diff --git a/cmd/config_test.go b/cmd/config_test.go new file mode 100644 index 000000000..e9ad0dffa --- /dev/null +++ b/cmd/config_test.go @@ -0,0 +1,261 @@ +package cmd_test + +import ( + "io/ioutil" + "net/http" + "os" + "os/exec" + "path/filepath" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" + "github.com/onsi/gomega/gexec" + "github.com/onsi/gomega/ghttp" +) + +func appendPostHandler(server *ghttp.Server, authToken string, statusCode int, expectedRequestJson string, responseBody string) { + server.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("POST", "/"), + ghttp.VerifyHeader(http.Header{ + "Authorization": []string{authToken}, + }), + ghttp.VerifyContentType("application/json; charset=utf-8"), + // From Gomegas ghttp.VerifyJson to avoid the + // VerifyContentType("application/json") check + // that fails with "application/json; charset=utf-8" + func(w http.ResponseWriter, req *http.Request) { + body, err := ioutil.ReadAll(req.Body) + req.Body.Close() + Expect(err).ShouldNot(HaveOccurred()) + Expect(body).Should(MatchJSON(expectedRequestJson), "JSON Mismatch") + }, + ghttp.RespondWith(statusCode, `{ "data": `+responseBody+`}`), + ), + ) +} + +type configYaml struct { + TempHome string + Path string + YamlFile *os.File +} + +func openConfigYaml() (configYaml, error) { + var ( + config configYaml = configYaml{} + err error + ) + + const ( + configDir = ".circleci" + configFile = "config.yaml" + ) + + tempHome, err := ioutil.TempDir("", "circleci-cli-test-") + if err != nil { + return config, err + } + + err = os.Mkdir(filepath.Join(tempHome, configDir), 0700) + if err != nil { + return config, err + } + + config.Path = filepath.Join(tempHome, configDir, configFile) + + var file *os.File + file, err = os.OpenFile( + config.Path, + os.O_RDWR|os.O_CREATE, + 0600, + ) + if err != nil { + return config, err + } + + config.YamlFile = file + + return config, nil +} + +var _ = Describe("Config", func() { + Describe("with an api and config.yml", func() { + var ( + testServer *ghttp.Server + config configYaml + ) + + BeforeEach(func() { + var err error + config, err = openConfigYaml() + Expect(err).ToNot(HaveOccurred()) + + testServer = ghttp.NewServer() + }) + + AfterEach(func() { + config.YamlFile.Close() + os.RemoveAll(config.TempHome) + + testServer.Close() + }) + + Describe("when validating config", func() { + var ( + token string + command *exec.Cmd + ) + + BeforeEach(func() { + token = "testtoken" + command = exec.Command(pathCLI, + "config", "validate", + "-t", token, + "-e", testServer.URL(), + "-p", config.Path, + ) + }) + + It("works", func() { + _, err := config.YamlFile.Write([]byte(`some config`)) + Expect(err).ToNot(HaveOccurred()) + + gqlResponse := `{ + "buildConfig": { + "sourceYaml": "hello world", + "valid": true, + "errors": [] + } + }` + + expectedRequestJson := ` { + "query": "\n\t\tquery ValidateConfig ($config: String!) {\n\t\t\tbuildConfig(configYaml: $config) {\n\t\t\t\tvalid,\n\t\t\t\terrors { message },\n\t\t\t\tsourceYaml\n\t\t\t}\n\t\t}", + "variables": { + "config": "some config" + } + }` + + appendPostHandler(testServer, token, http.StatusOK, expectedRequestJson, gqlResponse) + + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Out).Should(gbytes.Say("Config is valid")) + Eventually(session).Should(gexec.Exit(0)) + }) + + It("prints errors if invalid", func() { + _, err := config.YamlFile.Write([]byte(`some config`)) + Expect(err).ToNot(HaveOccurred()) + + gqlResponse := `{ + "buildConfig": { + "sourceYaml": "hello world", + "valid": false, + "errors": [ + {"message": "invalid_config"} + ] + } + }` + + expectedRequestJson := ` { + "query": "\n\t\tquery ValidateConfig ($config: String!) {\n\t\t\tbuildConfig(configYaml: $config) {\n\t\t\t\tvalid,\n\t\t\t\terrors { message },\n\t\t\t\tsourceYaml\n\t\t\t}\n\t\t}", + "variables": { + "config": "some config" + } + }` + + appendPostHandler(testServer, token, http.StatusOK, expectedRequestJson, gqlResponse) + + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Err).Should(gbytes.Say("Error:")) + Eventually(session.Err).Should(gbytes.Say("-- invalid_config")) + Eventually(session).Should(gexec.Exit(255)) + + }) + }) + + Describe("when expanding config", func() { + var ( + token string + command *exec.Cmd + ) + + BeforeEach(func() { + token = "testtoken" + command = exec.Command(pathCLI, + "config", "expand", + "-t", token, + "-e", testServer.URL(), + "-p", config.Path, + ) + }) + + It("works", func() { + _, err := config.YamlFile.Write([]byte(`some config`)) + Expect(err).ToNot(HaveOccurred()) + + gqlResponse := `{ + "buildConfig": { + "outputYaml": "hello world", + "valid": true, + "errors": [] + } + }` + + expectedRequestJson := ` { + "query": "\n\t\tquery ExpandConfig($config: String!) {\n\t\t\tbuildConfig(configYaml: $config) {\n\t\t\t\toutputYaml\n\t\t\t\tvalid\n\t\t\t\terrors { message }\n\t\t\t}\n\t\t}\n\t", + "variables": { + "config": "some config" + } + }` + + appendPostHandler(testServer, token, http.StatusOK, expectedRequestJson, gqlResponse) + + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Out).Should(gbytes.Say("hello world")) + Eventually(session).Should(gexec.Exit(0)) + }) + + It("prints errors if invalid", func() { + _, err := config.YamlFile.Write([]byte(`some config`)) + Expect(err).ToNot(HaveOccurred()) + + gqlResponse := `{ + "buildConfig": { + "outputYaml": "hello world", + "valid": false, + "errors": [ + {"message": "error1"}, + {"message": "error2"} + ] + } + }` + + expectedRequestJson := ` { + "query": "\n\t\tquery ExpandConfig($config: String!) {\n\t\t\tbuildConfig(configYaml: $config) {\n\t\t\t\toutputYaml\n\t\t\t\tvalid\n\t\t\t\terrors { message }\n\t\t\t}\n\t\t}\n\t", + "variables": { + "config": "some config" + } + }` + + appendPostHandler(testServer, token, http.StatusOK, expectedRequestJson, gqlResponse) + + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Err).Should(gbytes.Say("Error:")) + Eventually(session.Err).Should(gbytes.Say("-- error1,")) + Eventually(session.Err).Should(gbytes.Say("-- error2,")) + Eventually(session).Should(gexec.Exit(255)) + + }) + }) + }) +})