mirror of
https://github.com/hashicorp/vagrant.git
synced 2026-05-28 04:36:05 -04:00
Remove customized require behaviors and modify the bin executable to check for missing tools that Vagrant expects to exist when running outside of an installer.
315 lines
12 KiB
Ruby
315 lines
12 KiB
Ruby
# Copyright (c) HashiCorp, Inc.
|
|
# SPDX-License-Identifier: BUSL-1.1
|
|
|
|
require "json"
|
|
require "log4r"
|
|
|
|
module VagrantPlugins
|
|
module DockerProvider
|
|
class Driver
|
|
class Compose < Driver
|
|
|
|
# @return [Integer] Maximum number of seconds to wait for lock
|
|
LOCK_TIMEOUT = 60
|
|
# @return [String] Compose file format version
|
|
COMPOSE_VERSION = "2".freeze
|
|
|
|
# @return [Pathname] data directory to store composition
|
|
attr_reader :data_directory
|
|
# @return [Vagrant::Machine]
|
|
attr_reader :machine
|
|
|
|
# Create a new driver instance
|
|
#
|
|
# @param [Vagrant::Machine] machine Machine instance for this driver
|
|
def initialize(machine)
|
|
if !Vagrant::Util::Which.which("docker-compose")
|
|
raise Errors::DockerComposeNotInstalledError
|
|
end
|
|
super()
|
|
@machine = machine
|
|
@data_directory = Pathname.new(machine.env.local_data_path).
|
|
join("docker-compose")
|
|
@data_directory.mkpath
|
|
@logger = Log4r::Logger.new("vagrant::docker::driver::compose")
|
|
@compose_lock = Mutex.new
|
|
@logger.debug("Docker compose driver initialize for machine `#{@machine.name}` (`#{@machine.id}`)")
|
|
@logger.debug("Data directory for composition file `#{@data_directory}`")
|
|
end
|
|
|
|
# Updates the docker compose config file with the given arguments
|
|
#
|
|
# @param [String] dir - local directory or git repo URL
|
|
# @param [Hash] opts - valid key: extra_args
|
|
# @param [Block] block
|
|
# @return [Nil]
|
|
def build(dir, **opts, &block)
|
|
name = machine.name.to_s
|
|
@logger.debug("Applying build for `#{name}` using `#{dir}` directory.")
|
|
begin
|
|
update_composition do |composition|
|
|
services = composition["services"] ||= {}
|
|
services[name] ||= {}
|
|
services[name]["build"] = {"context" => dir}
|
|
# Extract custom dockerfile location if set
|
|
if opts[:extra_args] && opts[:extra_args].include?("--file")
|
|
services[name]["build"]["dockerfile"] = opts[:extra_args][opts[:extra_args].index("--file") + 1]
|
|
end
|
|
# Extract any build args that can be found
|
|
case opts[:extra_args]
|
|
when Array
|
|
if opts[:extra_args].include?("--build-arg")
|
|
idx = 0
|
|
extra_args = {}
|
|
while(idx < opts[:extra_args].size)
|
|
arg_value = opts[:extra_args][idx]
|
|
idx += 1
|
|
if arg_value.start_with?("--build-arg")
|
|
if !arg_value.include?("=")
|
|
arg_value = opts[:extra_args][idx]
|
|
idx += 1
|
|
end
|
|
key, val = arg_value.to_s.split("=", 2).to_s.split("=")
|
|
extra_args[key] = val
|
|
end
|
|
end
|
|
end
|
|
when Hash
|
|
services[name]["build"]["args"] = opts[:extra_args]
|
|
end
|
|
end
|
|
rescue => error
|
|
@logger.error("Failed to apply build using `#{dir}` directory: #{error.class} - #{error}")
|
|
update_composition do |composition|
|
|
composition["services"].delete(name)
|
|
end
|
|
raise
|
|
end
|
|
end
|
|
|
|
def create(params, **opts, &block)
|
|
# NOTE: Use the direct machine name as we don't
|
|
# need to worry about uniqueness with compose
|
|
name = machine.name.to_s
|
|
image = params.fetch(:image)
|
|
links = Array(params.fetch(:links, [])).map do |link|
|
|
case link
|
|
when Array
|
|
link
|
|
else
|
|
link.to_s.split(":")
|
|
end
|
|
end
|
|
ports = Array(params[:ports])
|
|
volumes = Array(params[:volumes]).map do |v|
|
|
v = v.to_s
|
|
host, guest = v.split(":", 2)
|
|
if v.include?(":") && (Vagrant::Util::Platform.windows? || Vagrant::Util::Platform.wsl?)
|
|
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]+/, "")
|
|
end
|
|
# if host path is a volume key, don't expand it.
|
|
# if both exist (a path and a key) show warning and move on
|
|
# otherwise assume it's a realative path and expand the host path
|
|
compose_config = get_composition
|
|
if compose_config["volumes"] && compose_config["volumes"].keys.include?(host)
|
|
if File.directory?(@machine.env.cwd.join(host).to_s)
|
|
@machine.env.ui.warn(I18n.t("docker_provider.volume_path_not_expanded",
|
|
host: host))
|
|
end
|
|
else
|
|
@logger.debug("Path expanding #{host} to current Vagrant working dir instead of docker-compose config file directory")
|
|
host = @machine.env.cwd.join(host).to_s
|
|
end
|
|
"#{host}:#{guest}"
|
|
end
|
|
cmd = Array(params.fetch(:cmd))
|
|
env = Hash[*params.fetch(:env).flatten.map(&:to_s)]
|
|
expose = Array(params[:expose])
|
|
@logger.debug("Creating container `#{name}`")
|
|
begin
|
|
update_args = [:apply]
|
|
update_args.push(:detach) if params[:detach]
|
|
update_args << block
|
|
update_composition(*update_args) do |composition|
|
|
services = composition["services"] ||= {}
|
|
services[name] ||= {}
|
|
if params[:extra_args].is_a?(Hash)
|
|
services[name].merge!(
|
|
Hash[
|
|
params[:extra_args].map{ |k, v|
|
|
[k.to_s, v]
|
|
}
|
|
]
|
|
)
|
|
end
|
|
services[name].merge!(
|
|
"environment" => env,
|
|
"expose" => expose,
|
|
"ports" => ports,
|
|
"volumes" => volumes,
|
|
"links" => links,
|
|
"command" => cmd
|
|
)
|
|
services[name]["image"] = image if image
|
|
services[name]["hostname"] = params[:hostname] if params[:hostname]
|
|
services[name]["privileged"] = true if params[:privileged]
|
|
services[name]["pty"] = true if params[:pty]
|
|
end
|
|
rescue => error
|
|
@logger.error("Failed to create container `#{name}`: #{error.class} - #{error}")
|
|
update_composition do |composition|
|
|
composition["services"].delete(name)
|
|
end
|
|
raise
|
|
end
|
|
get_container_id(name)
|
|
end
|
|
|
|
def rm(cid)
|
|
if created?(cid)
|
|
destroy = false
|
|
synchronized do
|
|
compose_execute("rm", "-f", machine.name.to_s)
|
|
update_composition do |composition|
|
|
if composition["services"] && composition["services"].key?(machine.name.to_s)
|
|
@logger.info("Removing container `#{machine.name}`")
|
|
if composition["services"].size > 1
|
|
composition["services"].delete(machine.name.to_s)
|
|
else
|
|
destroy = true
|
|
end
|
|
end
|
|
end
|
|
if destroy
|
|
@logger.info("No containers remain. Destroying full environment.")
|
|
compose_execute("down", "--volumes", "--rmi", "local")
|
|
@logger.info("Deleting composition path `#{composition_path}`")
|
|
composition_path.delete
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def rmi(*_)
|
|
true
|
|
end
|
|
|
|
def created?(cid)
|
|
result = super
|
|
if !result
|
|
composition = get_composition
|
|
if composition["services"] && composition["services"].has_key?(machine.name.to_s)
|
|
result = true
|
|
end
|
|
end
|
|
result
|
|
end
|
|
|
|
private
|
|
|
|
# Lookup the ID for the container with the given name
|
|
#
|
|
# @param [String] name Name of container
|
|
# @return [String] Container ID
|
|
def get_container_id(name)
|
|
compose_execute("ps", "-q", name).chomp
|
|
end
|
|
|
|
# Execute a `docker-compose` command
|
|
def compose_execute(*cmd, **opts, &block)
|
|
synchronized do
|
|
execute("docker-compose", "-f", composition_path.to_s,
|
|
"-p", machine.env.cwd.basename.to_s, *cmd, **opts, &block)
|
|
end
|
|
end
|
|
|
|
# Apply any changes made to the composition
|
|
def apply_composition!(*args)
|
|
block = args.detect{|arg| arg.is_a?(Proc) }
|
|
execute_args = ["up", "--remove-orphans"]
|
|
if args.include?(:detach)
|
|
execute_args << "-d"
|
|
end
|
|
machine.env.lock("compose", retry: true) do
|
|
if block
|
|
compose_execute(*execute_args, &block)
|
|
else
|
|
compose_execute(*execute_args)
|
|
end
|
|
end
|
|
end
|
|
|
|
# Update the composition and apply changes if requested
|
|
#
|
|
# @param [Boolean] apply Apply composition changes
|
|
def update_composition(*args)
|
|
synchronized do
|
|
machine.env.lock("compose", retry: true) do
|
|
composition = get_composition
|
|
result = yield composition
|
|
write_composition(composition)
|
|
if args.include?(:apply) || (args.include?(:conditional) && result)
|
|
apply_composition!(*args)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# @return [Hash] current composition contents
|
|
def get_composition
|
|
composition = {"version" => COMPOSE_VERSION.dup}
|
|
if composition_path.exist?
|
|
composition = Vagrant::Util::DeepMerge.deep_merge(composition, YAML.load(composition_path.read))
|
|
end
|
|
composition = Vagrant::Util::DeepMerge.deep_merge(composition, machine.provider_config.compose_configuration.dup)
|
|
@logger.debug("Fetched composition with provider configuration applied: #{composition}")
|
|
composition
|
|
end
|
|
|
|
# Save the composition
|
|
#
|
|
# @param [Hash] composition New composition
|
|
def write_composition(composition)
|
|
@logger.debug("Saving composition to `#{composition_path}`: #{composition}")
|
|
tmp_file = Tempfile.new("vagrant-docker-compose")
|
|
tmp_file.write(composition.to_yaml)
|
|
tmp_file.close
|
|
synchronized do
|
|
FileUtils.mv(tmp_file.path, composition_path.to_s)
|
|
end
|
|
end
|
|
|
|
# @return [Pathname] path to the docker-compose.yml file
|
|
def composition_path
|
|
data_directory.join("docker-compose.yml")
|
|
end
|
|
|
|
def synchronized
|
|
if !@compose_lock.owned?
|
|
timeout = LOCK_TIMEOUT.to_f
|
|
until @compose_lock.owned?
|
|
if @compose_lock.try_lock
|
|
if timeout > 0
|
|
timeout -= sleep(1)
|
|
else
|
|
raise Errors::ComposeLockTimeoutError
|
|
end
|
|
end
|
|
end
|
|
got_lock = true
|
|
end
|
|
begin
|
|
result = yield
|
|
ensure
|
|
@compose_lock.unlock if got_lock
|
|
end
|
|
result
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|