vagrant/plugins/providers/docker/driver.rb

342 lines
11 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
# Returns the id for a new container built from `docker build`. Raises
# an exception if the id was unable to be captured from the output
#
# @return [String] id - ID matched from the docker build output.
def build(dir, **opts, &block)
args = Array(opts[:extra_args])
args << dir
opts = {with_stderr: true}
result = execute('docker', 'build', *args, opts, &block)
matches = result.match(/Successfully built (?<id>.+)$/i)
if !matches
# Check for the new output format 'writing image sha256...'
# In this case, docker builtkit is enabled. Its format is different
# from standard docker
@logger.warn("Could not determine docker container ID. Scanning for buildkit output instead")
matches = result.match(/writing image .+:(?<id>[0-9a-z]+) done/i)
if !matches
# This will cause a stack trace in Vagrant, but it is a bug
# if this happens anyways.
raise Errors::BuildError, result: result
end
end
# Return the matched group `id`
matches[:id]
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 => 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)
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+/
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)
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