Skip to content

Commit

Permalink
refactor: Abstracted the different config validation routes
Browse files Browse the repository at this point in the history
  • Loading branch information
JulesFaucherre committed Aug 7, 2023
1 parent bed1100 commit fde1bce
Show file tree
Hide file tree
Showing 12 changed files with 359 additions and 131 deletions.
9 changes: 7 additions & 2 deletions api/rest/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
Expand Down Expand Up @@ -84,7 +85,7 @@ func (c *Client) enrichRequestHeaders(req *http.Request, payload interface{}) {
}
}

func (c *Client) DoRequest(req *http.Request, resp interface{}) (statusCode int, err error) {
func (c *Client) DoRequest(req *http.Request, resp interface{}) (int, error) {
httpResp, err := c.client.Do(req)
if err != nil {
fmt.Printf("failed to make http request: %s\n", err.Error())
Expand All @@ -96,10 +97,14 @@ func (c *Client) DoRequest(req *http.Request, resp interface{}) (statusCode int,
httpError := struct {
Message string `json:"message"`
}{}
err = json.NewDecoder(httpResp.Body).Decode(&httpError)
body, err := ioutil.ReadAll(httpResp.Body)
if err != nil {
return httpResp.StatusCode, err
}
err = json.Unmarshal(body, &httpError)
if err != nil {
return httpResp.StatusCode, &HTTPError{Code: httpResp.StatusCode, Message: string(body)}
}
return httpResp.StatusCode, &HTTPError{Code: httpResp.StatusCode, Message: httpError.Message}
}

Expand Down
12 changes: 10 additions & 2 deletions cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ func newConfigCommand(globalConfig *settings.Config) *cobra.Command {
Aliases: []string{"check"},
Short: "Check that the config file is well formed.",
RunE: func(cmd *cobra.Command, args []string) error {
compiler := config.New(globalConfig)
compiler, err := config.NewWithConfig(globalConfig)
if err != nil {
return err
}

orgID, _ := cmd.Flags().GetString("org-id")
orgSlug, _ := cmd.Flags().GetString("org-slug")
path := config.DefaultConfigPath
Expand Down Expand Up @@ -86,7 +90,11 @@ func newConfigCommand(globalConfig *settings.Config) *cobra.Command {
Use: "process <path>",
Short: "Validate config and display expanded configuration.",
RunE: func(cmd *cobra.Command, args []string) error {
compiler := config.New(globalConfig)
compiler, err := config.NewWithConfig(globalConfig)
if err != nil {
return err
}

pipelineParamsFilePath, _ := cmd.Flags().GetString("pipeline-parameters")
orgID, _ := cmd.Flags().GetString("org-id")
orgSlug, _ := cmd.Flags().GetString("org-slug")
Expand Down
71 changes: 71 additions & 0 deletions config/api_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package config

import (
"fmt"
"net/url"
"sync"

"github.com/CircleCI-Public/circleci-cli/api/graphql"
"github.com/CircleCI-Public/circleci-cli/api/rest"
"github.com/CircleCI-Public/circleci-cli/settings"
)

var (
compiler APIClient
compilerError error
once sync.Once

compilePath = "compile-config-with-defaults"
)

type apiClientVersion string

type APIClient interface {
CompileConfig(configContent string, orgID string, params Parameters, values Values) (*ConfigResponse, error)
}

func GetAPIClient(config *settings.Config) (APIClient, error) {
if compiler == nil {
once.Do(func() {
compiler, compilerError = newAPIClient(config)
})
}
return compiler, compilerError
}

func newAPIClient(config *settings.Config) (APIClient, error) {
hostValue := getCompileHost(config.Host)
restClient := rest.NewFromConfig(hostValue, config)

version, err := detectAPIClientVersion(restClient)
if err != nil {
return nil, err
}

switch version {
case v1_string:
return &v1APIClient{graphql.NewClient(config.HTTPClient, config.Host, config.Endpoint, config.Token, config.Debug)}, nil
case v2_string:
return &v2APIClient{restClient}, nil
default:
return nil, fmt.Errorf("Unable to recognise your Server's config file API")
}
}

func detectAPIClientVersion(restClient *rest.Client) (apiClientVersion, error) {
req, err := restClient.NewRequest("POST", &url.URL{Path: compilePath}, nil)
if err != nil {
return "", err
}

statusCode, err := restClient.DoRequest(req, nil)
if err != nil {
if _, ok := err.(*rest.HTTPError); ok {
return "", err
}
}
if statusCode == 404 {
return v1_string, nil
}
return v2_string, nil
}
61 changes: 61 additions & 0 deletions config/api_client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package config

import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"

"github.com/CircleCI-Public/circleci-cli/api/rest"
"gotest.tools/v3/assert"
)

func TestAPIClient(t *testing.T) {
t.Run("detectCompilerVersion", func(t *testing.T) {
t.Run("when the route returns a 400 tells that the version is v2", func(t *testing.T) {
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(400)
w.Header().Set("Content-Type", "application/octet-stream")
fmt.Fprintf(w, "Invalid input")
}))
url, err := url.Parse(svr.URL)
assert.NilError(t, err)

restClient := rest.New(url, "token", http.DefaultClient)
version, err := detectAPIClientVersion(restClient)
assert.NilError(t, err)
assert.Equal(t, version, v2_string)
})

t.Run("when the route returns a 404 tells that the version is v1", func(t *testing.T) {
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(404)
w.Header().Set("Content-Type", "application/octet-stream")
fmt.Fprintf(w, "Invalid input")
}))
url, err := url.Parse(svr.URL)
assert.NilError(t, err)

restClient := rest.New(url, "token", http.DefaultClient)
version, err := detectAPIClientVersion(restClient)
assert.NilError(t, err)
assert.Equal(t, version, v1_string)
})

t.Run("in any other case, return an error", func(t *testing.T) {
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(500)
w.Header().Set("Content-Type", "application/octet-stream")
fmt.Fprintf(w, "Invalid input")
}))
url, err := url.Parse(svr.URL)
assert.NilError(t, err)

restClient := rest.New(url, "token", http.DefaultClient)
version, err := detectAPIClientVersion(restClient)
assert.Equal(t, version, apiClientVersion(""))
assert.Error(t, err, "Invalid input")
})
})
}
40 changes: 31 additions & 9 deletions config/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"net/http/httptest"
"testing"

"github.com/CircleCI-Public/circleci-cli/api/collaborators"
"github.com/CircleCI-Public/circleci-cli/api/rest"
"github.com/CircleCI-Public/circleci-cli/settings"
"github.com/stretchr/testify/assert"
)
Expand All @@ -18,7 +20,11 @@ func TestGetOrgID(t *testing.T) {
fmt.Fprintf(w, `[{"vcs_type":"circleci","slug":"gh/test","id":"2345"}]`)
}))
defer svr.Close()
compiler := New(&settings.Config{Host: svr.URL, HTTPClient: http.DefaultClient})
cfg := &settings.Config{Host: svr.URL, HTTPClient: http.DefaultClient}
apiClient := &v2APIClient{rest.NewFromConfig(cfg.Host, cfg)}
collaboratorsClient, err := collaborators.NewCollaboratorsRestClient(*cfg)
assert.NoError(t, err)
compiler := New(apiClient, collaboratorsClient)

t.Run("returns the original org-id passed if it is set", func(t *testing.T) {
expected := "1234"
Expand Down Expand Up @@ -66,9 +72,13 @@ func TestValidateConfig(t *testing.T) {
fmt.Fprintf(w, `{"valid":true,"source-yaml":"%s","output-yaml":"%s","errors":[]}`, testYaml, testYaml)
}))
defer svr.Close()
compiler := New(&settings.Config{Host: svr.URL, HTTPClient: http.DefaultClient})
cfg := &settings.Config{Host: svr.URL, HTTPClient: http.DefaultClient}
apiClient := &v2APIClient{rest.NewFromConfig(cfg.Host, cfg)}
collaboratorsClient, err := collaborators.NewCollaboratorsRestClient(*cfg)
assert.NoError(t, err)
compiler := New(apiClient, collaboratorsClient)

err := compiler.ValidateConfig(ValidateConfigOpts{
err = compiler.ValidateConfig(ValidateConfigOpts{
ConfigPath: "testdata/config.yml",
})
assert.NoError(t, err)
Expand All @@ -87,9 +97,13 @@ func TestValidateConfig(t *testing.T) {
fmt.Fprintf(w, `{"valid":true,"source-yaml":"%s","output-yaml":"%s","errors":[]}`, testYaml, testYaml)
}))
defer svr.Close()
compiler := New(&settings.Config{Host: svr.URL, HTTPClient: http.DefaultClient})
cfg := &settings.Config{Host: svr.URL, HTTPClient: http.DefaultClient}
apiClient := &v2APIClient{rest.NewFromConfig(cfg.Host, cfg)}
collaboratorsClient, err := collaborators.NewCollaboratorsRestClient(*cfg)
assert.NoError(t, err)
compiler := New(apiClient, collaboratorsClient)

err := compiler.ValidateConfig(ValidateConfigOpts{
err = compiler.ValidateConfig(ValidateConfigOpts{
ConfigPath: "testdata/config.yml",
OrgID: "1234",
})
Expand Down Expand Up @@ -119,9 +133,13 @@ func TestValidateConfig(t *testing.T) {
svr := httptest.NewServer(mux)
defer svr.Close()

compiler := New(&settings.Config{Host: svr.URL, HTTPClient: http.DefaultClient})
cfg := &settings.Config{Host: svr.URL, HTTPClient: http.DefaultClient}
apiClient := &v2APIClient{rest.NewFromConfig(cfg.Host, cfg)}
collaboratorsClient, err := collaborators.NewCollaboratorsRestClient(*cfg)
assert.NoError(t, err)
compiler := New(apiClient, collaboratorsClient)

err := compiler.ValidateConfig(ValidateConfigOpts{
err = compiler.ValidateConfig(ValidateConfigOpts{
ConfigPath: "testdata/config.yml",
OrgSlug: "gh/test",
})
Expand Down Expand Up @@ -151,9 +169,13 @@ func TestValidateConfig(t *testing.T) {
svr := httptest.NewServer(mux)
defer svr.Close()

compiler := New(&settings.Config{Host: svr.URL, HTTPClient: http.DefaultClient})
cfg := &settings.Config{Host: svr.URL, HTTPClient: http.DefaultClient}
apiClient := &v2APIClient{rest.NewFromConfig(cfg.Host, cfg)}
collaboratorsClient, err := collaborators.NewCollaboratorsRestClient(*cfg)
assert.NoError(t, err)
compiler := New(apiClient, collaboratorsClient)

err := compiler.ValidateConfig(ValidateConfigOpts{
err = compiler.ValidateConfig(ValidateConfigOpts{
ConfigPath: "testdata/config.yml",
OrgSlug: "gh/nonexistent",
})
Expand Down
74 changes: 14 additions & 60 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,9 @@ package config
import (
"fmt"
"io"
"net/url"
"os"

"github.com/CircleCI-Public/circleci-cli/api/collaborators"
"github.com/CircleCI-Public/circleci-cli/api/graphql"
"github.com/CircleCI-Public/circleci-cli/api/rest"
"github.com/CircleCI-Public/circleci-cli/settings"
"github.com/pkg/errors"
)
Expand All @@ -22,29 +19,28 @@ var (
)

type ConfigCompiler struct {
host string
compileRestClient *rest.Client
collaborators collaborators.CollaboratorsClient

cfg *settings.Config
legacyGraphQLClient *graphql.Client
apiClient APIClient
collaborators collaborators.CollaboratorsClient
}

func New(cfg *settings.Config) *ConfigCompiler {
hostValue := GetCompileHost(cfg.Host)
func NewWithConfig(cfg *settings.Config) (*ConfigCompiler, error) {
apiClient, err := GetAPIClient(cfg)
if err != nil {
return nil, err
}
collaboratorsClient, err := collaborators.NewCollaboratorsRestClient(*cfg)
if err != nil {
panic(err)
return nil, err
}
return New(apiClient, collaboratorsClient), nil
}

func New(apiClient APIClient, collaboratorsClient collaborators.CollaboratorsClient) *ConfigCompiler {
configCompiler := &ConfigCompiler{
host: hostValue,
compileRestClient: rest.NewFromConfig(hostValue, cfg),
collaborators: collaboratorsClient,
cfg: cfg,
apiClient: apiClient,
collaborators: collaboratorsClient,
}

configCompiler.legacyGraphQLClient = graphql.NewClient(cfg.HTTPClient, cfg.Host, cfg.Endpoint, cfg.Token, cfg.Debug)
return configCompiler
}

Expand Down Expand Up @@ -94,49 +90,7 @@ func (c *ConfigCompiler) ConfigQuery(
return nil, fmt.Errorf("failed to load yaml config from config path provider: %w", err)
}

compileRequest := CompileConfigRequest{
ConfigYaml: configString,
Options: Options{
OwnerID: orgID,
PipelineValues: values,
PipelineParameters: params,
},
}

req, err := c.compileRestClient.NewRequest(
"POST",
&url.URL{
Path: "compile-config-with-defaults",
},
compileRequest,
)
if err != nil {
return nil, fmt.Errorf("an error occurred creating the request: %w", err)
}

configCompilationResp := &ConfigResponse{}
statusCode, originalErr := c.compileRestClient.DoRequest(req, configCompilationResp)
if statusCode == 404 {
fmt.Fprintf(os.Stderr, "You are using a old version of CircleCI Server, please consider updating\n")
legacyResponse, err := c.legacyConfigQueryByOrgID(configString, orgID, params, values, c.cfg)
if err != nil {
return nil, err
}
return legacyResponse, nil
}
if originalErr != nil {
return nil, fmt.Errorf("config compilation request returned an error: %w", originalErr)
}

if statusCode != 200 {
return nil, errors.New("unable to validate or compile config")
}

if len(configCompilationResp.Errors) > 0 {
return nil, fmt.Errorf("config compilation contains errors: %s", configCompilationResp.Errors)
}

return configCompilationResp, nil
return c.apiClient.CompileConfig(configString, orgID, params, values)
}

func loadYaml(path string) (string, error) {
Expand Down
Loading

0 comments on commit fde1bce

Please sign in to comment.