Merge pull request #831 from basecamp/check-buildx-contexts

Check that we have valid contexts before building
This commit is contained in:
Donal McBreen
2024-06-06 08:12:05 +01:00
committed by GitHub
11 changed files with 122 additions and 16 deletions

View File

@@ -35,22 +35,25 @@ class Kamal::Cli::Build < Kamal::Cli::Base
run_locally do run_locally do
begin begin
KAMAL.with_verbosity(:debug) do context_hosts = capture_with_info(*KAMAL.builder.context_hosts).split("\n")
Dir.chdir(KAMAL.config.builder.build_directory) { execute *push }
if context_hosts != KAMAL.builder.config_context_hosts
warn "Context hosts have changed, so re-creating builder, was: #{context_hosts.join(", ")}], now: #{KAMAL.builder.config_context_hosts.join(", ")}"
cli.remove
cli.create
end end
rescue SSHKit::Command::Failed => e rescue SSHKit::Command::Failed => e
if e.message =~ /(no builder)|(no such file or directory)/ warn "Missing compatible builder, so creating a new one first"
warn "Missing compatible builder, so creating a new one first" if e.message =~ /(context not found|no builder)/
cli.create
if cli.create
KAMAL.with_verbosity(:debug) do
Dir.chdir(KAMAL.config.builder.build_directory) { execute *push }
end
end
else else
raise raise
end end
end end
KAMAL.with_verbosity(:debug) do
Dir.chdir(KAMAL.config.builder.build_directory) { execute *push }
end
end end
end end

View File

@@ -1,7 +1,8 @@
require "active_support/core_ext/string/filters" require "active_support/core_ext/string/filters"
class Kamal::Commands::Builder < Kamal::Commands::Base class Kamal::Commands::Builder < Kamal::Commands::Base
delegate :create, :remove, :push, :clean, :pull, :info, :validate_image, to: :target delegate :create, :remove, :push, :clean, :pull, :info, :context_hosts, :config_context_hosts, :validate_image,
to: :target
include Clone include Clone

View File

@@ -2,6 +2,8 @@
class Kamal::Commands::Builder::Base < Kamal::Commands::Base class Kamal::Commands::Builder::Base < Kamal::Commands::Base
class BuilderError < StandardError; end class BuilderError < StandardError; end
ENDPOINT_DOCKER_HOST_INSPECT = "'{{.Endpoints.docker.Host}}'"
delegate :argumentize, to: Kamal::Utils delegate :argumentize, to: Kamal::Utils
delegate :args, :secrets, :dockerfile, :target, :local_arch, :local_host, :remote_arch, :remote_host, :cache_from, :cache_to, :ssh, to: :builder_config delegate :args, :secrets, :dockerfile, :target, :local_arch, :local_host, :remote_arch, :remote_host, :cache_from, :cache_to, :ssh, to: :builder_config
@@ -30,6 +32,13 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
) )
end end
def context_hosts
:true
end
def config_context_hosts
[]
end
private private
def build_tags def build_tags
@@ -74,4 +83,8 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
def builder_config def builder_config
config.builder config.builder
end end
def context_host(builder_name)
docker :context, :inspect, builder_name, "--format", ENDPOINT_DOCKER_HOST_INSPECT
end
end end

View File

@@ -22,6 +22,10 @@ class Kamal::Commands::Builder::Multiarch < Kamal::Commands::Builder::Base
build_context build_context
end end
def context_hosts
docker :buildx, :inspect, builder_name, "> /dev/null"
end
private private
def builder_name def builder_name
"kamal-#{config.service}-multiarch" "kamal-#{config.service}-multiarch"

View File

@@ -12,6 +12,16 @@ class Kamal::Commands::Builder::Multiarch::Remote < Kamal::Commands::Builder::Mu
super super
end end
def context_hosts
chain \
context_host(builder_name_with_arch(local_arch)),
context_host(builder_name_with_arch(remote_arch))
end
def config_context_hosts
[ local_host, remote_host ].compact
end
private private
def builder_name def builder_name
super + "-remote" super + "-remote"

View File

@@ -1,6 +1,6 @@
class Kamal::Commands::Builder::Native::Cached < Kamal::Commands::Builder::Native class Kamal::Commands::Builder::Native::Cached < Kamal::Commands::Builder::Native
def create def create
docker :buildx, :create, "--use", "--driver=docker-container" docker :buildx, :create, "--name", builder_name, "--use", "--driver=docker-container"
end end
def remove def remove
@@ -13,4 +13,13 @@ class Kamal::Commands::Builder::Native::Cached < Kamal::Commands::Builder::Nativ
*build_options, *build_options,
build_context build_context
end end
def context_hosts
docker :buildx, :inspect, builder_name, "> /dev/null"
end
private
def builder_name
"kamal-#{config.service}-native-cached"
end
end end

View File

@@ -26,6 +26,14 @@ class Kamal::Commands::Builder::Native::Remote < Kamal::Commands::Builder::Nativ
build_context build_context
end end
def context_hosts
context_host(builder_name_with_arch)
end
def config_context_hosts
[ remote_host ]
end
private private
def builder_name def builder_name

View File

@@ -21,6 +21,10 @@ class CliBuildTest < CliTestCase
.with(:git, "-C", anything, :status, "--porcelain") .with(:git, "-C", anything, :status, "--porcelain")
.returns("") .returns("")
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :buildx, :inspect, "kamal-app-multiarch", "> /dev/null")
.returns("")
run_command("push", "--verbose").tap do |output| run_command("push", "--verbose").tap do |output|
assert_hook_ran "pre-build", output, **hook_variables assert_hook_ran "pre-build", output, **hook_variables
assert_match /Cloning repo into build directory/, output assert_match /Cloning repo into build directory/, output
@@ -121,11 +125,9 @@ class CliBuildTest < CliTestCase
SSHKit::Backend::Abstract.any_instance.expects(:execute) SSHKit::Backend::Abstract.any_instance.expects(:execute)
.with(:docker, :buildx, :create, "--use", "--name", "kamal-app-multiarch") .with(:docker, :buildx, :create, "--use", "--name", "kamal-app-multiarch")
SSHKit::Backend::Abstract.any_instance.expects(:execute) SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with { |*args| args[0..1] == [ :docker, :buildx ] } .with(:docker, :buildx, :inspect, "kamal-app-multiarch", "> /dev/null")
.raises(SSHKit::Command::Failed.new("no builder")) .raises(SSHKit::Command::Failed.new("no builder"))
.then
.returns(true)
SSHKit::Backend::Abstract.any_instance.expects(:execute).with { |*args| args.first.start_with?("git") } SSHKit::Backend::Abstract.any_instance.expects(:execute).with { |*args| args.first.start_with?("git") }
@@ -137,6 +139,9 @@ class CliBuildTest < CliTestCase
.with(:git, "-C", anything, :status, "--porcelain") .with(:git, "-C", anything, :status, "--porcelain")
.returns("") .returns("")
SSHKit::Backend::Abstract.any_instance.expects(:execute)
.with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "kamal-app-multiarch", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".")
run_command("push").tap do |output| run_command("push").tap do |output|
assert_match /WARN Missing compatible builder, so creating a new one first/, output assert_match /WARN Missing compatible builder, so creating a new one first/, output
end end

View File

@@ -36,6 +36,9 @@ class CliTestCase < ActiveSupport::TestCase
.with { |arg1, arg2| arg1 == :mkdir && arg2 == ".kamal/locks/app" } .with { |arg1, arg2| arg1 == :mkdir && arg2 == ".kamal/locks/app" }
SSHKit::Backend::Abstract.any_instance.stubs(:execute) SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |arg1, arg2| arg1 == :rm && arg2 == ".kamal/locks/app/details" } .with { |arg1, arg2| arg1 == :rm && arg2 == ".kamal/locks/app/details" }
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info)
.with { |*args| args[0..2] == [ :docker, :buildx, :inspect ] }
.returns("")
end end
def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: false) def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: false)

View File

@@ -116,6 +116,10 @@ class CliMainTest < CliTestCase
.with(:git, "-C", anything, :status, "--porcelain") .with(:git, "-C", anything, :status, "--porcelain")
.returns("") .returns("")
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :buildx, :inspect, "kamal-app-multiarch", "> /dev/null")
.returns("")
assert_raises(Kamal::Cli::LockError) do assert_raises(Kamal::Cli::LockError) do
run_command("deploy") run_command("deploy")
end end
@@ -145,6 +149,10 @@ class CliMainTest < CliTestCase
.with(:git, "-C", anything, :status, "--porcelain") .with(:git, "-C", anything, :status, "--porcelain")
.returns("") .returns("")
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :buildx, :inspect, "kamal-app-multiarch", "> /dev/null")
.returns("")
assert_raises(SSHKit::Runner::ExecuteError) do assert_raises(SSHKit::Runner::ExecuteError) do
run_command("deploy") run_command("deploy")
end end

View File

@@ -158,6 +158,48 @@ class CommandsBuilderTest < ActiveSupport::TestCase
builder.push.join(" ") builder.push.join(" ")
end end
test "multiarch context hosts" do
command = new_builder_command
assert_equal "docker buildx inspect kamal-app-multiarch > /dev/null", command.context_hosts.join(" ")
assert_equal "", command.config_context_hosts.join(" ")
end
test "native context hosts" do
command = new_builder_command(builder: { "multiarch" => false })
assert_equal :true, command.context_hosts
assert_equal "", command.config_context_hosts.join(" ")
end
test "native cached context hosts" do
command = new_builder_command(builder: { "multiarch" => false, "cache" => { "type" => "registry" } })
assert_equal "docker buildx inspect kamal-app-native-cached > /dev/null", command.context_hosts.join(" ")
assert_equal "", command.config_context_hosts.join(" ")
end
test "native remote context hosts" do
command = new_builder_command(builder: { "remote" => { "arch" => "amd64", "host" => "ssh://host" } })
assert_equal "docker context inspect kamal-app-native-remote-amd64 --format '{{.Endpoints.docker.Host}}'", command.context_hosts.join(" ")
assert_equal [ "ssh://host" ], command.config_context_hosts
end
test "multiarch remote context hosts" do
command = new_builder_command(builder: {
"remote" => { "arch" => "amd64", "host" => "ssh://host" },
"local" => { "arch" => "arm64" }
})
assert_equal "docker context inspect kamal-app-multiarch-remote-arm64 --format '{{.Endpoints.docker.Host}}' ; docker context inspect kamal-app-multiarch-remote-amd64 --format '{{.Endpoints.docker.Host}}'", command.context_hosts.join(" ")
assert_equal [ "ssh://host" ], command.config_context_hosts
end
test "multiarch remote context hosts with local host" do
command = new_builder_command(builder: {
"remote" => { "arch" => "amd64", "host" => "ssh://host" },
"local" => { "arch" => "arm64", "host" => "unix:///var/run/docker.sock" }
})
assert_equal "docker context inspect kamal-app-multiarch-remote-arm64 --format '{{.Endpoints.docker.Host}}' ; docker context inspect kamal-app-multiarch-remote-amd64 --format '{{.Endpoints.docker.Host}}'", command.context_hosts.join(" ")
assert_equal [ "unix:///var/run/docker.sock", "ssh://host" ], command.config_context_hosts
end
private private
def new_builder_command(additional_config = {}) def new_builder_command(additional_config = {})
Kamal::Commands::Builder.new(Kamal::Configuration.new(@config.merge(additional_config), version: "123")) Kamal::Commands::Builder.new(Kamal::Configuration.new(@config.merge(additional_config), version: "123"))