Skip to content

Commit

Permalink
Merge pull request #6 from circleci/logger
Browse files Browse the repository at this point in the history
 CIRCLE-11151: CLI can accept a GQL query from stdin and call api-service
  • Loading branch information
Zachary Scott authored Jun 6, 2018
2 parents 61f09cd + 3fdf2b3 commit 0e27100
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 96 deletions.
10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,13 @@ cover:
.PHONY: lint
lint:
gometalinter --deadline 60s --vendor ./...

.PHONY: doc
doc:
godoc -http=:6060

.PHONY: dev
dev:
go get golang.org/x/tools/cmd/godoc
go get -u github.com/alecthomas/gometalinter
gometalinter --install
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,30 @@ To make sure dependencies are installed:
$ dep ensure
```

## Linting

We use [`gometalinter`](github.com/alecthomas/gometalinter) for linting.

You can install it via `$ make dev` or manually with:

```
$ go get -u github.com/alecthomas/gometalinter
$ gometalinter --install
```

Then you can run it with `$ make lint`.

## Known Issues

* ...

## Doc

You can view `godoc` of cli in your browser.
You can view `godoc` of the cli in your browser.

After installing it either via `go get golang.org/x/tools/cmd/godoc` or running `make dev`.

1. Run `godoc -http=:6060`
1. Run `make doc` or `godoc -http=:6060`
2. Access http://localhost:6060/pkg/github.com/circleci/circleci-cli/

## Editor support
Expand Down
43 changes: 43 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package client

import (
"context"

"github.com/circleci/circleci-cli/logger"
"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,
}
}

// 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)

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)
return resp, err
}
19 changes: 7 additions & 12 deletions cmd/diagnostic.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package cmd

import (
"fmt"

"github.com/spf13/cobra"
"github.com/spf13/viper"
)
Expand All @@ -14,21 +12,18 @@ var diagnosticCmd = &cobra.Command{
}

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

fmt.Printf("\n---\nCircleCI CLI Diagnostics\n---\n\n")
fmt.Printf("Config found: `%v`\n", viper.ConfigFileUsed())
Logger.Infoln("\n---\nCircleCI CLI Diagnostics\n---\n")
Logger.Infof("Config found: `%v`\n", viper.ConfigFileUsed())

if host == "host" || host == "" {
fmt.Println("Please set a host!")
} else {
fmt.Printf("Host is: %s\n", host)
}
Logger.Infof("GraphQL API endpoint: %s\n", endpoint)

if token == "token" || token == "" {
fmt.Println("Please set a token!")
var err error
Logger.FatalOnError("Please set a token!", err)
} else {
fmt.Println("OK, got a token.")
Logger.Infoln("OK, got a token.")
}
}
58 changes: 11 additions & 47 deletions cmd/query.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package cmd

import (
"context"
"encoding/json"
"fmt"
"log"
"io/ioutil"
"os"

"github.com/machinebox/graphql"
"github.com/circleci/circleci-cli/client"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
Expand All @@ -18,50 +17,15 @@ var queryCmd = &cobra.Command{
}

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

query := `
query IntrospectionQuery {
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
...FullType
}
directives {
name
description
}
}
}
fragment FullType on __Type {
kind
name
description
fields(includeDeprecated: true) {
name
}
}`

req := graphql.NewRequest(query)
req.Header.Set("Authorization", token)

ctx := context.Background()
var resp map[string]interface{}

fmt.Print("Querying ", host, " with:\n\n", query, "\n\n")
if err := client.Run(ctx, req, &resp); err != nil {
log.Fatal(err)
}
query, err := ioutil.ReadAll(os.Stdin)
Logger.FatalOnError("Something happened", err)

resp, err := client.Run(string(query))
Logger.FatalOnError("Something happend", err)
b, err := json.MarshalIndent(resp, "", " ")
if err != nil {
log.Fatalln("Could not parse graphql response", err.Error())
}
fmt.Print("Result: \n\n")
fmt.Println(string(b))
Logger.FatalOnError("Could not parse graphql response", err)

Logger.Infoln(string(b))
}
62 changes: 27 additions & 35 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ package cmd

import (
"fmt"
"log"
"os"

"github.com/circleci/circleci-cli/logger"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

// Execute adds all child commands to RootCmd and
// Execute adds all child commands to rootCmd and
// sets flags appropriately. This function is called
// by main.main(). It only needs to happen once to
// the RootCmd.
Expand All @@ -27,32 +27,32 @@ var rootCmd = &cobra.Command{
}

var (
verbose bool
cfgFile string
cfgName = "cli"
cfgPathDefault = fmt.Sprintf("%s/.circleci/%s.yml", os.Getenv("HOME"), cfgName)
)

// Logger is exposed here so we can access it from subcommands.
// This allows us to print to the log at anytime from within the `cmd` package.
var Logger *logger.Logger

func addCommands() {
rootCmd.AddCommand(diagnosticCmd)
rootCmd.AddCommand(queryCmd)
}

func fatalOnError(msg string, err error) {
if err == nil {
return
}
log.Fatalln(msg, err.Error())
}

func init() {
cobra.OnInitialize(initConfig)

rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging.")
Logger = logger.NewLogger(verbose)
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file (default is $HOME/.circleci/cli.yml)")
rootCmd.PersistentFlags().StringP("host", "H", "https://circleci.com", "the host of your CircleCI install")
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")

fatalOnError("Error binding host flag", viper.BindPFlag("host", rootCmd.PersistentFlags().Lookup("host")))
fatalOnError("Error binding token flag", viper.BindPFlag("token", rootCmd.PersistentFlags().Lookup("token")))
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")))
}

// TODO: move config stuff to it's own package
Expand All @@ -61,10 +61,10 @@ func initConfig() {
return
}

fatalOnError("Error creating a new config file", createConfig())
Logger.FatalOnError("Error creating a new config file", createConfig())

cfgFile = cfgPathDefault
fatalOnError(
Logger.FatalOnError(
"Failed to re-read config after creating a new file",
readConfig(), // reload config after creating it
)
Expand All @@ -81,7 +81,7 @@ func readConfig() (err error) {
}

// read in environment variables that match
// set a prefix for config, i.e. CIRCLECI_CLI_HOST
// set a prefix for config, i.e. CIRCLECI_CLI_ENDPOINT
viper.SetEnvPrefix("circleci_cli")
viper.AutomaticEnv()

Expand All @@ -93,18 +93,18 @@ func readConfig() (err error) {
func createConfig() (err error) {
// Don't support creating config at --config flag, only default
if cfgFile != "" {
fmt.Printf("Setting up default config at: %v\n", cfgPathDefault)
Logger.Debug("Setting up default config at: %v\n", cfgPathDefault)
}

path := fmt.Sprintf("%s/.circleci", os.Getenv("HOME"))

if _, err = os.Stat(path); os.IsNotExist(err) {
fatalOnError(
Logger.FatalOnError(
fmt.Sprintf("Error creating directory: '%s'", path),
os.Mkdir(path, 0644),
)
} else {
fatalOnError(fmt.Sprintf("Error accessing '%s'", path), err)
Logger.FatalOnError(fmt.Sprintf("Error accessing '%s'", path), err)
}

// Create default config file
Expand All @@ -115,37 +115,29 @@ func createConfig() (err error) {
// open file with read & write
file, err := os.OpenFile(cfgPathDefault, os.O_RDWR, 0600)
if err != nil {
fmt.Println(err.Error())
os.Exit(-1)
Logger.FatalOnError("", err)
}
defer fatalOnError("Error closing config file", file.Close())
defer Logger.FatalOnError("Error closing config file", file.Close())

// read flag values
host := viper.GetString("host")
endpoint := viper.GetString("endpoint")
token := viper.GetString("token")

if host == "host" || host == "" {
fmt.Print("Please enter the HTTP(S) host of your CircleCI installation:")
fmt.Scanln(&host)
fmt.Println("OK.")
}

if token == "token" || token == "" {
fmt.Print("Please enter your CircleCI API token: ")
Logger.Info("Please enter your CircleCI API token: ")
fmt.Scanln(&token)
fmt.Println("OK.")
Logger.Infoln("OK.")
}

// format input
configValues := fmt.Sprintf("host: %v\ntoken: %v\n", host, token)
configValues := fmt.Sprintf("endpoint: %v\ntoken: %v\n", endpoint, token)

// write new config values to file
if _, err = file.WriteString(configValues); err != nil {
fmt.Println(err.Error())
os.Exit(-1)
Logger.FatalOnError("", err)
}

fmt.Printf("Your configuration has been created in `%v`.\n", cfgPathDefault)
fmt.Println("It can edited manually for advanced settings.")
Logger.Info("Your configuration has been created in `%v`.\n", cfgPathDefault)
Logger.Infoln("It can edited manually for advanced settings.")
return err
}
Loading

0 comments on commit 0e27100

Please sign in to comment.