diff --git a/api/api.go b/api/api.go new file mode 100644 index 000000000..5333c7c86 --- /dev/null +++ b/api/api.go @@ -0,0 +1,103 @@ +package api + +import ( + "context" + "io/ioutil" + "strings" + + "github.com/CircleCI-Public/circleci-cli/client" + "github.com/CircleCI-Public/circleci-cli/logger" + "github.com/pkg/errors" + "github.com/spf13/viper" +) + +// ConfigResponse is 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 ConfigResponse struct { + Valid bool + SourceYaml string + OutputYaml string + + Errors []struct { + Message string + } +} + +// ToError returns an error created from any error messages, or nil. +func (response ConfigResponse) ToError() error { + messages := []string{} + + for i := range response.Errors { + messages = append(messages, response.Errors[i].Message) + } + + return errors.New(strings.Join(messages, ": ")) +} + +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 buildAndOrbQuery(ctx context.Context, logger *logger.Logger, configPath string, response interface{}, query string) error { + config, err := loadYaml(configPath) + if err != nil { + return err + } + + request := client.NewAuthorizedRequest(viper.GetString("token"), query) + request.Var("config", config) + client := client.NewClient(viper.GetString("endpoint"), logger) + + err = client.Run(ctx, request, response) + + if err != nil { + return errors.Wrap(err, "Unable to validate config") + } + + return nil +} + +// ConfigQuery calls the GQL API to validate and expand config +func ConfigQuery(ctx context.Context, logger *logger.Logger, configPath string) (*ConfigResponse, error) { + var response struct { + BuildConfig struct { + ConfigResponse + } + } + return &response.BuildConfig.ConfigResponse, buildAndOrbQuery(ctx, logger, configPath, &response, ` + query ValidateConfig ($config: String!) { + buildConfig(configYaml: $config) { + valid, + errors { message }, + sourceYaml, + outputYaml + } + }`) +} + +// OrbQuery validated and expands an orb. +func OrbQuery(ctx context.Context, logger *logger.Logger, configPath string) (*ConfigResponse, error) { + var response struct { + OrbConfig struct { + ConfigResponse + } + } + + return &response.OrbConfig.ConfigResponse, buildAndOrbQuery(ctx, logger, configPath, &response, ` + query ValidateOrb ($config: String!) { + orbConfig(orbYaml: $config) { + valid, + errors { message }, + sourceYaml, + outputYaml + } + }`) +} diff --git a/cmd/config.go b/cmd/config.go index d6022df8e..be612317d 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -1,16 +1,10 @@ package cmd import ( - "bytes" "context" - "io/ioutil" - "github.com/CircleCI-Public/circleci-cli/client" - "github.com/pkg/errors" - - "github.com/machinebox/graphql" + "github.com/CircleCI-Public/circleci-cli/api" "github.com/spf13/cobra" - "github.com/spf13/viper" ) // Path to the config.yml file to operate on. @@ -40,99 +34,17 @@ func init() { configCmd.AddCommand(expandCommand) } -// 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(ctx context.Context, query string, variables map[string]string, response interface{}) error { - - request := client.NewAuthorizedRequest(viper.GetString("token"), query) - - 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") - } - - return errors.New(buffer.String()) -} - -func configQuery(ctx context.Context) (*buildConfigResponse, error) { - - query := ` - query ValidateConfig ($config: String!) { - buildConfig(configYaml: $config) { - valid, - errors { message }, - sourceYaml, - outputYaml - } - }` - - config, err := loadYaml(configPath) - if err != nil { - return nil, err - } - - variables := map[string]string{ - "config": config, - } - - var response buildConfigResponse - err = queryAPI(ctx, query, variables, &response) - if err != nil { - return nil, errors.Wrap(err, "Unable to validate config") - } - - return &response, nil -} - func validateConfig(cmd *cobra.Command, args []string) error { ctx := context.Background() - response, err := configQuery(ctx) + response, err := api.ConfigQuery(ctx, Logger, configPath) if err != nil { return err } - if !response.BuildConfig.Valid { - return response.processErrors() + if !response.Valid { + return response.ToError() } Logger.Infof("Config file at %s is valid", configPath) @@ -142,16 +54,16 @@ func validateConfig(cmd *cobra.Command, args []string) error { func expandConfig(cmd *cobra.Command, args []string) error { ctx := context.Background() - response, err := configQuery(ctx) + response, err := api.ConfigQuery(ctx, Logger, configPath) if err != nil { return err } - if !response.BuildConfig.Valid { - return response.processErrors() + if !response.Valid { + return response.ToError() } - Logger.Info(response.BuildConfig.OutputYaml) + Logger.Info(response.OutputYaml) return nil } diff --git a/cmd/config_test.go b/cmd/config_test.go index ad7636f16..be179adf4 100644 --- a/cmd/config_test.go +++ b/cmd/config_test.go @@ -103,7 +103,7 @@ var _ = Describe("Config", func() { Expect(err).ShouldNot(HaveOccurred()) Eventually(session.Err).Should(gbytes.Say("Error:")) - Eventually(session.Err).Should(gbytes.Say("-- invalid_config")) + Eventually(session.Err).Should(gbytes.Say("invalid_config")) Eventually(session).ShouldNot(gexec.Exit(0)) }) }) @@ -179,9 +179,7 @@ var _ = Describe("Config", func() { 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.Err).Should(gbytes.Say("Error: error1: error2")) Eventually(session).ShouldNot(gexec.Exit(0)) }) }) diff --git a/cmd/orb.go b/cmd/orb.go index e6c2359cb..5d5243696 100644 --- a/cmd/orb.go +++ b/cmd/orb.go @@ -1,10 +1,9 @@ package cmd import ( - "bytes" "context" - "io/ioutil" + "github.com/CircleCI-Public/circleci-cli/api" "github.com/CircleCI-Public/circleci-cli/client" "github.com/pkg/errors" @@ -129,73 +128,18 @@ query ListOrbs ($after: String!) { } } return nil - -} - -func loadOrbYaml(path string) (string, error) { - - orb, err := ioutil.ReadFile(path) - - if err != nil { - return "", errors.Wrapf(err, "Could not load orb file at %s", path) - } - - return string(orb), nil -} - -func (response orbConfigResponse) processErrors() error { - var buffer bytes.Buffer - - buffer.WriteString("\n") - for i := range response.OrbConfig.Errors { - buffer.WriteString("-- ") - buffer.WriteString(response.OrbConfig.Errors[i].Message) - buffer.WriteString(",\n") - } - - return errors.New(buffer.String()) -} - -func orbValidateQuery(ctx context.Context) (*orbConfigResponse, error) { - - query := ` - query ValidateOrb ($orb: String!) { - orbConfig(orbYaml: $orb) { - valid, - errors { message }, - sourceYaml, - outputYaml - } - }` - - orb, err := loadOrbYaml(orbPath) - if err != nil { - return nil, err - } - - variables := map[string]string{ - "orb": orb, - } - - var response orbConfigResponse - err = queryAPI(ctx, query, variables, &response) - if err != nil { - return nil, errors.Wrap(err, "Unable to validate orb") - } - - return &response, nil } func validateOrb(cmd *cobra.Command, args []string) error { ctx := context.Background() - response, err := orbValidateQuery(ctx) + response, err := api.OrbQuery(ctx, Logger, orbPath) if err != nil { return err } - if !response.OrbConfig.Valid { - return response.processErrors() + if !response.Valid { + return response.ToError() } Logger.Infof("Orb at %s is valid", orbPath) @@ -205,16 +149,16 @@ func validateOrb(cmd *cobra.Command, args []string) error { func expandOrb(cmd *cobra.Command, args []string) error { ctx := context.Background() - response, err := orbValidateQuery(ctx) + response, err := api.OrbQuery(ctx, Logger, orbPath) if err != nil { return err } - if !response.OrbConfig.Valid { - return response.processErrors() + if !response.Valid { + return response.ToError() } - Logger.Info(response.OrbConfig.OutputYaml) + Logger.Info(response.OutputYaml) return nil } diff --git a/cmd/orb_test.go b/cmd/orb_test.go index a1615587a..13bda98d6 100644 --- a/cmd/orb_test.go +++ b/cmd/orb_test.go @@ -62,9 +62,9 @@ var _ = Describe("Orb", func() { }` expectedRequestJson := ` { - "query": "\n\t\tquery ValidateOrb ($orb: String!) {\n\t\t\torbConfig(orbYaml: $orb) {\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!) {\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}", "variables": { - "orb": "{}" + "config": "{}" } }` @@ -95,9 +95,9 @@ var _ = Describe("Orb", func() { }` expectedRequestJson := ` { - "query": "\n\t\tquery ValidateOrb ($orb: String!) {\n\t\t\torbConfig(orbYaml: $orb) {\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!) {\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}", "variables": { - "orb": "some orb" + "config": "some orb" } }` appendPostHandler(testServer, token, http.StatusOK, expectedRequestJson, gqlResponse) @@ -106,8 +106,7 @@ var _ = Describe("Orb", func() { 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_orb")) + Eventually(session.Err).Should(gbytes.Say("Error: invalid_orb")) Eventually(session).ShouldNot(gexec.Exit(0)) }) }) @@ -142,9 +141,9 @@ var _ = Describe("Orb", func() { }` expectedRequestJson := ` { - "query": "\n\t\tquery ValidateOrb ($orb: String!) {\n\t\t\torbConfig(orbYaml: $orb) {\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!) {\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}", "variables": { - "orb": "some orb" + "config": "some orb" } }` @@ -175,9 +174,9 @@ var _ = Describe("Orb", func() { }` expectedRequestJson := ` { - "query": "\n\t\tquery ValidateOrb ($orb: String!) {\n\t\t\torbConfig(orbYaml: $orb) {\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!) {\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}", "variables": { - "orb": "some orb" + "config": "some orb" } }` @@ -187,9 +186,7 @@ var _ = Describe("Orb", func() { 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.Err).Should(gbytes.Say("Error: error1: error2")) Eventually(session).ShouldNot(gexec.Exit(0)) })