Maintenance mode

Adds support for maintenance mode to Kamal.

There are two new commands:
- `kamal app maintenance` - puts the app in maintenance mode
- `kamal app live` - puts the app back in live mode

In maintenance mode, the kamal proxy will respond to requests with a
503 status code. It will use an error page built into kamal proxy.

You can use your own error page by setting `error_pages_path` in the
configuration. This will copy any 4xx.html or 5xx.html files from that
page to a volume mounted into the proxy container.
This commit is contained in:
Donal McBreen
2025-04-16 12:21:47 +01:00
parent 26b6c072f3
commit 354530f3b8
22 changed files with 319 additions and 30 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 ]

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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`:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -37,6 +37,7 @@ proxy:
- X-Request-Start
asset_path: /usr/share/nginx/html/versions
error_pages_path: error_pages
registry:
server: registry:4443

View File

@@ -0,0 +1,8 @@
<html>
<head>
<title>503 Service Interrupted</title>
</head>
<body>
<p>Custom Maintenance Page: {{ .Message }}</p>
</body>
</html>

View File

@@ -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)

View File

@@ -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