providers/docker: Import code from https://github.com/fgrehm/docker-provider with some initial tweaks

This commit is contained in:
Fabio Rehm 2014-03-26 20:45:46 -03:00 committed by Mitchell Hashimoto
parent efc1122c42
commit 5a60e568ce
21 changed files with 1254 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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