diff --git a/lib/kamal/cli/app.rb b/lib/kamal/cli/app.rb index 553ccbb2..d2f9c3f9 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -8,8 +8,10 @@ class Kamal::Cli::App < Kamal::Cli::Base # Assets are prepared in a separate step to ensure they are on all hosts before booting on(KAMAL.hosts) do + Kamal::Cli::App::ErrorPages.new(host, self).run + KAMAL.roles_on(host).each do |role| - Kamal::Cli::App::PrepareAssets.new(host, role, self).run + Kamal::Cli::App::Assets.new(host, role, self).run end end @@ -249,7 +251,37 @@ class Kamal::Cli::App < Kamal::Cli::Base stop remove_containers remove_images - remove_app_directory + remove_app_directories + end + end + + desc "live", "Set the app to live mode" + def live + with_lock do + on(KAMAL.proxy_hosts) do |host| + roles = KAMAL.roles_on(host) + + roles.each do |role| + execute *KAMAL.app(role: role, host: host).live if role.running_proxy? + end + end + end + end + + desc "maintenance", "Set the app to maintenance mode" + option :drain_timeout, type: :numeric, desc: "How long to allow in-flight requests to complete (defaults to drain_timeout from config)" + option :message, type: :string, desc: "Message to display to clients while stopped" + def maintenance + maintenance_options = { drain_timeout: options[:drain_timeout] || KAMAL.config.drain_timeout, message: options[:message] } + + with_lock do + on(KAMAL.proxy_hosts) do |host| + roles = KAMAL.roles_on(host) + + roles.each do |role| + execute *KAMAL.app(role: role, host: host).maintenance(**maintenance_options) if role.running_proxy? + end + end end end @@ -291,16 +323,19 @@ class Kamal::Cli::App < Kamal::Cli::Base end end - desc "remove_app_directory", "Remove the service directory from servers", hide: true - def remove_app_directory + desc "remove_app_directories", "Remove the app directories from servers", hide: true + def remove_app_directories with_lock do on(KAMAL.hosts) do |host| roles = KAMAL.roles_on(host) roles.each do |role| - execute *KAMAL.auditor.record("Removed #{KAMAL.config.app_directory} on all servers", role: role), verbosity: :debug + execute *KAMAL.auditor.record("Removed #{KAMAL.config.app_directory}", role: role), verbosity: :debug execute *KAMAL.server.remove_app_directory, raise_on_non_zero_exit: false end + + execute *KAMAL.auditor.record("Removed #{KAMAL.config.app_directory}"), verbosity: :debug + execute *KAMAL.app.remove_proxy_app_directory, raise_on_non_zero_exit: false end end end diff --git a/lib/kamal/cli/app/prepare_assets.rb b/lib/kamal/cli/app/assets.rb similarity index 93% rename from lib/kamal/cli/app/prepare_assets.rb rename to lib/kamal/cli/app/assets.rb index dd28fa41..2cab1423 100644 --- a/lib/kamal/cli/app/prepare_assets.rb +++ b/lib/kamal/cli/app/assets.rb @@ -1,4 +1,4 @@ -class Kamal::Cli::App::PrepareAssets +class Kamal::Cli::App::Assets attr_reader :host, :role, :sshkit delegate :execute, :capture_with_info, :info, to: :sshkit delegate :assets?, to: :role diff --git a/lib/kamal/cli/app/boot.rb b/lib/kamal/cli/app/boot.rb index 8ddcaac3..a7eb1c1a 100644 --- a/lib/kamal/cli/app/boot.rb +++ b/lib/kamal/cli/app/boot.rb @@ -70,6 +70,7 @@ class Kamal::Cli::App::Boot def stop_old_version(version) execute *app.stop(version: version), raise_on_non_zero_exit: false execute *app.clean_up_assets if assets? + execute *app.clean_up_error_pages if KAMAL.config.error_pages_path end def release_barrier diff --git a/lib/kamal/cli/app/error_pages.rb b/lib/kamal/cli/app/error_pages.rb new file mode 100644 index 00000000..664f7eff --- /dev/null +++ b/lib/kamal/cli/app/error_pages.rb @@ -0,0 +1,33 @@ +class Kamal::Cli::App::ErrorPages + ERROR_PAGES_GLOB = "{4??.html,5??.html}" + + attr_reader :host, :sshkit + delegate :upload!, :execute, to: :sshkit + + def initialize(host, sshkit) + @host = host + @sshkit = sshkit + end + + def run + if KAMAL.config.error_pages_path + with_error_pages_tmpdir do |local_error_pages_dir| + execute *KAMAL.app.create_error_pages_directory + upload! local_error_pages_dir, KAMAL.config.proxy_error_pages_directory, mode: "0700", recursive: true + end + end + end + + private + def with_error_pages_tmpdir + Dir.mktmpdir("kamal-error-pages") do |tmpdir| + error_pages_dir = File.join(tmpdir, KAMAL.config.version) + FileUtils.mkdir(error_pages_dir) + + if (files = Dir[File.join(KAMAL.config.error_pages_path, ERROR_PAGES_GLOB)]).any? + FileUtils.cp(files, error_pages_dir) + yield error_pages_dir + end + end + end +end diff --git a/lib/kamal/commands/app.rb b/lib/kamal/commands/app.rb index 6c4df0e4..d80684fb 100644 --- a/lib/kamal/commands/app.rb +++ b/lib/kamal/commands/app.rb @@ -1,5 +1,5 @@ class Kamal::Commands::App < Kamal::Commands::Base - include Assets, Containers, Execution, Images, Logging, Proxy + include Assets, Containers, ErrorPages, Execution, Images, Logging, Proxy ACTIVE_DOCKER_STATUSES = [ :running, :restarting ] diff --git a/lib/kamal/commands/app/error_pages.rb b/lib/kamal/commands/app/error_pages.rb new file mode 100644 index 00000000..d51dba3f --- /dev/null +++ b/lib/kamal/commands/app/error_pages.rb @@ -0,0 +1,9 @@ +module Kamal::Commands::App::ErrorPages + def create_error_pages_directory + make_directory(config.proxy_error_pages_directory) + end + + def clean_up_error_pages + [ :find, config.proxy_error_pages_directory, "-mindepth", "1", "-maxdepth", "1", "!", "-name", KAMAL.config.version, "-exec", "rm", "-rf", "{} +" ] + end +end diff --git a/lib/kamal/commands/app/proxy.rb b/lib/kamal/commands/app/proxy.rb index 777a4aaf..56d0ccea 100644 --- a/lib/kamal/commands/app/proxy.rb +++ b/lib/kamal/commands/app/proxy.rb @@ -9,6 +9,18 @@ module Kamal::Commands::App::Proxy proxy_exec :remove, role.container_prefix end + def live + proxy_exec :resume, role.container_prefix + end + + def maintenance(**options) + proxy_exec :stop, role.container_prefix, *role.proxy.stop_command_args(**options) + end + + def remove_proxy_app_directory + remove_directory config.proxy_app_directory + end + private def proxy_exec(*command) docker :exec, proxy_container_name, "kamal-proxy", *command diff --git a/lib/kamal/commands/proxy.rb b/lib/kamal/commands/proxy.rb index c6b9929c..658252de 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -110,6 +110,6 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base "--detach", "--restart", "unless-stopped", "--volume", "kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", - *config.proxy_app_config_volume.docker_args + *config.proxy_apps_volume.docker_args end end diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index 7772985b..bd22178f 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -105,6 +105,10 @@ class Kamal::Configuration raw_config.minimum_version end + def service_and_destination + [ service, destination ].compact.join("-") + end + def roles servers.roles end @@ -210,7 +214,7 @@ class Kamal::Configuration end def app_directory - File.join apps_directory, [ service, destination ].compact.join("-") + File.join apps_directory, service_and_destination end def env_directory @@ -229,6 +233,10 @@ class Kamal::Configuration raw_config.asset_path end + def error_pages_path + raw_config.error_pages_path + end + def env_tags @env_tags ||= if (tags = raw_config.env["tags"]) tags.collect { |name, config| Env::Tag.new(name, config: config, secrets: secrets) } @@ -300,14 +308,34 @@ class Kamal::Configuration File.join proxy_directory, "image_version" end - def proxy_app_config_directory - File.join proxy_directory, "app-config" + def proxy_apps_directory + File.join proxy_directory, "apps-config" end - def proxy_app_config_volume + def proxy_apps_container_directory + "/home/kamal-proxy/.apps-config" + end + + def proxy_apps_volume Volume.new \ - host_path: proxy_app_config_directory, - container_path: "/home/kamal-proxy/.app-config" + host_path: proxy_apps_directory, + container_path: proxy_apps_container_directory + end + + def proxy_app_directory + File.join proxy_apps_directory, service_and_destination + end + + def proxy_app_container_directory + File.join proxy_apps_container_directory, service_and_destination + end + + def proxy_error_pages_directory + File.join proxy_app_directory, "error_pages" + end + + def proxy_error_pages_container_directory + File.join proxy_app_container_directory, "error_pages" end def to_h diff --git a/lib/kamal/configuration/docs/configuration.yml b/lib/kamal/configuration/docs/configuration.yml index 6b059346..76f57eae 100644 --- a/lib/kamal/configuration/docs/configuration.yml +++ b/lib/kamal/configuration/docs/configuration.yml @@ -82,6 +82,12 @@ asset_path: /path/to/assets # See https://kamal-deploy.org/docs/hooks for more information: hooks_path: /user_home/kamal/hooks +# Error pages +# +# A directory relative to the app root to find error pages for the proxy to serve. +# Any files in the format 4xx.html or 5xx.html will be copied to the hosts. +error_pages_path: public + # Require destinations # # Whether deployments require a destination to be specified, defaults to `false`: diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb index a8272e88..19bc25f7 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -44,7 +44,8 @@ class Kamal::Configuration::Proxy "forward-headers": proxy_config.dig("forward_headers"), "tls-redirect": proxy_config.dig("ssl_redirect"), "log-request-header": proxy_config.dig("logging", "request_headers") || DEFAULT_LOG_REQUEST_HEADERS, - "log-response-header": proxy_config.dig("logging", "response_headers") + "log-response-header": proxy_config.dig("logging", "response_headers"), + "error-pages": error_pages }.compact end @@ -52,6 +53,17 @@ class Kamal::Configuration::Proxy optionize ({ target: "#{target}:#{app_port}" }).merge(deploy_options), with: "=" end + def stop_options(drain_timeout: nil, message: nil) + { + "drain-timeout": seconds_duration(drain_timeout), + message: message + }.compact + end + + def stop_command_args(**options) + optionize stop_options(**options), with: "=" + end + def merge(other) self.class.new config: config, proxy_config: proxy_config.deep_merge(other.proxy_config) end @@ -60,4 +72,8 @@ class Kamal::Configuration::Proxy def seconds_duration(value) value ? "#{value}s" : nil end + + def error_pages + File.join config.proxy_error_pages_container_directory, config.version if config.error_pages_path + end end diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index 19190032..2ed00182 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -192,6 +192,19 @@ class CliAppTest < CliTestCase Thread.report_on_exception = true end + test "boot with error pages" do + with_error_pages(directory: "public") do + stub_running + run_command("boot", config: :with_error_pages).tap do |output| + assert_match /Uploading .*kamal-error-pages.*\/latest to \.kamal\/proxy\/apps-config\/app\/error_pages/, output + assert_match "docker tag dhh/app:latest dhh/app:latest", output + assert_match /docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname 1.1.1.1-[0-9a-f]{12} /, output + assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output + assert_match "Running /usr/bin/env find .kamal/proxy/apps-config/app/error_pages -mindepth 1 -maxdepth 1 ! -name latest -exec rm -rf {} + on 1.1.1.1", output + end + end + end + test "start" do SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("999") # old version @@ -244,9 +257,11 @@ class CliAppTest < CliTestCase test "remove" do run_command("remove").tap do |output| - assert_match /#{Regexp.escape("sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --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 --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker stop")}/, output - assert_match /#{Regexp.escape("docker container prune --force --filter label=service=app")}/, output - assert_match /#{Regexp.escape("docker image prune --all --force --filter label=service=app")}/, output + assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --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 --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker stop", output + assert_match "docker container prune --force --filter label=service=app", output + assert_match "docker image prune --all --force --filter label=service=app", output + assert_match "rm -r .kamal/apps/app on 1.1.1.1", output + assert_match "rm -r .kamal/proxy/apps-config/app on 1.1.1.1", output end end @@ -268,6 +283,13 @@ class CliAppTest < CliTestCase end end + test "remove_app_directories" do + run_command("remove_app_directories").tap do |output| + assert_match "rm -r .kamal/apps/app on 1.1.1.1", output + assert_match "rm -r .kamal/proxy/apps-config/app on 1.1.1.1", output + end + end + test "exec" do run_command("exec", "ruby -v").tap do |output| assert_match "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:latest ruby -v", output @@ -437,6 +459,24 @@ class CliAppTest < CliTestCase end end + test "live" do + run_command("live").tap do |output| + assert_match "docker exec kamal-proxy kamal-proxy resume app-web on 1.1.1.1", output + end + end + + test "maintenance" do + run_command("maintenance").tap do |output| + assert_match "docker exec kamal-proxy kamal-proxy stop app-web --drain-timeout=\"30s\" on 1.1.1.1", output + end + end + + test "maintenance with options" do + run_command("maintenance", "--message", "Hello", "--drain_timeout", "10").tap do |output| + assert_match "docker exec kamal-proxy kamal-proxy stop app-web --drain-timeout=\"10s\" --message=\"Hello\" on 1.1.1.1", output + end + end + private def run_command(*command, config: :with_accessories, host: "1.1.1.1", allow_execute_error: false) stdouted do diff --git a/test/cli/proxy_test.rb b/test/cli/proxy_test.rb index ae717f70..2ec4c726 100644 --- a/test/cli/proxy_test.rb +++ b/test/cli/proxy_test.rb @@ -56,12 +56,12 @@ class CliProxyTest < CliTestCase 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 "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_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/app-config:/home/kamal-proxy/.app-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_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 "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 "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_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/app-config:/home/kamal-proxy/.app-config 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_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 end end diff --git a/test/commands/app_test.rb b/test/commands/app_test.rb index 75241597..33418a00 100644 --- a/test/commands/app_test.rb +++ b/test/commands/app_test.rb @@ -497,6 +497,30 @@ class CommandsAppTest < ActiveSupport::TestCase ], new_command(asset_path: "/public/assets").clean_up_assets end + test "live" do + assert_equal \ + "docker exec kamal-proxy kamal-proxy resume app-web", + new_command.live.join(" ") + end + + test "maintenance" do + assert_equal \ + "docker exec kamal-proxy kamal-proxy stop app-web", + new_command.maintenance.join(" ") + end + + test "maintenance with options" do + assert_equal \ + "docker exec kamal-proxy kamal-proxy stop app-web --drain-timeout=\"10s\" --message=\"Hi\"", + new_command.maintenance(drain_timeout: 10, message: "Hi").join(" ") + end + + test "remove_proxy_app_directory" do + assert_equal \ + "rm -r .kamal/proxy/apps-config/app", + new_command.remove_proxy_app_directory.join(" ") + end + private def new_command(role: "web", host: "1.1.1.1", **additional_config) config = Kamal::Configuration.new(@config.merge(additional_config), destination: @destination, version: "999") diff --git a/test/commands/proxy_test.rb b/test/commands/proxy_test.rb index c55fbdeb..21863fc6 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_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/app-config:/home/kamal-proxy/.app-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_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", 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_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/app-config:/home/kamal-proxy/.app-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_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", new_command.run.join(" ") end diff --git a/test/configuration_test.rb b/test/configuration_test.rb index c1aaa697..04be59ea 100644 --- a/test/configuration_test.rb +++ b/test/configuration_test.rb @@ -395,4 +395,13 @@ class ConfigurationTest < ActiveSupport::TestCase assert_equal "Different roles can't share the same host for SSL: foo.example.com", exception.message end + + test "proxy directories" do + assert_equal ".kamal/proxy/apps-config", @config.proxy_apps_directory + assert_equal "/home/kamal-proxy/.apps-config", @config.proxy_apps_container_directory + assert_equal ".kamal/proxy/apps-config/app", @config.proxy_app_directory + assert_equal "/home/kamal-proxy/.apps-config/app", @config.proxy_app_container_directory + assert_equal ".kamal/proxy/apps-config/app/error_pages", @config.proxy_error_pages_directory + assert_equal "/home/kamal-proxy/.apps-config/app/error_pages", @config.proxy_error_pages_container_directory + end end diff --git a/test/fixtures/deploy_with_error_pages.yml b/test/fixtures/deploy_with_error_pages.yml new file mode 100644 index 00000000..7806c54c --- /dev/null +++ b/test/fixtures/deploy_with_error_pages.yml @@ -0,0 +1,11 @@ +service: app +image: dhh/app +servers: + - "1.1.1.1" + - "1.1.1.2" +registry: + username: user + password: pw +builder: + arch: amd64 +error_pages_path: public diff --git a/test/integration/app_test.rb b/test/integration/app_test.rb index f128054b..29d122f3 100644 --- a/test/integration/app_test.rb +++ b/test/integration/app_test.rb @@ -48,9 +48,36 @@ class AppTest < IntegrationTest assert_match "App Host: vm1", exec_output assert_match /1 root 0:\d\d nginx/, exec_output + kamal :app, :maintenance + assert_app_in_maintenance + + kamal :app, :live + assert_app_is_up + kamal :app, :remove assert_app_not_found assert_app_directory_removed end + + test "custom error pages" do + @app = "app_with_roles" + + kamal :deploy + assert_app_is_up + + kamal :app, :maintenance + assert_app_in_maintenance message: "Custom Maintenance Page" + + kamal :app, :live + kamal :app, :maintenance, "--message", "\"Testing Maintence Mode\"" + assert_app_in_maintenance message: "Custom Maintenance Page: Testing Maintence Mode" + + second_version = update_app_rev + + kamal :redeploy + + kamal :app, :maintenance + assert_app_in_maintenance message: "Custom Maintenance Page" + end end 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 28449e0e..6c5ef9f4 100644 --- a/test/integration/docker/deployer/app_with_roles/config/deploy.yml +++ b/test/integration/docker/deployer/app_with_roles/config/deploy.yml @@ -37,6 +37,7 @@ proxy: - X-Request-Start asset_path: /usr/share/nginx/html/versions +error_pages_path: error_pages registry: server: registry:4443 diff --git a/test/integration/docker/deployer/app_with_roles/error_pages/503.html b/test/integration/docker/deployer/app_with_roles/error_pages/503.html new file mode 100644 index 00000000..3f844fb8 --- /dev/null +++ b/test/integration/docker/deployer/app_with_roles/error_pages/503.html @@ -0,0 +1,8 @@ + +
+Custom Maintenance Page: {{ .Message }}
+ + diff --git a/test/integration/integration_test.rb b/test/integration/integration_test.rb index e6e5520d..631d8fc4 100644 --- a/test/integration/integration_test.rb +++ b/test/integration/integration_test.rb @@ -45,15 +45,22 @@ class IntegrationTest < ActiveSupport::TestCase end def assert_app_is_down - response = app_response - debug_response_code(response, "502") - assert_equal "502", response.code + assert_app_error_code("502") + end + + def assert_app_in_maintenance(message: nil) + assert_app_error_code("503", message: message) end def assert_app_not_found + assert_app_error_code("404") + end + + def assert_app_error_code(code, message: nil) response = app_response - debug_response_code(response, "404") - assert_equal "404", response.code + debug_response_code(response, code) + assert_equal code, response.code + assert_match message, response.body.strip if message end def assert_app_is_up(version: nil, app: @app) diff --git a/test/test_helper.rb b/test/test_helper.rb index e77a0a8a..1704fa29 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -58,9 +58,7 @@ class ActiveSupport::TestCase def setup_test_secrets(**files) @original_pwd = Dir.pwd @secrets_tmpdir = Dir.mktmpdir - fixtures_dup = File.join(@secrets_tmpdir, "test") - FileUtils.mkdir_p(fixtures_dup) - FileUtils.cp_r("test/fixtures/", fixtures_dup) + copy_fixtures(@secrets_tmpdir) Dir.chdir(@secrets_tmpdir) FileUtils.mkdir_p(".kamal") @@ -75,6 +73,30 @@ class ActiveSupport::TestCase Dir.chdir(@original_pwd) FileUtils.rm_rf(@secrets_tmpdir) end + + def with_error_pages(directory:) + error_pages_tmpdir = Dir.mktmpdir + + Dir.mktmpdir do |tmpdir| + copy_fixtures(tmpdir) + + Dir.chdir(tmpdir) do + FileUtils.mkdir_p(directory) + Dir.chdir(directory) do + File.write("404.html", "404 page") + File.write("503.html", "503 page") + end + + yield + end + end + end + + def copy_fixtures(to_dir) + new_test_dir = File.join(to_dir, "test") + FileUtils.mkdir_p(new_test_dir) + FileUtils.cp_r("test/fixtures/", new_test_dir) + end end class SecretAdapterTestCase < ActiveSupport::TestCase