From b12de87388d12e56082b062ef18dfe4a1028b728 Mon Sep 17 00:00:00 2001 From: xiaohui Date: Wed, 17 Jul 2024 10:36:58 +0800 Subject: [PATCH 01/27] git clone with --recurse-submodules --- lib/kamal/commands/builder/clone.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/kamal/commands/builder/clone.rb b/lib/kamal/commands/builder/clone.rb index b40e9e4e..17d9c931 100644 --- a/lib/kamal/commands/builder/clone.rb +++ b/lib/kamal/commands/builder/clone.rb @@ -6,7 +6,7 @@ module Kamal::Commands::Builder::Clone end def clone - git :clone, Kamal::Git.root, path: clone_directory + git :clone, Kamal::Git.root, "--recurse-submodules", path: clone_directory end def clone_reset_steps @@ -14,7 +14,8 @@ module Kamal::Commands::Builder::Clone git(:remote, "set-url", :origin, Kamal::Git.root, path: build_directory), git(:fetch, :origin, path: build_directory), git(:reset, "--hard", Kamal::Git.revision, path: build_directory), - git(:clean, "-fdx", path: build_directory) + git(:clean, "-fdx", path: build_directory), + git(:submodule, :update, "--init", path: build_directory) ] end From b8af719bb7c8d8739d74405d1ef4c6676c09e7f2 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Tue, 18 Jun 2024 16:11:01 +0100 Subject: [PATCH 02/27] Add aliases to Kamal Aliases are defined in the configuration file under the `aliases` key. The configuration is a map of alias name to command. When we run the command the we just do a literal replacement of the alias with the string. So if we have: ```yaml aliases: console: app exec -r console -i --reuse "rails console" ``` Then running `kamal console -r workers` will run the command ```sh $ kamal app exec -r console -i --reuse "rails console" -r workers ``` Because of the order Thor parses the arguments, this allows us to override the role from the alias command. There might be cases where we need to munge the command a bit more but that would involve getting into Thor command parsing internals, which are complicated and possibly subject to change. There's a chance that your aliases could conflict with future built-in commands, but there's not likely to be many of those and if it happens you'll get a validation error when you upgrade. Thanks to @dhnaranjo for the idea! --- Gemfile.lock | 2 +- bin/docs | 20 +++++---- kamal.gemspec | 2 +- lib/kamal/cli/alias/command.rb | 9 ++++ lib/kamal/cli/base.rb | 13 ++++-- lib/kamal/commander.rb | 10 ++++- lib/kamal/configuration.rb | 3 +- lib/kamal/configuration/alias.rb | 15 +++++++ lib/kamal/configuration/docs/alias.yml | 26 +++++++++++ .../configuration/docs/configuration.yml | 6 +++ lib/kamal/configuration/validator.rb | 44 ++++++++++--------- lib/kamal/configuration/validator/alias.rb | 15 +++++++ test/cli/cli_test_case.rb | 8 ++++ test/cli/main_test.rb | 33 +++++++++++++- test/fixtures/deploy_with_aliases.yml | 20 +++++++++ .../deployer/app/.kamal/hooks/pre-connect | 4 -- .../app_with_roles/.kamal/hooks/pre-connect | 4 -- .../deployer/app_with_roles/config/deploy.yml | 3 ++ test/integration/main_test.rb | 13 ++++++ 19 files changed, 204 insertions(+), 46 deletions(-) create mode 100644 lib/kamal/cli/alias/command.rb create mode 100644 lib/kamal/configuration/alias.rb create mode 100644 lib/kamal/configuration/docs/alias.yml create mode 100644 lib/kamal/configuration/validator/alias.rb create mode 100644 test/fixtures/deploy_with_aliases.yml diff --git a/Gemfile.lock b/Gemfile.lock index a24796bd..6d553e8c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,7 +10,7 @@ PATH ed25519 (~> 1.2) net-ssh (~> 7.0) sshkit (>= 1.23.0, < 2.0) - thor (~> 1.2) + thor (~> 1.3) zeitwerk (~> 2.5) GEM diff --git a/bin/docs b/bin/docs index 08b2937a..a8731ce2 100755 --- a/bin/docs +++ b/bin/docs @@ -17,6 +17,7 @@ end DOCS = { "accessory" => "Accessories", + "alias" => "Aliases", "boot" => "Booting", "builder" => "Builders", "configuration" => "Configuration overview", @@ -67,26 +68,27 @@ class DocWriter output.puts place = :new_section elsif line =~ /^ *#/ - generate_line(line, place: place) + generate_line(line, heading: place == :new_section) place = :in_section else output.puts "```yaml" - output.print line + output.puts line place = :in_yaml end - when :in_yaml + when :in_yaml, :in_empty_line_yaml if line =~ /^ *#/ output.puts "```" - generate_line(line, place: :new_section) + generate_line(line, heading: place == :in_empty_line_yaml) place = :in_section + elsif line.empty? + place = :in_empty_line_yaml else - output.puts - output.print line + output.puts line end end end - output.puts "\n```" if place == :in_yaml + output.puts "```" if place == :in_yaml end def generate_header @@ -98,7 +100,7 @@ class DocWriter output.puts end - def generate_line(line, place: :in_section) + def generate_line(line, heading: false) line = line.gsub(/^ *#\s?/, "") if line =~ /(.*)kamal docs ([a-z]*)(.*)/ @@ -109,7 +111,7 @@ class DocWriter line = "#{$1}[#{titlify($2.split("/").last)}](#{$2})#{$3}" end - if place == :new_section + if heading output.puts "## [#{line}](##{linkify(line)})" else output.puts line diff --git a/kamal.gemspec b/kamal.gemspec index 3b20e845..ff499f4e 100644 --- a/kamal.gemspec +++ b/kamal.gemspec @@ -14,7 +14,7 @@ Gem::Specification.new do |spec| spec.add_dependency "activesupport", ">= 7.0" spec.add_dependency "sshkit", ">= 1.23.0", "< 2.0" spec.add_dependency "net-ssh", "~> 7.0" - spec.add_dependency "thor", "~> 1.2" + spec.add_dependency "thor", "~> 1.3" spec.add_dependency "dotenv", "~> 2.8" spec.add_dependency "zeitwerk", "~> 2.5" spec.add_dependency "ed25519", "~> 1.2" diff --git a/lib/kamal/cli/alias/command.rb b/lib/kamal/cli/alias/command.rb new file mode 100644 index 00000000..4bb70c5a --- /dev/null +++ b/lib/kamal/cli/alias/command.rb @@ -0,0 +1,9 @@ +class Kamal::Cli::Alias::Command < Thor::DynamicCommand + def run(instance, args = []) + if (_alias = KAMAL.config.aliases[name]) + Kamal::Cli::Main.start(Shellwords.split(_alias.command) + ARGV[1..-1]) + else + super + end + end +end diff --git a/lib/kamal/cli/base.rb b/lib/kamal/cli/base.rb index 0710c088..8032a4cb 100644 --- a/lib/kamal/cli/base.rb +++ b/lib/kamal/cli/base.rb @@ -6,7 +6,8 @@ module Kamal::Cli class Base < Thor include SSHKit::DSL - def self.exit_on_failure?() true end + def self.exit_on_failure?() false end + def self.dynamic_command_class() Kamal::Cli::Alias::Command end class_option :verbose, type: :boolean, aliases: "-v", desc: "Detailed logging" class_option :quiet, type: :boolean, aliases: "-q", desc: "Minimal logging" @@ -22,8 +23,14 @@ module Kamal::Cli class_option :skip_hooks, aliases: "-H", type: :boolean, default: false, desc: "Don't run hooks" - def initialize(*) - super + def initialize(args = [], local_options = {}, config = {}) + if config[:current_command].is_a?(Kamal::Cli::Alias::Command) + # When Thor generates a dynamic command, it doesn't attempt to parse the arguments. + # For our purposes, it means the arguments are passed in args rather than local_options. + super([], args, config) + else + super + end @original_env = ENV.to_h.dup load_env initialize_commander(options_with_subcommand_class_options) diff --git a/lib/kamal/commander.rb b/lib/kamal/commander.rb index c28fda82..ae98e0f8 100644 --- a/lib/kamal/commander.rb +++ b/lib/kamal/commander.rb @@ -27,7 +27,11 @@ class Kamal::Commander def specific_primary! @specifics = nil - self.specific_hosts = [ config.primary_host ] + if specific_roles.present? + self.specific_hosts = [ specific_roles.first.primary_host ] + else + self.specific_hosts = [ config.primary_host ] + end end def specific_roles=(role_names) @@ -113,6 +117,10 @@ class Kamal::Commander @traefik ||= Kamal::Commands::Traefik.new(config) end + def alias(name) + config.aliases[name] + end + def with_verbosity(level) old_level = self.verbosity diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index 8d989464..af3754eb 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -11,7 +11,7 @@ class Kamal::Configuration delegate :argumentize, :optionize, to: Kamal::Utils attr_reader :destination, :raw_config - attr_reader :accessories, :boot, :builder, :env, :healthcheck, :logging, :traefik, :servers, :ssh, :sshkit, :registry + attr_reader :accessories, :aliases, :boot, :builder, :env, :healthcheck, :logging, :traefik, :servers, :ssh, :sshkit, :registry include Validation @@ -54,6 +54,7 @@ class Kamal::Configuration @registry = Registry.new(config: self) @accessories = @raw_config.accessories&.keys&.collect { |name| Accessory.new(name, config: self) } || [] + @aliases = @raw_config.aliases&.keys&.to_h { |name| [ name, Alias.new(name, config: self) ] } || {} @boot = Boot.new(config: self) @builder = Builder.new(config: self) @env = Env.new(config: @raw_config.env || {}) diff --git a/lib/kamal/configuration/alias.rb b/lib/kamal/configuration/alias.rb new file mode 100644 index 00000000..327578d9 --- /dev/null +++ b/lib/kamal/configuration/alias.rb @@ -0,0 +1,15 @@ +class Kamal::Configuration::Alias + include Kamal::Configuration::Validation + + attr_reader :name, :command + + def initialize(name, config:) + @name, @command = name.inquiry, config.raw_config["aliases"][name] + + validate! \ + command, + example: validation_yml["aliases"]["uname"], + context: "aliases/#{name}", + with: Kamal::Configuration::Validator::Alias + end +end diff --git a/lib/kamal/configuration/docs/alias.yml b/lib/kamal/configuration/docs/alias.yml new file mode 100644 index 00000000..4c28cfe3 --- /dev/null +++ b/lib/kamal/configuration/docs/alias.yml @@ -0,0 +1,26 @@ +# Aliases +# +# Aliases are shortcuts for Kamal commands. +# +# For example, for a Rails app, you might open a console with: +# +# ```shell +# kamal app exec -i -r console "rails console" +# ``` +# +# By defining an alias, like this: +aliases: + console: app exec -r console -i "rails console" +# You can now open the console with: +# ```shell +# kamal console +# ``` + +# Configuring aliases +# +# Aliases are defined in the root config under the alias key +# +# Each alias is named and can only contain lowercase letters, numbers, dashes and underscores. + +aliases: + uname: app exec -p -q -r web "uname -a" diff --git a/lib/kamal/configuration/docs/configuration.yml b/lib/kamal/configuration/docs/configuration.yml index fc9245c5..f1045dd6 100644 --- a/lib/kamal/configuration/docs/configuration.yml +++ b/lib/kamal/configuration/docs/configuration.yml @@ -166,3 +166,9 @@ healthcheck: # Docker logging configuration, see kamal docs logging logging: ... + +# Aliases +# +# Alias configuration, see kamal docs alias +aliases: + ... diff --git a/lib/kamal/configuration/validator.rb b/lib/kamal/configuration/validator.rb index c7ba0f72..3374e004 100644 --- a/lib/kamal/configuration/validator.rb +++ b/lib/kamal/configuration/validator.rb @@ -13,32 +13,34 @@ class Kamal::Configuration::Validator private def validate_against_example!(validation_config, example) - validate_type! validation_config, Hash + validate_type! validation_config, example.class - check_unknown_keys! validation_config, example + if example.class == Hash + check_unknown_keys! validation_config, example - validation_config.each do |key, value| - next if extension?(key) - with_context(key) do - example_value = example[key] + validation_config.each do |key, value| + next if extension?(key) + with_context(key) do + example_value = example[key] - if example_value == "..." - validate_type! value, *(Array if key == :servers), Hash - elsif key == "hosts" - validate_servers! value - elsif example_value.is_a?(Array) - validate_array_of! value, example_value.first.class - elsif example_value.is_a?(Hash) - case key.to_s - when "options", "args" - validate_type! value, Hash - when "labels" - validate_hash_of! value, example_value.first[1].class + if example_value == "..." + validate_type! value, *(Array if key == :servers), Hash + elsif key == "hosts" + validate_servers! value + elsif example_value.is_a?(Array) + validate_array_of! value, example_value.first.class + elsif example_value.is_a?(Hash) + case key.to_s + when "options", "args" + validate_type! value, Hash + when "labels" + validate_hash_of! value, example_value.first[1].class + else + validate_against_example! value, example_value + end else - validate_against_example! value, example_value + validate_type! value, example_value.class end - else - validate_type! value, example_value.class end end end diff --git a/lib/kamal/configuration/validator/alias.rb b/lib/kamal/configuration/validator/alias.rb new file mode 100644 index 00000000..5873d6e7 --- /dev/null +++ b/lib/kamal/configuration/validator/alias.rb @@ -0,0 +1,15 @@ +class Kamal::Configuration::Validator::Alias < Kamal::Configuration::Validator + def validate! + super + + name = context.delete_prefix("aliases/") + + if name !~ /\A[a-z0-9_-]+\z/ + error "Invalid alias name: '#{name}'. Must only contain lowercase letters, alphanumeric, hyphens and underscores." + end + + if Kamal::Cli::Main.commands.include?(name) + error "Alias '#{name}' conflicts with a built-in command." + end + end +end diff --git a/test/cli/cli_test_case.rb b/test/cli/cli_test_case.rb index 4c6b491d..231da3d0 100644 --- a/test/cli/cli_test_case.rb +++ b/test/cli/cli_test_case.rb @@ -63,4 +63,12 @@ class CliTestCase < ActiveSupport::TestCase assert_match expected, output end + + def with_argv(*argv) + old_argv = ARGV + ARGV.replace(*argv) + yield + ensure + ARGV.replace(old_argv) + end end diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index e562499b..fa0ed553 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -537,9 +537,40 @@ class CliMainTest < CliTestCase assert_equal Kamal::VERSION, version end + test "run an alias for details" do + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:details") + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:details") + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:details", [ "all" ]) + + run_command("info", config_file: "deploy_with_aliases") + end + + test "run an alias for a console" do + run_command("console", config_file: "deploy_with_aliases").tap do |output| + assert_match "docker exec app-console-999 bin/console on 1.1.1.5", output + assert_match "App Host: 1.1.1.5", output + end + end + + test "run an alias for a console overriding role" do + run_command("console", "-r", "workers", config_file: "deploy_with_aliases").tap do |output| + assert_match "docker exec app-workers-999 bin/console on 1.1.1.3", output + assert_match "App Host: 1.1.1.3", output + end + end + + test "run an alias for a console passing command" do + run_command("exec", "bin/job", config_file: "deploy_with_aliases").tap do |output| + assert_match "docker exec app-console-999 bin/job on 1.1.1.5", output + assert_match "App Host: 1.1.1.5", output + end + end + private def run_command(*command, config_file: "deploy_simple") - stdouted { Kamal::Cli::Main.start([ *command, "-c", "test/fixtures/#{config_file}.yml" ]) } + with_argv([ *command, "-c", "test/fixtures/#{config_file}.yml" ]) do + stdouted { Kamal::Cli::Main.start } + end end def with_test_dotenv(**files) diff --git a/test/fixtures/deploy_with_aliases.yml b/test/fixtures/deploy_with_aliases.yml new file mode 100644 index 00000000..6a8f00b8 --- /dev/null +++ b/test/fixtures/deploy_with_aliases.yml @@ -0,0 +1,20 @@ +service: app +image: dhh/app +servers: + web: + - 1.1.1.1 + - 1.1.1.2 + workers: + hosts: + - 1.1.1.3 + - 1.1.1.4 + console: + hosts: + - 1.1.1.5 +registry: + username: user + password: pw +aliases: + info: details + console: app exec --reuse -p -r console "bin/console" + exec: app exec --reuse -p -r console diff --git a/test/integration/docker/deployer/app/.kamal/hooks/pre-connect b/test/integration/docker/deployer/app/.kamal/hooks/pre-connect index e6c573d4..e17784a5 100755 --- a/test/integration/docker/deployer/app/.kamal/hooks/pre-connect +++ b/test/integration/docker/deployer/app/.kamal/hooks/pre-connect @@ -1,8 +1,4 @@ #!/bin/sh echo "About to lock..." -if [ "$KAMAL_HOSTS" != "vm1,vm2" ]; then - echo "Expected hosts to be 'vm1,vm2', got $KAMAL_HOSTS" - exit 1 -fi mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-connect diff --git a/test/integration/docker/deployer/app_with_roles/.kamal/hooks/pre-connect b/test/integration/docker/deployer/app_with_roles/.kamal/hooks/pre-connect index 10095286..e17784a5 100755 --- a/test/integration/docker/deployer/app_with_roles/.kamal/hooks/pre-connect +++ b/test/integration/docker/deployer/app_with_roles/.kamal/hooks/pre-connect @@ -1,8 +1,4 @@ #!/bin/sh echo "About to lock..." -if [ "$KAMAL_HOSTS" != "vm1,vm2,vm3" ]; then - echo "Expected hosts to be 'vm1,vm2,vm3', got $KAMAL_HOSTS" - exit 1 -fi mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-connect diff --git a/test/integration/docker/deployer/app_with_roles/config/deploy.yml b/test/integration/docker/deployer/app_with_roles/config/deploy.yml index 2cf362c6..e5c6e28a 100644 --- a/test/integration/docker/deployer/app_with_roles/config/deploy.yml +++ b/test/integration/docker/deployer/app_with_roles/config/deploy.yml @@ -37,3 +37,6 @@ accessories: - web stop_wait_time: 1 readiness_delay: 0 +aliases: + whome: version + worker_hostname: app exec -r workers -q --reuse hostname diff --git a/test/integration/main_test.rb b/test/integration/main_test.rb index c4558c1d..8ea04b11 100644 --- a/test/integration/main_test.rb +++ b/test/integration/main_test.rb @@ -82,6 +82,19 @@ class MainTest < IntegrationTest assert_equal({ "cmd"=>"wget -qO- http://localhost > /dev/null || exit 1", "interval"=>"1s", "max_attempts"=>3, "port"=>3000, "path"=>"/up", "cord"=>"/tmp/kamal-cord", "log_lines"=>50 }, config[:healthcheck]) end + test "aliases" do + @app = "app_with_roles" + + kamal :envify + kamal :deploy + + output = kamal :whome, capture: true + assert_equal Kamal::VERSION, output + + output = kamal :worker_hostname, capture: true + assert_match /App Host: vm3\nvm3-[0-9a-f]{12}$/, output + end + test "setup and remove" do # Check remove completes when nothing has been setup yet kamal :remove, "-y" From 9ccfe20b107cec85aca8f37834cf7c66add972c3 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Mon, 26 Aug 2024 11:06:41 +0100 Subject: [PATCH 03/27] Fix up tests --- test/cli/build_test.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/cli/build_test.rb b/test/cli/build_test.rb index 18ff254b..ed3cfcdb 100644 --- a/test/cli/build_test.rb +++ b/test/cli/build_test.rb @@ -42,7 +42,7 @@ class CliBuildTest < CliTestCase SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "--version", "&&", :docker, :buildx, "version") SSHKit::Backend::Abstract.any_instance.expects(:execute) - .with(:git, "-C", "#{Dir.tmpdir}/kamal-clones/app-#{pwd_sha}", :clone, Dir.pwd) + .with(:git, "-C", "#{Dir.tmpdir}/kamal-clones/app-#{pwd_sha}", :clone, Dir.pwd, "--recurse-submodules") .raises(SSHKit::Command::Failed.new("fatal: destination path 'kamal' already exists and is not an empty directory")) .then .returns(true) @@ -50,6 +50,7 @@ class CliBuildTest < CliTestCase SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :fetch, :origin) SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :reset, "--hard", Kamal::Git.revision) SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :clean, "-fdx") + SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :submodule, :update, "--init") 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", ".") @@ -88,7 +89,7 @@ class CliBuildTest < CliTestCase SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "--version", "&&", :docker, :buildx, "version") SSHKit::Backend::Abstract.any_instance.expects(:execute) - .with(:git, "-C", "#{Dir.tmpdir}/kamal-clones/app-#{pwd_sha}", :clone, Dir.pwd) + .with(:git, "-C", "#{Dir.tmpdir}/kamal-clones/app-#{pwd_sha}", :clone, Dir.pwd, "--recurse-submodules") .raises(SSHKit::Command::Failed.new("fatal: destination path 'kamal' already exists and is not an empty directory")) .then .returns(true) From d6f5da92be4356f8b7dabd7bf10addeb9ac336ba Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 28 Aug 2024 09:43:06 +0100 Subject: [PATCH 04/27] Bump version for 1.8.2 --- Gemfile.lock | 2 +- lib/kamal/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a24796bd..b34eb268 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - kamal (1.8.1) + kamal (1.8.2) activesupport (>= 7.0) base64 (~> 0.2) bcrypt_pbkdf (~> 1.0) diff --git a/lib/kamal/version.rb b/lib/kamal/version.rb index 524b325b..155d1fde 100644 --- a/lib/kamal/version.rb +++ b/lib/kamal/version.rb @@ -1,3 +1,3 @@ module Kamal - VERSION = "1.8.1" + VERSION = "1.8.2" end From 579e169be21ca27b8d1b50d1a75574316a17edb2 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 28 Aug 2024 10:58:25 +0100 Subject: [PATCH 05/27] Allow multiple arguments for `exec` commands If you can have an alias like: ``` aliases: rails: app exec -p rails ``` Then `kamal rails db:migrate:status` will execute `kamal app exec -p rails db:migrate:status`. So this works, we'll allow multiple arguments `app exec` and `server exec` to accept multiple arguments. The arguments are combined by simply joining them with a space. This means that these are equivalent: ``` kamal app exec -p rails db:migrate:status kamal app exec -p "rails db:migrate:status" ``` If you want to pass an argument with spaces, you'll need to quote it: ``` kamal app exec -p "git commit -am \"My comment\"" kamal app exec -p git commit -am "\"My comment\"" ``` --- lib/kamal/cli/app.rb | 5 +++-- lib/kamal/cli/server.rb | 3 ++- lib/kamal/utils.rb | 4 ++++ test/cli/app_test.rb | 6 ++++++ test/cli/main_test.rb | 7 +++++++ test/cli/server_test.rb | 18 ++++++++++++++++-- test/fixtures/deploy_with_aliases.yml | 1 + .../deployer/app_with_roles/config/deploy.yml | 1 + test/integration/main_test.rb | 3 +++ 9 files changed, 43 insertions(+), 5 deletions(-) diff --git a/lib/kamal/cli/app.rb b/lib/kamal/cli/app.rb index 19bf84cd..ce21b2cf 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -71,11 +71,12 @@ class Kamal::Cli::App < Kamal::Cli::Base end end - desc "exec [CMD]", "Execute a custom command on servers within the app container (use --help to show options)" + desc "exec [CMD...]", "Execute a custom command on servers within the app container (use --help to show options)" option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)" option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one" option :env, aliases: "-e", type: :hash, desc: "Set environment variables for the command" - def exec(cmd) + def exec(*cmd) + cmd = Kamal::Utils.join_commands(cmd) env = options[:env] case when options[:interactive] && options[:reuse] diff --git a/lib/kamal/cli/server.rb b/lib/kamal/cli/server.rb index b545050f..5b1b0cc7 100644 --- a/lib/kamal/cli/server.rb +++ b/lib/kamal/cli/server.rb @@ -1,7 +1,8 @@ class Kamal::Cli::Server < Kamal::Cli::Base desc "exec", "Run a custom command on the server (use --help to show options)" option :interactive, type: :boolean, aliases: "-i", default: false, desc: "Run the command interactively (use for console/bash)" - def exec(cmd) + def exec(*cmd) + cmd = Kamal::Utils.join_commands(cmd) hosts = KAMAL.hosts | KAMAL.accessory_hosts case diff --git a/lib/kamal/utils.rb b/lib/kamal/utils.rb index e6b28e43..a6ba5702 100644 --- a/lib/kamal/utils.rb +++ b/lib/kamal/utils.rb @@ -77,4 +77,8 @@ module Kamal::Utils def stable_sort!(elements, &block) elements.sort_by!.with_index { |element, index| [ block.call(element), index ] } end + + def join_commands(commands) + commands.map(&:strip).join(" ") + end end diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index 27e3ed9d..8218dba8 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -247,6 +247,12 @@ class CliAppTest < CliTestCase end end + test "exec separate arguments" do + run_command("exec", "ruby", " -v").tap do |output| + assert_match "docker run --rm --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v", output + end + end + test "exec with reuse" do run_command("exec", "--reuse", "ruby -v").tap do |output| assert_match "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done", output # Get current version diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index fa0ed553..60192cf6 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -566,6 +566,13 @@ class CliMainTest < CliTestCase end end + test "append to command with an alias" do + run_command("rails", "db:migrate:status", config_file: "deploy_with_aliases").tap do |output| + assert_match "docker exec app-console-999 rails db:migrate:status on 1.1.1.5", output + assert_match "App Host: 1.1.1.5", output + end + end + private def run_command(*command, config_file: "deploy_simple") with_argv([ *command, "-c", "test/fixtures/#{config_file}.yml" ]) do diff --git a/test/cli/server_test.rb b/test/cli/server_test.rb index 5d9fec4d..110e217d 100644 --- a/test/cli/server_test.rb +++ b/test/cli/server_test.rb @@ -3,8 +3,8 @@ require_relative "cli_test_case" class CliServerTest < CliTestCase test "running a command with exec" do SSHKit::Backend::Abstract.any_instance.stubs(:capture) - .with("date", verbosity: 1) - .returns("Today") + .with("date", verbosity: 1) + .returns("Today") hosts = "1.1.1.1".."1.1.1.4" run_command("exec", "date").tap do |output| @@ -15,6 +15,20 @@ class CliServerTest < CliTestCase end end + test "running a command with exec multiple arguments" do + SSHKit::Backend::Abstract.any_instance.stubs(:capture) + .with("date -j", verbosity: 1) + .returns("Today") + + hosts = "1.1.1.1".."1.1.1.4" + run_command("exec", "date", "-j").tap do |output| + hosts.map do |host| + assert_match "Running 'date -j' on #{hosts.to_a.join(', ')}...", output + assert_match "App Host: #{host}\nToday", output + end + end + end + test "bootstrap already installed" do stub_setup SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(true).at_least_once diff --git a/test/fixtures/deploy_with_aliases.yml b/test/fixtures/deploy_with_aliases.yml index 6a8f00b8..bb54fa76 100644 --- a/test/fixtures/deploy_with_aliases.yml +++ b/test/fixtures/deploy_with_aliases.yml @@ -18,3 +18,4 @@ aliases: info: details console: app exec --reuse -p -r console "bin/console" exec: app exec --reuse -p -r console + rails: app exec --reuse -p -r console rails diff --git a/test/integration/docker/deployer/app_with_roles/config/deploy.yml b/test/integration/docker/deployer/app_with_roles/config/deploy.yml index e5c6e28a..7133c023 100644 --- a/test/integration/docker/deployer/app_with_roles/config/deploy.yml +++ b/test/integration/docker/deployer/app_with_roles/config/deploy.yml @@ -40,3 +40,4 @@ readiness_delay: 0 aliases: whome: version worker_hostname: app exec -r workers -q --reuse hostname + uname: server exec -q -p uname diff --git a/test/integration/main_test.rb b/test/integration/main_test.rb index 8ea04b11..742e111c 100644 --- a/test/integration/main_test.rb +++ b/test/integration/main_test.rb @@ -93,6 +93,9 @@ class MainTest < IntegrationTest output = kamal :worker_hostname, capture: true assert_match /App Host: vm3\nvm3-[0-9a-f]{12}$/, output + + output = kamal :uname, "-o", capture: true + assert_match "App Host: vm1\nGNU/Linux", output end test "setup and remove" do From 2515bd705cc89f364b8a13da5013c674514628b1 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 29 Aug 2024 08:33:21 +0100 Subject: [PATCH 06/27] Switch the version on main to 2.0.0.alpha All development is now for the 2.0.0 release. --- Gemfile.lock | 2 +- lib/kamal/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index b34eb268..99626da8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - kamal (1.8.2) + kamal (2.0.0.alpha) activesupport (>= 7.0) base64 (~> 0.2) bcrypt_pbkdf (~> 1.0) diff --git a/lib/kamal/version.rb b/lib/kamal/version.rb index 155d1fde..4c771efd 100644 --- a/lib/kamal/version.rb +++ b/lib/kamal/version.rb @@ -1,3 +1,3 @@ module Kamal - VERSION = "1.8.2" + VERSION = "2.0.0.alpha" end From 0ab838bc253c4f1f5a3d3c2fae4ab4def833d080 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 6 Jun 2024 09:43:58 +0100 Subject: [PATCH 07/27] Combine multiarch and native/cache builders Combine the two builders, as they are almost identical. The only difference was whether the platforms were set. The native cached builder wasn't using the context it created, so now we do. We'll set the driver to `docker-container` - it seems to be the default but the Docker docs claim it is `docker`. --- lib/kamal/commands/builder.rb | 33 +++++++------------ lib/kamal/commands/builder/base.rb | 5 ++- .../builder/{multiarch.rb => local.rb} | 20 ++++++----- .../commands/builder/multiarch/remote.rb | 27 +++++++++++++-- lib/kamal/commands/builder/native/cached.rb | 25 -------------- test/cli/build_test.rb | 20 +++++------ test/cli/main_test.rb | 4 +-- test/commands/builder_test.rb | 24 +++++++------- 8 files changed, 76 insertions(+), 82 deletions(-) rename lib/kamal/commands/builder/{multiarch.rb => local.rb} (53%) delete mode 100644 lib/kamal/commands/builder/native/cached.rb diff --git a/lib/kamal/commands/builder.rb b/lib/kamal/commands/builder.rb index 13c3d829..c7be883a 100644 --- a/lib/kamal/commands/builder.rb +++ b/lib/kamal/commands/builder.rb @@ -11,22 +11,17 @@ class Kamal::Commands::Builder < Kamal::Commands::Base end def target - if config.builder.multiarch? - if config.builder.remote? - if config.builder.local? - multiarch_remote - else - native_remote - end - else - multiarch - end + case + when !config.builder.multiarch? && !config.builder.cached? + native + when !config.builder.multiarch? && config.builder.cached? + local + when config.builder.local? && config.builder.remote? + multiarch_remote + when config.builder.remote? + native_remote else - if config.builder.cached? - native_cached - else - native - end + local end end @@ -34,16 +29,12 @@ class Kamal::Commands::Builder < Kamal::Commands::Base @native ||= Kamal::Commands::Builder::Native.new(config) end - def native_cached - @native ||= Kamal::Commands::Builder::Native::Cached.new(config) - end - def native_remote @native ||= Kamal::Commands::Builder::Native::Remote.new(config) end - def multiarch - @multiarch ||= Kamal::Commands::Builder::Multiarch.new(config) + def local + @local ||= Kamal::Commands::Builder::Local.new(config) end def multiarch_remote diff --git a/lib/kamal/commands/builder/base.rb b/lib/kamal/commands/builder/base.rb index cedfeadf..a59c5561 100644 --- a/lib/kamal/commands/builder/base.rb +++ b/lib/kamal/commands/builder/base.rb @@ -5,7 +5,10 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base ENDPOINT_DOCKER_HOST_INSPECT = "'{{.Endpoints.docker.Host}}'" 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, :multiarch?, :ssh, + to: :builder_config def clean docker :image, :rm, "--force", config.absolute_image diff --git a/lib/kamal/commands/builder/multiarch.rb b/lib/kamal/commands/builder/local.rb similarity index 53% rename from lib/kamal/commands/builder/multiarch.rb rename to lib/kamal/commands/builder/local.rb index ae1d423f..7af8bc06 100644 --- a/lib/kamal/commands/builder/multiarch.rb +++ b/lib/kamal/commands/builder/local.rb @@ -1,6 +1,6 @@ -class Kamal::Commands::Builder::Multiarch < Kamal::Commands::Builder::Base +class Kamal::Commands::Builder::Local < Kamal::Commands::Builder::Base def create - docker :buildx, :create, "--use", "--name", builder_name + docker :buildx, :create, "--name", builder_name, "--driver=docker-container" end def remove @@ -16,7 +16,7 @@ class Kamal::Commands::Builder::Multiarch < Kamal::Commands::Builder::Base def push docker :buildx, :build, "--push", - "--platform", platform_names, + *platform_options, "--builder", builder_name, *build_options, build_context @@ -28,14 +28,16 @@ class Kamal::Commands::Builder::Multiarch < Kamal::Commands::Builder::Base private def builder_name - "kamal-#{config.service}-multiarch" + "kamal-local" end - def platform_names - if local_arch - "linux/#{local_arch}" - else - "linux/amd64,linux/arm64" + def platform_options + if multiarch? + if local_arch + [ "--platform", "linux/#{local_arch}" ] + else + [ "--platform", "linux/amd64,linux/arm64" ] + end end end end diff --git a/lib/kamal/commands/builder/multiarch/remote.rb b/lib/kamal/commands/builder/multiarch/remote.rb index d60bee77..01114bce 100644 --- a/lib/kamal/commands/builder/multiarch/remote.rb +++ b/lib/kamal/commands/builder/multiarch/remote.rb @@ -1,4 +1,4 @@ -class Kamal::Commands::Builder::Multiarch::Remote < Kamal::Commands::Builder::Multiarch +class Kamal::Commands::Builder::Multiarch::Remote < Kamal::Commands::Builder::Base def create combine \ create_contexts, @@ -12,6 +12,21 @@ class Kamal::Commands::Builder::Multiarch::Remote < Kamal::Commands::Builder::Mu super end + def info + combine \ + docker(:context, :ls), + docker(:buildx, :ls) + end + + def push + docker :buildx, :build, + "--push", + *platform_options, + "--builder", builder_name, + *build_options, + build_context + end + def context_hosts chain \ context_host(builder_name_with_arch(local_arch)), @@ -24,7 +39,7 @@ class Kamal::Commands::Builder::Multiarch::Remote < Kamal::Commands::Builder::Mu private def builder_name - super + "-remote" + "kamal-#{config.service}-multiarch-remote" end def builder_name_with_arch(arch) @@ -58,4 +73,12 @@ class Kamal::Commands::Builder::Multiarch::Remote < Kamal::Commands::Builder::Mu def remove_context(arch) docker :context, :rm, builder_name_with_arch(arch) end + + def platform_options + if local_arch + [ "--platform", "linux/#{local_arch}" ] + else + [ "--platform", "linux/amd64,linux/arm64" ] + end + end end diff --git a/lib/kamal/commands/builder/native/cached.rb b/lib/kamal/commands/builder/native/cached.rb deleted file mode 100644 index 8f65d5f3..00000000 --- a/lib/kamal/commands/builder/native/cached.rb +++ /dev/null @@ -1,25 +0,0 @@ -class Kamal::Commands::Builder::Native::Cached < Kamal::Commands::Builder::Native - def create - docker :buildx, :create, "--name", builder_name, "--use", "--driver=docker-container" - end - - def remove - docker :buildx, :rm, builder_name - end - - def push - docker :buildx, :build, - "--push", - *build_options, - build_context - end - - def context_hosts - docker :buildx, :inspect, builder_name, "> /dev/null" - end - - private - def builder_name - "kamal-#{config.service}-native-cached" - end -end diff --git a/test/cli/build_test.rb b/test/cli/build_test.rb index ed3cfcdb..47423761 100644 --- a/test/cli/build_test.rb +++ b/test/cli/build_test.rb @@ -22,7 +22,7 @@ class CliBuildTest < CliTestCase .returns("") SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :buildx, :inspect, "kamal-app-multiarch", "> /dev/null") + .with(:docker, :buildx, :inspect, "kamal-local", "> /dev/null") .returns("") run_command("push", "--verbose").tap do |output| @@ -30,7 +30,7 @@ class CliBuildTest < CliTestCase assert_match /Cloning repo into build directory/, output assert_match /git -C #{Dir.tmpdir}\/kamal-clones\/app-#{pwd_sha} clone #{Dir.pwd}/, 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 /docker buildx build --push --platform linux\/amd64,linux\/arm64 --builder kamal-local -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile \. as .*@localhost/, output end end end @@ -53,7 +53,7 @@ class CliBuildTest < CliTestCase SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :submodule, :update, "--init") 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", ".") + .with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "kamal-local", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".") SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:git, "-C", anything, :"rev-parse", :HEAD) @@ -78,7 +78,7 @@ class CliBuildTest < CliTestCase assert_no_match /Cloning repo into build directory/, output assert_hook_ran "pre-build", output, **hook_variables 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 /docker buildx build --push --platform linux\/amd64,linux\/arm64 --builder kamal-local -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile . as .*@localhost/, output end end @@ -124,10 +124,10 @@ class CliBuildTest < CliTestCase .with(:docker, "--version", "&&", :docker, :buildx, "version") SSHKit::Backend::Abstract.any_instance.expects(:execute) - .with(:docker, :buildx, :create, "--use", "--name", "kamal-app-multiarch") + .with(:docker, :buildx, :create, "--name", "kamal-local", "--driver=docker-container") SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :buildx, :inspect, "kamal-app-multiarch", "> /dev/null") + .with(:docker, :buildx, :inspect, "kamal-local", "> /dev/null") .raises(SSHKit::Command::Failed.new("no builder")) SSHKit::Backend::Abstract.any_instance.expects(:execute).with { |*args| args.first.start_with?("git") } @@ -141,7 +141,7 @@ class CliBuildTest < CliTestCase .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", ".") + .with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "kamal-local", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".") run_command("push").tap do |output| assert_match /WARN Missing compatible builder, so creating a new one first/, output @@ -207,7 +207,7 @@ class CliBuildTest < CliTestCase test "create" do run_command("create").tap do |output| - assert_match /docker buildx create --use --name kamal-app-multiarch/, output + assert_match /docker buildx create --name kamal-local --driver=docker-container/, output end end @@ -240,7 +240,7 @@ class CliBuildTest < CliTestCase test "remove" do run_command("remove").tap do |output| - assert_match /docker buildx rm kamal-app-multiarch/, output + assert_match /docker buildx rm kamal-local/, output end end @@ -250,7 +250,7 @@ class CliBuildTest < CliTestCase .returns("docker builder info") run_command("details").tap do |output| - assert_match /Builder: multiarch/, output + assert_match /Builder: local/, output assert_match /docker builder info/, output end end diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index 60192cf6..fd37e4f9 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -122,7 +122,7 @@ class CliMainTest < CliTestCase .returns("") SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :buildx, :inspect, "kamal-app-multiarch", "> /dev/null") + .with(:docker, :buildx, :inspect, "kamal-local", "> /dev/null") .returns("") SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) @@ -160,7 +160,7 @@ class CliMainTest < CliTestCase .returns("") SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :buildx, :inspect, "kamal-app-multiarch", "> /dev/null") + .with(:docker, :buildx, :inspect, "kamal-local", "> /dev/null") .returns("") SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) diff --git a/test/commands/builder_test.rb b/test/commands/builder_test.rb index f3faa5f6..6e282be7 100644 --- a/test/commands/builder_test.rb +++ b/test/commands/builder_test.rb @@ -7,9 +7,9 @@ class CommandsBuilderTest < ActiveSupport::TestCase test "target multiarch by default" do builder = new_builder_command(builder: { "cache" => { "type" => "gha" } }) - assert_equal "multiarch", builder.name + assert_equal "local", builder.name 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 .", + "docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-local -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 @@ -23,9 +23,9 @@ class CommandsBuilderTest < ActiveSupport::TestCase test "target native cached when multiarch is off and cache is set" do builder = new_builder_command(builder: { "multiarch" => false, "cache" => { "type" => "gha" } }) - assert_equal "native/cached", builder.name + assert_equal "local", builder.name 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 .", + "docker buildx build --push --builder kamal-local -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 @@ -39,9 +39,9 @@ class CommandsBuilderTest < ActiveSupport::TestCase test "target multiarch local when arch is set" do builder = new_builder_command(builder: { "local" => { "arch" => "amd64" } }) - assert_equal "multiarch", builder.name + assert_equal "local", builder.name 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 .", + "docker buildx build --push --platform linux/amd64 --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile .", builder.push.join(" ") end @@ -93,7 +93,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase test "build context" do builder = new_builder_command(builder: { "context" => ".." }) 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 ..", + "docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ..", builder.push.join(" ") end @@ -107,7 +107,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase test "multiarch push with build args" do builder = new_builder_command(builder: { "args" => { "a" => 1, "b" => 2 } }) 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 .", + "docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile .", builder.push.join(" ") end @@ -133,7 +133,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase 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", + "docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo", builder.push.join(" ") end @@ -147,7 +147,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase 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", + "docker buildx build --push --builder kamal-local -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 @@ -160,7 +160,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase 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 "docker buildx inspect kamal-local > /dev/null", command.context_hosts.join(" ") assert_equal "", command.config_context_hosts.join(" ") end @@ -172,7 +172,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase 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 "docker buildx inspect kamal-local > /dev/null", command.context_hosts.join(" ") assert_equal "", command.config_context_hosts.join(" ") end From 3f6433892902d4beeef496d9ef78a123620f7c5e Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 6 Jun 2024 10:11:03 +0100 Subject: [PATCH 08/27] Move native remote to just remote It's just a remote builder, that will build whichever platform is asked for, so let's remove the "native" part. We'll also remove the service name from the builder name, so multiple services can share the same builder. --- lib/kamal/commands/builder.rb | 6 +++--- .../commands/builder/{native => }/remote.rb | 17 ++++++----------- test/cli/build_test.rb | 8 ++++---- test/commands/builder_test.rb | 8 ++++---- 4 files changed, 17 insertions(+), 22 deletions(-) rename lib/kamal/commands/builder/{native => }/remote.rb (61%) diff --git a/lib/kamal/commands/builder.rb b/lib/kamal/commands/builder.rb index c7be883a..8f8ea7a8 100644 --- a/lib/kamal/commands/builder.rb +++ b/lib/kamal/commands/builder.rb @@ -19,7 +19,7 @@ class Kamal::Commands::Builder < Kamal::Commands::Base when config.builder.local? && config.builder.remote? multiarch_remote when config.builder.remote? - native_remote + remote else local end @@ -29,8 +29,8 @@ class Kamal::Commands::Builder < Kamal::Commands::Base @native ||= Kamal::Commands::Builder::Native.new(config) end - def native_remote - @native ||= Kamal::Commands::Builder::Native::Remote.new(config) + def remote + @remote ||= Kamal::Commands::Builder::Remote.new(config) end def local diff --git a/lib/kamal/commands/builder/native/remote.rb b/lib/kamal/commands/builder/remote.rb similarity index 61% rename from lib/kamal/commands/builder/native/remote.rb rename to lib/kamal/commands/builder/remote.rb index 9d03b8db..d4c56990 100644 --- a/lib/kamal/commands/builder/native/remote.rb +++ b/lib/kamal/commands/builder/remote.rb @@ -1,4 +1,4 @@ -class Kamal::Commands::Builder::Native::Remote < Kamal::Commands::Builder::Native +class Kamal::Commands::Builder::Remote < Kamal::Commands::Builder::Base def create chain \ create_context, @@ -27,7 +27,7 @@ class Kamal::Commands::Builder::Native::Remote < Kamal::Commands::Builder::Nativ end def context_hosts - context_host(builder_name_with_arch) + context_host(builder_name) end def config_context_hosts @@ -37,11 +37,7 @@ class Kamal::Commands::Builder::Native::Remote < Kamal::Commands::Builder::Nativ private def builder_name - "kamal-#{config.service}-native-remote" - end - - def builder_name_with_arch - "#{builder_name}-#{remote_arch}" + "kamal-remote-#{remote_arch}" end def platform @@ -49,16 +45,15 @@ class Kamal::Commands::Builder::Native::Remote < Kamal::Commands::Builder::Nativ end def create_context - docker :context, :create, - builder_name_with_arch, "--description", "'#{builder_name} #{remote_arch} native host'", "--docker", "'host=#{remote_host}'" + docker :context, :create, builder_name, "--description", "'#{builder_name} host'", "--docker", "'host=#{remote_host}'" end def remove_context - docker :context, :rm, builder_name_with_arch + docker :context, :rm, builder_name end def create_buildx - docker :buildx, :create, "--name", builder_name, builder_name_with_arch, "--platform", platform + docker :buildx, :create, "--name", builder_name, builder_name, "--platform", platform end def remove_buildx diff --git a/test/cli/build_test.rb b/test/cli/build_test.rb index 47423761..9158d0fe 100644 --- a/test/cli/build_test.rb +++ b/test/cli/build_test.rb @@ -214,16 +214,16 @@ class CliBuildTest < CliTestCase test "create remote" do run_command("create", fixture: :with_remote_builder).tap do |output| assert_match "Running /usr/bin/env true on 1.1.1.5", output - assert_match "docker context create kamal-app-native-remote-amd64 --description 'kamal-app-native-remote amd64 native host' --docker 'host=ssh://app@1.1.1.5'", output - assert_match "docker buildx create --name kamal-app-native-remote kamal-app-native-remote-amd64 --platform linux/amd64", output + assert_match "docker context create kamal-remote-amd64 --description 'kamal-remote-amd64 host' --docker 'host=ssh://app@1.1.1.5'", output + assert_match "docker buildx create --name kamal-remote-amd64 kamal-remote-amd64 --platform linux/amd64", output end end test "create remote with custom ports" do run_command("create", fixture: :with_remote_builder_and_custom_ports).tap do |output| assert_match "Running /usr/bin/env true on 1.1.1.5", output - assert_match "docker context create kamal-app-native-remote-amd64 --description 'kamal-app-native-remote amd64 native host' --docker 'host=ssh://app@1.1.1.5:2122'", output - assert_match "docker buildx create --name kamal-app-native-remote kamal-app-native-remote-amd64 --platform linux/amd64", output + assert_match "docker context create kamal-remote-amd64 --description 'kamal-remote-amd64 host' --docker 'host=ssh://app@1.1.1.5:2122'", output + assert_match "docker buildx create --name kamal-remote-amd64 kamal-remote-amd64 --platform linux/amd64", output end end diff --git a/test/commands/builder_test.rb b/test/commands/builder_test.rb index 6e282be7..2bd1c577 100644 --- a/test/commands/builder_test.rb +++ b/test/commands/builder_test.rb @@ -47,9 +47,9 @@ class CommandsBuilderTest < ActiveSupport::TestCase test "target native remote when only remote is set" do builder = new_builder_command(builder: { "remote" => { "arch" => "amd64" }, "cache" => { "type" => "gha" } }) - assert_equal "native/remote", builder.name + assert_equal "remote", builder.name 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 .", + "docker buildx build --push --platform linux/amd64 --builder kamal-remote-amd64 -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 @@ -154,7 +154,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase 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", + "docker buildx build --push --platform linux/amd64 --builder kamal-remote-amd64 -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo", builder.push.join(" ") end @@ -178,7 +178,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase 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 "docker context inspect kamal-remote-amd64 --format '{{.Endpoints.docker.Host}}'", command.context_hosts.join(" ") assert_equal [ "ssh://host" ], command.config_context_hosts end From d48080c772a7ccb18ae5b9a00b21d39fedf0c25b Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 6 Jun 2024 10:18:15 +0100 Subject: [PATCH 09/27] Dump native builder We already ensure that buildx is installed, so let's always use it. --- lib/kamal/commands/builder.rb | 6 +----- lib/kamal/commands/builder/native.rb | 20 -------------------- test/commands/builder_test.rb | 12 ++++++------ 3 files changed, 7 insertions(+), 31 deletions(-) delete mode 100644 lib/kamal/commands/builder/native.rb diff --git a/lib/kamal/commands/builder.rb b/lib/kamal/commands/builder.rb index 8f8ea7a8..f73fb5f8 100644 --- a/lib/kamal/commands/builder.rb +++ b/lib/kamal/commands/builder.rb @@ -13,7 +13,7 @@ class Kamal::Commands::Builder < Kamal::Commands::Base def target case when !config.builder.multiarch? && !config.builder.cached? - native + local when !config.builder.multiarch? && config.builder.cached? local when config.builder.local? && config.builder.remote? @@ -25,10 +25,6 @@ class Kamal::Commands::Builder < Kamal::Commands::Base end end - def native - @native ||= Kamal::Commands::Builder::Native.new(config) - end - def remote @remote ||= Kamal::Commands::Builder::Remote.new(config) end diff --git a/lib/kamal/commands/builder/native.rb b/lib/kamal/commands/builder/native.rb deleted file mode 100644 index 599bdc0f..00000000 --- a/lib/kamal/commands/builder/native.rb +++ /dev/null @@ -1,20 +0,0 @@ -class Kamal::Commands::Builder::Native < Kamal::Commands::Builder::Base - def create - # No-op on native without cache - end - - def remove - # No-op on native without cache - end - - def info - # No-op on native - end - - def push - combine \ - docker(:build, *build_options, build_context), - docker(:push, config.absolute_image), - docker(:push, config.latest_image) - end -end diff --git a/test/commands/builder_test.rb b/test/commands/builder_test.rb index 2bd1c577..17e4f4c4 100644 --- a/test/commands/builder_test.rb +++ b/test/commands/builder_test.rb @@ -15,9 +15,9 @@ class CommandsBuilderTest < ActiveSupport::TestCase test "target native when multiarch is off" do builder = new_builder_command(builder: { "multiarch" => false }) - assert_equal "native", builder.name + assert_equal "local", builder.name 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", + "docker buildx build --push --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile .", builder.push.join(" ") end @@ -100,7 +100,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase test "native push with build args" do builder = new_builder_command(builder: { "multiarch" => false, "args" => { "a" => 1, "b" => 2 } }) 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", + "docker buildx build --push --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile .", builder.push.join(" ") end @@ -114,7 +114,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase test "native push with build secrets" do builder = new_builder_command(builder: { "multiarch" => false, "secrets" => [ "a", "b" ] }) 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", + "docker buildx build --push --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"a\" --secret id=\"b\" --file Dockerfile .", builder.push.join(" ") end @@ -140,7 +140,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase 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", + "docker buildx build --push --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo", builder.push.join(" ") end @@ -166,7 +166,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase test "native context hosts" do command = new_builder_command(builder: { "multiarch" => false }) - assert_equal :true, command.context_hosts + assert_equal "docker buildx inspect kamal-local > /dev/null", command.context_hosts.join(" ") assert_equal "", command.config_context_hosts.join(" ") end From ed148628fb237319babc08b840dcf94d554e9941 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 6 Jun 2024 11:11:54 +0100 Subject: [PATCH 10/27] Local build doesn't need a builder --- lib/kamal/commands/builder/local.rb | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/kamal/commands/builder/local.rb b/lib/kamal/commands/builder/local.rb index 7af8bc06..b404dd16 100644 --- a/lib/kamal/commands/builder/local.rb +++ b/lib/kamal/commands/builder/local.rb @@ -1,10 +1,8 @@ class Kamal::Commands::Builder::Local < Kamal::Commands::Builder::Base def create - docker :buildx, :create, "--name", builder_name, "--driver=docker-container" end def remove - docker :buildx, :rm, builder_name end def info @@ -22,10 +20,6 @@ class Kamal::Commands::Builder::Local < Kamal::Commands::Builder::Base build_context end - def context_hosts - docker :buildx, :inspect, builder_name, "> /dev/null" - end - private def builder_name "kamal-local" From c048c097ed3a374c56d46e32bf6aef541ed2853f Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 6 Jun 2024 12:07:20 +0100 Subject: [PATCH 11/27] Create a context for local builds This ensures we use the docker-container driver and not whatever the local default is. --- lib/kamal/commands/builder/local.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/kamal/commands/builder/local.rb b/lib/kamal/commands/builder/local.rb index b404dd16..7af8bc06 100644 --- a/lib/kamal/commands/builder/local.rb +++ b/lib/kamal/commands/builder/local.rb @@ -1,8 +1,10 @@ class Kamal::Commands::Builder::Local < Kamal::Commands::Builder::Base def create + docker :buildx, :create, "--name", builder_name, "--driver=docker-container" end def remove + docker :buildx, :rm, builder_name end def info @@ -20,6 +22,10 @@ class Kamal::Commands::Builder::Local < Kamal::Commands::Builder::Base build_context end + def context_hosts + docker :buildx, :inspect, builder_name, "> /dev/null" + end + private def builder_name "kamal-local" From 2aeabda455a6e32f77f5039cfc1a11b62b0fec0e Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 6 Jun 2024 12:17:07 +0100 Subject: [PATCH 12/27] Move multiarch remote builder to hybrid builder Include the host name in the builder name, so we can have one builder per host/arch across all kamal projects. Inherit from the remote builder. The difference in the hybrid builder is that we create a local buildx instance and append the remote context to it. --- lib/kamal/commands/builder.rb | 6 +- lib/kamal/commands/builder/hybrid.rb | 34 ++++++++ .../commands/builder/multiarch/remote.rb | 84 ------------------- lib/kamal/commands/builder/remote.rb | 28 +++---- 4 files changed, 51 insertions(+), 101 deletions(-) create mode 100644 lib/kamal/commands/builder/hybrid.rb delete mode 100644 lib/kamal/commands/builder/multiarch/remote.rb diff --git a/lib/kamal/commands/builder.rb b/lib/kamal/commands/builder.rb index f73fb5f8..c34b6492 100644 --- a/lib/kamal/commands/builder.rb +++ b/lib/kamal/commands/builder.rb @@ -17,7 +17,7 @@ class Kamal::Commands::Builder < Kamal::Commands::Base when !config.builder.multiarch? && config.builder.cached? local when config.builder.local? && config.builder.remote? - multiarch_remote + hybrid when config.builder.remote? remote else @@ -33,8 +33,8 @@ class Kamal::Commands::Builder < Kamal::Commands::Base @local ||= Kamal::Commands::Builder::Local.new(config) end - def multiarch_remote - @multiarch_remote ||= Kamal::Commands::Builder::Multiarch::Remote.new(config) + def hybrid + @hybrid ||= Kamal::Commands::Builder::Hybrid.new(config) end diff --git a/lib/kamal/commands/builder/hybrid.rb b/lib/kamal/commands/builder/hybrid.rb new file mode 100644 index 00000000..3db6a10c --- /dev/null +++ b/lib/kamal/commands/builder/hybrid.rb @@ -0,0 +1,34 @@ +class Kamal::Commands::Builder::Hybrid < Kamal::Commands::Builder::Base + def create + combine \ + create_local_buildx, + create_remote_context, + append_remote_buildx + end + + def context_hosts + chain \ + context_host(builder_name) + end + + def config_context_hosts + [ remote_host ].compact + end + + private + def builder_name + "kamal-hybrid-#{remote_host.gsub(/[^a-z0-9_-]/, "-")}-#{local_arch}-#{remote_arch}" + end + + def create_local_buildx + docker :buildx, :create, "--name", builder_name, "--platform", "linux/#{local_arch}", "--driver=docker-container" + end + + def append_remote_buildx + docker :buildx, :create, "--append", "--name", builder_name, builder_name, "--platform", "linux/#{remote_arch}" + end + + def platform_options + [ "--platform", "linux/#{local_arch},linux/#{remote_arch}" ] + end +end diff --git a/lib/kamal/commands/builder/multiarch/remote.rb b/lib/kamal/commands/builder/multiarch/remote.rb deleted file mode 100644 index 01114bce..00000000 --- a/lib/kamal/commands/builder/multiarch/remote.rb +++ /dev/null @@ -1,84 +0,0 @@ -class Kamal::Commands::Builder::Multiarch::Remote < Kamal::Commands::Builder::Base - def create - combine \ - create_contexts, - create_local_buildx, - append_remote_buildx - end - - def remove - combine \ - remove_contexts, - super - end - - def info - combine \ - docker(:context, :ls), - docker(:buildx, :ls) - end - - def push - docker :buildx, :build, - "--push", - *platform_options, - "--builder", builder_name, - *build_options, - build_context - 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 - def builder_name - "kamal-#{config.service}-multiarch-remote" - end - - def builder_name_with_arch(arch) - "#{builder_name}-#{arch}" - end - - def create_local_buildx - docker :buildx, :create, "--name", builder_name, builder_name_with_arch(local_arch), "--platform", "linux/#{local_arch}" - end - - def append_remote_buildx - docker :buildx, :create, "--append", "--name", builder_name, builder_name_with_arch(remote_arch), "--platform", "linux/#{remote_arch}" - end - - def create_contexts - combine \ - create_context(local_arch, local_host), - create_context(remote_arch, remote_host) - end - - def create_context(arch, host) - docker :context, :create, builder_name_with_arch(arch), "--description", "'#{builder_name} #{arch} native host'", "--docker", "'host=#{host}'" - end - - def remove_contexts - combine \ - remove_context(local_arch), - remove_context(remote_arch) - end - - def remove_context(arch) - docker :context, :rm, builder_name_with_arch(arch) - end - - def platform_options - if local_arch - [ "--platform", "linux/#{local_arch}" ] - else - [ "--platform", "linux/amd64,linux/arm64" ] - end - end -end diff --git a/lib/kamal/commands/builder/remote.rb b/lib/kamal/commands/builder/remote.rb index d4c56990..cad8cd0b 100644 --- a/lib/kamal/commands/builder/remote.rb +++ b/lib/kamal/commands/builder/remote.rb @@ -1,13 +1,13 @@ class Kamal::Commands::Builder::Remote < Kamal::Commands::Builder::Base def create chain \ - create_context, + create_remote_context, create_buildx end def remove chain \ - remove_context, + remove_remote_context, remove_buildx end @@ -19,11 +19,11 @@ class Kamal::Commands::Builder::Remote < Kamal::Commands::Builder::Base def push docker :buildx, :build, - "--push", - "--platform", platform, - "--builder", builder_name, - *build_options, - build_context + "--push", + *platform_options, + "--builder", builder_name, + *build_options, + build_context end def context_hosts @@ -37,18 +37,14 @@ class Kamal::Commands::Builder::Remote < Kamal::Commands::Builder::Base private def builder_name - "kamal-remote-#{remote_arch}" + "kamal-remote-#{remote_host.gsub(/[^a-z0-9_-]/, "-")}-#{remote_arch}" end - def platform - "linux/#{remote_arch}" - end - - def create_context + def create_remote_context docker :context, :create, builder_name, "--description", "'#{builder_name} host'", "--docker", "'host=#{remote_host}'" end - def remove_context + def remove_remote_context docker :context, :rm, builder_name end @@ -59,4 +55,8 @@ class Kamal::Commands::Builder::Remote < Kamal::Commands::Builder::Base def remove_buildx docker :buildx, :rm, builder_name end + + def platform_options + [ "--platform", "linux/#{remote_arch}" ] + end end From d6a5cf3c78889001fd15e79e4155029123603823 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 6 Jun 2024 12:43:27 +0100 Subject: [PATCH 13/27] Rip out context_hosts checks The remote host is now encoded in the builder name so we don't need to check it. We'll just do an inspect to confirm the builder exists. --- lib/kamal/cli/build.rb | 14 +++------- lib/kamal/commands/builder.rb | 3 +- lib/kamal/commands/builder/base.rb | 28 +++++++++++++------ lib/kamal/commands/builder/hybrid.rb | 17 +++-------- lib/kamal/commands/builder/local.rb | 19 ------------- lib/kamal/commands/builder/remote.rb | 17 ++++------- test/commands/builder_test.rb | 42 ---------------------------- 7 files changed, 34 insertions(+), 106 deletions(-) diff --git a/lib/kamal/cli/build.rb b/lib/kamal/cli/build.rb index c655c6ae..eb431b33 100644 --- a/lib/kamal/cli/build.rb +++ b/lib/kamal/cli/build.rb @@ -30,18 +30,9 @@ class Kamal::Cli::Build < Kamal::Cli::Base say "Building with uncommitted changes:\n #{uncommitted_changes}", :yellow end - # Get the command here to ensure the Dir.chdir doesn't interfere with it - push = KAMAL.builder.push - run_locally do begin - context_hosts = capture_with_info(*KAMAL.builder.context_hosts).split("\n") - - 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 + execute *KAMAL.builder.buildx_inspect rescue SSHKit::Command::Failed => e if e.message =~ /(context not found|no builder|does not exist)/ warn "Missing compatible builder, so creating a new one first" @@ -51,6 +42,9 @@ class Kamal::Cli::Build < Kamal::Cli::Base end end + # Get the command here to ensure the Dir.chdir doesn't interfere with it + push = KAMAL.builder.push + KAMAL.with_verbosity(:debug) do Dir.chdir(KAMAL.config.builder.build_directory) { execute *push } end diff --git a/lib/kamal/commands/builder.rb b/lib/kamal/commands/builder.rb index c34b6492..1e764154 100644 --- a/lib/kamal/commands/builder.rb +++ b/lib/kamal/commands/builder.rb @@ -1,8 +1,7 @@ require "active_support/core_ext/string/filters" class Kamal::Commands::Builder < Kamal::Commands::Base - delegate :create, :remove, :push, :clean, :pull, :info, :context_hosts, :config_context_hosts, :validate_image, - :first_mirror, to: :target + delegate :create, :remove, :push, :clean, :pull, :info, :buildx_inspect, :validate_image, :first_mirror, to: :target include Clone diff --git a/lib/kamal/commands/builder/base.rb b/lib/kamal/commands/builder/base.rb index a59c5561..763b3142 100644 --- a/lib/kamal/commands/builder/base.rb +++ b/lib/kamal/commands/builder/base.rb @@ -1,4 +1,3 @@ - class Kamal::Commands::Builder::Base < Kamal::Commands::Base class BuilderError < StandardError; end @@ -14,10 +13,29 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base docker :image, :rm, "--force", config.absolute_image end + def push + docker :buildx, :build, + "--push", + *platform_options, + "--builder", builder_name, + *build_options, + build_context + end + def pull docker :pull, config.absolute_image end + def info + combine \ + docker(:context, :ls), + docker(:buildx, :ls) + end + + def buildx_inspect + docker :buildx, :inspect, builder_name + end + def build_options [ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh ] end @@ -35,14 +53,6 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base ) end - def context_hosts - :true - end - - def config_context_hosts - [] - end - def first_mirror docker(:info, "--format '{{index .RegistryConfig.Mirrors 0}}'") end diff --git a/lib/kamal/commands/builder/hybrid.rb b/lib/kamal/commands/builder/hybrid.rb index 3db6a10c..ac1d2e6c 100644 --- a/lib/kamal/commands/builder/hybrid.rb +++ b/lib/kamal/commands/builder/hybrid.rb @@ -1,4 +1,4 @@ -class Kamal::Commands::Builder::Hybrid < Kamal::Commands::Builder::Base +class Kamal::Commands::Builder::Hybrid < Kamal::Commands::Builder::Remote def create combine \ create_local_buildx, @@ -6,18 +6,9 @@ class Kamal::Commands::Builder::Hybrid < Kamal::Commands::Builder::Base append_remote_buildx end - def context_hosts - chain \ - context_host(builder_name) - end - - def config_context_hosts - [ remote_host ].compact - end - private def builder_name - "kamal-hybrid-#{remote_host.gsub(/[^a-z0-9_-]/, "-")}-#{local_arch}-#{remote_arch}" + "kamal-hybrid-#{local_arch}-#{remote_arch}-#{remote_host.gsub(/[^a-z0-9_-]/, "-")}" end def create_local_buildx @@ -28,7 +19,7 @@ class Kamal::Commands::Builder::Hybrid < Kamal::Commands::Builder::Base docker :buildx, :create, "--append", "--name", builder_name, builder_name, "--platform", "linux/#{remote_arch}" end - def platform_options - [ "--platform", "linux/#{local_arch},linux/#{remote_arch}" ] + def platform + "linux/#{local_arch},linux/#{remote_arch}" end end diff --git a/lib/kamal/commands/builder/local.rb b/lib/kamal/commands/builder/local.rb index 7af8bc06..955143e0 100644 --- a/lib/kamal/commands/builder/local.rb +++ b/lib/kamal/commands/builder/local.rb @@ -7,25 +7,6 @@ class Kamal::Commands::Builder::Local < Kamal::Commands::Builder::Base docker :buildx, :rm, builder_name end - def info - combine \ - docker(:context, :ls), - docker(:buildx, :ls) - end - - def push - docker :buildx, :build, - "--push", - *platform_options, - "--builder", builder_name, - *build_options, - build_context - end - - def context_hosts - docker :buildx, :inspect, builder_name, "> /dev/null" - end - private def builder_name "kamal-local" diff --git a/lib/kamal/commands/builder/remote.rb b/lib/kamal/commands/builder/remote.rb index cad8cd0b..ed34f209 100644 --- a/lib/kamal/commands/builder/remote.rb +++ b/lib/kamal/commands/builder/remote.rb @@ -26,18 +26,9 @@ class Kamal::Commands::Builder::Remote < Kamal::Commands::Builder::Base build_context end - def context_hosts - context_host(builder_name) - end - - def config_context_hosts - [ remote_host ] - end - - private def builder_name - "kamal-remote-#{remote_host.gsub(/[^a-z0-9_-]/, "-")}-#{remote_arch}" + "kamal-remote-#{remote_arch}-#{remote_host.gsub(/[^a-z0-9_-]/, "-")}" end def create_remote_context @@ -57,6 +48,10 @@ class Kamal::Commands::Builder::Remote < Kamal::Commands::Builder::Base end def platform_options - [ "--platform", "linux/#{remote_arch}" ] + [ "--platform", platform ] + end + + def platform + "linux/#{remote_arch}" end end diff --git a/test/commands/builder_test.rb b/test/commands/builder_test.rb index 17e4f4c4..f04e53b6 100644 --- a/test/commands/builder_test.rb +++ b/test/commands/builder_test.rb @@ -158,48 +158,6 @@ class CommandsBuilderTest < ActiveSupport::TestCase builder.push.join(" ") end - test "multiarch context hosts" do - command = new_builder_command - assert_equal "docker buildx inspect kamal-local > /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 "docker buildx inspect kamal-local > /dev/null", command.context_hosts.join(" ") - 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-local > /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-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 - test "mirror count" do command = new_builder_command assert_equal "docker info --format '{{index .RegistryConfig.Mirrors 0}}'", command.first_mirror.join(" ") From 374c117b791c5b4ca90a2ad17b521e3f05280165 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 6 Jun 2024 12:48:50 +0100 Subject: [PATCH 14/27] Validate multiarch configuration Remote and local are only allowed when multiarch is enabled. Remote requires a host and arch, local only requires an arch. --- lib/kamal/commands/builder/base.rb | 2 +- lib/kamal/configuration/builder.rb | 24 ++++++++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/lib/kamal/commands/builder/base.rb b/lib/kamal/commands/builder/base.rb index 763b3142..1cac6a63 100644 --- a/lib/kamal/commands/builder/base.rb +++ b/lib/kamal/commands/builder/base.rb @@ -5,7 +5,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base delegate :argumentize, to: Kamal::Utils delegate \ - :args, :secrets, :dockerfile, :target, :local_arch, :local_host, :remote_arch, :remote_host, + :args, :secrets, :dockerfile, :target, :local_arch, :remote_arch, :remote_host, :cache_from, :cache_to, :multiarch?, :ssh, to: :builder_config diff --git a/lib/kamal/configuration/builder.rb b/lib/kamal/configuration/builder.rb index 74c8d1c6..a6440dd1 100644 --- a/lib/kamal/configuration/builder.rb +++ b/lib/kamal/configuration/builder.rb @@ -59,10 +59,6 @@ class Kamal::Configuration::Builder builder_config["local"]["arch"] if local? end - def local_host - builder_config["local"]["host"] if local? - end - def remote_arch builder_config["remote"]["arch"] if remote? end @@ -115,6 +111,26 @@ class Kamal::Configuration::Builder end private + def valid? + if multiarch? + if local? + raise ArgumentError, "Invalid builder configuration: local configuration, arch required" unless local_arch + end + + if remote? + raise ArgumentError, "Invalid builder configuration: remote configuration, arch required" unless remote_arch + raise ArgumentError, "Invalid builder configuration: remote configuration, arch required" unless remote_host + end + else + raise ArgumentError, "Invalid builder configuration: multiarch must be enabled for local configuration" if local? + raise ArgumentError, "Invalid builder configuration: multiarch must be enabled for remote configuration" if remote? + end + + if @options["cache"] && @options["cache"]["type"] + raise ArgumentError, "Invalid cache type: #{@options["cache"]["type"]}" unless [ "gha", "registry" ].include?(@options["cache"]["type"]) + end + end + def cache_image builder_config["cache"]&.fetch("image", nil) || "#{image}-build-cache" end From 98c951bbdb39f036d9de052fe3ef57ad7df72e27 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 6 Jun 2024 12:53:08 +0100 Subject: [PATCH 15/27] Simplfy choosing a builder --- lib/kamal/commands/builder.rb | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/kamal/commands/builder.rb b/lib/kamal/commands/builder.rb index 1e764154..1f105d53 100644 --- a/lib/kamal/commands/builder.rb +++ b/lib/kamal/commands/builder.rb @@ -2,6 +2,7 @@ require "active_support/core_ext/string/filters" class Kamal::Commands::Builder < Kamal::Commands::Base delegate :create, :remove, :push, :clean, :pull, :info, :buildx_inspect, :validate_image, :first_mirror, to: :target + delegate :multiarch?, :local?, :remote?, to: "config.builder" include Clone @@ -10,15 +11,12 @@ class Kamal::Commands::Builder < Kamal::Commands::Base end def target - case - when !config.builder.multiarch? && !config.builder.cached? - local - when !config.builder.multiarch? && config.builder.cached? - local - when config.builder.local? && config.builder.remote? - hybrid - when config.builder.remote? - remote + if remote? + if local? + hybrid + else + remote + end else local end From 7ddb122a221b2acb84c00debacf793b2c6a125a7 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 6 Jun 2024 13:12:44 +0100 Subject: [PATCH 16/27] Get tests passing --- test/cli/build_test.rb | 16 ++++++---------- test/cli/cli_test_case.rb | 5 ++--- test/cli/main_test.rb | 8 -------- test/commands/builder_test.rb | 14 +++++++------- test/configuration/builder_test.rb | 3 +-- 5 files changed, 16 insertions(+), 30 deletions(-) diff --git a/test/cli/build_test.rb b/test/cli/build_test.rb index 9158d0fe..4f05866c 100644 --- a/test/cli/build_test.rb +++ b/test/cli/build_test.rb @@ -21,10 +21,6 @@ class CliBuildTest < CliTestCase .with(:git, "-C", anything, :status, "--porcelain") .returns("") - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :buildx, :inspect, "kamal-local", "> /dev/null") - .returns("") - run_command("push", "--verbose").tap do |output| assert_hook_ran "pre-build", output, **hook_variables assert_match /Cloning repo into build directory/, output @@ -126,8 +122,8 @@ class CliBuildTest < CliTestCase SSHKit::Backend::Abstract.any_instance.expects(:execute) .with(:docker, :buildx, :create, "--name", "kamal-local", "--driver=docker-container") - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :buildx, :inspect, "kamal-local", "> /dev/null") + SSHKit::Backend::Abstract.any_instance.expects(:execute) + .with(:docker, :buildx, :inspect, "kamal-local") .raises(SSHKit::Command::Failed.new("no builder")) SSHKit::Backend::Abstract.any_instance.expects(:execute).with { |*args| args.first.start_with?("git") } @@ -214,16 +210,16 @@ class CliBuildTest < CliTestCase test "create remote" do run_command("create", fixture: :with_remote_builder).tap do |output| assert_match "Running /usr/bin/env true on 1.1.1.5", output - assert_match "docker context create kamal-remote-amd64 --description 'kamal-remote-amd64 host' --docker 'host=ssh://app@1.1.1.5'", output - assert_match "docker buildx create --name kamal-remote-amd64 kamal-remote-amd64 --platform linux/amd64", output + assert_match "docker context create kamal-remote-amd64-ssh---app-1-1-1-5 --description 'kamal-remote-amd64-ssh---app-1-1-1-5 host' --docker 'host=ssh://app@1.1.1.5'", output + assert_match "docker buildx create --name kamal-remote-amd64-ssh---app-1-1-1-5 kamal-remote-amd64-ssh---app-1-1-1-5 --platform linux/amd64", output end end test "create remote with custom ports" do run_command("create", fixture: :with_remote_builder_and_custom_ports).tap do |output| assert_match "Running /usr/bin/env true on 1.1.1.5", output - assert_match "docker context create kamal-remote-amd64 --description 'kamal-remote-amd64 host' --docker 'host=ssh://app@1.1.1.5:2122'", output - assert_match "docker buildx create --name kamal-remote-amd64 kamal-remote-amd64 --platform linux/amd64", output + assert_match "docker context create kamal-remote-amd64-ssh---app-1-1-1-5-2122 --description 'kamal-remote-amd64-ssh---app-1-1-1-5-2122 host' --docker 'host=ssh://app@1.1.1.5:2122'", output + assert_match "docker buildx create --name kamal-remote-amd64-ssh---app-1-1-1-5-2122 kamal-remote-amd64-ssh---app-1-1-1-5-2122 --platform linux/amd64", output end end diff --git a/test/cli/cli_test_case.rb b/test/cli/cli_test_case.rb index 231da3d0..5777bf57 100644 --- a/test/cli/cli_test_case.rb +++ b/test/cli/cli_test_case.rb @@ -36,9 +36,8 @@ class CliTestCase < ActiveSupport::TestCase .with { |arg1, arg2| arg1 == :mkdir && arg2 == ".kamal/locks/app" } SSHKit::Backend::Abstract.any_instance.stubs(:execute) .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("") + SSHKit::Backend::Abstract.any_instance.stubs(:execute) + .with(:docker, :buildx, :inspect, "kamal-local") end def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: false) diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index fd37e4f9..2b87191c 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -121,10 +121,6 @@ class CliMainTest < CliTestCase .with(:git, "-C", anything, :status, "--porcelain") .returns("") - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :buildx, :inspect, "kamal-local", "> /dev/null") - .returns("") - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:docker, :info, "--format '{{index .RegistryConfig.Mirrors 0}}'") .returns("") @@ -159,10 +155,6 @@ class CliMainTest < CliTestCase .with(:git, "-C", anything, :status, "--porcelain") .returns("") - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :buildx, :inspect, "kamal-local", "> /dev/null") - .returns("") - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:docker, :info, "--format '{{index .RegistryConfig.Mirrors 0}}'") .returns("") diff --git a/test/commands/builder_test.rb b/test/commands/builder_test.rb index f04e53b6..3fea77e7 100644 --- a/test/commands/builder_test.rb +++ b/test/commands/builder_test.rb @@ -30,10 +30,10 @@ class CommandsBuilderTest < ActiveSupport::TestCase end test "target multiarch remote when local and remote is set" do - builder = new_builder_command(builder: { "local" => {}, "remote" => {}, "cache" => { "type" => "gha" } }) - assert_equal "multiarch/remote", builder.name + builder = new_builder_command(builder: { "local" => { "arch" => "arm64" }, "remote" => { "arch" => "amd64", "host" => "ssh://app@127.0.0.1" }, "cache" => { "type" => "gha" } }) + assert_equal "hybrid", builder.name 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 .", + "docker buildx build --push --platform linux/arm64,linux/amd64 --builder kamal-hybrid-arm64-amd64-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 .", builder.push.join(" ") end @@ -46,10 +46,10 @@ class CommandsBuilderTest < ActiveSupport::TestCase end test "target native remote when only remote is set" do - builder = new_builder_command(builder: { "remote" => { "arch" => "amd64" }, "cache" => { "type" => "gha" } }) + builder = new_builder_command(builder: { "remote" => { "arch" => "amd64", "host" => "ssh://app@host" }, "cache" => { "type" => "gha" } }) assert_equal "remote", builder.name assert_equal \ - "docker buildx build --push --platform linux/amd64 --builder kamal-remote-amd64 -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .", + "docker buildx build --push --platform linux/amd64 --builder kamal-remote-amd64-ssh---app-host -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 @@ -152,9 +152,9 @@ class CommandsBuilderTest < ActiveSupport::TestCase end test "remote context build" do - builder = new_builder_command(builder: { "remote" => { "arch" => "amd64" }, "context" => "./foo" }) + builder = new_builder_command(builder: { "remote" => { "arch" => "amd64", "host" => "ssh://app@host" }, "context" => "./foo" }) assert_equal \ - "docker buildx build --push --platform linux/amd64 --builder kamal-remote-amd64 -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo", + "docker buildx build --push --platform linux/amd64 --builder kamal-remote-amd64-ssh---app-host -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo", builder.push.join(" ") end diff --git a/test/configuration/builder_test.rb b/test/configuration/builder_test.rb index 4b37b5e8..94acdacc 100644 --- a/test/configuration/builder_test.rb +++ b/test/configuration/builder_test.rb @@ -42,7 +42,7 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase test "setting both local and remote configs" do @deploy_with_builder_option[:builder] = { - "local" => { "arch" => "arm64", "host" => "unix:///Users/<%= `whoami`.strip %>/.docker/run/docker.sock" }, + "local" => { "arch" => "arm64" }, "remote" => { "arch" => "amd64", "host" => "ssh://root@192.168.0.1" } } @@ -53,7 +53,6 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase assert_equal "ssh://root@192.168.0.1", config_with_builder_option.builder.remote_host assert_equal "arm64", config_with_builder_option.builder.local_arch - assert_equal "unix:///Users/<%= `whoami`.strip %>/.docker/run/docker.sock", config_with_builder_option.builder.local_host end test "cached?" do From bd1726f305e52ec9b29481535e4d7955261e1c16 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 31 Jul 2024 16:47:08 +0100 Subject: [PATCH 17/27] docker buildx build -> docker build --- lib/kamal/commands/builder/base.rb | 2 +- lib/kamal/commands/builder/remote.rb | 2 +- lib/kamal/configuration/docs/builder.yml | 4 ++-- test/cli/build_test.rb | 10 ++++----- test/commands/builder_test.rb | 28 ++++++++++++------------ 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/lib/kamal/commands/builder/base.rb b/lib/kamal/commands/builder/base.rb index 1cac6a63..8d0d629b 100644 --- a/lib/kamal/commands/builder/base.rb +++ b/lib/kamal/commands/builder/base.rb @@ -14,7 +14,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base end def push - docker :buildx, :build, + docker :build, "--push", *platform_options, "--builder", builder_name, diff --git a/lib/kamal/commands/builder/remote.rb b/lib/kamal/commands/builder/remote.rb index ed34f209..f59071fd 100644 --- a/lib/kamal/commands/builder/remote.rb +++ b/lib/kamal/commands/builder/remote.rb @@ -18,7 +18,7 @@ class Kamal::Commands::Builder::Remote < Kamal::Commands::Builder::Base end def push - docker :buildx, :build, + docker :build, "--push", *platform_options, "--builder", builder_name, diff --git a/lib/kamal/configuration/docs/builder.yml b/lib/kamal/configuration/docs/builder.yml index b1071105..5dc200bc 100644 --- a/lib/kamal/configuration/docs/builder.yml +++ b/lib/kamal/configuration/docs/builder.yml @@ -1,10 +1,10 @@ # Builder # -# The builder configuration controls how the application is built with `docker build` or `docker buildx build` +# The builder configuration controls how the application is built with `docker build` # # If no configuration is specified, Kamal will: # 1. Create a buildx context called `kamal--multiarch` -# 2. Use `docker buildx build` to build a multiarch image for linux/amd64,linux/arm64 with that context +# 2. Use `docker build` to build a multiarch image for linux/amd64,linux/arm64 with that context # # See https://kamal-deploy.org/docs/configuration/builder-examples/ for more information diff --git a/test/cli/build_test.rb b/test/cli/build_test.rb index 4f05866c..457516f4 100644 --- a/test/cli/build_test.rb +++ b/test/cli/build_test.rb @@ -26,7 +26,7 @@ class CliBuildTest < CliTestCase assert_match /Cloning repo into build directory/, output assert_match /git -C #{Dir.tmpdir}\/kamal-clones\/app-#{pwd_sha} clone #{Dir.pwd}/, output assert_match /docker --version && docker buildx version/, output - assert_match /docker buildx build --push --platform linux\/amd64,linux\/arm64 --builder kamal-local -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile \. as .*@localhost/, output + assert_match /docker build --push --platform linux\/amd64,linux\/arm64 --builder kamal-local -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile \. as .*@localhost/, output end end end @@ -49,7 +49,7 @@ class CliBuildTest < CliTestCase SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :submodule, :update, "--init") SSHKit::Backend::Abstract.any_instance.expects(:execute) - .with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "kamal-local", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".") + .with(:docker, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "kamal-local", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".") SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:git, "-C", anything, :"rev-parse", :HEAD) @@ -74,7 +74,7 @@ class CliBuildTest < CliTestCase assert_no_match /Cloning repo into build directory/, output assert_hook_ran "pre-build", output, **hook_variables assert_match /docker --version && docker buildx version/, output - assert_match /docker buildx build --push --platform linux\/amd64,linux\/arm64 --builder kamal-local -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile . as .*@localhost/, output + assert_match /docker build --push --platform linux\/amd64,linux\/arm64 --builder kamal-local -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile . as .*@localhost/, output end end @@ -137,7 +137,7 @@ class CliBuildTest < CliTestCase .returns("") SSHKit::Backend::Abstract.any_instance.expects(:execute) - .with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "kamal-local", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".") + .with(:docker, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "kamal-local", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".") run_command("push").tap do |output| assert_match /WARN Missing compatible builder, so creating a new one first/, output @@ -161,7 +161,7 @@ class CliBuildTest < CliTestCase error = assert_raises(Kamal::Cli::HookError) { run_command("push") } assert_equal "Hook `pre-build` failed:\nfailed", error.message - assert @executions.none? { |args| args[0..2] == [ :docker, :buildx, :build ] } + assert @executions.none? { |args| args[0..2] == [ :docker, :build ] } end test "pull" do diff --git a/test/commands/builder_test.rb b/test/commands/builder_test.rb index 3fea77e7..620aa73d 100644 --- a/test/commands/builder_test.rb +++ b/test/commands/builder_test.rb @@ -9,7 +9,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase builder = new_builder_command(builder: { "cache" => { "type" => "gha" } }) assert_equal "local", builder.name assert_equal \ - "docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-local -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .", + "docker build --push --platform linux/amd64,linux/arm64 --builder kamal-local -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 @@ -17,7 +17,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase builder = new_builder_command(builder: { "multiarch" => false }) assert_equal "local", builder.name assert_equal \ - "docker buildx build --push --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile .", + "docker build --push --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile .", builder.push.join(" ") end @@ -25,7 +25,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase builder = new_builder_command(builder: { "multiarch" => false, "cache" => { "type" => "gha" } }) assert_equal "local", builder.name assert_equal \ - "docker buildx build --push --builder kamal-local -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .", + "docker build --push --builder kamal-local -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 @@ -33,7 +33,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase builder = new_builder_command(builder: { "local" => { "arch" => "arm64" }, "remote" => { "arch" => "amd64", "host" => "ssh://app@127.0.0.1" }, "cache" => { "type" => "gha" } }) assert_equal "hybrid", builder.name assert_equal \ - "docker buildx build --push --platform linux/arm64,linux/amd64 --builder kamal-hybrid-arm64-amd64-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 .", + "docker build --push --platform linux/arm64,linux/amd64 --builder kamal-hybrid-arm64-amd64-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 .", builder.push.join(" ") end @@ -41,7 +41,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase builder = new_builder_command(builder: { "local" => { "arch" => "amd64" } }) assert_equal "local", builder.name assert_equal \ - "docker buildx build --push --platform linux/amd64 --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile .", + "docker build --push --platform linux/amd64 --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile .", builder.push.join(" ") end @@ -49,7 +49,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase builder = new_builder_command(builder: { "remote" => { "arch" => "amd64", "host" => "ssh://app@host" }, "cache" => { "type" => "gha" } }) assert_equal "remote", builder.name assert_equal \ - "docker buildx build --push --platform linux/amd64 --builder kamal-remote-amd64-ssh---app-host -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .", + "docker build --push --platform linux/amd64 --builder kamal-remote-amd64-ssh---app-host -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 @@ -93,28 +93,28 @@ class CommandsBuilderTest < ActiveSupport::TestCase test "build context" do builder = new_builder_command(builder: { "context" => ".." }) assert_equal \ - "docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ..", + "docker build --push --platform linux/amd64,linux/arm64 --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ..", builder.push.join(" ") end test "native push with build args" do builder = new_builder_command(builder: { "multiarch" => false, "args" => { "a" => 1, "b" => 2 } }) assert_equal \ - "docker buildx build --push --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile .", + "docker build --push --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile .", builder.push.join(" ") end test "multiarch push with build args" do builder = new_builder_command(builder: { "args" => { "a" => 1, "b" => 2 } }) assert_equal \ - "docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile .", + "docker build --push --platform linux/amd64,linux/arm64 --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile .", builder.push.join(" ") end test "native push with build secrets" do builder = new_builder_command(builder: { "multiarch" => false, "secrets" => [ "a", "b" ] }) assert_equal \ - "docker buildx build --push --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"a\" --secret id=\"b\" --file Dockerfile .", + "docker build --push --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"a\" --secret id=\"b\" --file Dockerfile .", builder.push.join(" ") end @@ -133,28 +133,28 @@ class CommandsBuilderTest < ActiveSupport::TestCase 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-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo", + "docker build --push --platform linux/amd64,linux/arm64 --builder kamal-local -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 buildx build --push --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo", + "docker build --push --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo", 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 --builder kamal-local -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile ./foo", + "docker build --push --builder kamal-local -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", "host" => "ssh://app@host" }, "context" => "./foo" }) assert_equal \ - "docker buildx build --push --platform linux/amd64 --builder kamal-remote-amd64-ssh---app-host -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo", + "docker build --push --platform linux/amd64 --builder kamal-remote-amd64-ssh---app-host -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo", builder.push.join(" ") end From cffb6c3d7ec5612b0998e94e15501e63acb19ebe Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 1 Aug 2024 11:57:43 +0100 Subject: [PATCH 18/27] Allow the driver to be set --- lib/kamal/commands/builder/base.rb | 4 ++-- lib/kamal/commands/builder/hybrid.rb | 4 ++-- lib/kamal/commands/builder/local.rb | 6 +++--- lib/kamal/commands/builder/remote.rb | 2 +- lib/kamal/configuration/builder.rb | 13 +++++++++++++ lib/kamal/configuration/docs/builder.yml | 5 +++++ .../docker/deployer/app/config/deploy.yml | 1 + .../deployer/app_with_roles/config/deploy.yml | 1 + 8 files changed, 28 insertions(+), 8 deletions(-) diff --git a/lib/kamal/commands/builder/base.rb b/lib/kamal/commands/builder/base.rb index 8d0d629b..8f68b334 100644 --- a/lib/kamal/commands/builder/base.rb +++ b/lib/kamal/commands/builder/base.rb @@ -6,7 +6,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base delegate :argumentize, to: Kamal::Utils delegate \ :args, :secrets, :dockerfile, :target, :local_arch, :remote_arch, :remote_host, - :cache_from, :cache_to, :multiarch?, :ssh, + :cache_from, :cache_to, :multiarch?, :ssh, :driver, :docker_driver?, to: :builder_config def clean @@ -17,7 +17,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base docker :build, "--push", *platform_options, - "--builder", builder_name, + *([ "--builder", builder_name ] unless docker_driver?), *build_options, build_context end diff --git a/lib/kamal/commands/builder/hybrid.rb b/lib/kamal/commands/builder/hybrid.rb index ac1d2e6c..f8d873dd 100644 --- a/lib/kamal/commands/builder/hybrid.rb +++ b/lib/kamal/commands/builder/hybrid.rb @@ -8,11 +8,11 @@ class Kamal::Commands::Builder::Hybrid < Kamal::Commands::Builder::Remote private def builder_name - "kamal-hybrid-#{local_arch}-#{remote_arch}-#{remote_host.gsub(/[^a-z0-9_-]/, "-")}" + "kamal-hybrid-#{driver}-#{local_arch}-#{remote_arch}-#{remote_host.gsub(/[^a-z0-9_-]/, "-")}" end def create_local_buildx - docker :buildx, :create, "--name", builder_name, "--platform", "linux/#{local_arch}", "--driver=docker-container" + docker :buildx, :create, "--name", builder_name, "--platform", "linux/#{local_arch}", "--driver=#{driver}" end def append_remote_buildx diff --git a/lib/kamal/commands/builder/local.rb b/lib/kamal/commands/builder/local.rb index 955143e0..64b933ae 100644 --- a/lib/kamal/commands/builder/local.rb +++ b/lib/kamal/commands/builder/local.rb @@ -1,15 +1,15 @@ class Kamal::Commands::Builder::Local < Kamal::Commands::Builder::Base def create - docker :buildx, :create, "--name", builder_name, "--driver=docker-container" + docker :buildx, :create, "--name", builder_name, "--driver=#{driver}" unless docker_driver? end def remove - docker :buildx, :rm, builder_name + docker :buildx, :rm, builder_name unless docker_driver? end private def builder_name - "kamal-local" + "kamal-local-#{driver}" end def platform_options diff --git a/lib/kamal/commands/builder/remote.rb b/lib/kamal/commands/builder/remote.rb index f59071fd..3196a6e9 100644 --- a/lib/kamal/commands/builder/remote.rb +++ b/lib/kamal/commands/builder/remote.rb @@ -28,7 +28,7 @@ class Kamal::Commands::Builder::Remote < Kamal::Commands::Builder::Base private def builder_name - "kamal-remote-#{remote_arch}-#{remote_host.gsub(/[^a-z0-9_-]/, "-")}" + "kamal-remote-#{driver}-#{remote_arch}-#{remote_host.gsub(/[^a-z0-9_-]/, "-")}" end def create_remote_context diff --git a/lib/kamal/configuration/builder.rb b/lib/kamal/configuration/builder.rb index a6440dd1..6229a518 100644 --- a/lib/kamal/configuration/builder.rb +++ b/lib/kamal/configuration/builder.rb @@ -55,6 +55,10 @@ class Kamal::Configuration::Builder builder_config["context"] || "." end + def driver + builder_config.fetch("driver", "docker-container") + end + def local_arch builder_config["local"]["arch"] if local? end @@ -110,6 +114,10 @@ class Kamal::Configuration::Builder end end + def docker_driver? + driver == "docker" + end + private def valid? if multiarch? @@ -121,6 +129,10 @@ class Kamal::Configuration::Builder raise ArgumentError, "Invalid builder configuration: remote configuration, arch required" unless remote_arch raise ArgumentError, "Invalid builder configuration: remote configuration, arch required" unless remote_host end + + if docker_driver? + raise ArgumentError, "Invalid builder configuration: the docker driver does not support multiarch builds" + end else raise ArgumentError, "Invalid builder configuration: multiarch must be enabled for local configuration" if local? raise ArgumentError, "Invalid builder configuration: multiarch must be enabled for remote configuration" if remote? @@ -128,6 +140,7 @@ class Kamal::Configuration::Builder if @options["cache"] && @options["cache"]["type"] raise ArgumentError, "Invalid cache type: #{@options["cache"]["type"]}" unless [ "gha", "registry" ].include?(@options["cache"]["type"]) + raise ArgumentError, "The docker driver does not support caching" if docker_driver? end end diff --git a/lib/kamal/configuration/docs/builder.yml b/lib/kamal/configuration/docs/builder.yml index 5dc200bc..9695a468 100644 --- a/lib/kamal/configuration/docs/builder.yml +++ b/lib/kamal/configuration/docs/builder.yml @@ -18,6 +18,11 @@ builder: # Enables multiarch builds, defaults to `true` multiarch: false + # Driver + # + # The build driver to use, defaults to `docker-container` + driver: docker + # Local configuration # # The build configuration for local builds, only used if multiarch is enabled (the default) diff --git a/test/integration/docker/deployer/app/config/deploy.yml b/test/integration/docker/deployer/app/config/deploy.yml index 397a49ca..72d09d91 100644 --- a/test/integration/docker/deployer/app/config/deploy.yml +++ b/test/integration/docker/deployer/app/config/deploy.yml @@ -24,6 +24,7 @@ registry: password: root builder: multiarch: false + driver: docker args: COMMIT_SHA: <%= `git rev-parse HEAD` %> healthcheck: diff --git a/test/integration/docker/deployer/app_with_roles/config/deploy.yml b/test/integration/docker/deployer/app_with_roles/config/deploy.yml index 7133c023..b3ccefa7 100644 --- a/test/integration/docker/deployer/app_with_roles/config/deploy.yml +++ b/test/integration/docker/deployer/app_with_roles/config/deploy.yml @@ -18,6 +18,7 @@ registry: password: root builder: multiarch: false + driver: docker args: COMMIT_SHA: <%= `git rev-parse HEAD` %> healthcheck: From 56268d724d0ab96bf770894344fb8683e4cea55a Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 1 Aug 2024 14:06:41 +0100 Subject: [PATCH 19/27] Simplify the builders configuration 1. Add driver as an option, defaulting to `docker-container`. For a "native" build you can set it to `docker` 2. Set arch as a array of architectures to build for, defaulting to `[ "amd64", "arm64" ]` unless you are using the docker driver in which case we default to not setting a platform 3. Remote is now just a connection string for the remote builder 4. If remote is set, we only use it for non-local arches, if we are only building for the local arch, we'll ignore it. Examples: On arm64, build for arm64 locally, amd64 remotely or On amd64, build for amd64 locally, arm64 remotely: ```yaml builder: remote: ssh://docker@docker-builder ``` On arm64, build amd64 on remote, On amd64 build locally: ```yaml builder: arch: - amd64 remote: host: ssh://docker@docker-builder ``` Build amd64 on local: ```yaml builder: arch: - amd64 ``` Use docker driver, building for local arch: ```yaml builder: driver: docker ``` --- lib/kamal/cli/build.rb | 2 +- lib/kamal/commands/builder.rb | 2 +- lib/kamal/commands/builder/base.rb | 12 ++- lib/kamal/commands/builder/hybrid.rb | 10 +- lib/kamal/commands/builder/local.rb | 10 -- lib/kamal/commands/builder/remote.rb | 23 +---- lib/kamal/configuration/builder.rb | 69 ++++++------- lib/kamal/configuration/docs/builder.yml | 32 ++----- lib/kamal/configuration/validator.rb | 16 +++- test/cli/build_test.rb | 22 ++--- test/cli/cli_test_case.rb | 2 +- test/commands/builder_test.rb | 96 ++++++++----------- test/configuration/builder_test.rb | 30 ++---- test/configuration/validation_test.rb | 6 +- test/fixtures/deploy_with_remote_builder.yml | 5 +- ...y_with_remote_builder_and_custom_ports.yml | 5 +- .../docker/deployer/app/config/deploy.yml | 1 - .../deployer/app_with_roles/config/deploy.yml | 1 - test/integration/main_test.rb | 2 +- 19 files changed, 140 insertions(+), 206 deletions(-) diff --git a/lib/kamal/cli/build.rb b/lib/kamal/cli/build.rb index eb431b33..ac73d11d 100644 --- a/lib/kamal/cli/build.rb +++ b/lib/kamal/cli/build.rb @@ -66,7 +66,7 @@ class Kamal::Cli::Build < Kamal::Cli::Base desc "create", "Create a build setup" def create - if (remote_host = KAMAL.config.builder.remote_host) + if (remote_host = KAMAL.config.builder.remote) connect_to_remote_host(remote_host) end diff --git a/lib/kamal/commands/builder.rb b/lib/kamal/commands/builder.rb index 1f105d53..e69e3c2f 100644 --- a/lib/kamal/commands/builder.rb +++ b/lib/kamal/commands/builder.rb @@ -2,7 +2,7 @@ require "active_support/core_ext/string/filters" class Kamal::Commands::Builder < Kamal::Commands::Base delegate :create, :remove, :push, :clean, :pull, :info, :buildx_inspect, :validate_image, :first_mirror, to: :target - delegate :multiarch?, :local?, :remote?, to: "config.builder" + delegate :local?, :remote?, to: "config.builder" include Clone diff --git a/lib/kamal/commands/builder/base.rb b/lib/kamal/commands/builder/base.rb index 8f68b334..5c03881f 100644 --- a/lib/kamal/commands/builder/base.rb +++ b/lib/kamal/commands/builder/base.rb @@ -5,8 +5,8 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base delegate :argumentize, to: Kamal::Utils delegate \ - :args, :secrets, :dockerfile, :target, :local_arch, :remote_arch, :remote_host, - :cache_from, :cache_to, :multiarch?, :ssh, :driver, :docker_driver?, + :args, :secrets, :dockerfile, :target, :arches, :local_arches, :remote_arches, :remote, + :cache_from, :cache_to, :ssh, :driver, :docker_driver?, to: :builder_config def clean @@ -16,7 +16,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base def push docker :build, "--push", - *platform_options, + *platform_options(arches), *([ "--builder", builder_name ] unless docker_driver?), *build_options, build_context @@ -33,7 +33,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base end def buildx_inspect - docker :buildx, :inspect, builder_name + docker :buildx, :inspect, builder_name unless docker_driver? end def build_options @@ -104,4 +104,8 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base def context_host(builder_name) docker :context, :inspect, builder_name, "--format", ENDPOINT_DOCKER_HOST_INSPECT end + + def platform_options(arches) + argumentize "--platform", arches.map { |arch| "linux/#{arch}" }.join(",") if arches.any? + end end diff --git a/lib/kamal/commands/builder/hybrid.rb b/lib/kamal/commands/builder/hybrid.rb index f8d873dd..5434169a 100644 --- a/lib/kamal/commands/builder/hybrid.rb +++ b/lib/kamal/commands/builder/hybrid.rb @@ -8,18 +8,14 @@ class Kamal::Commands::Builder::Hybrid < Kamal::Commands::Builder::Remote private def builder_name - "kamal-hybrid-#{driver}-#{local_arch}-#{remote_arch}-#{remote_host.gsub(/[^a-z0-9_-]/, "-")}" + "kamal-hybrid-#{driver}-#{remote.gsub(/[^a-z0-9_-]/, "-")}" end def create_local_buildx - docker :buildx, :create, "--name", builder_name, "--platform", "linux/#{local_arch}", "--driver=#{driver}" + docker :buildx, :create, *platform_options(local_arches), "--name", builder_name, "--driver=#{driver}" end def append_remote_buildx - docker :buildx, :create, "--append", "--name", builder_name, builder_name, "--platform", "linux/#{remote_arch}" - end - - def platform - "linux/#{local_arch},linux/#{remote_arch}" + docker :buildx, :create, *platform_options(remote_arches), "--append", "--name", builder_name, builder_name end end diff --git a/lib/kamal/commands/builder/local.rb b/lib/kamal/commands/builder/local.rb index 64b933ae..c4215e33 100644 --- a/lib/kamal/commands/builder/local.rb +++ b/lib/kamal/commands/builder/local.rb @@ -11,14 +11,4 @@ class Kamal::Commands::Builder::Local < Kamal::Commands::Builder::Base def builder_name "kamal-local-#{driver}" end - - def platform_options - if multiarch? - if local_arch - [ "--platform", "linux/#{local_arch}" ] - else - [ "--platform", "linux/amd64,linux/arm64" ] - end - end - end end diff --git a/lib/kamal/commands/builder/remote.rb b/lib/kamal/commands/builder/remote.rb index 3196a6e9..b4644047 100644 --- a/lib/kamal/commands/builder/remote.rb +++ b/lib/kamal/commands/builder/remote.rb @@ -17,22 +17,13 @@ class Kamal::Commands::Builder::Remote < Kamal::Commands::Builder::Base docker(:buildx, :ls) end - def push - docker :build, - "--push", - *platform_options, - "--builder", builder_name, - *build_options, - build_context - end - private def builder_name - "kamal-remote-#{driver}-#{remote_arch}-#{remote_host.gsub(/[^a-z0-9_-]/, "-")}" + "kamal-remote-#{driver}-#{remote.gsub(/[^a-z0-9_-]/, "-")}" end def create_remote_context - docker :context, :create, builder_name, "--description", "'#{builder_name} host'", "--docker", "'host=#{remote_host}'" + docker :context, :create, builder_name, "--description", "'#{builder_name} host'", "--docker", "'host=#{remote}'" end def remove_remote_context @@ -40,18 +31,10 @@ class Kamal::Commands::Builder::Remote < Kamal::Commands::Builder::Base end def create_buildx - docker :buildx, :create, "--name", builder_name, builder_name, "--platform", platform + docker :buildx, :create, "--name", builder_name, builder_name end def remove_buildx docker :buildx, :rm, builder_name end - - def platform_options - [ "--platform", platform ] - end - - def platform - "linux/#{remote_arch}" - end end diff --git a/lib/kamal/configuration/builder.rb b/lib/kamal/configuration/builder.rb index 6229a518..4f51669a 100644 --- a/lib/kamal/configuration/builder.rb +++ b/lib/kamal/configuration/builder.rb @@ -19,16 +19,38 @@ class Kamal::Configuration::Builder builder_config end - def multiarch? - builder_config["multiarch"] != false + def remote + builder_config["remote"] end - def local? - !!builder_config["local"] + def arches + Array(builder_config.fetch("arch", default_arch)) + end + + def local_arches + @local_arches ||= if remote + uname_m = `uname -m`.strip + local_arch = uname_m == "x86_64" ? "amd64" : uname_m + arches & [ local_arch ] + else + arches + end + end + + def remote_arches + @remote_arches ||= if remote + arches - local_arches + else + [] + end end def remote? - !!builder_config["remote"] + remote_arches.any? + end + + def local? + arches.empty? || local_arches.any? end def cached? @@ -59,18 +81,6 @@ class Kamal::Configuration::Builder builder_config.fetch("driver", "docker-container") end - def local_arch - builder_config["local"]["arch"] if local? - end - - def remote_arch - builder_config["remote"]["arch"] if remote? - end - - def remote_host - builder_config["remote"]["host"] if remote? - end - def cache_from if cached? case builder_config["cache"]["type"] @@ -120,27 +130,14 @@ class Kamal::Configuration::Builder private def valid? - if multiarch? - if local? - raise ArgumentError, "Invalid builder configuration: local configuration, arch required" unless local_arch - end - - if remote? - raise ArgumentError, "Invalid builder configuration: remote configuration, arch required" unless remote_arch - raise ArgumentError, "Invalid builder configuration: remote configuration, arch required" unless remote_host - end - - if docker_driver? - raise ArgumentError, "Invalid builder configuration: the docker driver does not support multiarch builds" - end - else - raise ArgumentError, "Invalid builder configuration: multiarch must be enabled for local configuration" if local? - raise ArgumentError, "Invalid builder configuration: multiarch must be enabled for remote configuration" if remote? + if docker_driver? + raise ArgumentError, "Invalid builder configuration: the `docker` driver does not not support remote builders" if remote + raise ArgumentError, "Invalid builder configuration: the `docker` driver does not not support caching" if cached? + raise ArgumentError, "Invalid builder configuration: the `docker` driver does not not support multiple arches" if arches.many? end if @options["cache"] && @options["cache"]["type"] raise ArgumentError, "Invalid cache type: #{@options["cache"]["type"]}" unless [ "gha", "registry" ].include?(@options["cache"]["type"]) - raise ArgumentError, "The docker driver does not support caching" if docker_driver? end end @@ -179,4 +176,8 @@ class Kamal::Configuration::Builder def pwd_sha Digest::SHA256.hexdigest(Dir.pwd)[0..12] end + + def default_arch + docker_driver? ? [] : [ "amd64", "arm64" ] + end end diff --git a/lib/kamal/configuration/docs/builder.yml b/lib/kamal/configuration/docs/builder.yml index 9695a468..5455f1a2 100644 --- a/lib/kamal/configuration/docs/builder.yml +++ b/lib/kamal/configuration/docs/builder.yml @@ -3,7 +3,7 @@ # The builder configuration controls how the application is built with `docker build` # # If no configuration is specified, Kamal will: -# 1. Create a buildx context called `kamal--multiarch` +# 1. Create a buildx context called `kamal-local-docker-container`, using the docker-container driver # 2. Use `docker build` to build a multiarch image for linux/amd64,linux/arm64 with that context # # See https://kamal-deploy.org/docs/configuration/builder-examples/ for more information @@ -12,41 +12,29 @@ # # Options go under the builder key in the root configuration. builder: - - # Multiarch - # - # Enables multiarch builds, defaults to `true` - multiarch: false - # Driver # # The build driver to use, defaults to `docker-container` driver: docker - # Local configuration + # Arch # - # The build configuration for local builds, only used if multiarch is enabled (the default) - # - # If there is no remote configuration, by default we build for amd64 and arm64. - # If you only want to build for one architecture, you can specify it here. - # The docker socket is optional and uses the default docker host socket when not specified - local: - arch: amd64 - host: /var/run/docker.sock + # The architectures to build for, defaults to `[ amd64, arm64 ]` + # Unless you are using the docker driver, when it defaults to the local architecture + # You can set an array or just a single value + arch: + - amd64 # Remote configuration # - # The build configuration for remote builds, also only used if multiarch is enabled. - # The arch is required and can be either amd64 or arm64. - remote: - arch: arm64 - host: ssh://docker@docker-builder + # If you have a remote builder, you can configure it here + remote: ssh://docker@docker-builder # Builder cache # # The type must be either 'gha' or 'registry' # - # The image is only used for registry cache + # The image is only used for registry cache. Not compatible with the docker driver cache: type: registry options: mode=max diff --git a/lib/kamal/configuration/validator.rb b/lib/kamal/configuration/validator.rb index 3374e004..2ac8482d 100644 --- a/lib/kamal/configuration/validator.rb +++ b/lib/kamal/configuration/validator.rb @@ -28,7 +28,11 @@ class Kamal::Configuration::Validator elsif key == "hosts" validate_servers! value elsif example_value.is_a?(Array) - validate_array_of! value, example_value.first.class + if key == "arch" + validate_array_of_or_type! value, example_value.first.class + else + validate_array_of! value, example_value.first.class + end elsif example_value.is_a?(Hash) case key.to_s when "options", "args" @@ -71,6 +75,16 @@ class Kamal::Configuration::Validator value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(Numeric) || value.is_a?(TrueClass) || value.is_a?(FalseClass) end + def validate_array_of_or_type!(value, type) + if value.is_a?(Array) + validate_array_of! value, type + else + validate_type! value, type + end + rescue Kamal::ConfigurationError + type_error(Array, type) + end + def validate_array_of!(array, type) validate_type! array, Array diff --git a/test/cli/build_test.rb b/test/cli/build_test.rb index 457516f4..c8f01540 100644 --- a/test/cli/build_test.rb +++ b/test/cli/build_test.rb @@ -26,7 +26,7 @@ class CliBuildTest < CliTestCase assert_match /Cloning repo into build directory/, output assert_match /git -C #{Dir.tmpdir}\/kamal-clones\/app-#{pwd_sha} clone #{Dir.pwd}/, output assert_match /docker --version && docker buildx version/, output - assert_match /docker build --push --platform linux\/amd64,linux\/arm64 --builder kamal-local -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile \. as .*@localhost/, output + assert_match /docker build --push --platform linux\/amd64,linux\/arm64 --builder kamal-local-docker-container -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile \. as .*@localhost/, output end end end @@ -49,7 +49,7 @@ class CliBuildTest < CliTestCase SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :submodule, :update, "--init") SSHKit::Backend::Abstract.any_instance.expects(:execute) - .with(:docker, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "kamal-local", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".") + .with(:docker, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".") SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:git, "-C", anything, :"rev-parse", :HEAD) @@ -74,7 +74,7 @@ class CliBuildTest < CliTestCase assert_no_match /Cloning repo into build directory/, output assert_hook_ran "pre-build", output, **hook_variables assert_match /docker --version && docker buildx version/, output - assert_match /docker build --push --platform linux\/amd64,linux\/arm64 --builder kamal-local -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile . as .*@localhost/, output + assert_match /docker build --push --platform linux\/amd64,linux\/arm64 --builder kamal-local-docker-container -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile . as .*@localhost/, output end end @@ -120,10 +120,10 @@ class CliBuildTest < CliTestCase .with(:docker, "--version", "&&", :docker, :buildx, "version") SSHKit::Backend::Abstract.any_instance.expects(:execute) - .with(:docker, :buildx, :create, "--name", "kamal-local", "--driver=docker-container") + .with(:docker, :buildx, :create, "--name", "kamal-local-docker-container", "--driver=docker-container") SSHKit::Backend::Abstract.any_instance.expects(:execute) - .with(:docker, :buildx, :inspect, "kamal-local") + .with(:docker, :buildx, :inspect, "kamal-local-docker-container") .raises(SSHKit::Command::Failed.new("no builder")) SSHKit::Backend::Abstract.any_instance.expects(:execute).with { |*args| args.first.start_with?("git") } @@ -137,7 +137,7 @@ class CliBuildTest < CliTestCase .returns("") SSHKit::Backend::Abstract.any_instance.expects(:execute) - .with(:docker, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "kamal-local", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".") + .with(:docker, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".") run_command("push").tap do |output| assert_match /WARN Missing compatible builder, so creating a new one first/, output @@ -203,23 +203,23 @@ class CliBuildTest < CliTestCase test "create" do run_command("create").tap do |output| - assert_match /docker buildx create --name kamal-local --driver=docker-container/, output + assert_match /docker buildx create --name kamal-local-docker-container --driver=docker-container/, output end end test "create remote" do run_command("create", fixture: :with_remote_builder).tap do |output| assert_match "Running /usr/bin/env true on 1.1.1.5", output - assert_match "docker context create kamal-remote-amd64-ssh---app-1-1-1-5 --description 'kamal-remote-amd64-ssh---app-1-1-1-5 host' --docker 'host=ssh://app@1.1.1.5'", output - assert_match "docker buildx create --name kamal-remote-amd64-ssh---app-1-1-1-5 kamal-remote-amd64-ssh---app-1-1-1-5 --platform linux/amd64", output + assert_match "docker context create kamal-remote-docker-container-ssh---app-1-1-1-5 --description 'kamal-remote-docker-container-ssh---app-1-1-1-5 host' --docker 'host=ssh://app@1.1.1.5'", output + assert_match "docker buildx create --name kamal-remote-docker-container-ssh---app-1-1-1-5 kamal-remote-docker-container-ssh---app-1-1-1-5", output end end test "create remote with custom ports" do run_command("create", fixture: :with_remote_builder_and_custom_ports).tap do |output| assert_match "Running /usr/bin/env true on 1.1.1.5", output - assert_match "docker context create kamal-remote-amd64-ssh---app-1-1-1-5-2122 --description 'kamal-remote-amd64-ssh---app-1-1-1-5-2122 host' --docker 'host=ssh://app@1.1.1.5:2122'", output - assert_match "docker buildx create --name kamal-remote-amd64-ssh---app-1-1-1-5-2122 kamal-remote-amd64-ssh---app-1-1-1-5-2122 --platform linux/amd64", output + assert_match "docker context create kamal-remote-docker-container-ssh---app-1-1-1-5-2122 --description 'kamal-remote-docker-container-ssh---app-1-1-1-5-2122 host' --docker 'host=ssh://app@1.1.1.5:2122'", output + assert_match "docker buildx create --name kamal-remote-docker-container-ssh---app-1-1-1-5-2122 kamal-remote-docker-container-ssh---app-1-1-1-5-2122", output end end diff --git a/test/cli/cli_test_case.rb b/test/cli/cli_test_case.rb index 5777bf57..c522a32f 100644 --- a/test/cli/cli_test_case.rb +++ b/test/cli/cli_test_case.rb @@ -37,7 +37,7 @@ class CliTestCase < ActiveSupport::TestCase SSHKit::Backend::Abstract.any_instance.stubs(:execute) .with { |arg1, arg2| arg1 == :rm && arg2 == ".kamal/locks/app/details" } SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with(:docker, :buildx, :inspect, "kamal-local") + .with(:docker, :buildx, :inspect, "kamal-local-docker-container") end def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: false) diff --git a/test/commands/builder_test.rb b/test/commands/builder_test.rb index 620aa73d..960c17fe 100644 --- a/test/commands/builder_test.rb +++ b/test/commands/builder_test.rb @@ -5,51 +5,51 @@ class CommandsBuilderTest < ActiveSupport::TestCase @config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ] } end - test "target multiarch by default" do + test "target linux/amd64,linux/arm64 locally by default" do builder = new_builder_command(builder: { "cache" => { "type" => "gha" } }) assert_equal "local", builder.name assert_equal \ - "docker build --push --platform linux/amd64,linux/arm64 --builder kamal-local -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .", + "docker build --push --platform linux/amd64,linux/arm64 --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 .", builder.push.join(" ") end - test "target native when multiarch is off" do - builder = new_builder_command(builder: { "multiarch" => false }) + test "target specified arch locally by default" do + builder = new_builder_command(builder: { "arch" => [ "amd64" ] }) assert_equal "local", builder.name assert_equal \ - "docker build --push --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile .", + "docker build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile .", builder.push.join(" ") end - test "target native cached when multiarch is off and cache is set" do - builder = new_builder_command(builder: { "multiarch" => false, "cache" => { "type" => "gha" } }) + test "build with caching" do + builder = new_builder_command(builder: { "cache" => { "type" => "gha" } }) assert_equal "local", builder.name assert_equal \ - "docker build --push --builder kamal-local -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .", + "docker build --push --platform linux/amd64,linux/arm64 --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 .", builder.push.join(" ") end - test "target multiarch remote when local and remote is set" do - builder = new_builder_command(builder: { "local" => { "arch" => "arm64" }, "remote" => { "arch" => "amd64", "host" => "ssh://app@127.0.0.1" }, "cache" => { "type" => "gha" } }) + test "hybrid build if remote is set" do + builder = new_builder_command(builder: { "remote" => "ssh://app@127.0.0.1", "cache" => { "type" => "gha" } }) assert_equal "hybrid", builder.name assert_equal \ - "docker build --push --platform linux/arm64,linux/amd64 --builder kamal-hybrid-arm64-amd64-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 .", + "docker build --push --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 .", builder.push.join(" ") end - test "target multiarch local when arch is set" do - builder = new_builder_command(builder: { "local" => { "arch" => "amd64" } }) - assert_equal "local", builder.name - assert_equal \ - "docker build --push --platform linux/amd64 --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile .", - builder.push.join(" ") - end - - test "target native remote when only remote is set" do - builder = new_builder_command(builder: { "remote" => { "arch" => "amd64", "host" => "ssh://app@host" }, "cache" => { "type" => "gha" } }) + 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 build --push --platform linux/amd64 --builder kamal-remote-amd64-ssh---app-host -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .", + "docker build --push --platform linux/#{remote_arch} --builder kamal-remote-docker-container-ssh---app-host -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 "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 build --push --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 .", builder.push.join(" ") end @@ -93,28 +93,21 @@ class CommandsBuilderTest < ActiveSupport::TestCase test "build context" do builder = new_builder_command(builder: { "context" => ".." }) assert_equal \ - "docker build --push --platform linux/amd64,linux/arm64 --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ..", + "docker build --push --platform linux/amd64,linux/arm64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ..", builder.push.join(" ") end - test "native push with build args" do - builder = new_builder_command(builder: { "multiarch" => false, "args" => { "a" => 1, "b" => 2 } }) - assert_equal \ - "docker build --push --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile .", - builder.push.join(" ") - end - - test "multiarch push with build args" do + test "push with build args" do builder = new_builder_command(builder: { "args" => { "a" => 1, "b" => 2 } }) assert_equal \ - "docker build --push --platform linux/amd64,linux/arm64 --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile .", + "docker build --push --platform linux/amd64,linux/arm64 --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 .", builder.push.join(" ") end - test "native push with build secrets" do - builder = new_builder_command(builder: { "multiarch" => false, "secrets" => [ "a", "b" ] }) + test "push with build secrets" do + builder = new_builder_command(builder: { "secrets" => [ "a", "b" ] }) assert_equal \ - "docker build --push --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"a\" --secret id=\"b\" --file Dockerfile .", + "docker build --push --platform linux/amd64,linux/arm64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"a\" --secret id=\"b\" --file Dockerfile .", builder.push.join(" ") end @@ -130,31 +123,10 @@ 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 context build" do + test "context build" do builder = new_builder_command(builder: { "context" => "./foo" }) assert_equal \ - "docker build --push --platform linux/amd64,linux/arm64 --builder kamal-local -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 --push --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo", - builder.push.join(" ") - end - - test "cached context build" do - builder = new_builder_command(builder: { "multiarch" => false, "context" => "./foo", "cache" => { "type" => "gha" } }) - assert_equal \ - "docker build --push --builder kamal-local -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", "host" => "ssh://app@host" }, "context" => "./foo" }) - assert_equal \ - "docker build --push --platform linux/amd64 --builder kamal-remote-amd64-ssh---app-host -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo", + "docker build --push --platform linux/amd64,linux/arm64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo", builder.push.join(" ") end @@ -171,4 +143,12 @@ class CommandsBuilderTest < ActiveSupport::TestCase def build_directory "#{Dir.tmpdir}/kamal-clones/app/kamal/" end + + def local_arch + `uname -m`.strip == "x86_64" ? "amd64" : "arm64" + end + + def remote_arch + `uname -m`.strip == "x86_64" ? "arm64" : "amd64" + end end diff --git a/test/configuration/builder_test.rb b/test/configuration/builder_test.rb index 94acdacc..efcd613b 100644 --- a/test/configuration/builder_test.rb +++ b/test/configuration/builder_test.rb @@ -14,45 +14,29 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase } end - test "multiarch?" do - assert_equal true, config.builder.multiarch? - end - - test "setting multiarch to false" do - @deploy_with_builder_option[:builder] = { "multiarch" => false } - - assert_equal false, config_with_builder_option.builder.multiarch? - end - test "local?" do - assert_equal false, config.builder.local? + assert_equal true, config.builder.local? end test "remote?" do assert_equal false, config.builder.remote? end - test "remote_arch" do - assert_nil config.builder.remote_arch - end - - test "remote_host" do - assert_nil config.builder.remote_host + test "remote" do + assert_nil config.builder.remote end test "setting both local and remote configs" do @deploy_with_builder_option[:builder] = { - "local" => { "arch" => "arm64" }, - "remote" => { "arch" => "amd64", "host" => "ssh://root@192.168.0.1" } + "arch" => [ "amd64", "arm64" ], + "remote" => "ssh://root@192.168.0.1" } assert_equal true, config_with_builder_option.builder.local? assert_equal true, config_with_builder_option.builder.remote? - assert_equal "amd64", config_with_builder_option.builder.remote_arch - assert_equal "ssh://root@192.168.0.1", config_with_builder_option.builder.remote_host - - assert_equal "arm64", config_with_builder_option.builder.local_arch + assert_equal [ "amd64", "arm64" ], config_with_builder_option.builder.arches + assert_equal "ssh://root@192.168.0.1", config_with_builder_option.builder.remote end test "cached?" do diff --git a/test/configuration/validation_test.rb b/test/configuration/validation_test.rb index b7bd6b6a..b39dbc36 100644 --- a/test/configuration/validation_test.rb +++ b/test/configuration/validation_test.rb @@ -90,10 +90,8 @@ class ConfigurationValidationTest < ActiveSupport::TestCase test "builder" do assert_error "builder: unknown key: foo", builder: { "foo" => "bar" } - assert_error "builder/remote: should be a hash", builder: { "remote" => true } - assert_error "builder/remote: unknown key: foo", builder: { "remote" => { "foo" => "bar" } } - assert_error "builder/local: unknown key: foo", builder: { "local" => { "foo" => "bar" } } - assert_error "builder/remote/arch: should be a string", builder: { "remote" => { "arch" => [] } } + assert_error "builder/remote: should be a string", builder: { "remote" => { "foo" => "bar" } } + assert_error "builder/arch: should be an array or a string", builder: { "arch" => {} } assert_error "builder/args: should be a hash", builder: { "args" => [ "foo" ] } assert_error "builder/cache/options: should be a string", builder: { "cache" => { "options" => [] } } end diff --git a/test/fixtures/deploy_with_remote_builder.yml b/test/fixtures/deploy_with_remote_builder.yml index 44103499..a67963c2 100644 --- a/test/fixtures/deploy_with_remote_builder.yml +++ b/test/fixtures/deploy_with_remote_builder.yml @@ -36,6 +36,5 @@ accessories: readiness_delay: 0 builder: - remote: - arch: amd64 - host: ssh://app@1.1.1.5 + arch: <%= `uname -m`.strip == "x86_64" ? "arm64" : "amd64" %> + remote: ssh://app@1.1.1.5 diff --git a/test/fixtures/deploy_with_remote_builder_and_custom_ports.yml b/test/fixtures/deploy_with_remote_builder_and_custom_ports.yml index d1e81836..d76100ff 100644 --- a/test/fixtures/deploy_with_remote_builder_and_custom_ports.yml +++ b/test/fixtures/deploy_with_remote_builder_and_custom_ports.yml @@ -40,6 +40,5 @@ ssh: port: 22 builder: - remote: - arch: amd64 - host: ssh://app@1.1.1.5:2122 + arch: <%= `uname -m`.strip == "x86_64" ? "arm64" : "amd64" %> + remote: ssh://app@1.1.1.5:2122 diff --git a/test/integration/docker/deployer/app/config/deploy.yml b/test/integration/docker/deployer/app/config/deploy.yml index 72d09d91..99bfc297 100644 --- a/test/integration/docker/deployer/app/config/deploy.yml +++ b/test/integration/docker/deployer/app/config/deploy.yml @@ -23,7 +23,6 @@ registry: username: root password: root builder: - multiarch: false driver: docker args: COMMIT_SHA: <%= `git rev-parse HEAD` %> diff --git a/test/integration/docker/deployer/app_with_roles/config/deploy.yml b/test/integration/docker/deployer/app_with_roles/config/deploy.yml index b3ccefa7..d7e757cc 100644 --- a/test/integration/docker/deployer/app_with_roles/config/deploy.yml +++ b/test/integration/docker/deployer/app_with_roles/config/deploy.yml @@ -17,7 +17,6 @@ registry: username: root password: root builder: - multiarch: false driver: docker args: COMMIT_SHA: <%= `git rev-parse HEAD` %> diff --git a/test/integration/main_test.rb b/test/integration/main_test.rb index 742e111c..d0cf024e 100644 --- a/test/integration/main_test.rb +++ b/test/integration/main_test.rb @@ -77,7 +77,7 @@ class MainTest < IntegrationTest assert_equal "app-#{version}", config[:service_with_version] assert_equal [], config[:volume_args] assert_equal({ user: "root", port: 22, keepalive: true, keepalive_interval: 30, log_level: :fatal }, config[:ssh_options]) - assert_equal({ "multiarch" => false, "args" => { "COMMIT_SHA" => version } }, config[:builder]) + assert_equal({ "driver" => "docker", "args" => { "COMMIT_SHA" => version } }, config[:builder]) assert_equal [ "--log-opt", "max-size=\"10m\"" ], config[:logging] assert_equal({ "cmd"=>"wget -qO- http://localhost > /dev/null || exit 1", "interval"=>"1s", "max_attempts"=>3, "port"=>3000, "path"=>"/up", "cord"=>"/tmp/kamal-cord", "log_lines"=>50 }, config[:healthcheck]) end From d2d0223c370f4face9030b311762e920c1182b28 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 28 Aug 2024 11:35:39 +0100 Subject: [PATCH 20/27] Require an arch to be set, and default to amd64 in the template --- lib/kamal/cli/templates/deploy.yml | 14 ++-- lib/kamal/configuration/validator/builder.rb | 2 + test/cli/build_test.rb | 8 +-- test/commands/accessory_test.rb | 1 + test/commands/app_test.rb | 2 +- test/commands/auditor_test.rb | 2 +- test/commands/builder_test.rb | 22 +++--- test/commands/docker_test.rb | 2 +- test/commands/hook_test.rb | 2 +- test/commands/lock_test.rb | 2 +- test/commands/prune_test.rb | 2 +- test/commands/registry_test.rb | 1 + test/commands/server_test.rb | 2 +- test/commands/traefik_test.rb | 2 +- test/configuration/accessory_test.rb | 1 + test/configuration/builder_test.rb | 72 ++++++++----------- test/configuration/env/tags_test.rb | 4 ++ test/configuration/role_test.rb | 1 + test/configuration/ssh_test.rb | 1 + test/configuration/sshkit_test.rb | 1 + test/configuration/validation_test.rb | 1 + test/configuration_test.rb | 5 +- test/fixtures/deploy.erb.yml | 2 + test/fixtures/deploy_for_dest.yml | 2 + test/fixtures/deploy_for_required_dest.yml | 2 + .../deploy_primary_web_role_override.yml | 2 + test/fixtures/deploy_simple.yml | 2 + test/fixtures/deploy_with_accessories.yml | 2 + test/fixtures/deploy_with_assets.yml | 2 + test/fixtures/deploy_with_boot_strategy.yml | 2 + test/fixtures/deploy_with_env_tags.yml | 2 + test/fixtures/deploy_with_extensions.yml | 2 + ...ploy_with_low_percentage_boot_strategy.yml | 2 + .../deploy_with_multiple_traefik_roles.yml | 2 + .../deploy_with_percentage_boot_strategy.yml | 2 + test/fixtures/deploy_with_remote_builder.yml | 2 + ...y_with_remote_builder_and_custom_ports.yml | 2 + test/fixtures/deploy_with_roles.yml | 2 + test/fixtures/deploy_with_secrets.yml | 2 + .../deploy_with_two_roles_one_host.yml | 2 + .../deploy_with_uncommon_hostnames.yml | 2 + test/fixtures/deploy_without_clone.yml | 1 + test/fixtures/deploy_workers_only.yml | 2 + .../docker/deployer/app/config/deploy.yml | 1 + .../deployer/app_with_roles/config/deploy.yml | 1 + test/integration/main_test.rb | 2 +- 46 files changed, 118 insertions(+), 77 deletions(-) diff --git a/lib/kamal/cli/templates/deploy.yml b/lib/kamal/cli/templates/deploy.yml index 2602c66d..119961b9 100644 --- a/lib/kamal/cli/templates/deploy.yml +++ b/lib/kamal/cli/templates/deploy.yml @@ -18,6 +18,10 @@ registry: password: - KAMAL_REGISTRY_PASSWORD +# Configure builder setup. +builder: + arch: amd64 + # Inject ENV variables into containers (secrets come from .env). # Remember to run `kamal env push` after making changes! # env: @@ -30,16 +34,6 @@ registry: # ssh: # user: app -# Configure builder setup. -# builder: -# args: -# RUBY_VERSION: 3.2.0 -# secrets: -# - GITHUB_TOKEN -# remote: -# arch: amd64 -# host: ssh://app@192.168.0.1 - # Use accessory services (secrets come from .env). # accessories: # db: diff --git a/lib/kamal/configuration/validator/builder.rb b/lib/kamal/configuration/validator/builder.rb index ebccdf81..4bd815bf 100644 --- a/lib/kamal/configuration/validator/builder.rb +++ b/lib/kamal/configuration/validator/builder.rb @@ -5,5 +5,7 @@ class Kamal::Configuration::Validator::Builder < Kamal::Configuration::Validator if config["cache"] && config["cache"]["type"] error "Invalid cache type: #{config["cache"]["type"]}" unless [ "gha", "registry" ].include?(config["cache"]["type"]) end + + error "Builder arch not set" unless config["arch"].present? end end diff --git a/test/cli/build_test.rb b/test/cli/build_test.rb index c8f01540..11b6fb1e 100644 --- a/test/cli/build_test.rb +++ b/test/cli/build_test.rb @@ -26,7 +26,7 @@ class CliBuildTest < CliTestCase assert_match /Cloning repo into build directory/, output assert_match /git -C #{Dir.tmpdir}\/kamal-clones\/app-#{pwd_sha} clone #{Dir.pwd}/, output assert_match /docker --version && docker buildx version/, output - assert_match /docker build --push --platform linux\/amd64,linux\/arm64 --builder kamal-local-docker-container -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile \. as .*@localhost/, output + assert_match /docker build --push --platform linux\/amd64 --builder kamal-local-docker-container -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile \. as .*@localhost/, output end end end @@ -49,7 +49,7 @@ class CliBuildTest < CliTestCase SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :submodule, :update, "--init") SSHKit::Backend::Abstract.any_instance.expects(:execute) - .with(:docker, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".") + .with(:docker, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".") SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:git, "-C", anything, :"rev-parse", :HEAD) @@ -74,7 +74,7 @@ class CliBuildTest < CliTestCase assert_no_match /Cloning repo into build directory/, output assert_hook_ran "pre-build", output, **hook_variables assert_match /docker --version && docker buildx version/, output - assert_match /docker build --push --platform linux\/amd64,linux\/arm64 --builder kamal-local-docker-container -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile . as .*@localhost/, output + assert_match /docker build --push --platform linux\/amd64 --builder kamal-local-docker-container -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile . as .*@localhost/, output end end @@ -137,7 +137,7 @@ class CliBuildTest < CliTestCase .returns("") SSHKit::Backend::Abstract.any_instance.expects(:execute) - .with(:docker, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".") + .with(:docker, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".") run_command("push").tap do |output| assert_match /WARN Missing compatible builder, so creating a new one first/, output diff --git a/test/commands/accessory_test.rb b/test/commands/accessory_test.rb index 94c1bebc..40002988 100644 --- a/test/commands/accessory_test.rb +++ b/test/commands/accessory_test.rb @@ -5,6 +5,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase @config = { service: "app", image: "dhh/app", registry: { "server" => "private.registry", "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], + builder: { "arch" => "amd64" }, accessories: { "mysql" => { "image" => "private.registry/mysql:8.0", diff --git a/test/commands/app_test.rb b/test/commands/app_test.rb index 502501cd..bee7dc34 100644 --- a/test/commands/app_test.rb +++ b/test/commands/app_test.rb @@ -5,7 +5,7 @@ class CommandsAppTest < ActiveSupport::TestCase ENV["RAILS_MASTER_KEY"] = "456" Kamal::Configuration.any_instance.stubs(:run_id).returns("12345678901234567890123456789012") - @config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], env: { "secret" => [ "RAILS_MASTER_KEY" ] } } + @config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], env: { "secret" => [ "RAILS_MASTER_KEY" ] }, builder: { "arch" => "amd64" } } end teardown do diff --git a/test/commands/auditor_test.rb b/test/commands/auditor_test.rb index 5b7a34c5..2aaafd67 100644 --- a/test/commands/auditor_test.rb +++ b/test/commands/auditor_test.rb @@ -8,7 +8,7 @@ class CommandsAuditorTest < ActiveSupport::TestCase freeze_time @config = { - service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ] + service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, builder: { "arch" => "amd64" }, servers: [ "1.1.1.1" ] } @auditor = new_command diff --git a/test/commands/builder_test.rb b/test/commands/builder_test.rb index 960c17fe..b7094126 100644 --- a/test/commands/builder_test.rb +++ b/test/commands/builder_test.rb @@ -2,14 +2,14 @@ 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" ] } + @config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], builder: { "arch" => "amd64" } } end - test "target linux/amd64,linux/arm64 locally by default" do + test "target linux/amd64 locally by default" do builder = new_builder_command(builder: { "cache" => { "type" => "gha" } }) assert_equal "local", builder.name assert_equal \ - "docker build --push --platform linux/amd64,linux/arm64 --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 .", + "docker build --push --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 .", builder.push.join(" ") end @@ -25,12 +25,12 @@ class CommandsBuilderTest < ActiveSupport::TestCase builder = new_builder_command(builder: { "cache" => { "type" => "gha" } }) assert_equal "local", builder.name assert_equal \ - "docker build --push --platform linux/amd64,linux/arm64 --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 .", + "docker build --push --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 .", builder.push.join(" ") end - test "hybrid build if remote is set" do - builder = new_builder_command(builder: { "remote" => "ssh://app@127.0.0.1", "cache" => { "type" => "gha" } }) + 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 build --push --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 .", @@ -93,21 +93,21 @@ class CommandsBuilderTest < ActiveSupport::TestCase test "build context" do builder = new_builder_command(builder: { "context" => ".." }) assert_equal \ - "docker build --push --platform linux/amd64,linux/arm64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ..", + "docker build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ..", builder.push.join(" ") end test "push with build args" do builder = new_builder_command(builder: { "args" => { "a" => 1, "b" => 2 } }) assert_equal \ - "docker build --push --platform linux/amd64,linux/arm64 --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 .", + "docker build --push --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 .", builder.push.join(" ") end test "push with build secrets" do builder = new_builder_command(builder: { "secrets" => [ "a", "b" ] }) assert_equal \ - "docker build --push --platform linux/amd64,linux/arm64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"a\" --secret id=\"b\" --file Dockerfile .", + "docker build --push --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 .", builder.push.join(" ") end @@ -126,7 +126,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase test "context build" do builder = new_builder_command(builder: { "context" => "./foo" }) assert_equal \ - "docker build --push --platform linux/amd64,linux/arm64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo", + "docker build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo", builder.push.join(" ") end @@ -137,7 +137,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase private 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.deep_merge(additional_config), version: "123")) end def build_directory diff --git a/test/commands/docker_test.rb b/test/commands/docker_test.rb index 0c0e976b..db2ed45c 100644 --- a/test/commands/docker_test.rb +++ b/test/commands/docker_test.rb @@ -3,7 +3,7 @@ require "test_helper" class CommandsDockerTest < ActiveSupport::TestCase setup do @config = { - service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ] + service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], builder: { "arch" => "amd64" } } @docker = Kamal::Commands::Docker.new(Kamal::Configuration.new(@config)) end diff --git a/test/commands/hook_test.rb b/test/commands/hook_test.rb index c0e6e98f..60438c66 100644 --- a/test/commands/hook_test.rb +++ b/test/commands/hook_test.rb @@ -8,7 +8,7 @@ class CommandsHookTest < ActiveSupport::TestCase @config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], - traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } } + builder: { "arch" => "amd64" }, traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } } } @performer = Kamal::Git.email.presence || `whoami`.chomp diff --git a/test/commands/lock_test.rb b/test/commands/lock_test.rb index fc2b15ab..02871922 100644 --- a/test/commands/lock_test.rb +++ b/test/commands/lock_test.rb @@ -4,7 +4,7 @@ class CommandsLockTest < ActiveSupport::TestCase setup do @config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], - traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } } + builder: { "arch" => "amd64" }, traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } } } end diff --git a/test/commands/prune_test.rb b/test/commands/prune_test.rb index c4a56a9a..430a13db 100644 --- a/test/commands/prune_test.rb +++ b/test/commands/prune_test.rb @@ -4,7 +4,7 @@ class CommandsPruneTest < ActiveSupport::TestCase setup do @config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], - traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } } + builder: { "arch" => "amd64" }, traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } } } end diff --git a/test/commands/registry_test.rb b/test/commands/registry_test.rb index c25d7585..17376fef 100755 --- a/test/commands/registry_test.rb +++ b/test/commands/registry_test.rb @@ -8,6 +8,7 @@ class CommandsRegistryTest < ActiveSupport::TestCase "password" => "secret", "server" => "hub.docker.com" }, + builder: { "arch" => "amd64" }, servers: [ "1.1.1.1" ] } @registry = Kamal::Commands::Registry.new Kamal::Configuration.new(@config) diff --git a/test/commands/server_test.rb b/test/commands/server_test.rb index 9e063f77..8c465fd9 100644 --- a/test/commands/server_test.rb +++ b/test/commands/server_test.rb @@ -4,7 +4,7 @@ class CommandsServerTest < ActiveSupport::TestCase setup do @config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], - traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } } + builder: { "arch" => "amd64" }, traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } } } end diff --git a/test/commands/traefik_test.rb b/test/commands/traefik_test.rb index a46fc284..446c3077 100644 --- a/test/commands/traefik_test.rb +++ b/test/commands/traefik_test.rb @@ -5,7 +5,7 @@ class CommandsTraefikTest < ActiveSupport::TestCase @image = "traefik:test" @config = { - service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], + service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], builder: { "arch" => "amd64" }, traefik: { "image" => @image, "args" => { "accesslog.format" => "json", "api.insecure" => true, "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } } } diff --git a/test/configuration/accessory_test.rb b/test/configuration/accessory_test.rb index 581cdab3..3f939607 100644 --- a/test/configuration/accessory_test.rb +++ b/test/configuration/accessory_test.rb @@ -8,6 +8,7 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase "web" => [ "1.1.1.1", "1.1.1.2" ], "workers" => [ "1.1.1.3", "1.1.1.4" ] }, + builder: { "arch" => "amd64" }, env: { "REDIS_URL" => "redis://x/y" }, accessories: { "mysql" => { diff --git a/test/configuration/builder_test.rb b/test/configuration/builder_test.rb index efcd613b..3df477c9 100644 --- a/test/configuration/builder_test.rb +++ b/test/configuration/builder_test.rb @@ -4,13 +4,7 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase setup do @deploy = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, - servers: [ "1.1.1.1" ] - } - - @deploy_with_builder_option = { - service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, - servers: [ "1.1.1.1" ], - builder: {} + builder: { "arch" => "amd64" }, servers: [ "1.1.1.1" ] } end @@ -27,16 +21,16 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase end test "setting both local and remote configs" do - @deploy_with_builder_option[:builder] = { + @deploy[:builder] = { "arch" => [ "amd64", "arm64" ], "remote" => "ssh://root@192.168.0.1" } - assert_equal true, config_with_builder_option.builder.local? - assert_equal true, config_with_builder_option.builder.remote? + assert_equal true, config.builder.local? + assert_equal true, config.builder.remote? - assert_equal [ "amd64", "arm64" ], config_with_builder_option.builder.arches - assert_equal "ssh://root@192.168.0.1", config_with_builder_option.builder.remote + assert_equal [ "amd64", "arm64" ], config.builder.arches + assert_equal "ssh://root@192.168.0.1", config.builder.remote end test "cached?" do @@ -44,10 +38,10 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase end test "invalid cache type specified" do - @deploy_with_builder_option[:builder] = { "cache" => { "type" => "invalid" } } + @deploy[:builder]["cache"] = { "type" => "invalid" } assert_raises(Kamal::ConfigurationError) do - config_with_builder_option.builder + config.builder end end @@ -60,32 +54,32 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase end test "setting gha cache" do - @deploy_with_builder_option[:builder] = { "cache" => { "type" => "gha", "options" => "mode=max" } } + @deploy[:builder] = { "arch" => "amd64", "cache" => { "type" => "gha", "options" => "mode=max" } } - assert_equal "type=gha", config_with_builder_option.builder.cache_from - assert_equal "type=gha,mode=max", config_with_builder_option.builder.cache_to + assert_equal "type=gha", config.builder.cache_from + assert_equal "type=gha,mode=max", config.builder.cache_to end test "setting registry cache" do - @deploy_with_builder_option[:builder] = { "cache" => { "type" => "registry", "options" => "mode=max,image-manifest=true,oci-mediatypes=true" } } + @deploy[:builder] = { "arch" => "amd64", "cache" => { "type" => "registry", "options" => "mode=max,image-manifest=true,oci-mediatypes=true" } } - assert_equal "type=registry,ref=dhh/app-build-cache", config_with_builder_option.builder.cache_from - assert_equal "type=registry,mode=max,image-manifest=true,oci-mediatypes=true,ref=dhh/app-build-cache", config_with_builder_option.builder.cache_to + assert_equal "type=registry,ref=dhh/app-build-cache", config.builder.cache_from + assert_equal "type=registry,mode=max,image-manifest=true,oci-mediatypes=true,ref=dhh/app-build-cache", config.builder.cache_to end test "setting registry cache when using a custom registry" do - @deploy_with_builder_option[:registry]["server"] = "registry.example.com" - @deploy_with_builder_option[:builder] = { "cache" => { "type" => "registry", "options" => "mode=max,image-manifest=true,oci-mediatypes=true" } } + @deploy[:registry]["server"] = "registry.example.com" + @deploy[:builder] = { "arch" => "amd64", "cache" => { "type" => "registry", "options" => "mode=max,image-manifest=true,oci-mediatypes=true" } } - assert_equal "type=registry,ref=registry.example.com/dhh/app-build-cache", config_with_builder_option.builder.cache_from - assert_equal "type=registry,mode=max,image-manifest=true,oci-mediatypes=true,ref=registry.example.com/dhh/app-build-cache", config_with_builder_option.builder.cache_to + assert_equal "type=registry,ref=registry.example.com/dhh/app-build-cache", config.builder.cache_from + assert_equal "type=registry,mode=max,image-manifest=true,oci-mediatypes=true,ref=registry.example.com/dhh/app-build-cache", config.builder.cache_to end test "setting registry cache with image" do - @deploy_with_builder_option[:builder] = { "cache" => { "type" => "registry", "image" => "kamal", "options" => "mode=max" } } + @deploy[:builder] = { "arch" => "amd64", "cache" => { "type" => "registry", "image" => "kamal", "options" => "mode=max" } } - assert_equal "type=registry,ref=kamal", config_with_builder_option.builder.cache_from - assert_equal "type=registry,mode=max,ref=kamal", config_with_builder_option.builder.cache_to + assert_equal "type=registry,ref=kamal", config.builder.cache_from + assert_equal "type=registry,mode=max,ref=kamal", config.builder.cache_to end test "args" do @@ -93,9 +87,9 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase end test "setting args" do - @deploy_with_builder_option[:builder] = { "args" => { "key" => "value" } } + @deploy[:builder]["args"] = { "key" => "value" } - assert_equal({ "key" => "value" }, config_with_builder_option.builder.args) + assert_equal({ "key" => "value" }, config.builder.args) end test "secrets" do @@ -103,9 +97,9 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase end test "setting secrets" do - @deploy_with_builder_option[:builder] = { "secrets" => [ "GITHUB_TOKEN" ] } + @deploy[:builder]["secrets"] = [ "GITHUB_TOKEN" ] - assert_equal [ "GITHUB_TOKEN" ], config_with_builder_option.builder.secrets + assert_equal [ "GITHUB_TOKEN" ], config.builder.secrets end test "dockerfile" do @@ -113,9 +107,9 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase end test "setting dockerfile" do - @deploy_with_builder_option[:builder] = { "dockerfile" => "Dockerfile.dev" } + @deploy[:builder]["dockerfile"] = "Dockerfile.dev" - assert_equal "Dockerfile.dev", config_with_builder_option.builder.dockerfile + assert_equal "Dockerfile.dev", config.builder.dockerfile end test "context" do @@ -123,9 +117,9 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase end test "setting context" do - @deploy_with_builder_option[:builder] = { "context" => ".." } + @deploy[:builder]["context"] = ".." - assert_equal "..", config_with_builder_option.builder.context + assert_equal "..", config.builder.context end test "ssh" do @@ -133,17 +127,13 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase end test "setting ssh params" do - @deploy_with_builder_option[:builder] = { "ssh" => "default=$SSH_AUTH_SOCK" } + @deploy[:builder]["ssh"] = "default=$SSH_AUTH_SOCK" - assert_equal "default=$SSH_AUTH_SOCK", config_with_builder_option.builder.ssh + assert_equal "default=$SSH_AUTH_SOCK", config.builder.ssh end private def config Kamal::Configuration.new(@deploy) end - - def config_with_builder_option - Kamal::Configuration.new(@deploy_with_builder_option) - end end diff --git a/test/configuration/env/tags_test.rb b/test/configuration/env/tags_test.rb index c36b6057..0fb649d1 100644 --- a/test/configuration/env/tags_test.rb +++ b/test/configuration/env/tags_test.rb @@ -5,6 +5,7 @@ class ConfigurationEnvTagsTest < ActiveSupport::TestCase @deploy = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ { "1.1.1.1" => "odd" }, { "1.1.1.2" => "even" }, { "1.1.1.3" => [ "odd", "three" ] } ], + builder: { "arch" => "amd64" }, env: { "clear" => { "REDIS_URL" => "redis://x/y", "THREE" => "false" }, "tags" => { @@ -64,6 +65,7 @@ class ConfigurationEnvTagsTest < ActiveSupport::TestCase deploy = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ { "1.1.1.1" => [ "first", "second" ] } ], + builder: { "arch" => "amd64" }, env: { "tags" => { "first" => { "TYPE" => "first" }, @@ -82,6 +84,7 @@ class ConfigurationEnvTagsTest < ActiveSupport::TestCase deploy = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ { "1.1.1.1" => "secrets" } ], + builder: { "arch" => "amd64" }, env: { "tags" => { "secrets" => { "secret" => [ "PASSWORD" ] } @@ -99,6 +102,7 @@ class ConfigurationEnvTagsTest < ActiveSupport::TestCase deploy = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ { "1.1.1.1" => "clearly" } ], + builder: { "arch" => "amd64" }, env: { "tags" => { "clearly" => { "clear" => { "FOO" => "bar" } } diff --git a/test/configuration/role_test.rb b/test/configuration/role_test.rb index da9a3d1c..37c26fcd 100644 --- a/test/configuration/role_test.rb +++ b/test/configuration/role_test.rb @@ -5,6 +5,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase @deploy = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1", "1.1.1.2" ], + builder: { "arch" => "amd64" }, env: { "REDIS_URL" => "redis://x/y" } } diff --git a/test/configuration/ssh_test.rb b/test/configuration/ssh_test.rb index f49c0055..76d1ae9a 100644 --- a/test/configuration/ssh_test.rb +++ b/test/configuration/ssh_test.rb @@ -5,6 +5,7 @@ class ConfigurationSshTest < ActiveSupport::TestCase @deploy = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, + builder: { "arch" => "amd64" }, env: { "REDIS_URL" => "redis://x/y" }, servers: [ "1.1.1.1", "1.1.1.2" ], volumes: [ "/local/path:/container/path" ] diff --git a/test/configuration/sshkit_test.rb b/test/configuration/sshkit_test.rb index dc8c2258..1608e6ee 100644 --- a/test/configuration/sshkit_test.rb +++ b/test/configuration/sshkit_test.rb @@ -6,6 +6,7 @@ class ConfigurationSshkitTest < ActiveSupport::TestCase service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, env: { "REDIS_URL" => "redis://x/y" }, + builder: { "arch" => "amd64" }, servers: [ "1.1.1.1", "1.1.1.2" ], volumes: [ "/local/path:/container/path" ] } diff --git a/test/configuration/validation_test.rb b/test/configuration/validation_test.rb index b39dbc36..ac409e84 100644 --- a/test/configuration/validation_test.rb +++ b/test/configuration/validation_test.rb @@ -101,6 +101,7 @@ class ConfigurationValidationTest < ActiveSupport::TestCase valid_config = { service: "app", image: "app", + builder: { "arch" => "amd64" }, registry: { "username" => "user", "password" => "secret" }, servers: [ "1.1.1.1" ] } diff --git a/test/configuration_test.rb b/test/configuration_test.rb index 043a4c8c..aa78dda9 100644 --- a/test/configuration_test.rb +++ b/test/configuration_test.rb @@ -8,6 +8,7 @@ class ConfigurationTest < ActiveSupport::TestCase @deploy = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, + builder: { "arch" => "amd64" }, env: { "REDIS_URL" => "redis://x/y" }, servers: [ "1.1.1.1", "1.1.1.2" ], volumes: [ "/local/path:/container/path" ] @@ -121,7 +122,7 @@ class ConfigurationTest < ActiveSupport::TestCase test "version from uncommitted context" do ENV.delete("VERSION") - config = Kamal::Configuration.new(@deploy.tap { |c| c[:builder] = { "context" => "." } }) + 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") @@ -267,7 +268,7 @@ class ConfigurationTest < ActiveSupport::TestCase ssh_options: { user: "root", port: 22, log_level: :fatal, keepalive: true, keepalive_interval: 30 }, sshkit: {}, volume_args: [ "--volume", "/local/path:/container/path" ], - builder: {}, + builder: { "arch" => "amd64" }, logging: [ "--log-opt", "max-size=\"10m\"" ], healthcheck: { "cmd"=>"curl -f http://localhost:3000/up || exit 1", "interval" => "1s", "path"=>"/up", "port"=>3000, "max_attempts" => 7, "cord" => "/tmp/kamal-cord", "log_lines" => 50 } } diff --git a/test/fixtures/deploy.erb.yml b/test/fixtures/deploy.erb.yml index a668fb3a..9b9d202d 100644 --- a/test/fixtures/deploy.erb.yml +++ b/test/fixtures/deploy.erb.yml @@ -9,3 +9,5 @@ registry: server: registry.digitalocean.com username: <%= "my-user" %> password: <%= "my-password" %> +builder: + arch: amd64 diff --git a/test/fixtures/deploy_for_dest.yml b/test/fixtures/deploy_for_dest.yml index 96740aec..1784d95f 100644 --- a/test/fixtures/deploy_for_dest.yml +++ b/test/fixtures/deploy_for_dest.yml @@ -4,3 +4,5 @@ registry: server: registry.digitalocean.com username: <%= "my-user" %> password: <%= "my-password" %> +builder: + arch: amd64 diff --git a/test/fixtures/deploy_for_required_dest.yml b/test/fixtures/deploy_for_required_dest.yml index 28438363..1102df95 100644 --- a/test/fixtures/deploy_for_required_dest.yml +++ b/test/fixtures/deploy_for_required_dest.yml @@ -4,4 +4,6 @@ registry: server: registry.digitalocean.com username: <%= "my-user" %> password: <%= "my-password" %> +builder: + arch: amd64 require_destination: true diff --git a/test/fixtures/deploy_primary_web_role_override.yml b/test/fixtures/deploy_primary_web_role_override.yml index 694a9be7..aa52f5ed 100644 --- a/test/fixtures/deploy_primary_web_role_override.yml +++ b/test/fixtures/deploy_primary_web_role_override.yml @@ -17,4 +17,6 @@ registry: server: registry.digitalocean.com username: user password: pw +builder: + arch: amd64 primary_role: web_tokyo diff --git a/test/fixtures/deploy_simple.yml b/test/fixtures/deploy_simple.yml index 520c138e..c143f800 100644 --- a/test/fixtures/deploy_simple.yml +++ b/test/fixtures/deploy_simple.yml @@ -6,3 +6,5 @@ servers: registry: username: user password: pw +builder: + arch: amd64 diff --git a/test/fixtures/deploy_with_accessories.yml b/test/fixtures/deploy_with_accessories.yml index dd78e9f4..29f502ec 100644 --- a/test/fixtures/deploy_with_accessories.yml +++ b/test/fixtures/deploy_with_accessories.yml @@ -10,6 +10,8 @@ servers: registry: username: user password: pw +builder: + arch: amd64 accessories: mysql: diff --git a/test/fixtures/deploy_with_assets.yml b/test/fixtures/deploy_with_assets.yml index 0b6b7cf5..2879d445 100644 --- a/test/fixtures/deploy_with_assets.yml +++ b/test/fixtures/deploy_with_assets.yml @@ -6,4 +6,6 @@ servers: registry: username: user password: pw +builder: + arch: amd64 asset_path: /public/assets diff --git a/test/fixtures/deploy_with_boot_strategy.yml b/test/fixtures/deploy_with_boot_strategy.yml index 7691eb2e..963bb7c3 100644 --- a/test/fixtures/deploy_with_boot_strategy.yml +++ b/test/fixtures/deploy_with_boot_strategy.yml @@ -7,6 +7,8 @@ servers: workers: - "1.1.1.3" - "1.1.1.4" +builder: + arch: amd64 registry: username: user diff --git a/test/fixtures/deploy_with_env_tags.yml b/test/fixtures/deploy_with_env_tags.yml index f0a24760..6822b49d 100644 --- a/test/fixtures/deploy_with_env_tags.yml +++ b/test/fixtures/deploy_with_env_tags.yml @@ -11,6 +11,8 @@ servers: - 1.1.1.4: site1 - 1.2.1.3: site2 - 1.2.1.4: [ site2 experimental ] +builder: + arch: amd64 env: clear: TEST: "root" diff --git a/test/fixtures/deploy_with_extensions.yml b/test/fixtures/deploy_with_extensions.yml index 7d6a9db2..4a5f934a 100644 --- a/test/fixtures/deploy_with_extensions.yml +++ b/test/fixtures/deploy_with_extensions.yml @@ -21,4 +21,6 @@ registry: server: registry.digitalocean.com username: user password: pw +builder: + arch: amd64 primary_role: web_tokyo diff --git a/test/fixtures/deploy_with_low_percentage_boot_strategy.yml b/test/fixtures/deploy_with_low_percentage_boot_strategy.yml index 9b6a3c64..8698b89f 100644 --- a/test/fixtures/deploy_with_low_percentage_boot_strategy.yml +++ b/test/fixtures/deploy_with_low_percentage_boot_strategy.yml @@ -7,6 +7,8 @@ servers: workers: - "1.1.1.3" - "1.1.1.4" +builder: + arch: amd64 registry: username: user diff --git a/test/fixtures/deploy_with_multiple_traefik_roles.yml b/test/fixtures/deploy_with_multiple_traefik_roles.yml index a36a409f..a1270583 100644 --- a/test/fixtures/deploy_with_multiple_traefik_roles.yml +++ b/test/fixtures/deploy_with_multiple_traefik_roles.yml @@ -26,6 +26,8 @@ servers: hosts: - 1.1.1.3 - 1.1.1.4 +builder: + arch: amd64 env: REDIS_URL: redis://x/y registry: diff --git a/test/fixtures/deploy_with_percentage_boot_strategy.yml b/test/fixtures/deploy_with_percentage_boot_strategy.yml index 9b6a3c64..8698b89f 100644 --- a/test/fixtures/deploy_with_percentage_boot_strategy.yml +++ b/test/fixtures/deploy_with_percentage_boot_strategy.yml @@ -7,6 +7,8 @@ servers: workers: - "1.1.1.3" - "1.1.1.4" +builder: + arch: amd64 registry: username: user diff --git a/test/fixtures/deploy_with_remote_builder.yml b/test/fixtures/deploy_with_remote_builder.yml index a67963c2..118142f7 100644 --- a/test/fixtures/deploy_with_remote_builder.yml +++ b/test/fixtures/deploy_with_remote_builder.yml @@ -32,6 +32,8 @@ accessories: port: 6379 directories: - data:/data +builder: + arch: amd64 readiness_delay: 0 diff --git a/test/fixtures/deploy_with_remote_builder_and_custom_ports.yml b/test/fixtures/deploy_with_remote_builder_and_custom_ports.yml index d76100ff..0f409da4 100644 --- a/test/fixtures/deploy_with_remote_builder_and_custom_ports.yml +++ b/test/fixtures/deploy_with_remote_builder_and_custom_ports.yml @@ -32,6 +32,8 @@ accessories: port: 6379 directories: - data:/data +builder: + arch: amd64 readiness_delay: 0 diff --git a/test/fixtures/deploy_with_roles.yml b/test/fixtures/deploy_with_roles.yml index 0e405241..1eb8cc5c 100644 --- a/test/fixtures/deploy_with_roles.yml +++ b/test/fixtures/deploy_with_roles.yml @@ -14,3 +14,5 @@ registry: server: registry.digitalocean.com username: user password: pw +builder: + arch: amd64 diff --git a/test/fixtures/deploy_with_secrets.yml b/test/fixtures/deploy_with_secrets.yml index a03cecbf..35145a3e 100644 --- a/test/fixtures/deploy_with_secrets.yml +++ b/test/fixtures/deploy_with_secrets.yml @@ -9,3 +9,5 @@ registry: env: secret: - PASSWORD +builder: + arch: amd64 diff --git a/test/fixtures/deploy_with_two_roles_one_host.yml b/test/fixtures/deploy_with_two_roles_one_host.yml index cae05469..939ff7e5 100644 --- a/test/fixtures/deploy_with_two_roles_one_host.yml +++ b/test/fixtures/deploy_with_two_roles_one_host.yml @@ -13,3 +13,5 @@ registry: server: registry.digitalocean.com username: user password: pw +builder: + arch: amd64 diff --git a/test/fixtures/deploy_with_uncommon_hostnames.yml b/test/fixtures/deploy_with_uncommon_hostnames.yml index 71e7a601..112f7b39 100644 --- a/test/fixtures/deploy_with_uncommon_hostnames.yml +++ b/test/fixtures/deploy_with_uncommon_hostnames.yml @@ -6,3 +6,5 @@ servers: registry: username: user password: pw +builder: + arch: amd64 diff --git a/test/fixtures/deploy_without_clone.yml b/test/fixtures/deploy_without_clone.yml index abb1224a..c322155f 100644 --- a/test/fixtures/deploy_without_clone.yml +++ b/test/fixtures/deploy_without_clone.yml @@ -36,4 +36,5 @@ accessories: readiness_delay: 0 builder: + arch: amd64 context: "." diff --git a/test/fixtures/deploy_workers_only.yml b/test/fixtures/deploy_workers_only.yml index 75cbbde1..d83adeeb 100644 --- a/test/fixtures/deploy_workers_only.yml +++ b/test/fixtures/deploy_workers_only.yml @@ -10,3 +10,5 @@ primary_role: workers registry: username: user password: pw +builder: + arch: amd64 diff --git a/test/integration/docker/deployer/app/config/deploy.yml b/test/integration/docker/deployer/app/config/deploy.yml index 99bfc297..de701afc 100644 --- a/test/integration/docker/deployer/app/config/deploy.yml +++ b/test/integration/docker/deployer/app/config/deploy.yml @@ -24,6 +24,7 @@ registry: password: root builder: driver: docker + arch: <%= uname_m = `uname -m`; uname_m == "x86_64" ? "amd64" : uname_m %> args: COMMIT_SHA: <%= `git rev-parse HEAD` %> healthcheck: diff --git a/test/integration/docker/deployer/app_with_roles/config/deploy.yml b/test/integration/docker/deployer/app_with_roles/config/deploy.yml index d7e757cc..baedfaf5 100644 --- a/test/integration/docker/deployer/app_with_roles/config/deploy.yml +++ b/test/integration/docker/deployer/app_with_roles/config/deploy.yml @@ -18,6 +18,7 @@ registry: password: root builder: driver: docker + arch: <%= uname_m = `uname -m`; uname_m == "x86_64" ? "amd64" : uname_m %> args: COMMIT_SHA: <%= `git rev-parse HEAD` %> healthcheck: diff --git a/test/integration/main_test.rb b/test/integration/main_test.rb index d0cf024e..a1e81493 100644 --- a/test/integration/main_test.rb +++ b/test/integration/main_test.rb @@ -77,7 +77,7 @@ class MainTest < IntegrationTest assert_equal "app-#{version}", config[:service_with_version] assert_equal [], config[:volume_args] assert_equal({ user: "root", port: 22, keepalive: true, keepalive_interval: 30, log_level: :fatal }, config[:ssh_options]) - assert_equal({ "driver" => "docker", "args" => { "COMMIT_SHA" => version } }, config[:builder]) + assert_equal({ "driver" => "docker", "arch" => "amd64", "args" => { "COMMIT_SHA" => version } }, config[:builder]) assert_equal [ "--log-opt", "max-size=\"10m\"" ], config[:logging] assert_equal({ "cmd"=>"wget -qO- http://localhost > /dev/null || exit 1", "interval"=>"1s", "max_attempts"=>3, "port"=>3000, "path"=>"/up", "cord"=>"/tmp/kamal-cord", "log_lines"=>50 }, config[:healthcheck]) end From eab7d3adc5f581e1b9d9ff25db69d137f8ada0e7 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 28 Aug 2024 11:51:15 +0100 Subject: [PATCH 21/27] Keep buildx build, in case of old docker versions which don't default to buildkit --- lib/kamal/commands/builder/base.rb | 2 +- test/cli/build_test.rb | 8 ++++---- test/commands/builder_test.rb | 20 ++++++++++---------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/kamal/commands/builder/base.rb b/lib/kamal/commands/builder/base.rb index 5c03881f..027b26c2 100644 --- a/lib/kamal/commands/builder/base.rb +++ b/lib/kamal/commands/builder/base.rb @@ -14,7 +14,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base end def push - docker :build, + docker :buildx, :build, "--push", *platform_options(arches), *([ "--builder", builder_name ] unless docker_driver?), diff --git a/test/cli/build_test.rb b/test/cli/build_test.rb index 11b6fb1e..826bd713 100644 --- a/test/cli/build_test.rb +++ b/test/cli/build_test.rb @@ -26,7 +26,7 @@ class CliBuildTest < CliTestCase assert_match /Cloning repo into build directory/, output assert_match /git -C #{Dir.tmpdir}\/kamal-clones\/app-#{pwd_sha} clone #{Dir.pwd}/, output assert_match /docker --version && docker buildx version/, output - assert_match /docker build --push --platform linux\/amd64 --builder kamal-local-docker-container -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile \. as .*@localhost/, output + assert_match /docker buildx build --push --platform linux\/amd64 --builder kamal-local-docker-container -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile \. as .*@localhost/, output end end end @@ -49,7 +49,7 @@ class CliBuildTest < CliTestCase SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :submodule, :update, "--init") SSHKit::Backend::Abstract.any_instance.expects(:execute) - .with(:docker, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".") + .with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".") SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:git, "-C", anything, :"rev-parse", :HEAD) @@ -74,7 +74,7 @@ class CliBuildTest < CliTestCase assert_no_match /Cloning repo into build directory/, output assert_hook_ran "pre-build", output, **hook_variables assert_match /docker --version && docker buildx version/, output - assert_match /docker build --push --platform linux\/amd64 --builder kamal-local-docker-container -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile . as .*@localhost/, output + assert_match /docker buildx build --push --platform linux\/amd64 --builder kamal-local-docker-container -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile . as .*@localhost/, output end end @@ -137,7 +137,7 @@ class CliBuildTest < CliTestCase .returns("") SSHKit::Backend::Abstract.any_instance.expects(:execute) - .with(:docker, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".") + .with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".") run_command("push").tap do |output| assert_match /WARN Missing compatible builder, so creating a new one first/, output diff --git a/test/commands/builder_test.rb b/test/commands/builder_test.rb index b7094126..759278ac 100644 --- a/test/commands/builder_test.rb +++ b/test/commands/builder_test.rb @@ -9,7 +9,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase builder = new_builder_command(builder: { "cache" => { "type" => "gha" } }) assert_equal "local", builder.name assert_equal \ - "docker build --push --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 .", + "docker buildx build --push --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 .", builder.push.join(" ") end @@ -17,7 +17,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase builder = new_builder_command(builder: { "arch" => [ "amd64" ] }) assert_equal "local", builder.name assert_equal \ - "docker build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile .", + "docker buildx build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile .", builder.push.join(" ") end @@ -25,7 +25,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase builder = new_builder_command(builder: { "cache" => { "type" => "gha" } }) assert_equal "local", builder.name assert_equal \ - "docker build --push --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 .", + "docker buildx build --push --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 .", builder.push.join(" ") end @@ -33,7 +33,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase 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 build --push --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 .", + "docker buildx build --push --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 .", builder.push.join(" ") end @@ -41,7 +41,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase builder = new_builder_command(builder: { "arch" => [ "#{remote_arch}" ], "remote" => "ssh://app@host", "cache" => { "type" => "gha" } }) assert_equal "remote", builder.name assert_equal \ - "docker build --push --platform linux/#{remote_arch} --builder kamal-remote-docker-container-ssh---app-host -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .", + "docker buildx build --push --platform linux/#{remote_arch} --builder kamal-remote-docker-container-ssh---app-host -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 @@ -49,7 +49,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase builder = new_builder_command(builder: { "arch" => [ "#{local_arch}" ], "remote" => "ssh://app@host", "cache" => { "type" => "gha" } }) assert_equal "local", builder.name assert_equal \ - "docker build --push --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 .", + "docker buildx build --push --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 .", builder.push.join(" ") end @@ -93,21 +93,21 @@ class CommandsBuilderTest < ActiveSupport::TestCase test "build context" do builder = new_builder_command(builder: { "context" => ".." }) assert_equal \ - "docker build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ..", + "docker buildx build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ..", builder.push.join(" ") end test "push with build args" do builder = new_builder_command(builder: { "args" => { "a" => 1, "b" => 2 } }) assert_equal \ - "docker build --push --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 .", + "docker buildx build --push --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 .", builder.push.join(" ") end test "push with build secrets" do builder = new_builder_command(builder: { "secrets" => [ "a", "b" ] }) assert_equal \ - "docker build --push --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 .", + "docker buildx build --push --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 .", builder.push.join(" ") end @@ -126,7 +126,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase test "context build" do builder = new_builder_command(builder: { "context" => "./foo" }) assert_equal \ - "docker build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo", + "docker buildx build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo", builder.push.join(" ") end From 5f2384f123738922d35b2b74d3ec587403be125a Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 28 Aug 2024 15:50:46 +0100 Subject: [PATCH 22/27] Use docker info to get arch --- lib/kamal/configuration/builder.rb | 4 +--- lib/kamal/utils.rb | 12 ++++++++++++ test/commands/builder_test.rb | 4 ++-- test/fixtures/deploy_with_remote_builder.yml | 2 +- .../deploy_with_remote_builder_and_custom_ports.yml | 2 +- .../docker/deployer/app/config/deploy.yml | 2 +- .../docker/deployer/app_with_roles/config/deploy.yml | 2 +- 7 files changed, 19 insertions(+), 9 deletions(-) diff --git a/lib/kamal/configuration/builder.rb b/lib/kamal/configuration/builder.rb index 4f51669a..0daf7e74 100644 --- a/lib/kamal/configuration/builder.rb +++ b/lib/kamal/configuration/builder.rb @@ -29,9 +29,7 @@ class Kamal::Configuration::Builder def local_arches @local_arches ||= if remote - uname_m = `uname -m`.strip - local_arch = uname_m == "x86_64" ? "amd64" : uname_m - arches & [ local_arch ] + arches & [ Kamal::Utils.docker_arch ] else arches end diff --git a/lib/kamal/utils.rb b/lib/kamal/utils.rb index a6ba5702..46736ba5 100644 --- a/lib/kamal/utils.rb +++ b/lib/kamal/utils.rb @@ -81,4 +81,16 @@ module Kamal::Utils def join_commands(commands) commands.map(&:strip).join(" ") end + + def docker_arch + arch = `docker info --format '{{.Architecture}}'`.strip + case arch + when /aarch64/ + "arm64" + when /x86_64/ + "amd64" + else + arch + end + end end diff --git a/test/commands/builder_test.rb b/test/commands/builder_test.rb index 759278ac..c43c5c85 100644 --- a/test/commands/builder_test.rb +++ b/test/commands/builder_test.rb @@ -145,10 +145,10 @@ class CommandsBuilderTest < ActiveSupport::TestCase end def local_arch - `uname -m`.strip == "x86_64" ? "amd64" : "arm64" + Kamal::Utils.docker_arch end def remote_arch - `uname -m`.strip == "x86_64" ? "arm64" : "amd64" + Kamal::Utils.docker_arch == "arm64" ? "amd64" : "arm64" end end diff --git a/test/fixtures/deploy_with_remote_builder.yml b/test/fixtures/deploy_with_remote_builder.yml index 118142f7..6555ee6f 100644 --- a/test/fixtures/deploy_with_remote_builder.yml +++ b/test/fixtures/deploy_with_remote_builder.yml @@ -38,5 +38,5 @@ builder: readiness_delay: 0 builder: - arch: <%= `uname -m`.strip == "x86_64" ? "arm64" : "amd64" %> + arch: <%= Kamal::Utils.docker_arch == "arm64" ? "amd64" : "arm64" %> remote: ssh://app@1.1.1.5 diff --git a/test/fixtures/deploy_with_remote_builder_and_custom_ports.yml b/test/fixtures/deploy_with_remote_builder_and_custom_ports.yml index 0f409da4..0b39259c 100644 --- a/test/fixtures/deploy_with_remote_builder_and_custom_ports.yml +++ b/test/fixtures/deploy_with_remote_builder_and_custom_ports.yml @@ -42,5 +42,5 @@ ssh: port: 22 builder: - arch: <%= `uname -m`.strip == "x86_64" ? "arm64" : "amd64" %> + arch: <%= Kamal::Utils.docker_arch == "arm64" ? "amd64" : "arm64" %> remote: ssh://app@1.1.1.5:2122 diff --git a/test/integration/docker/deployer/app/config/deploy.yml b/test/integration/docker/deployer/app/config/deploy.yml index de701afc..887de825 100644 --- a/test/integration/docker/deployer/app/config/deploy.yml +++ b/test/integration/docker/deployer/app/config/deploy.yml @@ -24,7 +24,7 @@ registry: password: root builder: driver: docker - arch: <%= uname_m = `uname -m`; uname_m == "x86_64" ? "amd64" : uname_m %> + arch: <%= Kamal::Utils.docker_arch %> args: COMMIT_SHA: <%= `git rev-parse HEAD` %> healthcheck: diff --git a/test/integration/docker/deployer/app_with_roles/config/deploy.yml b/test/integration/docker/deployer/app_with_roles/config/deploy.yml index baedfaf5..3b942665 100644 --- a/test/integration/docker/deployer/app_with_roles/config/deploy.yml +++ b/test/integration/docker/deployer/app_with_roles/config/deploy.yml @@ -18,7 +18,7 @@ registry: password: root builder: driver: docker - arch: <%= uname_m = `uname -m`; uname_m == "x86_64" ? "amd64" : uname_m %> + arch: <%= Kamal::Utils.docker_arch %> args: COMMIT_SHA: <%= `git rev-parse HEAD` %> healthcheck: From 9f0b10425c1c48b987ce56999a1d8b8e3f4fb6d1 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 29 Aug 2024 09:16:07 +0100 Subject: [PATCH 23/27] Fix aliases tests --- test/fixtures/deploy_with_aliases.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/fixtures/deploy_with_aliases.yml b/test/fixtures/deploy_with_aliases.yml index bb54fa76..ec7b14a0 100644 --- a/test/fixtures/deploy_with_aliases.yml +++ b/test/fixtures/deploy_with_aliases.yml @@ -11,6 +11,8 @@ servers: console: hosts: - 1.1.1.5 +builder: + arch: amd64 registry: username: user password: pw From 362f5d00f607834880896cfd1887f0009fa624bf Mon Sep 17 00:00:00 2001 From: Matt Layman Date: Sat, 31 Aug 2024 23:29:30 -0400 Subject: [PATCH 24/27] Fix typos in "Environment variables" docs. --- lib/kamal/configuration/docs/env.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/kamal/configuration/docs/env.yml b/lib/kamal/configuration/docs/env.yml index 3c661c70..0ca2bfa9 100644 --- a/lib/kamal/configuration/docs/env.yml +++ b/lib/kamal/configuration/docs/env.yml @@ -1,7 +1,7 @@ # Environment variables # -# Environment variables can be set directory in the Kamal configuration or -# for loaded from a .env file, for secrets that should not be checked into Git. +# Environment variables can be set directly in the Kamal configuration or +# loaded from a .env file, for secrets that should not be checked into Git. # Reading environment variables from the configuration # From 5cda3086c4d70a5d9eb2c28cc2720dfbfd0c56b9 Mon Sep 17 00:00:00 2001 From: Matt Layman Date: Sat, 31 Aug 2024 23:38:28 -0400 Subject: [PATCH 25/27] Found a typo in the healthcheck docs. --- lib/kamal/configuration/docs/healthcheck.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/kamal/configuration/docs/healthcheck.yml b/lib/kamal/configuration/docs/healthcheck.yml index e29771f5..3c55d5dd 100644 --- a/lib/kamal/configuration/docs/healthcheck.yml +++ b/lib/kamal/configuration/docs/healthcheck.yml @@ -3,7 +3,7 @@ # On roles that are running Traefik, Kamal will supply a default healthcheck to `docker run`. # For other roles, by default no healthcheck is supplied. # -# If no healthcheck is supplied and the image does not define one, they we wait for the container +# If no healthcheck is supplied and the image does not define one, then we wait for the container # to reach a running state and then pause for the readiness delay. # # The default healthcheck is `curl -f http://localhost:/`, so it assumes that `curl` From e557eea79c8dfbc5aed2be89524baa0c3c6eb85e Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Mon, 2 Sep 2024 15:10:39 +0100 Subject: [PATCH 26/27] Build and clean remote builders correctly Check that the builder and context match what we expect, and if not remove and re-create them. --- lib/kamal/cli/build.rb | 9 +++- lib/kamal/commands/base.rb | 4 ++ lib/kamal/commands/builder.rb | 2 +- lib/kamal/commands/builder/base.rb | 6 +-- lib/kamal/commands/builder/hybrid.rb | 2 +- lib/kamal/commands/builder/remote.rb | 31 ++++++++++++-- lib/kamal/commands/prune.rb | 2 +- test/cli/build_test.rb | 20 +++++++-- test/commands/builder_test.rb | 2 +- test/fixtures/deploy_with_hybrid_builder.yml | 42 +++++++++++++++++++ test/fixtures/deploy_with_remote_builder.yml | 2 - ...y_with_remote_builder_and_custom_ports.yml | 2 - 12 files changed, 101 insertions(+), 23 deletions(-) create mode 100644 test/fixtures/deploy_with_hybrid_builder.yml diff --git a/lib/kamal/cli/build.rb b/lib/kamal/cli/build.rb index ac73d11d..93e1efd9 100644 --- a/lib/kamal/cli/build.rb +++ b/lib/kamal/cli/build.rb @@ -32,10 +32,15 @@ class Kamal::Cli::Build < Kamal::Cli::Base run_locally do begin - execute *KAMAL.builder.buildx_inspect + execute *KAMAL.builder.inspect_builder rescue SSHKit::Command::Failed => e - if e.message =~ /(context not found|no builder|does not exist)/ + if e.message =~ /(context not found|no builder|no compatible builder|does not exist)/ warn "Missing compatible builder, so creating a new one first" + begin + cli.remove + rescue SSHKit::Command::Failed + raise unless e.message =~ /(context not found|no builder|does not exist)/ + end cli.create else raise diff --git a/lib/kamal/commands/base.rb b/lib/kamal/commands/base.rb index 2173d064..39e60d50 100644 --- a/lib/kamal/commands/base.rb +++ b/lib/kamal/commands/base.rb @@ -81,6 +81,10 @@ module Kamal::Commands [ :git, *([ "-C", path ] if path), *args.compact ] end + def grep(*args) + args.compact.unshift :grep + end + def tags(**details) Kamal::Tags.from_config(config, **details) end diff --git a/lib/kamal/commands/builder.rb b/lib/kamal/commands/builder.rb index e69e3c2f..cd2980fb 100644 --- a/lib/kamal/commands/builder.rb +++ b/lib/kamal/commands/builder.rb @@ -1,7 +1,7 @@ require "active_support/core_ext/string/filters" class Kamal::Commands::Builder < Kamal::Commands::Base - delegate :create, :remove, :push, :clean, :pull, :info, :buildx_inspect, :validate_image, :first_mirror, to: :target + delegate :create, :remove, :push, :clean, :pull, :info, :inspect_builder, :validate_image, :first_mirror, to: :target delegate :local?, :remote?, to: "config.builder" include Clone diff --git a/lib/kamal/commands/builder/base.rb b/lib/kamal/commands/builder/base.rb index 027b26c2..1e6f5be3 100644 --- a/lib/kamal/commands/builder/base.rb +++ b/lib/kamal/commands/builder/base.rb @@ -32,7 +32,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base docker(:buildx, :ls) end - def buildx_inspect + def inspect_builder docker :buildx, :inspect, builder_name unless docker_driver? end @@ -101,10 +101,6 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base config.builder end - def context_host(builder_name) - docker :context, :inspect, builder_name, "--format", ENDPOINT_DOCKER_HOST_INSPECT - end - def platform_options(arches) argumentize "--platform", arches.map { |arch| "linux/#{arch}" }.join(",") if arches.any? end diff --git a/lib/kamal/commands/builder/hybrid.rb b/lib/kamal/commands/builder/hybrid.rb index 5434169a..ad9561aa 100644 --- a/lib/kamal/commands/builder/hybrid.rb +++ b/lib/kamal/commands/builder/hybrid.rb @@ -16,6 +16,6 @@ class Kamal::Commands::Builder::Hybrid < Kamal::Commands::Builder::Remote end def append_remote_buildx - docker :buildx, :create, *platform_options(remote_arches), "--append", "--name", builder_name, builder_name + docker :buildx, :create, *platform_options(remote_arches), "--append", "--name", builder_name, remote_context_name end end diff --git a/lib/kamal/commands/builder/remote.rb b/lib/kamal/commands/builder/remote.rb index b4644047..805e2b53 100644 --- a/lib/kamal/commands/builder/remote.rb +++ b/lib/kamal/commands/builder/remote.rb @@ -17,21 +17,44 @@ class Kamal::Commands::Builder::Remote < Kamal::Commands::Builder::Base docker(:buildx, :ls) end + def inspect_builder + combine \ + combine inspect_buildx, inspect_remote_context, + [ "(echo no compatible builder && exit 1)" ], + by: "||" + end + private def builder_name - "kamal-remote-#{driver}-#{remote.gsub(/[^a-z0-9_-]/, "-")}" + "kamal-remote-#{remote.gsub(/[^a-z0-9_-]/, "-")}" + end + + def remote_context_name + "#{builder_name}-context" + end + + def inspect_buildx + pipe \ + docker(:buildx, :inspect, builder_name), + grep("-q", "Endpoint:.*#{remote_context_name}") + end + + def inspect_remote_context + pipe \ + docker(:context, :inspect, remote_context_name, "--format", ENDPOINT_DOCKER_HOST_INSPECT), + grep("-xq", remote) end def create_remote_context - docker :context, :create, builder_name, "--description", "'#{builder_name} host'", "--docker", "'host=#{remote}'" + docker :context, :create, remote_context_name, "--description", "'#{builder_name} host'", "--docker", "'host=#{remote}'" end def remove_remote_context - docker :context, :rm, builder_name + docker :context, :rm, remote_context_name end def create_buildx - docker :buildx, :create, "--name", builder_name, builder_name + docker :buildx, :create, "--name", builder_name, remote_context_name end def remove_buildx diff --git a/lib/kamal/commands/prune.rb b/lib/kamal/commands/prune.rb index c893edd8..b820b5af 100644 --- a/lib/kamal/commands/prune.rb +++ b/lib/kamal/commands/prune.rb @@ -9,7 +9,7 @@ class Kamal::Commands::Prune < Kamal::Commands::Base def tagged_images pipe \ docker(:image, :ls, *service_filter, "--format", "'{{.ID}} {{.Repository}}:{{.Tag}}'"), - "grep -v -w \"#{active_image_list}\"", + grep("-v -w \"#{active_image_list}\""), "while read image tag; do docker rmi $tag; done" end diff --git a/test/cli/build_test.rb b/test/cli/build_test.rb index 826bd713..4259fa5b 100644 --- a/test/cli/build_test.rb +++ b/test/cli/build_test.rb @@ -119,6 +119,9 @@ class CliBuildTest < CliTestCase SSHKit::Backend::Abstract.any_instance.expects(:execute) .with(:docker, "--version", "&&", :docker, :buildx, "version") + SSHKit::Backend::Abstract.any_instance.expects(:execute) + .with(:docker, :buildx, :rm, "kamal-local-docker-container") + SSHKit::Backend::Abstract.any_instance.expects(:execute) .with(:docker, :buildx, :create, "--name", "kamal-local-docker-container", "--driver=docker-container") @@ -210,16 +213,25 @@ class CliBuildTest < CliTestCase test "create remote" do run_command("create", fixture: :with_remote_builder).tap do |output| assert_match "Running /usr/bin/env true on 1.1.1.5", output - assert_match "docker context create kamal-remote-docker-container-ssh---app-1-1-1-5 --description 'kamal-remote-docker-container-ssh---app-1-1-1-5 host' --docker 'host=ssh://app@1.1.1.5'", output - assert_match "docker buildx create --name kamal-remote-docker-container-ssh---app-1-1-1-5 kamal-remote-docker-container-ssh---app-1-1-1-5", output + assert_match "docker context create kamal-remote-ssh---app-1-1-1-5-context --description 'kamal-remote-ssh---app-1-1-1-5 host' --docker 'host=ssh://app@1.1.1.5'", output + assert_match "docker buildx create --name kamal-remote-ssh---app-1-1-1-5 kamal-remote-ssh---app-1-1-1-5-context", output end end test "create remote with custom ports" do run_command("create", fixture: :with_remote_builder_and_custom_ports).tap do |output| assert_match "Running /usr/bin/env true on 1.1.1.5", output - assert_match "docker context create kamal-remote-docker-container-ssh---app-1-1-1-5-2122 --description 'kamal-remote-docker-container-ssh---app-1-1-1-5-2122 host' --docker 'host=ssh://app@1.1.1.5:2122'", output - assert_match "docker buildx create --name kamal-remote-docker-container-ssh---app-1-1-1-5-2122 kamal-remote-docker-container-ssh---app-1-1-1-5-2122", output + assert_match "docker context create kamal-remote-ssh---app-1-1-1-5-2122-context --description 'kamal-remote-ssh---app-1-1-1-5-2122 host' --docker 'host=ssh://app@1.1.1.5:2122'", output + assert_match "docker buildx create --name kamal-remote-ssh---app-1-1-1-5-2122 kamal-remote-ssh---app-1-1-1-5-2122-context", output + end + end + + test "create hybrid" do + run_command("create", fixture: :with_hybrid_builder).tap do |output| + assert_match "Running /usr/bin/env true on 1.1.1.5", output + assert_match "docker buildx create --platform linux/#{Kamal::Utils.docker_arch} --name kamal-hybrid-docker-container-ssh---app-1-1-1-5 --driver=docker-container", output + assert_match "docker context create kamal-hybrid-docker-container-ssh---app-1-1-1-5-context --description 'kamal-hybrid-docker-container-ssh---app-1-1-1-5 host' --docker 'host=ssh://app@1.1.1.5'", output + assert_match "docker buildx create --platform linux/#{Kamal::Utils.docker_arch == "amd64" ? "arm64" : "amd64"} --append --name kamal-hybrid-docker-container-ssh---app-1-1-1-5 kamal-hybrid-docker-container-ssh---app-1-1-1-5-context", output end end diff --git a/test/commands/builder_test.rb b/test/commands/builder_test.rb index c43c5c85..f44f00e0 100644 --- a/test/commands/builder_test.rb +++ b/test/commands/builder_test.rb @@ -41,7 +41,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase 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 --push --platform linux/#{remote_arch} --builder kamal-remote-docker-container-ssh---app-host -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .", + "docker buildx build --push --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 .", builder.push.join(" ") end diff --git a/test/fixtures/deploy_with_hybrid_builder.yml b/test/fixtures/deploy_with_hybrid_builder.yml new file mode 100644 index 00000000..0db184fb --- /dev/null +++ b/test/fixtures/deploy_with_hybrid_builder.yml @@ -0,0 +1,42 @@ +service: app +image: dhh/app +servers: + web: + - "1.1.1.1" + - "1.1.1.2" + workers: + - "1.1.1.3" + - "1.1.1.4" +registry: + username: user + password: pw + +accessories: + mysql: + image: mysql:5.7 + host: 1.1.1.3 + port: 3306 + env: + clear: + MYSQL_ROOT_HOST: '%' + secret: + - MYSQL_ROOT_PASSWORD + files: + - test/fixtures/files/my.cnf:/etc/mysql/my.cnf + directories: + - data:/var/lib/mysql + redis: + image: redis:latest + roles: + - web + port: 6379 + directories: + - data:/data + +readiness_delay: 0 + +builder: + arch: + - arm64 + - amd64 + remote: ssh://app@1.1.1.5 diff --git a/test/fixtures/deploy_with_remote_builder.yml b/test/fixtures/deploy_with_remote_builder.yml index 6555ee6f..d5db9632 100644 --- a/test/fixtures/deploy_with_remote_builder.yml +++ b/test/fixtures/deploy_with_remote_builder.yml @@ -32,8 +32,6 @@ accessories: port: 6379 directories: - data:/data -builder: - arch: amd64 readiness_delay: 0 diff --git a/test/fixtures/deploy_with_remote_builder_and_custom_ports.yml b/test/fixtures/deploy_with_remote_builder_and_custom_ports.yml index 0b39259c..1cfd5471 100644 --- a/test/fixtures/deploy_with_remote_builder_and_custom_ports.yml +++ b/test/fixtures/deploy_with_remote_builder_and_custom_ports.yml @@ -32,8 +32,6 @@ accessories: port: 6379 directories: - data:/data -builder: - arch: amd64 readiness_delay: 0 From a2549b1f6098f86686fef5cf338edb0dc072740f Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Tue, 3 Sep 2024 14:33:25 +0100 Subject: [PATCH 27/27] Allow disabling of local builds To disable local builds set: ``` builder: local: false remote: ssh://docker@docker-builder ``` --- lib/kamal/configuration/builder.rb | 10 ++++++++-- lib/kamal/configuration/docs/builder.yml | 5 +++++ lib/kamal/configuration/validator/builder.rb | 2 ++ test/commands/builder_test.rb | 8 ++++++++ test/configuration/builder_test.rb | 17 +++++++++++++++++ 5 files changed, 40 insertions(+), 2 deletions(-) diff --git a/lib/kamal/configuration/builder.rb b/lib/kamal/configuration/builder.rb index 0daf7e74..bad3b386 100644 --- a/lib/kamal/configuration/builder.rb +++ b/lib/kamal/configuration/builder.rb @@ -28,7 +28,9 @@ class Kamal::Configuration::Builder end def local_arches - @local_arches ||= if remote + @local_arches ||= if local_disabled? + [] + elsif remote arches & [ Kamal::Utils.docker_arch ] else arches @@ -48,7 +50,7 @@ class Kamal::Configuration::Builder end def local? - arches.empty? || local_arches.any? + !local_disabled? && (arches.empty? || local_arches.any?) end def cached? @@ -79,6 +81,10 @@ class Kamal::Configuration::Builder builder_config.fetch("driver", "docker-container") end + def local_disabled? + builder_config["local"] == false + end + def cache_from if cached? case builder_config["cache"]["type"] diff --git a/lib/kamal/configuration/docs/builder.yml b/lib/kamal/configuration/docs/builder.yml index 5455f1a2..6209a4a6 100644 --- a/lib/kamal/configuration/docs/builder.yml +++ b/lib/kamal/configuration/docs/builder.yml @@ -30,6 +30,11 @@ builder: # If you have a remote builder, you can configure it here remote: ssh://docker@docker-builder + # Whether to allow local builds + # + # Defaults to true + local: true + # Builder cache # # The type must be either 'gha' or 'registry' diff --git a/lib/kamal/configuration/validator/builder.rb b/lib/kamal/configuration/validator/builder.rb index 4bd815bf..8c8fe1f0 100644 --- a/lib/kamal/configuration/validator/builder.rb +++ b/lib/kamal/configuration/validator/builder.rb @@ -7,5 +7,7 @@ class Kamal::Configuration::Validator::Builder < Kamal::Configuration::Validator end error "Builder arch not set" unless config["arch"].present? + + error "Cannot disable local builds, no remote is set" if config["local"] == false && config["remote"].blank? end end diff --git a/test/commands/builder_test.rb b/test/commands/builder_test.rb index f44f00e0..45c40388 100644 --- a/test/commands/builder_test.rb +++ b/test/commands/builder_test.rb @@ -37,6 +37,14 @@ class CommandsBuilderTest < ActiveSupport::TestCase 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 --push --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 .", + 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 diff --git a/test/configuration/builder_test.rb b/test/configuration/builder_test.rb index 3df477c9..a4fa7fbb 100644 --- a/test/configuration/builder_test.rb +++ b/test/configuration/builder_test.rb @@ -132,6 +132,23 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase assert_equal "default=$SSH_AUTH_SOCK", config.builder.ssh end + test "local disabled but no remote set" do + @deploy[:builder]["local"] = false + + assert_raises(Kamal::ConfigurationError) do + config.builder + end + end + + test "local disabled all arches are remote" do + @deploy[:builder]["local"] = false + @deploy[:builder]["remote"] = "ssh://root@192.168.0.1" + @deploy[:builder]["arch"] = [ "amd64", "arm64" ] + + assert_equal [], config.builder.local_arches + assert_equal [ "amd64", "arm64" ], config.builder.remote_arches + end + private def config Kamal::Configuration.new(@deploy)