From 3dd9730c5df245d4d6ca0a2d3dd34ad840271950 Mon Sep 17 00:00:00 2001 From: JulesFaucherre Date: Wed, 19 Jul 2023 09:20:24 +0200 Subject: [PATCH] refactor: Moved the orb validation in a orb API module --- api/api.go | 127 ------------------------------------------ api/orb/client.go | 105 ++++++++++++++++++++++++++++++++++ api/orb/deprecated.go | 50 +++++++++++++++++ api/orb/latest.go | 48 ++++++++++++++++ api/orb/yaml.go | 24 ++++++++ cmd/orb.go | 13 ++++- cmd/orb_test.go | 106 ++++++++++++++++++++++++++++++++++- 7 files changed, 341 insertions(+), 132 deletions(-) create mode 100644 api/orb/client.go create mode 100644 api/orb/deprecated.go create mode 100644 api/orb/latest.go create mode 100644 api/orb/yaml.go diff --git a/api/api.go b/api/api.go index 0c51b486d..2dbaca0bd 100644 --- a/api/api.go +++ b/api/api.go @@ -512,133 +512,6 @@ func WhoamiQuery(cl *graphql.Client) (*WhoamiResponse, error) { return &response, nil } -// OrbQuery validated and processes an orb. -func OrbQuery(cl *graphql.Client, configPath string, ownerId string) (*ConfigResponse, error) { - var response OrbConfigResponse - - config, err := loadYaml(configPath) - if err != nil { - return nil, err - } - - request, err := makeOrbRequest(cl, config, ownerId) - if err != nil { - return nil, err - } - - err = cl.Run(request, &response) - if err != nil { - return nil, errors.Wrap(err, "Unable to validate config") - } - - if len(response.OrbConfig.ConfigResponse.Errors) > 0 { - return nil, response.OrbConfig.ConfigResponse.Errors - } - - return &response.OrbConfig.ConfigResponse, nil -} - -func makeOrbRequest(cl *graphql.Client, configContent string, ownerId string) (*graphql.Request, error) { - handlesOwner := orbQueryHandleOwnerId(cl) - - if handlesOwner { - query := ` - query ValidateOrb ($config: String!, $owner: UUID) { - orbConfig(orbYaml: $config, ownerId: $owner) { - valid, - errors { message }, - sourceYaml, - outputYaml - } - }` - - request := graphql.NewRequest(query) - request.Var("config", configContent) - - if ownerId != "" { - request.Var("owner", ownerId) - } - - request.SetToken(cl.Token) - return request, nil - } - - if ownerId != "" { - return nil, errors.Errorf("Your version of Server does not support validating orbs that refer to other private orbs. Please see the README for more information on server compatibility: https://github.com/CircleCI-Public/circleci-cli#server-compatibility") - } - query := ` - query ValidateOrb ($config: String!) { - orbConfig(orbYaml: $config) { - valid, - errors { message }, - sourceYaml, - outputYaml - } - }` - - request := graphql.NewRequest(query) - request.Var("config", configContent) - - request.SetToken(cl.Token) - return request, nil -} - -type OrbIntrospectionResponse struct { - Schema struct { - Query struct { - Fields []struct { - Name string `json:"name"` - Args []struct { - Name string `json:"name"` - } `json:"args"` - } `json:"fields"` - } `json:"queryType"` - } `json:"__schema"` -} - -func orbQueryHandleOwnerId(cl *graphql.Client) bool { - query := ` -query ValidateOrb { - __schema { - queryType { - fields(includeDeprecated: true) { - name - args { - name - __typename - type { - name - } - } - } - } - } -}` - request := graphql.NewRequest(query) - response := OrbIntrospectionResponse{} - err := cl.Run(request, &response) - if err != nil { - return false - } - - request.SetToken(cl.Token) - - // Find the orbConfig query method, look at its arguments, if it has the "ownerId" argument, return true - for _, field := range response.Schema.Query.Fields { - if field.Name == "orbConfig" { - for _, arg := range field.Args { - if arg.Name == "ownerId" { - return true - } - } - } - } - - // else return false, ownerId is not supported - - return false -} - // OrbImportVersion publishes a new version of an orb using the provided source and id. func OrbImportVersion(cl *graphql.Client, orbSrc string, orbID string, orbVersion string) (*Orb, error) { var response OrbImportVersionResponse diff --git a/api/orb/client.go b/api/orb/client.go new file mode 100644 index 000000000..bc71ac2c0 --- /dev/null +++ b/api/orb/client.go @@ -0,0 +1,105 @@ +package orb + +import ( + "fmt" + "sync" + + "github.com/CircleCI-Public/circleci-cli/api" + "github.com/CircleCI-Public/circleci-cli/api/graphql" + "github.com/CircleCI-Public/circleci-cli/settings" +) + +var ( + once sync.Once + client Client +) + +// 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 QueryResponse struct { + OrbConfig struct { + api.ConfigResponse + } +} + +type Client interface { + OrbQuery(configPath string, ownerId string) (*api.ConfigResponse, error) +} + +func GetClient(config *settings.Config) Client { + once.Do(func() { + createClient(config) + }) + return client +} + +func createClient(config *settings.Config) { + gql := graphql.NewClient(config.HTTPClient, config.Host, config.Endpoint, config.Token, config.Debug) + + ok, err := orbQueryHandleOwnerId(gql) + if err != nil { + fmt.Printf("While requesting orb server: %s", err) + return + } else if ok { + client = &latestClient{gql} + } else { + client = &deprecatedClient{gql} + } +} + +type OrbIntrospectionResponse struct { + Schema struct { + Query struct { + Fields []struct { + Name string `json:"name"` + Args []struct { + Name string `json:"name"` + } `json:"args"` + } `json:"fields"` + } `json:"queryType"` + } `json:"__schema"` +} + +func orbQueryHandleOwnerId(gql *graphql.Client) (bool, error) { + query := ` +query ValidateOrb { + __schema { + queryType { + fields(includeDeprecated: true) { + name + args { + name + __typename + type { + name + } + } + } + } + } +}` + request := graphql.NewRequest(query) + response := OrbIntrospectionResponse{} + err := gql.Run(request, &response) + if err != nil { + return false, err + } + + request.SetToken(gql.Token) + + // Find the orbConfig query method, look at its arguments, if it has the "ownerId" argument, return true + for _, field := range response.Schema.Query.Fields { + if field.Name == "orbConfig" { + for _, arg := range field.Args { + if arg.Name == "ownerId" { + return true, nil + } + } + } + } + + // else return false, ownerId is not supported + + return false, nil +} diff --git a/api/orb/deprecated.go b/api/orb/deprecated.go new file mode 100644 index 000000000..ca73f8c01 --- /dev/null +++ b/api/orb/deprecated.go @@ -0,0 +1,50 @@ +package orb + +import ( + "github.com/CircleCI-Public/circleci-cli/api" + "github.com/CircleCI-Public/circleci-cli/api/graphql" + "github.com/pkg/errors" +) + +type deprecatedClient struct { + gql *graphql.Client +} + +func (deprecated *deprecatedClient) OrbQuery(configPath string, ownerId string) (*api.ConfigResponse, error) { + if ownerId != "" { + return nil, errors.New("Your version of Server does not support validating orbs that refer to other private orbs. Please see the README for more information on server compatibility: https://github.com/CircleCI-Public/circleci-cli#server-compatibility") + } + + var response QueryResponse + + configContent, err := loadYaml(configPath) + if err != nil { + return nil, err + } + + query := ` + query ValidateOrb ($config: String!) { + orbConfig(orbYaml: $config) { + valid, + errors { message }, + sourceYaml, + outputYaml + } + }` + + request := graphql.NewRequest(query) + request.Var("config", configContent) + + request.SetToken(deprecated.gql.Token) + + err = deprecated.gql.Run(request, &response) + if err != nil { + return nil, errors.Wrap(err, "Unable to validate config") + } + + if len(response.OrbConfig.ConfigResponse.Errors) > 0 { + return nil, response.OrbConfig.ConfigResponse.Errors + } + + return &response.OrbConfig.ConfigResponse, nil +} diff --git a/api/orb/latest.go b/api/orb/latest.go new file mode 100644 index 000000000..91918688d --- /dev/null +++ b/api/orb/latest.go @@ -0,0 +1,48 @@ +package orb + +import ( + "github.com/CircleCI-Public/circleci-cli/api" + "github.com/CircleCI-Public/circleci-cli/api/graphql" + "github.com/pkg/errors" +) + +type latestClient struct { + gql *graphql.Client +} + +func (latest *latestClient) OrbQuery(configPath string, ownerId string) (*api.ConfigResponse, error) { + var response QueryResponse + + configContent, err := loadYaml(configPath) + if err != nil { + return nil, err + } + + query := ` + query ValidateOrb ($config: String!, $owner: UUID) { + orbConfig(orbYaml: $config, ownerId: $owner) { + valid, + errors { message }, + sourceYaml, + outputYaml + } + }` + request := graphql.NewRequest(query) + request.Var("config", configContent) + + if ownerId != "" { + request.Var("owner", ownerId) + } + request.SetToken(latest.gql.Token) + + err = latest.gql.Run(request, &response) + if err != nil { + return nil, errors.Wrap(err, "Unable to validate config") + } + + if len(response.OrbConfig.ConfigResponse.Errors) > 0 { + return nil, response.OrbConfig.ConfigResponse.Errors + } + + return &response.OrbConfig.ConfigResponse, nil +} diff --git a/api/orb/yaml.go b/api/orb/yaml.go new file mode 100644 index 000000000..22cae2e91 --- /dev/null +++ b/api/orb/yaml.go @@ -0,0 +1,24 @@ +package orb + +import ( + "io" + "os" + + "github.com/pkg/errors" +) + +func loadYaml(path string) (string, error) { + var err error + var config []byte + if path == "-" { + config, err = io.ReadAll(os.Stdin) + } else { + config, err = os.ReadFile(path) + } + + if err != nil { + return "", errors.Wrapf(err, "Could not load config file at %s", path) + } + + return string(config), nil +} diff --git a/cmd/orb.go b/cmd/orb.go index a7037c7d8..87e454c49 100644 --- a/cmd/orb.go +++ b/cmd/orb.go @@ -20,6 +20,7 @@ import ( "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/api/orb" "github.com/CircleCI-Public/circleci-cli/filetree" "github.com/CircleCI-Public/circleci-cli/process" "github.com/CircleCI-Public/circleci-cli/prompt" @@ -732,7 +733,11 @@ func validateOrb(opts orbOptions, org orbOrgOptions) error { return fmt.Errorf("failed to get the appropriate org-id: %s", err.Error()) } - _, err = api.OrbQuery(opts.cl, opts.args[0], orgId) + client := orb.GetClient(opts.cfg) + if client == nil { + return fmt.Errorf("Unable to validate orb") + } + _, err = client.OrbQuery(opts.args[0], orgId) if err != nil { return err @@ -754,7 +759,11 @@ func processOrb(opts orbOptions, org orbOrgOptions) error { return fmt.Errorf("failed to get the appropriate org-id: %s", err.Error()) } - response, err := api.OrbQuery(opts.cl, opts.args[0], orgId) + client := orb.GetClient(opts.cfg) + if client == nil { + return fmt.Errorf("Unable to validate orb") + } + response, err := client.OrbQuery(opts.args[0], orgId) if err != nil { return err diff --git a/cmd/orb_test.go b/cmd/orb_test.go index 7b4e917ba..a2f98ca13 100644 --- a/cmd/orb_test.go +++ b/cmd/orb_test.go @@ -210,9 +210,12 @@ See a full explanation and documentation on orbs here: https://circleci.com/docs }) }) - Describe("with old server version", func() { + Describe("org-id parameter", func() { BeforeEach(func() { token = "testtoken" + }) + + It("should use the old GraphQL resolver when the parameter is not present", func() { command = exec.Command(pathCLI, "orb", "validate", "--skip-update-check", @@ -229,9 +232,7 @@ See a full explanation and documentation on orbs here: https://circleci.com/docs panic(err) } }() - }) - It("should use the old GraphQL resolver", func() { By("setting up a mock server") mockOrbIntrospection(false, "", tempSettings) @@ -279,6 +280,105 @@ See a full explanation and documentation on orbs here: https://circleci.com/docs Eventually(session.Out).Should(gbytes.Say("Orb input is valid.")) Eventually(session).Should(gexec.Exit(0)) }) + + It("indicate a deprecation error when using the parameter with outdated Server", func() { + command = exec.Command(pathCLI, + "orb", "validate", + "--skip-update-check", + "--token", token, + "--host", tempSettings.TestServer.URL(), + "--org-id", "org-id", + "-", + ) + stdin, err := command.StdinPipe() + Expect(err).ToNot(HaveOccurred()) + go func() { + defer stdin.Close() + _, err := io.WriteString(stdin, "{}") + if err != nil { + panic(err) + } + }() + + By("setting up a mock server") + + mockOrbIntrospection(false, "", tempSettings) + + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Err).Should(gbytes.Say("Your version of Server does not support validating orbs that refer to other private orbs. Please see the README for more information on server compatibility: https://github.com/CircleCI-Public/circleci-cli#server-compatibility")) + Eventually(session).Should(gexec.Exit(-1)) + }) + + It("should work properly with modern Server", func() { + command = exec.Command(pathCLI, + "orb", "validate", + "--skip-update-check", + "--token", token, + "--host", tempSettings.TestServer.URL(), + "--org-id", "org-id", + "-", + ) + stdin, err := command.StdinPipe() + Expect(err).ToNot(HaveOccurred()) + go func() { + defer stdin.Close() + _, err := io.WriteString(stdin, "{}") + if err != nil { + panic(err) + } + }() + + By("setting up a mock server") + + mockOrbIntrospection(true, "", tempSettings) + gqlResponse := `{ + "orbConfig": { + "sourceYaml": "{}", + "valid": true, + "errors": [] + } + }` + + response := struct { + Query string `json:"query"` + Variables struct { + Config string `json:"config"` + Owner string `json:"owner"` + } `json:"variables"` + }{ + Query: ` + query ValidateOrb ($config: String!, $owner: UUID) { + orbConfig(orbYaml: $config, ownerId: $owner) { + valid, + errors { message }, + sourceYaml, + outputYaml + } + }`, + Variables: struct { + Config string `json:"config"` + Owner string `json:"owner"` + }{ + Config: "{}", + Owner: "org-id", + }, + } + expected, err := json.Marshal(response) + Expect(err).ShouldNot(HaveOccurred()) + + tempSettings.AppendPostHandler(token, clitest.MockRequestResponse{ + Status: http.StatusOK, + Request: string(expected), + Response: gqlResponse}) + + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Out).Should(gbytes.Say("Orb input is valid.")) + Eventually(session).Should(gexec.Exit(0)) + }) }) Context("with 'some orb'", func() {