-
Notifications
You must be signed in to change notification settings - Fork 237
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 #968 from CircleCI-Public/task/DEVEX-1028/cli-abst…
…ract-api-calls-to-allow-better-testing-and-backward-compatibility-ORB-ABSTRACTION refactor: Moved the orb validation in a orb API module
- Loading branch information
Showing
6 changed files
with
384 additions
and
170 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
package orb | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
"os" | ||
|
||
"github.com/CircleCI-Public/circleci-cli/api" | ||
"github.com/CircleCI-Public/circleci-cli/api/graphql" | ||
"github.com/CircleCI-Public/circleci-cli/settings" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
type clientVersion string | ||
|
||
// 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 NewClient(config *settings.Config) (Client, error) { | ||
gql := graphql.NewClient(config.HTTPClient, config.Host, config.Endpoint, config.Token, config.Debug) | ||
|
||
clientVersion, err := detectClientVersion(gql) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
switch clientVersion { | ||
case v1_string: | ||
return &v1Client{gql}, nil | ||
case v2_string: | ||
return &v2Client{gql}, nil | ||
default: | ||
return nil, fmt.Errorf("Unable to recognise your server orb API") | ||
} | ||
} | ||
|
||
// detectClientVersion returns the highest available version of the orb API | ||
// | ||
// To do that it checks that whether the GraphQL query has the parameter "ownerId" or not. | ||
// If it does not have the parameter, the function returns `v1_string` else it returns `v2_string` | ||
func detectClientVersion(gql *graphql.Client) (clientVersion, error) { | ||
handlesOwnerId, err := orbQueryHandleOwnerId(gql) | ||
if err != nil { | ||
return "", err | ||
} | ||
if !handlesOwnerId { | ||
return v1_string, nil | ||
} | ||
return v2_string, 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(gql *graphql.Client) (bool, error) { | ||
query := `query IntrospectionQuery { | ||
_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 | ||
} | ||
|
||
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 | ||
} |
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,53 @@ | ||
package orb | ||
|
||
import ( | ||
"github.com/CircleCI-Public/circleci-cli/api" | ||
"github.com/CircleCI-Public/circleci-cli/api/graphql" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
// This client makes request to servers that **DON'T** have the field `ownerId` in the GraphQL query method: `orbConfig` | ||
|
||
const v1_string clientVersion = "v1" | ||
|
||
type v1Client struct { | ||
gql *graphql.Client | ||
} | ||
|
||
func (client *v1Client) 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(client.gql.Token) | ||
|
||
err = client.gql.Run(request, &response) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "Validating config") | ||
} | ||
|
||
if len(response.OrbConfig.ConfigResponse.Errors) > 0 { | ||
return nil, response.OrbConfig.ConfigResponse.Errors | ||
} | ||
|
||
return &response.OrbConfig.ConfigResponse, nil | ||
} |
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,52 @@ | ||
package orb | ||
|
||
import ( | ||
"github.com/CircleCI-Public/circleci-cli/api" | ||
"github.com/CircleCI-Public/circleci-cli/api/graphql" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
// This client makes request to servers that **DO** have the field `ownerId` in the GraphQL query method: `orbConfig` | ||
|
||
const v2_string clientVersion = "v2" | ||
|
||
type v2Client struct { | ||
gql *graphql.Client | ||
} | ||
|
||
func (client *v2Client) 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(client.gql.Token) | ||
|
||
err = client.gql.Run(request, &response) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "Validating config") | ||
} | ||
|
||
if len(response.OrbConfig.ConfigResponse.Errors) > 0 { | ||
return nil, response.OrbConfig.ConfigResponse.Errors | ||
} | ||
|
||
return &response.OrbConfig.ConfigResponse, nil | ||
} |
Oops, something went wrong.