From 49afdbb09a2301641559b05e90abd2895e24ad3a Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 20 Mar 2024 16:37:09 +0000 Subject: [PATCH] Always send the clear env to the container Secret and clear env variables have different lifecycles. The clear ones are part of the repo, so it makes sense to always deploy them with the rest of the repo. The secret ones are external so we can't be sure that they are up to date, therefore they require an explicit push via `envify` or `env push`. We'll keep the env file, but now it just contains secrets. The clear values are passed directly to `docker run`. --- lib/kamal/cli/env.rb | 6 +-- lib/kamal/commands/accessory.rb | 4 +- lib/kamal/commands/app.rb | 4 +- lib/kamal/commands/traefik.rb | 20 +++----- lib/kamal/configuration.rb | 11 +++-- lib/kamal/configuration/accessory.rb | 22 +++------ lib/kamal/configuration/env.rb | 40 +++++++++++++++ lib/kamal/configuration/role.rb | 42 +++------------- lib/kamal/env_file.rb | 14 +----- test/cli/accessory_test.rb | 4 +- test/commands/accessory_test.rb | 8 +-- test/commands/traefik_test.rb | 8 +-- test/configuration/accessory_test.rb | 20 +++----- test/configuration/env_test.rb | 74 ++++++++++++++++++++++++++++ test/configuration/role_test.rb | 47 +++++++----------- test/env_file_test.rb | 69 +++----------------------- test/integration/main_test.rb | 8 ++- 17 files changed, 201 insertions(+), 200 deletions(-) create mode 100644 lib/kamal/configuration/env.rb create mode 100644 test/configuration/env_test.rb diff --git a/lib/kamal/cli/env.rb b/lib/kamal/cli/env.rb index 1a02aeb1..3cfb1741 100644 --- a/lib/kamal/cli/env.rb +++ b/lib/kamal/cli/env.rb @@ -9,20 +9,20 @@ class Kamal::Cli::Env < Kamal::Cli::Base KAMAL.roles_on(host).each do |role| execute *KAMAL.app(role: role).make_env_directory - upload! StringIO.new(role.env_file), role.host_env_file_path, mode: 400 + upload! role.env.secrets_io, role.env.secrets_file, mode: 400 end end on(KAMAL.traefik_hosts) do execute *KAMAL.traefik.make_env_directory - upload! StringIO.new(KAMAL.traefik.env_file), KAMAL.traefik.host_env_file_path, mode: 400 + upload! KAMAL.traefik.env.secrets_io, KAMAL.traefik.env.secrets_file, mode: 400 end on(KAMAL.accessory_hosts) do KAMAL.accessories_on(host).each do |accessory| accessory_config = KAMAL.config.accessory(accessory) execute *KAMAL.accessory(accessory).make_env_directory - upload! StringIO.new(accessory_config.env_file), accessory_config.host_env_file_path, mode: 400 + upload! accessory_config.env.secrets_io, accessory_config.env.secrets_file, mode: 400 end end end diff --git a/lib/kamal/commands/accessory.rb b/lib/kamal/commands/accessory.rb index c4d4545f..661ab7ee 100644 --- a/lib/kamal/commands/accessory.rb +++ b/lib/kamal/commands/accessory.rb @@ -99,11 +99,11 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base end def make_env_directory - make_directory accessory_config.host_env_directory + make_directory accessory_config.env.secrets_directory end def remove_env_file - [ :rm, "-f", accessory_config.host_env_file_path ] + [ :rm, "-f", accessory_config.env.secrets_file ] end private diff --git a/lib/kamal/commands/app.rb b/lib/kamal/commands/app.rb index c49c228b..1695b32a 100644 --- a/lib/kamal/commands/app.rb +++ b/lib/kamal/commands/app.rb @@ -68,11 +68,11 @@ class Kamal::Commands::App < Kamal::Commands::Base def make_env_directory - make_directory role.host_env_directory + make_directory role.env.secrets_directory end def remove_env_file - [ :rm, "-f", role.host_env_file_path ] + [ :rm, "-f", role.env.secrets_file ] end diff --git a/lib/kamal/commands/traefik.rb b/lib/kamal/commands/traefik.rb index ca5b65bd..569c2c2c 100644 --- a/lib/kamal/commands/traefik.rb +++ b/lib/kamal/commands/traefik.rb @@ -71,20 +71,18 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base "#{host_port}:#{CONTAINER_PORT}" end - def env_file - Kamal::EnvFile.new(config.traefik.fetch("env", {})) - end - - def host_env_file_path - File.join host_env_directory, "traefik.env" + def env + Kamal::Configuration::Env.from_config \ + config: config.traefik.fetch("env", {}), + secrets_file: File.join(config.host_env_directory, "traefik", "traefik.env") end def make_env_directory - make_directory(host_env_directory) + make_directory(env.secrets_directory) end def remove_env_file - [ :rm, "-f", host_env_file_path ] + [ :rm, "-f", env.secrets_file ] end private @@ -97,11 +95,7 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base end def env_args - argumentize "--env-file", host_env_file_path - end - - def host_env_directory - File.join config.host_env_directory, "traefik" + env.args end def labels diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index 7ac5dc1b..bd5189d9 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -6,7 +6,7 @@ require "erb" require "net/ssh/proxy/jump" class Kamal::Configuration - delegate :service, :image, :servers, :env, :labels, :registry, :stop_wait_time, :hooks_path, :logging, to: :raw_config, allow_nil: true + delegate :service, :image, :servers, :labels, :registry, :stop_wait_time, :hooks_path, :logging, to: :raw_config, allow_nil: true delegate :argumentize, :optionize, to: Kamal::Utils attr_reader :destination, :raw_config @@ -216,12 +216,17 @@ class Kamal::Configuration raw_config.hooks_path || ".kamal/hooks" end + def asset_path + raw_config.asset_path + end + + def host_env_directory "#{run_directory}/env" end - def asset_path - raw_config.asset_path + def env + raw_config.env || {} end diff --git a/lib/kamal/configuration/accessory.rb b/lib/kamal/configuration/accessory.rb index f83b6a1e..1477031e 100644 --- a/lib/kamal/configuration/accessory.rb +++ b/lib/kamal/configuration/accessory.rb @@ -42,23 +42,13 @@ class Kamal::Configuration::Accessory end def env - specifics["env"] || {} - end - - def env_file - Kamal::EnvFile.new(env) - end - - def host_env_directory - File.join config.host_env_directory, "accessories" - end - - def host_env_file_path - File.join host_env_directory, "#{service_name}.env" + Kamal::Configuration::Env.from_config \ + config: specifics.fetch("env", {}), + secrets_file: File.join(config.host_env_directory, "accessories", "#{service_name}.env") end def env_args - argumentize "--env-file", host_env_file_path + env.args end def files @@ -111,10 +101,10 @@ class Kamal::Configuration::Accessory end def with_clear_env_loaded - (env["clear"] || env).each { |k, v| ENV[k] = v } + env.clear.each { |k, v| ENV[k] = v } yield ensure - (env["clear"] || env).each { |k, v| ENV.delete(k) } + env.clear.each { |k, v| ENV.delete(k) } end def read_dynamic_file(local_file) diff --git a/lib/kamal/configuration/env.rb b/lib/kamal/configuration/env.rb new file mode 100644 index 00000000..f9b8cd66 --- /dev/null +++ b/lib/kamal/configuration/env.rb @@ -0,0 +1,40 @@ +class Kamal::Configuration::Env + attr_reader :secrets_keys, :clear, :secrets_file + delegate :argumentize, to: Kamal::Utils + + def self.from_config(config:, secrets_file: nil) + secrets_keys = config.fetch("secret", []) + clear = config.fetch("clear", config.key?("secret") ? {} : config) + + new clear: clear, secrets_keys: secrets_keys, secrets_file: secrets_file + end + + def initialize(clear:, secrets_keys:, secrets_file:) + @clear = clear + @secrets_keys = secrets_keys + @secrets_file = secrets_file + end + + def args + [ "--env-file", secrets_file, *argumentize("--env", clear) ] + end + + def secrets_io + StringIO.new(Kamal::EnvFile.new(secrets).to_s) + end + + def secrets + @secrets ||= secrets_keys.to_h { |key| [ key, ENV.fetch(key) ] } + end + + def secrets_directory + File.dirname(secrets_file) + end + + def merge(other) + self.class.new \ + clear: @clear.merge(other.clear), + secrets_keys: @secrets_keys | other.secrets_keys, + secrets_file: secrets_file + end +end diff --git a/lib/kamal/configuration/role.rb b/lib/kamal/configuration/role.rb index 4f0f1d67..d77d6ee0 100644 --- a/lib/kamal/configuration/role.rb +++ b/lib/kamal/configuration/role.rb @@ -51,27 +51,11 @@ class Kamal::Configuration::Role def env - if config.env && config.env["secret"] - merged_env_with_secrets - else - merged_env - end - end - - def env_file - Kamal::EnvFile.new(env) - end - - def host_env_directory - File.join config.host_env_directory, "roles" - end - - def host_env_file_path - File.join host_env_directory, "#{[ config.service, name, config.destination ].compact.join("-")}.env" + @env ||= base_env.merge(specialized_env) end def env_args - argumentize "--env-file", host_env_file_path + env.args end def asset_volume_args @@ -217,7 +201,7 @@ class Kamal::Configuration::Role end def traefik_service - [ config.service, name, config.destination ].compact.join("-") + container_prefix end def custom_labels @@ -236,24 +220,14 @@ class Kamal::Configuration::Role end def specialized_env - specializations["env"] || {} - end - - def merged_env - config.env&.merge(specialized_env) || {} + Kamal::Configuration::Env.from_config config: specializations.fetch("env", {}) end # Secrets are stored in an array, which won't merge by default, so have to do it by hand. - def merged_env_with_secrets - merged_env.tap do |new_env| - new_env["secret"] = Array(config.env["secret"]) + Array(specialized_env["secret"]) - - # If there's no secret/clear split, everything is clear - clear_app_env = config.env["secret"] ? Array(config.env["clear"]) : Array(config.env["clear"] || config.env) - clear_role_env = specialized_env["secret"] ? Array(specialized_env["clear"]) : Array(specialized_env["clear"] || specialized_env) - - new_env["clear"] = clear_app_env.to_h.merge(clear_role_env.to_h) - end + def base_env + Kamal::Configuration::Env.from_config \ + config: config.env, + secrets_file: File.join(config.host_env_directory, "roles", "#{container_prefix}.env") end def http_health_check(port:, path:) diff --git a/lib/kamal/env_file.rb b/lib/kamal/env_file.rb index 24c3353a..0c9f2341 100644 --- a/lib/kamal/env_file.rb +++ b/lib/kamal/env_file.rb @@ -6,18 +6,8 @@ class Kamal::EnvFile def to_s env_file = StringIO.new.tap do |contents| - if (secrets = @env["secret"]).present? - @env.fetch("secret", @env)&.each do |key| - contents << docker_env_file_line(key, ENV.fetch(key)) - end - - @env["clear"]&.each do |key, value| - contents << docker_env_file_line(key, value) - end - else - @env.fetch("clear", @env)&.each do |key, value| - contents << docker_env_file_line(key, value) - end + @env.each do |key, value| + contents << docker_env_file_line(key, value) end end.string diff --git a/test/cli/accessory_test.rb b/test/cli/accessory_test.rb index f6e6045b..ecce810f 100644 --- a/test/cli/accessory_test.rb +++ b/test/cli/accessory_test.rb @@ -7,7 +7,7 @@ class CliAccessoryTest < CliTestCase run_command("boot", "mysql").tap do |output| assert_match /docker login.*on 1.1.1.3/, output - assert_match "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 --env-file .kamal/env/accessories/app-mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output + assert_match "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 --env-file .kamal/env/accessories/app-mysql.env --env MYSQL_ROOT_HOST=\"%\" --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output end end @@ -21,7 +21,7 @@ class CliAccessoryTest < CliTestCase assert_match /docker login.*on 1.1.1.3/, output assert_match /docker login.*on 1.1.1.1/, output assert_match /docker login.*on 1.1.1.2/, output - assert_match "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 --env-file .kamal/env/accessories/app-mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output + assert_match "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 --env-file .kamal/env/accessories/app-mysql.env --env MYSQL_ROOT_HOST=\"%\" --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.2", output end diff --git a/test/commands/accessory_test.rb b/test/commands/accessory_test.rb index f0e1fbe7..da8c3057 100644 --- a/test/commands/accessory_test.rb +++ b/test/commands/accessory_test.rb @@ -50,11 +50,11 @@ class CommandsAccessoryTest < ActiveSupport::TestCase test "run" do assert_equal \ - "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 --env-file .kamal/env/accessories/app-mysql.env --label service=\"app-mysql\" private.registry/mysql:8.0", + "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 --env-file .kamal/env/accessories/app-mysql.env --env MYSQL_ROOT_HOST=\"%\" --label service=\"app-mysql\" private.registry/mysql:8.0", new_command(:mysql).run.join(" ") assert_equal \ - "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume /var/lib/redis:/data --label service=\"app-redis\" --label cache=\"true\" redis:latest", + "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --env SOMETHING=\"else\" --volume /var/lib/redis:/data --label service=\"app-redis\" --label cache=\"true\" redis:latest", new_command(:redis).run.join(" ") assert_equal \ @@ -91,7 +91,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase test "execute in new container" do assert_equal \ - "docker run --rm --env-file .kamal/env/accessories/app-mysql.env private.registry/mysql:8.0 mysql -u root", + "docker run --rm --env-file .kamal/env/accessories/app-mysql.env --env MYSQL_ROOT_HOST=\"%\" private.registry/mysql:8.0 mysql -u root", new_command(:mysql).execute_in_new_container("mysql", "-u", "root").join(" ") end @@ -103,7 +103,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase test "execute in new container over ssh" do new_command(:mysql).stub(:run_over_ssh, ->(cmd) { cmd.join(" ") }) do - assert_match %r{docker run -it --rm --env-file .kamal/env/accessories/app-mysql.env private.registry/mysql:8.0 mysql -u root}, + assert_match %r{docker run -it --rm --env-file .kamal/env/accessories/app-mysql.env --env MYSQL_ROOT_HOST=\"%\" private.registry/mysql:8.0 mysql -u root}, new_command(:mysql).execute_in_new_container_over_ssh("mysql", "-u", "root") end end diff --git a/test/commands/traefik_test.rb b/test/commands/traefik_test.rb index 4ba98584..157670a3 100644 --- a/test/commands/traefik_test.rb +++ b/test/commands/traefik_test.rb @@ -177,14 +177,10 @@ class CommandsTraefikTest < ActiveSupport::TestCase new_command.follow_logs(host: @config[:servers].first, grep: "hello!") end - test "env_file" do + test "secrets io" do @config[:traefik]["env"] = { "secret" => %w[EXAMPLE_API_KEY] } - assert_equal "EXAMPLE_API_KEY=456\n", new_command.env_file.to_s - end - - test "host_env_file_path" do - assert_equal ".kamal/env/traefik/traefik.env", new_command.host_env_file_path + assert_equal "EXAMPLE_API_KEY=456\n", new_command.env.secrets_io.string end test "make_env_directory" do diff --git a/test/configuration/accessory_test.rb b/test/configuration/accessory_test.rb index fea05f6c..d53ac9ba 100644 --- a/test/configuration/accessory_test.rb +++ b/test/configuration/accessory_test.rb @@ -113,29 +113,25 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase end test "env args" do - assert_equal [ "--env-file", ".kamal/env/accessories/app-mysql.env" ], @config.accessory(:mysql).env_args - assert_equal [ "--env-file", ".kamal/env/accessories/app-redis.env" ], @config.accessory(:redis).env_args + 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 - test "env file with secret" do + test "env with secrets" do ENV["MYSQL_ROOT_PASSWORD"] = "secret123" - expected = <<~ENV + expected_secrets_file = <<~ENV MYSQL_ROOT_PASSWORD=secret123 - MYSQL_ROOT_HOST=% ENV - assert_equal expected, @config.accessory(:mysql).env_file.to_s + 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 "host_env_directory" do - assert_equal ".kamal/env/accessories", @config.accessory(:mysql).host_env_directory - end - - test "host_env_file_path" do - assert_equal ".kamal/env/accessories/app-mysql.env", @config.accessory(:mysql).host_env_file_path + test "env secrets path" do + assert_equal ".kamal/env/accessories/app-mysql.env", @config.accessory(:mysql).env.secrets_file end test "volume args" do diff --git a/test/configuration/env_test.rb b/test/configuration/env_test.rb new file mode 100644 index 00000000..d24c6211 --- /dev/null +++ b/test/configuration/env_test.rb @@ -0,0 +1,74 @@ +require "test_helper" + +class ConfigurationEnvTest < ActiveSupport::TestCase + require "test_helper" + + test "simple" do + assert_config \ + config: { "foo" => "bar", "baz" => "haz" }, + clear: { "foo" => "bar", "baz" => "haz" }, + secrets: {} + end + + test "clear" do + assert_config \ + config: { "clear" => { "foo" => "bar", "baz" => "haz" } }, + clear: { "foo" => "bar", "baz" => "haz" }, + secrets: {} + end + + test "secret" do + ENV["PASSWORD"] = "hello" + env = Kamal::Configuration::Env.from_config config: { "secret" => [ "PASSWORD" ] } + + assert_config \ + config: { "secret" => [ "PASSWORD" ] }, + clear: {}, + secrets: { "PASSWORD" => "hello" } + ensure + ENV.delete "PASSWORD" + end + + test "missing secret" do + env = { + "secret" => [ "PASSWORD" ] + } + + assert_raises(KeyError) { Kamal::Configuration::Env.from_config(config: { "secret" => [ "PASSWORD" ] }).secrets } + end + + test "secret and clear" do + ENV["PASSWORD"] = "hello" + 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 + end + + private + def assert_config(config:, clear:, secrets:) + env = Kamal::Configuration::Env.from_config config: config + assert_equal clear, env.clear + assert_equal secrets, env.secrets + end +end diff --git a/test/configuration/role_test.rb b/test/configuration/role_test.rb index 8c4560cd..1c57a06a 100644 --- a/test/configuration/role_test.rb +++ b/test/configuration/role_test.rb @@ -70,14 +70,10 @@ class ConfigurationRoleTest < ActiveSupport::TestCase end test "env overwritten by role" do - assert_equal "redis://a/b", @config_with_roles.role(:workers).env["REDIS_URL"] + assert_equal "redis://a/b", @config_with_roles.role(:workers).env.clear["REDIS_URL"] - expected_env = <<~ENV - REDIS_URL=redis://a/b - WEB_CONCURRENCY=4 - ENV - - assert_equal expected_env, @config_with_roles.role(:workers).env_file.to_s + assert_equal "\n", @config_with_roles.role(:workers).env.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 end test "container name" do @@ -90,7 +86,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase end test "env args" do - assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env" ], @config_with_roles.role(:workers).env_args + 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 end test "env secret overwritten by role" do @@ -116,14 +112,13 @@ class ConfigurationRoleTest < ActiveSupport::TestCase ENV["REDIS_PASSWORD"] = "secret456" ENV["DB_PASSWORD"] = "secret&\"123" - expected = <<~ENV + expected_secrets_file = <<~ENV REDIS_PASSWORD=secret456 DB_PASSWORD=secret&\"123 - REDIS_URL=redis://a/b - WEB_CONCURRENCY=4 ENV - assert_equal expected, @config_with_roles.role(:workers).env_file.to_s + assert_equal expected_secrets_file, @config_with_roles.role(:workers).env.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 ensure ENV["REDIS_PASSWORD"] = nil ENV["DB_PASSWORD"] = nil @@ -142,13 +137,12 @@ class ConfigurationRoleTest < ActiveSupport::TestCase ENV["DB_PASSWORD"] = "secret123" - expected = <<~ENV + expected_secrets_file = <<~ENV DB_PASSWORD=secret123 - REDIS_URL=redis://a/b - WEB_CONCURRENCY=4 ENV - assert_equal expected, @config_with_roles.role(:workers).env_file.to_s + assert_equal expected_secrets_file, @config_with_roles.role(:workers).env.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 ensure ENV["DB_PASSWORD"] = nil end @@ -165,13 +159,12 @@ class ConfigurationRoleTest < ActiveSupport::TestCase ENV["REDIS_PASSWORD"] = "secret456" - expected = <<~ENV + expected_secrets_file = <<~ENV REDIS_PASSWORD=secret456 - REDIS_URL=redis://a/b - WEB_CONCURRENCY=4 ENV - assert_equal expected, @config_with_roles.role(:workers).env_file.to_s + assert_equal expected_secrets_file, @config_with_roles.role(:workers).env.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 ensure ENV["REDIS_PASSWORD"] = nil end @@ -194,22 +187,18 @@ class ConfigurationRoleTest < ActiveSupport::TestCase ENV["REDIS_PASSWORD"] = "secret456" - expected = <<~ENV + expected_secrets_file = <<~ENV REDIS_PASSWORD=secret456 - REDIS_URL=redis://c/d ENV - assert_equal expected, @config_with_roles.role(:workers).env_file.to_s + assert_equal expected_secrets_file, @config_with_roles.role(:workers).env.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 ensure ENV["REDIS_PASSWORD"] = nil end - test "host_env_directory" do - assert_equal ".kamal/env/roles", @config_with_roles.role(:workers).host_env_directory - end - - test "host_env_file_path" do - assert_equal ".kamal/env/roles/app-workers.env", @config_with_roles.role(:workers).host_env_file_path + test "env secrets_file" do + assert_equal ".kamal/env/roles/app-workers.env", @config_with_roles.role(:workers).env.secrets_file end test "uses cord" do diff --git a/test/env_file_test.rb b/test/env_file_test.rb index e4a1322c..6fcef6e3 100644 --- a/test/env_file_test.rb +++ b/test/env_file_test.rb @@ -1,7 +1,7 @@ require "test_helper" class EnvFileTest < ActiveSupport::TestCase - test "env file simple" do + test "to_s" do env = { "foo" => "bar", "baz" => "haz" @@ -11,80 +11,27 @@ class EnvFileTest < ActiveSupport::TestCase Kamal::EnvFile.new(env).to_s end - test "env file clear" do - env = { - "clear" => { - "foo" => "bar", - "baz" => "haz" - } - } - - assert_equal "foo=bar\nbaz=haz\n", \ - Kamal::EnvFile.new(env).to_s - end - - test "env file empty" do + test "to_s empty" do assert_equal "\n", Kamal::EnvFile.new({}).to_s end - test "env file secret" do - ENV["PASSWORD"] = "hello" + test "to_s escaped newline" do env = { - "secret" => [ "PASSWORD" ] + "foo" => "hello\\nthere" } - assert_equal "PASSWORD=hello\n", \ + assert_equal "foo=hello\\\\nthere\n", \ Kamal::EnvFile.new(env).to_s ensure ENV.delete "PASSWORD" end - test "env file secret escaped newline" do - ENV["PASSWORD"] = "hello\\nthere" + test "to_s newline" do env = { - "secret" => [ "PASSWORD" ] + "foo" => "hello\nthere" } - assert_equal "PASSWORD=hello\\\\nthere\n", \ - Kamal::EnvFile.new(env).to_s - ensure - ENV.delete "PASSWORD" - end - - test "env file secret newline" do - ENV["PASSWORD"] = "hello\nthere" - env = { - "secret" => [ "PASSWORD" ] - } - - assert_equal "PASSWORD=hello\\nthere\n", \ - Kamal::EnvFile.new(env).to_s - ensure - ENV.delete "PASSWORD" - end - - test "env file missing secret" do - env = { - "secret" => [ "PASSWORD" ] - } - - assert_raises(KeyError) { Kamal::EnvFile.new(env).to_s } - - ensure - ENV.delete "PASSWORD" - end - - test "env file secret and clear" do - ENV["PASSWORD"] = "hello" - env = { - "secret" => [ "PASSWORD" ], - "clear" => { - "foo" => "bar", - "baz" => "haz" - } - } - - assert_equal "PASSWORD=hello\nfoo=bar\nbaz=haz\n", \ + assert_equal "foo=hello\\nthere\n", \ Kamal::EnvFile.new(env).to_s ensure ENV.delete "PASSWORD" diff --git a/test/integration/main_test.rb b/test/integration/main_test.rb index 7c858a17..e4f8db61 100644 --- a/test/integration/main_test.rb +++ b/test/integration/main_test.rb @@ -4,7 +4,7 @@ class MainTest < IntegrationTest test "envify, deploy, redeploy, rollback, details and audit" do kamal :envify assert_local_env_file "SECRET_TOKEN=1234" - assert_remote_env_file "SECRET_TOKEN=1234\nCLEAR_TOKEN=4321" + assert_remote_env_file "SECRET_TOKEN=1234" remove_local_env_file first_version = latest_app_version @@ -14,6 +14,8 @@ class MainTest < IntegrationTest kamal :deploy assert_app_is_up version: first_version assert_hooks_ran "pre-connect", "pre-build", "pre-deploy", "post-deploy" + assert_env :CLEAR_TOKEN, "4321", version: first_version + assert_env :SECRET_TOKEN, "1234", version: first_version second_version = update_app_rev @@ -94,6 +96,10 @@ class MainTest < IntegrationTest assert_equal contents, deployer_exec("cat .env", capture: true) end + def assert_env(key, value, version:) + assert_equal "#{key}=#{value}", docker_compose("exec vm1 docker exec app-web-#{version} env | grep #{key}", capture: true) + end + def remove_local_env_file deployer_exec("rm .env") end