Merge pull request #10702 from briancain/docker-network-support

Docker Provider Network Support
This commit is contained in:
Brian Cain 2019-03-25 15:43:23 -07:00 committed by GitHub
commit ec67151312
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1874 additions and 9 deletions

View File

@ -161,6 +161,7 @@ module VagrantPlugins
b4.use action_halt
b4.use HostMachineSyncFoldersDisable
b4.use Destroy
b4.use DestroyNetwork
b4.use DestroyBuildImage
else
b4.use Message,
@ -243,6 +244,7 @@ module VagrantPlugins
b2.use PrepareNFSValidIds
b2.use SyncedFolderCleanup
b2.use PrepareNFSSettings
b2.use PrepareNetworks
b2.use Login
b2.use Build
@ -265,6 +267,7 @@ module VagrantPlugins
end
end
b2.use ConnectNetworks
b2.use Start
b2.use WaitForRunning
@ -292,9 +295,11 @@ module VagrantPlugins
action_root = Pathname.new(File.expand_path("../action", __FILE__))
autoload :Build, action_root.join("build")
autoload :CompareSyncedFolders, action_root.join("compare_synced_folders")
autoload :ConnectNetworks, action_root.join("connect_networks")
autoload :Create, action_root.join("create")
autoload :Destroy, action_root.join("destroy")
autoload :DestroyBuildImage, action_root.join("destroy_build_image")
autoload :DestroyNetwork, action_root.join("destroy_network")
autoload :ForwardedPorts, action_root.join("forwarded_ports")
autoload :HasSSH, action_root.join("has_ssh")
autoload :HostMachine, action_root.join("host_machine")
@ -308,12 +313,13 @@ module VagrantPlugins
autoload :IsBuild, action_root.join("is_build")
autoload :IsHostMachineCreated, action_root.join("is_host_machine_created")
autoload :Login, action_root.join("login")
autoload :Pull, action_root.join("pull")
autoload :PrepareSSH, action_root.join("prepare_ssh")
autoload :Stop, action_root.join("stop")
autoload :PrepareNetworks, action_root.join("prepare_networks")
autoload :PrepareNFSValidIds, action_root.join("prepare_nfs_valid_ids")
autoload :PrepareNFSSettings, action_root.join("prepare_nfs_settings")
autoload :PrepareSSH, action_root.join("prepare_ssh")
autoload :Pull, action_root.join("pull")
autoload :Start, action_root.join("start")
autoload :Stop, action_root.join("stop")
autoload :WaitForRunning, action_root.join("wait_for_running")
end
end

View File

@ -0,0 +1,77 @@
require 'ipaddr'
require 'log4r'
require 'vagrant/util/scoped_hash_override'
module VagrantPlugins
module DockerProvider
module Action
class ConnectNetworks
include Vagrant::Util::ScopedHashOverride
def initialize(app, env)
@app = app
@logger = Log4r::Logger.new('vagrant::plugins::docker::connectnetworks')
end
# Generate CLI arguments for creating the docker network.
#
# @param [Hash] options Options from the network config
# @returns[Array<String> Network create arguments
def generate_connect_cli_arguments(options)
options.map do |key, value|
# If value is false, option is not set
next if value.to_s == "false"
# If value is true, consider feature flag with no value
opt = value.to_s == "true" ? [] : [value]
opt.unshift("--#{key.to_s.tr("_", "-")}")
end.flatten.compact
end
# Execute the action
def call(env)
# If we are using a host VM, then don't worry about it
machine = env[:machine]
if machine.provider.host_vm?
@logger.debug("Not setting up networks because docker host_vm is in use")
return @app.call(env)
end
env[:ui].info(I18n.t("docker_provider.network_connect"))
connections = env[:docker_connects] || {}
machine.config.vm.networks.each_with_index do |args, idx|
type, options = args
next if type != :private_network && type != :public_network
network_options = scoped_hash_override(options, :docker_connect)
network_options.delete_if{|k,_| options.key?(k)}
network_name = connections[idx]
if !network_name
raise Errors::NetworkNameMissing,
index: idx,
container: machine.name
end
@logger.debug("Connecting network #{network_name} to container guest #{machine.name}")
if options[:ip] && options[:type] != "dhcp"
if IPAddr.new(options[:ip]).ipv4?
network_options[:ip] = options[:ip]
else
network_options[:ip6] = options[:ip]
end
end
network_options[:alias] = options[:alias] if options[:alias]
connect_opts = generate_connect_cli_arguments(network_options)
machine.provider.driver.connect_network(network_name, machine.id, connect_opts)
end
@app.call(env)
end
end
end
end
end

View File

@ -0,0 +1,50 @@
require 'log4r'
module VagrantPlugins
module DockerProvider
module Action
class DestroyNetwork
@@lock = Mutex.new
def initialize(app, env)
@app = app
@logger = Log4r::Logger.new('vagrant::plugins::docker::network')
end
def call(env)
# If we are using a host VM, then don't worry about it
machine = env[:machine]
if machine.provider.host_vm?
@logger.debug("Not setting up networks because docker host_vm is in use")
return @app.call(env)
end
@@lock.synchronize do
machine.env.lock("docker-network-destroy", retry: true) do
machine.config.vm.networks.each do |type, options|
next if type != :private_network && type != :public_network
vagrant_networks = machine.provider.driver.list_network_names.find_all do |n|
n.start_with?("vagrant_network")
end
vagrant_networks.each do |network_name|
if machine.provider.driver.existing_named_network?(network_name) &&
!machine.provider.driver.network_used?(network_name)
env[:ui].info(I18n.t("docker_provider.network_destroy", network_name: network_name))
machine.provider.driver.rm_network(network_name)
else
@logger.debug("Network #{network_name} not found or in use")
end
end
end
end
end
@app.call(env)
end
end
end
end
end

View File

@ -0,0 +1,355 @@
require 'ipaddr'
require 'log4r'
require 'vagrant/util/scoped_hash_override'
module VagrantPlugins
module DockerProvider
module Action
class PrepareNetworks
include Vagrant::Util::ScopedHashOverride
@@lock = Mutex.new
def initialize(app, env)
@app = app
@logger = Log4r::Logger.new('vagrant::plugins::docker::preparenetworks')
end
# Generate CLI arguments for creating the docker network.
#
# @param [Hash] options Options from the network config
# @returns[Array<String>] Network create arguments
def generate_create_cli_arguments(options)
options.map do |key, value|
# If value is false, option is not set
next if value.to_s == "false"
# If value is true, consider feature flag with no value
opt = value.to_s == "true" ? [] : [value]
opt.unshift("--#{key.to_s.tr("_", "-")}")
end.flatten.compact
end
# @return [Array<Socket::Ifaddr>] interface list
def list_interfaces
Socket.getifaddrs.find_all do |i|
i.addr.ip? && !i.addr.ipv4_loopback? &&
!i.addr.ipv6_loopback? && !i.addr.ipv6_linklocal?
end
end
# Validates that a network name exists. If it does not
# exist, an exception is raised.
#
# @param [String] network_name Name of existing network
# @param [Hash] env Local call env
# @return [Boolean]
def validate_network_name!(network_name, env)
if !env[:machine].provider.driver.existing_named_network?(network_name)
raise Errors::NetworkNameUndefined,
network_name: network_name
end
true
end
# Validates that the provided options are compatible with a
# pre-existing network. Raises exceptions on invalid configurations
#
# @param [String] network_name Name of the network
# @param [Hash] root_options Root networking options
# @param [Hash] network_options Docker scoped networking options
# @param [Driver] driver Docker driver
# @return [Boolean]
def validate_network_configuration!(network_name, root_options, network_options, driver)
if root_options[:ip] &&
driver.network_containing_address(root_options[:ip]) != network_name
raise Errors::NetworkAddressInvalid,
address: root_options[:ip],
network_name: network_name
end
if network_options[:subnet] &&
driver.network_containing_address(network_options[:subnet]) != network_name
raise Errors::NetworkSubnetInvalid,
subnet: network_options[:subnet],
network_name: network_name
end
true
end
# Generate configuration for private network
#
# @param [Hash] root_options Root networking options
# @param [Hash] net_options Docker scoped networking options
# @param [Hash] env Local call env
# @return [String, Hash] Network name and updated network_options
def process_private_network(root_options, network_options, env)
if root_options[:name] && validate_network_name!(root_options[:name], env)
network_name = root_options[:name]
end
if root_options[:type].to_s == "dhcp"
if !root_options[:ip] && !root_options[:subnet]
network_name = "vagrant_network" if !network_name
return [network_name, network_options]
end
if root_options[:subnet]
addr = IPAddr.new(root_options[:subnet])
root_options[:netmask] = addr.prefix
end
end
if root_options[:ip]
addr = IPAddr.new(root_options[:ip])
elsif addr.nil?
raise Errors::NetworkIPAddressRequired
end
# If address is ipv6, enable ipv6 support
network_options[:ipv6] = addr.ipv6?
# If no mask is provided, attempt to locate any existing
# network which contains the assigned IP address
if !root_options[:netmask] && !network_name
network_name = env[:machine].provider.driver.
network_containing_address(root_options[:ip])
# When no existing network is found, we are creating
# a new network. Since no mask was provided, default
# to /24 for ipv4 and /64 for ipv6
if !network_name
root_options[:netmask] = addr.ipv4? ? 24 : 64
end
end
# With no network name, process options to find or determine
# name for new network
if !network_name
if !root_options[:subnet]
# Only generate a subnet if not given one
subnet = IPAddr.new("#{addr}/#{root_options[:netmask]}")
network = "#{subnet}/#{root_options[:netmask]}"
else
network = root_options[:subnet]
end
network_options[:subnet] = network
existing_network = env[:machine].provider.driver.
network_defined?(network)
if !existing_network
network_name = "vagrant_network_#{network}"
else
if !existing_network.to_s.start_with?("vagrant_network")
env[:ui].warn(I18n.t("docker_provider.subnet_exists",
network_name: existing_network,
subnet: network))
end
network_name = existing_network
end
end
[network_name, network_options]
end
# Generate configuration for public network
#
# @param [Hash] root_options Root networking options
# @param [Hash] net_options Docker scoped networking options
# @param [Hash] env Local call env
# @return [String, Hash] Network name and updated network_options
def process_public_network(root_options, net_options, env)
if root_options[:name] && validate_network_name!(root_options[:name], env)
network_name = root_options[:name]
end
if !network_name
valid_interfaces = list_interfaces
if valid_interfaces.empty?
raise Errors::NetworkNoInterfaces
elsif valid_interfaces.size == 1
bridge_interface = valid_interfaces.first
elsif i = valid_interfaces.detect{|i| Array(root_options[:bridge]).include?(i.name) }
bridge_interface = i
end
if !bridge_interface
env[:ui].info(I18n.t("vagrant.actions.vm.bridged_networking.available"),
prefix: false)
valid_interfaces.each_with_index do |int, i|
env[:ui].info("#{i + 1}) #{int.name}", prefix: false)
end
env[:ui].info(I18n.t(
"vagrant.actions.vm.bridged_networking.choice_help") + "\n",
prefix: false
)
end
while !bridge_interface
choice = env[:ui].ask(
I18n.t("vagrant.actions.vm.bridged_networking.select_interface") + " ",
prefix: false)
bridge_interface = valid_interfaces[choice.to_i - 1]
end
base_opts = Vagrant::Util::HashWithIndifferentAccess.new
base_opts[:opt] = "parent=#{bridge_interface.name}"
subnet = IPAddr.new(bridge_interface.addr.ip_address <<
"/" << bridge_interface.netmask.ip_unpack.first)
base_opts[:subnet] = "#{subnet}/#{subnet.prefix}"
subnet_addr = IPAddr.new(base_opts[:subnet])
base_opts[:driver] = "macvlan"
base_opts[:gateway] = subnet_addr.succ.to_s
base_opts[:ipv6] = subnet_addr.ipv6?
network_options = base_opts.merge(net_options)
# Check if network already exists for this subnet
network_name = env[:machine].provider.driver.
network_containing_address(network_options[:gateway])
if !network_name
network_name = "vagrant_network_public_#{bridge_interface.name}"
end
# If the network doesn't already exist, gather available address range
# within subnet which docker can provide addressing
if !env[:machine].provider.driver.existing_named_network?(network_name)
if !net_options[:gateway]
network_options[:gateway] = request_public_gateway(
network_options, bridge_interface.name, env)
end
network_options[:ip_range] = request_public_iprange(
network_options, bridge_interface.name, env)
end
end
[network_name, network_options]
end
# Request the gateway address for the public network
#
# @param [Hash] network_options Docker scoped networking options
# @param [String] interface The bridge interface used
# @param [Hash] env Local call env
# @return [String] Gateway address
def request_public_gateway(network_options, interface, env)
subnet = IPAddr.new(network_options[:subnet])
gateway = nil
while !gateway
gateway = env[:ui].ask(I18n.t(
"docker_provider.network_bridge_gateway_request",
interface: interface,
default_gateway: network_options[:gateway]) + " ",
prefix: false
).strip
if gateway.empty?
gateway = network_options[:gateway]
end
begin
gateway = IPAddr.new(gateway)
if !subnet.include?(gateway)
env[:ui].warn(I18n.t("docker_provider.network_bridge_gateway_outofbounds",
gateway: gateway,
subnet: network_options[:subnet]) + "\n", prefix: false)
end
rescue IPAddr::InvalidAddressError
env[:ui].warn(I18n.t("docker_provider.network_bridge_gateway_invalid",
gateway: gateway) + "\n", prefix: false)
gateway = nil
end
end
gateway.to_s
end
# Request the IP range allowed for use by docker when creating a new
# public network
#
# @param [Hash] network_options Docker scoped networking options
# @param [String] interface The bridge interface used
# @param [Hash] env Local call env
# @return [String] Address range
def request_public_iprange(network_options, interface, env)
return network_options[:ip_range] if network_options[:ip_range]
subnet = IPAddr.new(network_options[:subnet])
env[:ui].info(I18n.t(
"docker_provider.network_bridge_iprange_info") + "\n",
prefix: false
)
range = nil
while !range
range = env[:ui].ask(I18n.t(
"docker_provider.network_bridge_iprange_request",
interface: interface,
default_range: network_options[:subnet]) + " ",
prefix: false
).strip
if range.empty?
range = network_options[:subnet]
end
begin
range = IPAddr.new(range)
if !subnet.include?(range)
puts "we in here"
env[:ui].warn(I18n.t(
"docker_provider.network_bridge_iprange_outofbounds",
subnet: network_options[:subnet],
range: "#{range}/#{range.prefix}"
) + "\n", prefix: false)
range = nil
end
rescue IPAddr::InvalidAddressError
env[:ui].warn(I18n.t(
"docker_provider.network_bridge_iprange_invalid",
range: range) + "\n", prefix: false)
range = nil
end
end
"#{range}/#{range.prefix}"
end
# Execute the action
def call(env)
# If we are using a host VM, then don't worry about it
machine = env[:machine]
if machine.provider.host_vm?
@logger.debug("Not setting up networks because docker host_vm is in use")
return @app.call(env)
end
connections = {}
@@lock.synchronize do
machine.env.lock("docker-network-create", retry: true) do
env[:ui].info(I18n.t("docker_provider.network_create"))
machine.config.vm.networks.each_with_index do |net_info, net_idx|
type, options = net_info
network_options = scoped_hash_override(options, :docker_network)
network_options.delete_if{|k,_| options.key?(k)}
case type
when :public_network
network_name, network_options = process_public_network(
options, network_options, env)
when :private_network
network_name, network_options = process_private_network(
options, network_options, env)
else
next # unsupported type so ignore
end
if !network_name
raise Errors::NetworkInvalidOption, container: machine.name
end
if !machine.provider.driver.existing_named_network?(network_name)
@logger.debug("Creating network #{network_name}")
cli_opts = generate_create_cli_arguments(network_options)
machine.provider.driver.create_network(network_name, cli_opts)
else
@logger.debug("Network #{network_name} already created")
validate_network_configuration!(network_name, options, network_options, machine.provider.driver)
end
connections[net_idx] = network_name
end
end
end
env[:docker_connects] = connections
@app.call(env)
end
end
end
end
end

View File

@ -152,21 +152,25 @@ module VagrantPlugins
def rmi(id)
execute('docker', 'rmi', id)
return true
rescue Exception => e
rescue => e
return false if e.to_s.include?("is using it")
raise if !e.to_s.include?("No such image")
end
# Inspect the provided container
#
# @param [String] cid ID or name of container
# @return [Hash]
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
JSON.parse(execute('docker', 'inspect', cid)).first
end
# @return [Array<String>] list of all container IDs
def all_containers
execute('docker', 'ps', '-a', '-q', '--no-trunc').to_s.split
end
# @return [String] IP address of the docker bridge
def docker_bridge_ip
output = execute('/sbin/ip', '-4', 'addr', 'show', 'scope', 'global', 'docker0')
if output =~ /^\s+inet ([0-9.]+)\/[0-9]+\s+/
@ -177,9 +181,149 @@ module VagrantPlugins
end
end
# @param [String] network - name of network to connect conatiner to
# @param [String] cid - container id
# @param [Array] opts - An array of flags used for listing networks
def connect_network(network, cid, opts=nil)
command = ['docker', 'network', 'connect', network, cid].push(*opts)
output = execute(*command)
output
end
# @param [String] network - name of network to create
# @param [Array] opts - An array of flags used for listing networks
def create_network(network, opts=nil)
command = ['docker', 'network', 'create', network].push(*opts)
output = execute(*command)
output
end
# @param [String] network - name of network to disconnect container from
# @param [String] cid - container id
def disconnect_network(network, cid)
command = ['docker', 'network', 'disconnect', network, cid, "--force"]
output = execute(*command)
output
end
# @param [Array] networks - list of networks to inspect
# @param [Array] opts - An array of flags used for listing networks
def inspect_network(network, opts=nil)
command = ['docker', 'network', 'inspect'] + Array(network)
command = command.push(*opts)
output = execute(*command)
begin
JSON.load(output)
rescue JSON::ParserError
@logger.warn("Failed to parse network inspection of network: #{network}")
@logger.debug("Failed network output content: `#{output.inspect}`")
nil
end
end
# @param [String] opts - Flags used for listing networks
def list_network(*opts)
command = ['docker', 'network', 'ls', *opts]
output = execute(*command)
output
end
# Will delete _all_ defined but unused networks in the docker engine. Even
# networks not created by Vagrant.
#
# @param [Array] opts - An array of flags used for listing networks
def prune_network(opts=nil)
command = ['docker', 'network', 'prune', '--force'].push(*opts)
output = execute(*command)
output
end
# Delete network(s)
#
# @param [String] network - name of network to remove
def rm_network(*network)
command = ['docker', 'network', 'rm', *network]
output = execute(*command)
output
end
# @param [Array] opts - An array of flags used for listing networks
def execute(*cmd, **opts, &block)
@executor.execute(*cmd, **opts, &block)
end
# ######################
# Docker network helpers
# ######################
# Determines if a given network has been defined through vagrant with a given
# subnet string
#
# @param [String] subnet_string - Subnet to look for
# @return [String] network name - Name of network with requested subnet.`nil` if not found
def network_defined?(subnet_string)
all_networks = list_network_names
network_info = inspect_network(all_networks)
network_info.each do |network|
config = network["IPAM"]["Config"]
if (config.size > 0 &&
config.first["Subnet"] == subnet_string)
@logger.debug("Found existing network #{network["Name"]} already configured with #{subnet_string}")
return network["Name"]
end
end
return nil
end
# Locate network which contains given address
#
# @param [String] address IP address
# @return [String] network name
def network_containing_address(address)
names = list_network_names
networks = inspect_network(names)
return if !networks
networks.each do |net|
next if !net["IPAM"]
config = net["IPAM"]["Config"]
next if !config || config.size < 1
config.each do |opts|
subnet = IPAddr.new(opts["Subnet"])
if subnet.include?(address)
return net["Name"]
end
end
end
nil
end
# Looks to see if a docker network has already been defined
# with the given name
#
# @param [String] network_name - name of network to look for
# @return [Bool]
def existing_named_network?(network_name)
result = list_network_names
result.any?{|net_name| net_name == network_name}
end
# @return [Array<String>] list of all docker networks
def list_network_names
list_network("--format={{.Name}}").split("\n").map(&:strip)
end
# Returns true or false if network is in use or not.
# Nil if Vagrant fails to receive proper JSON from `docker network inspect`
#
# @param [String] network - name of network to look for
# @return [Bool,nil]
def network_used?(network)
result = inspect_network(network)
return nil if !result
return result.first["Containers"].size > 0
end
end
end
end

View File

@ -45,6 +45,34 @@ module VagrantPlugins
error_key(:docker_provider_nfs_without_privileged)
end
class NetworkAddressInvalid < DockerError
error_key(:network_address_invalid)
end
class NetworkIPAddressRequired < DockerError
error_key(:network_address_required)
end
class NetworkSubnetInvalid < DockerError
error_key(:network_subnet_invalid)
end
class NetworkInvalidOption < DockerError
error_key(:network_invalid_option)
end
class NetworkNameMissing < DockerError
error_key(:network_name_missing)
end
class NetworkNameUndefined < DockerError
error_key(:network_name_undefined)
end
class NetworkNoInterfaces < DockerError
error_key(:network_no_interfaces)
end
class PackageNotSupported < DockerError
error_key(:package_not_supported)
end

View File

@ -2148,6 +2148,8 @@ en:
choice_help: |-
When choosing an interface, it is usually the one that is
being used to connect to the internet.
select_interface: |-
Which interface should the network bridge to?
specific_not_found: |-
Specific bridge '%{bridge}' not found. You may be asked to specify
which network to bridge to.

View File

@ -45,6 +45,35 @@ en:
This container requires a host VM, and the state of that VM
is unknown. Run `vagrant up` to verify that the container and
its host VM is running, then try again.
network_bridge_gateway_invalid: |-
The provided gateway IP address is invalid (%{gateway}). Please
provide a valid IP address.
network_bridge_gateway_outofbounds: |-
The provided gateway IP (%{gateway}) is not within the defined
subnet (%{subnet}). Please provide an IP address within the
defined subnet.
network_bridge_gateway_request: |-
Gateway IP address for %{interface} interface [%{default_gateway}]:
network_bridge_iprange_info: |-
When an explicit address is not provided to a container attached
to this bridged network, docker will supply an address to the
container. This is independent of the local DHCP service that
may be available on the network.
network_bridge_iprange_invalid: |-
The provided IP address range is invalid (%{range}). Please
provide a valid range.
network_bridge_iprange_outofbounds: |-
The provided IP address range (%{range}) is not within the
defined subnet (%{subnet}). Please provide an address range
within the defined subnet.
network_bridge_iprange_request: |-
Available address range for assignment on %{interface} interface [%{default_range}]:
network_create: |-
Creating and configuring docker networks...
network_connect: |-
Enabling network interfaces...
network_destroy: |-
Removing network %{network_name} ...
not_created_skip: |-
Container not created. Skipping.
not_docker_provider: |-
@ -66,6 +95,9 @@ en:
ssh_through_host_vm: |-
SSH will be proxied through the Docker virtual machine since we're
not running Docker natively. This is just a notice, and not an error.
subnet_exists: |-
A network called '%{network_name}' using subnet '%{subnet}' is already in use.
Using '%{network_name}' instead of creating a new network...
synced_folders_changed: |-
Vagrant has noticed that the synced folder definitions have changed.
With Docker, these synced folder changes won't take effect until you
@ -197,6 +229,38 @@ en:
is functional and properly configured.
Host VM ID: %{id}
network_address_invalid: |-
The configured network address is not valid within the configured
subnet of the defined network. Please update the network settings
and try again.
Configured address: %{address}
Network name: %{network_name}
network_address_required: |-
An IP address is required if not using `type: "dhcp"` or not specifying a `subnet`.
network_invalid_option: |-
Invalid option given for docker network for guest "%{container}". Must specify either
a `subnet` or use `type: "dhcp"`.
network_name_missing: |-
The Docker provider is unable to connect the container to the
defined network due to a missing network name. Please validate
your configuration and try again.
Container: %{container}
Network Number: %{index}
network_name_undefined: |-
The Docker provider was unable to configure networking using the
provided network name `%{network_name}`. Please ensure the network
name is correct and exists, then try again.
network_no_interfaces: |-
The Docker provider was unable to list any available interfaces to bridge
the public network with.
network_subnet_invalid: |-
The configured network subnet is not valid for the defined network.
Please update the network settings and try again.
Configured subnet: %{subnet}
Network name: %{network_name}
package_not_supported: |-
The "package" command is not supported with the Docker provider.
If you'd like to commit or push your Docker container, please SSH

View File

@ -0,0 +1,128 @@
require_relative "../../../../base"
require_relative "../../../../../../plugins/providers/docker/action/connect_networks"
describe VagrantPlugins::DockerProvider::Action::ConnectNetworks do
include_context "unit"
include_context "virtualbox"
let(:sandbox) { isolated_environment }
let(:iso_env) do
# We have to create a Vagrantfile so there is a root path
sandbox.vagrantfile("")
sandbox.create_vagrant_env
end
let(:machine) do
iso_env.machine(iso_env.machine_names[0], :docker).tap do |m|
allow(m).to receive(:id).and_return("12345")
allow(m.provider).to receive(:driver).and_return(driver)
allow(m.provider).to receive(:host_vm?).and_return(false)
allow(m.config.vm).to receive(:networks).and_return(networks)
end
end
let(:docker_connects) { {0=>"vagrant_network_172.20.0.0/16", 1=>"vagrant_network_public_wlp4s0", 2=>"vagrant_network_2a02:6b8:b010:9020:1::/80"} }
let(:env) {{ machine: machine, ui: machine.ui, root_path: Pathname.new("."),
docker_connects: docker_connects }}
let(:app) { lambda { |*args| }}
let(:driver) { double("driver", create: "abcd1234") }
let(:networks) { [[:private_network,
{:ip=>"172.20.128.2",
:subnet=>"172.20.0.0/16",
:driver=>"bridge",
:internal=>"true",
:alias=>"mynetwork",
:protocol=>"tcp",
:id=>"80e017d5-388f-4a2f-a3de-f8dce8156a58"}],
[:public_network,
{:ip=>"172.30.130.2",
:subnet=>"172.30.0.0/16",
:driver=>"bridge",
:id=>"30e017d5-488f-5a2f-a3ke-k8dce8246b60"}],
[:private_network,
{:type=>"dhcp",
:ipv6=>"true",
:subnet=>"2a02:6b8:b010:9020:1::/80",
:protocol=>"tcp",
:id=>"b8f23054-38d5-45c3-99ea-d33fc5d1b9f2"}],
[:forwarded_port,
{:guest=>22, :host=>2200, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}]]
}
subject { described_class.new(app, env) }
after do
sandbox.close
end
describe "#call" do
it "calls the next action in the chain" do
allow(driver).to receive(:host_vm?).and_return(false)
allow(driver).to receive(:connect_network).and_return(true)
called = false
app = ->(*args) { called = true }
action = described_class.new(app, env)
action.call(env)
expect(called).to eq(true)
end
it "connects all of the avaiable networks to a container" do
expect(driver).to receive(:connect_network).with("vagrant_network_172.20.0.0/16", "12345", ["--ip", "172.20.128.2", "--alias", "mynetwork"])
expect(driver).to receive(:connect_network).with("vagrant_network_public_wlp4s0", "12345", ["--ip", "172.30.130.2"])
expect(driver).to receive(:connect_network).with("vagrant_network_2a02:6b8:b010:9020:1::/80", "12345", [])
subject.call(env)
end
context "with missing env values" do
it "raises an error if the network name is missing" do
env[:docker_connects] = {}
expect{subject.call(env)}.to raise_error(VagrantPlugins::DockerProvider::Errors::NetworkNameMissing)
end
end
end
describe "#generate_connect_cli_arguments" do
let(:network_options) {
{:ip=>"172.20.128.2",
:subnet=>"172.20.0.0/16",
:driver=>"bridge",
:internal=>"true",
:alias=>"mynetwork",
:protocol=>"tcp",
:id=>"80e017d5-388f-4a2f-a3de-f8dce8156a58"} }
let(:false_network_options) {
{:ip=>"172.20.128.2",
:subnet=>"172.20.0.0/16",
:driver=>"bridge",
:internal=>"false",
:alias=>"mynetwork",
:protocol=>"tcp",
:id=>"80e017d5-388f-4a2f-a3de-f8dce8156a58"} }
it "removes false values" do
cli_args = subject.generate_connect_cli_arguments(false_network_options)
expect(cli_args).to eq(["--ip", "172.20.128.2", "--subnet", "172.20.0.0/16", "--driver", "bridge", "--alias", "mynetwork", "--protocol", "tcp", "--id", "80e017d5-388f-4a2f-a3de-f8dce8156a58"])
end
it "removes true and leaves flag value in arguments" do
cli_args = subject.generate_connect_cli_arguments(network_options)
expect(cli_args).to eq(["--ip", "172.20.128.2", "--subnet", "172.20.0.0/16", "--driver", "bridge", "--internal", "--alias", "mynetwork", "--protocol", "tcp", "--id", "80e017d5-388f-4a2f-a3de-f8dce8156a58"])
end
it "takes options and generates cli flags" do
cli_args = subject.generate_connect_cli_arguments(network_options)
expect(cli_args).to eq(["--ip", "172.20.128.2", "--subnet", "172.20.0.0/16", "--driver", "bridge", "--internal", "--alias", "mynetwork", "--protocol", "tcp", "--id", "80e017d5-388f-4a2f-a3de-f8dce8156a58"])
end
end
end

View File

@ -0,0 +1,104 @@
require_relative "../../../../base"
require_relative "../../../../../../plugins/providers/docker/action/destroy_network"
describe VagrantPlugins::DockerProvider::Action::DestroyNetwork do
include_context "unit"
let(:sandbox) { isolated_environment }
let(:iso_env) do
# We have to create a Vagrantfile so there is a root path
sandbox.vagrantfile("")
sandbox.create_vagrant_env
end
let(:machine) do
iso_env.machine(iso_env.machine_names[0], :docker).tap do |m|
allow(m.provider).to receive(:driver).and_return(driver)
allow(m.config.vm).to receive(:networks).and_return(networks)
end
end
let(:env) {{ machine: machine, ui: machine.ui, root_path: Pathname.new(".") }}
let(:app) { lambda { |*args| }}
let(:driver) { double("driver", create: "abcd1234") }
let(:networks) { [[:private_network,
{:ip=>"172.20.128.2",
:subnet=>"172.20.0.0/16",
:driver=>"bridge",
:internal=>"true",
:alias=>"mynetwork",
:protocol=>"tcp",
:id=>"80e017d5-388f-4a2f-a3de-f8dce8156a58"}],
[:private_network,
{:type=>"dhcp",
:ipv6=>"true",
:subnet=>"2a02:6b8:b010:9020:1::/80",
:protocol=>"tcp",
:id=>"b8f23054-38d5-45c3-99ea-d33fc5d1b9f2"}],
[:forwarded_port,
{:guest=>22, :host=>2200, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}]]
}
subject { described_class.new(app, env) }
after do
sandbox.close
end
describe "#call" do
let(:network_names) { ["vagrant_network_172.20.0.0/16", "vagrant_network_2a02:6b8:b010:9020:1::/80"] }
it "calls the next action in the chain" do
allow(driver).to receive(:host_vm?).and_return(false)
allow(driver).to receive(:existing_network?).and_return(true)
allow(driver).to receive(:network_used?).and_return(true)
allow(driver).to receive(:list_network_names).and_return([])
called = false
app = ->(*args) { called = true }
action = described_class.new(app, env)
action.call(env)
expect(called).to eq(true)
end
it "calls the proper driver method to destroy the network" do
allow(driver).to receive(:list_network_names).and_return(network_names)
allow(driver).to receive(:host_vm?).and_return(false)
allow(driver).to receive(:existing_named_network?).with("vagrant_network_172.20.0.0/16").
and_return(true)
allow(driver).to receive(:network_used?).with("vagrant_network_172.20.0.0/16").
and_return(false)
allow(driver).to receive(:existing_named_network?).with("vagrant_network_2a02:6b8:b010:9020:1::/80").
and_return(true)
allow(driver).to receive(:network_used?).with("vagrant_network_2a02:6b8:b010:9020:1::/80").
and_return(false)
expect(driver).to receive(:rm_network).with("vagrant_network_172.20.0.0/16").twice
expect(driver).to receive(:rm_network).with("vagrant_network_2a02:6b8:b010:9020:1::/80").twice
subject.call(env)
end
it "doesn't destroy the network if another container is still using it" do
allow(driver).to receive(:host_vm?).and_return(false)
allow(driver).to receive(:list_network_names).and_return(network_names)
allow(driver).to receive(:existing_named_network?).with("vagrant_network_172.20.0.0/16").
and_return(true)
allow(driver).to receive(:network_used?).with("vagrant_network_172.20.0.0/16").
and_return(true)
allow(driver).to receive(:existing_named_network?).with("vagrant_network_2a02:6b8:b010:9020:1::/80").
and_return(true)
allow(driver).to receive(:network_used?).with("vagrant_network_2a02:6b8:b010:9020:1::/80").
and_return(true)
expect(driver).not_to receive(:rm_network).with("vagrant_network_172.20.0.0/16")
expect(driver).not_to receive(:rm_network).with("vagrant_network_2a02:6b8:b010:9020:1::/80")
subject.call(env)
end
end
end

View File

@ -0,0 +1,340 @@
require_relative "../../../../base"
require_relative "../../../../../../plugins/providers/docker/action/prepare_networks"
describe VagrantPlugins::DockerProvider::Action::PrepareNetworks do
include_context "unit"
include_context "virtualbox"
let(:sandbox) { isolated_environment }
let(:iso_env) do
# We have to create a Vagrantfile so there is a root path
sandbox.vagrantfile("")
sandbox.create_vagrant_env
end
let(:machine) do
iso_env.machine(iso_env.machine_names[0], :docker).tap do |m|
allow(m.provider).to receive(:driver).and_return(driver)
allow(m.config.vm).to receive(:networks).and_return(networks)
end
end
let(:env) {{ machine: machine, ui: machine.ui, root_path: Pathname.new(".") }}
let(:app) { lambda { |*args| }}
let(:driver) { double("driver", create: "abcd1234") }
let(:networks) { [[:private_network,
{:ip=>"172.20.128.2",
:subnet=>"172.20.0.0/16",
:driver=>"bridge",
:internal=>"true",
:alias=>"mynetwork",
:protocol=>"tcp",
:id=>"80e017d5-388f-4a2f-a3de-f8dce8156a58"}],
[:public_network,
{:ip=>"172.30.130.2",
:subnet=>"172.30.0.0/16",
:driver=>"bridge",
:id=>"30e017d5-488f-5a2f-a3ke-k8dce8246b60"}],
[:private_network,
{:type=>"dhcp",
:ipv6=>"true",
:subnet=>"2a02:6b8:b010:9020:1::/80",
:protocol=>"tcp",
:id=>"b8f23054-38d5-45c3-99ea-d33fc5d1b9f2"}],
[:forwarded_port,
{:guest=>22, :host=>2200, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}]]
}
let(:invalid_network) {
[[:private_network,
{:ipv6=>"true",
:protocol=>"tcp",
:id=>"b8f23054-38d5-45c3-99ea-d33fc5d1b9f2"}]]
}
subject { described_class.new(app, env) }
after do
sandbox.close
end
describe "#call" do
it "calls the next action in the chain" do
allow(driver).to receive(:host_vm?).and_return(false)
allow(driver).to receive(:existing_named_network?).and_return(false)
allow(driver).to receive(:create_network).and_return(true)
called = false
app = ->(*args) { called = true }
action = described_class.new(app, env)
allow(action).to receive(:process_public_network).and_return(["name", {}])
allow(action).to receive(:process_private_network).and_return(["name", {}])
action.call(env)
expect(called).to eq(true)
end
it "calls the proper driver methods to setup a network" do
allow(driver).to receive(:host_vm?).and_return(false)
allow(driver).to receive(:existing_named_network?).and_return(false)
allow(driver).to receive(:network_containing_address).
with("172.20.128.2").and_return(nil)
allow(driver).to receive(:network_containing_address).
with("192.168.1.1").and_return(nil)
allow(driver).to receive(:network_defined?).with("172.20.128.0/24").
and_return(false)
allow(driver).to receive(:network_defined?).with("172.30.128.0/24").
and_return(false)
allow(driver).to receive(:network_defined?).with("2a02:6b8:b010:9020:1::/80").
and_return(false)
allow(subject).to receive(:request_public_gateway).and_return("1234")
allow(subject).to receive(:request_public_iprange).and_return("1234")
expect(subject).to receive(:process_private_network).with(networks[0][1], {}, env).
and_return(["vagrant_network_172.20.128.0/24", {:ipv6=>false, :subnet=>"172.20.128.0/24"}])
expect(subject).to receive(:process_public_network).with(networks[1][1], {}, env).
and_return(["vagrant_network_public_wlp4s0", {"opt"=>"parent=wlp4s0", "subnet"=>"192.168.1.0/24", "driver"=>"macvlan", "gateway"=>"1234", "ipv6"=>false, "ip_range"=>"1234"}])
expect(subject).to receive(:process_private_network).with(networks[2][1], {}, env).
and_return(["vagrant_network_2a02:6b8:b010:9020:1::/80", {:ipv6=>true, :subnet=>"2a02:6b8:b010:9020:1::/80"}])
allow(machine.ui).to receive(:ask).and_return("1")
expect(driver).to receive(:create_network).
with("vagrant_network_172.20.128.0/24", ["--subnet", "172.20.128.0/24"])
expect(driver).to receive(:create_network).
with("vagrant_network_public_wlp4s0", ["--opt", "parent=wlp4s0", "--subnet", "192.168.1.0/24", "--driver", "macvlan", "--gateway", "1234", "--ip-range", "1234"])
expect(driver).to receive(:create_network).
with("vagrant_network_2a02:6b8:b010:9020:1::/80", ["--ipv6", "--subnet", "2a02:6b8:b010:9020:1::/80"])
subject.call(env)
expect(env[:docker_connects]).to eq({0=>"vagrant_network_172.20.128.0/24", 1=>"vagrant_network_public_wlp4s0", 2=>"vagrant_network_2a02:6b8:b010:9020:1::/80"})
end
it "uses an existing network if a matching subnet is found" do
allow(driver).to receive(:host_vm?).and_return(false)
allow(driver).to receive(:network_containing_address).
with("172.20.128.2").and_return(nil)
allow(driver).to receive(:network_containing_address).
with("192.168.1.1").and_return(nil)
allow(driver).to receive(:network_defined?).with("172.20.128.0/24").
and_return("vagrant_network_172.20.128.0/24")
allow(driver).to receive(:network_defined?).with("172.30.128.0/24").
and_return("vagrant_network_public_wlp4s0")
allow(driver).to receive(:network_defined?).with("2a02:6b8:b010:9020:1::/80").
and_return("vagrant_network_2a02:6b8:b010:9020:1::/80")
allow(machine.ui).to receive(:ask).and_return("1")
expect(driver).to receive(:existing_named_network?).
with("vagrant_network_172.20.128.0/24").and_return(true)
expect(driver).to receive(:existing_named_network?).
with("vagrant_network_public_wlp4s0").and_return(true)
expect(driver).to receive(:existing_named_network?).
with("vagrant_network_2a02:6b8:b010:9020:1::/80").and_return(true)
expect(subject).to receive(:process_private_network).with(networks[0][1], {}, env).
and_return(["vagrant_network_172.20.128.0/24", {:ipv6=>false, :subnet=>"172.20.128.0/24"}])
expect(subject).to receive(:process_public_network).with(networks[1][1], {}, env).
and_return(["vagrant_network_public_wlp4s0", {"opt"=>"parent=wlp4s0", "subnet"=>"192.168.1.0/24", "driver"=>"macvlan", "gateway"=>"1234", "ipv6"=>false, "ip_range"=>"1234"}])
expect(subject).to receive(:process_private_network).with(networks[2][1], {}, env).
and_return(["vagrant_network_2a02:6b8:b010:9020:1::/80", {:ipv6=>true, :subnet=>"2a02:6b8:b010:9020:1::/80"}])
expect(driver).not_to receive(:create_network)
expect(subject).to receive(:validate_network_configuration!).
with("vagrant_network_172.20.128.0/24", networks[0][1],
{:ipv6=>false, :subnet=>"172.20.128.0/24"}, driver)
expect(subject).to receive(:validate_network_configuration!).
with("vagrant_network_public_wlp4s0", networks[1][1],
{"opt"=>"parent=wlp4s0", "subnet"=>"192.168.1.0/24", "driver"=>"macvlan", "gateway"=>"1234", "ipv6"=>false, "ip_range"=>"1234"}, driver)
expect(subject).to receive(:validate_network_configuration!).
with("vagrant_network_2a02:6b8:b010:9020:1::/80", networks[2][1],
{:ipv6=>true, :subnet=>"2a02:6b8:b010:9020:1::/80"}, driver)
subject.call(env)
end
it "raises an error if an inproper network configuration is given" do
allow(machine.config.vm).to receive(:networks).and_return(invalid_network)
allow(driver).to receive(:host_vm?).and_return(false)
allow(driver).to receive(:existing_network?).and_return(false)
expect{ subject.call(env) }.to raise_error(VagrantPlugins::DockerProvider::Errors::NetworkIPAddressRequired)
end
end
describe "#generate_create_cli_arguments" do
let(:network_options) {
{:ip=>"172.20.128.2",
:subnet=>"172.20.0.0/16",
:driver=>"bridge",
:internal=>"true",
:alias=>"mynetwork",
:protocol=>"tcp",
:id=>"80e017d5-388f-4a2f-a3de-f8dce8156a58"} }
let(:false_network_options) {
{:ip=>"172.20.128.2",
:subnet=>"172.20.0.0/16",
:driver=>"bridge",
:internal=>"false",
:alias=>"mynetwork",
:protocol=>"tcp",
:id=>"80e017d5-388f-4a2f-a3de-f8dce8156a58"} }
it "returns an array of cli arguments" do
cli_args = subject.generate_create_cli_arguments(network_options)
expect(cli_args).to eq( ["--ip", "172.20.128.2", "--subnet", "172.20.0.0/16", "--driver", "bridge", "--internal", "--alias", "mynetwork", "--protocol", "tcp", "--id", "80e017d5-388f-4a2f-a3de-f8dce8156a58"])
end
it "removes option if set to false" do
cli_args = subject.generate_create_cli_arguments(false_network_options)
expect(cli_args).to eq( ["--ip", "172.20.128.2", "--subnet", "172.20.0.0/16", "--driver", "bridge", "--alias", "mynetwork", "--protocol", "tcp", "--id", "80e017d5-388f-4a2f-a3de-f8dce8156a58"])
end
end
describe "#validate_network_name!" do
let(:netname) { "vagrant_network" }
it "returns true if name exists" do
allow(driver).to receive(:existing_named_network?).with(netname).
and_return(true)
expect(subject.validate_network_name!(netname, env)).to be_truthy
end
it "raises an error if name does not exist" do
allow(driver).to receive(:existing_named_network?).with(netname).
and_return(false)
expect{subject.validate_network_name!(netname, env)}.to raise_error(VagrantPlugins::DockerProvider::Errors::NetworkNameUndefined)
end
end
describe "#validate_network_configuration!" do
let(:netname) { "vagrant_network_172.20.128.0/24" }
let(:options) { {:ip=>"172.20.128.2", :subnet=>"172.20.0.0/16", :driver=>"bridge", :internal=>"true", :alias=>"mynetwork", :protocol=>"tcp", :id=>"80e017d5-388f-4a2f-a3de-f8dce8156a58", :netmask=>24} }
let(:network_options) { {:ipv6=>false, :subnet=>"172.20.128.0/24"} }
it "returns true if all options are valid" do
allow(driver).to receive(:network_containing_address).with(options[:ip]).
and_return(netname)
allow(driver).to receive(:network_containing_address).with(network_options[:subnet]).
and_return(netname)
expect(subject.validate_network_configuration!(netname, options, network_options, driver)).
to be_truthy
end
it "raises an error of the address is invalid" do
allow(driver).to receive(:network_containing_address).with(options[:ip]).
and_return("fakename")
expect{subject.validate_network_configuration!(netname, options, network_options, driver)}.
to raise_error(VagrantPlugins::DockerProvider::Errors::NetworkAddressInvalid)
end
it "raises an error of the subnet is invalid" do
allow(driver).to receive(:network_containing_address).with(options[:ip]).
and_return(netname)
allow(driver).to receive(:network_containing_address).with(network_options[:subnet]).
and_return("fakename")
expect{subject.validate_network_configuration!(netname, options, network_options, driver)}.
to raise_error(VagrantPlugins::DockerProvider::Errors::NetworkSubnetInvalid)
end
end
describe "#process_private_network" do
let(:options) { {:ip=>"172.20.128.2", :subnet=>"172.20.0.0/16", :driver=>"bridge", :internal=>"true", :alias=>"mynetwork", :protocol=>"tcp", :id=>"80e017d5-388f-4a2f-a3de-f8dce8156a58", :netmask=>24} }
let(:dhcp_options) { {type: "dhcp"} }
let(:bad_options) { {driver: "bridge"} }
it "generates a network name and config for a dhcp private network" do
network_name, network_options = subject.process_private_network(dhcp_options, {}, env)
expect(network_name).to eq("vagrant_network")
expect(network_options).to eq({})
end
it "generates a network name and options for a static ip" do
allow(driver).to receive(:network_defined?).and_return(nil)
network_name, network_options = subject.process_private_network(options, {}, env)
expect(network_name).to eq("vagrant_network_172.20.0.0/16")
expect(network_options).to eq({:ipv6=>false, :subnet=>"172.20.0.0/16"})
end
it "raises an error if no ip address or type `dhcp` was given" do
expect{subject.process_private_network(bad_options, {}, env)}.
to raise_error(VagrantPlugins::DockerProvider::Errors::NetworkIPAddressRequired)
end
end
describe "#process_public_network" do
let(:options) { {:ip=>"172.30.130.2", :subnet=>"172.30.0.0/16", :driver=>"bridge", :id=>"30e017d5-488f-5a2f-a3ke-k8dce8246b60"} }
let(:ipaddr) { double("ipaddr", prefix: 22, succ: "10.1.10.2", ipv6?: false) }
it "raises an error if there are no network interfaces" do
expect(subject).to receive(:list_interfaces).and_return([])
expect{subject.process_public_network(options, {}, env)}.
to raise_error(VagrantPlugins::DockerProvider::Errors::NetworkNoInterfaces)
end
it "generates a network name and configuration" do
allow(machine.ui).to receive(:ask).and_return("1")
allow(subject).to receive(:request_public_gateway).and_return("1234")
allow(subject).to receive(:request_public_iprange).and_return("1234")
allow(IPAddr).to receive(:new).and_return(ipaddr)
allow(driver).to receive(:existing_named_network?).and_return(false)
allow(driver).to receive(:network_containing_address).
with("10.1.10.2").and_return("vagrant_network_public")
network_name, network_options = subject.process_public_network(options, {}, env)
expect(network_name).to eq("vagrant_network_public")
end
end
describe "#request_public_gateway" do
let(:options) { {:ip=>"172.30.130.2", :subnet=>"172.30.0.0/16", :driver=>"bridge", :id=>"30e017d5-488f-5a2f-a3ke-k8dce8246b60"} }
let(:ipaddr) { double("ipaddr", to_s: "172.30.130.2", prefix: 22, succ: "172.30.130.3",
ipv6?: false) }
it "requests a gateway" do
allow(IPAddr).to receive(:new).and_return(ipaddr)
allow(ipaddr).to receive(:include?).and_return(false)
allow(machine.ui).to receive(:ask).and_return("1")
addr = subject.request_public_gateway(options, "bridge", env)
expect(addr).to eq("172.30.130.2")
end
end
describe "#request_public_iprange" do
let(:options) { {:ip=>"172.30.130.2", :subnet=>"172.30.0.0/16", :driver=>"bridge", :id=>"30e017d5-488f-5a2f-a3ke-k8dce8246b60"} }
let(:ipaddr) { double("ipaddr", to_s: "172.30.100.2", prefix: 22, succ: "172.30.100.3",
ipv6?: false) }
let(:subnet) { double("ipaddr", to_s: "172.30.130.2", prefix: 22, succ: "172.30.130.3",
ipv6?: false) }
it "requests a public ip range" do
allow(IPAddr).to receive(:new).with(options[:subnet]).and_return(subnet)
allow(IPAddr).to receive(:new).with("172.30.130.2").and_return(ipaddr)
allow(subnet).to receive(:include?).and_return(true)
allow(machine.ui).to receive(:ask).and_return(options[:ip])
addr = subject.request_public_iprange(options, "bridge", env)
end
end
end

View File

@ -10,6 +10,149 @@ describe VagrantPlugins::DockerProvider::Driver do
allow(subject).to receive(:execute) { |*args| @cmd = args.join(' ') }
end
let(:docker_network_struct) {
[
{
"Name": "bridge",
"Id": "ae74f6cc18bbcde86326937797070b814cc71bfc4a6d8e3e8cf3b2cc5c7f4a7d",
"Created": "2019-03-20T14:10:06.313314662-07:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": nil,
"Config": [
{
"Subnet": "172.17.0.0/16",
"Gateway": "172.17.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"a1ee9b12bcea8268495b1f43e8d1285df1925b7174a695075f6140adb9415d87": {
"Name": "vagrant-sandbox_docker-1_1553116237",
"EndpointID": "fc1b0ed6e4f700cf88bb26a98a0722655191542e90df3e3492461f4d1f3c0cae",
"MacAddress": "02:42:ac:11:00:02",
"IPv4Address": "172.17.0.2/16",
"IPv6Address": ""
}
},
"Options": {
"com.docker.network.bridge.default_bridge": "true",
"com.docker.network.bridge.enable_icc": "true",
"com.docker.network.bridge.enable_ip_masquerade": "true",
"com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
"com.docker.network.bridge.name": "docker0",
"com.docker.network.driver.mtu": "1500"
},
"Labels": {}
},
{
"Name": "host",
"Id": "2a2845e77550e33bf3e97bda8b71477ac7d3ccf78bc9102585fdb6056fb84cbf",
"Created": "2018-09-28T10:54:08.633543196-07:00",
"Scope": "local",
"Driver": "host",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": nil,
"Config": []
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {},
"Options": {},
"Labels": {}
},
{
"Name": "vagrant_network",
"Id": "93385d4fd3cf7083a36e62fa72a0ad0a21203d0ddf48409c32b550cd8462b3ba",
"Created": "2019-03-20T14:10:36.828235585-07:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"a1ee9b12bcea8268495b1f43e8d1285df1925b7174a695075f6140adb9415d87": {
"Name": "vagrant-sandbox_docker-1_1553116237",
"EndpointID": "9502cd9d37ae6815e3ffeb0bc2de9b84f79e7223e8a1f8f4ccc79459e96c7914",
"MacAddress": "02:42:ac:12:00:02",
"IPv4Address": "172.18.0.2/16",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {}
},
{
"Name": "vagrant_network_172.20.0.0/16",
"Id": "649f0ab3ef0eef6f2a025c0d0398bd7b9b4d05ec88b0d7bd573b44153d903cfb",
"Created": "2019-03-20T14:10:37.088885647-07:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.20.0.0/16"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"a1ee9b12bcea8268495b1f43e8d1285df1925b7174a695075f6140adb9415d87": {
"Name": "vagrant-sandbox_docker-1_1553116237",
"EndpointID": "e19156f8018f283468227fa97c145f4ea0eaba652fb7e977a0c759b1c3ec168a",
"MacAddress": "02:42:ac:14:80:02",
"IPv4Address": "172.20.0.2/16",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {}
}
].to_json }
describe '#create' do
let(:params) { {
image: 'jimi/hendrix:electric-ladyland',
@ -251,4 +394,139 @@ describe VagrantPlugins::DockerProvider::Driver do
expect(subject.docker_bridge_ip).to eq('123.456.789.012')
end
end
describe '#docker_connect_network' do
let(:opts) { ["--ip", "172.20.128.2"] }
it 'connects a network to a container' do
expect(subject).to receive(:execute).with("docker", "network", "connect", "vagrant_network", cid, "--ip", "172.20.128.2")
subject.connect_network("vagrant_network", cid, opts)
end
end
describe '#docker_create_network' do
let(:opts) { ["--subnet", "172.20.0.0/16"] }
it 'creates a network' do
expect(subject).to receive(:execute).with("docker", "network", "create", "vagrant_network", "--subnet", "172.20.0.0/16")
subject.create_network("vagrant_network", opts)
end
end
describe '#docker_disconnet_network' do
it 'disconnects a network from a container' do
expect(subject).to receive(:execute).with("docker", "network", "disconnect", "vagrant_network", cid, "--force")
subject.disconnect_network("vagrant_network", cid)
end
end
describe '#docker_inspect_network' do
it 'gets info about a network' do
expect(subject).to receive(:execute).with("docker", "network", "inspect", "vagrant_network")
subject.inspect_network("vagrant_network")
end
end
describe '#docker_list_network' do
it 'lists docker networks' do
expect(subject).to receive(:execute).with("docker", "network", "ls")
subject.list_network()
end
end
describe '#docker_rm_network' do
it 'deletes a docker network' do
expect(subject).to receive(:execute).with("docker", "network", "rm", "vagrant_network")
subject.rm_network("vagrant_network")
end
end
describe '#network_defined?' do
let(:subnet_string) { "172.20.0.0/16" }
let(:network_names) { ["vagrant_network_172.20.0.0/16", "bridge", "null" ] }
it "returns network name if defined" do
allow(subject).to receive(:list_network_names).and_return(network_names)
allow(subject).to receive(:inspect_network).and_return(JSON.load(docker_network_struct))
network_name = subject.network_defined?(subnet_string)
expect(network_name).to eq("vagrant_network_172.20.0.0/16")
end
it "returns nil name if not defined" do
allow(subject).to receive(:list_network_names).and_return(network_names)
allow(subject).to receive(:inspect_network).and_return(JSON.load(docker_network_struct))
network_name = subject.network_defined?("120.20.0.0/24")
expect(network_name).to eq(nil)
end
end
describe '#network_containing_address' do
let(:address) { "172.20.128.2" }
let(:network_names) { ["vagrant_network_172.20.0.0/16", "bridge", "null" ] }
it "returns the network name if it contains the requested address" do
allow(subject).to receive(:list_network_names).and_return(network_names)
allow(subject).to receive(:inspect_network).and_return(JSON.load(docker_network_struct))
network_name = subject.network_containing_address(address)
expect(network_name).to eq("vagrant_network_172.20.0.0/16")
end
it "returns nil if no networks contain the requested address" do
allow(subject).to receive(:list_network_names).and_return(network_names)
allow(subject).to receive(:inspect_network).and_return(JSON.load(docker_network_struct))
network_name = subject.network_containing_address("127.0.0.1")
expect(network_name).to eq(nil)
end
end
describe '#existing_named_network?' do
let(:network_names) { ["vagrant_network_172.20.0.0/16", "bridge", "null" ] }
it "returns true if the network exists" do
allow(subject).to receive(:list_network_names).and_return(network_names)
expect(subject.existing_named_network?("vagrant_network_172.20.0.0/16")).to be_truthy
end
it "returns false if the network does not exist" do
allow(subject).to receive(:list_network_names).and_return(network_names)
expect(subject.existing_named_network?("vagrant_network_17.0.0/16")).to be_falsey
end
end
describe '#list_network_names' do
let(:unparsed_network_names) { "vagrant_network_172.20.0.0/16\nbridge\nnull" }
let(:network_names) { ["vagrant_network_172.20.0.0/16", "bridge", "null" ] }
it "lists the network names" do
allow(subject).to receive(:list_network).with("--format={{.Name}}").
and_return(unparsed_network_names)
expect(subject.list_network_names).to eq(network_names)
end
end
describe '#network_used?' do
let(:network_name) { "vagrant_network_172.20.0.0/16" }
it "returns nil if no networks" do
allow(subject).to receive(:inspect_network).with(network_name).and_return(nil)
expect(subject.network_used?(network_name)).to eq(nil)
end
it "returns true if network has containers in use" do
allow(subject).to receive(:inspect_network).with(network_name).and_return([JSON.load(docker_network_struct).last])
expect(subject.network_used?(network_name)).to be_truthy
end
it "returns false if network has containers in use" do
allow(subject).to receive(:inspect_network).with("host").and_return([JSON.load(docker_network_struct)[1]])
expect(subject.network_used?("host")).to be_falsey
end
end
end

View File

@ -73,8 +73,6 @@ This helps keep your Vagrantfile similar to how it has always looked.
The Docker provider does not support specifying options for `owner` or `group`
on folders synced with a docker container.
Private and public networks are not currently supported.
### Volume Consistency
Docker's [volume consistency](https://docs.docker.com/v17.09/engine/admin/volumes/bind-mounts/) setting can be specified using the `docker_consistency` option when defining a synced folder. This can

View File

@ -0,0 +1,290 @@
---
layout: "docs"
page_title: "Networking - Docker Provider"
sidebar_current: "providers-docker-networking"
description: |-
The Vagrant Docker provider supports using the private network using the
`docker network` commands.
---
# Networking
Vagrant uses the `docker network` command under the hood to create and manage
networks for containers. Vagrant will do its best to create and manage networks
for any containers configured inside the Vagrantfile. Each docker network is grouped
by the subnet used for a requested ip address.
For each newly unique network, Vagrant will run the `docker network create` subcommand
with the provided options from the network config inside your Vagrantfile. If multiple
networks share the same subnet, Vagrant will reuse that existing network for multiple
containers. Once these networks have been created, Vagrant will attach these
networks to the requested containers using the `docker network connect` for each
network.
Vagrant names the networks inside docker as `vagrant_network` or `vagrant_network_<subnet here>`
where `<subnet_here>` is the subnet for the network if defined by the user. An
example of these networks is shown later in this page. If no subnet is requested
for the network, Vagrant will connect the `vagrant_network` to the container.
When destroying containers through Vagrant, Vagrant will clean up the network if
there are no more containers using the network.
## Docker Network Options
Most of the options work similar to other Vagrant providers. Defining either an
ip or using `type: 'dhcp'` will give you a network on your container.
```ruby
docker.vm.network :private_network, type: "dhcp"
docker.vm.network :private_network, ip: "172.20.128.2"
```
If you want to set something specific with a new network you can use scoped options
which align with the command line flags for the [docker network create](https://docs.docker.com/engine/reference/commandline/network_create/)
command. If there are any specific options you want to enable from the `docker network create`
command, you can specify them like this:
```ruby
docker.vm.network :private_network, type: "dhcp", docker_network__internal: true
```
This will enable the `internal` option for the network when created with `docker network create`.
Where `option` corresponds to the given flag that will be provided to the `docker network create`
command. Similarly, if there is a value you wish to enable when connecting a container
to a given network, you can use the following value in your network config:
```ruby
docker_connect__option: "value"
```
When the docker provider creates a new network a netmask is required. If the netmask
is not provided, Vagrant will default to a `/24` for IPv4 and `/64` for IPv6. To provide
a different mask, set it using the `netmask` option:
```ruby
docker.vm.network :private_network, ip: "172.20.128.2", netmask: 16
```
For networks which set the type to "dhcp", it is also possible to specify a specific
subnet for the network connection. This allows containers to connect to networks other
than the default `vagrant_network` network. The docker provider supports specifying
the desired subnet in two ways. The first is by using the `ip` and `netmask` options:
```ruby
docker.vm.network :private_network, type: "dhcp", ip: "172.20.128.0", netmask: 24
```
The second is by using the `subnet` option:
```ruby
docker.vm.network :private_network, type: "dhcp", subnet: "172.20.128.0/24"
```
### Public Networks
The Vagrant docker provider also supports defining public networks. The easiest way
to define a public network is by setting the `type` option to "dhcp":
```ruby
docker.vm.network :public_network, type: "dhcp"
```
A bridge interface is required when setting up a public network. When no bridge
device name is provided, Vagrant will prompt for the appropriate device to use. This
can also be set using the `bridge` option:
```ruby
docker.vm.network :public_network, type: "dhcp", bridge: "eth0"
```
The `bridge` option also supports a list of interfaces which can be used for
setting up the network. Vagrant will inspect the defined interfaces and use
the first active interface when setting up the network:
```ruby
docker.vm.network :public_network, type: "dhcp", bridge: ["eth0", "wlan0"]
```
The available IP range for the bridge interface must be known when setting up
the docker network. Even though a DHCP service may be available on the public
network, docker will manage IP addresses provided to containers. This means
that the subnet provided when defining the available IP range for the network
should not be included within the subnet managed by the DHCP service. Vagrant
will prompt for the available IP range information, however, it can also be
provided in the Vagrantfile using the `docker_network__ip_range` option:
```ruby
docker.vm.network :public_network, type: "dhcp", bridge: "eth0", docker_network__ip_range: "192.168.1.252/30"
```
Finally, the gateway for the interface is required during setup. The docker
provider will default the gateway address to the first address available for
the subnet of the bridge device. Vagrant will prompt for confirmation to use
the default address. The address can also be manually set in the Vagrantfile
using the `docker_network__gateway` option:
```ruby
docker.vm.network :public_network, type: "dhcp", bridge: "eth0", docker_network__gateway: "192.168.1.2"
```
More examples are shared below which demonstrate creating a few common network
interfaces.
## Docker Network Example
The following Vagrantfile will generate these networks for a container:
1. A IPv4 IP address assigned by DHCP
2. A IPv4 IP address 172.20.128.2 on a network with subnet 172.20.0.0/16
3. A IPv6 IP address assigned by DHCP on subnet 2a02:6b8:b010:9020:1::/80
```ruby
Vagrant.configure("2") do |config|
config.vm.define "docker" do |docker|
docker.vm.network :private_network, type: "dhcp", docker_network__internal: true
docker.vm.network :private_network,
ip: "172.20.128.2", netmask: "16"
docker.vm.network :private_network, type: "dhcp", subnet: "2a02:6b8:b010:9020:1::/80"
docker.vm.provider "docker" do |d|
d.build_dir = "docker_build_dir"
end
end
end
```
You can test that your container has the proper configured networks by looking
at the result of running `ip addr`, for example:
```
brian@localghost:vagrant-sandbox % docker ps ±[●][master]
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
370f4e5d2217 196a06ef12f5 "tail -f /dev/null" 5 seconds ago Up 3 seconds 80/tcp, 443/tcp vagrant-sandbox_docker-1_1551810440
brian@localghost:vagrant-sandbox % docker exec 370f4e5d2217 ip addr ±[●][master]
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
24: eth0@if25: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
27: eth1@if28: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:13:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.19.0.2/16 brd 172.19.255.255 scope global eth1
valid_lft forever preferred_lft forever
30: eth2@if31: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:14:80:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.20.128.2/16 brd 172.20.255.255 scope global eth2
valid_lft forever preferred_lft forever
33: eth3@if34: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:15:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.21.0.2/16 brd 172.21.255.255 scope global eth3
valid_lft forever preferred_lft forever
inet6 2a02:6b8:b010:9020:1::2/80 scope global nodad
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe15:2/64 scope link
valid_lft forever preferred_lft forever
```
You can also connect your containers to a docker network that was created outside
of Vagrant:
```
$ docker network create my-custom-network --subnet=172.20.0.0/16
```
```ruby
Vagrant.configure("2") do |config|
config.vm.define "docker" do |docker|
docker.vm.network :private_network, type: "dhcp" name: "my-custom-network"
docker.vm.provider "docker" do |d|
d.build_dir = "docker_build_dir"
end
end
end
```
Vagrant will not delete or modify these outside networks when deleting the container, however.
## Useful Debugging Tips
The `docker network` command provides some helpful insights to what might be going
on with the networks Vagrant creates. For example, if you want to know what networks
you currently have running on your machine, you can run the `docker network ls` command:
```
brian@localghost:vagrant-sandbox % docker network ls ±[●][master]
NETWORK ID NAME DRIVER SCOPE
a2bfc26bd876 bridge bridge local
2a2845e77550 host host local
f36682aeba68 none null local
00d4986c7dc2 vagrant_network bridge local
d02420ff4c39 vagrant_network_2a02:6b8:b010:9020:1::/80 bridge local
799ae9dbaf98 vagrant_network_172.20.0.0/16 bridge local
```
You can also inspect any network for more information:
```
brian@localghost:vagrant-sandbox % docker network inspect vagrant_network ±[●][master]
[
{
"Name": "vagrant_network",
"Id": "00d4986c7dc2ed7bf1961989ae1cfe98504c711f9de2f547e5dfffe2bb819fc2",
"Created": "2019-03-05T10:27:21.558824922-08:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.19.0.0/16",
"Gateway": "172.19.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"370f4e5d2217e698b16376583fbf051dd34018e5fd18958b604017def92fea63": {
"Name": "vagrant-sandbox_docker-1_1551810440",
"EndpointID": "166b7ca8960a9f20a150bb75a68d07e27e674781ed9f916e9aa58c8bc2539a61",
"MacAddress": "02:42:ac:13:00:02",
"IPv4Address": "172.19.0.2/16",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {}
}
]
```
## Caveats
For now, Vagrant only looks at the subnet when figuring out if it should create
a new network for a guest container. If you bring up a container with a network,
and then change or add some new options (but leave the subnet the same), it will
not apply those changes or create a new network.
Because the `--link` flag for the `docker network connect` command is considered
legacy, Vagrant does not support that option when creating containers and connecting
networks.
## More Information
For more information on how docker manages its networks, please refer to their
documentation:
- https://docs.docker.com/network/
- https://docs.docker.com/engine/reference/commandline/network/

View File

@ -166,6 +166,7 @@
<li<%= sidebar_current("providers-docker-commands") %>><a href="/docs/docker/commands.html">Commands</a></li>
<li<%= sidebar_current("providers-docker-boxes") %>><a href="/docs/docker/boxes.html">Boxes</a></li>
<li<%= sidebar_current("providers-docker-configuration") %>><a href="/docs/docker/configuration.html">Configuration</a></li>
<li<%= sidebar_current("providers-docker-networking") %>><a href="/docs/docker/networking.html">Networking</a></li>
</ul>
</li>
<li<%= sidebar_current("providers-hyperv") %>>