Add a hosts create command (#25)

pull/26/head
Jeff Mitchell 6 years ago committed by GitHub
parent 17fffe7c23
commit e3c7ca3070
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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 {

@ -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`
)

@ -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
},
}

@ -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 {

@ -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

@ -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=<id> -host=<addr> -name=<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
}

@ -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 {

@ -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 {

Loading…
Cancel
Save