diff --git a/internal/cmd/base/base.go b/internal/cmd/base/base.go index 83cb69dd28..f1ab85b7f7 100644 --- a/internal/cmd/base/base.go +++ b/internal/cmd/base/base.go @@ -7,12 +7,14 @@ import ( "fmt" "io" "io/ioutil" + "os" "regexp" "strings" "sync" "github.com/hashicorp/watchtower/api" "github.com/mitchellh/cli" + "github.com/pkg/errors" "github.com/posener/complete" ) @@ -29,27 +31,103 @@ const ( var reRemoveWhitespace = regexp.MustCompile(`[\s]+`) type Command struct { - UI cli.Ui - Address string - Context context.Context + Context context.Context + UI cli.Ui + ShutdownCh chan struct{} flags *FlagSets flagsOnce sync.Once - flagCACert string - flagCAPath string - flagClientCert string - flagClientKey string + flagAddress string + flagOrg string + flagProject string + + flagTLSCACert string + flagTLSCAPath string + flagTLSClientCert string + flagTLSClientKey string flagTLSServerName string flagTLSInsecure bool flagFormat string flagField string flagOutputCurlString bool + + client *api.Client } -func (c *Command) SetAddress(addr string) { - c.Address = addr +// 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 + } + + if c.flagAddress != NotSetValue { + c.client.SetAddress(c.flagAddress) + } + 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) + } + + return c.client, nil } type FlagSetBit uint @@ -73,26 +151,39 @@ func (c *Command) FlagSet(bit FlagSetBit) *FlagSets { bit = bit | FlagSetHTTP if bit&FlagSetHTTP != 0 { - f := set.NewFlagSet("HTTP Options") + f := set.NewFlagSet("Connection Options") - addrStringVar := &StringVar{ + f.StringVar(&StringVar{ Name: FlagNameAddress, - Target: &c.Address, + Target: &c.flagAddress, + Default: NotSetValue, EnvVar: api.EnvWatchtowerAddress, Completion: complete.PredictAnything, Usage: "Address of the Watchtower controller.", - } - if c.Address != "" { - addrStringVar.Default = c.Address - } else { - addrStringVar.Default = "https://127.0.0.1:9200" - } - f.StringVar(addrStringVar) + }) + + f.StringVar(&StringVar{ + Name: FlagNameOrg, + Target: &c.flagOrg, + Default: NotSetValue, + EnvVar: api.EnvWatchtowerOrg, + Completion: complete.PredictAnything, + Usage: "Organization in which to make the request; overrides any set in the address.", + }) + + 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.", + }) f.StringVar(&StringVar{ Name: FlagNameCACert, - Target: &c.flagCACert, - Default: "", + Target: &c.flagTLSCACert, + Default: NotSetValue, EnvVar: api.EnvWatchtowerCACert, Completion: complete.PredictFiles("*"), Usage: "Path on the local disk to a single PEM-encoded CA " + @@ -102,8 +193,8 @@ func (c *Command) FlagSet(bit FlagSetBit) *FlagSets { f.StringVar(&StringVar{ Name: FlagNameCAPath, - Target: &c.flagCAPath, - Default: "", + Target: &c.flagTLSCAPath, + Default: NotSetValue, EnvVar: api.EnvWatchtowerCAPath, Completion: complete.PredictDirs("*"), Usage: "Path on the local disk to a directory of PEM-encoded CA " + @@ -112,8 +203,8 @@ func (c *Command) FlagSet(bit FlagSetBit) *FlagSets { f.StringVar(&StringVar{ Name: FlagNameClientCert, - Target: &c.flagClientCert, - Default: "", + Target: &c.flagTLSClientCert, + Default: NotSetValue, EnvVar: api.EnvWatchtowerClientCert, Completion: complete.PredictFiles("*"), Usage: "Path on the local disk to a single PEM-encoded CA " + @@ -123,8 +214,8 @@ func (c *Command) FlagSet(bit FlagSetBit) *FlagSets { f.StringVar(&StringVar{ Name: FlagNameClientKey, - Target: &c.flagClientKey, - Default: "", + Target: &c.flagTLSClientKey, + Default: NotSetValue, EnvVar: api.EnvWatchtowerClientKey, Completion: complete.PredictFiles("*"), Usage: "Path on the local disk to a single PEM-encoded private key " + @@ -134,7 +225,7 @@ func (c *Command) FlagSet(bit FlagSetBit) *FlagSets { f.StringVar(&StringVar{ Name: FlagTLSServerName, Target: &c.flagTLSServerName, - Default: "", + Default: NotSetValue, EnvVar: api.EnvWatchtowerTLSServerName, Completion: complete.PredictAnything, Usage: "Name to use as the SNI host when connecting to the Watchtower " + @@ -142,23 +233,20 @@ func (c *Command) FlagSet(bit FlagSetBit) *FlagSets { }) f.BoolVar(&BoolVar{ - Name: FlagNameTLSInsecure, - Target: &c.flagTLSInsecure, - Default: false, - EnvVar: api.EnvWatchtowerTLSInsecure, + Name: FlagNameTLSInsecure, + Target: &c.flagTLSInsecure, + EnvVar: api.EnvWatchtowerTLSInsecure, 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.", }) f.BoolVar(&BoolVar{ - Name: "output-curl-string", - Target: &c.flagOutputCurlString, - Default: false, + Name: "output-curl-string", + Target: &c.flagOutputCurlString, Usage: "Instead of executing the request, print an equivalent cURL " + "command string and exit.", }) - } if bit&(FlagSetOutputField|FlagSetOutputFormat) != 0 { diff --git a/internal/cmd/base/const.go b/internal/cmd/base/const.go index 01ed7c57dd..621202ea91 100644 --- a/internal/cmd/base/const.go +++ b/internal/cmd/base/const.go @@ -1,29 +1,36 @@ package base const ( - // flagNameAddress is the flag used in the base command to read in the + // FlagNameAddress is the flag used in the base command to read in the // address of the Watchtower server. FlagNameAddress = "address" - // flagnameCACert is the flag used in the base command to read in the CA + // FlagNameOrg is the flag used in the base command to read in the org in + // which to make a request. + FlagNameOrg = "org" + // FlagNameProject is the flag used in the base command to read in the + // project in which to make a request. + FlagNameProject = "project" + // FlagnameCACert is the flag used in the base command to read in the CA // cert. FlagNameCACert = "ca-cert" - // flagnameCAPath is the flag used in the base command to read in the CA + // FlagnameCAPath is the flag used in the base command to read in the CA // cert path. FlagNameCAPath = "ca-path" - //flagNameClientCert is the flag used in the base command to read in the - //client key + // FlagNameClientCert is the flag used in the base command to read in the + // client key FlagNameClientKey = "client-key" - //flagNameClientCert is the flag used in the base command to read in the - //client cert + // FlagNameClientCert is the flag used in the base command to read in the + // client cert FlagNameClientCert = "client-cert" - // flagNameTLSInsecure is the flag used in the base command to read in + // FlagNameTLSInsecure is the flag used in the base command to read in // the option to ignore TLS certificate verification. FlagNameTLSInsecure = "tls-insecure" - // flagTLSServerName is the flag used in the base command to read in + // FlagTLSServerName is the flag used in the base command to read in // the TLS server name. FlagTLSServerName = "tls-server-name" ) -const EnvWatchtowerCLINoColor = `WATCHTOWER_CLI_NO_COLOR` -const EnvWatchtowerCLIFormat = `WATCHTOWER_CLI_FORMAT` - +const ( + EnvWatchtowerCLINoColor = `WATCHTOWER_CLI_NO_COLOR` + EnvWatchtowerCLIFormat = `WATCHTOWER_CLI_FORMAT` +) diff --git a/internal/cmd/commands.go b/internal/cmd/commands.go index 2dbbf88241..62d81e0d0e 100644 --- a/internal/cmd/commands.go +++ b/internal/cmd/commands.go @@ -1,6 +1,7 @@ package cmd import ( + "context" "os" "os/signal" "syscall" @@ -8,6 +9,7 @@ import ( "github.com/hashicorp/watchtower/internal/cmd/base" "github.com/hashicorp/watchtower/internal/cmd/commands/controller" "github.com/hashicorp/watchtower/internal/cmd/commands/dev" + "github.com/hashicorp/watchtower/internal/cmd/commands/hosts" "github.com/hashicorp/watchtower/internal/cmd/commands/worker" "github.com/mitchellh/cli" ) @@ -16,47 +18,56 @@ import ( var Commands map[string]cli.CommandFactory func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { - /* - getBaseCommand := func() *base.Command { - return &base.Command{ - UI: ui, - Address: runOpts.Address, - } + getBaseCommand := func() *base.Command { + ctx, cancel := context.WithCancel(context.Background()) + ret := &base.Command{ + UI: ui, + ShutdownCh: MakeShutdownCh(), + Context: ctx, } - */ + + go func() { + <-ret.ShutdownCh + cancel() + }() + + return ret + } Commands = map[string]cli.CommandFactory{ "controller": func() (cli.Command, error) { return &controller.Command{ Command: &base.Command{ - UI: serverCmdUi, - Address: runOpts.Address, + UI: serverCmdUi, + ShutdownCh: MakeShutdownCh(), }, - ShutdownCh: MakeShutdownCh(), - SighupCh: MakeSighupCh(), - SigUSR2Ch: MakeSigUSR2Ch(), + SighupCh: MakeSighupCh(), + SigUSR2Ch: MakeSigUSR2Ch(), }, nil }, "worker": func() (cli.Command, error) { return &worker.Command{ Command: &base.Command{ - UI: serverCmdUi, - Address: runOpts.Address, + UI: serverCmdUi, + ShutdownCh: MakeShutdownCh(), }, - ShutdownCh: MakeShutdownCh(), - SighupCh: MakeSighupCh(), - SigUSR2Ch: MakeSigUSR2Ch(), + SighupCh: MakeSighupCh(), + SigUSR2Ch: MakeSigUSR2Ch(), }, nil }, "dev": func() (cli.Command, error) { return &dev.Command{ Command: &base.Command{ - UI: serverCmdUi, - Address: runOpts.Address, + UI: serverCmdUi, + ShutdownCh: MakeShutdownCh(), }, - ShutdownCh: MakeShutdownCh(), - SighupCh: MakeSighupCh(), - SigUSR2Ch: MakeSigUSR2Ch(), + SighupCh: MakeSighupCh(), + SigUSR2Ch: MakeSigUSR2Ch(), + }, nil + }, + "hosts create": func() (cli.Command, error) { + return &hosts.CreateCommand{ + Command: getBaseCommand(), }, nil }, } diff --git a/internal/cmd/commands/controller/controller.go b/internal/cmd/commands/controller/controller.go index 2ac8547d79..8f6db30cff 100644 --- a/internal/cmd/commands/controller/controller.go +++ b/internal/cmd/commands/controller/controller.go @@ -25,10 +25,10 @@ type Command struct { *base.Command *base.Server - ShutdownCh chan struct{} - SighupCh chan struct{} - ReloadedCh chan struct{} - SigUSR2Ch chan struct{} + ExtShutdownCh chan struct{} + SighupCh chan struct{} + ReloadedCh chan struct{} + SigUSR2Ch chan struct{} cleanupGuard sync.Once @@ -343,9 +343,14 @@ func (c *Command) WaitForInterrupt() int { // Wait for shutdown shutdownTriggered := false + shutdownCh := c.ShutdownCh + if c.ExtShutdownCh != nil { + shutdownCh = c.ExtShutdownCh + } + for !shutdownTriggered { select { - case <-c.ShutdownCh: + case <-shutdownCh: c.UI.Output("==> Watchtower controller shutdown triggered") if err := c.controller.Shutdown(); err != nil { diff --git a/internal/cmd/commands/dev/dev.go b/internal/cmd/commands/dev/dev.go index f668e63b5b..e4c84dfd87 100644 --- a/internal/cmd/commands/dev/dev.go +++ b/internal/cmd/commands/dev/dev.go @@ -21,7 +21,6 @@ type Command struct { *base.Command *base.Server - ShutdownCh chan struct{} SighupCh chan struct{} childSighupCh []chan struct{} ReloadedCh chan struct{} @@ -244,11 +243,11 @@ func (c *Command) Run(args []string) int { c.childSighupCh = append(c.childSighupCh, controllerSighupCh) devController := &controllercmd.Command{ - Command: c.Command, - Server: c.Server, - ShutdownCh: childShutdownCh, - SighupCh: controllerSighupCh, - Config: devConfig, + Command: c.Command, + Server: c.Server, + ExtShutdownCh: childShutdownCh, + SighupCh: controllerSighupCh, + Config: devConfig, } if err := devController.Start(); err != nil { c.UI.Error(err.Error()) @@ -258,11 +257,11 @@ func (c *Command) Run(args []string) int { workerSighupCh := make(chan struct{}) c.childSighupCh = append(c.childSighupCh, workerSighupCh) devWorker := &workercmd.Command{ - Command: c.Command, - Server: c.Server, - ShutdownCh: childShutdownCh, - SighupCh: workerSighupCh, - Config: devConfig, + Command: c.Command, + Server: c.Server, + ExtShutdownCh: childShutdownCh, + SighupCh: workerSighupCh, + Config: devConfig, } if err := devWorker.Start(); err != nil { c.UI.Error(err.Error()) @@ -286,7 +285,8 @@ func (c *Command) Run(args []string) int { case <-c.ShutdownCh: c.UI.Output("==> Watchtower dev environment shutdown triggered") - close(childShutdownCh) + childShutdownCh <- struct{}{} + childShutdownCh <- struct{}{} shutdownTriggered = true diff --git a/internal/cmd/commands/hosts/host_create.go b/internal/cmd/commands/hosts/host_create.go new file mode 100644 index 0000000000..3dcb67a017 --- /dev/null +++ b/internal/cmd/commands/hosts/host_create.go @@ -0,0 +1,131 @@ +package hosts + +import ( + "fmt" + "strings" + + "github.com/hashicorp/watchtower/api" + "github.com/hashicorp/watchtower/api/hosts" + "github.com/hashicorp/watchtower/internal/cmd/base" + "github.com/kr/pretty" + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +var _ cli.Command = (*CreateCommand)(nil) +var _ cli.CommandAutocomplete = (*CreateCommand)(nil) + +type CreateCommand struct { + *base.Command + + flagHost string + flagName string + flagCatalog string +} + +func (c *CreateCommand) Synopsis() string { + return "Creates a host in the given host catalog" +} + +func (c *CreateCommand) Help() string { + helpText := ` +Usage: watchtower hosts create + + Creates a host in the given host catalog. This command will result in an + error for any catalog that does not support manual host creation. + + Example: + + $ watchtower hosts create -catalog= -host= -name= + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *CreateCommand) Flags() *base.FlagSets { + set := c.FlagSet(base.FlagSetHTTP | base.FlagSetOutputFormat) + + f := set.NewFlagSet("Command Options") + + f.StringVar(&base.StringVar{ + Name: "host", + Target: &c.flagHost, + Completion: complete.PredictAnything, + Usage: "The host's address; can be an IP address or DNS name", + }) + + f.StringVar(&base.StringVar{ + Name: "name", + Target: &c.flagName, + Completion: complete.PredictAnything, + Usage: "A friendly name for the host for display purposes", + }) + + f.StringVar(&base.StringVar{ + Name: "catalog", + Target: &c.flagCatalog, + Completion: complete.PredictAnything, + Usage: "The ID of the host catalog in which the host should be created", + }) + + return set +} + +func (c *CreateCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictAnything +} + +func (c *CreateCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *CreateCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + switch { + case c.flagCatalog == "": + c.UI.Error("Catalog ID must be provided via -catalog") + return 1 + case c.flagHost == "": + c.UI.Error("Host address must be provided via -host") + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + catalog := &hosts.HostCatalog{ + Client: client, + Id: api.String(c.flagCatalog), + } + + host := &hosts.Host{ + FriendlyName: api.StringOrNil(c.flagName), + Address: api.String(c.flagHost), + } + + var apiErr *api.Error + host, apiErr, err = catalog.CreateHost(c.Context, host) + + switch { + case err != nil: + c.UI.Error(fmt.Errorf("error creating host: %w", err).Error()) + return 2 + case apiErr != nil: + c.UI.Error(pretty.Sprint(apiErr)) + return 2 + default: + c.UI.Info(pretty.Sprint(host)) + } + + return 0 +} diff --git a/internal/cmd/commands/worker/worker.go b/internal/cmd/commands/worker/worker.go index 301037f358..15663ea98c 100644 --- a/internal/cmd/commands/worker/worker.go +++ b/internal/cmd/commands/worker/worker.go @@ -23,10 +23,10 @@ type Command struct { *base.Command *base.Server - ShutdownCh chan struct{} - SighupCh chan struct{} - ReloadedCh chan struct{} - SigUSR2Ch chan struct{} + ExtShutdownCh chan struct{} + SighupCh chan struct{} + ReloadedCh chan struct{} + SigUSR2Ch chan struct{} cleanupGuard sync.Once @@ -254,9 +254,14 @@ func (c *Command) WaitForInterrupt() int { // Wait for shutdown shutdownTriggered := false + shutdownCh := c.ShutdownCh + if c.ExtShutdownCh != nil { + shutdownCh = c.ExtShutdownCh + } + for !shutdownTriggered { select { - case <-c.ShutdownCh: + case <-shutdownCh: c.UI.Output("==> Watchtower worker shutdown triggered") if err := c.worker.Shutdown(); err != nil { diff --git a/internal/cmd/main.go b/internal/cmd/main.go index 7ac2242356..0b6c8210e7 100644 --- a/internal/cmd/main.go +++ b/internal/cmd/main.go @@ -12,7 +12,7 @@ import ( "text/tabwriter" "github.com/fatih/color" - "github.com/hashicorp/vault/api" + "github.com/hashicorp/watchtower/api" "github.com/hashicorp/watchtower/internal/cmd/base" colorable "github.com/mattn/go-colorable" "github.com/mitchellh/cli" @@ -178,7 +178,7 @@ func RunCustom(args []string, runOpts *RunOptions) int { exitCode, err := cli.Run() if outputCurlString { if exitCode == 0 { - fmt.Fprint(runOpts.Stderr, "Could not generate cURL command") + fmt.Fprint(runOpts.Stderr, "Could not generate cURL command\n") return 1 } else { if api.LastOutputStringError == nil {