Lazily load secrets whenever needed

This commit is contained in:
Donal McBreen
2024-08-05 14:41:50 +01:00
committed by Donal McBreen
parent 6a06efc9d9
commit 56754fe40c
43 changed files with 391 additions and 529 deletions

View File

@@ -116,25 +116,12 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
end
test "env args" do
assert_equal [ "--env-file", ".kamal/env/accessories/app-mysql.env", "--env", "MYSQL_ROOT_HOST=\"%\"" ], @config.accessory(:mysql).env_args
assert_equal [ "--env-file", ".kamal/env/accessories/app-redis.env", "--env", "SOMETHING=\"else\"" ], @config.accessory(:redis).env_args
end
with_test_secrets("secrets" => "MYSQL_ROOT_PASSWORD=secret123") do
config = Kamal::Configuration.new(@deploy)
test "env with secrets" do
ENV["MYSQL_ROOT_PASSWORD"] = "secret123"
expected_secrets_file = <<~ENV
MYSQL_ROOT_PASSWORD=secret123
ENV
assert_equal expected_secrets_file, @config.accessory(:mysql).env.secrets_io.string
assert_equal [ "--env-file", ".kamal/env/accessories/app-mysql.env", "--env", "MYSQL_ROOT_HOST=\"%\"" ], @config.accessory(:mysql).env_args
ensure
ENV["MYSQL_ROOT_PASSWORD"] = nil
end
test "env secrets path" do
assert_equal ".kamal/env/accessories/app-mysql.env", @config.accessory(:mysql).env.secrets_file
assert_equal [ "--env", "MYSQL_ROOT_HOST=\"%\"", "--env", "MYSQL_ROOT_PASSWORD=\"secret123\"" ], config.accessory(:mysql).env_args.map(&:to_s)
assert_equal [ "--env", "SOMETHING=\"else\"" ], @config.accessory(:redis).env_args
end
end
test "volume args" do

View File

@@ -93,13 +93,15 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
end
test "secrets" do
assert_equal [], config.builder.secrets
assert_equal({}, config.builder.secrets)
end
test "setting secrets" do
@deploy[:builder]["secrets"] = [ "GITHUB_TOKEN" ]
with_test_secrets("secrets" => "GITHUB_TOKEN=secret123") do
@deploy[:builder]["secrets"] = [ "GITHUB_TOKEN" ]
assert_equal [ "GITHUB_TOKEN" ], config.builder.secrets
assert_equal({ "GITHUB_TOKEN" => "secret123" }, config.builder.secrets)
end
end
test "dockerfile" do

View File

@@ -79,23 +79,21 @@ class ConfigurationEnvTagsTest < ActiveSupport::TestCase
end
test "tag secret env" do
ENV["PASSWORD"] = "hello"
deploy = {
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" },
servers: [ { "1.1.1.1" => "secrets" } ],
builder: { "arch" => "amd64" },
env: {
"tags" => {
"secrets" => { "secret" => [ "PASSWORD" ] }
with_test_secrets("secrets" => "PASSWORD=hello") do
deploy = {
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" },
servers: [ { "1.1.1.1" => "secrets" } ],
builder: { "arch" => "amd64" },
env: {
"tags" => {
"secrets" => { "secret" => [ "PASSWORD" ] }
}
}
}
}
config = Kamal::Configuration.new(deploy)
assert_equal "hello", config.role("web").env("1.1.1.1").secrets["PASSWORD"]
ensure
ENV.delete "PASSWORD"
config = Kamal::Configuration.new(deploy)
assert_equal "hello", config.role("web").env("1.1.1.1").secrets["PASSWORD"]
end
end
test "tag clear env" do

View File

@@ -6,27 +6,21 @@ class ConfigurationEnvTest < ActiveSupport::TestCase
test "simple" do
assert_config \
config: { "foo" => "bar", "baz" => "haz" },
clear: { "foo" => "bar", "baz" => "haz" },
secrets: {}
results: { "foo" => "bar", "baz" => "haz" }
end
test "clear" do
assert_config \
config: { "clear" => { "foo" => "bar", "baz" => "haz" } },
clear: { "foo" => "bar", "baz" => "haz" },
secrets: {}
results: { "foo" => "bar", "baz" => "haz" }
end
test "secret" do
ENV["PASSWORD"] = "hello"
env = Kamal::Configuration::Env.new config: { "secret" => [ "PASSWORD" ] }
assert_config \
config: { "secret" => [ "PASSWORD" ] },
clear: {},
secrets: { "PASSWORD" => "hello" }
ensure
ENV.delete "PASSWORD"
with_test_secrets("secrets" => "PASSWORD=hello") do
assert_config \
config: { "secret" => [ "PASSWORD" ] },
results: { "PASSWORD" => "hello" }
end
end
test "missing secret" do
@@ -34,41 +28,29 @@ class ConfigurationEnvTest < ActiveSupport::TestCase
"secret" => [ "PASSWORD" ]
}
assert_raises(KeyError) { Kamal::Configuration::Env.new(config: { "secret" => [ "PASSWORD" ] }).secrets }
assert_raises(Kamal::ConfigurationError) { Kamal::Configuration::Env.new(config: { "secret" => [ "PASSWORD" ] }, secrets: Kamal::Configuration::Secrets.new).args }
end
test "secret and clear" do
ENV["PASSWORD"] = "hello"
config = {
"secret" => [ "PASSWORD" ],
"clear" => {
"foo" => "bar",
"baz" => "haz"
with_test_secrets("secrets" => "PASSWORD=hello") do
config = {
"secret" => [ "PASSWORD" ],
"clear" => {
"foo" => "bar",
"baz" => "haz"
}
}
}
assert_config \
config: config,
clear: { "foo" => "bar", "baz" => "haz" },
secrets: { "PASSWORD" => "hello" }
ensure
ENV.delete "PASSWORD"
end
test "stringIO conversion" do
env = {
"foo" => "bar",
"baz" => "haz"
}
assert_equal "foo=bar\nbaz=haz\n", \
StringIO.new(Kamal::EnvFile.new(env)).read
assert_config \
config: config,
results: { "foo" => "bar", "baz" => "haz", "PASSWORD" => "hello" }
end
end
private
def assert_config(config:, clear:, secrets:)
env = Kamal::Configuration::Env.new config: config, secrets_file: "secrets.env"
assert_equal clear, env.clear
assert_equal secrets, env.secrets
def assert_config(config:, results:)
env = Kamal::Configuration::Env.new config: config, secrets: Kamal::Configuration::Secrets.new
expected_args = results.to_a.flat_map { |key, value| [ "--env", "#{key}=\"#{value}\"" ] }
assert_equal expected_args, env.args.map(&:to_s) #  to_s removes the redactions
end
end

View File

@@ -9,8 +9,6 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
env: { "REDIS_URL" => "redis://x/y" }
}
@config = Kamal::Configuration.new(@deploy)
@deploy_with_roles = @deploy.dup.merge({
servers: {
"web" => [ "1.1.1.1", "1.1.1.2" ],
@@ -24,31 +22,29 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
}
}
})
@config_with_roles = Kamal::Configuration.new(@deploy_with_roles)
end
test "hosts" do
assert_equal [ "1.1.1.1", "1.1.1.2" ], @config.role(:web).hosts
assert_equal [ "1.1.1.3", "1.1.1.4" ], @config_with_roles.role(:workers).hosts
assert_equal [ "1.1.1.1", "1.1.1.2" ], config.role(:web).hosts
assert_equal [ "1.1.1.3", "1.1.1.4" ], config_with_roles.role(:workers).hosts
end
test "cmd" do
assert_nil @config.role(:web).cmd
assert_equal "bin/jobs", @config_with_roles.role(:workers).cmd
assert_nil config.role(:web).cmd
assert_equal "bin/jobs", config_with_roles.role(:workers).cmd
end
test "label args" do
assert_equal [ "--label", "service=\"app\"", "--label", "role=\"workers\"", "--label", "destination" ], @config_with_roles.role(:workers).label_args
assert_equal [ "--label", "service=\"app\"", "--label", "role=\"workers\"", "--label", "destination" ], config_with_roles.role(:workers).label_args
end
test "special label args for web" do
assert_equal [ "--label", "service=\"app\"", "--label", "role=\"web\"", "--label", "destination", "--label", "traefik.http.services.app-web.loadbalancer.server.scheme=\"http\"", "--label", "traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.routers.app-web.priority=\"2\"", "--label", "traefik.http.middlewares.app-web-retry.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\"", "--label", "traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\"" ], @config.role(:web).label_args
assert_equal [ "--label", "service=\"app\"", "--label", "role=\"web\"", "--label", "destination", "--label", "traefik.http.services.app-web.loadbalancer.server.scheme=\"http\"", "--label", "traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.routers.app-web.priority=\"2\"", "--label", "traefik.http.middlewares.app-web-retry.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\"", "--label", "traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\"" ], config.role(:web).label_args
end
test "custom labels" do
@deploy[:labels] = { "my.custom.label" => "50" }
assert_equal "50", @config.role(:web).labels["my.custom.label"]
assert_equal "50", config.role(:web).labels["my.custom.label"]
end
test "custom labels via role specialization" do
@@ -59,7 +55,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
test "overwriting default traefik label" do
@deploy[:labels] = { "traefik.http.routers.app-web.rule" => "\"Host(\\`example.com\\`) || (Host(\\`example.org\\`) && Path(\\`/traefik\\`))\"" }
assert_equal "\"Host(\\`example.com\\`) || (Host(\\`example.org\\`) && Path(\\`/traefik\\`))\"", @config.role(:web).labels["traefik.http.routers.app-web.rule"]
assert_equal "\"Host(\\`example.com\\`) || (Host(\\`example.org\\`) && Path(\\`/traefik\\`))\"", config.role(:web).labels["traefik.http.routers.app-web.rule"]
end
test "default traefik label on non-web role" do
@@ -71,166 +67,149 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
end
test "env overwritten by role" do
assert_equal "redis://a/b", @config_with_roles.role(:workers).env("1.1.1.3").clear["REDIS_URL"]
assert_equal "redis://a/b", config_with_roles.role(:workers).env("1.1.1.3").clear["REDIS_URL"]
assert_equal "\n", @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://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"" ], @config_with_roles.role(:workers).env_args("1.1.1.3")
assert_equal [
"--env", "REDIS_URL=\"redis://a/b\"",
"--env", "WEB_CONCURRENCY=\"4\"" ],
config_with_roles.role(:workers).env_args("1.1.1.3")
end
test "container name" do
ENV["VERSION"] = "12345"
assert_equal "app-workers-12345", @config_with_roles.role(:workers).container_name
assert_equal "app-web-12345", @config_with_roles.role(:web).container_name
assert_equal "app-workers-12345", config_with_roles.role(:workers).container_name
assert_equal "app-web-12345", config_with_roles.role(:web).container_name
ensure
ENV.delete("VERSION")
end
test "env args" do
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")
assert_equal [ "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"" ], config_with_roles.role(:workers).env_args("1.1.1.3")
end
test "env secret overwritten by role" do
@deploy_with_roles[:env] = {
"clear" => {
"REDIS_URL" => "redis://a/b"
},
"secret" => [
"REDIS_PASSWORD"
]
}
with_test_secrets("secrets" => "REDIS_PASSWORD=secret456\nDB_PASSWORD=secret&\"123") do
@deploy_with_roles[:env] = {
"clear" => {
"REDIS_URL" => "redis://a/b"
},
"secret" => [
"REDIS_PASSWORD"
]
}
@deploy_with_roles[:servers]["workers"]["env"] = {
"clear" => {
"REDIS_URL" => "redis://a/b",
"WEB_CONCURRENCY" => "4"
},
"secret" => [
"DB_PASSWORD"
]
}
@deploy_with_roles[:servers]["workers"]["env"] = {
"clear" => {
"REDIS_URL" => "redis://a/b",
"WEB_CONCURRENCY" => "4"
},
"secret" => [
"DB_PASSWORD"
]
}
ENV["REDIS_PASSWORD"] = "secret456"
ENV["DB_PASSWORD"] = "secret&\"123"
expected_secrets_file = <<~ENV
REDIS_PASSWORD=secret456
DB_PASSWORD=secret&\"123
ENV
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
ENV["DB_PASSWORD"] = nil
assert_equal [
"--env", "REDIS_URL=\"redis://a/b\"",
"--env", "WEB_CONCURRENCY=\"4\"",
"--env", "REDIS_PASSWORD=\"secret456\"",
"--env", "DB_PASSWORD=\"secret&\\\"123\"" ],
config_with_roles.role(:workers).env_args("1.1.1.3").map(&:to_s)
end
end
test "env secrets only in role" do
@deploy_with_roles[:servers]["workers"]["env"] = {
"clear" => {
"REDIS_URL" => "redis://a/b",
"WEB_CONCURRENCY" => "4"
},
"secret" => [
"DB_PASSWORD"
]
}
with_test_secrets("secrets" => "DB_PASSWORD=secret123") do
@deploy_with_roles[:servers]["workers"]["env"] = {
"clear" => {
"REDIS_URL" => "redis://a/b",
"WEB_CONCURRENCY" => "4"
},
"secret" => [
"DB_PASSWORD"
]
}
ENV["DB_PASSWORD"] = "secret123"
expected_secrets_file = <<~ENV
DB_PASSWORD=secret123
ENV
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
assert_equal [
"--env", "REDIS_URL=\"redis://a/b\"",
"--env", "WEB_CONCURRENCY=\"4\"",
"--env", "DB_PASSWORD=\"secret123\"" ],
config_with_roles.role(:workers).env_args("1.1.1.3").map(&:to_s)
end
end
test "env secrets only at top level" do
@deploy_with_roles[:env] = {
"clear" => {
"REDIS_URL" => "redis://a/b"
},
"secret" => [
"REDIS_PASSWORD"
]
}
with_test_secrets("secrets" => "REDIS_PASSWORD=secret456") do
@deploy_with_roles[:env] = {
"clear" => {
"REDIS_URL" => "redis://a/b"
},
"secret" => [
"REDIS_PASSWORD"
]
}
ENV["REDIS_PASSWORD"] = "secret456"
expected_secrets_file = <<~ENV
REDIS_PASSWORD=secret456
ENV
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
assert_equal [
"--env", "REDIS_URL=\"redis://a/b\"",
"--env", "WEB_CONCURRENCY=\"4\"",
"--env", "REDIS_PASSWORD=\"secret456\"" ],
config_with_roles.role(:workers).env_args("1.1.1.3").map(&:to_s)
end
end
test "env overwritten by role with secrets" do
@deploy_with_roles[:env] = {
"clear" => {
"REDIS_URL" => "redis://a/b"
},
"secret" => [
"REDIS_PASSWORD"
]
}
@deploy_with_roles[:servers]["workers"]["env"] = {
"clear" => {
"REDIS_URL" => "redis://c/d"
with_test_secrets("secrets" => "REDIS_PASSWORD=secret456") do
@deploy_with_roles[:env] = {
"clear" => {
"REDIS_URL" => "redis://a/b"
},
"secret" => [
"REDIS_PASSWORD"
]
}
}
ENV["REDIS_PASSWORD"] = "secret456"
@deploy_with_roles[:servers]["workers"]["env"] = {
"clear" => {
"REDIS_URL" => "redis://c/d"
}
}
expected_secrets_file = <<~ENV
REDIS_PASSWORD=secret456
ENV
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
test "env secrets_file" do
assert_equal ".kamal/env/roles/app-workers.env", @config_with_roles.role(:workers).env("1.1.1.3").secrets_file
config = config_with_roles
assert_equal [
"--env", "REDIS_URL=\"redis://c/d\"",
"--env", "REDIS_PASSWORD=\"secret456\"" ],
config.role(:workers).env_args("1.1.1.3").map(&:to_s)
end
end
test "uses cord" do
assert @config_with_roles.role(:web).uses_cord?
assert_not @config_with_roles.role(:workers).uses_cord?
assert config_with_roles.role(:web).uses_cord?
assert_not config_with_roles.role(:workers).uses_cord?
end
test "cord host file" do
assert_match %r{.kamal/cords/app-web-[0-9a-f]{32}/cord}, @config_with_roles.role(:web).cord_host_file
assert_match %r{.kamal/cords/app-web-[0-9a-f]{32}/cord}, config_with_roles.role(:web).cord_host_file
end
test "cord volume" do
assert_equal "/tmp/kamal-cord", @config_with_roles.role(:web).cord_volume.container_path
assert_match %r{.kamal/cords/app-web-[0-9a-f]{32}}, @config_with_roles.role(:web).cord_volume.host_path
assert_equal "--volume", @config_with_roles.role(:web).cord_volume.docker_args[0]
assert_match %r{\$\(pwd\)/.kamal/cords/app-web-[0-9a-f]{32}:/tmp/kamal-cord}, @config_with_roles.role(:web).cord_volume.docker_args[1]
assert_equal "/tmp/kamal-cord", config_with_roles.role(:web).cord_volume.container_path
assert_match %r{.kamal/cords/app-web-[0-9a-f]{32}}, config_with_roles.role(:web).cord_volume.host_path
assert_equal "--volume", config_with_roles.role(:web).cord_volume.docker_args[0]
assert_match %r{\$\(pwd\)/.kamal/cords/app-web-[0-9a-f]{32}:/tmp/kamal-cord}, config_with_roles.role(:web).cord_volume.docker_args[1]
end
test "cord container file" do
assert_equal "/tmp/kamal-cord/cord", @config_with_roles.role(:web).cord_container_file
assert_equal "/tmp/kamal-cord/cord", config_with_roles.role(:web).cord_container_file
end
test "asset path and volume args" do
ENV["VERSION"] = "12345"
assert_nil @config_with_roles.role(:web).asset_volume_args
assert_nil @config_with_roles.role(:workers).asset_volume_args
assert_nil @config_with_roles.role(:web).asset_path
assert_nil @config_with_roles.role(:workers).asset_path
assert_not @config_with_roles.role(:web).assets?
assert_not @config_with_roles.role(:workers).assets?
assert_nil config_with_roles.role(:web).asset_volume_args
assert_nil config_with_roles.role(:workers).asset_volume_args
assert_nil config_with_roles.role(:web).asset_path
assert_nil config_with_roles.role(:workers).asset_path
assert_not config_with_roles.role(:web).assets?
assert_not config_with_roles.role(:workers).assets?
config_with_assets = Kamal::Configuration.new(@deploy_with_roles.dup.tap { |c|
c[:asset_path] = "foo"
@@ -258,17 +237,26 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
test "asset extracted path" do
ENV["VERSION"] = "12345"
assert_equal ".kamal/assets/extracted/app-web-12345", @config_with_roles.role(:web).asset_extracted_path
assert_equal ".kamal/assets/extracted/app-workers-12345", @config_with_roles.role(:workers).asset_extracted_path
assert_equal ".kamal/assets/extracted/app-web-12345", config_with_roles.role(:web).asset_extracted_path
assert_equal ".kamal/assets/extracted/app-workers-12345", config_with_roles.role(:workers).asset_extracted_path
ensure
ENV.delete("VERSION")
end
test "asset volume path" do
ENV["VERSION"] = "12345"
assert_equal ".kamal/assets/volumes/app-web-12345", @config_with_roles.role(:web).asset_volume_path
assert_equal ".kamal/assets/volumes/app-workers-12345", @config_with_roles.role(:workers).asset_volume_path
assert_equal ".kamal/assets/volumes/app-web-12345", config_with_roles.role(:web).asset_volume_path
assert_equal ".kamal/assets/volumes/app-workers-12345", config_with_roles.role(:workers).asset_volume_path
ensure
ENV.delete("VERSION")
end
private
def config
Kamal::Configuration.new(@deploy)
end
def config_with_roles
Kamal::Configuration.new(@deploy_with_roles)
end
end