From 1369c46a83ac7ca29cee243cc96c929ea555f137 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 11 Jul 2024 13:05:32 +0100 Subject: [PATCH] Seed docker mirrors by pulling once per mirror first Find the first registry mirror on each host. If we find any, pull the images on one host per mirror, then do the remainder concurrently. The initial pulls will seed the mirrors ensuring that we pull the image from Docker Hub once each. This works best if there is only one mirror on each host. --- lib/kamal/cli/build.rb | 37 ++++++++++++++++++++++++++---- lib/kamal/commands/builder.rb | 2 +- lib/kamal/commands/builder/base.rb | 4 ++++ test/cli/build_test.rb | 29 +++++++++++++++++++++++ test/cli/main_test.rb | 10 ++++++++ test/commands/builder_test.rb | 5 ++++ 6 files changed, 81 insertions(+), 6 deletions(-) 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"))