Skip to content

Commit

Permalink
Merge pull request runatlantis#170 from hootsuite/flags
Browse files Browse the repository at this point in the history
Test cmd/server
  • Loading branch information
lkysow authored Oct 29, 2017
2 parents 96be4e9 + 3d361eb commit 6e85321
Show file tree
Hide file tree
Showing 9 changed files with 485 additions and 118 deletions.
2 changes: 1 addition & 1 deletion bootstrap/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ Follow these instructions to create a token (we don't store any tokens):
// wait for sigterm or siginit signal
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
<- signalChan
<-signalChan
colorstring.Println("\n[red]shutdown signal received, exiting....")
colorstring.Println("\n[green]Thank you for using atlantis :) \n[white]For more information about how to use atlantis in production go to: https://github.com/hootsuite/atlantis")
return nil
Expand Down
2 changes: 1 addition & 1 deletion bootstrap/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func downloadFile(url string, path string) error {
}
defer response.Body.Close()

_, err = io.Copy(output, response.Body);
_, err = io.Copy(output, response.Body)
return err
}

Expand Down
25 changes: 16 additions & 9 deletions cmd/bootstrap.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
package cmd

import (
"fmt"
"os"

"github.com/hootsuite/atlantis/bootstrap"
"github.com/spf13/cobra"
)

var bootstrapCmd = &cobra.Command{
Use: "bootstrap",
Short: "Start a guided tour of Atlantis",
RunE: withErrPrint(func(cmd *cobra.Command, args []string) error {
return bootstrap.Start()
}),
}
// BootstrapCmd starts the bootstrap process for testing out Atlantis.
type BootstrapCmd struct{}

func init() {
RootCmd.AddCommand(bootstrapCmd)
// Init returns the runnable cobra command.
func (b *BootstrapCmd) Init() *cobra.Command {
return &cobra.Command{
Use: "bootstrap",
Short: "Start a guided tour of Atlantis",
RunE: func(cmd *cobra.Command, args []string) error {
err := bootstrap.Start()
fmt.Fprintf(os.Stderr, "\033[31mError: %s\033[39m\n\n", err.Error())
return err
},
}
}
4 changes: 0 additions & 4 deletions cmd/cmd_test.go

This file was deleted.

223 changes: 140 additions & 83 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,77 +7,78 @@ import (
"strings"

"github.com/hootsuite/atlantis/server"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

// To add a new flag you must
// 1. Add a const with the flag name (in alphabetic order)
// 2. Add a new field to server.ServerConfig and set the mapstructure tag equal to the flag name
// 3. Add your flag's description etc. to the stringFlags, intFlags, or boolFlags slices
// To add a new flag you must:
// 1. Add a const with the flag name (in alphabetic order).
// 2. Add a new field to server.ServerConfig and set the mapstructure tag equal to the flag name.
// 3. Add your flag's description etc. to the stringFlags, intFlags, or boolFlags slices.
const (
atlantisURLFlag = "atlantis-url"
configFlag = "config"
dataDirFlag = "data-dir"
ghHostnameFlag = "gh-hostname"
ghTokenFlag = "gh-token"
ghUserFlag = "gh-user"
ghWebHookSecret = "gh-webhook-secret"
logLevelFlag = "log-level"
portFlag = "port"
requireApprovalFlag = "require-approval"
AtlantisURLFlag = "atlantis-url"
ConfigFlag = "config"
DataDirFlag = "data-dir"
GHHostnameFlag = "gh-hostname"
GHTokenFlag = "gh-token"
GHUserFlag = "gh-user"
GHWebHookSecret = "gh-webhook-secret"
LogLevelFlag = "log-level"
PortFlag = "port"
RequireApprovalFlag = "require-approval"
)

var stringFlags = []stringFlag{
{
name: atlantisURLFlag,
description: "URL that Atlantis can be reached at. Defaults to http://$(hostname):$port where $port is from --" + portFlag + ".",
name: AtlantisURLFlag,
description: "URL that Atlantis can be reached at. Defaults to http://$(hostname):$port where $port is from --" + PortFlag + ".",
},
{
name: configFlag,
name: ConfigFlag,
description: "Path to config file.",
},
{
name: dataDirFlag,
name: DataDirFlag,
description: "Path to directory to store Atlantis data.",
value: "~/.atlantis",
},
{
name: ghHostnameFlag,
name: GHHostnameFlag,
description: "Hostname of your Github Enterprise installation. If using github.com, no need to set.",
value: "github.com",
},
{
name: ghTokenFlag,
name: GHTokenFlag,
description: "[REQUIRED] GitHub token of API user. Can also be specified via the ATLANTIS_GH_TOKEN environment variable.",
env: "ATLANTIS_GH_TOKEN",
},
{
name: ghUserFlag,
name: GHUserFlag,
description: "[REQUIRED] GitHub username of API user.",
},
{
name: ghWebHookSecret,
name: GHWebHookSecret,
description: "Optional secret used for GitHub webhooks (see https://developer.github.com/webhooks/securing/). If not specified, Atlantis won't validate the incoming webhook call.",
env: "ATLANTIS_GH_WEBHOOK_SECRET",
},
{
name: logLevelFlag,
name: LogLevelFlag,
description: "Log level. Either debug, info, warn, or error.",
value: "info",
},
}
var boolFlags = []boolFlag{
{
name: requireApprovalFlag,
name: RequireApprovalFlag,
description: "Require pull requests to be \"Approved\" before allowing the apply command to be run.",
value: false,
},
}
var intFlags = []intFlag{
{
name: portFlag,
name: PortFlag,
description: "Port to bind to.",
value: 4141,
},
Expand All @@ -100,77 +101,119 @@ type boolFlag struct {
value bool
}

var serverCmd = &cobra.Command{
Use: "server",
Short: "Start the atlantis server",
Long: `Start the atlantis server
// ServerCmd is an abstraction that helps us test. It allows
// us to mock out starting the actual server.
type ServerCmd struct {
ServerCreator ServerCreator
Viper *viper.Viper
// SilenceOutput set to true means nothing gets printed.
// Useful for testing to keep the logs clean.
SilenceOutput bool
}

Flags can also be set in a yaml config file (see --` + configFlag + `).
Config file values are overridden by environment variables which in turn are overridden by flags.`,
SilenceErrors: true,

PreRunE: withErrPrint(func(cmd *cobra.Command, args []string) error {
// if passed a config file then try and load it
configFile := viper.GetString(configFlag)
if configFile != "" {
viper.SetConfigFile(configFile)
if err := viper.ReadInConfig(); err != nil {
return errors.Wrapf(err, "invalid config: reading %s", configFile)
}
}
return nil
}),
RunE: withErrPrint(func(cmd *cobra.Command, args []string) error {
var config server.ServerConfig
if err := viper.Unmarshal(&config); err != nil {
return err
}
if err := validate(config); err != nil {
return err
}
if err := setAtlantisURL(&config); err != nil {
return err
}
sanitizeGithubUser(&config)
// ServerCreator creates servers.
// It's an abstraction to help us test.
type ServerCreator interface {
NewServer(config server.ServerConfig) (ServerStarter, error)
}

// config looks good, start the server
server, err := server.NewServer(config)
if err != nil {
return errors.Wrap(err, "initializing server")
}
return server.Start()
}),
// DefaultServerCreator is the concrete implementation of ServerCreator.
type DefaultServerCreator struct{}

// ServerStarter is for starting up a server.
// It's an abstraction to help us test.
type ServerStarter interface {
Start() error
}

func init() {
// if a user passes in an invalid flag, tell them what the flag was
serverCmd.SetFlagErrorFunc(func(c *cobra.Command, err error) error {
// NewServer returns the real Atlantis server object.
func (d *DefaultServerCreator) NewServer(config server.ServerConfig) (ServerStarter, error) {
return server.NewServer(config)
}

// Init returns the runnable cobra command.
func (s *ServerCmd) Init() *cobra.Command {
c := &cobra.Command{
Use: "server",
Short: "Start the atlantis server",
Long: `Start the atlantis server
Flags can also be set in a yaml config file (see --` + ConfigFlag + `).
Config file values are overridden by environment variables which in turn are overridden by flags.`,
SilenceErrors: true,
SilenceUsage: s.SilenceOutput,
PreRunE: s.withErrPrint(func(cmd *cobra.Command, args []string) error {
return s.preRun()
}),
RunE: s.withErrPrint(func(cmd *cobra.Command, args []string) error {
return s.run()
}),
}

// If a user passes in an invalid flag, tell them what the flag was.
c.SetFlagErrorFunc(func(c *cobra.Command, err error) error {
fmt.Fprintf(os.Stderr, "\033[31mError: %s\033[39m\n\n", err.Error())
return err
})

// set string flags
// Set string flags.
for _, f := range stringFlags {
serverCmd.Flags().String(f.name, f.value, f.description)
c.Flags().String(f.name, f.value, f.description)
if f.env != "" {
viper.BindEnv(f.name, f.env)
s.Viper.BindEnv(f.name, f.env)
}
viper.BindPFlag(f.name, serverCmd.Flags().Lookup(f.name))
s.Viper.BindPFlag(f.name, c.Flags().Lookup(f.name))
}

// set int flags
// Set int flags.
for _, f := range intFlags {
serverCmd.Flags().Int(f.name, f.value, f.description)
viper.BindPFlag(f.name, serverCmd.Flags().Lookup(f.name))
c.Flags().Int(f.name, f.value, f.description)
s.Viper.BindPFlag(f.name, c.Flags().Lookup(f.name))
}

// set bool flags
// Set bool flags.
for _, f := range boolFlags {
serverCmd.Flags().Bool(f.name, f.value, f.description)
viper.BindPFlag(f.name, serverCmd.Flags().Lookup(f.name))
c.Flags().Bool(f.name, f.value, f.description)
s.Viper.BindPFlag(f.name, c.Flags().Lookup(f.name))
}

return c
}

func (s *ServerCmd) preRun() error {
// If passed a config file then try and load it.
configFile := s.Viper.GetString(ConfigFlag)
if configFile != "" {
s.Viper.SetConfigFile(configFile)
if err := s.Viper.ReadInConfig(); err != nil {
return errors.Wrapf(err, "invalid config: reading %s", configFile)
}
}
return nil
}

func (s *ServerCmd) run() error {
var config server.ServerConfig
if err := s.Viper.Unmarshal(&config); err != nil {
return err
}
if err := validate(config); err != nil {
return err
}
if err := setAtlantisURL(&config); err != nil {
return err
}
if err := setDataDir(&config); err != nil {
return err
}
sanitizeGithubUser(&config)

RootCmd.AddCommand(serverCmd)
// Config looks good. Start the server.
server, err := s.ServerCreator.NewServer(config)
if err != nil {
return errors.Wrap(err, "initializing server")
}
return server.Start()
}

func validate(config server.ServerConfig) error {
Expand All @@ -179,10 +222,10 @@ func validate(config server.ServerConfig) error {
return errors.New("invalid log level: not one of debug, info, warn, error")
}
if config.GithubUser == "" {
return fmt.Errorf("--%s must be set", ghUserFlag)
return fmt.Errorf("--%s must be set", GHUserFlag)
}
if config.GithubToken == "" {
return fmt.Errorf("--%s must be set", ghTokenFlag)
return fmt.Errorf("--%s must be set", GHTokenFlag)
}
return nil
}
Expand All @@ -199,18 +242,32 @@ func setAtlantisURL(config *server.ServerConfig) error {
return nil
}

// setDataDir checks if ~ was used in data-dir and converts it to the actual
// home directory. If we don't do this, we'll create a directory called "~"
// instead of actually using home.
func setDataDir(config *server.ServerConfig) error {
if strings.HasPrefix(config.DataDir, "~/") {
expanded, err := homedir.Expand(config.DataDir)
if err != nil {
return errors.Wrap(err, "determining home directory")
}
config.DataDir = expanded
}
return nil
}

// sanitizeGithubUser trims @ from the front of the github username if it exists.
func sanitizeGithubUser(config *server.ServerConfig) {
config.GithubUser = strings.TrimPrefix(config.GithubUser, "@")
}

// withErrPrint prints out any errors to a terminal in red.
func withErrPrint(f func(*cobra.Command, []string) error) func(*cobra.Command, []string) error {
func (s *ServerCmd) withErrPrint(f func(*cobra.Command, []string) error) func(*cobra.Command, []string) error {
return func(cmd *cobra.Command, args []string) error {
if err := f(cmd, args); err != nil {
err := f(cmd, args)
if err != nil && !s.SilenceOutput {
fmt.Fprintf(os.Stderr, "\033[31mError: %s\033[39m\n\n", err.Error())
return err
}
return nil
return err
}
}
Loading

0 comments on commit 6e85321

Please sign in to comment.