2020-02-12 13:53:27 -05:00
package base
import (
"bytes"
"context"
"flag"
"fmt"
"io"
"io/ioutil"
2020-05-04 23:04:55 -04:00
"os"
2020-06-24 13:08:13 -07:00
"os/signal"
2020-02-12 13:53:27 -05:00
"regexp"
"strings"
"sync"
2020-06-24 13:08:13 -07:00
"syscall"
2020-02-12 13:53:27 -05:00
2020-05-04 12:41:36 -04:00
"github.com/hashicorp/watchtower/api"
2020-02-12 13:53:27 -05:00
"github.com/mitchellh/cli"
2020-05-04 23:04:55 -04:00
"github.com/pkg/errors"
2020-02-12 13:53:27 -05:00
"github.com/posener/complete"
2020-07-14 14:32:47 -07:00
"github.com/zalando/go-keyring"
2020-02-12 13:53:27 -05:00
)
const (
// maxLineLength is the maximum width of any line.
maxLineLength int = 78
// NotSetValue is a flag value for a not-set value
NotSetValue = "(not set)"
2020-07-14 14:32:47 -07:00
envTokenName = "WATCHTOWER_TOKEN_NAME"
2020-02-12 13:53:27 -05:00
)
// reRemoveWhitespace is a regular expression for stripping whitespace from
// a string.
var reRemoveWhitespace = regexp . MustCompile ( `[\s]+` )
2020-03-10 15:48:04 -04:00
type Command struct {
2020-05-04 23:04:55 -04:00
Context context . Context
UI cli . Ui
ShutdownCh chan struct {}
2020-02-12 13:53:27 -05:00
flags * FlagSets
flagsOnce sync . Once
2020-05-05 11:19:42 -04:00
flagAddr string
2020-05-04 23:04:55 -04:00
flagOrg string
flagProject string
flagTLSCACert string
flagTLSCAPath string
flagTLSClientCert string
flagTLSClientKey string
2020-02-12 13:53:27 -05:00
flagTLSServerName string
flagTLSInsecure bool
flagFormat string
flagField string
2020-07-14 14:32:47 -07:00
FlagTokenName string
2020-02-12 13:53:27 -05:00
flagOutputCurlString bool
2020-05-04 23:04:55 -04:00
client * api . Client
2020-02-12 13:53:27 -05:00
}
2020-06-24 13:08:13 -07:00
// New returns a new instance of a base.Command type
2020-06-24 15:10:12 -05:00
func NewCommand ( ui cli . Ui ) * Command {
2020-06-24 13:08:13 -07:00
ctx , cancel := context . WithCancel ( context . Background ())
ret := & Command {
UI : ui ,
ShutdownCh : MakeShutdownCh (),
Context : ctx ,
}
go func () {
<- ret . ShutdownCh
cancel ()
}()
return ret
}
// MakeShutdownCh returns a channel that can be used for shutdown
// notifications for commands. This channel will send a message for every
// SIGINT or SIGTERM received.
func MakeShutdownCh () chan struct {} {
resultCh := make ( chan struct {})
shutdownCh := make ( chan os . Signal , 4 )
signal . Notify ( shutdownCh , os . Interrupt , syscall . SIGTERM )
go func () {
<- shutdownCh
close ( resultCh )
}()
return resultCh
}
2020-05-04 23:04:55 -04:00
// Client returns the HTTP API client. The client is cached on the command to
// save performance on future calls.
func ( c * Command ) Client () ( * api . Client , error ) {
// Read the test client if present
if c . client != nil {
return c . client , nil
}
config , err := api . DefaultConfig ()
if err != nil {
return nil , err
}
if c . flagOutputCurlString {
config . OutputCurlString = c . flagOutputCurlString
}
c . client , err = api . NewClient ( config )
if err != nil {
return nil , err
}
2020-05-05 11:19:42 -04:00
if c . flagAddr != NotSetValue {
c . client . SetAddr ( c . flagAddr )
2020-05-04 23:04:55 -04:00
}
if c . flagOrg != NotSetValue {
c . client . SetOrg ( c . flagOrg )
}
if c . flagProject != NotSetValue {
c . client . SetProject ( c . flagProject )
}
// If we need custom TLS configuration, then set it
var modifiedTLS bool
tlsConfig := config . TLSConfig
if c . flagTLSCACert != NotSetValue {
tlsConfig . CACert = c . flagTLSCACert
modifiedTLS = true
}
if c . flagTLSCAPath != NotSetValue {
tlsConfig . CAPath = c . flagTLSCAPath
modifiedTLS = true
}
if c . flagTLSClientCert != NotSetValue {
tlsConfig . ClientCert = c . flagTLSClientCert
modifiedTLS = true
}
if c . flagTLSClientKey != NotSetValue {
tlsConfig . ClientKey = c . flagTLSClientKey
modifiedTLS = true
}
if c . flagTLSServerName != NotSetValue {
tlsConfig . ServerName = c . flagTLSServerName
modifiedTLS = true
}
if c . flagTLSInsecure {
tlsConfig . Insecure = c . flagTLSInsecure
modifiedTLS = true
}
if modifiedTLS {
// Setup TLS config
if err := c . client . SetTLSConfig ( tlsConfig ); err != nil {
return nil , errors . Wrap ( err , "failed to setup TLS config" )
}
}
// Turn off retries on the CLI
if os . Getenv ( api . EnvWatchtowerMaxRetries ) == "" {
c . client . SetMaxRetries ( 0 )
}
2020-07-14 14:32:47 -07:00
if c . client . Token () == "" {
tokenName := "default"
if c . FlagTokenName != "" {
tokenName = c . FlagTokenName
}
if tokenName != "none" {
token , err := keyring . Get ( "HashiCorp Watchtower Auth Token" , tokenName )
if err != nil {
if err == keyring . ErrNotFound {
c . UI . Info ( "No saved credential found, continuing without" )
} else {
c . UI . Error ( fmt . Sprintf ( "Error reading auth token from system credential store: %s" , err ))
}
}
if token != "" {
c . client . SetToken ( token )
}
}
}
2020-05-04 23:04:55 -04:00
return c . client , nil
2020-02-12 13:53:27 -05:00
}
type FlagSetBit uint
const (
FlagSetNone FlagSetBit = 1 << iota
FlagSetHTTP
2020-07-14 14:32:47 -07:00
FlagSetClient
2020-02-12 13:53:27 -05:00
FlagSetOutputField
FlagSetOutputFormat
)
// FlagSet creates the flags for this command. The result is cached on the
// command to save performance on future calls.
2020-03-10 15:48:04 -04:00
func ( c * Command ) FlagSet ( bit FlagSetBit ) * FlagSets {
2020-02-12 13:53:27 -05:00
c . flagsOnce . Do ( func () {
set := NewFlagSets ( c . UI )
// These flag sets will apply to all leaf subcommands.
// TODO: Optional, but FlagSetHTTP can be safely removed from the individual
// Flags() subcommands.
bit = bit | FlagSetHTTP
if bit & FlagSetHTTP != 0 {
2020-05-04 23:04:55 -04:00
f := set . NewFlagSet ( "Connection Options" )
2020-02-12 13:53:27 -05:00
2020-05-04 23:04:55 -04:00
f . StringVar ( & StringVar {
2020-05-05 11:19:42 -04:00
Name : FlagNameAddr ,
Target : & c . flagAddr ,
2020-05-04 23:04:55 -04:00
Default : NotSetValue ,
2020-05-05 11:19:42 -04:00
EnvVar : api . EnvWatchtowerAddr ,
2020-02-12 13:53:27 -05:00
Completion : complete . PredictAnything ,
2020-05-05 11:19:42 -04:00
Usage : "Addr of the Watchtower controller, as a complete URL (e.g. https://watchtower.example.com:9200)." ,
2020-05-04 23:04:55 -04:00
})
f . StringVar ( & StringVar {
Name : FlagNameOrg ,
Target : & c . flagOrg ,
Default : NotSetValue ,
EnvVar : api . EnvWatchtowerOrg ,
Completion : complete . PredictAnything ,
2020-07-11 12:00:49 -04:00
Usage : "Org in which to make the request; overrides any set in the address." ,
2020-05-04 23:04:55 -04:00
})
f . StringVar ( & StringVar {
Name : FlagNameProject ,
Target : & c . flagProject ,
Default : NotSetValue ,
EnvVar : api . EnvWatchtowerProject ,
Completion : complete . PredictAnything ,
Usage : "Project in which to make the request; overrides any set in the address." ,
})
2020-02-12 13:53:27 -05:00
f . StringVar ( & StringVar {
Name : FlagNameCACert ,
2020-05-04 23:04:55 -04:00
Target : & c . flagTLSCACert ,
Default : NotSetValue ,
2020-05-04 12:41:36 -04:00
EnvVar : api . EnvWatchtowerCACert ,
2020-02-12 13:53:27 -05:00
Completion : complete . PredictFiles ( "*" ),
Usage : "Path on the local disk to a single PEM-encoded CA " +
"certificate to verify the Controller or Worker's server's SSL certificate. This " +
"takes precedence over -ca-path." ,
})
f . StringVar ( & StringVar {
Name : FlagNameCAPath ,
2020-05-04 23:04:55 -04:00
Target : & c . flagTLSCAPath ,
Default : NotSetValue ,
2020-05-04 12:41:36 -04:00
EnvVar : api . EnvWatchtowerCAPath ,
2020-02-12 13:53:27 -05:00
Completion : complete . PredictDirs ( "*" ),
Usage : "Path on the local disk to a directory of PEM-encoded CA " +
"certificates to verify the SSL certificate of the Controller." ,
})
f . StringVar ( & StringVar {
Name : FlagNameClientCert ,
2020-05-04 23:04:55 -04:00
Target : & c . flagTLSClientCert ,
Default : NotSetValue ,
2020-05-04 12:41:36 -04:00
EnvVar : api . EnvWatchtowerClientCert ,
2020-02-12 13:53:27 -05:00
Completion : complete . PredictFiles ( "*" ),
Usage : "Path on the local disk to a single PEM-encoded CA " +
"certificate to use for TLS authentication to the Watchtower Controller. If " +
"this flag is specified, -client-key is also required." ,
})
f . StringVar ( & StringVar {
Name : FlagNameClientKey ,
2020-05-04 23:04:55 -04:00
Target : & c . flagTLSClientKey ,
Default : NotSetValue ,
2020-05-04 12:41:36 -04:00
EnvVar : api . EnvWatchtowerClientKey ,
2020-02-12 13:53:27 -05:00
Completion : complete . PredictFiles ( "*" ),
Usage : "Path on the local disk to a single PEM-encoded private key " +
"matching the client certificate from -client-cert." ,
})
f . StringVar ( & StringVar {
Name : FlagTLSServerName ,
Target : & c . flagTLSServerName ,
2020-05-04 23:04:55 -04:00
Default : NotSetValue ,
2020-05-04 12:41:36 -04:00
EnvVar : api . EnvWatchtowerTLSServerName ,
2020-02-12 13:53:27 -05:00
Completion : complete . PredictAnything ,
Usage : "Name to use as the SNI host when connecting to the Watchtower " +
"server via TLS." ,
})
f . BoolVar ( & BoolVar {
2020-05-04 23:04:55 -04:00
Name : FlagNameTLSInsecure ,
Target : & c . flagTLSInsecure ,
EnvVar : api . EnvWatchtowerTLSInsecure ,
2020-02-12 13:53:27 -05:00
Usage : "Disable verification of TLS certificates. Using this option " +
"is highly discouraged as it decreases the security of data " +
"transmissions to and from the Watchtower server." ,
})
2020-07-14 14:32:47 -07:00
}
if bit & FlagSetClient != 0 {
f := set . NewFlagSet ( "Client Options" )
f . StringVar ( & StringVar {
Name : "token-name" ,
Target : & c . FlagTokenName ,
EnvVar : envTokenName ,
Usage : "If specified, the given value will be used as the name when storing the token in the system credential store. This can allow switching user identities for different commands." ,
})
2020-02-12 13:53:27 -05:00
f . BoolVar ( & BoolVar {
2020-05-04 23:04:55 -04:00
Name : "output-curl-string" ,
Target : & c . flagOutputCurlString ,
2020-02-12 13:53:27 -05:00
Usage : "Instead of executing the request, print an equivalent cURL " +
"command string and exit." ,
})
}
if bit & ( FlagSetOutputField | FlagSetOutputFormat ) != 0 {
f := set . NewFlagSet ( "Output Options" )
if bit & FlagSetOutputField != 0 {
f . StringVar ( & StringVar {
Name : "field" ,
Target : & c . flagField ,
Default : "" ,
Completion : complete . PredictAnything ,
Usage : "Print only the field with the given name. Specifying " +
"this option will take precedence over other formatting " +
"directives. The result will not have a trailing newline " +
"making it ideal for piping to other processes." ,
})
}
if bit & FlagSetOutputFormat != 0 {
f . StringVar ( & StringVar {
Name : "format" ,
Target : & c . flagFormat ,
Default : "table" ,
EnvVar : EnvWatchtowerCLIFormat ,
Completion : complete . PredictSet ( "table" , "json" , "yaml" ),
Usage : "Print the output in the given format. Valid formats " +
"are \"table\", \"json\", or \"yaml\"." ,
})
}
}
c . flags = set
})
return c . flags
}
// FlagSets is a group of flag sets.
type FlagSets struct {
flagSets [] * FlagSet
mainSet * flag . FlagSet
hiddens map [ string ] struct {}
completions complete . Flags
}
// NewFlagSets creates a new flag sets.
func NewFlagSets ( ui cli . Ui ) * FlagSets {
mainSet := flag . NewFlagSet ( "" , flag . ContinueOnError )
// Errors and usage are controlled by the CLI.
mainSet . Usage = func () {}
mainSet . SetOutput ( ioutil . Discard )
return & FlagSets {
flagSets : make ([] * FlagSet , 0 , 6 ),
mainSet : mainSet ,
hiddens : make ( map [ string ] struct {}),
completions : complete . Flags {},
}
}
// NewFlagSet creates a new flag set from the given flag sets.
func ( f * FlagSets ) NewFlagSet ( name string ) * FlagSet {
flagSet := NewFlagSet ( name )
flagSet . mainSet = f . mainSet
flagSet . completions = f . completions
f . flagSets = append ( f . flagSets , flagSet )
return flagSet
}
// Completions returns the completions for this flag set.
func ( f * FlagSets ) Completions () complete . Flags {
return f . completions
}
// Parse parses the given flags, returning any errors.
func ( f * FlagSets ) Parse ( args [] string ) error {
return f . mainSet . Parse ( args )
}
// Parsed reports whether the command-line flags have been parsed.
func ( f * FlagSets ) Parsed () bool {
return f . mainSet . Parsed ()
}
// Args returns the remaining args after parsing.
func ( f * FlagSets ) Args () [] string {
return f . mainSet . Args ()
}
// Visit visits the flags in lexicographical order, calling fn for each. It
// visits only those flags that have been set.
func ( f * FlagSets ) Visit ( fn func ( * flag . Flag )) {
f . mainSet . Visit ( fn )
}
// Help builds custom help for this command, grouping by flag set.
func ( fs * FlagSets ) Help () string {
var out bytes . Buffer
for _ , set := range fs . flagSets {
printFlagTitle ( & out , set . name + ":" )
set . VisitAll ( func ( f * flag . Flag ) {
// Skip any hidden flags
if v , ok := f . Value .( FlagVisibility ); ok && v . Hidden () {
return
}
printFlagDetail ( & out , f )
})
}
return strings . TrimRight ( out . String (), "\n" )
}
// FlagSet is a grouped wrapper around a real flag set and a grouped flag set.
type FlagSet struct {
name string
flagSet * flag . FlagSet
mainSet * flag . FlagSet
completions complete . Flags
}
// NewFlagSet creates a new flag set.
func NewFlagSet ( name string ) * FlagSet {
return & FlagSet {
name : name ,
flagSet : flag . NewFlagSet ( name , flag . ContinueOnError ),
}
}
// Name returns the name of this flag set.
func ( f * FlagSet ) Name () string {
return f . name
}
func ( f * FlagSet ) Visit ( fn func ( * flag . Flag )) {
f . flagSet . Visit ( fn )
}
func ( f * FlagSet ) VisitAll ( fn func ( * flag . Flag )) {
f . flagSet . VisitAll ( fn )
}
// printFlagTitle prints a consistently-formatted title to the given writer.
func printFlagTitle ( w io . Writer , s string ) {
fmt . Fprintf ( w , "%s\n\n" , s )
}
// printFlagDetail prints a single flag to the given writer.
func printFlagDetail ( w io . Writer , f * flag . Flag ) {
// Check if the flag is hidden - do not print any flag detail or help output
// if it is hidden.
if h , ok := f . Value .( FlagVisibility ); ok && h . Hidden () {
return
}
// Check for a detailed example
example := ""
if t , ok := f . Value .( FlagExample ); ok {
example = t . Example ()
}
if example != "" {
fmt . Fprintf ( w , " -%s=<%s>\n" , f . Name , example )
} else {
fmt . Fprintf ( w , " -%s\n" , f . Name )
}
usage := reRemoveWhitespace . ReplaceAllString ( f . Usage , " " )
indented := WrapAtLengthWithPadding ( usage , 6 )
fmt . Fprintf ( w , "%s\n\n" , indented )
}