diff --git a/Gemfile.lock b/Gemfile.lock index a24796bd..b3946bdc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - kamal (1.8.1) + kamal (2.0.0.alpha) activesupport (>= 7.0) base64 (~> 0.2) bcrypt_pbkdf (~> 1.0) @@ -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/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/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/cli/build.rb b/lib/kamal/cli/build.rb index c655c6ae..93e1efd9 100644 --- a/lib/kamal/cli/build.rb +++ b/lib/kamal/cli/build.rb @@ -30,27 +30,26 @@ 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.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 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 @@ -72,7 +71,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/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/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/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/commands/base.rb b/lib/kamal/commands/base.rb index 731e543f..59928d95 100644 --- a/lib/kamal/commands/base.rb +++ b/lib/kamal/commands/base.rb @@ -85,6 +85,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 41f07dc2..c50bc3c5 100644 --- a/lib/kamal/commands/builder.rb +++ b/lib/kamal/commands/builder.rb @@ -1,8 +1,8 @@ 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, :inspect_builder, :validate_image, :first_mirror, to: :target + delegate :local?, :remote?, :pack?, to: "config.builder" include Clone @@ -11,49 +11,33 @@ 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 - elsif config.builder.pack? - pack + if remote? + if local? + hybrid else - multiarch + remote end + elsif pack? + pack else - if config.builder.cached? - native_cached - else - native - end + local end end - def native - @native ||= Kamal::Commands::Builder::Native.new(config) + def remote + @remote ||= Kamal::Commands::Builder::Remote.new(config) end - def native_cached - @native ||= Kamal::Commands::Builder::Native::Cached.new(config) + def local + @local ||= Kamal::Commands::Builder::Local.new(config) end - def native_remote - @native ||= Kamal::Commands::Builder::Native::Remote.new(config) - end - - def multiarch - @multiarch ||= Kamal::Commands::Builder::Multiarch.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 def pack - @pack ||= Kamal::Commands::Builder::Native::Pack.new(config) + @pack ||= Kamal::Commands::Builder::Pack.new(config) end def ensure_local_dependencies_installed diff --git a/lib/kamal/commands/builder/base.rb b/lib/kamal/commands/builder/base.rb index fa095f60..4dfaf46c 100644 --- a/lib/kamal/commands/builder/base.rb +++ b/lib/kamal/commands/builder/base.rb @@ -1,20 +1,42 @@ - class Kamal::Commands::Builder::Base < Kamal::Commands::Base class BuilderError < StandardError; end ENDPOINT_DOCKER_HOST_INSPECT = "'{{.Endpoints.docker.Host}}'" delegate :argumentize, to: Kamal::Utils - delegate :args, :secrets, :dockerfile, :target, :local_arch, :local_host, :pack_arch, :pack_builder, :pack_buildpacks, :remote_arch, :remote_host, :cache_from, :cache_to, :ssh, to: :builder_config + delegate \ + :args, :secrets, :dockerfile, :target, :arches, :local_arches, :remote_arches, :remote, + :pack_builder, :pack_buildpacks, + :cache_from, :cache_to, :ssh, :driver, :docker_driver?, + to: :builder_config def clean docker :image, :rm, "--force", config.absolute_image end + def push + docker :buildx, :build, + "--push", + *platform_options(arches), + *([ "--builder", builder_name ] unless docker_driver?), + *build_options, + build_context + end + def pull docker :pull, config.absolute_image end + def info + combine \ + docker(:context, :ls), + docker(:buildx, :ls) + end + + def inspect_builder + docker :buildx, :inspect, builder_name unless docker_driver? + end + def build_options [ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh ] end @@ -32,14 +54,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 @@ -88,7 +102,7 @@ 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 + def platform_options(arches) + argumentize "--platform", arches.map { |arch| "linux/#{arch}" }.join(",") if arches.any? end end 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 diff --git a/lib/kamal/commands/builder/hybrid.rb b/lib/kamal/commands/builder/hybrid.rb new file mode 100644 index 00000000..ad9561aa --- /dev/null +++ b/lib/kamal/commands/builder/hybrid.rb @@ -0,0 +1,21 @@ +class Kamal::Commands::Builder::Hybrid < Kamal::Commands::Builder::Remote + def create + combine \ + create_local_buildx, + create_remote_context, + append_remote_buildx + end + + private + def builder_name + "kamal-hybrid-#{driver}-#{remote.gsub(/[^a-z0-9_-]/, "-")}" + end + + def create_local_buildx + docker :buildx, :create, *platform_options(local_arches), "--name", builder_name, "--driver=#{driver}" + end + + def append_remote_buildx + docker :buildx, :create, *platform_options(remote_arches), "--append", "--name", builder_name, remote_context_name + end +end diff --git a/lib/kamal/commands/builder/local.rb b/lib/kamal/commands/builder/local.rb new file mode 100644 index 00000000..c4215e33 --- /dev/null +++ b/lib/kamal/commands/builder/local.rb @@ -0,0 +1,14 @@ +class Kamal::Commands::Builder::Local < Kamal::Commands::Builder::Base + def create + docker :buildx, :create, "--name", builder_name, "--driver=#{driver}" unless docker_driver? + end + + def remove + docker :buildx, :rm, builder_name unless docker_driver? + end + + private + def builder_name + "kamal-local-#{driver}" + end +end diff --git a/lib/kamal/commands/builder/multiarch.rb b/lib/kamal/commands/builder/multiarch.rb deleted file mode 100644 index ae1d423f..00000000 --- a/lib/kamal/commands/builder/multiarch.rb +++ /dev/null @@ -1,41 +0,0 @@ -class Kamal::Commands::Builder::Multiarch < Kamal::Commands::Builder::Base - def create - docker :buildx, :create, "--use", "--name", builder_name - end - - def remove - docker :buildx, :rm, builder_name - end - - def info - combine \ - docker(:context, :ls), - docker(:buildx, :ls) - end - - def push - docker :buildx, :build, - "--push", - "--platform", platform_names, - "--builder", builder_name, - *build_options, - build_context - end - - def context_hosts - docker :buildx, :inspect, builder_name, "> /dev/null" - end - - private - def builder_name - "kamal-#{config.service}-multiarch" - end - - def platform_names - if local_arch - "linux/#{local_arch}" - else - "linux/amd64,linux/arm64" - end - 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 d60bee77..00000000 --- a/lib/kamal/commands/builder/multiarch/remote.rb +++ /dev/null @@ -1,61 +0,0 @@ -class Kamal::Commands::Builder::Multiarch::Remote < Kamal::Commands::Builder::Multiarch - def create - combine \ - create_contexts, - create_local_buildx, - append_remote_buildx - end - - def remove - combine \ - remove_contexts, - super - 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 - super + "-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 -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/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/lib/kamal/commands/builder/native/remote.rb b/lib/kamal/commands/builder/native/remote.rb deleted file mode 100644 index 9d03b8db..00000000 --- a/lib/kamal/commands/builder/native/remote.rb +++ /dev/null @@ -1,67 +0,0 @@ -class Kamal::Commands::Builder::Native::Remote < Kamal::Commands::Builder::Native - def create - chain \ - create_context, - create_buildx - end - - def remove - chain \ - remove_context, - remove_buildx - end - - def info - chain \ - docker(:context, :ls), - docker(:buildx, :ls) - end - - def push - docker :buildx, :build, - "--push", - "--platform", platform, - "--builder", builder_name, - *build_options, - build_context - end - - def context_hosts - context_host(builder_name_with_arch) - end - - def config_context_hosts - [ remote_host ] - end - - - private - def builder_name - "kamal-#{config.service}-native-remote" - end - - def builder_name_with_arch - "#{builder_name}-#{remote_arch}" - end - - def platform - "linux/#{remote_arch}" - end - - def create_context - docker :context, :create, - builder_name_with_arch, "--description", "'#{builder_name} #{remote_arch} native host'", "--docker", "'host=#{remote_host}'" - end - - def remove_context - docker :context, :rm, builder_name_with_arch - end - - def create_buildx - docker :buildx, :create, "--name", builder_name, builder_name_with_arch, "--platform", platform - end - - def remove_buildx - docker :buildx, :rm, builder_name - end -end diff --git a/lib/kamal/commands/builder/native/pack.rb b/lib/kamal/commands/builder/pack.rb similarity index 86% rename from lib/kamal/commands/builder/native/pack.rb rename to lib/kamal/commands/builder/pack.rb index f3e7d27a..3b8fbb94 100644 --- a/lib/kamal/commands/builder/native/pack.rb +++ b/lib/kamal/commands/builder/pack.rb @@ -1,4 +1,4 @@ -class Kamal::Commands::Builder::Native::Pack < Kamal::Commands::Builder::Native +class Kamal::Commands::Builder::Pack < Kamal::Commands::Builder::Base def push combine \ pack(:build, @@ -17,7 +17,7 @@ class Kamal::Commands::Builder::Native::Pack < Kamal::Commands::Builder::Native private def platform - "linux/#{pack_arch}" + "linux/#{local_arches.first}" end def buildpacks diff --git a/lib/kamal/commands/builder/remote.rb b/lib/kamal/commands/builder/remote.rb new file mode 100644 index 00000000..805e2b53 --- /dev/null +++ b/lib/kamal/commands/builder/remote.rb @@ -0,0 +1,63 @@ +class Kamal::Commands::Builder::Remote < Kamal::Commands::Builder::Base + def create + chain \ + create_remote_context, + create_buildx + end + + def remove + chain \ + remove_remote_context, + remove_buildx + end + + def info + chain \ + docker(:context, :ls), + 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-#{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, remote_context_name, "--description", "'#{builder_name} host'", "--docker", "'host=#{remote}'" + end + + def remove_remote_context + docker :context, :rm, remote_context_name + end + + def create_buildx + docker :buildx, :create, "--name", builder_name, remote_context_name + end + + def remove_buildx + docker :buildx, :rm, builder_name + end +end 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/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/builder.rb b/lib/kamal/configuration/builder.rb index 634b1846..f863e7f2 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 local_disabled? + [] + elsif remote + arches & [ Kamal::Utils.docker_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? + !local_disabled? && (arches.empty? || local_arches.any?) end def cached? @@ -59,12 +81,8 @@ class Kamal::Configuration::Builder builder_config["context"] || "." end - def local_arch - builder_config["local"]["arch"] if local? - end - - def local_host - builder_config["local"]["host"] if local? + def driver + builder_config.fetch("driver", "docker-container") end def pack_arch @@ -79,12 +97,8 @@ class Kamal::Configuration::Builder builder_config["pack"]["buildpacks"] if pack? end - def remote_arch - builder_config["remote"]["arch"] if remote? - end - - def remote_host - builder_config["remote"]["host"] if remote? + def local_disabled? + builder_config["local"] == false end def cache_from @@ -130,7 +144,23 @@ class Kamal::Configuration::Builder end end + def docker_driver? + driver == "docker" + end + private + def valid? + 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"]) + end + end + def cache_image builder_config["cache"]&.fetch("image", nil) || "#{image}-build-cache" end @@ -166,4 +196,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/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/builder.yml b/lib/kamal/configuration/docs/builder.yml index 706fe83c..26c22867 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 +# 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,37 +12,34 @@ # # Options go under the builder key in the root configuration. builder: + # Driver + # + # The build driver to use, defaults to `docker-container` + driver: docker - # Multiarch + # Arch # - # Enables multiarch builds, defaults to `true` - multiarch: false - - # Local configuration - # - # 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 + + # Whether to allow local builds + # + # Defaults to true + local: true # Buildpack configuration # # The build configuration for using pack to build a Cloud Native Buildpack image. pack: builder: heroku/builder:24 - arch: amd64 buildpacks: - heroku/ruby - heroku/procfile @@ -51,7 +48,7 @@ builder: # # 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/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/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 # 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` diff --git a/lib/kamal/configuration/validator.rb b/lib/kamal/configuration/validator.rb index c7ba0f72..2ac8482d 100644 --- a/lib/kamal/configuration/validator.rb +++ b/lib/kamal/configuration/validator.rb @@ -13,32 +13,38 @@ 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) + 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" + 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 @@ -69,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/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/lib/kamal/configuration/validator/builder.rb b/lib/kamal/configuration/validator/builder.rb index ebccdf81..8c8fe1f0 100644 --- a/lib/kamal/configuration/validator/builder.rb +++ b/lib/kamal/configuration/validator/builder.rb @@ -5,5 +5,9 @@ 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? + + error "Cannot disable local builds, no remote is set" if config["local"] == false && config["remote"].blank? end end diff --git a/lib/kamal/utils.rb b/lib/kamal/utils.rb index e6b28e43..46736ba5 100644 --- a/lib/kamal/utils.rb +++ b/lib/kamal/utils.rb @@ -77,4 +77,20 @@ 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 + + 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/lib/kamal/version.rb b/lib/kamal/version.rb index 524b325b..4c771efd 100644 --- a/lib/kamal/version.rb +++ b/lib/kamal/version.rb @@ -1,3 +1,3 @@ module Kamal - VERSION = "1.8.1" + VERSION = "2.0.0.alpha" 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/build_test.rb b/test/cli/build_test.rb index 18ff254b..4259fa5b 100644 --- a/test/cli/build_test.rb +++ b/test/cli/build_test.rb @@ -21,16 +21,12 @@ class CliBuildTest < CliTestCase .with(:git, "-C", anything, :status, "--porcelain") .returns("") - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :buildx, :inspect, "kamal-app-multiarch", "> /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 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 --builder kamal-local-docker-container -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile \. as .*@localhost/, output end end end @@ -42,7 +38,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,9 +46,10 @@ 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", ".") + .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) @@ -77,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-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 --builder kamal-local-docker-container -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile . as .*@localhost/, output end end @@ -88,7 +85,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) @@ -123,10 +120,13 @@ 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, :rm, "kamal-local-docker-container") - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :buildx, :inspect, "kamal-app-multiarch", "> /dev/null") + SSHKit::Backend::Abstract.any_instance.expects(:execute) + .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-docker-container") .raises(SSHKit::Command::Failed.new("no builder")) SSHKit::Backend::Abstract.any_instance.expects(:execute).with { |*args| args.first.start_with?("git") } @@ -140,7 +140,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", "--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 @@ -164,7 +164,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 @@ -206,23 +206,32 @@ 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-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-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-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-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-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 @@ -239,7 +248,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 @@ -249,7 +258,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/cli_test_case.rb b/test/cli/cli_test_case.rb index 4c6b491d..c522a32f 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-docker-container") end def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: false) @@ -63,4 +62,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..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-app-multiarch", "> /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-app-multiarch", "> /dev/null") - .returns("") - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:docker, :info, "--format '{{index .RegistryConfig.Mirrors 0}}'") .returns("") @@ -537,9 +529,47 @@ 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 + + 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") - 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/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/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 2f6f86b1..be6686c5 100644 --- a/test/commands/builder_test.rb +++ b/test/commands/builder_test.rb @@ -2,54 +2,62 @@ 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 multiarch by default" do + test "target linux/amd64 locally 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 --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 }) - assert_equal "native", builder.name + test "target specified arch locally by default" do + builder = new_builder_command(builder: { "arch" => [ "amd64" ] }) + 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 --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" } }) - assert_equal "native/cached", builder.name + test "build with caching" do + builder = new_builder_command(builder: { "cache" => { "type" => "gha" } }) + 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 --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 "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 + test "hybrid build if remote is set and building multiarch" do + builder = new_builder_command(builder: { "arch" => [ "amd64", "arm64" ], "remote" => "ssh://app@127.0.0.1", "cache" => { "type" => "gha" } }) + assert_equal "hybrid", builder.name assert_equal \ - "docker buildx build --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/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 "multiarch", builder.name + 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 --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-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 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 + test "target remote when remote set and arch is non local" do + builder = new_builder_command(builder: { "arch" => [ "#{remote_arch}" ], "remote" => "ssh://app@host", "cache" => { "type" => "gha" } }) + assert_equal "remote", builder.name assert_equal \ - "docker buildx build --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/#{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 + + test "target local when remote set and arch is local" do + builder = new_builder_command(builder: { "arch" => [ "#{local_arch}" ], "remote" => "ssh://app@host", "cache" => { "type" => "gha" } }) + assert_equal "local", builder.name + assert_equal \ + "docker buildx build --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 @@ -101,28 +109,21 @@ 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 --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 -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile . && docker push dhh/app:123 && docker push dhh/app:latest", - builder.push.join(" ") - 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 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 --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 -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 --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 @@ -138,76 +139,13 @@ 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 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 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo", builder.push.join(" ") end - test "native context build" do - builder = new_builder_command(builder: { "multiarch" => false, "context" => "./foo" }) - assert_equal \ - "docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo && docker push dhh/app:123 && docker push dhh/app:latest", - builder.push.join(" ") - end - - test "cached context build" do - builder = new_builder_command(builder: { "multiarch" => false, "context" => "./foo", "cache" => { "type" => "gha" } }) - assert_equal \ - "docker buildx build --push -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile ./foo", - builder.push.join(" ") - end - - test "remote context build" do - builder = new_builder_command(builder: { "remote" => { "arch" => "amd64" }, "context" => "./foo" }) - assert_equal \ - "docker buildx build --push --platform linux/amd64 --builder kamal-app-native-remote -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo", - builder.push.join(" ") - end - - 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 "", command.config_context_hosts.join(" ") - end - - test "native context hosts" do - command = new_builder_command(builder: { "multiarch" => false }) - assert_equal :true, command.context_hosts - 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-app-native-cached > /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-app-native-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(" ") @@ -215,10 +153,18 @@ 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 "#{Dir.tmpdir}/kamal-clones/app/kamal/" end + + def local_arch + Kamal::Utils.docker_arch + end + + def remote_arch + Kamal::Utils.docker_arch == "arm64" ? "amd64" : "arm64" + end end 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 092edd4f..0aab0216 100644 --- a/test/configuration/builder_test.rb +++ b/test/configuration/builder_test.rb @@ -4,28 +4,12 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase setup do @deploy = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, - servers: [ "1.1.1.1" ] + builder: { "arch" => "amd64" }, 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: {} - } - 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 @@ -37,41 +21,33 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase end test "pack? with pack builder" do - @deploy[:builder] = { "pack" => { "builder" => "heroku/builder:24" } } + @deploy[:builder] = { "arch" => "arm64", "pack" => { "builder" => "heroku/builder:24" } } assert config.builder.pack? end test "pack details" do - @deploy[:builder] = { "pack" => { "arch" => "amd64", "builder" => "heroku/builder:24", "buildpacks" => [ "heroku/ruby", "heroku/procfile" ] } } + @deploy[:builder] = { "arch" => "amd64", "pack" => { "builder" => "heroku/builder:24", "buildpacks" => [ "heroku/ruby", "heroku/procfile" ] } } - assert_equal "amd64", config.builder.pack_arch assert_equal "heroku/builder:24", config.builder.pack_builder assert_equal [ "heroku/ruby", "heroku/procfile" ], config.builder.pack_buildpacks 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", "host" => "unix:///Users/<%= `whoami`.strip %>/.docker/run/docker.sock" }, - "remote" => { "arch" => "amd64", "host" => "ssh://root@192.168.0.1" } + @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", 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 "unix:///Users/<%= `whoami`.strip %>/.docker/run/docker.sock", config_with_builder_option.builder.local_host + assert_equal [ "amd64", "arm64" ], config.builder.arches + assert_equal "ssh://root@192.168.0.1", config.builder.remote end test "cached?" do @@ -79,10 +55,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 @@ -95,32 +71,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 @@ -128,9 +104,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 @@ -138,9 +114,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 @@ -148,9 +124,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 @@ -158,9 +134,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 @@ -168,17 +144,30 @@ 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 + + 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) 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 b7bd6b6a..ac409e84 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 @@ -103,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_aliases.yml b/test/fixtures/deploy_with_aliases.yml new file mode 100644 index 00000000..ec7b14a0 --- /dev/null +++ b/test/fixtures/deploy_with_aliases.yml @@ -0,0 +1,23 @@ +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 +builder: + arch: amd64 +registry: + username: user + password: pw +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/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_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_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 44103499..d5db9632 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: <%= 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 d1e81836..1cfd5471 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: <%= Kamal::Utils.docker_arch == "arm64" ? "amd64" : "arm64" %> + remote: ssh://app@1.1.1.5:2122 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/.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/config/deploy.yml b/test/integration/docker/deployer/app/config/deploy.yml index 397a49ca..887de825 100644 --- a/test/integration/docker/deployer/app/config/deploy.yml +++ b/test/integration/docker/deployer/app/config/deploy.yml @@ -23,7 +23,8 @@ registry: username: root password: root builder: - multiarch: false + driver: docker + arch: <%= Kamal::Utils.docker_arch %> args: COMMIT_SHA: <%= `git rev-parse HEAD` %> healthcheck: 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..3b942665 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,8 @@ registry: username: root password: root builder: - multiarch: false + driver: docker + arch: <%= Kamal::Utils.docker_arch %> args: COMMIT_SHA: <%= `git rev-parse HEAD` %> healthcheck: @@ -37,3 +38,7 @@ accessories: - web stop_wait_time: 1 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 c4558c1d..a1e81493 100644 --- a/test/integration/main_test.rb +++ b/test/integration/main_test.rb @@ -77,11 +77,27 @@ 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", "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 + 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 + + output = kamal :uname, "-o", capture: true + assert_match "App Host: vm1\nGNU/Linux", output + end + test "setup and remove" do # Check remove completes when nothing has been setup yet kamal :remove, "-y"