diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f8094c05..67316a75 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,5 @@ name: CI -on: +on: push: branches: - main @@ -12,17 +12,29 @@ jobs: - "2.7" - "3.1" - "3.2" + - "3.3" gemfile: - Gemfile + - gemfiles/ruby_2.7.gemfile - gemfiles/rails_edge.gemfile - continue-on-error: [false] + exclude: + - ruby-version: "2.7" + gemfile: Gemfile + - ruby-version: "2.7" + gemfile: gemfiles/rails_edge.gemfile + - ruby-version: "3.1" + gemfile: gemfiles/ruby_2.7.gemfile + - ruby-version: "3.2" + gemfile: gemfiles/ruby_2.7.gemfile + - ruby-version: "3.3" + gemfile: gemfiles/ruby_2.7.gemfile name: ${{ format('Tests (Ruby {0})', matrix.ruby-version) }} runs-on: ubuntu-latest - continue-on-error: ${{ matrix.continue-on-error }} + continue-on-error: true env: BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install Ruby uses: ruby/setup-ruby@v1 diff --git a/Gemfile.lock b/Gemfile.lock index aba3b3e5..68e0225d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,8 +1,9 @@ PATH remote: . specs: - kamal (1.3.0) + kamal (1.3.1) activesupport (>= 7.0) + base64 (~> 0.2) bcrypt_pbkdf (~> 1.0) concurrent-ruby (~> 1.2) dotenv (~> 2.8) @@ -15,82 +16,111 @@ PATH GEM remote: https://rubygems.org/ specs: - actionpack (7.0.4.3) - actionview (= 7.0.4.3) - activesupport (= 7.0.4.3) - rack (~> 2.0, >= 2.2.0) + actionpack (7.1.2) + actionview (= 7.1.2) + activesupport (= 7.1.2) + nokogiri (>= 1.8.5) + racc + rack (>= 2.2.4) + rack-session (>= 1.0.1) rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actionview (7.0.4.3) - activesupport (= 7.0.4.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + actionview (7.1.2) + activesupport (= 7.1.2) builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activesupport (7.0.4.3) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activesupport (7.1.2) + base64 + bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) minitest (>= 5.1) + mutex_m tzinfo (~> 2.0) + base64 (0.2.0) bcrypt_pbkdf (1.1.0) + bigdecimal (3.1.5) builder (3.2.4) concurrent-ruby (1.2.2) + connection_pool (2.4.1) crass (1.0.6) - debug (1.7.2) - irb (>= 1.5.0) - reline (>= 0.3.1) + debug (1.9.1) + irb (~> 1.10) + reline (>= 0.3.8) dotenv (2.8.1) + drb (2.2.0) + ruby2_keywords ed25519 (1.3.0) erubi (1.12.0) - i18n (1.12.0) + i18n (1.14.1) concurrent-ruby (~> 1.0) - io-console (0.6.0) - irb (1.6.3) - reline (>= 0.3.0) - loofah (2.20.0) + io-console (0.7.1) + irb (1.11.0) + rdoc + reline (>= 0.3.8) + loofah (2.22.0) crass (~> 1.0.2) - nokogiri (>= 1.5.9) - method_source (1.0.0) - minitest (5.18.0) - mocha (2.0.2) + nokogiri (>= 1.12.0) + minitest (5.20.0) + mocha (2.1.0) ruby2_keywords (>= 0.0.5) + mutex_m (0.2.0) net-scp (4.0.0) net-ssh (>= 2.6.5, < 8.0.0) - net-ssh (7.1.0) - nokogiri (1.14.2-arm64-darwin) + net-ssh (7.2.1) + nokogiri (1.16.0-arm64-darwin) racc (~> 1.4) - nokogiri (1.14.2-x86_64-darwin) + nokogiri (1.16.0-x86_64-darwin) racc (~> 1.4) - nokogiri (1.14.2-x86_64-linux) + nokogiri (1.16.0-x86_64-linux) racc (~> 1.4) - racc (1.6.2) - rack (2.2.6.4) + psych (5.1.2) + stringio + racc (1.7.3) + rack (3.0.8) + rack-session (2.0.0) + rack (>= 3.0.0) rack-test (2.1.0) rack (>= 1.3) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + rackup (2.1.0) + rack (>= 3) + webrick (~> 1.8) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.5.0) - loofah (~> 2.19, >= 2.19.1) - railties (7.0.4.3) - actionpack (= 7.0.4.3) - activesupport (= 7.0.4.3) - method_source + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + railties (7.1.2) + actionpack (= 7.1.2) + activesupport (= 7.1.2) + irb + rackup (>= 1.0.0) rake (>= 12.2) - thor (~> 1.0) - zeitwerk (~> 2.5) - rake (13.0.6) - reline (0.3.3) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) + rake (13.1.0) + rdoc (6.6.2) + psych (>= 4.0.0) + reline (0.4.2) io-console (~> 0.5) ruby2_keywords (0.0.5) - sshkit (1.21.4) + sshkit (1.21.7) + mutex_m net-scp (>= 1.1.2) net-ssh (>= 2.8.0) - thor (1.2.1) + stringio (3.1.0) + thor (1.3.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - zeitwerk (2.6.7) + webrick (1.8.1) + zeitwerk (2.6.12) PLATFORMS arm64-darwin diff --git a/gemfiles/ruby_2.7.gemfile b/gemfiles/ruby_2.7.gemfile new file mode 100644 index 00000000..1463b323 --- /dev/null +++ b/gemfiles/ruby_2.7.gemfile @@ -0,0 +1,6 @@ +source 'https://rubygems.org' +git_source(:github) { |repo| "https://github.com/#{repo}.git" } + +gemspec path: "../" + +gem "nokogiri", "~> 1.15.0" diff --git a/kamal.gemspec b/kamal.gemspec index 8dcaa6ea..55f18ce9 100644 --- a/kamal.gemspec +++ b/kamal.gemspec @@ -20,6 +20,7 @@ Gem::Specification.new do |spec| spec.add_dependency "ed25519", "~> 1.2" spec.add_dependency "bcrypt_pbkdf", "~> 1.0" spec.add_dependency "concurrent-ruby", "~> 1.2" + spec.add_dependency "base64", "~> 0.2" spec.add_development_dependency "debug" spec.add_development_dependency "mocha" diff --git a/lib/kamal/cli/accessory.rb b/lib/kamal/cli/accessory.rb index 47bc1a06..b9cfee1f 100644 --- a/lib/kamal/cli/accessory.rb +++ b/lib/kamal/cli/accessory.rb @@ -5,11 +5,11 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base if name == "all" KAMAL.accessory_names.each { |accessory_name| boot(accessory_name) } else - with_accessory(name) do |accessory| + with_accessory(name) do |accessory, hosts| directories(name) upload(name) - on(accessory.hosts) do + on(hosts) do execute *KAMAL.registry.login if login execute *KAMAL.auditor.record("Booted #{name} accessory"), verbosity: :debug execute *accessory.run @@ -22,8 +22,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base desc "upload [NAME]", "Upload accessory files to host", hide: true def upload(name) mutating do - with_accessory(name) do |accessory| - on(accessory.hosts) do + with_accessory(name) do |accessory, hosts| + on(hosts) do accessory.files.each do |(local, remote)| accessory.ensure_local_file_present(local) @@ -39,8 +39,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base desc "directories [NAME]", "Create accessory directories on host", hide: true def directories(name) mutating do - with_accessory(name) do |accessory| - on(accessory.hosts) do + with_accessory(name) do |accessory, hosts| + on(hosts) do accessory.directories.keys.each do |host_path| execute *accessory.make_directory(host_path) end @@ -55,8 +55,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base if name == "all" KAMAL.accessory_names.each { |accessory_name| reboot(accessory_name) } else - with_accessory(name) do |accessory| - on(accessory.hosts) do + with_accessory(name) do |accessory, hosts| + on(hosts) do execute *KAMAL.registry.login end @@ -71,8 +71,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base desc "start [NAME]", "Start existing accessory container on host" def start(name) mutating do - with_accessory(name) do |accessory| - on(accessory.hosts) do + with_accessory(name) do |accessory, hosts| + on(hosts) do execute *KAMAL.auditor.record("Started #{name} accessory"), verbosity: :debug execute *accessory.start end @@ -83,8 +83,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base desc "stop [NAME]", "Stop existing accessory container on host" def stop(name) mutating do - with_accessory(name) do |accessory| - on(accessory.hosts) do + with_accessory(name) do |accessory, hosts| + on(hosts) do execute *KAMAL.auditor.record("Stopped #{name} accessory"), verbosity: :debug execute *accessory.stop, raise_on_non_zero_exit: false end @@ -107,8 +107,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base if name == "all" KAMAL.accessory_names.each { |accessory_name| details(accessory_name) } else - with_accessory(name) do |accessory| - on(accessory.hosts) { puts capture_with_info(*accessory.info) } + with_accessory(name) do |accessory, hosts| + on(hosts) { puts capture_with_info(*accessory.info) } end end end @@ -117,7 +117,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base 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" def exec(name, cmd) - with_accessory(name) do |accessory| + with_accessory(name) do |accessory, hosts| case when options[:interactive] && options[:reuse] say "Launching interactive command with via SSH from existing container...", :magenta @@ -129,14 +129,14 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base when options[:reuse] say "Launching command from existing container...", :magenta - on(accessory.hosts) do + on(hosts) do execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug capture_with_info(*accessory.execute_in_existing_container(cmd)) end else say "Launching command from new container...", :magenta - on(accessory.hosts) do + on(hosts) do execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug capture_with_info(*accessory.execute_in_new_container(cmd)) end @@ -150,12 +150,12 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)" option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)" def logs(name) - with_accessory(name) do |accessory| + with_accessory(name) do |accessory, hosts| grep = options[:grep] if options[:follow] run_locally do - info "Following logs on #{accessory.hosts}..." + info "Following logs on #{hosts}..." info accessory.follow_logs(grep: grep) exec accessory.follow_logs(grep: grep) end @@ -163,7 +163,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base since = options[:since] lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set - on(accessory.hosts) do + on(hosts) do puts capture_with_info(*accessory.logs(since: since, lines: lines, grep: grep)) end end @@ -192,8 +192,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base desc "remove_container [NAME]", "Remove accessory container from host", hide: true def remove_container(name) mutating do - with_accessory(name) do |accessory| - on(accessory.hosts) do + with_accessory(name) do |accessory, hosts| + on(hosts) do execute *KAMAL.auditor.record("Remove #{name} accessory container"), verbosity: :debug execute *accessory.remove_container end @@ -204,8 +204,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base desc "remove_image [NAME]", "Remove accessory image from host", hide: true def remove_image(name) mutating do - with_accessory(name) do |accessory| - on(accessory.hosts) do + with_accessory(name) do |accessory, hosts| + on(hosts) do execute *KAMAL.auditor.record("Removed #{name} accessory image"), verbosity: :debug execute *accessory.remove_image end @@ -216,8 +216,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base desc "remove_service_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host", hide: true def remove_service_directory(name) mutating do - with_accessory(name) do |accessory| - on(accessory.hosts) do + with_accessory(name) do |accessory, hosts| + on(hosts) do execute *accessory.remove_service_directory end end @@ -227,7 +227,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base private def with_accessory(name) if accessory = KAMAL.accessory(name) - yield accessory + yield accessory, accessory_hosts(accessory) else error_on_missing_accessory(name) end @@ -240,4 +240,12 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base "No accessory by the name of '#{name}'" + (options ? " (options: #{options.to_sentence})" : "") end + + def accessory_hosts(accessory) + if KAMAL.specific_hosts&.any? + KAMAL.specific_hosts & accessory.hosts + else + accessory.hosts + end + end end diff --git a/lib/kamal/cli/prune.rb b/lib/kamal/cli/prune.rb index 236c7d55..498e4ec4 100644 --- a/lib/kamal/cli/prune.rb +++ b/lib/kamal/cli/prune.rb @@ -18,12 +18,16 @@ class Kamal::Cli::Prune < Kamal::Cli::Base end end - desc "containers", "Prune all stopped containers, except the last 5" + desc "containers", "Prune all stopped containers, except the last n (default 5)" + option :retain, type: :numeric, default: nil, desc: "Number of containers to retain" def containers + retain = options.fetch(:retain, KAMAL.config.retain_containers) + raise "retain must be at least 1" if retain < 1 + mutating do on(KAMAL.hosts) do execute *KAMAL.auditor.record("Pruned containers"), verbosity: :debug - execute *KAMAL.prune.app_containers + execute *KAMAL.prune.app_containers(retain: retain) execute *KAMAL.prune.healthcheck_containers end end diff --git a/lib/kamal/commands/builder/base.rb b/lib/kamal/commands/builder/base.rb index 8723fa45..f710807c 100644 --- a/lib/kamal/commands/builder/base.rb +++ b/lib/kamal/commands/builder/base.rb @@ -3,7 +3,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base class BuilderError < StandardError; end delegate :argumentize, to: Kamal::Utils - delegate :args, :secrets, :dockerfile, :local_arch, :local_host, :remote_arch, :remote_host, :cache_from, :cache_to, to: :builder_config + delegate :args, :secrets, :dockerfile, :local_arch, :local_host, :remote_arch, :remote_host, :cache_from, :cache_to, :ssh, to: :builder_config def clean docker :image, :rm, "--force", config.absolute_image @@ -14,7 +14,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base end def build_options - [ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile ] + [ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_ssh ] end def build_context @@ -60,6 +60,10 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base end end + def build_ssh + argumentize "--ssh", ssh if ssh.present? + end + def builder_config config.builder end diff --git a/lib/kamal/commands/builder/multiarch.rb b/lib/kamal/commands/builder/multiarch.rb index 458c4ac6..2f9e4d19 100644 --- a/lib/kamal/commands/builder/multiarch.rb +++ b/lib/kamal/commands/builder/multiarch.rb @@ -10,7 +10,7 @@ class Kamal::Commands::Builder::Multiarch < Kamal::Commands::Builder::Base def push docker :buildx, :build, "--push", - "--platform", "linux/amd64,linux/arm64", + "--platform", platform_names, "--builder", builder_name, *build_options, build_context @@ -26,4 +26,12 @@ class Kamal::Commands::Builder::Multiarch < Kamal::Commands::Builder::Base 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/lock.rb b/lib/kamal/commands/lock.rb index 82340714..607bd79d 100644 --- a/lib/kamal/commands/lock.rb +++ b/lib/kamal/commands/lock.rb @@ -1,5 +1,6 @@ require "active_support/duration" require "time" +require "base64" class Kamal::Commands::Lock < Kamal::Commands::Base def acquire(message, version) diff --git a/lib/kamal/commands/prune.rb b/lib/kamal/commands/prune.rb index f9f37b24..40f7dfc4 100644 --- a/lib/kamal/commands/prune.rb +++ b/lib/kamal/commands/prune.rb @@ -13,10 +13,10 @@ class Kamal::Commands::Prune < Kamal::Commands::Base "while read image tag; do docker rmi $tag; done" end - def app_containers(keep_last: 5) + def app_containers(retain:) pipe \ docker(:ps, "-q", "-a", *service_filter, *stopped_containers_filters), - "tail -n +#{keep_last + 1}", + "tail -n +#{retain + 1}", "while read container_id; do docker rm $container_id; done" end diff --git a/lib/kamal/commands/traefik.rb b/lib/kamal/commands/traefik.rb index c6ec8c8d..bb8f048b 100644 --- a/lib/kamal/commands/traefik.rb +++ b/lib/kamal/commands/traefik.rb @@ -1,7 +1,7 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base delegate :argumentize, :optionize, to: Kamal::Utils - DEFAULT_IMAGE = "traefik:v2.9" + DEFAULT_IMAGE = "traefik:v2.10" CONTAINER_PORT = 80 DEFAULT_ARGS = { 'log.level' => 'DEBUG' diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index e5ecbb53..a3b074d5 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -127,6 +127,10 @@ class Kamal::Configuration raw_config.require_destination end + def retain_containers + raw_config.retain_containers || 5 + end + def volume_args if raw_config.volumes.present? @@ -218,7 +222,7 @@ class Kamal::Configuration def valid? - ensure_destination_if_required && ensure_required_keys_present && ensure_valid_kamal_version + ensure_destination_if_required && ensure_required_keys_present && ensure_valid_kamal_version && ensure_retain_containers_valid end def to_h @@ -291,6 +295,12 @@ class Kamal::Configuration true end + def ensure_retain_containers_valid + raise ArgumentError, "Must retain at least 1 container" if retain_containers < 1 + + true + end + def role_names raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort diff --git a/lib/kamal/configuration/accessory.rb b/lib/kamal/configuration/accessory.rb index 8fdcd227..d26c6c85 100644 --- a/lib/kamal/configuration/accessory.rb +++ b/lib/kamal/configuration/accessory.rb @@ -8,7 +8,7 @@ class Kamal::Configuration::Accessory end def service_name - "#{config.service}-#{name}" + specifics["service"] || "#{config.service}-#{name}" end def image diff --git a/lib/kamal/configuration/builder.rb b/lib/kamal/configuration/builder.rb index c7d81922..9daa3138 100644 --- a/lib/kamal/configuration/builder.rb +++ b/lib/kamal/configuration/builder.rb @@ -81,6 +81,10 @@ class Kamal::Configuration::Builder end end + def ssh + @options["ssh"] + end + private def valid? if @options["cache"] && @options["cache"]["type"] diff --git a/lib/kamal/sshkit_with_ext.rb b/lib/kamal/sshkit_with_ext.rb index eb8f359b..e0c62c3a 100644 --- a/lib/kamal/sshkit_with_ext.rb +++ b/lib/kamal/sshkit_with_ext.rb @@ -1,5 +1,6 @@ require "sshkit" require "sshkit/dsl" +require "net/scp" require "active_support/core_ext/hash/deep_merge" require "json" diff --git a/lib/kamal/version.rb b/lib/kamal/version.rb index e9f06ea9..59756e50 100644 --- a/lib/kamal/version.rb +++ b/lib/kamal/version.rb @@ -1,3 +1,3 @@ module Kamal - VERSION = "1.3.0" + VERSION = "1.3.1" end diff --git a/test/cli/accessory_test.rb b/test/cli/accessory_test.rb index c4cf4828..d8f3fc63 100644 --- a/test/cli/accessory_test.rb +++ b/test/cli/accessory_test.rb @@ -148,6 +148,30 @@ class CliAccessoryTest < CliTestCase assert_match "rm -rf app-mysql", run_command("remove_service_directory", "mysql") end + test "hosts param respected" do + Kamal::Cli::Accessory.any_instance.expects(:directories).with("redis") + Kamal::Cli::Accessory.any_instance.expects(:upload).with("redis") + + run_command("boot", "redis", "--hosts", "1.1.1.1").tap do |output| + assert_match /docker login.*on 1.1.1.1/, output + refute_match /docker login.*on 1.1.1.2/, output + assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output + refute_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.2", output + end + end + + test "hosts param intersected with configuration" do + Kamal::Cli::Accessory.any_instance.expects(:directories).with("redis") + Kamal::Cli::Accessory.any_instance.expects(:upload).with("redis") + + run_command("boot", "redis", "--hosts", "1.1.1.1,1.1.1.3").tap do |output| + assert_match /docker login.*on 1.1.1.1/, output + refute_match /docker login.*on 1.1.1.3/, output + assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output + refute_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.3", output + end + end + private def run_command(*command) stdouted { Kamal::Cli::Accessory.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) } diff --git a/test/cli/prune_test.rb b/test/cli/prune_test.rb index 77c3ebd4..11acc469 100644 --- a/test/cli/prune_test.rb +++ b/test/cli/prune_test.rb @@ -20,6 +20,15 @@ class CliPruneTest < CliTestCase assert_match /docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +6 | while read container_id; do docker rm $container_id; done on 1.1.1.\d/, output assert_match /docker container prune --force --filter label=service=healthcheck-app on 1.1.1.\d/, output end + + run_command("containers", "--retain", "10").tap do |output| + assert_match /docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +11 | while read container_id; do docker rm $container_id; done on 1.1.1.\d/, output + assert_match /docker container prune --force --filter label=service=healthcheck-app on 1.1.1.\d/, output + end + + assert_raises(RuntimeError, "retain must be at least 1") do + run_command("containers", "--retain", "0") + end end private diff --git a/test/commands/accessory_test.rb b/test/commands/accessory_test.rb index 687c7fe5..92ed1211 100644 --- a/test/commands/accessory_test.rb +++ b/test/commands/accessory_test.rb @@ -34,6 +34,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase ] }, "busybox" => { + "service" => "custom-busybox", "image" => "busybox:latest", "host" => "1.1.1.7" } @@ -57,7 +58,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase new_command(:redis).run.join(" ") assert_equal \ - "docker run --name app-busybox --detach --restart unless-stopped --log-opt max-size=\"10m\" --env-file .kamal/env/accessories/app-busybox.env --label service=\"app-busybox\" busybox:latest", + "docker run --name custom-busybox --detach --restart unless-stopped --log-opt max-size=\"10m\" --env-file .kamal/env/accessories/custom-busybox.env --label service=\"custom-busybox\" busybox:latest", new_command(:busybox).run.join(" ") end @@ -65,7 +66,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase @config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } } assert_equal \ - "docker run --name app-busybox --detach --restart unless-stopped --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --env-file .kamal/env/accessories/app-busybox.env --label service=\"app-busybox\" busybox:latest", + "docker run --name custom-busybox --detach --restart unless-stopped --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --env-file .kamal/env/accessories/custom-busybox.env --label service=\"custom-busybox\" busybox:latest", new_command(:busybox).run.join(" ") end diff --git a/test/commands/builder_test.rb b/test/commands/builder_test.rb index c0256113..51bb837c 100644 --- a/test/commands/builder_test.rb +++ b/test/commands/builder_test.rb @@ -37,6 +37,14 @@ class CommandsBuilderTest < ActiveSupport::TestCase builder.push.join(" ") end + test "target multiarch local when arch is set" do + builder = new_builder_command(builder: { "local" => { "arch" => "amd64" } }) + assert_equal "multiarch", 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 .", + 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 @@ -103,6 +111,14 @@ class CommandsBuilderTest < ActiveSupport::TestCase builder.push.join(" ") end + test "build with ssh agent socket" do + builder = new_builder_command(builder: { "ssh" => 'default=$SSH_AUTH_SOCK' }) + + assert_equal \ + "-t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile --ssh default=$SSH_AUTH_SOCK", + builder.target.build_options.join(" ") + end + test "validate image" do 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 diff --git a/test/commands/prune_test.rb b/test/commands/prune_test.rb index 00db4ccc..c4a56a9a 100644 --- a/test/commands/prune_test.rb +++ b/test/commands/prune_test.rb @@ -23,7 +23,11 @@ class CommandsPruneTest < ActiveSupport::TestCase test "app containers" do assert_equal \ "docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +6 | while read container_id; do docker rm $container_id; done", - new_command.app_containers.join(" ") + new_command.app_containers(retain: 5).join(" ") + + assert_equal \ + "docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +4 | while read container_id; do docker rm $container_id; done", + new_command.app_containers(retain: 3).join(" ") end test "healthcheck containers" do diff --git a/test/configuration/accessory_test.rb b/test/configuration/accessory_test.rb index 522dc631..4b56be51 100644 --- a/test/configuration/accessory_test.rb +++ b/test/configuration/accessory_test.rb @@ -49,6 +49,7 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase } }, "monitoring" => { + "service" => "custom-monitoring", "image" => "monitoring:latest", "roles" => [ "web" ], "port" => "4321:4321", @@ -72,6 +73,7 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase test "service name" do assert_equal "app-mysql", @config.accessory(:mysql).service_name assert_equal "app-redis", @config.accessory(:redis).service_name + assert_equal "custom-monitoring", @config.accessory(:monitoring).service_name end test "port" do diff --git a/test/configuration/builder_test.rb b/test/configuration/builder_test.rb index da30d915..4f4bbc40 100644 --- a/test/configuration/builder_test.rb +++ b/test/configuration/builder_test.rb @@ -148,4 +148,14 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase assert_equal "..", @config_with_builder_option.builder.context end + + test "ssh" do + assert_nil @config.builder.ssh + end + + test "setting ssh params" do + @deploy_with_builder_option[:builder] = { "ssh" => 'default=$SSH_AUTH_SOCK' } + + assert_equal 'default=$SSH_AUTH_SOCK', @config_with_builder_option.builder.ssh + end end diff --git a/test/configuration_test.rb b/test/configuration_test.rb index 392b6afb..fbc87a91 100644 --- a/test/configuration_test.rb +++ b/test/configuration_test.rb @@ -299,7 +299,7 @@ class ConfigurationTest < ActiveSupport::TestCase assert_equal "alternate_web", config.primary_role assert_equal "1.1.1.4", config.primary_host - assert config.role(:alternate_web).primary? + assert config.role(:alternate_web).primary? assert config.role(:alternate_web).running_traefik? end @@ -309,4 +309,12 @@ class ConfigurationTest < ActiveSupport::TestCase end assert_match /bar isn't defined/, error.message end + + test "retain_containers" do + assert_equal 5, @config.retain_containers + config = Kamal::Configuration.new(@deploy_with_roles.merge(retain_containers: 2)) + assert_equal 2, config.retain_containers + + assert_raises(ArgumentError) { Kamal::Configuration.new(@deploy_with_roles.merge(retain_containers: 0)) } + end end diff --git a/test/integration/docker/deployer/app/config/deploy.yml b/test/integration/docker/deployer/app/config/deploy.yml index ec070f6c..baedca99 100644 --- a/test/integration/docker/deployer/app/config/deploy.yml +++ b/test/integration/docker/deployer/app/config/deploy.yml @@ -24,9 +24,10 @@ traefik: args: accesslog: true accesslog.format: json - image: registry:4443/traefik:v2.9 + image: registry:4443/traefik:v2.10 accessories: busybox: + service: custom-busybox image: registry:4443/busybox:1.36.0 cmd: sh -c 'echo "Starting busybox..."; trap exit term; while true; do sleep 1; done' roles: diff --git a/test/integration/docker/deployer/setup.sh b/test/integration/docker/deployer/setup.sh index 50af91bf..0cd511d9 100755 --- a/test/integration/docker/deployer/setup.sh +++ b/test/integration/docker/deployer/setup.sh @@ -19,5 +19,9 @@ push_image_to_registry_4443() { install_kamal push_image_to_registry_4443 nginx 1-alpine-slim -push_image_to_registry_4443 traefik v2.9 +push_image_to_registry_4443 traefik v2.10 push_image_to_registry_4443 busybox 1.36.0 + +# .ssh is on a shared volume that persists between runs. Clean it up as the +# churn of temporary vm IPs can eventually create conflicts. +rm -f /root/.ssh/known_hosts diff --git a/test/integration/integration_test.rb b/test/integration/integration_test.rb index 950e41ed..7d620438 100644 --- a/test/integration/integration_test.rb +++ b/test/integration/integration_test.rb @@ -103,7 +103,7 @@ class IntegrationTest < ActiveSupport::TestCase assert_equal "200", code end - def wait_for_healthy(timeout: 20) + def wait_for_healthy(timeout: 30) timeout_at = Time.now + timeout while docker_compose("ps -a | tail -n +2 | grep -v '(healthy)' | wc -l", capture: true) != "0" if timeout_at < Time.now diff --git a/test/integration/main_test.rb b/test/integration/main_test.rb index a0364fb2..856781cb 100644 --- a/test/integration/main_test.rb +++ b/test/integration/main_test.rb @@ -32,7 +32,7 @@ class MainTest < IntegrationTest assert_match /Traefik Host: vm2/, details assert_match /App Host: vm1/, details assert_match /App Host: vm2/, details - assert_match /traefik:v2.9/, details + assert_match /traefik:v2.10/, details assert_match /registry:4443\/app:#{first_version}/, details audit = kamal :audit, capture: true diff --git a/test/integration/traefik_test.rb b/test/integration/traefik_test.rb index ed8a956f..6f735612 100644 --- a/test/integration/traefik_test.rb +++ b/test/integration/traefik_test.rb @@ -52,11 +52,11 @@ class TraefikTest < IntegrationTest private def assert_traefik_running - assert_match /traefik:v2.9 "\/entrypoint.sh/, traefik_details + assert_match /traefik:v2.10 "\/entrypoint.sh/, traefik_details end def assert_traefik_not_running - refute_match /traefik:v2.9 "\/entrypoint.sh/, traefik_details + refute_match /traefik:v2.10 "\/entrypoint.sh/, traefik_details end def traefik_details