diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8da7d39c..4e0358b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,16 +26,12 @@ jobs: fail-fast: false matrix: ruby-version: - - "3.1" - "3.2" - "3.3" - "3.4" gemfile: - Gemfile - gemfiles/rails_edge.gemfile - exclude: - - ruby-version: "3.1" - gemfile: gemfiles/rails_edge.gemfile name: ${{ format('Tests (Ruby {0})', matrix.ruby-version) }} runs-on: ubuntu-latest env: diff --git a/Gemfile.lock b/Gemfile.lock index b85c77fa..3fb94ec5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,13 +1,13 @@ PATH remote: . specs: - kamal (2.5.3) + kamal (2.6.1) activesupport (>= 7.0) base64 (~> 0.2) bcrypt_pbkdf (~> 1.0) concurrent-ruby (~> 1.2) dotenv (~> 3.1) - ed25519 (~> 1.2) + ed25519 (~> 1.4) net-ssh (~> 7.3) sshkit (>= 1.23.0, < 2.0) thor (~> 1.3) @@ -60,7 +60,7 @@ GEM reline (>= 0.3.8) dotenv (3.1.5) drb (2.2.1) - ed25519 (1.3.0) + ed25519 (1.4.0) erubi (1.13.0) i18n (1.14.6) concurrent-ruby (~> 1.0) diff --git a/kamal.gemspec b/kamal.gemspec index 7218f865..2d66d09a 100644 --- a/kamal.gemspec +++ b/kamal.gemspec @@ -17,7 +17,7 @@ Gem::Specification.new do |spec| spec.add_dependency "thor", "~> 1.3" spec.add_dependency "dotenv", "~> 3.1" spec.add_dependency "zeitwerk", ">= 2.6.18", "< 3.0" - spec.add_dependency "ed25519", "~> 1.2" + spec.add_dependency "ed25519", "~> 1.4" spec.add_dependency "bcrypt_pbkdf", "~> 1.0" spec.add_dependency "concurrent-ruby", "~> 1.2" spec.add_dependency "base64", "~> 0.2" diff --git a/lib/kamal/cli/build.rb b/lib/kamal/cli/build.rb index b9b6a5d5..2327943e 100644 --- a/lib/kamal/cli/build.rb +++ b/lib/kamal/cli/build.rb @@ -14,6 +14,10 @@ class Kamal::Cli::Build < Kamal::Cli::Base def push cli = self + # Ensure pre-connect hooks run before the build, they may needed for a remote builder + # or the pre-build hooks. + pre_connect_if_required + ensure_docker_installed login_to_registry_locally diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index cd4e45d9..0e00af6c 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -31,6 +31,8 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base option :registry, type: :string, default: nil, desc: "Registry to use for the proxy image" option :repository, type: :string, default: nil, desc: "Repository for the proxy image" option :image_version, type: :string, default: nil, desc: "Version of the proxy to run" + option :metrics_port, type: :numeric, default: nil, desc: "Port to report prometheus metrics on" + option :debug, type: :boolean, default: false, desc: "Whether to run the proxy in debug mode" option :docker_options, type: :array, default: [], desc: "Docker options to pass to the proxy container", banner: "option=value option2=value2" def boot_config(subcommand) proxy_boot_config = KAMAL.config.proxy_boot @@ -40,6 +42,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base boot_options = [ *(proxy_boot_config.publish_args(options[:http_port], options[:https_port], options[:publish_host_ip]) if options[:publish]), *(proxy_boot_config.logging_args(options[:log_max_size])), + *("--expose=#{options[:metrics_port]}" if options[:metrics_port]), *options[:docker_options].map { |option| "--#{option}" } ] @@ -51,6 +54,9 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base image_version = options[:image_version] + run_command_options = { debug: options[:debug] || nil, "metrics-port": options[:metrics_port] }.compact + run_command = "kamal-proxy run #{Kamal::Utils.optionize(run_command_options).join(" ")}" if run_command_options.any? + on(KAMAL.proxy_hosts) do |host| execute(*KAMAL.proxy.ensure_proxy_directory) if boot_options != proxy_boot_config.default_boot_options @@ -70,6 +76,12 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base else execute *KAMAL.proxy.reset_image_version, raise_on_non_zero_exit: false end + + if run_command + upload! StringIO.new(run_command), proxy_boot_config.run_command_file + else + execute *KAMAL.proxy.reset_run_command, raise_on_non_zero_exit: false + end end when "get" @@ -81,6 +93,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base execute *KAMAL.proxy.reset_boot_options, raise_on_non_zero_exit: false execute *KAMAL.proxy.reset_image, raise_on_non_zero_exit: false execute *KAMAL.proxy.reset_image_version, raise_on_non_zero_exit: false + execute *KAMAL.proxy.reset_run_command, raise_on_non_zero_exit: false end else raise ArgumentError, "Unknown boot_config subcommand #{subcommand}" @@ -107,18 +120,6 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base execute *KAMAL.proxy.ensure_apps_config_directory execute *KAMAL.proxy.run - - KAMAL.roles_on(host).select(&:running_proxy?).each do |role| - app = KAMAL.app(role: role, host: host) - - version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip - endpoint = capture_with_info(*app.container_id_for_version(version)).strip - - if endpoint.present? - info "Deploying #{endpoint} for role `#{role}` on #{host}..." - execute *app.deploy(target: endpoint) - end - end end run_hook "post-proxy-reboot", hosts: host_list end diff --git a/lib/kamal/cli/templates/sample_hooks/post-deploy.sample b/lib/kamal/cli/templates/sample_hooks/post-deploy.sample index 75efafc1..fd364c2a 100755 --- a/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +++ b/lib/kamal/cli/templates/sample_hooks/post-deploy.sample @@ -7,7 +7,7 @@ # KAMAL_PERFORMER # KAMAL_VERSION # KAMAL_HOSTS -# KAMAL_ROLE (if set) +# KAMAL_ROLES (if set) # KAMAL_DESTINATION (if set) # KAMAL_RUNTIME diff --git a/lib/kamal/cli/templates/sample_hooks/pre-build.sample b/lib/kamal/cli/templates/sample_hooks/pre-build.sample index f87d8113..c5a55678 100755 --- a/lib/kamal/cli/templates/sample_hooks/pre-build.sample +++ b/lib/kamal/cli/templates/sample_hooks/pre-build.sample @@ -13,7 +13,7 @@ # KAMAL_PERFORMER # KAMAL_VERSION # KAMAL_HOSTS -# KAMAL_ROLE (if set) +# KAMAL_ROLES (if set) # KAMAL_DESTINATION (if set) if [ -n "$(git status --porcelain)" ]; then diff --git a/lib/kamal/cli/templates/sample_hooks/pre-connect.sample b/lib/kamal/cli/templates/sample_hooks/pre-connect.sample index 18e61d7e..77744bdc 100755 --- a/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +++ b/lib/kamal/cli/templates/sample_hooks/pre-connect.sample @@ -9,7 +9,7 @@ # KAMAL_PERFORMER # KAMAL_VERSION # KAMAL_HOSTS -# KAMAL_ROLE (if set) +# KAMAL_ROLES (if set) # KAMAL_DESTINATION (if set) # KAMAL_RUNTIME diff --git a/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample b/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample index 06b86aa1..665197f1 100755 --- a/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +++ b/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample @@ -13,7 +13,7 @@ # KAMAL_HOSTS # KAMAL_COMMAND # KAMAL_SUBCOMMAND -# KAMAL_ROLE (if set) +# KAMAL_ROLES (if set) # KAMAL_DESTINATION (if set) # Only check the build status for production deployments diff --git a/lib/kamal/commander/specifics.rb b/lib/kamal/commander/specifics.rb index e609baba..d61f6b28 100644 --- a/lib/kamal/commander/specifics.rb +++ b/lib/kamal/commander/specifics.rb @@ -11,7 +11,7 @@ class Kamal::Commander::Specifics @primary_role = primary_or_first_role(roles_on(primary_host)) stable_sort!(roles) { |role| role == primary_role ? 0 : 1 } - stable_sort!(hosts) { |host| roles_on(host).any? { |role| role == primary_role } ? 0 : 1 } + sort_primary_role_hosts_first!(hosts) end def roles_on(host) @@ -19,7 +19,7 @@ class Kamal::Commander::Specifics end def app_hosts - config.app_hosts & specified_hosts + @app_hosts ||= sort_primary_role_hosts_first!(config.app_hosts & specified_hosts) end def proxy_hosts @@ -55,4 +55,8 @@ class Kamal::Commander::Specifics specified_hosts end end + + def sort_primary_role_hosts_first!(hosts) + stable_sort!(hosts) { |host| roles_on(host).any? { |role| role == primary_role } ? 0 : 1 } + end end diff --git a/lib/kamal/commands/proxy.rb b/lib/kamal/commands/proxy.rb index 2193fff7..7699dde2 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -70,7 +70,7 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base end def boot_config - [ :echo, "#{substitute(read_boot_options)} #{substitute(read_image)}:#{substitute(read_image_version)}" ] + [ :echo, "#{substitute(read_boot_options)} #{substitute(read_image)}:#{substitute(read_image_version)} #{substitute(read_run_command)}" ] end def read_boot_options @@ -85,6 +85,10 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base read_file(config.proxy_boot.image_version_file, default: Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION) end + def read_run_command + read_file(config.proxy_boot.run_command_file) + end + def reset_boot_options remove_file config.proxy_boot.options_file end @@ -97,6 +101,10 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base remove_file config.proxy_boot.image_version_file end + def reset_run_command + remove_file config.proxy_boot.run_command_file + end + private def container_name config.proxy_boot.container_name diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index d057c89a..60526800 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -63,7 +63,7 @@ class Kamal::Configuration @env = Env.new(config: @raw_config.env || {}, secrets: secrets) @logging = Logging.new(logging_config: @raw_config.logging) - @proxy = Proxy.new(config: self, proxy_config: @raw_config.key?(:proxy) ? @raw_config.proxy : {}, secrets: secrets) + @proxy = Proxy.new(config: self, proxy_config: @raw_config.proxy, secrets: secrets) @proxy_boot = Proxy::Boot.new(config: self) @ssh = Ssh.new(config: self) @sshkit = Sshkit.new(config: self) diff --git a/lib/kamal/configuration/accessory.rb b/lib/kamal/configuration/accessory.rb index fd410ab1..c5bdc93d 100644 --- a/lib/kamal/configuration/accessory.rb +++ b/lib/kamal/configuration/accessory.rb @@ -32,7 +32,7 @@ class Kamal::Configuration::Accessory end def hosts - hosts_from_host || hosts_from_hosts || hosts_from_roles + hosts_from_host || hosts_from_hosts || hosts_from_roles || hosts_from_tags end def port @@ -202,11 +202,31 @@ class Kamal::Configuration::Accessory end def hosts_from_roles - if accessory_config.key?("roles") + if accessory_config.key?("role") + config.role(accessory_config["role"])&.hosts + elsif accessory_config.key?("roles") accessory_config["roles"].flat_map { |role| config.role(role)&.hosts } end end + def hosts_from_tags + if accessory_config.key?("tag") + extract_hosts_from_config_with_tag(accessory_config["tag"]) + elsif accessory_config.key?("tags") + accessory_config["tags"].flat_map { |tag| extract_hosts_from_config_with_tag(tag) } + end + end + + def extract_hosts_from_config_with_tag(tag) + if (servers_with_roles = config.raw_config.servers).is_a?(Hash) + servers_with_roles.flat_map do |role, servers_in_role| + servers_in_role.filter_map do |host| + host.keys.first if host.is_a?(Hash) && host.values.first.include?(tag) + end + end + end + end + def network accessory_config["network"] || DEFAULT_NETWORK end @@ -214,6 +234,8 @@ class Kamal::Configuration::Accessory def ensure_valid_roles if accessory_config["roles"] && (missing_roles = accessory_config["roles"] - config.roles.map(&:name)).any? raise Kamal::ConfigurationError, "accessories/#{name}: unknown roles #{missing_roles.join(", ")}" + elsif accessory_config["role"] && !config.role(accessory_config["role"]) + raise Kamal::ConfigurationError, "accessories/#{name}: unknown role #{accessory_config["role"]}" end end end diff --git a/lib/kamal/configuration/docs/accessory.yml b/lib/kamal/configuration/docs/accessory.yml index 1852681e..3983211a 100644 --- a/lib/kamal/configuration/docs/accessory.yml +++ b/lib/kamal/configuration/docs/accessory.yml @@ -46,13 +46,18 @@ accessories: # Accessory hosts # - # Specify one of `host`, `hosts`, or `roles`: + # Specify one of `host`, `hosts`, `role`, `roles`, `tag` or `tags`: host: mysql-db1 hosts: - mysql-db1 - mysql-db2 + role: mysql roles: - mysql + tag: writer + tags: + - writer + - reader # Custom command # diff --git a/lib/kamal/configuration/docs/proxy.yml b/lib/kamal/configuration/docs/proxy.yml index 5f63c6ff..0a471148 100644 --- a/lib/kamal/configuration/docs/proxy.yml +++ b/lib/kamal/configuration/docs/proxy.yml @@ -10,11 +10,6 @@ # They are application-specific, so they are not shared when multiple applications # run on the same proxy. # -# The proxy is enabled by default on the primary role but can be disabled by -# setting `proxy: false`. -# -# It is disabled by default on all other roles but can be enabled by setting -# `proxy: true` or providing a proxy configuration. proxy: # Hosts # @@ -126,3 +121,30 @@ proxy: response_headers: - X-Request-ID - X-Request-Start + +# Enabling/disabling the proxy on roles +# +# The proxy is enabled by default on the primary role but can be disabled by +# setting `proxy: false` in the primary role's configuration. +# +# ```yaml +# servers: +# web: +# hosts: +# - ... +# proxy: false +# ``` +# +# It is disabled by default on all other roles but can be enabled by setting +# `proxy: true` or providing a proxy configuration for that role. +# +# ```yaml +# servers: +# web: +# hosts: +# - ... +# web2: +# hosts: +# - ... +# proxy: true +# ``` diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb index 0f24fbe5..2c3617b2 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -11,6 +11,7 @@ class Kamal::Configuration::Proxy def initialize(config:, proxy_config:, secrets:, context: "proxy") @config = config @proxy_config = proxy_config + @proxy_config = {} if @proxy_config.nil? @secrets = secrets validate! @proxy_config, with: Kamal::Configuration::Validator::Proxy, context: context end diff --git a/lib/kamal/configuration/proxy/boot.rb b/lib/kamal/configuration/proxy/boot.rb index a4f4e83c..4f724d28 100644 --- a/lib/kamal/configuration/proxy/boot.rb +++ b/lib/kamal/configuration/proxy/boot.rb @@ -1,5 +1,5 @@ class Kamal::Configuration::Proxy::Boot - MINIMUM_VERSION = "v0.8.7" + MINIMUM_VERSION = "v0.9.0" DEFAULT_HTTP_PORT = 80 DEFAULT_HTTPS_PORT = 443 DEFAULT_LOG_MAX_SIZE = "10m" @@ -66,6 +66,10 @@ class Kamal::Configuration::Proxy::Boot File.join host_directory, "image_version" end + def run_command_file + File.join host_directory, "run_command" + end + def apps_directory File.join host_directory, "apps-config" end diff --git a/lib/kamal/configuration/validator/accessory.rb b/lib/kamal/configuration/validator/accessory.rb index 8b3d5b71..564cfc7e 100644 --- a/lib/kamal/configuration/validator/accessory.rb +++ b/lib/kamal/configuration/validator/accessory.rb @@ -2,8 +2,8 @@ class Kamal::Configuration::Validator::Accessory < Kamal::Configuration::Validat def validate! super - if (config.keys & [ "host", "hosts", "roles" ]).size != 1 - error "specify one of `host`, `hosts` or `roles`" + if (config.keys & [ "host", "hosts", "role", "roles", "tag", "tags" ]).size != 1 + error "specify one of `host`, `hosts`, `role`, `roles`, `tag` or `tags`" end validate_docker_options!(config["options"]) diff --git a/lib/kamal/version.rb b/lib/kamal/version.rb index 3099a7cc..d9029c54 100644 --- a/lib/kamal/version.rb +++ b/lib/kamal/version.rb @@ -1,3 +1,3 @@ module Kamal - VERSION = "2.5.3" + VERSION = "2.6.1" end diff --git a/test/cli/build_test.rb b/test/cli/build_test.rb index c0a236ad..0bdd6b65 100644 --- a/test/cli/build_test.rb +++ b/test/cli/build_test.rb @@ -21,6 +21,7 @@ class CliBuildTest < CliTestCase .returns("") run_command("push", "--verbose").tap do |output| + assert_hook_ran "pre-connect", output assert_hook_ran "pre-build", output assert_match /Cloning repo into build directory/, output assert_match /git -C #{Dir.tmpdir}\/kamal-clones\/app-#{pwd_sha} clone #{Dir.pwd}/, output diff --git a/test/cli/proxy_test.rb b/test/cli/proxy_test.rb index 428fc515..e6b0eaea 100644 --- a/test/cli/proxy_test.rb +++ b/test/cli/proxy_test.rb @@ -5,7 +5,7 @@ class CliProxyTest < CliTestCase run_command("boot").tap do |output| assert_match "docker login", output assert_match "mkdir -p .kamal/proxy/apps-config", output - assert_match "echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") $(cat .kamal/proxy/image 2> /dev/null || echo \"basecamp/kamal-proxy\"):$(cat .kamal/proxy/image_version 2> /dev/null || echo \"#{Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION}\") | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", output + assert_match "echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") $(cat .kamal/proxy/image 2> /dev/null || echo \"basecamp/kamal-proxy\"):$(cat .kamal/proxy/image_version 2> /dev/null || echo \"#{Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION}\") $(cat .kamal/proxy/run_command 2> /dev/null || echo \"\") | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", output end end @@ -19,7 +19,7 @@ class CliProxyTest < CliTestCase exception = assert_raises do run_command("boot").tap do |output| assert_match "docker login", output - assert_match "echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") $(cat .kamal/proxy/image 2> /dev/null || echo \"basecamp/kamal-proxy\"):$(cat .kamal/proxy/image_version 2> /dev/null || echo \"#{Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION}\") | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", output + assert_match "echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") $(cat .kamal/proxy/image 2> /dev/null || echo \"basecamp/kamal-proxy\"):$(cat .kamal/proxy/image_version 2> /dev/null || echo \"#{Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION}\") $(cat .kamal/proxy/run_command 2> /dev/null || echo \"\") | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", output end end @@ -37,49 +37,27 @@ class CliProxyTest < CliTestCase run_command("boot").tap do |output| assert_match "docker login", output - assert_match "docker container start kamal-proxy || echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") $(cat .kamal/proxy/image 2> /dev/null || echo \"basecamp/kamal-proxy\"):$(cat .kamal/proxy/image_version 2> /dev/null || echo \"#{Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION}\") | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", output + assert_match "docker container start kamal-proxy || echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") $(cat .kamal/proxy/image 2> /dev/null || echo \"basecamp/kamal-proxy\"):$(cat .kamal/proxy/image_version 2> /dev/null || echo \"#{Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION}\") $(cat .kamal/proxy/run_command 2> /dev/null || echo \"\") | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", output end ensure Thread.report_on_exception = false end test "reboot" do - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet") - .returns("abcdefabcdef") - .at_least_once - - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with { |*args| args[0..1] == [ :sh, "-c" ] } - .returns("123") - .at_least_once - run_command("reboot", "-y").tap do |output| assert_match "docker container stop kamal-proxy on 1.1.1.1", output assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.1", output assert_match "mkdir -p .kamal/proxy/apps-config on 1.1.1.1", output - assert_match "echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") $(cat .kamal/proxy/image 2> /dev/null || echo \"basecamp/kamal-proxy\"):$(cat .kamal/proxy/image_version 2> /dev/null || echo \"#{Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION}\") | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --volume $(pwd)/.kamal/proxy/apps-config:/home/kamal-proxy/.apps-config on 1.1.1.1", output - assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target=\"abcdefabcdef:80\" --deploy-timeout=\"6s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\" on 1.1.1.1", output + assert_match "echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") $(cat .kamal/proxy/image 2> /dev/null || echo \"basecamp/kamal-proxy\"):$(cat .kamal/proxy/image_version 2> /dev/null || echo \"#{Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION}\") $(cat .kamal/proxy/run_command 2> /dev/null || echo \"\") | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --volume $(pwd)/.kamal/proxy/apps-config:/home/kamal-proxy/.apps-config on 1.1.1.1", output assert_match "docker container stop kamal-proxy on 1.1.1.2", output assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.2", output assert_match "mkdir -p .kamal/proxy/apps-config on 1.1.1.1", output - assert_match "echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") $(cat .kamal/proxy/image 2> /dev/null || echo \"basecamp/kamal-proxy\"):$(cat .kamal/proxy/image_version 2> /dev/null || echo \"#{Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION}\") | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --volume $(pwd)/.kamal/proxy/apps-config:/home/kamal-proxy/.apps-config on 1.1.1.2", output - assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target=\"abcdefabcdef:80\" --deploy-timeout=\"6s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\" on 1.1.1.2", output + assert_match "echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") $(cat .kamal/proxy/image 2> /dev/null || echo \"basecamp/kamal-proxy\"):$(cat .kamal/proxy/image_version 2> /dev/null || echo \"#{Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION}\") $(cat .kamal/proxy/run_command 2> /dev/null || echo \"\") | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --volume $(pwd)/.kamal/proxy/apps-config:/home/kamal-proxy/.apps-config on 1.1.1.2", output end end test "reboot --rolling" do - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet") - .returns("abcdefabcdef") - .at_least_once - - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with { |*args| args[0..1] == [ :sh, "-c" ] } - .returns("123") - .at_least_once - run_command("reboot", "--rolling", "-y").tap do |output| assert_match "Running docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.1", output end @@ -199,7 +177,7 @@ class CliProxyTest < CliTestCase assert_match "/usr/bin/env mkdir -p .kamal", output assert_match "docker network create kamal", output assert_match "docker login -u [REDACTED] -p [REDACTED]", output - assert_match "docker container start kamal-proxy || echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") $(cat .kamal/proxy/image 2> /dev/null || echo \"basecamp/kamal-proxy\"):$(cat .kamal/proxy/image_version 2> /dev/null || echo \"#{Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION}\") | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", output + assert_match "docker container start kamal-proxy || echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") $(cat .kamal/proxy/image 2> /dev/null || echo \"basecamp/kamal-proxy\"):$(cat .kamal/proxy/image_version 2> /dev/null || echo \"#{Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION}\") $(cat .kamal/proxy/run_command 2> /dev/null || echo \"\") | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", output assert_match "/usr/bin/env mkdir -p .kamal", output assert_match %r{docker rename app-web-latest app-web-latest_replaced_.*}, output assert_match "/usr/bin/env mkdir -p .kamal/apps/app/env/roles", output @@ -243,7 +221,9 @@ class CliProxyTest < CliTestCase assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output assert_match "Running /usr/bin/env rm .kamal/proxy/options on #{host}", output assert_match "Running /usr/bin/env rm .kamal/proxy/image on #{host}", output - assert_match "Running /usr/bin/env rm .kamal/proxy/image_version on #{host}", output end + assert_match "Running /usr/bin/env rm .kamal/proxy/image_version on #{host}", output + assert_match "Running /usr/bin/env rm .kamal/proxy/run_command on #{host}", output + end end end @@ -254,6 +234,7 @@ class CliProxyTest < CliTestCase assert_match "Uploading \"--log-opt max-size=10m\" to .kamal/proxy/options on #{host}", output assert_match "Running /usr/bin/env rm .kamal/proxy/image on #{host}", output assert_match "Running /usr/bin/env rm .kamal/proxy/image_version on #{host}", output + assert_match "Running /usr/bin/env rm .kamal/proxy/run_command on #{host}", output end end end @@ -265,6 +246,7 @@ class CliProxyTest < CliTestCase assert_match "Uploading \"--publish 80:80 --publish 443:443 --log-opt max-size=100m\" to .kamal/proxy/options on #{host}", output assert_match "Running /usr/bin/env rm .kamal/proxy/image on #{host}", output assert_match "Running /usr/bin/env rm .kamal/proxy/image_version on #{host}", output + assert_match "Running /usr/bin/env rm .kamal/proxy/run_command on #{host}", output end end end @@ -276,6 +258,7 @@ class CliProxyTest < CliTestCase assert_match "Uploading \"--publish 80:80 --publish 443:443\" to .kamal/proxy/options on #{host}", output assert_match "Running /usr/bin/env rm .kamal/proxy/image on #{host}", output assert_match "Running /usr/bin/env rm .kamal/proxy/image_version on #{host}", output + assert_match "Running /usr/bin/env rm .kamal/proxy/run_command on #{host}", output end end end @@ -322,6 +305,7 @@ class CliProxyTest < CliTestCase assert_match "Uploading \"--publish 80:80 --publish 443:443 --log-opt max-size=10m --label=foo=bar --add_host=thishost:thathost\" to .kamal/proxy/options on #{host}", output assert_match "Running /usr/bin/env rm .kamal/proxy/image on #{host}", output assert_match "Running /usr/bin/env rm .kamal/proxy/image_version on #{host}", output + assert_match "Running /usr/bin/env rm .kamal/proxy/run_command on #{host}", output end end end @@ -333,6 +317,7 @@ class CliProxyTest < CliTestCase assert_match "Running /usr/bin/env rm .kamal/proxy/options on #{host}", output assert_match "Uploading \"myreg/basecamp/kamal-proxy\" to .kamal/proxy/image on #{host}", output assert_match "Running /usr/bin/env rm .kamal/proxy/image_version on #{host}", output + assert_match "Running /usr/bin/env rm .kamal/proxy/run_command on #{host}", output end end end @@ -344,6 +329,7 @@ class CliProxyTest < CliTestCase assert_match "Running /usr/bin/env rm .kamal/proxy/options on #{host}", output assert_match "Uploading \"myrepo/kamal-proxy\" to .kamal/proxy/image on #{host}", output assert_match "Running /usr/bin/env rm .kamal/proxy/image_version on #{host}", output + assert_match "Running /usr/bin/env rm .kamal/proxy/run_command on #{host}", output end end end @@ -355,23 +341,37 @@ class CliProxyTest < CliTestCase assert_match "Running /usr/bin/env rm .kamal/proxy/options on #{host}", output assert_match "Running /usr/bin/env rm .kamal/proxy/image on #{host}", output assert_match "Uploading \"0.9.9\" to .kamal/proxy/image_version on #{host}", output + assert_match "Running /usr/bin/env rm .kamal/proxy/run_command on #{host}", output + end + end + end + + test "boot_config set run_command" do + run_command("boot_config", "set", "--metrics_port", "9000", "--debug", "true").tap do |output| + %w[ 1.1.1.1 1.1.1.2 ].each do |host| + assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output + assert_match "Uploading \"--publish 80:80 --publish 443:443 --log-opt max-size=10m --expose=9000\" to .kamal/proxy/options on #{host}", output + assert_match "Running /usr/bin/env rm .kamal/proxy/image on #{host}", output + assert_match "Running /usr/bin/env rm .kamal/proxy/image_version on #{host}", output + assert_match "Uploading \"kamal-proxy run --debug --metrics-port \\\"9000\\\"\" to .kamal/proxy/run_command on #{host}", output end end end test "boot_config set all" do - run_command("boot_config", "set", "--docker_options", "label=foo=bar", "--registry", "myreg", "--repository", "myrepo", "--image_version", "0.9.9").tap do |output| + run_command("boot_config", "set", "--docker_options", "label=foo=bar", "--registry", "myreg", "--repository", "myrepo", "--image_version", "0.9.9", "--metrics_port", "9000", "--debug", "true").tap do |output| %w[ 1.1.1.1 1.1.1.2 ].each do |host| - assert_match "Uploading \"--publish 80:80 --publish 443:443 --log-opt max-size=10m --label=foo=bar\" to .kamal/proxy/options on #{host}", output + assert_match "Uploading \"--publish 80:80 --publish 443:443 --log-opt max-size=10m --expose=9000 --label=foo=bar\" to .kamal/proxy/options on #{host}", output assert_match "Uploading \"myreg/myrepo/kamal-proxy\" to .kamal/proxy/image on #{host}", output assert_match "Uploading \"0.9.9\" to .kamal/proxy/image_version on #{host}", output + assert_match "Uploading \"kamal-proxy run --debug --metrics-port \\\"9000\\\"\" to .kamal/proxy/run_command on #{host}", output end end end test "boot_config get" do SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:echo, "$(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") $(cat .kamal/proxy/image 2> /dev/null || echo \"basecamp/kamal-proxy\"):$(cat .kamal/proxy/image_version 2> /dev/null || echo \"v0.8.7\")") + .with(:echo, "$(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") $(cat .kamal/proxy/image 2> /dev/null || echo \"basecamp/kamal-proxy\"):$(cat .kamal/proxy/image_version 2> /dev/null || echo \"#{Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION}\") $(cat .kamal/proxy/run_command 2> /dev/null || echo \"\")") .returns("--publish 80:80 --publish 8443:443 --label=foo=bar basecamp/kamal-proxy:v1.0.0") .twice diff --git a/test/commander_test.rb b/test/commander_test.rb index 4a3aa2e2..84115f3a 100644 --- a/test/commander_test.rb +++ b/test/commander_test.rb @@ -149,6 +149,12 @@ class CommanderTest < ActiveSupport::TestCase assert_equal [], @kamal.accessory_hosts end + test "primary role hosts are first" do + configure_with(:deploy_with_roles_workers_primary) + assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], @kamal.hosts + assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], @kamal.app_hosts + end + private def configure_with(variant) @kamal = Kamal::Commander.new.tap do |kamal| diff --git a/test/commands/proxy_test.rb b/test/commands/proxy_test.rb index e141725a..2bb2dadb 100644 --- a/test/commands/proxy_test.rb +++ b/test/commands/proxy_test.rb @@ -15,7 +15,7 @@ class CommandsProxyTest < ActiveSupport::TestCase test "run" do assert_equal \ - "echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") $(cat .kamal/proxy/image 2> /dev/null || echo \"basecamp/kamal-proxy\"):$(cat .kamal/proxy/image_version 2> /dev/null || echo \"#{Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION}\") | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --volume $(pwd)/.kamal/proxy/apps-config:/home/kamal-proxy/.apps-config", + "echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") $(cat .kamal/proxy/image 2> /dev/null || echo \"basecamp/kamal-proxy\"):$(cat .kamal/proxy/image_version 2> /dev/null || echo \"#{Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION}\") $(cat .kamal/proxy/run_command 2> /dev/null || echo \"\") | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --volume $(pwd)/.kamal/proxy/apps-config:/home/kamal-proxy/.apps-config", new_command.run.join(" ") end @@ -23,7 +23,7 @@ class CommandsProxyTest < ActiveSupport::TestCase @config.delete(:proxy) assert_equal \ - "echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") $(cat .kamal/proxy/image 2> /dev/null || echo \"basecamp/kamal-proxy\"):$(cat .kamal/proxy/image_version 2> /dev/null || echo \"#{Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION}\") | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --volume $(pwd)/.kamal/proxy/apps-config:/home/kamal-proxy/.apps-config", + "echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") $(cat .kamal/proxy/image 2> /dev/null || echo \"basecamp/kamal-proxy\"):$(cat .kamal/proxy/image_version 2> /dev/null || echo \"#{Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION}\") $(cat .kamal/proxy/run_command 2> /dev/null || echo \"\") | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --volume $(pwd)/.kamal/proxy/apps-config:/home/kamal-proxy/.apps-config", new_command.run.join(" ") end @@ -129,6 +129,12 @@ class CommandsProxyTest < ActiveSupport::TestCase new_command.read_image_version.join(" ") end + test "read_run_command" do + assert_equal \ + "cat .kamal/proxy/run_command 2> /dev/null || echo \"\"", + new_command.read_run_command.join(" ") + end + test "reset_boot_options" do assert_equal \ "rm .kamal/proxy/options", @@ -153,6 +159,12 @@ class CommandsProxyTest < ActiveSupport::TestCase new_command.ensure_apps_config_directory.join(" ") end + test "reset_run_command" do + assert_equal \ + "rm .kamal/proxy/run_command", + new_command.reset_run_command.join(" ") + end + private def new_command Kamal::Commands::Proxy.new(Kamal::Configuration.new(@config, version: "123")) diff --git a/test/configuration/accessory_test.rb b/test/configuration/accessory_test.rb index a652c2ac..0f13b33e 100644 --- a/test/configuration/accessory_test.rb +++ b/test/configuration/accessory_test.rb @@ -7,8 +7,8 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: { - "web" => [ "1.1.1.1", "1.1.1.2" ], - "workers" => [ "1.1.1.3", "1.1.1.4" ] + "web" => [ { "1.1.1.1" => "writer" }, { "1.1.1.2" => "reader" } ], + "workers" => [ { "1.1.1.3" => "writer" }, "1.1.1.4" ] }, builder: { "arch" => "amd64" }, env: { "REDIS_URL" => "redis://x/y" }, @@ -55,7 +55,7 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase "service" => "custom-monitoring", "image" => "monitoring:latest", "registry" => { "server" => "other.registry", "username" => "user", "password" => "pw" }, - "roles" => [ "web" ], + "role" => "web", "port" => "4321:4321", "labels" => { "cache" => "true" @@ -70,6 +70,14 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase "proxy" => { "host" => "monitoring.example.com" } + }, + "proxy" => { + "image" => "proxy:latest", + "tags" => [ "writer", "reader" ] + }, + "logger" => { + "image" => "logger:latest", + "tag" => "writer" } } } @@ -107,6 +115,8 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase assert_equal [ "1.1.1.5" ], @config.accessory(:mysql).hosts assert_equal [ "1.1.1.6", "1.1.1.7" ], @config.accessory(:redis).hosts assert_equal [ "1.1.1.1", "1.1.1.2" ], @config.accessory(:monitoring).hosts + assert_equal [ "1.1.1.1", "1.1.1.3", "1.1.1.2" ], @config.accessory(:proxy).hosts + assert_equal [ "1.1.1.1", "1.1.1.3" ], @config.accessory(:logger).hosts end test "missing host" do @@ -117,14 +127,14 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase end end - test "setting host, hosts and roles" do + test "setting host, hosts, roles and tags" do @deploy[:accessories]["mysql"]["hosts"] = [ "mysql-db1" ] @deploy[:accessories]["mysql"]["roles"] = [ "db" ] exception = assert_raises(Kamal::ConfigurationError) do Kamal::Configuration.new(@deploy) end - assert_equal "accessories/mysql: specify one of `host`, `hosts` or `roles`", exception.message + assert_equal "accessories/mysql: specify one of `host`, `hosts`, `role`, `roles`, `tag` or `tags`", exception.message end test "all hosts" do diff --git a/test/fixtures/deploy_with_roles_workers_primary.yml b/test/fixtures/deploy_with_roles_workers_primary.yml new file mode 100644 index 00000000..4e208c6b --- /dev/null +++ b/test/fixtures/deploy_with_roles_workers_primary.yml @@ -0,0 +1,19 @@ +service: app +image: dhh/app +servers: + workers: + - 1.1.1.1 + - 1.1.1.2 + web: + - 1.1.1.3 + - 1.1.1.4 +env: + REDIS_URL: redis://x/y +registry: + server: registry.digitalocean.com + username: user + password: pw +builder: + arch: amd64 +deploy_timeout: 1 +primary_role: workers diff --git a/test/integration/docker/deployer/app_with_proxied_accessory/.kamal/hooks/pre-deploy b/test/integration/docker/deployer/app_with_proxied_accessory/.kamal/hooks/pre-deploy new file mode 100755 index 00000000..af8d0b4a --- /dev/null +++ b/test/integration/docker/deployer/app_with_proxied_accessory/.kamal/hooks/pre-deploy @@ -0,0 +1,4 @@ +#!/bin/sh +set -e + +kamal proxy boot_config set --registry registry:4443 diff --git a/test/integration/docker/deployer/setup.sh b/test/integration/docker/deployer/setup.sh index 24f39d7f..1ad7998b 100755 --- a/test/integration/docker/deployer/setup.sh +++ b/test/integration/docker/deployer/setup.sh @@ -20,7 +20,7 @@ push_image_to_registry_4443() { install_kamal push_image_to_registry_4443 nginx 1-alpine-slim push_image_to_registry_4443 busybox 1.36.0 -push_image_to_registry_4443 basecamp/kamal-proxy v0.8.7 +push_image_to_registry_4443 basecamp/kamal-proxy v0.9.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.