Build from a git archive

Building directly from a checkout will pull in uncommitted files to or
more sneakily files that are git ignored, but not docker ignored.

To avoid this, we'll add an option to build from a git archive of HEAD
instead. Docker doesn't provide a way to build directly from a git
repo, so instead we create a tarball of the current HEAD with git
archive and pipe it into the build command.

When building from a git archive, we'll still display the warning about
uncommitted changes, but we won't add the `_uncommitted_...` suffix to
the container name as they won't be included in the build.

Perhaps this should be the default, but we'll leave that decision for
now.
This commit is contained in:
Donal McBreen
2024-03-04 14:57:55 +00:00
parent 786454f2ee
commit f3b7569032
11 changed files with 100 additions and 34 deletions

View File

@@ -31,6 +31,7 @@ registry:
# user: app
# Configure builder setup.
# Set git_archive: true to build from a git archive of HEAD
# builder:
# args:
# RUBY_VERSION: 3.2.0
@@ -39,6 +40,7 @@ registry:
# remote:
# arch: amd64
# host: ssh://app@192.168.0.1
# git_archive: false
# Use accessory services (secrets come from .env).
# accessories:

View File

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

View File

@@ -3,7 +3,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
class BuilderError < StandardError; end
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
docker :image, :rm, "--force", config.absolute_image
@@ -13,6 +13,16 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
docker :pull, config.absolute_image
end
def push
if git_archive?
pipe \
git(:archive, "--format=tar", :HEAD),
build_and_push
else
build_and_push
end
end
def build_options
[ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_ssh ]
end

View File

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

View File

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

View File

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

View File

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

View File

@@ -324,7 +324,10 @@ class Kamal::Configuration
def git_version
@git_version ||=
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
raise "Can't use commit hash as version, no git repository found in #{Dir.pwd}"
end

View File

@@ -40,7 +40,7 @@ class Kamal::Configuration::Builder
end
def context
@options["context"] || "."
@options["context"] || (git_archive? ? "-" : ".")
end
def local_arch
@@ -85,11 +85,18 @@ class Kamal::Configuration::Builder
@options["ssh"]
end
def git_archive?
@options["git_archive"]
end
private
def valid?
if @options["cache"] && @options["cache"]["type"]
raise ArgumentError, "Invalid cache type: #{@options["cache"]["type"]}" unless [ "gha", "registry" ].include?(@options["cache"]["type"])
end
if @options["context"] && @options["git_archive"]
raise ArgumentError, "Cannot set a builder context when building from a git archive"
end
end
def cache_image

View File

@@ -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(" ")
end
test "multiarch git archive build" do
builder = new_builder_command(builder: { "git_archive" => true })
assert_equal \
"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\" --file Dockerfile -",
builder.push.join(" ")
end
test "native git archive build" do
builder = new_builder_command(builder: { "multiarch" => false, "git_archive" => true })
assert_equal \
"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(" ")
end
test "cached git archive build" do
builder = new_builder_command(builder: { "multiarch" => false, "git_archive" => true, "cache" => { "type" => "gha" } })
assert_equal \
"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(" ")
end
test "remote git archive build" do
builder = new_builder_command(builder: { "remote" => { "arch" => "amd64" }, "git_archive" => true })
assert_equal \
"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 --label service=\"app\" --file Dockerfile -",
builder.push.join(" ")
end
private
def new_builder_command(additional_config = {})
Kamal::Commands::Builder.new(Kamal::Configuration.new(@config.merge(additional_config), version: "123"))

View File

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