mirror of https://github.com/docker/docs.git
466 lines
13 KiB
Go
466 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 to join a Swarm cluster",
|
|
},
|
|
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 master",
|
|
Value: &cli.StringSlice{},
|
|
},
|
|
cli.StringSliceFlag{
|
|
Name: "swarm-join-opt",
|
|
Usage: "Define arbitrary flags for Swarm join",
|
|
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") || c.Bool("swarm-master"),
|
|
Image: c.String("swarm-image"),
|
|
Agent: c.Bool("swarm"),
|
|
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"),
|
|
ArbitraryJoinFlags: c.StringSlice("swarm-join-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)
|
|
}
|