Skip to content

Commit

Permalink
Merge pull request #15 from circleci/validate
Browse files Browse the repository at this point in the history
Skeleton for Config Validate
  • Loading branch information
marcomorain authored Jun 18, 2018
2 parents 05cb0e6 + 1d9506a commit 9c2f3b5
Show file tree
Hide file tree
Showing 9 changed files with 199 additions and 68 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/vendor linguist-generated=true
2 changes: 0 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
VERSION=0.1
DATE = $(shell date "+%FT%T%z")
SHA=$(shell git rev-parse --short HEAD)

GOFILES = $(shell find . -name '*.go' -not -path './vendor/*')

Expand Down
41 changes: 20 additions & 21 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,37 +7,36 @@ import (
"github.com/machinebox/graphql"
)

// Client wraps a graphql.Client and other fields for making API calls.
type Client struct {
endpoint string
token string
client *graphql.Client
logger *logger.Logger
}

// NewClient returns a reference to a Client.
// We also call graphql.NewClient to initialize a new GraphQL Client.
// Then we pass the Logger originally constructed as cmd.Logger.
func NewClient(endpoint string, token string, logger *logger.Logger) *Client {
return &Client{
endpoint,
token,
graphql.NewClient(endpoint),
logger,
func NewClient(endpoint string, logger *logger.Logger) *graphql.Client {

client := graphql.NewClient(endpoint)

client.Log = func(s string) {
logger.Debug(s)
}

return client

}

// newAuthorizedRequest returns a new GraphQL request with the
// authorization headers set for CircleCI auth.
func newAuthorizedRequest(token, query string) *graphql.Request {
req := graphql.NewRequest(query)
req.Header.Set("Authorization", token)
return req
}

// Run will construct a request using graphql.NewRequest.
// Then it will execute the given query using graphql.Client.Run.
// This function will return the unmarshalled response as JSON.
func (c *Client) Run(query string) (map[string]interface{}, error) {
req := graphql.NewRequest(query)
req.Header.Set("Authorization", c.token)

func Run(client *graphql.Client, token, query string) (map[string]interface{}, error) {
req := newAuthorizedRequest(token, query)
ctx := context.Background()
var resp map[string]interface{}

c.logger.Debug("Querying %s with:\n\n%s\n\n", c.endpoint, query)
err := c.client.Run(ctx, req, &resp)
err := client.Run(ctx, req, &resp)
return resp, err
}
95 changes: 95 additions & 0 deletions cmd/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package cmd

import (
"bytes"
"context"
"fmt"
"io/ioutil"

"github.com/pkg/errors"

"github.com/machinebox/graphql"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

// Path to the config.yml file to operate on.
var configPath string

var configCmd = &cobra.Command{
Use: "config",
Short: "Operate on build config files",
}

var validateCommand = &cobra.Command{
Use: "validate",
Aliases: []string{"check"},
Short: "Check that the config file is well formed.",
RunE: validateConfig,
}

func init() {
validateCommand.Flags().StringVarP(&configPath, "path", "p", ".circleci/config.yml", "path to build config")
configCmd.AddCommand(validateCommand)
}

func validateConfig(cmd *cobra.Command, args []string) error {

ctx := context.Background()

// Define 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 validateResult struct {
BuildConfig struct {
Valid bool
SourceYaml string
Errors []struct {
Message string
}
}
}

request := graphql.NewRequest(`
query ValidateConfig ($config: String!) {
buildConfig(configYaml: $config) {
valid,
errors { message },
sourceYaml
}
}`)

config, err := ioutil.ReadFile(configPath)

if err != nil {
return errors.Wrapf(err, "Could not load config file at %s", configPath)
}

request.Var("config", string(config))

client := graphql.NewClient(viper.GetString("endpoint"))

var result validateResult

err = client.Run(ctx, request, &result)

if err != nil {
return errors.Wrap(err, "GraphQL query failed")
}

if !result.BuildConfig.Valid {

var buffer bytes.Buffer

for i := range result.BuildConfig.Errors {
buffer.WriteString(result.BuildConfig.Errors[i].Message)
buffer.WriteString("\n")
}

return fmt.Errorf("config file is invalid:\n%s", buffer.String())
}

fmt.Println("Config is valid")
return nil

}
12 changes: 6 additions & 6 deletions cmd/diagnostic.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import (
var diagnosticCmd = &cobra.Command{
Use: "diagnostic",
Short: "Check the status of your CircleCI CLI.",
Run: diagnostic,
RunE: diagnostic,
}

func diagnostic(cmd *cobra.Command, args []string) {
func diagnostic(cmd *cobra.Command, args []string) error {
endpoint := viper.GetString("endpoint")
token := viper.GetString("token")

Expand All @@ -23,10 +23,10 @@ func diagnostic(cmd *cobra.Command, args []string) {
Logger.Infof("GraphQL API endpoint: %s\n", endpoint)

if token == "token" || token == "" {
Logger.FatalOnError("Please set a token!", errors.New(""))
} else {
Logger.Infoln("OK, got a token.")
return errors.New("please set a token")
}

Logger.Infoln("OK, got a token.")
Logger.Infof("Verbose mode: %v\n", viper.GetBool("verbose"))

return nil
}
4 changes: 2 additions & 2 deletions cmd/diagnostic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,9 @@ token:
It("print error", func() {
session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
Expect(err).ShouldNot(HaveOccurred())
Eventually(session.Err).Should(gbytes.Say("Please set a token!"))
Eventually(session.Err).Should(gbytes.Say("Error: please set a token"))
Eventually(session.Out).Should(gbytes.Say("GraphQL API endpoint: https://example.com/graphql"))
Eventually(session).Should(gexec.Exit(1))
Eventually(session).Should(gexec.Exit(255))
})
})
})
Expand Down
15 changes: 9 additions & 6 deletions cmd/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,31 @@ import (
"os"

"github.com/circleci/circleci-cli/client"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

var queryCmd = &cobra.Command{
Use: "query",
Short: "Query the CircleCI GraphQL API.",
Run: query,
RunE: query,
}

func query(cmd *cobra.Command, args []string) {
client := client.NewClient(viper.GetString("endpoint"), viper.GetString("token"), Logger)
func query(cmd *cobra.Command, args []string) error {
c := client.NewClient(viper.GetString("endpoint"), Logger)

query, err := ioutil.ReadAll(os.Stdin)
if err != nil {
Logger.FatalOnError("Unable to read query", err)
return errors.Wrap(err, "Unable to read query from stdin")
}

resp, err := client.Run(string(query))
resp, err := client.Run(c, viper.GetString("token"), string(query))
if err != nil {
Logger.FatalOnError("Error occurred when running query", err)
return errors.Wrap(err, "Error occurred when running query")
}

Logger.Prettyify(resp)

return nil
}
58 changes: 27 additions & 31 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ package cmd
import (
"os"
"path"
"runtime"

"github.com/circleci/circleci-cli/logger"
"github.com/circleci/circleci-cli/settings"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
Expand Down Expand Up @@ -34,57 +35,52 @@ func addCommands() {
rootCmd.AddCommand(diagnosticCmd)
rootCmd.AddCommand(queryCmd)
rootCmd.AddCommand(configureCommand)
rootCmd.AddCommand(configCmd)

// Cobra has a peculiar default behaviour:
// https://github.com/spf13/cobra/issues/340
// If you expose a command with `RunE`, and return an error from your
// command, then Cobra will print the error message, followed by the usage
// infomation for the command. This makes it really difficult to see what's
// gone wrong. It usually prints a one line error message followed by 15
// lines of usage information.
// This flag disables that behaviour, so that if a comment fails, it prints
// just the error message.
rootCmd.SilenceUsage = true
}

func userHomeDir() string {
if runtime.GOOS == "windows" {
home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
if home == "" {
home = os.Getenv("USERPROFILE")
}
return home
func bindCobraFlagToViper(flag string) {
if err := viper.BindPFlag(flag, rootCmd.PersistentFlags().Lookup(flag)); err != nil {
panic(errors.Wrapf(err, "internal error binding cobra flag '%s' to viper", flag))
}
return os.Getenv("HOME")
}

func init() {

configDir := path.Join(userHomeDir(), ".circleci")
configDir := path.Join(settings.UserHomeDir(), ".circleci")

cobra.OnInitialize(setup)

viper.SetConfigName("cli")
viper.AddConfigPath(configDir)
viper.SetEnvPrefix("circleci_cli")
viper.AutomaticEnv()

err := viper.ReadInConfig()

// If reading the config file failed, then we want to create it.
// TODO - handle invalid YAML config files.
if err != nil {
if _, err = os.Stat(configDir); os.IsNotExist(err) {
if err = os.MkdirAll(configDir, 0700); err != nil {
panic(err)
}
}
if _, err = os.Create(path.Join(configDir, "cli.yml")); err != nil {
panic(err)
}
if err := settings.EnsureSettingsFileExists(configDir, "cli.yml"); err != nil {
panic(err)
}

if err := viper.ReadInConfig(); err != nil {
panic(err)
}

rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Enable verbose logging.")
rootCmd.PersistentFlags().StringP("endpoint", "e", "https://circleci.com/graphql", "the endpoint of your CircleCI GraphQL API")
rootCmd.PersistentFlags().StringP("token", "t", "", "your token for using CircleCI")

Logger.FatalOnError("Error binding endpoint flag", viper.BindPFlag("endpoint", rootCmd.PersistentFlags().Lookup("endpoint")))
Logger.FatalOnError("Error binding token flag", viper.BindPFlag("token", rootCmd.PersistentFlags().Lookup("token")))

err = viper.BindPFlag("verbose", rootCmd.PersistentFlags().Lookup("verbose"))

if err != nil {
panic(err)
for _, flag := range []string{"endpoint", "token", "verbose"} {
bindCobraFlagToViper(flag)
}

addCommands()
}

Expand Down
39 changes: 39 additions & 0 deletions settings/settings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package settings

import (
"os"
"path"
"runtime"
)

// UserHomeDir returns the path to the current user's HOME directory.
func UserHomeDir() string {
if runtime.GOOS == "windows" {
home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
if home == "" {
home = os.Getenv("USERPROFILE")
}
return home
}
return os.Getenv("HOME")
}

// EnsureSettingsFileExists does just that.
func EnsureSettingsFileExists(filepath, filename string) error {
// TODO - handle invalid YAML config files.
_, err := os.Stat(filepath)

if !os.IsNotExist(err) {
return nil
}

if err = os.MkdirAll(filepath, 0700); err != nil {
return err
}

if _, err = os.Create(path.Join(filepath, filename)); err != nil {
return err
}

return nil
}

0 comments on commit 9c2f3b5

Please sign in to comment.