diff --git a/lib/kamal/cli/build.rb b/lib/kamal/cli/build.rb index 3508079f..9ce1f4a6 100644 --- a/lib/kamal/cli/build.rb +++ b/lib/kamal/cli/build.rb @@ -59,11 +59,14 @@ class Kamal::Cli::Build < Kamal::Cli::Base desc "pull", "Pull app image from registry onto servers" def pull - on(KAMAL.hosts) do - execute *KAMAL.auditor.record("Pulled image with version #{KAMAL.config.version}"), verbosity: :debug - execute *KAMAL.builder.clean, raise_on_non_zero_exit: false - execute *KAMAL.builder.pull - execute *KAMAL.builder.validate_image + if (first_hosts = mirror_hosts).any? + #  Pull on a single host per mirror first to seed them + say "Pulling image on #{first_hosts.join(", ")} to seed the #{"mirror".pluralize(first_hosts.count)}...", :magenta + pull_on_hosts(first_hosts) + say "Pulling image on remaining hosts...", :magenta + pull_on_hosts(KAMAL.hosts - first_hosts) + else + pull_on_hosts(KAMAL.hosts) end end @@ -131,4 +134,28 @@ class Kamal::Cli::Build < Kamal::Cli::Base end end end + + def mirror_hosts + if KAMAL.hosts.many? + mirror_hosts = Concurrent::Hash.new + on(KAMAL.hosts) do |host| + first_mirror = capture_with_info(*KAMAL.builder.first_mirror).strip.presence + mirror_hosts[first_mirror] ||= host if first_mirror + rescue SSHKit::Command::Failed => e + raise unless e.message =~ /error calling index: reflect: slice index out of range/ + end + mirror_hosts.values + else + [] + end + end + + def pull_on_hosts(hosts) + on(hosts) do + execute *KAMAL.auditor.record("Pulled image with version #{KAMAL.config.version}"), verbosity: :debug + execute *KAMAL.builder.clean, raise_on_non_zero_exit: false + execute *KAMAL.builder.pull + execute *KAMAL.builder.validate_image + end + end end diff --git a/lib/kamal/commands/builder.rb b/lib/kamal/commands/builder.rb index 370f6b3a..13c3d829 100644 --- a/lib/kamal/commands/builder.rb +++ b/lib/kamal/commands/builder.rb @@ -2,7 +2,7 @@ require "active_support/core_ext/string/filters" class Kamal::Commands::Builder < Kamal::Commands::Base delegate :create, :remove, :push, :clean, :pull, :info, :context_hosts, :config_context_hosts, :validate_image, - to: :target + :first_mirror, to: :target include Clone diff --git a/lib/kamal/commands/builder/base.rb b/lib/kamal/commands/builder/base.rb index 112b7090..cedfeadf 100644 --- a/lib/kamal/commands/builder/base.rb +++ b/lib/kamal/commands/builder/base.rb @@ -40,6 +40,10 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base [] end + def first_mirror + docker(:info, "--format '{{index .RegistryConfig.Mirrors 0}}'") + end + private def build_tags [ "-t", config.absolute_image, "-t", config.latest_image ] diff --git a/test/cli/build_test.rb b/test/cli/build_test.rb index e7c7b333..4035ea51 100644 --- a/test/cli/build_test.rb +++ b/test/cli/build_test.rb @@ -169,12 +169,41 @@ class CliBuildTest < CliTestCase test "pull" do run_command("pull").tap do |output| + assert_match /docker info --format '{{index .RegistryConfig.Mirrors 0}}'/, output assert_match /docker image rm --force dhh\/app:999/, output assert_match /docker pull dhh\/app:999/, output assert_match "docker inspect -f '{{ .Config.Labels.service }}' dhh/app:999 | grep -x app || (echo \"Image dhh/app:999 is missing the 'service' label\" && exit 1)", output end end + test "pull with mirror" do + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:docker, :info, "--format '{{index .RegistryConfig.Mirrors 0}}'") + .returns("registry-mirror.example.com") + .at_least_once + + run_command("pull").tap do |output| + assert_match /Pulling image on 1\.1\.1\.\d to seed the mirror\.\.\./, output + assert_match "Pulling image on remaining hosts...", output + assert_match /docker pull dhh\/app:999/, output + assert_match "docker inspect -f '{{ .Config.Labels.service }}' dhh/app:999 | grep -x app || (echo \"Image dhh/app:999 is missing the 'service' label\" && exit 1)", output + end + end + + test "pull with mirrors" do + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:docker, :info, "--format '{{index .RegistryConfig.Mirrors 0}}'") + .returns("registry-mirror.example.com", "registry-mirror2.example.com") + .at_least_once + + run_command("pull").tap do |output| + assert_match /Pulling image on 1\.1\.1\.\d, 1\.1\.1\.\d to seed the mirrors\.\.\./, output + assert_match "Pulling image on remaining hosts...", output + assert_match /docker pull dhh\/app:999/, output + assert_match "docker inspect -f '{{ .Config.Labels.service }}' dhh/app:999 | grep -x app || (echo \"Image dhh/app:999 is missing the 'service' label\" && exit 1)", output + end + end + test "create" do run_command("create").tap do |output| assert_match /docker buildx create --use --name kamal-app-multiarch/, output diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index 4db97aaf..a60193d7 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -122,6 +122,11 @@ class CliMainTest < CliTestCase .with(:docker, :buildx, :inspect, "kamal-app-multiarch", "> /dev/null") .returns("") + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:docker, :info, "--format '{{index .RegistryConfig.Mirrors 0}}'") + .returns("") + .at_least_once + assert_raises(Kamal::Cli::LockError) do run_command("deploy") end @@ -155,6 +160,11 @@ class CliMainTest < CliTestCase .with(:docker, :buildx, :inspect, "kamal-app-multiarch", "> /dev/null") .returns("") + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:docker, :info, "--format '{{index .RegistryConfig.Mirrors 0}}'") + .returns("") + .at_least_once + assert_raises(SSHKit::Runner::ExecuteError) do run_command("deploy") end diff --git a/test/commands/builder_test.rb b/test/commands/builder_test.rb index 6bc9795d..f3faa5f6 100644 --- a/test/commands/builder_test.rb +++ b/test/commands/builder_test.rb @@ -200,6 +200,11 @@ class CommandsBuilderTest < ActiveSupport::TestCase assert_equal [ "unix:///var/run/docker.sock", "ssh://host" ], command.config_context_hosts end + test "mirror count" do + command = new_builder_command + assert_equal "docker info --format '{{index .RegistryConfig.Mirrors 0}}'", command.first_mirror.join(" ") + end + private def new_builder_command(additional_config = {}) Kamal::Commands::Builder.new(Kamal::Configuration.new(@config.merge(additional_config), version: "123"))