From f3b756903242dc175baaeaf502fd69476d401621 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Mon, 4 Mar 2024 14:57:55 +0000 Subject: [PATCH] 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. --- lib/kamal/cli/templates/deploy.yml | 2 ++ lib/kamal/commands/base.rb | 4 +++ lib/kamal/commands/builder/base.rb | 12 ++++++++- lib/kamal/commands/builder/multiarch.rb | 18 ++++++------- lib/kamal/commands/builder/native.rb | 15 +++++------ lib/kamal/commands/builder/native/cached.rb | 13 +++++----- lib/kamal/commands/builder/native/remote.rb | 18 ++++++------- lib/kamal/configuration.rb | 5 +++- lib/kamal/configuration/builder.rb | 9 ++++++- test/commands/builder_test.rb | 28 +++++++++++++++++++++ test/configuration_test.rb | 10 ++++++++ 11 files changed, 100 insertions(+), 34 deletions(-) diff --git a/lib/kamal/cli/templates/deploy.yml b/lib/kamal/cli/templates/deploy.yml index af018554..2c9568f4 100644 --- a/lib/kamal/cli/templates/deploy.yml +++ b/lib/kamal/cli/templates/deploy.yml @@ -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: diff --git a/lib/kamal/commands/base.rb b/lib/kamal/commands/base.rb index 7e09fc1c..27948434 100644 --- a/lib/kamal/commands/base.rb +++ b/lib/kamal/commands/base.rb @@ -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 diff --git a/lib/kamal/commands/builder/base.rb b/lib/kamal/commands/builder/base.rb index e288db18..95b079e1 100644 --- a/lib/kamal/commands/builder/base.rb +++ b/lib/kamal/commands/builder/base.rb @@ -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 diff --git a/lib/kamal/commands/builder/multiarch.rb b/lib/kamal/commands/builder/multiarch.rb index 2f9e4d19..b80200cd 100644 --- a/lib/kamal/commands/builder/multiarch.rb +++ b/lib/kamal/commands/builder/multiarch.rb @@ -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 diff --git a/lib/kamal/commands/builder/native.rb b/lib/kamal/commands/builder/native.rb index d67d3519..cc0f03b1 100644 --- a/lib/kamal/commands/builder/native.rb +++ b/lib/kamal/commands/builder/native.rb @@ -7,14 +7,15 @@ class Kamal::Commands::Builder::Native < Kamal::Commands::Builder::Base # No-op on native without cache end - def 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 + + private + def build_and_push + combine \ + docker(:build, *build_options, build_context), + docker(:push, config.absolute_image), + docker(:push, config.latest_image) + end end diff --git a/lib/kamal/commands/builder/native/cached.rb b/lib/kamal/commands/builder/native/cached.rb index f72d1192..b3a3d635 100644 --- a/lib/kamal/commands/builder/native/cached.rb +++ b/lib/kamal/commands/builder/native/cached.rb @@ -7,10 +7,11 @@ class Kamal::Commands::Builder::Native::Cached < Kamal::Commands::Builder::Nativ docker :buildx, :rm, builder_name end - def push - docker :buildx, :build, - "--push", - *build_options, - build_context - end + private + def build_and_push + docker :buildx, :build, + "--push", + *build_options, + build_context + end end diff --git a/lib/kamal/commands/builder/native/remote.rb b/lib/kamal/commands/builder/native/remote.rb index 67d68e9f..d3053d40 100644 --- a/lib/kamal/commands/builder/native/remote.rb +++ b/lib/kamal/commands/builder/native/remote.rb @@ -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 diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index bd5189d9..483ca770 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -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 diff --git a/lib/kamal/configuration/builder.rb b/lib/kamal/configuration/builder.rb index 33d10fbf..6805ca72 100644 --- a/lib/kamal/configuration/builder.rb +++ b/lib/kamal/configuration/builder.rb @@ -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 diff --git a/test/commands/builder_test.rb b/test/commands/builder_test.rb index 7b5a28e9..bc78184a 100644 --- a/test/commands/builder_test.rb +++ b/test/commands/builder_test.rb @@ -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")) diff --git a/test/configuration_test.rb b/test/configuration_test.rb index 9f50e9df..720f65fc 100644 --- a/test/configuration_test.rb +++ b/test/configuration_test.rb @@ -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