Files
docker-machine/commands/create.go
John Sirois 705865fa70 Fixup create command flag application.
Previously StringSlice flag defaults were not respected.  These use
special handling and that handling did not take account of default
values, always over-writing them.  Expand the CommandLine interface to
take advantage of the underlying codegansta/cli.Context IsSet method and
guard default string slice overwrites with an IsSet test.

Signed-off-by: John Sirois <john.sirois@gmail.com>
2016-03-29 11:53:22 -06:00

459 lines
13 KiB
Go

package commands
import (
"encoding/json"
"flag"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"errors"
"time"
"github.com/codegangsta/cli"
"github.com/docker/machine/commands/mcndirs"
"github.com/docker/machine/libmachine"
"github.com/docker/machine/libmachine/auth"
"github.com/docker/machine/libmachine/crashreport"
"github.com/docker/machine/libmachine/drivers"
"github.com/docker/machine/libmachine/drivers/rpc"
"github.com/docker/machine/libmachine/engine"
"github.com/docker/machine/libmachine/host"
"github.com/docker/machine/libmachine/log"
"github.com/docker/machine/libmachine/mcnerror"
"github.com/docker/machine/libmachine/mcnflag"
"github.com/docker/machine/libmachine/swarm"
)
var (
errNoMachineName = errors.New("Error: No machine name specified")
)
var (
SharedCreateFlags = []cli.Flag{
cli.StringFlag{
Name: "driver, d",
Usage: fmt.Sprintf(
"Driver to create machine with.",
),
Value: "none",
EnvVar: "MACHINE_DRIVER",
},
cli.StringFlag{
Name: "engine-install-url",
Usage: "Custom URL to use for engine installation",
Value: drivers.DefaultEngineInstallURL,
EnvVar: "MACHINE_DOCKER_INSTALL_URL",
},
cli.StringSliceFlag{
Name: "engine-opt",
Usage: "Specify arbitrary flags to include with the created engine in the form flag=value",
Value: &cli.StringSlice{},
},
cli.StringSliceFlag{
Name: "engine-insecure-registry",
Usage: "Specify insecure registries to allow with the created engine",
Value: &cli.StringSlice{},
},
cli.StringSliceFlag{
Name: "engine-registry-mirror",
Usage: "Specify registry mirrors to use",
Value: &cli.StringSlice{},
EnvVar: "ENGINE_REGISTRY_MIRROR",
},
cli.StringSliceFlag{
Name: "engine-label",
Usage: "Specify labels for the created engine",
Value: &cli.StringSlice{},
},
cli.StringFlag{
Name: "engine-storage-driver",
Usage: "Specify a storage driver to use with the engine",
},
cli.StringSliceFlag{
Name: "engine-env",
Usage: "Specify environment variables to set in the engine",
Value: &cli.StringSlice{},
},
cli.BoolFlag{
Name: "swarm",
Usage: "Configure Machine with Swarm",
},
cli.StringFlag{
Name: "swarm-image",
Usage: "Specify Docker image to use for Swarm",
Value: "swarm:latest",
EnvVar: "MACHINE_SWARM_IMAGE",
},
cli.BoolFlag{
Name: "swarm-master",
Usage: "Configure Machine to be a Swarm master",
},
cli.StringFlag{
Name: "swarm-discovery",
Usage: "Discovery service to use with Swarm",
Value: "",
},
cli.StringFlag{
Name: "swarm-strategy",
Usage: "Define a default scheduling strategy for Swarm",
Value: "spread",
},
cli.StringSliceFlag{
Name: "swarm-opt",
Usage: "Define arbitrary flags for swarm",
Value: &cli.StringSlice{},
},
cli.StringFlag{
Name: "swarm-host",
Usage: "ip/socket to listen on for Swarm master",
Value: "tcp://0.0.0.0:3376",
},
cli.StringFlag{
Name: "swarm-addr",
Usage: "addr to advertise for Swarm (default: detect and use the machine IP)",
Value: "",
},
cli.BoolFlag{
Name: "swarm-experimental",
Usage: "Enable Swarm experimental features",
},
cli.StringSliceFlag{
Name: "tls-san",
Usage: "Support extra SANs for TLS certs",
Value: &cli.StringSlice{},
},
}
)
func cmdCreateInner(c CommandLine, api libmachine.API) error {
if len(c.Args()) > 1 {
return fmt.Errorf("Invalid command line. Found extra arguments %v", c.Args()[1:])
}
name := c.Args().First()
if name == "" {
c.ShowHelp()
return errNoMachineName
}
validName := host.ValidateHostName(name)
if !validName {
return fmt.Errorf("Error creating machine: %s", mcnerror.ErrInvalidHostname)
}
if err := validateSwarmDiscovery(c.String("swarm-discovery")); err != nil {
return fmt.Errorf("Error parsing swarm discovery: %s", err)
}
// TODO: Fix hacky JSON solution
rawDriver, err := json.Marshal(&drivers.BaseDriver{
MachineName: name,
StorePath: c.GlobalString("storage-path"),
})
if err != nil {
return fmt.Errorf("Error attempting to marshal bare driver data: %s", err)
}
driverName := c.String("driver")
h, err := api.NewHost(driverName, rawDriver)
if err != nil {
return fmt.Errorf("Error getting new host: %s", err)
}
h.HostOptions = &host.Options{
AuthOptions: &auth.Options{
CertDir: mcndirs.GetMachineCertDir(),
CaCertPath: tlsPath(c, "tls-ca-cert", "ca.pem"),
CaPrivateKeyPath: tlsPath(c, "tls-ca-key", "ca-key.pem"),
ClientCertPath: tlsPath(c, "tls-client-cert", "cert.pem"),
ClientKeyPath: tlsPath(c, "tls-client-key", "key.pem"),
ServerCertPath: filepath.Join(mcndirs.GetMachineDir(), name, "server.pem"),
ServerKeyPath: filepath.Join(mcndirs.GetMachineDir(), name, "server-key.pem"),
StorePath: filepath.Join(mcndirs.GetMachineDir(), name),
ServerCertSANs: c.StringSlice("tls-san"),
},
EngineOptions: &engine.Options{
ArbitraryFlags: c.StringSlice("engine-opt"),
Env: c.StringSlice("engine-env"),
InsecureRegistry: c.StringSlice("engine-insecure-registry"),
Labels: c.StringSlice("engine-label"),
RegistryMirror: c.StringSlice("engine-registry-mirror"),
StorageDriver: c.String("engine-storage-driver"),
TLSVerify: true,
InstallURL: c.String("engine-install-url"),
},
SwarmOptions: &swarm.Options{
IsSwarm: c.Bool("swarm"),
Image: c.String("swarm-image"),
Master: c.Bool("swarm-master"),
Discovery: c.String("swarm-discovery"),
Address: c.String("swarm-addr"),
Host: c.String("swarm-host"),
Strategy: c.String("swarm-strategy"),
ArbitraryFlags: c.StringSlice("swarm-opt"),
IsExperimental: c.Bool("swarm-experimental"),
},
}
exists, err := api.Exists(h.Name)
if err != nil {
return fmt.Errorf("Error checking if host exists: %s", err)
}
if exists {
return mcnerror.ErrHostAlreadyExists{
Name: h.Name,
}
}
// driverOpts is the actual data we send over the wire to set the
// driver parameters (an interface fulfilling drivers.DriverOptions,
// concrete type rpcdriver.RpcFlags).
mcnFlags := h.Driver.GetCreateFlags()
driverOpts := getDriverOpts(c, mcnFlags)
if err := h.Driver.SetConfigFromFlags(driverOpts); err != nil {
return fmt.Errorf("Error setting machine configuration from flags provided: %s", err)
}
if err := api.Create(h); err != nil {
// Wait for all the logs to reach the client
time.Sleep(2 * time.Second)
vBoxLog := ""
if h.DriverName == "virtualbox" {
vBoxLog = filepath.Join(api.GetMachinesDir(), h.Name, h.Name, "Logs", "VBox.log")
}
return crashreport.CrashError{
Cause: err,
Command: "Create",
Context: "api.performCreate",
DriverName: h.DriverName,
LogFilePath: vBoxLog,
}
}
if err := api.Save(h); err != nil {
return fmt.Errorf("Error attempting to save store: %s", err)
}
log.Infof("To see how to connect your Docker Client to the Docker Engine running on this virtual machine, run: %s env %s", os.Args[0], name)
return nil
}
// The following function is needed because the CLI acrobatics that we're doing
// (with having an "outer" and "inner" function each with their own custom
// settings and flag parsing needs) are not well supported by codegangsta/cli.
//
// Instead of trying to make a convoluted series of flag parsing and relying on
// codegangsta/cli internals work well, we simply read the flags we're
// interested in from the outer function into module-level variables, and then
// use them from the "inner" function.
//
// I'm not very pleased about this, but it seems to be the only decent
// compromise without drastically modifying codegangsta/cli internals or our
// own CLI.
func flagHackLookup(flagName string) string {
// e.g. "-d" for "--driver"
flagPrefix := flagName[1:3]
// TODO: Should we support -flag-name (single hyphen) syntax as well?
for i, arg := range os.Args {
if strings.Contains(arg, flagPrefix) {
// format '--driver foo' or '-d foo'
if arg == flagPrefix || arg == flagName {
if i+1 < len(os.Args) {
return os.Args[i+1]
}
}
// format '--driver=foo' or '-d=foo'
if strings.HasPrefix(arg, flagPrefix+"=") || strings.HasPrefix(arg, flagName+"=") {
return strings.Split(arg, "=")[1]
}
}
}
return ""
}
func cmdCreateOuter(c CommandLine, api libmachine.API) error {
const (
flagLookupMachineName = "flag-lookup"
)
// We didn't recognize the driver name.
driverName := flagHackLookup("--driver")
if driverName == "" {
//TODO: Check Environment have to include flagHackLookup function.
driverName = os.Getenv("MACHINE_DRIVER")
if driverName == "" {
c.ShowHelp()
return nil // ?
}
}
// TODO: Fix hacky JSON solution
rawDriver, err := json.Marshal(&drivers.BaseDriver{
MachineName: flagLookupMachineName,
})
if err != nil {
return fmt.Errorf("Error attempting to marshal bare driver data: %s", err)
}
h, err := api.NewHost(driverName, rawDriver)
if err != nil {
return err
}
// TODO: So much flag manipulation and voodoo here, it seems to be
// asking for trouble.
//
// mcnFlags is the data we get back over the wire (type mcnflag.Flag)
// to indicate which parameters are available.
mcnFlags := h.Driver.GetCreateFlags()
// This bit will actually make "create" display the correct flags based
// on the requested driver.
cliFlags, err := convertMcnFlagsToCliFlags(mcnFlags)
if err != nil {
return fmt.Errorf("Error trying to convert provided driver flags to cli flags: %s", err)
}
for i := range c.Application().Commands {
cmd := &c.Application().Commands[i]
if cmd.HasName("create") {
cmd = addDriverFlagsToCommand(cliFlags, cmd)
}
}
return c.Application().Run(os.Args)
}
func getDriverOpts(c CommandLine, mcnflags []mcnflag.Flag) drivers.DriverOptions {
// TODO: This function is pretty damn YOLO and would benefit from some
// sanity checking around types and assertions.
//
// But, we need it so that we can actually send the flags for creating
// a machine over the wire (cli.Context is a no go since there is so
// much stuff in it).
driverOpts := rpcdriver.RPCFlags{
Values: make(map[string]interface{}),
}
for _, f := range mcnflags {
driverOpts.Values[f.String()] = f.Default()
// Hardcoded logic for boolean... :(
if f.Default() == nil {
driverOpts.Values[f.String()] = false
}
}
for _, name := range c.FlagNames() {
getter, ok := c.Generic(name).(flag.Getter)
if ok {
driverOpts.Values[name] = getter.Get()
} else {
// TODO: This is pretty hacky. StringSlice is the only
// type so far we have to worry about which is not a
// Getter, though.
if c.IsSet(name) {
driverOpts.Values[name] = c.StringSlice(name)
}
}
}
return driverOpts
}
func convertMcnFlagsToCliFlags(mcnFlags []mcnflag.Flag) ([]cli.Flag, error) {
cliFlags := []cli.Flag{}
for _, f := range mcnFlags {
switch t := f.(type) {
// TODO: It seems pretty wrong to just default "nil" to this,
// but cli.BoolFlag doesn't have a "Value" field (false is
// always the default)
case *mcnflag.BoolFlag:
f := f.(*mcnflag.BoolFlag)
cliFlags = append(cliFlags, cli.BoolFlag{
Name: f.Name,
EnvVar: f.EnvVar,
Usage: f.Usage,
})
case *mcnflag.IntFlag:
f := f.(*mcnflag.IntFlag)
cliFlags = append(cliFlags, cli.IntFlag{
Name: f.Name,
EnvVar: f.EnvVar,
Usage: f.Usage,
Value: f.Value,
})
case *mcnflag.StringFlag:
f := f.(*mcnflag.StringFlag)
cliFlags = append(cliFlags, cli.StringFlag{
Name: f.Name,
EnvVar: f.EnvVar,
Usage: f.Usage,
Value: f.Value,
})
case *mcnflag.StringSliceFlag:
f := f.(*mcnflag.StringSliceFlag)
cliFlags = append(cliFlags, cli.StringSliceFlag{
Name: f.Name,
EnvVar: f.EnvVar,
Usage: f.Usage,
//TODO: Is this used with defaults? Can we convert the literal []string to cli.StringSlice properly?
Value: &cli.StringSlice{},
})
default:
log.Warn("Flag is ", f)
return nil, fmt.Errorf("Flag is unrecognized flag type: %T", t)
}
}
return cliFlags, nil
}
func addDriverFlagsToCommand(cliFlags []cli.Flag, cmd *cli.Command) *cli.Command {
cmd.Flags = append(SharedCreateFlags, cliFlags...)
cmd.SkipFlagParsing = false
cmd.Action = runCommand(cmdCreateInner)
sort.Sort(ByFlagName(cmd.Flags))
return cmd
}
func validateSwarmDiscovery(discovery string) error {
if discovery == "" {
return nil
}
matched, err := regexp.MatchString(`[^:]*://.*`, discovery)
if err != nil {
return err
}
if matched {
return nil
}
return fmt.Errorf("Swarm Discovery URL was in the wrong format: %s", discovery)
}
func tlsPath(c CommandLine, flag string, defaultName string) string {
path := c.GlobalString(flag)
if path != "" {
return path
}
return filepath.Join(mcndirs.GetMachineCertDir(), defaultName)
}