diff --git a/plugins/providers/docker/config.rb b/plugins/providers/docker/config.rb index e733338c1..ca3d5a2ae 100644 --- a/plugins/providers/docker/config.rb +++ b/plugins/providers/docker/config.rb @@ -25,6 +25,12 @@ module VagrantPlugins # @return [Boolean] attr_accessor :compose + # Configuration Hash used for build the docker-compose composition + # file. This can be used for adding networks or volumes. + # + # @return [Hash] + attr_reader :compose_configuration + # An optional file name of a Dockerfile to be used when building # the image. This requires Docker >1.5.0. # @@ -145,6 +151,7 @@ module VagrantPlugins @build_dir = UNSET_VALUE @cmd = UNSET_VALUE @compose = UNSET_VALUE + @compose_configuration = {} @create_args = UNSET_VALUE @dockerfile = UNSET_VALUE @env = {} diff --git a/plugins/providers/docker/driver/compose.rb b/plugins/providers/docker/driver/compose.rb index 6cfd24cb2..1a2ff66a3 100644 --- a/plugins/providers/docker/driver/compose.rb +++ b/plugins/providers/docker/driver/compose.rb @@ -6,6 +6,8 @@ module VagrantPlugins 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 @@ -25,11 +27,22 @@ module VagrantPlugins @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 def build(dir, **opts, &block) - update_composition do |composition| - composition["build"] = dir + @logger.debug("Applying build using `#{dir}` directory.") + begin + update_composition(:apply) do |composition| + composition["build"] = dir + end + rescue => error + @logger.error("Failed to apply build using `#{dir}` directory: #{error.class} - #{error}") + update_composition do |composition| + composition.delete("build") + end + raise end end @@ -44,9 +57,9 @@ module VagrantPlugins cmd = Array(params.fetch(:cmd)) env = params.fetch(:env) expose = Array(params[:expose]) - + @logger.debug("Creating container `#{name}`") begin - update_composition do |composition| + update_composition(:apply) do |composition| services = composition["services"] ||= {} services[name] = { "image" => image, @@ -58,8 +71,9 @@ module VagrantPlugins "command" => cmd } end - rescue - update_composition(false) do |composition| + rescue => error + @logger.error("Failed to create container `#{name}`: #{error.class} - #{error}") + update_composition do |composition| composition["services"].delete(name) end raise @@ -71,16 +85,19 @@ module VagrantPlugins if created?(cid) destroy = false compose_execute("rm", "-f", machine.name.to_s) - update_composition do |composition| + update_composition(:conditional_apply) do |composition| if composition["services"] && composition["services"].key?(machine.name.to_s) @logger.info("Removing container `#{machine.name}`") composition["services"].delete(machine.name.to_s) destroy = composition["services"].empty? end + !destroy end if destroy @logger.info("No containers remain. Destroying full environment.") - compose_execute("down", "--remove-orphans") + compose_execute("down", "--remove-orphans", "--volumes", "--rmi", "local") + @logger.info("Deleting composition path `#{composition_path}`") + composition_path.delete end end end @@ -88,7 +105,7 @@ module VagrantPlugins def created?(cid) result = super if !result - composition = get_current_composition + composition = get_composition if composition["services"] && composition["services"].has_key?(machine.name.to_s) result = true end @@ -108,7 +125,7 @@ module VagrantPlugins # Execute a `docker-compose` command def compose_execute(*cmd, **opts) - @compose_lock.synchronize do + synchronized do execute("docker-compose", "-f", composition_path.to_s, "-p", machine.env.cwd.basename.to_s, *cmd, **opts) end @@ -124,23 +141,29 @@ module VagrantPlugins # Update the composition and apply changes if requested # # @param [Boolean] apply Apply composition changes - def update_composition(apply=true) - machine.env.lock("compose", retry: true) do - composition = get_current_composition - yield composition - write_composition(composition) - apply_composition! if apply + 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! + end + end end end # @return [Hash] current composition contents - def get_current_composition + def get_composition composition = {"version" => COMPOSE_VERSION.dup} if composition_path.exist? composition.merge!( YAML.load(composition_path.read) ) end + composition.merge!(machine.provider_config.compose_configuration.dup) + @logger.debug("Fetched composition with provider configuration applied: #{composition}") composition end @@ -148,10 +171,11 @@ module VagrantPlugins # # @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 - @compose_lock.synchronize do + synchronized do FileUtils.mv(tmp_file.path, composition_path.to_s) end end @@ -160,6 +184,28 @@ module VagrantPlugins 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 diff --git a/plugins/providers/docker/errors.rb b/plugins/providers/docker/errors.rb index 1387fbfec..096f6fa5b 100644 --- a/plugins/providers/docker/errors.rb +++ b/plugins/providers/docker/errors.rb @@ -9,6 +9,10 @@ module VagrantPlugins error_key(:communicator_non_docker) end + class ComposeLockTimeoutError < DockerError + error_key(:compose_lock_timeout) + end + class ContainerNotRunningError < DockerError error_key(:not_running) end diff --git a/templates/locales/providers_docker.yml b/templates/locales/providers_docker.yml index 946372396..0c10338ce 100644 --- a/templates/locales/providers_docker.yml +++ b/templates/locales/providers_docker.yml @@ -117,6 +117,11 @@ en: run exits and doesn't keep running. errors: + compose_lock_timeout: |- + Vagrant enountered a timeout waiting for the docker compose driver + to become available. Please try to run your command again. If you + continue to experience this error it may be resolved by disabling + parallel execution. not_created: |- The container hasn't been created yet. not_running: |- diff --git a/website/source/docs/docker/configuration.html.md b/website/source/docs/docker/configuration.html.md index b756d9931..a72d6d6e0 100644 --- a/website/source/docs/docker/configuration.html.md +++ b/website/source/docs/docker/configuration.html.md @@ -34,6 +34,11 @@ General settings: manage the lifecycle and configuration of containers. This defaults to false. + * `compose_configuration` (Hash) - Configuration values used for populating + the `docker-compose.yml` file. The value of this Hash is directly merged + and written to the `docker-compose.yml` file allowing customization of + non-services items like networks and volumes. + * `create_args` (array of strings) - Additional arguments to pass to `docker run` when the container is started. This can be used to set parameters that are not exposed via the Vagrantfile.