-
Notifications
You must be signed in to change notification settings - Fork 234
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #915 from CircleCI-Public/adding-fallback-config-c…
…ompilation-and-validation-option First attempt at adding a fallback option which queries graphQL should the http request fail with status 404
- Loading branch information
Showing
6 changed files
with
280 additions
and
38 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
package config | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"sort" | ||
"strings" | ||
|
||
"github.com/CircleCI-Public/circleci-cli/api/graphql" | ||
"github.com/CircleCI-Public/circleci-cli/settings" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
// GQLErrorsCollection is a slice of errors returned by the GraphQL server. | ||
// Each error is made up of a GQLResponseError type. | ||
type GQLErrorsCollection []GQLResponseError | ||
|
||
// BuildConfigResponse wraps the GQL result of the ConfigQuery | ||
type BuildConfigResponse struct { | ||
BuildConfig struct { | ||
LegacyConfigResponse | ||
} | ||
} | ||
|
||
// Error turns a GQLErrorsCollection into an acceptable error string that can be printed to the user. | ||
func (errs GQLErrorsCollection) Error() string { | ||
messages := []string{} | ||
|
||
for i := range errs { | ||
messages = append(messages, errs[i].Message) | ||
} | ||
|
||
return strings.Join(messages, "\n") | ||
} | ||
|
||
// LegacyConfigResponse 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 LegacyConfigResponse struct { | ||
Valid bool | ||
SourceYaml string | ||
OutputYaml string | ||
|
||
Errors GQLErrorsCollection | ||
} | ||
|
||
// GQLResponseError is a mapping of the data returned by the GraphQL server of key-value pairs. | ||
// Typically used with the structure "Message: string", but other response errors provide additional fields. | ||
type GQLResponseError struct { | ||
Message string | ||
Value string | ||
AllowedValues []string | ||
EnumType string | ||
Type string | ||
} | ||
|
||
// PrepareForGraphQL takes a golang homogenous map, and transforms it into a list of keyval pairs, since GraphQL does not support homogenous maps. | ||
func PrepareForGraphQL(kvMap Values) []KeyVal { | ||
// we need to create the slice of KeyVals in a deterministic order for testing purposes | ||
keys := make([]string, 0, len(kvMap)) | ||
for k := range kvMap { | ||
keys = append(keys, k) | ||
} | ||
sort.Strings(keys) | ||
|
||
kvs := make([]KeyVal, 0, len(kvMap)) | ||
for _, k := range keys { | ||
kvs = append(kvs, KeyVal{Key: k, Val: kvMap[k]}) | ||
} | ||
return kvs | ||
} | ||
|
||
func (c *ConfigCompiler) legacyConfigQueryByOrgID( | ||
configString string, | ||
orgID string, | ||
params Parameters, | ||
values Values, | ||
cfg *settings.Config, | ||
) (*ConfigResponse, error) { | ||
var response BuildConfigResponse | ||
// 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!], $orgSlug: String) { | ||
buildConfig(configYaml: $config, pipelineValues: $pipelineValues%s) { | ||
valid, | ||
errors { message }, | ||
sourceYaml, | ||
outputYaml | ||
} | ||
}`, | ||
fieldAddendums, | ||
) | ||
|
||
request := graphql.NewRequest(query) | ||
request.SetToken(cfg.Token) | ||
request.Var("config", configString) | ||
|
||
if values != nil { | ||
request.Var("pipelineValues", 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) | ||
} | ||
|
||
err := c.legacyGraphQLClient.Run(request, &response) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "Unable to validate config") | ||
} | ||
if len(response.BuildConfig.LegacyConfigResponse.Errors) > 0 { | ||
return nil, &response.BuildConfig.LegacyConfigResponse.Errors | ||
} | ||
|
||
return &ConfigResponse{ | ||
Valid: response.BuildConfig.LegacyConfigResponse.Valid, | ||
SourceYaml: response.BuildConfig.LegacyConfigResponse.SourceYaml, | ||
OutputYaml: response.BuildConfig.LegacyConfigResponse.OutputYaml, | ||
}, nil | ||
} | ||
|
||
// KeyVal is a data structure specifically for passing pipeline data to GraphQL which doesn't support free-form maps. | ||
type KeyVal struct { | ||
Key string `json:"key"` | ||
Val interface{} `json:"val"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
package config | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
|
||
"github.com/CircleCI-Public/circleci-cli/settings" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestLegacyFlow(t *testing.T) { | ||
t.Run("tests that the compiler defaults to the graphQL resolver should the original API request fail with 404", func(t *testing.T) { | ||
mux := http.NewServeMux() | ||
|
||
mux.HandleFunc("/compile-config-with-defaults", func(w http.ResponseWriter, r *http.Request) { | ||
w.WriteHeader(http.StatusNotFound) | ||
}) | ||
|
||
mux.HandleFunc("/me/collaborations", func(w http.ResponseWriter, r *http.Request) { | ||
w.Header().Set("Content-Type", "application/json") | ||
fmt.Fprintf(w, `[{"vcs_type":"circleci","slug":"gh/test","id":"2345"}]`) | ||
}) | ||
|
||
mux.HandleFunc("/graphql-unstable", func(w http.ResponseWriter, r *http.Request) { | ||
w.Header().Set("Content-Type", "application/json") | ||
fmt.Fprintf(w, `{"data":{"buildConfig": {"valid":true,"sourceYaml":"%s","outputYaml":"%s","errors":[]}}}`, testYaml, testYaml) | ||
}) | ||
|
||
svr := httptest.NewServer(mux) | ||
defer svr.Close() | ||
|
||
compiler := New(&settings.Config{ | ||
Host: svr.URL, | ||
Endpoint: "/graphql-unstable", | ||
HTTPClient: http.DefaultClient, | ||
Token: "", | ||
}) | ||
resp, err := compiler.ConfigQuery("testdata/config.yml", "1234", Parameters{}, Values{}) | ||
|
||
assert.Equal(t, true, resp.Valid) | ||
assert.NoError(t, err) | ||
}) | ||
|
||
t.Run("tests that the compiler handles errors properly when returned from the graphQL endpoint", func(t *testing.T) { | ||
mux := http.NewServeMux() | ||
|
||
mux.HandleFunc("/compile-config-with-defaults", func(w http.ResponseWriter, r *http.Request) { | ||
w.WriteHeader(http.StatusNotFound) | ||
}) | ||
|
||
mux.HandleFunc("/me/collaborations", func(w http.ResponseWriter, r *http.Request) { | ||
w.Header().Set("Content-Type", "application/json") | ||
fmt.Fprintf(w, `[{"vcs_type":"circleci","slug":"gh/test","id":"2345"}]`) | ||
}) | ||
|
||
mux.HandleFunc("/graphql-unstable", func(w http.ResponseWriter, r *http.Request) { | ||
w.Header().Set("Content-Type", "application/json") | ||
fmt.Fprintf(w, `{"data":{"buildConfig":{"errors":[{"message": "failed to validate"}]}}}`) | ||
}) | ||
|
||
svr := httptest.NewServer(mux) | ||
defer svr.Close() | ||
|
||
compiler := New(&settings.Config{ | ||
Host: svr.URL, | ||
Endpoint: "/graphql-unstable", | ||
HTTPClient: http.DefaultClient, | ||
Token: "", | ||
}) | ||
_, err := compiler.ConfigQuery("testdata/config.yml", "1234", Parameters{}, Values{}) | ||
assert.Error(t, err) | ||
assert.Contains(t, err.Error(), "failed to validate") | ||
}) | ||
|
||
t.Run("tests that the compiler fails out completely when a non-404 is returned from the http endpoint", func(t *testing.T) { | ||
mux := http.NewServeMux() | ||
gqlHitCounter := 0 | ||
|
||
mux.HandleFunc("/compile-config-with-defaults", func(w http.ResponseWriter, r *http.Request) { | ||
w.WriteHeader(http.StatusInternalServerError) | ||
|
||
}) | ||
|
||
mux.HandleFunc("/me/collaborations", func(w http.ResponseWriter, r *http.Request) { | ||
w.Header().Set("Content-Type", "application/json") | ||
fmt.Fprintf(w, `[{"vcs_type":"circleci","slug":"gh/test","id":"2345"}]`) | ||
}) | ||
|
||
mux.HandleFunc("/graphql-unstable", func(w http.ResponseWriter, r *http.Request) { | ||
w.Header().Set("Content-Type", "application/json") | ||
fmt.Fprintf(w, `{"data":{"buildConfig":{"errors":[{"message": "failed to validate"}]}}}`) | ||
gqlHitCounter++ | ||
}) | ||
|
||
svr := httptest.NewServer(mux) | ||
defer svr.Close() | ||
|
||
compiler := New(&settings.Config{ | ||
Host: svr.URL, | ||
Endpoint: "/graphql-unstable", | ||
HTTPClient: http.DefaultClient, | ||
Token: "", | ||
}) | ||
_, err := compiler.ConfigQuery("testdata/config.yml", "1234", Parameters{}, Values{}) | ||
assert.Error(t, err) | ||
assert.Contains(t, err.Error(), "config compilation request returned an error:") | ||
assert.Equal(t, 0, gqlHitCounter) | ||
}) | ||
} |