Brian Cain 5ed5868067
Inspect networks before creating new ones
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.
2019-03-12 10:36:57 -07:00

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