Files
kamal/test/commands/builder_test.rb
Donal McBreen 93d1bd1369 Redirect buildx build output to stdout
Docker buildx build outputs the build logs to stderr by default.
SSHKit displays stderr logs in red, which can suggest that an error has
occurred.

Redirect the output to stdout, so it shows in green. If there is an
error, the output will be repeated in red anyway.

Fixes: https://github.com/basecamp/kamal/issues/1356
2025-04-21 10:19:36 +01:00

216 lines
10 KiB
Ruby

require "test_helper"
class CommandsBuilderTest < ActiveSupport::TestCase
setup do
@config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], builder: { "arch" => "amd64" } }
end
test "target linux/amd64 locally by default" do
builder = new_builder_command(builder: { "cache" => { "type" => "gha" } })
assert_equal "local", builder.name
assert_equal \
"docker buildx build --output=type=registry --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile . 2>&1",
builder.push.join(" ")
end
test "target specified arch locally by default" do
builder = new_builder_command(builder: { "arch" => [ "amd64" ] })
assert_equal "local", builder.name
assert_equal \
"docker buildx build --output=type=registry --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile . 2>&1",
builder.push.join(" ")
end
test "build with caching" do
builder = new_builder_command(builder: { "cache" => { "type" => "gha" } })
assert_equal "local", builder.name
assert_equal \
"docker buildx build --output=type=registry --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile . 2>&1",
builder.push.join(" ")
end
test "hybrid build if remote is set and building multiarch" do
builder = new_builder_command(builder: { "arch" => [ "amd64", "arm64" ], "remote" => "ssh://app@127.0.0.1", "cache" => { "type" => "gha" } })
assert_equal "hybrid", builder.name
assert_equal \
"docker buildx build --output=type=registry --platform linux/amd64,linux/arm64 --builder kamal-hybrid-docker-container-ssh---app-127-0-0-1 -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile . 2>&1",
builder.push.join(" ")
end
test "remote build if remote is set and local disabled" do
builder = new_builder_command(builder: { "arch" => [ "amd64", "arm64" ], "remote" => "ssh://app@127.0.0.1", "cache" => { "type" => "gha" }, "local" => false })
assert_equal "remote", builder.name
assert_equal \
"docker buildx build --output=type=registry --platform linux/amd64,linux/arm64 --builder kamal-remote-ssh---app-127-0-0-1 -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile . 2>&1",
builder.push.join(" ")
end
test "target remote when remote set and arch is non local" do
builder = new_builder_command(builder: { "arch" => [ "#{remote_arch}" ], "remote" => "ssh://app@host", "cache" => { "type" => "gha" } })
assert_equal "remote", builder.name
assert_equal \
"docker buildx build --output=type=registry --platform linux/#{remote_arch} --builder kamal-remote-ssh---app-host -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile . 2>&1",
builder.push.join(" ")
end
test "target local when remote set and arch is local" do
builder = new_builder_command(builder: { "arch" => [ "#{local_arch}" ], "remote" => "ssh://app@host", "cache" => { "type" => "gha" } })
assert_equal "local", builder.name
assert_equal \
"docker buildx build --output=type=registry --platform linux/#{local_arch} --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile . 2>&1",
builder.push.join(" ")
end
test "cloud builder" do
builder = new_builder_command(builder: { "arch" => [ "#{local_arch}" ], "driver" => "cloud docker-org-name/builder-name" })
assert_equal "cloud", builder.name
assert_equal \
"docker buildx build --output=type=registry --platform linux/#{local_arch} --builder cloud-docker-org-name-builder-name -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile . 2>&1",
builder.push.join(" ")
end
test "build args" do
builder = new_builder_command(builder: { "args" => { "a" => 1, "b" => 2 } })
assert_equal \
"--label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile",
builder.target.build_options.join(" ")
end
test "build secrets" do
with_test_secrets("secrets" => "token_a=foo\ntoken_b=bar") do
FileUtils.touch("Dockerfile")
builder = new_builder_command(builder: { "secrets" => [ "token_a", "token_b" ] })
assert_equal \
"--label service=\"app\" --secret id=\"token_a\" --secret id=\"token_b\" --file Dockerfile",
builder.target.build_options.join(" ")
end
end
test "build dockerfile" do
Pathname.any_instance.expects(:exist?).returns(true).once
builder = new_builder_command(builder: { "dockerfile" => "Dockerfile.xyz" })
assert_equal \
"--label service=\"app\" --file Dockerfile.xyz",
builder.target.build_options.join(" ")
end
test "missing dockerfile" do
Pathname.any_instance.expects(:exist?).returns(false).once
builder = new_builder_command(builder: { "dockerfile" => "Dockerfile.xyz" })
assert_raises(Kamal::Commands::Builder::Base::BuilderError) do
builder.target.build_options.join(" ")
end
end
test "build target" do
builder = new_builder_command(builder: { "target" => "prod" })
assert_equal \
"--label service=\"app\" --file Dockerfile --target prod",
builder.target.build_options.join(" ")
end
test "build context" do
builder = new_builder_command(builder: { "context" => ".." })
assert_equal \
"docker buildx build --output=type=registry --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile .. 2>&1",
builder.push.join(" ")
end
test "push with build args" do
builder = new_builder_command(builder: { "args" => { "a" => 1, "b" => 2 } })
assert_equal \
"docker buildx build --output=type=registry --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile . 2>&1",
builder.push.join(" ")
end
test "push with build secrets" do
with_test_secrets("secrets" => "a=foo\nb=bar") do
FileUtils.touch("Dockerfile")
builder = new_builder_command(builder: { "secrets" => [ "a", "b" ] })
assert_equal \
"docker buildx build --output=type=registry --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"a\" --secret id=\"b\" --file Dockerfile . 2>&1",
builder.push.join(" ")
end
end
test "build with ssh agent socket" do
builder = new_builder_command(builder: { "ssh" => "default=$SSH_AUTH_SOCK" })
assert_equal \
"--label service=\"app\" --file Dockerfile --ssh default=$SSH_AUTH_SOCK",
builder.target.build_options.join(" ")
end
test "validate image" do
assert_equal "docker inspect -f '{{ .Config.Labels.service }}' dhh/app:123 | grep -x app || (echo \"Image dhh/app:123 is missing the 'service' label\" && exit 1)", new_builder_command.validate_image.join(" ")
end
test "context build" do
builder = new_builder_command(builder: { "context" => "./foo" })
assert_equal \
"docker buildx build --output=type=registry --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo 2>&1",
builder.push.join(" ")
end
test "push with provenance" do
builder = new_builder_command(builder: { "provenance" => "mode=max" })
assert_equal \
"docker buildx build --output=type=registry --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile --provenance mode=max . 2>&1",
builder.push.join(" ")
end
test "push with provenance false" do
builder = new_builder_command(builder: { "provenance" => false })
assert_equal \
"docker buildx build --output=type=registry --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile --provenance false . 2>&1",
builder.push.join(" ")
end
test "push with sbom" do
builder = new_builder_command(builder: { "sbom" => true })
assert_equal \
"docker buildx build --output=type=registry --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile --sbom true . 2>&1",
builder.push.join(" ")
end
test "push with sbom false" do
builder = new_builder_command(builder: { "sbom" => false })
assert_equal \
"docker buildx build --output=type=registry --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile --sbom false . 2>&1",
builder.push.join(" ")
end
test "mirror count" do
command = new_builder_command
assert_equal "docker info --format '{{index .RegistryConfig.Mirrors 0}}'", command.first_mirror.join(" ")
end
test "clone path with spaces" do
command = new_builder_command
Kamal::Git.stubs(:root).returns("/absolute/path with spaces")
clone_command = command.clone.join(" ")
clone_reset_commands = command.clone_reset_steps.map { |a| a.join(" ") }
assert_match(%r{path\\ with\\ space}, clone_command)
assert_no_match(%r{path with spaces}, clone_command)
clone_reset_commands.each do |command|
assert_match(%r{path\\ with\\ space}, command)
assert_no_match(%r{path with spaces}, command)
end
end
private
def new_builder_command(additional_config = {})
Kamal::Commands::Builder.new(Kamal::Configuration.new(@config.deep_merge(additional_config), version: "123"))
end
def local_arch
Kamal::Utils.docker_arch
end
def remote_arch
Kamal::Utils.docker_arch == "arm64" ? "amd64" : "arm64"
end
end