providers/docker: Import code from https://github.com/fgrehm/docker-provider with some initial tweaks
This commit is contained in:
parent
efc1122c42
commit
5a60e568ce
|
@ -0,0 +1,206 @@
|
|||
# docker-provider
|
||||
|
||||
[![Build Status](https://travis-ci.org/fgrehm/docker-provider.png?branch=master)](https://travis-ci.org/fgrehm/docker-provider) [![Gem Version](https://badge.fury.io/rb/docker-provider.png)](http://badge.fury.io/rb/docker-provider) [![Gittip](http://img.shields.io/gittip/fgrehm.svg)](https://www.gittip.com/fgrehm/)
|
||||
|
||||
A [Docker](http://www.docker.io/) provider for [Vagrant](http://www.vagrantup.com/)
|
||||
1.4+.
|
||||
|
||||
|
||||
## Warning
|
||||
|
||||
This is experimental, expect things to break.
|
||||
|
||||
|
||||
## Requirements
|
||||
|
||||
* Vagrant 1.4+
|
||||
* Docker 0.7.0+
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
* Support for Vagrant's `up`, `destroy`, `halt`, `reload` and `ssh` commands
|
||||
* Port forwarding
|
||||
* Synced / shared folders support
|
||||
* Set container hostnames from Vagrantfiles
|
||||
* Provision Docker containers with any built-in Vagrant provisioner (as long as the container has a SSH server running)
|
||||
|
||||
You can see the plugin in action by watching the following asciicasts I published
|
||||
prior to releasing 0.0.1:
|
||||
|
||||
* http://asciinema.org/a/6162
|
||||
* http://asciinema.org/a/6177
|
||||
|
||||
|
||||
## Getting started
|
||||
|
||||
If you are on a Mac / Windows machine, please fire up a x64 Linux VM with Docker +
|
||||
Vagrant 1.4+ installed or use [this Vagrantfile](https://gist.github.com/fgrehm/fc48fb51ec7df64439e4)
|
||||
and follow the instructions from within the VM.
|
||||
|
||||
_It is likely that the plugin works with [boot2docker](http://boot2docker.github.io/)
|
||||
but I personally haven't tried that yet. If you are able to give it a go please
|
||||
[let me know](https://github.com/fgrehm/docker-provider/issues/new)._
|
||||
|
||||
### Initial setup
|
||||
|
||||
_If you are trying things out from a Vagrant VM using the `Vagrantfile` gisted
|
||||
above, you can skip to the next section_
|
||||
|
||||
The plugin requires Docker's executable to be available on current user's `PATH`
|
||||
and that the current user has been added to the `docker` group since we are not
|
||||
using `sudo` when interacting with Docker's CLI. For more information on setting
|
||||
this up please check [this page](http://docs.docker.io/en/latest/installation/ubuntulinux/#giving-non-root-access).
|
||||
|
||||
### `vagrant up`
|
||||
|
||||
On its current state, the plugin is not "user friendly" and won't provide any kind
|
||||
of feedback about the process of downloading Docker images, so before you add a
|
||||
`docker-provider` [base box](http://docs.vagrantup.com/v2/boxes.html) it is recommended
|
||||
that you `docker pull` the associated base box images prior to spinning up `docker-provider`
|
||||
containers (otherwise you'll be staring at a blinking cursor without any progress
|
||||
information for a while).
|
||||
|
||||
Assuming you have Vagrant 1.4+ and Docker 0.7.0+ installed just sing that same
|
||||
old song:
|
||||
|
||||
```sh
|
||||
vagrant plugin install docker-provider
|
||||
docker pull fgrehm/vagrant-ubuntu:precise
|
||||
vagrant box add precise64 http://bit.ly/vagrant-docker-precise
|
||||
vagrant init precise64
|
||||
vagrant up --provider=docker
|
||||
```
|
||||
|
||||
Under the hood, that base box will [configure](#configuration) `docker-provider`
|
||||
to use the [`fgrehm/vagrant-ubuntu:precise`](https://index.docker.io/u/fgrehm/vagrant-ubuntu/)
|
||||
image that approximates a standard Vagrant box (`vagrant` user, default SSH key,
|
||||
etc.) and you should be good to go.
|
||||
|
||||
|
||||
## Using custom images
|
||||
|
||||
If you want to use a custom Docker image without creating a Vagrant base box,
|
||||
you can use a "dummy" box and configure things from your `Vagrantfile` like
|
||||
in [vagrant-digitalocean](https://github.com/smdahlen/vagrant-digitalocean#configure)
|
||||
or [vagrant-aws](https://github.com/mitchellh/vagrant-aws#quick-start):
|
||||
|
||||
```ruby
|
||||
Vagrant.configure("2") do |config|
|
||||
config.vm.box = "dummy"
|
||||
config.vm.box_url = "http://bit.ly/vagrant-docker-dummy"
|
||||
config.vm.provider :docker do |docker|
|
||||
docker.image = "your/image:tag"
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
This provider exposes a few provider-specific configuration options
|
||||
that are passed on to `docker run` under the hood when the container
|
||||
is being created:
|
||||
|
||||
* `image` - Docker image to run (required)
|
||||
* `privileged` - Give extended privileges to the container (defaults to false)
|
||||
* `cmd` - An array of strings that makes up for the command to run the container (defaults to what has been set on your `Dockerfile` as `CMD` or `ENTRYPOINT`)
|
||||
* `ports` - An array of strings that makes up for the mapped network ports
|
||||
* `volumes` - An array of strings that makes up for the data volumes used by the container
|
||||
|
||||
These can be set like typical provider-specific configuration:
|
||||
|
||||
```ruby
|
||||
Vagrant.configure("2") do |config|
|
||||
# ... other stuff
|
||||
|
||||
config.vm.provider :docker do |docker|
|
||||
docker.image = 'fgrehm/vagrant-ubuntu-dind:precise'
|
||||
docker.privileged = true
|
||||
docker.cmd = ['/dind', '/sbin/init']
|
||||
|
||||
docker.ports << '1234:22'
|
||||
docker.volumes << '/var/lib/docker'
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
|
||||
## Networks
|
||||
|
||||
Networking features in the form of `config.vm.network` are not supported with
|
||||
`docker-provider` apart from [forwarded ports]().
|
||||
If any of [`:private_network`](http://docs.vagrantup.com/v2/networking/private_network.html)
|
||||
or [`:public_network`](http://docs.vagrantup.com/v2/networking/public_network.html)
|
||||
are specified, Vagrant **won't** emit a warning.
|
||||
|
||||
The same applies to changes on forwarded ports after the container has been
|
||||
created, Vagrant **won't** emit a warning to let you know that the ports specified
|
||||
on your `Vagrantfile` differs from what has been passed on to `docker run` when
|
||||
creating the container.
|
||||
|
||||
_At some point the plugin will emit warnings on the scenarios described above, but
|
||||
not on its current state. Pull Requests are encouraged ;)_
|
||||
|
||||
|
||||
## Synced Folders
|
||||
|
||||
There is support for synced folders on the form of [Docker volumes](http://docs.docker.io/en/latest/use/working_with_volumes/#mount-a-host-directory-as-a-container-volume)
|
||||
but as with forwarded ports, you won't be able to change them after the container
|
||||
has been created. [NFS](http://docs.vagrantup.com/v2/synced-folders/nfs.html)
|
||||
synced folders are also supported (as long as you set the `privileged`
|
||||
[config](#configuration) to true so that `docker-provider` can mount it on the
|
||||
guest container) and are capable of being reconfigured between `vagrant reload`s
|
||||
(different from Docker volumes).
|
||||
|
||||
This is good enough for all built-in Vagrant provisioners (shell,
|
||||
chef, and puppet) to work!
|
||||
|
||||
_At some point the plugin will emit warnings when the configured `Vagrantfile`
|
||||
synced folders / volumes differs from the ones used upon the container creation,
|
||||
but not on its current state. Pull Requests are encouraged ;)_
|
||||
|
||||
|
||||
## Box format
|
||||
|
||||
Every provider in Vagrant must introduce a custom box format. This provider introduces
|
||||
`docker` boxes and you can view some examples in the [`boxes`](boxes) directory.
|
||||
That directory also contains instructions on how to build them.
|
||||
|
||||
The box format is basically just the required `metadata.json` file along with a
|
||||
`Vagrantfile` that does default settings for the provider-specific configuration
|
||||
for this provider.
|
||||
|
||||
|
||||
## Available base boxes
|
||||
|
||||
| LINK | DESCRIPTION |
|
||||
| --- | --- |
|
||||
| http://bit.ly/vagrant-docker-precise | Ubuntu 12.04 Precise x86_64 with Puppet and Chef preinstalled and configured to run `/sbin/init` |
|
||||
| http://bit.ly/vagrant-docker-precise-dind | Ubuntu 12.04 Precise x86_64 based on the box above and ready to run [DinD](https://github.com/jpetazzo/dind) |
|
||||
|
||||
|
||||
## Limitations
|
||||
|
||||
As explained on the [networks](#networks) and [synced folder](#synced-folders)
|
||||
sections above, there are some "gotchas" when using the plugin that you need to have
|
||||
in mind before you start to pull your hair out.
|
||||
|
||||
For instance, forwarded ports, synced folders and containers' hostnames will not be
|
||||
reconfigured on `vagrant reload`s if they have changed and the plugin **_will not
|
||||
give you any kind of warning or message_**. As an example, if you change your Puppet
|
||||
manifests / Chef cookbooks paths (which are shared / synced folders under the hood),
|
||||
**_you'll need to start from scratch_** (unless you make them NFS shared folders).
|
||||
This is due to a limitation in Docker itself as we can't change those parameters
|
||||
after the container has been created.
|
||||
|
||||
Forwarded ports automatic collision handling is **_not supported as well_**.
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork it
|
||||
2. Create your feature branch (`git checkout -b my-new-feature`)
|
||||
3. Commit your changes (`git commit -am 'Add some feature'`)
|
||||
4. Push to the branch (`git push origin my-new-feature`)
|
||||
5. Create new Pull Request
|
|
@ -0,0 +1,175 @@
|
|||
module VagrantPlugins
|
||||
module DockerProvider
|
||||
module Action
|
||||
# Shortcuts
|
||||
Builtin = Vagrant::Action::Builtin
|
||||
Builder = Vagrant::Action::Builder
|
||||
|
||||
# This action brings the "machine" up from nothing, including creating the
|
||||
# container, configuring metadata, and booting.
|
||||
def self.action_up
|
||||
Builder.new.tap do |b|
|
||||
b.use Builtin::ConfigValidate
|
||||
b.use Builtin::Call, Created do |env, b2|
|
||||
if !env[:result]
|
||||
b2.use Builtin::HandleBoxUrl
|
||||
# TODO: Find out where this fits into the process
|
||||
# b2.use Builtin::EnvSet, :port_collision_repair => true
|
||||
# b2.use Builtin::HandleForwardedPortCollisions
|
||||
b2.use Builtin::Provision
|
||||
b2.use PrepareNFSValidIds
|
||||
b2.use Builtin::SyncedFolderCleanup
|
||||
b2.use Builtin::SyncedFolders
|
||||
b2.use PrepareNFSSettings
|
||||
b2.use ForwardPorts
|
||||
# This will actually create and start, but that's fine
|
||||
b2.use Create
|
||||
b2.use action_boot
|
||||
else
|
||||
b2.use PrepareNFSValidIds
|
||||
b2.use Builtin::SyncedFolderCleanup
|
||||
b2.use Builtin::SyncedFolders
|
||||
b2.use PrepareNFSSettings
|
||||
b2.use action_start
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# This action just runs the provisioners on the machine.
|
||||
def self.action_provision
|
||||
Builder.new.tap do |b|
|
||||
b.use Builtin::ConfigValidate
|
||||
b.use Builtin::Call, Created do |env1, b2|
|
||||
if !env1[:result]
|
||||
b2.use Message, :not_created
|
||||
next
|
||||
end
|
||||
|
||||
b2.use Builtin::Call, IsRunning do |env2, b3|
|
||||
if !env2[:result]
|
||||
b3.use Message, :not_running
|
||||
next
|
||||
end
|
||||
|
||||
b3.use Builtin::Provision
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# This is the action that is primarily responsible for halting
|
||||
# the virtual machine, gracefully or by force.
|
||||
def self.action_halt
|
||||
Builder.new.tap do |b|
|
||||
b.use Builtin::Call, Created do |env, b2|
|
||||
if env[:result]
|
||||
b2.use Builtin::Call, Builtin::GracefulHalt, :stopped, :running do |env2, b3|
|
||||
if !env2[:result]
|
||||
b3.use Stop
|
||||
end
|
||||
end
|
||||
else
|
||||
b2.use Message, :not_created
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# This action is responsible for reloading the machine, which
|
||||
# brings it down, sucks in new configuration, and brings the
|
||||
# machine back up with the new configuration.
|
||||
def self.action_reload
|
||||
Builder.new.tap do |b|
|
||||
b.use Builtin::Call, Created do |env1, b2|
|
||||
if !env1[:result]
|
||||
b2.use Message, :not_created
|
||||
next
|
||||
end
|
||||
|
||||
b2.use Builtin::ConfigValidate
|
||||
b2.use action_halt
|
||||
b2.use action_start
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# This is the action that is primarily responsible for completely
|
||||
# freeing the resources of the underlying virtual machine.
|
||||
def self.action_destroy
|
||||
Builder.new.tap do |b|
|
||||
b.use Builtin::Call, Created do |env1, b2|
|
||||
if !env1[:result]
|
||||
b2.use Message, :not_created
|
||||
next
|
||||
end
|
||||
|
||||
b2.use Builtin::Call, Builtin::DestroyConfirm do |env2, b3|
|
||||
if env2[:result]
|
||||
b3.use Builtin::ConfigValidate
|
||||
b3.use Builtin::EnvSet, :force_halt => true
|
||||
b3.use action_halt
|
||||
b3.use Destroy
|
||||
b3.use Builtin::ProvisionerCleanup
|
||||
else
|
||||
b3.use Message, :will_not_destroy
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# This is the action that will exec into an SSH shell.
|
||||
def self.action_ssh
|
||||
Builder.new.tap do |b|
|
||||
b.use CheckRunning
|
||||
b.use Builtin::SSHExec
|
||||
end
|
||||
end
|
||||
|
||||
# This is the action that will run a single SSH command.
|
||||
def self.action_ssh_run
|
||||
Builder.new.tap do |b|
|
||||
b.use CheckRunning
|
||||
b.use Builtin::SSHRun
|
||||
end
|
||||
end
|
||||
|
||||
def self.action_start
|
||||
Builder.new.tap do |b|
|
||||
b.use Builtin::ConfigValidate
|
||||
b.use Builtin::Call, IsRunning do |env, b2|
|
||||
# If the container is running, then our work here is done, exit
|
||||
next if env[:result]
|
||||
|
||||
b2.use Builtin::Provision
|
||||
b2.use Message, :starting
|
||||
b2.use action_boot
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.action_boot
|
||||
Builder.new.tap do |b|
|
||||
# TODO: b.use Builtin::SetHostname
|
||||
b.use Start
|
||||
b.use Builtin::WaitForCommunicator
|
||||
end
|
||||
end
|
||||
|
||||
# The autoload farm
|
||||
action_root = Pathname.new(File.expand_path("../action", __FILE__))
|
||||
autoload :CheckRunning, action_root.join("check_running")
|
||||
autoload :Created, action_root.join("created")
|
||||
autoload :Create, action_root.join("create")
|
||||
autoload :Destroy, action_root.join("destroy")
|
||||
autoload :ForwardPorts, action_root.join("forward_ports")
|
||||
autoload :Stop, action_root.join("stop")
|
||||
autoload :Message, action_root.join("message")
|
||||
autoload :PrepareNFSValidIds, action_root.join("prepare_nfs_valid_ids")
|
||||
autoload :PrepareNFSSettings, action_root.join("prepare_nfs_settings")
|
||||
autoload :IsRunning, action_root.join("is_running")
|
||||
autoload :Start, action_root.join("start")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,25 @@
|
|||
module VagrantPlugins
|
||||
module DockerProvider
|
||||
module Action
|
||||
class CheckRunning
|
||||
def initialize(app, env)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
if env[:machine].state.id == :not_created
|
||||
raise Vagrant::Errors::VMNotCreatedError
|
||||
end
|
||||
|
||||
if env[:machine].state.id == :stopped
|
||||
raise Vagrant::Errors::VMNotRunningError
|
||||
end
|
||||
|
||||
# Call the next if we have one (but we shouldn't, since this
|
||||
# middleware is built to run with the Call-type middlewares)
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,59 @@
|
|||
module VagrantPlugins
|
||||
module DockerProvider
|
||||
module Action
|
||||
class Create
|
||||
def initialize(app, env)
|
||||
@app = app
|
||||
@@mutex ||= Mutex.new
|
||||
end
|
||||
|
||||
def call(env)
|
||||
@env = env
|
||||
@machine = env[:machine]
|
||||
@provider_config = @machine.provider_config
|
||||
@machine_config = @machine.config
|
||||
@driver = @machine.provider.driver
|
||||
|
||||
guard_cmd_configured!
|
||||
|
||||
cid = ''
|
||||
@@mutex.synchronize do
|
||||
cid = @driver.create(create_params)
|
||||
end
|
||||
|
||||
@machine.id = cid
|
||||
@app.call(env)
|
||||
end
|
||||
|
||||
def create_params
|
||||
container_name = "#{@env[:root_path].basename.to_s}_#{@machine.name}"
|
||||
container_name.gsub!(/[^-a-z0-9_]/i, "")
|
||||
container_name << "_#{Time.now.to_i}"
|
||||
|
||||
{
|
||||
image: @provider_config.image,
|
||||
cmd: @provider_config.cmd,
|
||||
ports: forwarded_ports,
|
||||
name: container_name,
|
||||
hostname: @machine_config.vm.hostname,
|
||||
volumes: @provider_config.volumes,
|
||||
privileged: @provider_config.privileged
|
||||
}
|
||||
end
|
||||
|
||||
def forwarded_ports
|
||||
@env[:forwarded_ports].map do |fp|
|
||||
# TODO: Support for the protocol argument
|
||||
"#{fp[:host]}:#{fp[:guest]}"
|
||||
end.compact
|
||||
end
|
||||
|
||||
def guard_cmd_configured!
|
||||
if ! @provider_config.image
|
||||
raise Errors::ImageNotConfiguredError, name: @machine.name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,18 @@
|
|||
module VagrantPlugins
|
||||
module DockerProvider
|
||||
module Action
|
||||
class Created
|
||||
def initialize(app, env)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
machine = env[:machine]
|
||||
driver = machine.provider.driver
|
||||
env[:result] = machine.id && driver.created?(machine.id)
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,24 @@
|
|||
module VagrantPlugins
|
||||
module DockerProvider
|
||||
module Action
|
||||
class Destroy
|
||||
def initialize(app, env)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
env[:ui].info I18n.t("vagrant.actions.vm.destroy.destroying")
|
||||
|
||||
machine = env[:machine]
|
||||
config = machine.provider_config
|
||||
driver = machine.provider.driver
|
||||
|
||||
driver.rm(machine.id)
|
||||
machine.id = nil
|
||||
|
||||
@app.call env
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,54 @@
|
|||
module VagrantPlugins
|
||||
module DockerProvider
|
||||
module Action
|
||||
class ForwardPorts
|
||||
def initialize(app, env)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
@env = env
|
||||
|
||||
env[:forwarded_ports] = compile_forwarded_ports(env[:machine].config)
|
||||
|
||||
if env[:forwarded_ports].any?
|
||||
env[:ui].info I18n.t("vagrant.actions.vm.forward_ports.forwarding")
|
||||
inform_forwarded_ports(env[:forwarded_ports])
|
||||
end
|
||||
|
||||
# FIXME: Check whether the container has already been created with
|
||||
# different exposed ports and let the user know about it
|
||||
|
||||
@app.call env
|
||||
end
|
||||
|
||||
def inform_forwarded_ports(ports)
|
||||
ports.each do |fp|
|
||||
message_attributes = {
|
||||
:adapter => 'eth0',
|
||||
:guest_port => fp[:guest],
|
||||
:host_port => fp[:host]
|
||||
}
|
||||
|
||||
@env[:ui].info(I18n.t("vagrant.actions.vm.forward_ports.forwarding_entry",
|
||||
message_attributes))
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def compile_forwarded_ports(config)
|
||||
mappings = {}
|
||||
|
||||
config.vm.networks.each do |type, options|
|
||||
if type == :forwarded_port && options[:id] != 'ssh'
|
||||
mappings[options[:host]] = options
|
||||
end
|
||||
end
|
||||
|
||||
mappings.values
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,20 @@
|
|||
module VagrantPlugins
|
||||
module DockerProvider
|
||||
module Action
|
||||
class IsRunning
|
||||
def initialize(app, env)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
machine = env[:machine]
|
||||
driver = machine.provider.driver
|
||||
|
||||
env[:result] = driver.running?(machine.id)
|
||||
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,23 @@
|
|||
module VagrantPlugins
|
||||
module DockerProvider
|
||||
module Action
|
||||
# XXX: Is this really needed? Should we contribute this back to Vagrant's core?
|
||||
class Message
|
||||
def initialize(app, env, msg_key, type = :info)
|
||||
@app = app
|
||||
@msg_key = msg_key
|
||||
@type = type
|
||||
end
|
||||
|
||||
def call(env)
|
||||
machine = env[:machine]
|
||||
message = I18n.t("docker_provider.messages.#{@msg_key}", name: machine.name)
|
||||
|
||||
env[:ui].send @type, message
|
||||
|
||||
@app.call env
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,59 @@
|
|||
require_relative '../errors'
|
||||
|
||||
module VagrantPlugins
|
||||
module DockerProvider
|
||||
module Action
|
||||
class PrepareNFSSettings
|
||||
include Vagrant::Util::Retryable
|
||||
|
||||
def initialize(app, env)
|
||||
@app = app
|
||||
@logger = Log4r::Logger.new("vagrant::action::vm::nfs")
|
||||
end
|
||||
|
||||
def call(env)
|
||||
@machine = env[:machine]
|
||||
|
||||
@app.call(env)
|
||||
|
||||
if using_nfs? && !privileged_container?
|
||||
raise Errors::NfsWithoutPrivilegedError
|
||||
end
|
||||
|
||||
if using_nfs?
|
||||
@logger.info("Using NFS, preparing NFS settings by reading host IP and machine IP")
|
||||
add_ips_to_env!(env)
|
||||
end
|
||||
end
|
||||
|
||||
# We're using NFS if we have any synced folder with NFS configured. If
|
||||
# we are not using NFS we don't need to do the extra work to
|
||||
# populate these fields in the environment.
|
||||
def using_nfs?
|
||||
@machine.config.vm.synced_folders.any? { |_, opts| opts[:type] == :nfs }
|
||||
end
|
||||
|
||||
def privileged_container?
|
||||
@machine.provider.driver.privileged?(@machine.id)
|
||||
end
|
||||
|
||||
# Extracts the proper host and guest IPs for NFS mounts and stores them
|
||||
# in the environment for the SyncedFolder action to use them in
|
||||
# mounting.
|
||||
#
|
||||
# The ! indicates that this method modifies its argument.
|
||||
def add_ips_to_env!(env)
|
||||
provider = env[:machine].provider
|
||||
|
||||
host_ip = provider.driver.docker_bridge_ip
|
||||
machine_ip = provider.ssh_info[:host]
|
||||
|
||||
raise Vagrant::Errors::NFSNoHostonlyNetwork if !host_ip || !machine_ip
|
||||
|
||||
env[:nfs_host_ip] = host_ip
|
||||
env[:nfs_machine_ip] = machine_ip
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
module VagrantPlugins
|
||||
module DockerProvider
|
||||
module Action
|
||||
class PrepareNFSValidIds
|
||||
def initialize(app, env)
|
||||
@app = app
|
||||
@logger = Log4r::Logger.new("vagrant::action::vm::nfs")
|
||||
end
|
||||
|
||||
def call(env)
|
||||
machine = env[:machine]
|
||||
env[:nfs_valid_ids] = machine.provider.driver.all_containers
|
||||
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,18 @@
|
|||
module VagrantPlugins
|
||||
module DockerProvider
|
||||
module Action
|
||||
class Start
|
||||
def initialize(app, env)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
machine = env[:machine]
|
||||
driver = machine.provider.driver
|
||||
driver.start(machine.id)
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,21 @@
|
|||
module VagrantPlugins
|
||||
module DockerProvider
|
||||
module Action
|
||||
class Stop
|
||||
def initialize(app, env)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
machine = env[:machine]
|
||||
driver = machine.provider.driver
|
||||
if driver.running?(machine.id)
|
||||
env[:ui].info I18n.t("docker_provider.messages.stopping")
|
||||
driver.stop(machine.id)
|
||||
end
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,35 @@
|
|||
module VagrantPlugins
|
||||
module DockerProvider
|
||||
class Config < Vagrant.plugin("2", :config)
|
||||
attr_accessor :image, :cmd, :ports, :volumes, :privileged
|
||||
|
||||
def initialize
|
||||
@image = nil
|
||||
@cmd = UNSET_VALUE
|
||||
@ports = []
|
||||
@privileged = UNSET_VALUE
|
||||
@volumes = []
|
||||
end
|
||||
|
||||
def finalize!
|
||||
@cmd = [] if @cmd == UNSET_VALUE
|
||||
@privileged = false if @privileged == UNSET_VALUE
|
||||
end
|
||||
|
||||
def validate(machine)
|
||||
errors = _detected_errors
|
||||
|
||||
# TODO: Detect if base image has a CMD / ENTRYPOINT set before erroring out
|
||||
errors << I18n.t("docker_provider.errors.config.cmd_not_set") if @cmd == UNSET_VALUE
|
||||
|
||||
{ "docker-provider" => errors }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def using_nfs?(machine)
|
||||
machine.config.vm.synced_folders.any? { |_, opts| opts[:type] == :nfs }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,138 @@
|
|||
require "vagrant/util/busy"
|
||||
require "vagrant/util/subprocess"
|
||||
require "vagrant/util/retryable"
|
||||
|
||||
require 'log4r'
|
||||
require 'json'
|
||||
|
||||
module VagrantPlugins
|
||||
module DockerProvider
|
||||
class Driver
|
||||
include Vagrant::Util::Retryable
|
||||
|
||||
def initialize
|
||||
@logger = Log4r::Logger.new("vagrant::docker::driver")
|
||||
end
|
||||
|
||||
def create(params)
|
||||
image = params.fetch(:image)
|
||||
ports = Array(params[:ports])
|
||||
volumes = Array(params[:volumes])
|
||||
name = params.fetch(:name)
|
||||
cmd = Array(params.fetch(:cmd))
|
||||
|
||||
run_cmd = %W(docker run -name #{name} -d)
|
||||
run_cmd += ports.map { |p| ['-p', p.to_s] }
|
||||
run_cmd += volumes.map { |v| ['-v', v.to_s] }
|
||||
run_cmd += %W(-privileged) if params[:privileged]
|
||||
run_cmd += %W(-h #{params[:hostname]}) if params[:hostname]
|
||||
run_cmd += [image, cmd]
|
||||
|
||||
retryable(tries: 10, sleep: 1) do
|
||||
execute(*run_cmd.flatten).chomp
|
||||
end
|
||||
end
|
||||
|
||||
def state(cid)
|
||||
case
|
||||
when running?(cid)
|
||||
:running
|
||||
when created?(cid)
|
||||
:stopped
|
||||
else
|
||||
:not_created
|
||||
end
|
||||
end
|
||||
|
||||
def created?(cid)
|
||||
result = execute('docker', 'ps', '-a', '-q', '-notrunc').to_s
|
||||
result =~ /^#{Regexp.escape cid}$/
|
||||
end
|
||||
|
||||
def running?(cid)
|
||||
result = execute('docker', 'ps', '-q', '-notrunc')
|
||||
result =~ /^#{Regexp.escape cid}$/m
|
||||
end
|
||||
|
||||
def privileged?(cid)
|
||||
inspect_container(cid)['HostConfig']['Privileged']
|
||||
end
|
||||
|
||||
def start(cid)
|
||||
unless running?(cid)
|
||||
execute('docker', 'start', cid)
|
||||
# This resets the cached information we have around, allowing `vagrant reload`s
|
||||
# to work properly
|
||||
# TODO: Add spec to verify this behavior
|
||||
@data = nil
|
||||
end
|
||||
end
|
||||
|
||||
def stop(cid)
|
||||
if running?(cid)
|
||||
execute('docker', 'stop', '-t', '1', cid)
|
||||
end
|
||||
end
|
||||
|
||||
def rm(cid)
|
||||
if created?(cid)
|
||||
execute('docker', 'rm', '-v', cid)
|
||||
end
|
||||
end
|
||||
|
||||
def inspect_container(cid)
|
||||
# DISCUSS: Is there a chance that this json will change after the container
|
||||
# has been brought up?
|
||||
@data ||= JSON.parse(execute('docker', 'inspect', cid)).first
|
||||
end
|
||||
|
||||
def all_containers
|
||||
execute('docker', 'ps', '-a', '-q', '-notrunc').to_s.split
|
||||
end
|
||||
|
||||
def docker_bridge_ip
|
||||
output = execute('/sbin/ip', '-4', 'addr', 'show', 'scope', 'global', 'docker0')
|
||||
if output =~ /^\s+inet ([0-9.]+)\/[0-9]+\s+/
|
||||
return $1.to_s
|
||||
else
|
||||
# TODO: Raise an user friendly message
|
||||
raise 'Unable to fetch docker bridge IP!'
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def execute(*cmd, &block)
|
||||
result = raw(*cmd, &block)
|
||||
|
||||
if result.exit_code != 0
|
||||
if @interrupted
|
||||
@logger.info("Exit code != 0, but interrupted. Ignoring.")
|
||||
else
|
||||
msg = result.stdout.gsub("\r\n", "\n")
|
||||
msg << result.stderr.gsub("\r\n", "\n")
|
||||
raise "#{cmd.inspect}\n#{msg}" #Errors::ExecuteError, :command => command.inspect
|
||||
end
|
||||
end
|
||||
|
||||
# Return the output, making sure to replace any Windows-style
|
||||
# newlines with Unix-style.
|
||||
result.stdout.gsub("\r\n", "\n")
|
||||
end
|
||||
|
||||
def raw(*cmd, &block)
|
||||
int_callback = lambda do
|
||||
@interrupted = true
|
||||
@logger.info("Interrupted.")
|
||||
end
|
||||
|
||||
# Append in the options for subprocess
|
||||
cmd << { :notify => [:stdout, :stderr] }
|
||||
|
||||
Vagrant::Util::Busy.busy(int_callback) do
|
||||
Vagrant::Util::Subprocess.execute(*cmd, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,14 @@
|
|||
require 'vagrant/errors'
|
||||
|
||||
module VagrantPlugins
|
||||
module DockerProvider
|
||||
module Errors
|
||||
class ImageNotConfiguredError < Vagrant::Errors::VagrantError
|
||||
error_key(:docker_provider_image_not_configured)
|
||||
end
|
||||
class NfsWithoutPrivilegedError < Vagrant::Errors::VagrantError
|
||||
error_key(:docker_provider_nfs_without_privileged)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,31 @@
|
|||
# TODO: Switch to Vagrant.require_version before 1.0.0
|
||||
# see: https://github.com/mitchellh/vagrant/blob/bc55081e9ffaa6820113e449a9f76b293a29b27d/lib/vagrant.rb#L202-L228
|
||||
unless Gem::Requirement.new('>= 1.4.0').satisfied_by?(Gem::Version.new(Vagrant::VERSION))
|
||||
raise 'docker-provider requires Vagrant >= 1.4.0 in order to work!'
|
||||
end
|
||||
|
||||
I18n.load_path << File.expand_path(File.dirname(__FILE__) + '/../../locales/en.yml')
|
||||
I18n.reload!
|
||||
|
||||
module VagrantPlugins
|
||||
module DockerProvider
|
||||
class Plugin < Vagrant.plugin("2")
|
||||
name "docker-provider"
|
||||
|
||||
provider(:docker, parallel: true) do
|
||||
require_relative 'provider'
|
||||
Provider
|
||||
end
|
||||
|
||||
config(:docker, :provider) do
|
||||
require_relative 'config'
|
||||
Config
|
||||
end
|
||||
|
||||
synced_folder(:docker) do
|
||||
require File.expand_path("../synced_folder", __FILE__)
|
||||
SyncedFolder
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,61 @@
|
|||
require "log4r"
|
||||
|
||||
require_relative 'driver'
|
||||
require_relative 'action'
|
||||
|
||||
module VagrantPlugins
|
||||
module DockerProvider
|
||||
class Provider < Vagrant.plugin("2", :provider)
|
||||
attr_reader :driver
|
||||
|
||||
def initialize(machine)
|
||||
@logger = Log4r::Logger.new("vagrant::provider::docker")
|
||||
@machine = machine
|
||||
@driver = Driver.new
|
||||
end
|
||||
|
||||
# @see Vagrant::Plugin::V2::Provider#action
|
||||
def action(name)
|
||||
action_method = "action_#{name}"
|
||||
return Action.send(action_method) if Action.respond_to?(action_method)
|
||||
nil
|
||||
end
|
||||
|
||||
# Returns the SSH info for accessing the Container.
|
||||
def ssh_info
|
||||
# If the Container is not created then we cannot possibly SSH into it, so
|
||||
# we return nil.
|
||||
return nil if state == :not_created
|
||||
|
||||
network = @driver.inspect_container(@machine.id)['NetworkSettings']
|
||||
ip = network['IPAddress']
|
||||
|
||||
# If we were not able to identify the container's IP, we return nil
|
||||
# here and we let Vagrant core deal with it ;)
|
||||
return nil unless ip
|
||||
|
||||
{
|
||||
:host => ip,
|
||||
:port => @machine.config.ssh.guest_port
|
||||
}
|
||||
end
|
||||
|
||||
def state
|
||||
state_id = nil
|
||||
state_id = :not_created if !@machine.id || !@driver.created?(@machine.id)
|
||||
state_id = @driver.state(@machine.id) if @machine.id && !state_id
|
||||
state_id = :unknown if !state_id
|
||||
|
||||
short = state_id.to_s.gsub("_", " ")
|
||||
long = I18n.t("vagrant.commands.status.#{state_id}")
|
||||
|
||||
Vagrant::MachineState.new(state_id, short, long)
|
||||
end
|
||||
|
||||
def to_s
|
||||
id = @machine.id ? @machine.id : "new container"
|
||||
"Docker (#{id})"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,20 @@
|
|||
module VagrantPlugins
|
||||
module DockerProvider
|
||||
class SyncedFolder < Vagrant.plugin("2", :synced_folder)
|
||||
def usable?(machine)
|
||||
# These synced folders only work if the provider is Docker
|
||||
machine.provider_name == :docker
|
||||
end
|
||||
|
||||
def prepare(machine, folders, _opts)
|
||||
# FIXME: Check whether the container has already been created with
|
||||
# different synced folders and let the user know about it
|
||||
folders.each do |id, data|
|
||||
host_path = File.expand_path(data[:hostpath], machine.env.root_path)
|
||||
guest_path = data[:guestpath]
|
||||
machine.provider_config.volumes << "#{host_path}:#{guest_path}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,32 @@
|
|||
en:
|
||||
docker_provider:
|
||||
messages:
|
||||
not_created: |-
|
||||
The container hasn't been created yet.
|
||||
not_running: |-
|
||||
The container is not currently running.
|
||||
will_not_destroy: |-
|
||||
The container '%{name}' will not be destroyed, since the confirmation
|
||||
was declined.
|
||||
starting: |-
|
||||
Starting container...
|
||||
stopping: |-
|
||||
Stopping container...
|
||||
container_ready: |-
|
||||
Container started and ready for use!
|
||||
|
||||
errors:
|
||||
config:
|
||||
cmd_not_set: |-
|
||||
The Docker command has not been set!
|
||||
|
||||
vagrant:
|
||||
errors:
|
||||
docker_provider_nfs_without_privileged: |-
|
||||
You've configured a NFS synced folder but didn't enable privileged
|
||||
mode for the container. Please set the `privileged` option to true
|
||||
on the provider block from your Vagrantfile, recreate the container
|
||||
and try again.
|
||||
|
||||
docker_provider_image_not_configured: |-
|
||||
The base Docker image has not been set for the '%{name}' VM!
|
|
@ -0,0 +1,202 @@
|
|||
require_relative "../../../base"
|
||||
|
||||
require Vagrant.source_root.join("plugins/providers/docker/driver")
|
||||
|
||||
describe VagrantPlugins::DockerProvider::Driver do
|
||||
let(:cmd_executed) { @cmd }
|
||||
let(:cid) { 'side-1-song-10' }
|
||||
|
||||
before do
|
||||
subject.stub(:execute) { |*args| @cmd = args.join(' ') }
|
||||
end
|
||||
|
||||
describe '#create' do
|
||||
let(:params) { {
|
||||
image: 'jimi/hendrix:eletric-ladyland',
|
||||
cmd: ['play', 'voodoo-chile'],
|
||||
ports: '8080:80',
|
||||
volumes: '/host/path:guest/path',
|
||||
name: cid,
|
||||
hostname: 'jimi-hendrix',
|
||||
privileged: true
|
||||
} }
|
||||
|
||||
before { subject.create(params) }
|
||||
|
||||
it 'runs a detached docker image' do
|
||||
expect(cmd_executed).to match(/^docker run .+ -d .+ #{Regexp.escape params[:image]}/)
|
||||
end
|
||||
|
||||
it 'sets container name' do
|
||||
expect(cmd_executed).to match(/-name #{Regexp.escape params[:name]}/)
|
||||
end
|
||||
|
||||
it 'forwards ports' do
|
||||
expect(cmd_executed).to match(/-p #{params[:ports]} .+ #{Regexp.escape params[:image]}/)
|
||||
end
|
||||
|
||||
it 'shares folders' do
|
||||
expect(cmd_executed).to match(/-v #{params[:volumes]} .+ #{Regexp.escape params[:image]}/)
|
||||
end
|
||||
|
||||
it 'is able to run a privileged container' do
|
||||
expect(cmd_executed).to match(/-privileged .+ #{Regexp.escape params[:image]}/)
|
||||
end
|
||||
|
||||
it 'sets the hostname if specified' do
|
||||
expect(cmd_executed).to match(/-h #{params[:hostname]} #{Regexp.escape params[:image]}/)
|
||||
end
|
||||
|
||||
it 'executes the provided command' do
|
||||
expect(cmd_executed).to match(/#{Regexp.escape params[:image]} #{Regexp.escape params[:cmd].join(' ')}/)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#created?' do
|
||||
let(:result) { subject.created?(cid) }
|
||||
|
||||
it 'performs the check on all containers list' do
|
||||
subject.created?(cid)
|
||||
expect(cmd_executed).to match(/docker ps \-a \-q/)
|
||||
end
|
||||
|
||||
context 'when container exists' do
|
||||
before { subject.stub(execute: "foo\n#{cid}\nbar") }
|
||||
it { expect(result).to be_true }
|
||||
end
|
||||
|
||||
context 'when container does not exist' do
|
||||
before { subject.stub(execute: "foo\n#{cid}extra\nbar") }
|
||||
it { expect(result).to be_false }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#running?' do
|
||||
let(:result) { subject.running?(cid) }
|
||||
|
||||
it 'performs the check on the running containers list' do
|
||||
subject.running?(cid)
|
||||
expect(cmd_executed).to match(/docker ps \-q/)
|
||||
expect(cmd_executed).to_not include('-a')
|
||||
end
|
||||
|
||||
context 'when container exists' do
|
||||
before { subject.stub(execute: "foo\n#{cid}\nbar") }
|
||||
it { expect(result).to be_true }
|
||||
end
|
||||
|
||||
context 'when container does not exist' do
|
||||
before { subject.stub(execute: "foo\n#{cid}extra\nbar") }
|
||||
it { expect(result).to be_false }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#privileged?' do
|
||||
it 'identifies privileged containers' do
|
||||
subject.stub(inspect_container: {'HostConfig' => {"Privileged" => true}})
|
||||
expect(subject).to be_privileged(cid)
|
||||
end
|
||||
|
||||
it 'identifies unprivileged containers' do
|
||||
subject.stub(inspect_container: {'HostConfig' => {"Privileged" => false}})
|
||||
expect(subject).to_not be_privileged(cid)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#start' do
|
||||
context 'when container is running' do
|
||||
before { subject.stub(running?: true) }
|
||||
|
||||
it 'does not start the container' do
|
||||
subject.should_not_receive(:execute).with('docker', 'start', cid)
|
||||
subject.start(cid)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when container is not running' do
|
||||
before { subject.stub(running?: false) }
|
||||
|
||||
it 'starts the container' do
|
||||
subject.should_receive(:execute).with('docker', 'start', cid)
|
||||
subject.start(cid)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#stop' do
|
||||
context 'when container is running' do
|
||||
before { subject.stub(running?: true) }
|
||||
|
||||
it 'stops the container' do
|
||||
subject.should_receive(:execute).with('docker', 'stop', '-t', '1', cid)
|
||||
subject.stop(cid)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when container is not running' do
|
||||
before { subject.stub(running?: false) }
|
||||
|
||||
it 'does not stop container' do
|
||||
subject.should_not_receive(:execute).with('docker', 'stop', '-t', '1', cid)
|
||||
subject.stop(cid)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#rm' do
|
||||
context 'when container has been created' do
|
||||
before { subject.stub(created?: true) }
|
||||
|
||||
it 'removes the container' do
|
||||
subject.should_receive(:execute).with('docker', 'rm', '-v', cid)
|
||||
subject.rm(cid)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when container has not been created' do
|
||||
before { subject.stub(created?: false) }
|
||||
|
||||
it 'does not attempt to remove the container' do
|
||||
subject.should_not_receive(:execute).with('docker', 'rm', '-v', cid)
|
||||
subject.rm(cid)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#inspect_container' do
|
||||
let(:data) { '[{"json": "value"}]' }
|
||||
|
||||
before { subject.stub(execute: data) }
|
||||
|
||||
it 'inspects the container' do
|
||||
subject.should_receive(:execute).with('docker', 'inspect', cid)
|
||||
subject.inspect_container(cid)
|
||||
end
|
||||
|
||||
it 'parses the json output' do
|
||||
expect(subject.inspect_container(cid)).to eq('json' => 'value')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#all_containers' do
|
||||
let(:containers) { "container1\ncontainer2" }
|
||||
|
||||
before { subject.stub(execute: containers) }
|
||||
|
||||
it 'returns an array of all known containers' do
|
||||
subject.should_receive(:execute).with('docker', 'ps', '-a', '-q', '-notrunc')
|
||||
expect(subject.all_containers).to eq(['container1', 'container2'])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#docker_bridge_ip' do
|
||||
let(:containers) { " inet 123.456.789.012/16 " }
|
||||
|
||||
before { subject.stub(execute: containers) }
|
||||
|
||||
it 'returns an array of all known containers' do
|
||||
subject.should_receive(:execute).with('/sbin/ip', '-4', 'addr', 'show', 'scope', 'global', 'docker0')
|
||||
expect(subject.docker_bridge_ip).to eq('123.456.789.012')
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue