diff --git a/circle.yml b/circle.yml index 3d8f45b8a1..06cfbdf06a 100644 --- a/circle.yml +++ b/circle.yml @@ -1,11 +1,9 @@ -# Pony-up! machine: pre: # Install gvm - bash < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/1.0.22/binscripts/gvm-installer) # Install bats - git clone https://github.com/sstephenson/bats.git && cd bats && sudo ./install.sh /usr/local - # - sudo apt-get install -y virtualbox post: - gvm install go1.5.2 -B --name=stable @@ -16,8 +14,6 @@ machine: BASE_DIR: src/github.com/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME # Trick circle brainflat "no absolute path" behavior BASE_STABLE: ../../../$HOME/.gvm/pkgsets/stable/global/$BASE_DIR - # BASE_BLEED: ../../../$HOME/.gvm/pkgsets/bleed/global/$BASE_DIR - BUILDTAGS: "" dependencies: override: @@ -31,23 +27,12 @@ test: - gvm use stable && go version - gvm use stable && make build: pwd: $BASE_STABLE + - gvm use stable && GO15VENDOREXPERIMENT=1 go get github.com/docker/docker-machine-driver-ci-test override: - DRIVER=none test/integration/run-bats.sh test/integration/cli: pwd: $BASE_STABLE timeout: 600 - # - DRIVER=virtualbox test/integration/run-bats.sh test/integration/core: - # pwd: $BASE_STABLE - # timeout: 1200 - # - DRIVER=digitalocean test/integration/run-bats.sh test/integration/core: - # pwd: $BASE_STABLE - # timeout: 1200 - # - DRIVER=azure test/integration/run-bats.sh test/integration/core: - # pwd: $BASE_STABLE - # timeout: 1200 - # - DRIVER=amazonec2 test/integration/run-bats.sh test/integration/core: - # pwd: $BASE_STABLE - # timeout: 1200 - - # - gvm use stable && DRIVER=amazonec2 test/integration/run-bats.sh test/integration/core: - # pwd: $BASE_STABLE + - PATH=../../../../bin:$PATH DRIVER=ci-test test/integration/run-bats.sh test/integration/3rdparty: + pwd: $BASE_STABLE + timeout: 600 diff --git a/docs/drivers/gce.md b/docs/drivers/gce.md index f071ca4c9d..60cdb24c81 100644 --- a/docs/drivers/gce.md +++ b/docs/drivers/gce.md @@ -38,7 +38,7 @@ To create a machine instance, specify `--driver google`, the project id and the ### Options -- `--google-project`: **required** The id of your project to use when launching the instance. + - `--google-project`: **required** The id of your project to use when launching the instance. - `--google-zone`: The zone to launch the instance. - `--google-machine-type`: The type of instance. - `--google-machine-image`: The absolute URL to a base VM image to instantiate. @@ -50,6 +50,7 @@ To create a machine instance, specify `--driver google`, the project id and the - `--google-preemptible`: Instance preemptibility. - `--google-tags`: Instance tags (comma-separated). - `--google-use-internal-ip`: When this option is used during create it will make docker-machine use internal rather than public NATed IPs. The flag is persistent in the sense that a machine created with it retains the IP. It's useful for managing docker machines from another machine on the same network e.g. while deploying swarm. + - `--google-use-existing`: Don't create a new VM, use an existing one. This is useful when you'd like to provision Docker on a VM you created yourself, maybe because it uses create options not supported by this driver. The GCE driver will use the `ubuntu-1510-wily-v20151114` instance image unless otherwise specified. To obtain a list of image URLs run: @@ -72,3 +73,4 @@ Environment variables and default values: | `--google-preemptible` | `GOOGLE_PREEMPTIBLE` | - | | `--google-tags` | `GOOGLE_TAGS` | - | | `--google-use-internal-ip` | `GOOGLE_USE_INTERNAL_IP` | - | +| `--google-use-existing` | `GOOGLE_USE_EXISTING` | - | diff --git a/drivers/google/compute_util.go b/drivers/google/compute_util.go index 87a4768d0c..6b50f6b7c9 100644 --- a/drivers/google/compute_util.go +++ b/drivers/google/compute_util.go @@ -88,6 +88,11 @@ func (c *ComputeUtil) disk() (*raw.Disk, error) { // deleteDisk deletes the persistent disk. func (c *ComputeUtil) deleteDisk() error { + disk, _ := c.disk() + if disk == nil { + return nil + } + log.Infof("Deleting disk.") op, err := c.service.Disks.Delete(c.project, c.zone, c.diskName()).Do() if err != nil { @@ -162,8 +167,9 @@ func (c *ComputeUtil) portsUsed() ([]string, error) { return ports, nil } -func (c *ComputeUtil) createFirewallRule() error { - log.Infof("Opening firewall ports.") +// openFirewallPorts configures the firewall to open docker and swarm ports. +func (c *ComputeUtil) openFirewallPorts() error { + log.Infof("Opening firewall ports") create := false rule, _ := c.firewallRule() @@ -213,11 +219,7 @@ func (c *ComputeUtil) instance() (*raw.Instance, error) { // createInstance creates a GCE VM instance. func (c *ComputeUtil) createInstance(d *Driver) error { - log.Infof("Creating instance.") - - if err := c.createFirewallRule(); err != nil { - return err - } + log.Infof("Creating instance") instance := &raw.Instance{ Name: c.instanceName, @@ -280,7 +282,7 @@ func (c *ComputeUtil) createInstance(d *Driver) error { return err } - log.Infof("Waiting for Instance...") + log.Infof("Waiting for Instance") if err = c.waitForRegionalOp(op.Name); err != nil { return err } @@ -290,15 +292,58 @@ func (c *ComputeUtil) createInstance(d *Driver) error { return err } - // Update the SSH Key - sshKey, err := ioutil.ReadFile(d.GetSSHKeyPath() + ".pub") + return c.uploadSSHKey(instance, d.GetSSHKeyPath()) +} + +// configureInstance configures an existing instance for use with Docker Machine. +func (c *ComputeUtil) configureInstance(d *Driver) error { + log.Infof("Configuring instance") + + instance, err := c.instance() if err != nil { return err } + if err := c.addFirewallTag(instance); err != nil { + return err + } + + return c.uploadSSHKey(instance, d.GetSSHKeyPath()) +} + +// addFirewallTag adds a tag to the instance to match the firewall rule. +func (c *ComputeUtil) addFirewallTag(instance *raw.Instance) error { + log.Infof("Adding tag for the firewall rule") + + tags := instance.Tags + for _, tag := range tags.Items { + if tag == firewallTargetTag { + return nil + } + } + + tags.Items = append(tags.Items, firewallTargetTag) + + op, err := c.service.Instances.SetTags(c.project, c.zone, instance.Name, tags).Do() + if err != nil { + return err + } + + return c.waitForRegionalOp(op.Name) +} + +// uploadSSHKey updates the instance metadata with the given ssh key. +func (c *ComputeUtil) uploadSSHKey(instance *raw.Instance, sshKeyPath string) error { log.Infof("Uploading SSH Key") + + sshKey, err := ioutil.ReadFile(sshKeyPath + ".pub") + if err != nil { + return err + } + metaDataValue := fmt.Sprintf("%s:%s %s\n", c.userName, strings.TrimSpace(string(sshKey)), c.userName) - op, err = c.service.Instances.SetMetadata(c.project, c.zone, c.instanceName, &raw.Metadata{ + + op, err := c.service.Instances.SetMetadata(c.project, c.zone, c.instanceName, &raw.Metadata{ Fingerprint: instance.Metadata.Fingerprint, Items: []*raw.MetadataItems{ { @@ -307,10 +352,6 @@ func (c *ComputeUtil) createInstance(d *Driver) error { }, }, }).Do() - if err != nil { - return err - } - log.Infof("Waiting for SSH Key") return c.waitForRegionalOp(op.Name) } @@ -360,6 +401,7 @@ func (c *ComputeUtil) startInstance() error { return c.waitForRegionalOp(op.Name) } +// waitForOp waits for the operation to finish. func (c *ComputeUtil) waitForOp(opGetter func() (*raw.Operation, error)) error { for { op, err := opGetter() @@ -367,7 +409,7 @@ func (c *ComputeUtil) waitForOp(opGetter func() (*raw.Operation, error)) error { return err } - log.Debugf("operation %q status: %s", op.Name, op.Status) + log.Debugf("Operation %q status: %s", op.Name, op.Status) if op.Status == "DONE" { if op.Error != nil { return fmt.Errorf("Operation error: %v", *op.Error.Errors[0]) @@ -379,7 +421,7 @@ func (c *ComputeUtil) waitForOp(opGetter func() (*raw.Operation, error)) error { return nil } -// waitForOp waits for the operation to finish. +// waitForRegionalOp waits for the regional operation to finish. func (c *ComputeUtil) waitForRegionalOp(name string) error { return c.waitForOp(func() (*raw.Operation, error) { return c.service.ZoneOperations.Get(c.project, c.zone, name).Do() diff --git a/drivers/google/google.go b/drivers/google/google.go index efdac41f29..ddaf59eb06 100644 --- a/drivers/google/google.go +++ b/drivers/google/google.go @@ -26,6 +26,7 @@ type Driver struct { DiskSize int Project string Tags string + UseExisting bool } const ( @@ -110,6 +111,11 @@ func (d *Driver) GetCreateFlags() []mcnflag.Flag { Usage: "Use internal GCE Instance IP rather than public one", EnvVar: "GOOGLE_USE_INTERNAL_IP", }, + mcnflag.BoolFlag{ + Name: "google-use-existing", + Usage: "Don't create a new VM, use an existing one", + EnvVar: "GOOGLE_USE_EXISTING", + }, } } @@ -156,15 +162,18 @@ func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error { } d.Zone = flags.String("google-zone") - d.MachineType = flags.String("google-machine-type") - d.MachineImage = flags.String("google-machine-image") - d.DiskSize = flags.Int("google-disk-size") - d.DiskType = flags.String("google-disk-type") - d.Address = flags.String("google-address") - d.Preemptible = flags.Bool("google-preemptible") - d.UseInternalIP = flags.Bool("google-use-internal-ip") - d.Scopes = flags.String("google-scopes") - d.Tags = flags.String("google-tags") + d.UseExisting = flags.Bool("google-use-existing") + if !d.UseExisting { + d.MachineType = flags.String("google-machine-type") + d.MachineImage = flags.String("google-machine-image") + d.DiskSize = flags.Int("google-disk-size") + d.DiskType = flags.String("google-disk-type") + d.Address = flags.String("google-address") + d.Preemptible = flags.Bool("google-preemptible") + d.UseInternalIP = flags.Bool("google-use-internal-ip") + d.Scopes = flags.String("google-scopes") + d.Tags = flags.String("google-tags") + } d.SSHUser = flags.String("google-username") d.SSHPort = 22 d.SetSwarmConfigFromFlags(flags) @@ -191,8 +200,15 @@ func (d *Driver) PreCreateCheck() error { // doesn't exist, so just check instance for nil. log.Infof("Check if the instance already exists") - if instance, _ := c.instance(); instance != nil { - return fmt.Errorf("Instance %v already exists.", d.MachineName) + instance, _ := c.instance() + if d.UseExisting { + if instance == nil { + return fmt.Errorf("Unable to find instance %q in zone %q.", d.MachineName, d.Zone) + } + } else { + if instance != nil { + return fmt.Errorf("Instance %q already exists in zone %q.", d.MachineName, d.Zone) + } } return nil @@ -213,6 +229,13 @@ func (d *Driver) Create() error { return err } + if err := c.openFirewallPorts(); err != nil { + return err + } + + if d.UseExisting { + return c.configureInstance(d) + } return c.createInstance(d) } diff --git a/libmachine/libmachine.go b/libmachine/libmachine.go index d287516932..e414d711fa 100644 --- a/libmachine/libmachine.go +++ b/libmachine/libmachine.go @@ -160,38 +160,39 @@ func (api *Client) performCreate(h *host.Host) error { return fmt.Errorf("Error saving host to store after attempting creation: %s", err) } - // TODO: Not really a fan of just checking "none" here. - if h.Driver.DriverName() != "none" { - log.Info("Waiting for machine to be running, this may take a few minutes...") - if err := mcnutils.WaitFor(drivers.MachineInState(h.Driver, state.Running)); err != nil { - return fmt.Errorf("Error waiting for machine to be running: %s", err) - } - - log.Info("Machine is running, waiting for SSH to be available...") - if err := drivers.WaitForSSH(h.Driver); err != nil { - return fmt.Errorf("Error waiting for SSH: %s", err) - } - - log.Info("Detecting operating system of created instance...") - provisioner, err := provision.DetectProvisioner(h.Driver) - if err != nil { - return fmt.Errorf("Error detecting OS: %s", err) - } - - log.Infof("Provisioning with %s...", provisioner.String()) - if err := provisioner.Provision(*h.HostOptions.SwarmOptions, *h.HostOptions.AuthOptions, *h.HostOptions.EngineOptions); err != nil { - return fmt.Errorf("Error running provisioning: %s", err) - } - - // We should check the connection to docker here - log.Info("Checking connection to Docker...") - if _, _, err = check.DefaultConnChecker.Check(h, false); err != nil { - return fmt.Errorf("Error checking the host: %s", err) - } - - log.Info("Docker is up and running!") + // TODO: Not really a fan of just checking "none" or "ci-test" here. + if h.Driver.DriverName() == "none" || h.Driver.DriverName() == "ci-test" { + return nil } + log.Info("Waiting for machine to be running, this may take a few minutes...") + if err := mcnutils.WaitFor(drivers.MachineInState(h.Driver, state.Running)); err != nil { + return fmt.Errorf("Error waiting for machine to be running: %s", err) + } + + log.Info("Machine is running, waiting for SSH to be available...") + if err := drivers.WaitForSSH(h.Driver); err != nil { + return fmt.Errorf("Error waiting for SSH: %s", err) + } + + log.Info("Detecting operating system of created instance...") + provisioner, err := provision.DetectProvisioner(h.Driver) + if err != nil { + return fmt.Errorf("Error detecting OS: %s", err) + } + + log.Infof("Provisioning with %s...", provisioner.String()) + if err := provisioner.Provision(*h.HostOptions.SwarmOptions, *h.HostOptions.AuthOptions, *h.HostOptions.EngineOptions); err != nil { + return fmt.Errorf("Error running provisioning: %s", err) + } + + // We should check the connection to docker here + log.Info("Checking connection to Docker...") + if _, _, err = check.DefaultConnChecker.Check(h, false); err != nil { + return fmt.Errorf("Error checking the host: %s", err) + } + + log.Info("Docker is up and running!") return nil } diff --git a/test/integration/3rdparty/commands.bats b/test/integration/3rdparty/commands.bats new file mode 100644 index 0000000000..117adb1a68 --- /dev/null +++ b/test/integration/3rdparty/commands.bats @@ -0,0 +1,34 @@ +#!/usr/bin/env bats + +load ${BASE_TEST_DIR}/helpers.bash + +only_if_env DRIVER ci-test + +@test "create" { + run machine create -d $DRIVER --url none default + [ "$status" -eq 0 ] +} + +@test "ls" { + run machine ls -q + [ "$status" -eq 0 ] + [[ ${#lines[@]} == 1 ]] + [[ ${lines[0]} = "default" ]] +} + +@test "url" { + run machine url default + [ "$status" -eq 0 ] + [[ ${output} == *"none"* ]] +} + +@test "status" { + run machine status default + [ "$status" -eq 0 ] + [[ ${output} == *"Running"* ]] +} + +@test "rm" { + run machine rm -y default + [ "$status" -eq 0 ] +}