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 (?.+)$/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 .+:(?[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] 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] 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