Merge pull request #700 from basecamp/git-archive-build

Build from a git archive
This commit is contained in:
Donal McBreen
2024-03-27 09:23:00 +00:00
committed by GitHub
12 changed files with 111 additions and 47 deletions

View File

@@ -78,6 +78,10 @@ module Kamal::Commands
args.compact.unshift :docker args.compact.unshift :docker
end end
def git(*args)
args.compact.unshift :git
end
def tags(**details) def tags(**details)
Kamal::Tags.from_config(config, **details) Kamal::Tags.from_config(config, **details)
end end

View File

@@ -3,7 +3,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
class BuilderError < StandardError; end class BuilderError < StandardError; end
delegate :argumentize, to: Kamal::Utils delegate :argumentize, to: Kamal::Utils
delegate :args, :secrets, :dockerfile, :local_arch, :local_host, :remote_arch, :remote_host, :cache_from, :cache_to, :ssh, to: :builder_config delegate :args, :secrets, :dockerfile, :local_arch, :local_host, :remote_arch, :remote_host, :cache_from, :cache_to, :ssh, :git_archive?, to: :builder_config
def clean def clean
docker :image, :rm, "--force", config.absolute_image docker :image, :rm, "--force", config.absolute_image
@@ -13,6 +13,16 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
docker :pull, config.absolute_image docker :pull, config.absolute_image
end end
def push
if git_archive?
pipe \
git(:archive, "--format=tar", :HEAD),
build_and_push
else
build_and_push
end
end
def build_options def build_options
[ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_ssh ] [ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_ssh ]
end end

View File

@@ -7,15 +7,6 @@ class Kamal::Commands::Builder::Multiarch < Kamal::Commands::Builder::Base
docker :buildx, :rm, builder_name docker :buildx, :rm, builder_name
end end
def push
docker :buildx, :build,
"--push",
"--platform", platform_names,
"--builder", builder_name,
*build_options,
build_context
end
def info def info
combine \ combine \
docker(:context, :ls), docker(:context, :ls),
@@ -34,4 +25,13 @@ class Kamal::Commands::Builder::Multiarch < Kamal::Commands::Builder::Base
"linux/amd64,linux/arm64" "linux/amd64,linux/arm64"
end end
end end
def build_and_push
docker :buildx, :build,
"--push",
"--platform", platform_names,
"--builder", builder_name,
*build_options,
build_context
end
end end

View File

@@ -7,14 +7,15 @@ class Kamal::Commands::Builder::Native < Kamal::Commands::Builder::Base
# No-op on native without cache # No-op on native without cache
end end
def push def info
# No-op on native
end
private
def build_and_push
combine \ combine \
docker(:build, *build_options, build_context), docker(:build, *build_options, build_context),
docker(:push, config.absolute_image), docker(:push, config.absolute_image),
docker(:push, config.latest_image) docker(:push, config.latest_image)
end end
def info
# No-op on native
end
end end

View File

@@ -7,7 +7,8 @@ class Kamal::Commands::Builder::Native::Cached < Kamal::Commands::Builder::Nativ
docker :buildx, :rm, builder_name docker :buildx, :rm, builder_name
end end
def push private
def build_and_push
docker :buildx, :build, docker :buildx, :build,
"--push", "--push",
*build_options, *build_options,

View File

@@ -11,15 +11,6 @@ class Kamal::Commands::Builder::Native::Remote < Kamal::Commands::Builder::Nativ
remove_buildx remove_buildx
end end
def push
docker :buildx, :build,
"--push",
"--platform", platform,
"--builder", builder_name,
*build_options,
build_context
end
def info def info
chain \ chain \
docker(:context, :ls), docker(:context, :ls),
@@ -56,4 +47,13 @@ class Kamal::Commands::Builder::Native::Remote < Kamal::Commands::Builder::Nativ
def remove_buildx def remove_buildx
docker :buildx, :rm, builder_name docker :buildx, :rm, builder_name
end end
def build_and_push
docker :buildx, :build,
"--push",
"--platform", platform,
"--builder", builder_name,
*build_options,
build_context
end
end end

View File

@@ -324,7 +324,10 @@ class Kamal::Configuration
def git_version def git_version
@git_version ||= @git_version ||=
if Kamal::Git.used? if Kamal::Git.used?
[ Kamal::Git.revision, Kamal::Git.uncommitted_changes.present? ? "_uncommitted_#{SecureRandom.hex(8)}" : "" ].join if Kamal::Git.uncommitted_changes.present? && !builder.git_archive?
uncommitted_suffix = "_uncommitted_#{SecureRandom.hex(8)}"
end
[ Kamal::Git.revision, uncommitted_suffix ].compact.join
else else
raise "Can't use commit hash as version, no git repository found in #{Dir.pwd}" raise "Can't use commit hash as version, no git repository found in #{Dir.pwd}"
end end

View File

@@ -40,7 +40,7 @@ class Kamal::Configuration::Builder
end end
def context def context
@options["context"] || "." @options["context"] || (git_archive? ? "-" : ".")
end end
def local_arch def local_arch
@@ -85,6 +85,10 @@ class Kamal::Configuration::Builder
@options["ssh"] @options["ssh"]
end end
def git_archive?
Kamal::Git.used? && @options["context"].nil?
end
private private
def valid? def valid?
if @options["cache"] && @options["cache"]["type"] if @options["cache"] && @options["cache"]["type"]

View File

@@ -15,7 +15,7 @@ class CliBuildTest < CliTestCase
run_command("push").tap do |output| run_command("push").tap do |output|
assert_hook_ran "pre-build", output, **hook_variables assert_hook_ran "pre-build", output, **hook_variables
assert_match /docker --version && docker buildx version/, output assert_match /docker --version && docker buildx version/, output
assert_match /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 \. as .*@localhost/, output assert_match /git archive -tar HEAD | 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 - as .*@localhost/, output
end end
end end
@@ -25,7 +25,10 @@ class CliBuildTest < CliTestCase
.with(:docker, "--version", "&&", :docker, :buildx, "version") .with(:docker, "--version", "&&", :docker, :buildx, "version")
SSHKit::Backend::Abstract.any_instance.stubs(:execute) SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |*args| args[0..1] == [ :docker, :buildx ] } .with(:docker, :buildx, :create, "--use", "--name", "kamal-app-multiarch")
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |*args| p args[0..6]; args[0..6] == [ :git, :archive, "--format=tar", :HEAD, "|", :docker, :buildx ] }
.raises(SSHKit::Command::Failed.new("no builder")) .raises(SSHKit::Command::Failed.new("no builder"))
.then .then
.returns(true) .returns(true)

View File

@@ -9,7 +9,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase
builder = new_builder_command(builder: { "cache" => { "type" => "gha" } }) builder = new_builder_command(builder: { "cache" => { "type" => "gha" } })
assert_equal "multiarch", builder.name assert_equal "multiarch", builder.name
assert_equal \ assert_equal \
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .", "git archive --format=tar HEAD | docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile -",
builder.push.join(" ") builder.push.join(" ")
end end
@@ -17,7 +17,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase
builder = new_builder_command(builder: { "multiarch" => false }) builder = new_builder_command(builder: { "multiarch" => false })
assert_equal "native", builder.name assert_equal "native", builder.name
assert_equal \ assert_equal \
"docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile . && docker push dhh/app:123 && docker push dhh/app:latest", "git archive --format=tar HEAD | docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile - && docker push dhh/app:123 && docker push dhh/app:latest",
builder.push.join(" ") builder.push.join(" ")
end end
@@ -25,7 +25,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase
builder = new_builder_command(builder: { "multiarch" => false, "cache" => { "type" => "gha" } }) builder = new_builder_command(builder: { "multiarch" => false, "cache" => { "type" => "gha" } })
assert_equal "native/cached", builder.name assert_equal "native/cached", builder.name
assert_equal \ assert_equal \
"docker buildx build --push -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .", "git archive --format=tar HEAD | docker buildx build --push -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile -",
builder.push.join(" ") builder.push.join(" ")
end end
@@ -33,7 +33,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase
builder = new_builder_command(builder: { "local" => {}, "remote" => {}, "cache" => { "type" => "gha" } }) builder = new_builder_command(builder: { "local" => {}, "remote" => {}, "cache" => { "type" => "gha" } })
assert_equal "multiarch/remote", builder.name assert_equal "multiarch/remote", builder.name
assert_equal \ assert_equal \
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch-remote -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .", "git archive --format=tar HEAD | docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch-remote -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile -",
builder.push.join(" ") builder.push.join(" ")
end end
@@ -41,7 +41,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase
builder = new_builder_command(builder: { "local" => { "arch" => "amd64" } }) builder = new_builder_command(builder: { "local" => { "arch" => "amd64" } })
assert_equal "multiarch", builder.name assert_equal "multiarch", builder.name
assert_equal \ assert_equal \
"docker buildx build --push --platform linux/amd64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile .", "git archive --format=tar HEAD | docker buildx build --push --platform linux/amd64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile -",
builder.push.join(" ") builder.push.join(" ")
end end
@@ -49,7 +49,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase
builder = new_builder_command(builder: { "remote" => { "arch" => "amd64" }, "cache" => { "type" => "gha" } }) builder = new_builder_command(builder: { "remote" => { "arch" => "amd64" }, "cache" => { "type" => "gha" } })
assert_equal "native/remote", builder.name assert_equal "native/remote", builder.name
assert_equal \ assert_equal \
"docker buildx build --push --platform linux/amd64 --builder kamal-app-native-remote -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .", "git archive --format=tar HEAD | docker buildx build --push --platform linux/amd64 --builder kamal-app-native-remote -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile -",
builder.push.join(" ") builder.push.join(" ")
end end
@@ -93,21 +93,21 @@ class CommandsBuilderTest < ActiveSupport::TestCase
test "native push with build args" do test "native push with build args" do
builder = new_builder_command(builder: { "multiarch" => false, "args" => { "a" => 1, "b" => 2 } }) builder = new_builder_command(builder: { "multiarch" => false, "args" => { "a" => 1, "b" => 2 } })
assert_equal \ assert_equal \
"docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile . && docker push dhh/app:123 && docker push dhh/app:latest", "git archive --format=tar HEAD | docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile - && docker push dhh/app:123 && docker push dhh/app:latest",
builder.push.join(" ") builder.push.join(" ")
end end
test "multiarch push with build args" do test "multiarch push with build args" do
builder = new_builder_command(builder: { "args" => { "a" => 1, "b" => 2 } }) builder = new_builder_command(builder: { "args" => { "a" => 1, "b" => 2 } })
assert_equal \ assert_equal \
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile .", "git archive --format=tar HEAD | docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile -",
builder.push.join(" ") builder.push.join(" ")
end end
test "native push with build secrets" do test "native push with build secrets" do
builder = new_builder_command(builder: { "multiarch" => false, "secrets" => [ "a", "b" ] }) builder = new_builder_command(builder: { "multiarch" => false, "secrets" => [ "a", "b" ] })
assert_equal \ assert_equal \
"docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"a\" --secret id=\"b\" --file Dockerfile . && docker push dhh/app:123 && docker push dhh/app:latest", "git archive --format=tar HEAD | docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"a\" --secret id=\"b\" --file Dockerfile - && docker push dhh/app:123 && docker push dhh/app:latest",
builder.push.join(" ") builder.push.join(" ")
end end
@@ -123,6 +123,34 @@ class CommandsBuilderTest < ActiveSupport::TestCase
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(" ") 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 end
test "multiarch context build" do
builder = new_builder_command(builder: { "context" => "./foo" })
assert_equal \
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo",
builder.push.join(" ")
end
test "native context build" do
builder = new_builder_command(builder: { "multiarch" => false, "context" => "./foo" })
assert_equal \
"docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo && docker push dhh/app:123 && docker push dhh/app:latest",
builder.push.join(" ")
end
test "cached context build" do
builder = new_builder_command(builder: { "multiarch" => false, "context" => "./foo", "cache" => { "type" => "gha" } })
assert_equal \
"docker buildx build --push -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile ./foo",
builder.push.join(" ")
end
test "remote context build" do
builder = new_builder_command(builder: { "remote" => { "arch" => "amd64" }, "context" => "./foo" })
assert_equal \
"docker buildx build --push --platform linux/amd64 --builder kamal-app-native-remote -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo",
builder.push.join(" ")
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"))

View File

@@ -140,7 +140,7 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
end end
test "context" do test "context" do
assert_equal ".", @config.builder.context assert_equal "-", @config.builder.context
end end
test "setting context" do test "setting context" do

View File

@@ -103,7 +103,17 @@ class ConfigurationTest < ActiveSupport::TestCase
Kamal::Git.expects(:revision).returns("git-version") Kamal::Git.expects(:revision).returns("git-version")
Kamal::Git.expects(:uncommitted_changes).returns("M file\n") Kamal::Git.expects(:uncommitted_changes).returns("M file\n")
assert_match /^git-version_uncommitted_[0-9a-f]{16}$/, @config.version assert_equal "git-version", @config.version
end
test "version from uncommitted context" do
ENV.delete("VERSION")
config = Kamal::Configuration.new(@deploy.tap { |c| c[:builder] = { "context" => "." } })
Kamal::Git.expects(:revision).returns("git-version")
Kamal::Git.expects(:uncommitted_changes).returns("M file\n")
assert_match /^git-version_uncommitted_[0-9a-f]{16}$/, config.version
end end
test "version from env" do test "version from env" do