This commit updates the behavior of how the docker provider creates new docker networks. It looks at each existing network to see if the requested subnet has already been configured in the docker engine. If so, Vagrant will use that network rather than creating a new one. This includes networks not created by Vagrant. Vagrant will not clean up these networks if created outside of Vagrant.
300 lines
9.8 KiB
Ruby
300 lines
9.8 KiB
Ruby
require "json"
|
|
require "log4r"
|
|
|
|
require_relative "./driver/compose"
|
|
|
|
module VagrantPlugins
|
|
module DockerProvider
|
|
class Driver
|
|
# The executor is responsible for actually executing Docker commands.
|
|
# This is set by the provider, but defaults to local execution.
|
|
attr_accessor :executor
|
|
|
|
def initialize
|
|
@logger = Log4r::Logger.new("vagrant::docker::driver")
|
|
@executor = Executor::Local.new
|
|
end
|
|
|
|
def build(dir, **opts, &block)
|
|
args = Array(opts[:extra_args])
|
|
args << dir
|
|
result = execute('docker', 'build', *args, &block)
|
|
matches = result.scan(/Successfully built (.+)$/i)
|
|
if matches.empty?
|
|
# This will cause a stack trace in Vagrant, but it is a bug
|
|
# if this happens anyways.
|
|
raise "UNKNOWN OUTPUT: #{result}"
|
|
end
|
|
|
|
# Return the last match, and the capture of it
|
|
matches[-1][0]
|
|
end
|
|
|
|
def create(params, **opts, &block)
|
|
image = params.fetch(:image)
|
|
links = params.fetch(:links)
|
|
ports = Array(params[:ports])
|
|
volumes = Array(params[:volumes])
|
|
name = params.fetch(:name)
|
|
cmd = Array(params.fetch(:cmd))
|
|
env = params.fetch(:env)
|
|
expose = Array(params[:expose])
|
|
|
|
run_cmd = %W(docker run --name #{name})
|
|
run_cmd << "-d" if params[:detach]
|
|
run_cmd += env.map { |k,v| ['-e', "#{k}=#{v}"] }
|
|
run_cmd += expose.map { |p| ['--expose', "#{p}"] }
|
|
run_cmd += links.map { |k, v| ['--link', "#{k}:#{v}"] }
|
|
run_cmd += ports.map { |p| ['-p', p.to_s] }
|
|
run_cmd += volumes.map { |v|
|
|
v = v.to_s
|
|
if v.include?(":") && @executor.windows?
|
|
if v.index(":") != v.rindex(":")
|
|
# If we have 2 colons, the host path is an absolute Windows URL
|
|
# and we need to remove the colon from it
|
|
host, colon, guest = v.rpartition(":")
|
|
host = "//" + host[0].downcase + host[2..-1]
|
|
v = [host, guest].join(":")
|
|
else
|
|
host, guest = v.split(":", 2)
|
|
host = Vagrant::Util::Platform.windows_path(host)
|
|
# NOTE: Docker does not support UNC style paths (which also
|
|
# means that there's no long path support). Hopefully this
|
|
# will be fixed someday and the gsub below can be removed.
|
|
host.gsub!(/^[^A-Za-z]+/, "")
|
|
v = [host, guest].join(":")
|
|
end
|
|
end
|
|
|
|
['-v', v.to_s]
|
|
}
|
|
run_cmd += %W(--privileged) if params[:privileged]
|
|
run_cmd += %W(-h #{params[:hostname]}) if params[:hostname]
|
|
run_cmd << "-t" if params[:pty]
|
|
run_cmd << "--rm=true" if params[:rm]
|
|
run_cmd += params[:extra_args] if params[:extra_args]
|
|
run_cmd += [image, cmd]
|
|
|
|
execute(*run_cmd.flatten, **opts, &block).chomp.lines.last
|
|
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', '--no-trunc').to_s
|
|
result =~ /^#{Regexp.escape cid}$/
|
|
end
|
|
|
|
def image?(id)
|
|
result = execute('docker', 'images', '-q').to_s
|
|
result =~ /^#{Regexp.escape(id)}$/
|
|
end
|
|
|
|
def running?(cid)
|
|
result = execute('docker', 'ps', '-q', '--no-trunc')
|
|
result =~ /^#{Regexp.escape cid}$/m
|
|
end
|
|
|
|
def privileged?(cid)
|
|
inspect_container(cid)['HostConfig']['Privileged']
|
|
end
|
|
|
|
def login(email, username, password, server)
|
|
cmd = %W(docker login)
|
|
cmd += ["-e", email] if email != ""
|
|
cmd += ["-u", username] if username != ""
|
|
cmd += ["-p", password] if password != ""
|
|
cmd << server if server && server != ""
|
|
|
|
execute(*cmd.flatten)
|
|
end
|
|
|
|
def logout(server)
|
|
cmd = %W(docker logout)
|
|
cmd << server if server && server != ""
|
|
execute(*cmd.flatten)
|
|
end
|
|
|
|
def pull(image)
|
|
execute('docker', 'pull', image)
|
|
end
|
|
|
|
def start(cid)
|
|
if !running?(cid)
|
|
execute('docker', 'start', cid)
|
|
# This resets the cached information we have around, allowing `vagrant reload`s
|
|
# to work properly
|
|
@data = nil
|
|
end
|
|
end
|
|
|
|
def stop(cid, timeout)
|
|
if running?(cid)
|
|
execute('docker', 'stop', '-t', timeout.to_s, cid)
|
|
end
|
|
end
|
|
|
|
def rm(cid)
|
|
if created?(cid)
|
|
execute('docker', 'rm', '-f', '-v', cid)
|
|
end
|
|
end
|
|
|
|
def rmi(id)
|
|
execute('docker', 'rmi', id)
|
|
return true
|
|
rescue Exception => e
|
|
return false if e.to_s.include?("is using it")
|
|
raise if !e.to_s.include?("No such image")
|
|
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', '--no-trunc').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
|
|
|
|
# @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)
|
|
output
|
|
end
|
|
|
|
# @param [Array] opts - An array of flags used for listing networks
|
|
def list_network(opts=nil)
|
|
command = ['docker', 'network', 'ls'].push(*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
|
|
|
|
# TODO: Note...cli can optionally take a list of networks to delete.
|
|
# We might need this later, but for now our helper takes 1 network at a time
|
|
#
|
|
# @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
|
|
# ######################
|
|
|
|
# @param [String] subnet_string - Subnet to look for
|
|
def subnet_defined?(subnet_string)
|
|
all_networks = list_network(["--format={{.Name}}"])
|
|
all_networks = all_networks.split("\n")
|
|
|
|
results = inspect_network(all_networks)
|
|
begin
|
|
networks_info = JSON.parse(results)
|
|
networks_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
|
|
rescue JSON::ParserError => e
|
|
@logger.warn("Could not properly parse response from `docker network inspect #{all_networks.join(" ")}`")
|
|
end
|
|
return nil
|
|
end
|
|
|
|
# Looks to see if a docker network has already been defined
|
|
#
|
|
# @param [String] network - name of network to look for
|
|
def existing_network?(network)
|
|
result = list_network(["--format={{.Name}}"])
|
|
#TODO: we should be more explicit here if we can
|
|
result.match?(/#{network}/)
|
|
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)
|
|
begin
|
|
result = JSON.parse(result)
|
|
return result.first["Containers"].size > 0
|
|
rescue JSON::ParserError => e
|
|
@logger.warn("Could not properly parse response from `docker network inspect #{network}`")
|
|
return nil
|
|
end
|
|
end
|
|
|
|
end
|
|
end
|
|
end
|