Merge pull request #828 from basecamp/configuration-validation

Configuration validation
This commit is contained in:
Donal McBreen
2024-06-18 08:31:47 +01:00
committed by GitHub
59 changed files with 1942 additions and 478 deletions

View File

@@ -380,19 +380,6 @@ class CliMainTest < CliTestCase
end
end
test "config with aliases" do
run_command("config", config_file: "deploy_with_aliases").tap do |output|
config = YAML.load(output)
assert_equal [ "web", "web_tokyo", "workers", "workers_tokyo" ], config[:roles]
assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], config[:hosts]
assert_equal "999", config[:version]
assert_equal "registry.digitalocean.com/dhh/app", config[:repository]
assert_equal "registry.digitalocean.com/dhh/app:999", config[:absolute_image]
assert_equal "app-999", config[:service_with_version]
end
end
test "init" do
Pathname.any_instance.expects(:exist?).returns(false).times(3)
Pathname.any_instance.stubs(:mkpath)
@@ -445,11 +432,10 @@ class CliMainTest < CliTestCase
end
test "envify" do
Pathname.any_instance.expects(:exist?).returns(true).times(3)
File.expects(:read).with(".env.erb").returns("HELLO=<%= 'world' %>")
File.expects(:write).with(".env", "HELLO=world", perm: 0600)
run_command("envify")
with_test_dot_env_erb(contents: "HELLO=<%= 'world' %>") do
run_command("envify")
assert_equal("HELLO=world", File.read(".env"))
end
end
test "envify with blank line trimming" do
@@ -460,19 +446,17 @@ class CliMainTest < CliTestCase
<% end -%>
EOF
Pathname.any_instance.expects(:exist?).returns(true).times(3)
File.expects(:read).with(".env.erb").returns(file.strip)
File.expects(:write).with(".env", "HELLO=world\nKEY=value\n", perm: 0600)
run_command("envify")
with_test_dot_env_erb(contents: file) do
run_command("envify")
assert_equal("HELLO=world\nKEY=value\n", File.read(".env"))
end
end
test "envify with destination" do
Pathname.any_instance.expects(:exist?).returns(true).times(4)
File.expects(:read).with(".env.world.erb").returns("HELLO=<%= 'world' %>")
File.expects(:write).with(".env.world", "HELLO=world", perm: 0600)
run_command("envify", "-d", "world", config_file: "deploy_for_dest")
with_test_dot_env_erb(contents: "HELLO=<%= 'world' %>", file: ".env.world.erb") do
run_command("envify", "-d", "world", config_file: "deploy_for_dest")
assert_equal "HELLO=world", File.read(".env.world")
end
end
test "envify with skip_push" do
@@ -508,6 +492,24 @@ class CliMainTest < CliTestCase
end
end
test "docs" do
run_command("docs").tap do |output|
assert_match "# Kamal Configuration", output
end
end
test "docs subsection" do
run_command("docs", "accessory").tap do |output|
assert_match "# Accessories", output
end
end
test "docs unknown" do
run_command("docs", "foo").tap do |output|
assert_match "No documentation found for foo", output
end
end
test "version" do
version = stdouted { Kamal::Cli::Main.new.version }
assert_equal Kamal::VERSION, version
@@ -517,4 +519,17 @@ class CliMainTest < CliTestCase
def run_command(*command, config_file: "deploy_simple")
stdouted { Kamal::Cli::Main.start([ *command, "-c", "test/fixtures/#{config_file}.yml" ]) }
end
def with_test_dot_env_erb(contents:, file: ".env.erb")
Dir.mktmpdir do |dir|
fixtures_dup = File.join(dir, "test")
FileUtils.mkdir_p(fixtures_dup)
FileUtils.cp_r("test/fixtures/", fixtures_dup)
Dir.chdir(dir) do
File.write(file, contents)
yield
end
end
end
end

View File

@@ -4,7 +4,7 @@ class CliTraefikTest < CliTestCase
test "boot" do
run_command("boot").tap do |output|
assert_match "docker login", output
assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output
assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Configuration::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output
end
end
@@ -14,7 +14,7 @@ class CliTraefikTest < CliTestCase
run_command("reboot", "-y").tap do |output|
assert_match "docker container stop traefik", output
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=Traefik", output
assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output
assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Configuration::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output
end
end

View File

@@ -137,17 +137,17 @@ class CommanderTest < ActiveSupport::TestCase
end
test "traefik hosts should observe filtered roles" do
configure_with(:deploy_with_aliases)
configure_with(:deploy_with_multiple_traefik_roles)
@kamal.specific_roles = [ "web_tokyo" ]
assert_equal [ "1.1.1.3", "1.1.1.4" ], @kamal.traefik_hosts
end
test "traefik hosts should observe filtered hosts" do
configure_with(:deploy_with_aliases)
configure_with(:deploy_with_multiple_traefik_roles)
@kamal.specific_hosts = [ "1.1.1.4" ]
assert_equal [ "1.1.1.4" ], @kamal.traefik_hosts
@kamal.specific_hosts = [ "1.1.1.2" ]
assert_equal [ "1.1.1.2" ], @kamal.traefik_hosts
end
private

View File

@@ -24,7 +24,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
"host" => "1.1.1.6",
"port" => "6379:6379",
"labels" => {
"cache" => true
"cache" => "true"
},
"env" => {
"SOMETHING" => "else"

View File

@@ -91,7 +91,7 @@ class CommandsTraefikTest < ActiveSupport::TestCase
@config.delete(:traefik)
assert_equal \
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"",
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{Kamal::Configuration::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"",
new_command.run.join(" ")
end

View File

@@ -35,7 +35,7 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
"hosts" => [ "1.1.1.6", "1.1.1.7" ],
"port" => "6379:6379",
"labels" => {
"cache" => true
"cache" => "true"
},
"env" => {
"SOMETHING" => "else"
@@ -44,7 +44,7 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
"/var/lib/redis:/data"
],
"options" => {
"cpus" => 4,
"cpus" => "4",
"memory" => "2GB"
}
},
@@ -54,13 +54,13 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
"roles" => [ "web" ],
"port" => "4321:4321",
"labels" => {
"cache" => true
"cache" => "true"
},
"env" => {
"STATSD_PORT" => "8126"
},
"options" => {
"cpus" => 4,
"cpus" => "4",
"memory" => "2GB"
}
}
@@ -89,22 +89,20 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
test "missing host" do
@deploy[:accessories]["mysql"]["host"] = nil
@config = Kamal::Configuration.new(@deploy)
assert_raises(ArgumentError) do
@config.accessory(:mysql).hosts
assert_raises(Kamal::ConfigurationError) do
Kamal::Configuration.new(@deploy)
end
end
test "setting host, hosts and roles" do
@deploy[:accessories]["mysql"]["hosts"] = true
@deploy[:accessories]["mysql"]["roles"] = true
@config = Kamal::Configuration.new(@deploy)
@deploy[:accessories]["mysql"]["hosts"] = [ "mysql-db1" ]
@deploy[:accessories]["mysql"]["roles"] = [ "db" ]
exception = assert_raises(ArgumentError) do
@config.accessory(:mysql).hosts
exception = assert_raises(Kamal::ConfigurationError) do
Kamal::Configuration.new(@deploy)
end
assert_equal "Specify one of `host`, `hosts` or `roles` for accessory `mysql`", exception.message
assert_equal "accessories/mysql: specify one of `host`, `hosts` or `roles`", exception.message
end
test "all hosts" do

View File

@@ -7,41 +7,37 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
servers: [ "1.1.1.1" ]
}
@config = Kamal::Configuration.new(@deploy)
@deploy_with_builder_option = {
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" },
servers: [ "1.1.1.1" ],
builder: {}
}
@config_with_builder_option = Kamal::Configuration.new(@deploy_with_builder_option)
end
test "multiarch?" do
assert_equal true, @config.builder.multiarch?
assert_equal true, config.builder.multiarch?
end
test "setting multiarch to false" do
@deploy_with_builder_option[:builder] = { "multiarch" => false }
assert_equal false, @config_with_builder_option.builder.multiarch?
assert_equal false, config_with_builder_option.builder.multiarch?
end
test "local?" do
assert_equal false, @config.builder.local?
assert_equal false, config.builder.local?
end
test "remote?" do
assert_equal false, @config.builder.remote?
assert_equal false, config.builder.remote?
end
test "remote_arch" do
assert_nil @config.builder.remote_arch
assert_nil config.builder.remote_arch
end
test "remote_host" do
assert_nil @config.builder.remote_host
assert_nil config.builder.remote_host
end
test "setting both local and remote configs" do
@@ -50,112 +46,121 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
"remote" => { "arch" => "amd64", "host" => "ssh://root@192.168.0.1" }
}
assert_equal true, @config_with_builder_option.builder.local?
assert_equal true, @config_with_builder_option.builder.remote?
assert_equal true, config_with_builder_option.builder.local?
assert_equal true, config_with_builder_option.builder.remote?
assert_equal "amd64", @config_with_builder_option.builder.remote_arch
assert_equal "ssh://root@192.168.0.1", @config_with_builder_option.builder.remote_host
assert_equal "amd64", config_with_builder_option.builder.remote_arch
assert_equal "ssh://root@192.168.0.1", config_with_builder_option.builder.remote_host
assert_equal "arm64", @config_with_builder_option.builder.local_arch
assert_equal "unix:///Users/<%= `whoami`.strip %>/.docker/run/docker.sock", @config_with_builder_option.builder.local_host
assert_equal "arm64", config_with_builder_option.builder.local_arch
assert_equal "unix:///Users/<%= `whoami`.strip %>/.docker/run/docker.sock", config_with_builder_option.builder.local_host
end
test "cached?" do
assert_equal false, @config.builder.cached?
assert_equal false, config.builder.cached?
end
test "invalid cache type specified" do
@deploy_with_builder_option[:builder] = { "cache" => { "type" => "invalid" } }
assert_raises(ArgumentError) do
@config_with_builder_option.builder
assert_raises(Kamal::ConfigurationError) do
config_with_builder_option.builder
end
end
test "cache_from" do
assert_nil @config.builder.cache_from
assert_nil config.builder.cache_from
end
test "cache_to" do
assert_nil @config.builder.cache_to
assert_nil config.builder.cache_to
end
test "setting gha cache" do
@deploy_with_builder_option[:builder] = { "cache" => { "type" => "gha", "options" => "mode=max" } }
assert_equal "type=gha", @config_with_builder_option.builder.cache_from
assert_equal "type=gha,mode=max", @config_with_builder_option.builder.cache_to
assert_equal "type=gha", config_with_builder_option.builder.cache_from
assert_equal "type=gha,mode=max", config_with_builder_option.builder.cache_to
end
test "setting registry cache" do
@deploy_with_builder_option[:builder] = { "cache" => { "type" => "registry", "options" => "mode=max,image-manifest=true,oci-mediatypes=true" } }
assert_equal "type=registry,ref=dhh/app-build-cache", @config_with_builder_option.builder.cache_from
assert_equal "type=registry,mode=max,image-manifest=true,oci-mediatypes=true,ref=dhh/app-build-cache", @config_with_builder_option.builder.cache_to
assert_equal "type=registry,ref=dhh/app-build-cache", config_with_builder_option.builder.cache_from
assert_equal "type=registry,mode=max,image-manifest=true,oci-mediatypes=true,ref=dhh/app-build-cache", config_with_builder_option.builder.cache_to
end
test "setting registry cache when using a custom registry" do
@config_with_builder_option.registry["server"] = "registry.example.com"
@deploy_with_builder_option[:registry]["server"] = "registry.example.com"
@deploy_with_builder_option[:builder] = { "cache" => { "type" => "registry", "options" => "mode=max,image-manifest=true,oci-mediatypes=true" } }
assert_equal "type=registry,ref=registry.example.com/dhh/app-build-cache", @config_with_builder_option.builder.cache_from
assert_equal "type=registry,mode=max,image-manifest=true,oci-mediatypes=true,ref=registry.example.com/dhh/app-build-cache", @config_with_builder_option.builder.cache_to
assert_equal "type=registry,ref=registry.example.com/dhh/app-build-cache", config_with_builder_option.builder.cache_from
assert_equal "type=registry,mode=max,image-manifest=true,oci-mediatypes=true,ref=registry.example.com/dhh/app-build-cache", config_with_builder_option.builder.cache_to
end
test "setting registry cache with image" do
@deploy_with_builder_option[:builder] = { "cache" => { "type" => "registry", "image" => "kamal", "options" => "mode=max" } }
assert_equal "type=registry,ref=kamal", @config_with_builder_option.builder.cache_from
assert_equal "type=registry,mode=max,ref=kamal", @config_with_builder_option.builder.cache_to
assert_equal "type=registry,ref=kamal", config_with_builder_option.builder.cache_from
assert_equal "type=registry,mode=max,ref=kamal", config_with_builder_option.builder.cache_to
end
test "args" do
assert_equal({}, @config.builder.args)
assert_equal({}, config.builder.args)
end
test "setting args" do
@deploy_with_builder_option[:builder] = { "args" => { "key" => "value" } }
assert_equal({ "key" => "value" }, @config_with_builder_option.builder.args)
assert_equal({ "key" => "value" }, config_with_builder_option.builder.args)
end
test "secrets" do
assert_equal [], @config.builder.secrets
assert_equal [], config.builder.secrets
end
test "setting secrets" do
@deploy_with_builder_option[:builder] = { "secrets" => [ "GITHUB_TOKEN" ] }
assert_equal [ "GITHUB_TOKEN" ], @config_with_builder_option.builder.secrets
assert_equal [ "GITHUB_TOKEN" ], config_with_builder_option.builder.secrets
end
test "dockerfile" do
assert_equal "Dockerfile", @config.builder.dockerfile
assert_equal "Dockerfile", config.builder.dockerfile
end
test "setting dockerfile" do
@deploy_with_builder_option[:builder] = { "dockerfile" => "Dockerfile.dev" }
assert_equal "Dockerfile.dev", @config_with_builder_option.builder.dockerfile
assert_equal "Dockerfile.dev", config_with_builder_option.builder.dockerfile
end
test "context" do
assert_equal ".", @config.builder.context
assert_equal ".", config.builder.context
end
test "setting context" do
@deploy_with_builder_option[:builder] = { "context" => ".." }
assert_equal "..", @config_with_builder_option.builder.context
assert_equal "..", config_with_builder_option.builder.context
end
test "ssh" do
assert_nil @config.builder.ssh
assert_nil config.builder.ssh
end
test "setting ssh params" do
@deploy_with_builder_option[:builder] = { "ssh" => "default=$SSH_AUTH_SOCK" }
assert_equal "default=$SSH_AUTH_SOCK", @config_with_builder_option.builder.ssh
assert_equal "default=$SSH_AUTH_SOCK", config_with_builder_option.builder.ssh
end
private
def config
Kamal::Configuration.new(@deploy)
end
def config_with_builder_option
Kamal::Configuration.new(@deploy_with_builder_option)
end
end

View File

@@ -19,7 +19,7 @@ class ConfigurationEnvTest < ActiveSupport::TestCase
test "secret" do
ENV["PASSWORD"] = "hello"
env = Kamal::Configuration::Env.from_config config: { "secret" => [ "PASSWORD" ] }
env = Kamal::Configuration::Env.new config: { "secret" => [ "PASSWORD" ] }
assert_config \
config: { "secret" => [ "PASSWORD" ] },
@@ -34,7 +34,7 @@ class ConfigurationEnvTest < ActiveSupport::TestCase
"secret" => [ "PASSWORD" ]
}
assert_raises(KeyError) { Kamal::Configuration::Env.from_config(config: { "secret" => [ "PASSWORD" ] }).secrets }
assert_raises(KeyError) { Kamal::Configuration::Env.new(config: { "secret" => [ "PASSWORD" ] }).secrets }
end
test "secret and clear" do
@@ -67,7 +67,7 @@ class ConfigurationEnvTest < ActiveSupport::TestCase
private
def assert_config(config:, clear:, secrets:)
env = Kamal::Configuration::Env.from_config config: config
env = Kamal::Configuration::Env.new config: config, secrets_file: "secrets.env"
assert_equal clear, env.clear
assert_equal secrets, env.secrets
end

View File

@@ -18,7 +18,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
"cmd" => "bin/jobs",
"env" => {
"REDIS_URL" => "redis://a/b",
"WEB_CONCURRENCY" => 4
"WEB_CONCURRENCY" => "4"
}
}
}
@@ -53,7 +53,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
test "custom labels via role specialization" do
@deploy_with_roles[:labels] = { "my.custom.label" => "50" }
@deploy_with_roles[:servers]["workers"]["labels"] = { "my.custom.label" => "70" }
assert_equal "70", @config_with_roles.role(:workers).labels["my.custom.label"]
assert_equal "70", Kamal::Configuration.new(@deploy_with_roles).role(:workers).labels["my.custom.label"]
end
test "overwriting default traefik label" do
@@ -63,7 +63,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
test "default traefik label on non-web role" do
config = Kamal::Configuration.new(@deploy_with_roles.tap { |c|
c[:servers]["beta"] = { "traefik" => "true", "hosts" => [ "1.1.1.5" ] }
c[:servers]["beta"] = { "traefik" => true, "hosts" => [ "1.1.1.5" ] }
})
assert_equal [ "--label", "service=\"app\"", "--label", "role=\"beta\"", "--label", "destination", "--label", "traefik.http.services.app-beta.loadbalancer.server.scheme=\"http\"", "--label", "traefik.http.routers.app-beta.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.routers.app-beta.priority=\"2\"", "--label", "traefik.http.middlewares.app-beta-retry.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app-beta-retry.retry.initialinterval=\"500ms\"", "--label", "traefik.http.routers.app-beta.middlewares=\"app-beta-retry@docker\"" ], config.role(:beta).label_args
@@ -102,7 +102,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
@deploy_with_roles[:servers]["workers"]["env"] = {
"clear" => {
"REDIS_URL" => "redis://a/b",
"WEB_CONCURRENCY" => 4
"WEB_CONCURRENCY" => "4"
},
"secret" => [
"DB_PASSWORD"
@@ -117,7 +117,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
DB_PASSWORD=secret&\"123
ENV
assert_equal expected_secrets_file, @config_with_roles.role(:workers).env("1.1.1.3").secrets_io.string
assert_equal expected_secrets_file, Kamal::Configuration.new(@deploy_with_roles).role(:workers).env("1.1.1.3").secrets_io.string
assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"" ], @config_with_roles.role(:workers).env_args("1.1.1.3")
ensure
ENV["REDIS_PASSWORD"] = nil
@@ -128,7 +128,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
@deploy_with_roles[:servers]["workers"]["env"] = {
"clear" => {
"REDIS_URL" => "redis://a/b",
"WEB_CONCURRENCY" => 4
"WEB_CONCURRENCY" => "4"
},
"secret" => [
"DB_PASSWORD"
@@ -141,7 +141,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
DB_PASSWORD=secret123
ENV
assert_equal expected_secrets_file, @config_with_roles.role(:workers).env("1.1.1.3").secrets_io.string
assert_equal expected_secrets_file, Kamal::Configuration.new(@deploy_with_roles).role(:workers).env("1.1.1.3").secrets_io.string
assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"" ], @config_with_roles.role(:workers).env_args("1.1.1.3")
ensure
ENV["DB_PASSWORD"] = nil
@@ -163,7 +163,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
REDIS_PASSWORD=secret456
ENV
assert_equal expected_secrets_file, @config_with_roles.role(:workers).env("1.1.1.3").secrets_io.string
assert_equal expected_secrets_file, Kamal::Configuration.new(@deploy_with_roles).role(:workers).env("1.1.1.3").secrets_io.string
assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"" ], @config_with_roles.role(:workers).env_args("1.1.1.3")
ensure
ENV["REDIS_PASSWORD"] = nil
@@ -191,8 +191,9 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
REDIS_PASSWORD=secret456
ENV
assert_equal expected_secrets_file, @config_with_roles.role(:workers).env("1.1.1.3").secrets_io.string
assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://c/d\"" ], @config_with_roles.role(:workers).env_args("1.1.1.3")
config = Kamal::Configuration.new(@deploy_with_roles)
assert_equal expected_secrets_file, config.role(:workers).env("1.1.1.3").secrets_io.string
assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://c/d\"" ], config.role(:workers).env_args("1.1.1.3")
ensure
ENV["REDIS_PASSWORD"] = nil
end

View File

@@ -0,0 +1,116 @@
require "test_helper"
class ConfigurationValidationTest < ActiveSupport::TestCase
test "unknown root key" do
assert_error "unknown key: unknown", unknown: "value"
assert_error "unknown keys: unknown, unknown2", unknown: "value", unknown2: "value"
end
test "wrong root types" do
[ :service, :image, :asset_path, :hooks_path, :primary_role, :minimum_version, :run_directory ].each do |key|
assert_error "#{key}: should be a string", **{ key => [] }
end
[ :require_destination, :allow_empty_roles ].each do |key|
assert_error "#{key}: should be a boolean", **{ key => "foo" }
end
[ :stop_wait_time, :retain_containers, :readiness_delay ].each do |key|
assert_error "#{key}: should be an integer", **{ key => "foo" }
end
assert_error "volumes: should be an array", volumes: "foo"
assert_error "servers: should be an array or a hash", servers: "foo"
[ :labels, :registry, :accessories, :env, :ssh, :sshkit, :builder, :traefik, :boot, :healthcheck, :logging ].each do |key|
assert_error "#{key}: should be a hash", **{ key =>[] }
end
end
test "servers" do
assert_error "servers: should be an array or a hash", servers: "foo"
assert_error "servers/0: should be a string or a hash", servers: [ [] ]
assert_error "servers/0: multiple hosts found", servers: [ { "a" => "b", "c" => "d" } ]
assert_error "servers/0/foo: should be a string or an array", servers: [ { "foo" => {} } ]
assert_error "servers/0/foo/0: should be a string", servers: [ { "foo" => [ [] ] } ]
end
test "roles" do
assert_error "servers/web: should be an array or a hash", servers: { "web" => "foo" }
assert_error "servers/web/hosts: should be an array", servers: { "web" => { "hosts" => "" } }
assert_error "servers/web/hosts/0: should be a string or a hash", servers: { "web" => { "hosts" => [ [] ] } }
assert_error "servers/web/options: should be a hash", servers: { "web" => { "options" => "" } }
assert_error "servers/web/logging/options: should be a hash", servers: { "web" => { "logging" => { "options" => "" } } }
assert_error "servers/web/logging/driver: should be a string", servers: { "web" => { "logging" => { "driver" => [] } } }
assert_error "servers/web/labels: should be a hash", servers: { "web" => { "labels" => [] } }
assert_error "servers/web/env: should be a hash", servers: { "web" => { "env" => [] } }
assert_error "servers/web/env: tags are only allowed in the root env", servers: { "web" => { "hosts" => [ "1.1.1.1" ], "env" => { "tags" => {} } } }
end
test "registry" do
assert_error "registry/username: is required", registry: {}
assert_error "registry/password: is required", registry: { "username" => "foo" }
assert_error "registry/password: should be a string or an array with one string (for secret lookup)", registry: { "username" => "foo", "password" => [ "SECRET1", "SECRET2" ] }
assert_error "registry/server: should be a string", registry: { "username" => "foo", "password" => "bar", "server" => [] }
end
test "accessories" do
assert_error "accessories/accessory1: should be a hash", accessories: { "accessory1" => [] }
assert_error "accessories/accessory1: unknown key: unknown", accessories: { "accessory1" => { "unknown" => "baz" } }
assert_error "accessories/accessory1/options: should be a hash", accessories: { "accessory1" => { "options" => [] } }
assert_error "accessories/accessory1/host: should be a string", accessories: { "accessory1" => { "host" => [] } }
assert_error "accessories/accessory1/env: should be a hash", accessories: { "accessory1" => { "env" => [] } }
assert_error "accessories/accessory1/env: tags are only allowed in the root env", accessories: { "accessory1" => { "host" => "host", "env" => { "tags" => {} } } }
end
test "env" do
assert_error "env: should be a hash", env: []
assert_error "env/FOO: should be a string", env: { "FOO" => [] }
assert_error "env/clear/FOO: should be a string", env: { "clear" => { "FOO" => [] } }
assert_error "env/secret: should be an array", env: { "secret" => { "FOO" => [] } }
assert_error "env/secret/0: should be a string", env: { "secret" => [ [] ] }
assert_error "env/tags: should be a hash", env: { "tags" => [] }
assert_error "env/tags/tag1: should be a hash", env: { "tags" => { "tag1" => "foo" } }
assert_error "env/tags/tag1/FOO: should be a string", env: { "tags" => { "tag1" => { "FOO" => [] } } }
assert_error "env/tags/tag1/clear/FOO: should be a string", env: { "tags" => { "tag1" => { "clear" => { "FOO" => [] } } } }
assert_error "env/tags/tag1/secret: should be an array", env: { "tags" => { "tag1" => { "secret" => {} } } }
assert_error "env/tags/tag1/secret/0: should be a string", env: { "tags" => { "tag1" => { "secret" => [ [] ] } } }
assert_error "env/tags/tag1: tags are only allowed in the root env", env: { "tags" => { "tag1" => { "tags" => {} } } }
end
test "ssh" do
assert_error "ssh: unknown key: foo", ssh: { "foo" => "bar" }
assert_error "ssh/user: should be a string", ssh: { "user" => [] }
end
test "sshkit" do
assert_error "sshkit: unknown key: foo", sshkit: { "foo" => "bar" }
assert_error "sshkit/max_concurrent_starts: should be an integer", sshkit: { "max_concurrent_starts" => "foo" }
end
test "builder" do
assert_error "builder: unknown key: foo", builder: { "foo" => "bar" }
assert_error "builder/remote: should be a hash", builder: { "remote" => true }
assert_error "builder/remote: unknown key: foo", builder: { "remote" => { "foo" => "bar" } }
assert_error "builder/local: unknown key: foo", builder: { "local" => { "foo" => "bar" } }
assert_error "builder/remote/arch: should be a string", builder: { "remote" => { "arch" => [] } }
assert_error "builder/args/foo: should be a string", builder: { "args" => { "foo" => [] } }
assert_error "builder/cache/options: should be a string", builder: { "cache" => { "options" => [] } }
end
private
def assert_error(message, **invalid_config)
valid_config = {
service: "app",
image: "app",
registry: { "username" => "user", "password" => "secret" },
servers: [ "1.1.1.1" ]
}
error = assert_raises Kamal::ConfigurationError do
Kamal::Configuration.new(valid_config.merge(invalid_config))
end
assert_equal message, error.message
end
end

View File

@@ -28,7 +28,7 @@ class ConfigurationTest < ActiveSupport::TestCase
%i[ service image registry ].each do |key|
test "#{key} config required" do
assert_raise(ArgumentError) do
assert_raise(Kamal::ConfigurationError) do
Kamal::Configuration.new @deploy.tap { _1.delete key }
end
end
@@ -36,19 +36,21 @@ class ConfigurationTest < ActiveSupport::TestCase
%w[ username password ].each do |key|
test "registry #{key} required" do
assert_raise(ArgumentError) do
assert_raise(Kamal::ConfigurationError) do
Kamal::Configuration.new @deploy.tap { _1[:registry].delete key }
end
end
end
test "service name valid" do
assert Kamal::Configuration.new(@deploy.tap { _1[:service] = "hey-app1_primary" }).valid?
assert Kamal::Configuration.new(@deploy.tap { _1[:service] = "MyApp" }).valid?
assert_nothing_raised do
Kamal::Configuration.new(@deploy.tap { _1[:service] = "hey-app1_primary" })
Kamal::Configuration.new(@deploy.tap { _1[:service] = "MyApp" })
end
end
test "service name invalid" do
assert_raise(ArgumentError) do
assert_raise(Kamal::ConfigurationError) do
Kamal::Configuration.new @deploy.tap { _1[:service] = "app.com" }
end
end
@@ -158,39 +160,34 @@ class ConfigurationTest < ActiveSupport::TestCase
assert_equal "healthcheck-app", @config.healthcheck_service
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
assert_raises(Kamal::ConfigurationError) do
Kamal::Configuration.new @deploy.merge(servers: [])
end
# Empty server list
assert_raises(ArgumentError) do
assert_raises(Kamal::ConfigurationError) do
Kamal::Configuration.new @deploy.merge(servers: { "web" => [] })
end
# Missing hosts key
assert_raises(ArgumentError) do
assert_raises(Kamal::ConfigurationError) do
Kamal::Configuration.new @deploy.merge(servers: { "web" => {} })
end
# Empty hosts list
assert_raises(ArgumentError) do
assert_raises(Kamal::ConfigurationError) do
Kamal::Configuration.new @deploy.merge(servers: { "web" => { "hosts" => [] } })
end
# Nil hosts
assert_raises(ArgumentError) do
assert_raises(Kamal::ConfigurationError) do
Kamal::Configuration.new @deploy.merge(servers: { "web" => { "hosts" => nil } })
end
# One role with hosts, one without
assert_raises(ArgumentError) do
assert_raises(Kamal::ConfigurationError) do
Kamal::Configuration.new @deploy.merge(servers: { "web" => %w[ web ], "workers" => { "hosts" => %w[ ] } })
end
end
@@ -200,7 +197,7 @@ class ConfigurationTest < ActiveSupport::TestCase
Kamal::Configuration.new @deploy.merge(servers: { "web" => %w[ web ], "workers" => { "hosts" => %w[ ] } }, allow_empty_roles: true)
end
assert_raises(ArgumentError) do
assert_raises(Kamal::ConfigurationError) do
Kamal::Configuration.new @deploy.merge(servers: { "web" => %w[], "workers" => { "hosts" => %w[] } }, allow_empty_roles: true)
end
end
@@ -215,17 +212,17 @@ class ConfigurationTest < ActiveSupport::TestCase
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
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
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"]
assert_equal "my-user", config.registry.username
end
test "destination yml config merge" do
@@ -249,7 +246,7 @@ class ConfigurationTest < ActiveSupport::TestCase
test "destination required" do
dest_config_file = Pathname.new(File.expand_path("fixtures/deploy_for_required_dest.yml", __dir__))
assert_raises(ArgumentError) do
assert_raises(Kamal::ConfigurationError) do
config = Kamal::Configuration.create_from config_file: dest_config_file
end
@@ -272,7 +269,7 @@ class ConfigurationTest < ActiveSupport::TestCase
volume_args: [ "--volume", "/local/path:/container/path" ],
builder: {},
logging: [ "--log-opt", "max-size=\"10m\"" ],
healthcheck: { "path"=>"/up", "port"=>3000, "max_attempts" => 7, "cord" => "/tmp/kamal-cord", "log_lines" => 50 } }
healthcheck: { "cmd"=>"curl -f http://localhost:3000/up || exit 1", "interval" => "1s", "path"=>"/up", "port"=>3000, "max_attempts" => 7, "cord" => "/tmp/kamal-cord", "log_lines" => 50 } }
assert_equal expected_config, @config.to_h
end
@@ -288,7 +285,7 @@ class ConfigurationTest < ActiveSupport::TestCase
end
test "min version is higher" do
assert_raises(ArgumentError) do
assert_raises(Kamal::ConfigurationError) do
Kamal::Configuration.new(@deploy.tap { |c| c.merge!(minimum_version: "10000.0.0") })
end
end
@@ -334,7 +331,7 @@ class ConfigurationTest < ActiveSupport::TestCase
end
test "primary role missing" do
error = assert_raises(ArgumentError) do
error = assert_raises(Kamal::ConfigurationError) do
Kamal::Configuration.new(@deploy.merge(primary_role: "bar"))
end
assert_match /bar isn't defined/, error.message
@@ -345,6 +342,6 @@ class ConfigurationTest < ActiveSupport::TestCase
config = Kamal::Configuration.new(@deploy_with_roles.merge(retain_containers: 2))
assert_equal 2, config.retain_containers
assert_raises(ArgumentError) { Kamal::Configuration.new(@deploy_with_roles.merge(retain_containers: 0)) }
assert_raises(Kamal::ConfigurationError) { Kamal::Configuration.new(@deploy_with_roles.merge(retain_containers: 0)) }
end
end

View File

@@ -2,12 +2,12 @@ service: app
image: dhh/app
servers:
web_chicago:
traefik: enabled
traefik: true
hosts:
- 1.1.1.1
- 1.1.1.2
web_tokyo:
traefik: enabled
traefik: true
hosts:
- 1.1.1.3
- 1.1.1.4

View File

@@ -1,36 +0,0 @@
# helper aliases
chicago_hosts: &chicago_hosts
hosts:
- 1.1.1.1
- 1.1.1.2
tokyo_hosts: &tokyo_hosts
hosts:
- 1.1.1.3
- 1.1.1.4
web_common: &web_common
env:
ROLE: "web"
traefik: true
# actual config
service: app
image: dhh/app
servers:
web:
<<: *chicago_hosts
<<: *web_common
web_tokyo:
<<: *tokyo_hosts
<<: *web_common
workers:
cmd: bin/jobs
<<: *chicago_hosts
workers_tokyo:
cmd: bin/jobs
<<: *tokyo_hosts
env:
REDIS_URL: redis://x/y
registry:
server: registry.digitalocean.com
username: user
password: pw

View File

@@ -0,0 +1,34 @@
# actual config
service: app
image: dhh/app
servers:
web:
hosts:
- 1.1.1.1
- 1.1.1.2
env:
ROLE: "web"
traefik: true
web_tokyo:
hosts:
- 1.1.1.3
- 1.1.1.4
env:
ROLE: "web"
traefik: true
workers:
cmd: bin/jobs
hosts:
- 1.1.1.1
- 1.1.1.2
workers_tokyo:
cmd: bin/jobs
hosts:
- 1.1.1.3
- 1.1.1.4
env:
REDIS_URL: redis://x/y
registry:
server: registry.digitalocean.com
username: user
password: pw

View File

@@ -146,6 +146,6 @@ class IntegrationTest < ActiveSupport::TestCase
end
def container_running?(host:, name:)
docker_compose("exec #{host} docker ps --filter=name=#{name} | tail -n+2", capture: true).tap { |x| p [ x, x.strip, x.strip.present? ] }.strip.present?
docker_compose("exec #{host} docker ps --filter=name=#{name} | tail -n+2", capture: true).strip.present?
end
end

View File

@@ -79,7 +79,7 @@ class MainTest < IntegrationTest
assert_equal({ user: "root", port: 22, keepalive: true, keepalive_interval: 30, log_level: :fatal }, config[:ssh_options])
assert_equal({ "multiarch" => false, "args" => { "COMMIT_SHA" => version } }, config[:builder])
assert_equal [ "--log-opt", "max-size=\"10m\"" ], config[:logging]
assert_equal({ "path" => "/up", "port" => 3000, "max_attempts" => 3, "cord"=>"/tmp/kamal-cord", "log_lines" => 50, "cmd"=>"wget -qO- http://localhost > /dev/null || exit 1" }, config[:healthcheck])
assert_equal({ "cmd"=>"wget -qO- http://localhost > /dev/null || exit 1", "interval"=>"1s", "max_attempts"=>3, "port"=>3000, "path"=>"/up", "cord"=>"/tmp/kamal-cord", "log_lines"=>50 }, config[:healthcheck])
end
test "setup and remove" do