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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
33
lib/kamal/cli/app/error_pages.rb
Normal file
33
lib/kamal/cli/app/error_pages.rb
Normal 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
|
||||
@@ -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 ]
|
||||
|
||||
|
||||
9
lib/kamal/commands/app/error_pages.rb
Normal file
9
lib/kamal/commands/app/error_pages.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
11
test/fixtures/deploy_with_error_pages.yml
vendored
Normal file
11
test/fixtures/deploy_with_error_pages.yml
vendored
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -37,6 +37,7 @@ proxy:
|
||||
- X-Request-Start
|
||||
|
||||
asset_path: /usr/share/nginx/html/versions
|
||||
error_pages_path: error_pages
|
||||
|
||||
registry:
|
||||
server: registry:4443
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>503 Service Interrupted</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Custom Maintenance Page: {{ .Message }}</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user