Skip to content

Commit

Permalink
Update project structure and add provider command
Browse files Browse the repository at this point in the history
  • Loading branch information
janos committed Dec 30, 2019
1 parent 94ffde6 commit a6c1e3a
Show file tree
Hide file tree
Showing 17 changed files with 578 additions and 339 deletions.
25 changes: 13 additions & 12 deletions newreleases/cmd/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
"newreleases.io/newreleases"
)

func init() {
func (c *command) initAuthCmd() (err error) {
authCmd := &cobra.Command{
Use: "auth",
Short: "Information about API authentication",
Expand All @@ -25,10 +25,10 @@ func init() {
Use: "list",
Short: "Get all API authentication keys",
RunE: func(cmd *cobra.Command, args []string) (err error) {
ctx, cancel := newClientContext()
ctx, cancel := newClientContext(c.config)
defer cancel()

keys, err := cmdAuthService.List(ctx)
keys, err := c.authService.List(ctx)
if err != nil {
return err
}
Expand All @@ -42,27 +42,28 @@ func init() {

return nil
},
PreRunE: setCmdAuthService,
PreRunE: c.setAuthService,
}

addClientFlags(listCmd)
if err := addClientFlags(listCmd, c.config); err != nil {
return err
}

authCmd.AddCommand(listCmd)

rootCmd.AddCommand(authCmd)
c.root.AddCommand(authCmd)
return nil
}

var cmdAuthService authService

func setCmdAuthService(cmd *cobra.Command, args []string) (err error) {
if cmdAuthService != nil {
func (c *command) setAuthService(cmd *cobra.Command, args []string) (err error) {
if c.authService != nil {
return nil
}
client, err := newClient()
client, err := c.getClient(cmd)
if err != nil {
return err
}
cmdAuthService = client.Auth
c.authService = client.Auth
return nil
}

Expand Down
15 changes: 9 additions & 6 deletions newreleases/cmd/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,19 @@ func TestAuthCmd(t *testing.T) {
} {
t.Run(tc.name, func(t *testing.T) {
var outputBuf bytes.Buffer
ExecuteT(t,
WithArgs("auth", "list"),
WithOutput(&outputBuf),
WithAuthService(tc.authService),
WithError(tc.wantError),

c := newCommand(t,
cmd.WithArgs("auth", "list"),
cmd.WithOutput(&outputBuf),
cmd.WithAuthService(tc.authService),
)
if err := c.Execute(); err != tc.wantError {
t.Fatalf("got error %v, want %v", err, tc.wantError)
}

gotOutput := outputBuf.String()
if gotOutput != tc.wantOutput {
t.Errorf("got error output %q, want %q", gotOutput, tc.wantOutput)
t.Errorf("got output %q, want %q", gotOutput, tc.wantOutput)
}
})
}
Expand Down
71 changes: 71 additions & 0 deletions newreleases/cmd/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (c) 2019, NewReleases CLI AUTHORS.
// All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package cmd

import (
"context"
"errors"
"net/url"
"time"

"github.com/spf13/cobra"
"github.com/spf13/viper"
"newreleases.io/newreleases"
)

func (c *command) getClient(cmd *cobra.Command) (client *newreleases.Client, err error) {
if c.client != nil {
return c.client, nil
}

authKey := c.config.GetString(optionNameAuthKey)
if authKey == "" {
return nil, errors.New("auth key not configured")
}
o, err := newClientOptions(cmd)
if err != nil {
return nil, err
}
c.client = newreleases.NewClient(authKey, o)
return c.client, nil
}

func newClientOptions(cmd *cobra.Command) (o *newreleases.ClientOptions, err error) {
v, err := cmd.Flags().GetString(optionNameAPIEndpoint)
if err != nil {
return nil, err
}
var baseURL *url.URL
if v != "" {
baseURL, err = url.Parse(v)
if err != nil {
return nil, err
}
}
return &newreleases.ClientOptions{BaseURL: baseURL}, nil
}

func newClientContext(config *viper.Viper) (ctx context.Context, cancel context.CancelFunc) {
return context.WithTimeout(context.Background(), config.GetDuration(optionNameTimeout))
}

func addClientFlags(cmd *cobra.Command, config *viper.Viper) (err error) {
flags := cmd.Flags()
flags.String(optionNameAuthKey, "", "API auth key")
flags.Duration(optionNameTimeout, 30*time.Second, "API request timeout")
flags.String(optionNameAPIEndpoint, "", "API Endpoint")
if err := flags.MarkHidden(optionNameAPIEndpoint); err != nil {
return err
}

if err := config.BindPFlag(optionNameAuthKey, flags.Lookup(optionNameAuthKey)); err != nil {
return err
}
if err := config.BindPFlag(optionNameTimeout, flags.Lookup(optionNameTimeout)); err != nil {
return err
}
return nil
}
168 changes: 99 additions & 69 deletions newreleases/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,13 @@
package cmd

import (
"context"
"errors"
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"time"

"github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/crypto/ssh/terminal"
"newreleases.io/newreleases"
)

Expand All @@ -26,93 +22,127 @@ const (
optionNameAPIEndpoint = "api-endpoint"
)

var cmdPasswordReader passwordReader = new(stdInPasswordReader)

type passwordReader interface {
ReadPassword() (password string, err error)
type command struct {
root *cobra.Command
config *viper.Viper
client *newreleases.Client
cfgFile string
homeDir string
passwordReader passwordReader
authService authService
providerService providerService
authKeysGetter authKeysGetter
}

type stdInPasswordReader struct{}
type option func(*command)

func (stdInPasswordReader) ReadPassword() (password string, err error) {
v, err := terminal.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
return "", err
func newCommand(opts ...option) (c *command, err error) {
c = &command{
root: &cobra.Command{
Use: "newreleases",
Short: "Release tracker for software engineers",
SilenceErrors: true,
SilenceUsage: true,
},
}
return string(v), err
}

func terminalPrompt(cmd *cobra.Command, reader interface{ ReadString(byte) (string, error) }, title string) (value string, err error) {
cmd.Print(title + ": ")
value, err = reader.ReadString('\n')
if err != nil {
return "", err
for _, o := range opts {
o(c)
}
if c.passwordReader == nil {
c.passwordReader = new(stdInPasswordReader)
}
return strings.TrimSpace(value), nil
}

func terminalPromptPassword(cmd *cobra.Command, title string) (password string, err error) {
cmd.Print(title + ": ")
password, err = cmdPasswordReader.ReadPassword()
cmd.Println()
if err != nil {
return "", err
c.initGlobalFlags()
if err := c.initConfig(); err != nil {
return nil, err
}
return password, nil
}

func writeConfig(cmd *cobra.Command, authKey string) (err error) {
viper.Set(optionNameAuthKey, strings.TrimSpace(authKey))
err = viper.WriteConfig()
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
err = viper.SafeWriteConfigAs(cfgFile)
if err := c.initAuthCmd(); err != nil {
return nil, err
}
return err
c.initConfigureCmd()
if err := c.initGetAuthKeyCmd(); err != nil {
return nil, err
}
if err := c.initProviderCmd(); err != nil {
return nil, err
}
c.initVersionCmd()
return c, nil
}

func newClient() (client *newreleases.Client, err error) {
authKey := viper.GetString(optionNameAuthKey)
if authKey == "" {
return nil, errors.New("auth key not configured")
func (c *command) Execute() (err error) {
return c.root.Execute()
}

// Execute parses command line arguments and runs appropriate functions.
func Execute() (err error) {
c, err := newCommand()
if err != nil {
return err
}
return newreleases.NewClient(authKey, newClientOptions()), nil
return c.Execute()
}

func addClientFlags(cmd *cobra.Command) {
flags := cmd.Flags()
flags.String(optionNameAuthKey, "", "API auth key")
flags.Duration(optionNameTimeout, 30*time.Second, "API request timeout")
flags.String(optionNameAPIEndpoint, "", "API Endpoint")
must(flags.MarkHidden(optionNameAPIEndpoint))

cobra.OnInitialize(func() {
must(viper.BindPFlag(optionNameAuthKey, flags.Lookup(optionNameAuthKey)))
must(viper.BindPFlag(optionNameTimeout, flags.Lookup(optionNameTimeout)))
})
func (c *command) initGlobalFlags() {
globalFlags := c.root.PersistentFlags()
globalFlags.StringVar(&c.cfgFile, "config", "", "config file (default is $HOME/.newreleases.yaml)")
}

func newClientOptions() (o *newreleases.ClientOptions) {
return &newreleases.ClientOptions{
BaseURL: mustURLParse(viper.GetString(optionNameAPIEndpoint)),
func (c *command) initConfig() (err error) {
config := viper.New()
configName := ".newreleases"
if c.cfgFile != "" {
// Use config file from the flag.
config.SetConfigFile(c.cfgFile)
} else {
// Find home directory.
if err := c.setHomeDir(); err != nil {
return err
}
// Search config in home directory with name ".newreleases" (without extension).
config.AddConfigPath(c.homeDir)
config.SetConfigName(configName)
}
}

func newClientContext() (ctx context.Context, cancel context.CancelFunc) {
return context.WithTimeout(context.Background(), viper.GetDuration(optionNameTimeout))
// Environment
config.SetEnvPrefix("newreleases")
config.AutomaticEnv() // read in environment variables that match
config.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))

if c.homeDir != "" && c.cfgFile == "" {
c.cfgFile = filepath.Join(c.homeDir, configName+".yaml")
}

// If a config file is found, read it in.
if err := config.ReadInConfig(); err != nil {
var e viper.ConfigFileNotFoundError
if !errors.As(err, &e) {
return err
}
}
c.config = config
return nil
}

func must(err error) {
func (c *command) setHomeDir() (err error) {
if c.homeDir != "" {
return
}
dir, err := homedir.Dir()
if err != nil {
fmt.Fprintln(os.Stderr, "Error:", err)
os.Exit(1)
return err
}
c.homeDir = dir
return nil
}

func mustURLParse(s string) (u *url.URL) {
if s == "" {
return nil
func (c *command) writeConfig(cmd *cobra.Command, authKey string) (err error) {
c.config.Set(optionNameAuthKey, strings.TrimSpace(authKey))
err = c.config.WriteConfig()
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
err = c.config.SafeWriteConfigAs(c.cfgFile)
}
u, err := url.Parse(s)
must(err)
return u
return err
}
Loading

0 comments on commit a6c1e3a

Please sign in to comment.