Skip to content

Commit

Permalink
Merge pull request #968 from CircleCI-Public/task/DEVEX-1028/cli-abst…
Browse files Browse the repository at this point in the history
…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
JulesFaucherre authored Aug 8, 2023
2 parents 08f5061 + d3625e8 commit 7c6c1ea
Show file tree
Hide file tree
Showing 6 changed files with 384 additions and 170 deletions.
127 changes: 0 additions & 127 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
131 changes: 131 additions & 0 deletions api/orb/client.go
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
}
53 changes: 53 additions & 0 deletions api/orb/v1_client.go
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
}
52 changes: 52 additions & 0 deletions api/orb/v2_client.go
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
}
Loading

0 comments on commit 7c6c1ea

Please sign in to comment.