Files
kamal/test/configuration_test.rb
Donal McBreen 8a41d15b69 Zero downtime deployment with cord file
When replacing a container currently we:
1. Boot the new container
2. Wait for it to become healthy
3. Stop the old container

Traefik will send requests to the old container until it notices that it
is unhealthy. But it may have stopped serving requests before that point
which can result in errors.

To get round that the new boot process is:

1. Create a directory with a single file on the host
2. Boot the new container, mounting the cord file into /tmp and
including a check for the file in the docker healthcheck
3. Wait for it to become healthy
4. Delete the healthcheck file ("cut the cord") for the old container
5. Wait for it to become unhealthy and give Traefik a couple of seconds
to notice
6. Stop the old container

The extra steps ensure that Traefik stops sending requests before the
old container is shutdown.
2023-09-06 14:35:30 +01:00

269 lines
8.8 KiB
Ruby

require "test_helper"
class ConfigurationTest < ActiveSupport::TestCase
setup do
ENV["RAILS_MASTER_KEY"] = "456"
ENV["VERSION"] = "missing"
@deploy = {
service: "app", image: "dhh/app",
registry: { "username" => "dhh", "password" => "secret" },
env: { "REDIS_URL" => "redis://x/y" },
servers: [ "1.1.1.1", "1.1.1.2" ],
volumes: ["/local/path:/container/path"]
}
@config = Kamal::Configuration.new(@deploy)
@deploy_with_roles = @deploy.dup.merge({
servers: { "web" => [ "1.1.1.1", "1.1.1.2" ], "workers" => { "hosts" => [ "1.1.1.1", "1.1.1.3" ] } } })
@config_with_roles = Kamal::Configuration.new(@deploy_with_roles)
end
teardown do
ENV.delete("RAILS_MASTER_KEY")
ENV.delete("VERSION")
end
%i[ service image registry ].each do |key|
test "#{key} config required" do
assert_raise(ArgumentError) do
Kamal::Configuration.new @deploy.tap { _1.delete key }
end
end
end
%w[ username password ].each do |key|
test "registry #{key} required" do
assert_raise(ArgumentError) do
Kamal::Configuration.new @deploy.tap { _1[:registry].delete key }
end
end
end
test "roles" do
assert_equal %w[ web ], @config.roles.collect(&:name)
assert_equal %w[ web workers ], @config_with_roles.roles.collect(&:name)
end
test "role" do
assert @config.role(:web).name.web?
assert_equal "workers", @config_with_roles.role(:workers).name
assert_nil @config.role(:missing)
end
test "all hosts" do
assert_equal [ "1.1.1.1", "1.1.1.2"], @config.all_hosts
assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3" ], @config_with_roles.all_hosts
end
test "primary web host" do
assert_equal "1.1.1.1", @config.primary_web_host
assert_equal "1.1.1.1", @config_with_roles.primary_web_host
end
test "traefik hosts" do
assert_equal [ "1.1.1.1", "1.1.1.2" ], @config_with_roles.traefik_hosts
@deploy_with_roles[:servers]["workers"]["traefik"] = true
config = Kamal::Configuration.new(@deploy_with_roles)
assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3" ], config.traefik_hosts
end
test "version no git repo" do
ENV.delete("VERSION")
@config.expects(:system).with("git rev-parse").returns(nil)
error = assert_raises(RuntimeError) { @config.version}
assert_match /no git repository found/, error.message
end
test "version from git committed" do
ENV.delete("VERSION")
@config.expects(:`).with("git rev-parse HEAD").returns("git-version")
Kamal::Utils.expects(:uncommitted_changes).returns("")
assert_equal "git-version", @config.version
end
test "version from git uncommitted" do
ENV.delete("VERSION")
@config.expects(:`).with("git rev-parse HEAD").returns("git-version")
Kamal::Utils.expects(:uncommitted_changes).returns("M file\n")
assert_match /^git-version_uncommitted_[0-9a-f]{16}$/, @config.version
end
test "version from env" do
ENV["VERSION"] = "env-version"
assert_equal "env-version", @config.version
end
test "version from arg" do
@config.version = "arg-version"
assert_equal "arg-version", @config.version
end
test "repository" do
assert_equal "dhh/app", @config.repository
config = Kamal::Configuration.new(@deploy.tap { |c| c[:registry].merge!({ "server" => "ghcr.io" }) })
assert_equal "ghcr.io/dhh/app", config.repository
end
test "absolute image" do
assert_equal "dhh/app:missing", @config.absolute_image
config = Kamal::Configuration.new(@deploy.tap { |c| c[:registry].merge!({ "server" => "ghcr.io" }) })
assert_equal "ghcr.io/dhh/app:missing", config.absolute_image
end
test "service with version" do
assert_equal "app-missing", @config.service_with_version
end
test "env with missing secret" do
assert_raises(KeyError) do
config = Kamal::Configuration.new(@deploy.tap { |c| c.merge!({
env: { "secret" => [ "PASSWORD" ] }
}) }).ensure_env_available
end
end
test "valid config" do
assert @config.valid?
assert @config_with_roles.valid?
end
test "hosts required for all roles" do
# Empty server list for implied web role
assert_raises(ArgumentError) do
Kamal::Configuration.new @deploy.merge(servers: [])
end
# Empty server list
assert_raises(ArgumentError) do
Kamal::Configuration.new @deploy.merge(servers: { "web" => [] })
end
# Missing hosts key
assert_raises(ArgumentError) do
Kamal::Configuration.new @deploy.merge(servers: { "web" => {} })
end
# Empty hosts list
assert_raises(ArgumentError) do
Kamal::Configuration.new @deploy.merge(servers: { "web" => { "hosts" => [] } })
end
# Nil hosts
assert_raises(ArgumentError) do
Kamal::Configuration.new @deploy.merge(servers: { "web" => { "hosts" => nil } })
end
# One role with hosts, one without
assert_raises(ArgumentError) do
Kamal::Configuration.new @deploy.merge(servers: { "web" => %w[ web ], "workers" => { "hosts" => %w[ ] } })
end
end
test "volume_args" do
assert_equal ["--volume", "/local/path:/container/path"], @config.volume_args
end
test "logging args default" do
assert_equal ["--log-opt", "max-size=\"10m\""], @config.logging_args
end
test "logging args with configured options" do
config = Kamal::Configuration.new(@deploy.tap { |c| c.merge!(logging: { "options" => { "max-size" => "100m", "max-file" => 5 } }) })
assert_equal ["--log-opt", "max-size=\"100m\"", "--log-opt", "max-file=\"5\""], @config.logging_args
end
test "logging args with configured driver and options" do
config = Kamal::Configuration.new(@deploy.tap { |c| c.merge!(logging: { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => 5 } }) })
assert_equal ["--log-driver", "\"local\"", "--log-opt", "max-size=\"100m\"", "--log-opt", "max-file=\"5\""], @config.logging_args
end
test "erb evaluation of yml config" do
config = Kamal::Configuration.create_from config_file: Pathname.new(File.expand_path("fixtures/deploy.erb.yml", __dir__))
assert_equal "my-user", config.registry["username"]
end
test "destination yml config merge" do
dest_config_file = Pathname.new(File.expand_path("fixtures/deploy_for_dest.yml", __dir__))
config = Kamal::Configuration.create_from config_file: dest_config_file, destination: "world"
assert_equal "1.1.1.1", config.all_hosts.first
config = Kamal::Configuration.create_from config_file: dest_config_file, destination: "mars"
assert_equal "1.1.1.3", config.all_hosts.first
end
test "destination yml config file missing" do
dest_config_file = Pathname.new(File.expand_path("fixtures/deploy_for_dest.yml", __dir__))
assert_raises(RuntimeError) do
config = Kamal::Configuration.create_from config_file: dest_config_file, destination: "missing"
end
end
test "to_h" do
expected_config = \
{ :roles=>["web"],
:hosts=>["1.1.1.1", "1.1.1.2"],
:primary_host=>"1.1.1.1",
:version=>"missing",
:repository=>"dhh/app",
:absolute_image=>"dhh/app:missing",
:service_with_version=>"app-missing",
:ssh_options=>{ :user=>"root", :auth_methods=>["publickey"], log_level: :fatal, keepalive: true, keepalive_interval: 30 },
:sshkit=>{},
:volume_args=>["--volume", "/local/path:/container/path"],
:builder=>{},
:logging=>["--log-opt", "max-size=\"10m\""],
:healthcheck=>{ "path"=>"/up", "port"=>3000, "max_attempts" => 7, "exposed_port" => 3999, "cord" => "/tmp/kamal-cord" }}
assert_equal expected_config, @config.to_h
end
test "min version is lower" do
config = Kamal::Configuration.new(@deploy.tap { |c| c.merge!(minimum_version: "0.0.1") })
assert_equal "0.0.1", config.minimum_version
end
test "min version is equal" do
config = Kamal::Configuration.new(@deploy.tap { |c| c.merge!(minimum_version: Kamal::VERSION) })
assert_equal Kamal::VERSION, config.minimum_version
end
test "min version is higher" do
assert_raises(ArgumentError) do
Kamal::Configuration.new(@deploy.tap { |c| c.merge!(minimum_version: "10000.0.0") })
end
end
test "run directory" do
config = Kamal::Configuration.new(@deploy)
assert_equal ".kamal", config.run_directory
config = Kamal::Configuration.new(@deploy.merge!(run_directory: "/root/kamal"))
assert_equal "/root/kamal", config.run_directory
end
test "run directory as docker volume" do
config = Kamal::Configuration.new(@deploy)
assert_equal "$(pwd)/.kamal", config.run_directory_as_docker_volume
config = Kamal::Configuration.new(@deploy.merge!(run_directory: "/root/kamal"))
assert_equal "/root/kamal", config.run_directory_as_docker_volume
end
test "run id" do
SecureRandom.expects(:hex).with(16).returns("09876543211234567890098765432112")
assert_equal "09876543211234567890098765432112", @config.run_id
end
end