Extract Kamal::EnvFile

Cleaning up the Utils junk drawer.
This commit is contained in:
dhh
2023-09-16 10:55:41 -07:00
parent 3a8eb0cf7d
commit 873be0b76b
7 changed files with 149 additions and 106 deletions

View File

@@ -1,5 +1,5 @@
class Kamal::Commands::Traefik < Kamal::Commands::Base class Kamal::Commands::Traefik < Kamal::Commands::Base
delegate :argumentize, :env_file_with_secrets, :optionize, to: Kamal::Utils delegate :argumentize, :optionize, to: Kamal::Utils
DEFAULT_IMAGE = "traefik:v2.9" DEFAULT_IMAGE = "traefik:v2.9"
CONTAINER_PORT = 80 CONTAINER_PORT = 80
@@ -64,7 +64,7 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base
end end
def env_file def env_file
env_file_with_secrets config.traefik.fetch("env", {}) Kamal::EnvFile.new(config.traefik.fetch("env", {}))
end end
def host_env_file_path def host_env_file_path

View File

@@ -1,5 +1,5 @@
class Kamal::Configuration::Accessory class Kamal::Configuration::Accessory
delegate :argumentize, :env_file_with_secrets, :optionize, to: Kamal::Utils delegate :argumentize, :optionize, to: Kamal::Utils
attr_accessor :name, :specifics attr_accessor :name, :specifics
@@ -46,7 +46,7 @@ class Kamal::Configuration::Accessory
end end
def env_file def env_file
env_file_with_secrets env Kamal::EnvFile.new(env)
end end
def host_env_directory def host_env_directory

View File

@@ -1,6 +1,6 @@
class Kamal::Configuration::Role class Kamal::Configuration::Role
CORD_FILE = "cord" CORD_FILE = "cord"
delegate :argumentize, :env_file_with_secrets, :optionize, to: Kamal::Utils delegate :argumentize, :optionize, to: Kamal::Utils
attr_accessor :name attr_accessor :name
@@ -46,7 +46,7 @@ class Kamal::Configuration::Role
end end
def env_file def env_file
env_file_with_secrets env Kamal::EnvFile.new(env)
end end
def host_env_directory def host_env_directory

41
lib/kamal/env_file.rb Normal file
View File

@@ -0,0 +1,41 @@
# Encode an env hash as a string where secret values have been looked up and all values escaped for Docker.
class Kamal::EnvFile
def initialize(env)
@env = env
end
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
end
end.string
# Ensure the file has some contents to avoid the SSHKIT empty file warning
env_file.presence || "\n"
end
alias to_str to_s
private
def docker_env_file_line(key, value)
"#{key.to_s}=#{escape_docker_env_file_value(value)}\n"
end
# Escape a value to make it safe to dump in a docker file.
def escape_docker_env_file_value(value)
# Doublequotes are treated literally in docker env files
# so remove leading and trailing ones and unescape any others
value.to_s.dump[1..-2].gsub(/\\"/, "\"")
end
end

View File

@@ -92,13 +92,6 @@ module Kamal::Utils
.gsub(DOLLAR_SIGN_WITHOUT_SHELL_EXPANSION_REGEX, '\$') .gsub(DOLLAR_SIGN_WITHOUT_SHELL_EXPANSION_REGEX, '\$')
end end
# Escape a value to make it safe to dump in a docker file.
def escape_docker_env_file_value(value)
# Doublequotes are treated literally in docker env files
# so remove leading and trailing ones and unescape any others
value.to_s.dump[1..-2].gsub(/\\"/, "\"")
end
# Abbreviate a git revhash for concise display # Abbreviate a git revhash for concise display
def abbreviate_version(version) def abbreviate_version(version)
if version if version
@@ -114,8 +107,4 @@ module Kamal::Utils
def uncommitted_changes def uncommitted_changes
`git status --porcelain`.strip `git status --porcelain`.strip
end end
def docker_env_file_line(key, value)
"#{key.to_s}=#{escape_docker_env_file_value(value)}\n"
end
end end

102
test/env_file_test.rb Normal file
View File

@@ -0,0 +1,102 @@
require "test_helper"
class EnvFileTest < ActiveSupport::TestCase
test "env file simple" do
env = {
"foo" => "bar",
"baz" => "haz"
}
assert_equal "foo=bar\nbaz=haz\n", \
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
assert_equal "\n", Kamal::EnvFile.new({}).to_s
end
test "env file secret" do
ENV["PASSWORD"] = "hello"
env = {
"secret" => [ "PASSWORD" ]
}
assert_equal "PASSWORD=hello\n", \
Kamal::EnvFile.new(env).to_s
ensure
ENV.delete "PASSWORD"
end
test "env file secret escaped 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 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", \
Kamal::EnvFile.new(env).to_s
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
end

View File

@@ -11,95 +11,6 @@ class UtilsTest < ActiveSupport::TestCase
Kamal::Utils.argumentize("--label", { foo: "bar" }, sensitive: true).last Kamal::Utils.argumentize("--label", { foo: "bar" }, sensitive: true).last
end end
test "env file simple" do
env = {
"foo" => "bar",
"baz" => "haz"
}
assert_equal "foo=bar\nbaz=haz\n", \
Kamal::Utils.env_file_with_secrets(env)
end
test "env file clear" do
env = {
"clear" => {
"foo" => "bar",
"baz" => "haz"
}
}
assert_equal "foo=bar\nbaz=haz\n", \
Kamal::Utils.env_file_with_secrets(env)
end
test "env file empty" do
assert_equal "\n", Kamal::Utils.env_file_with_secrets({})
end
test "env file secret" do
ENV["PASSWORD"] = "hello"
env = {
"secret" => [ "PASSWORD" ]
}
assert_equal "PASSWORD=hello\n", \
Kamal::Utils.env_file_with_secrets(env)
ensure
ENV.delete "PASSWORD"
end
test "env file secret escaped newline" do
ENV["PASSWORD"] = "hello\\nthere"
env = {
"secret" => [ "PASSWORD" ]
}
assert_equal "PASSWORD=hello\\\\nthere\n", \
Kamal::Utils.env_file_with_secrets(env)
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::Utils.env_file_with_secrets(env)
ensure
ENV.delete "PASSWORD"
end
test "env file missing secret" do
env = {
"secret" => [ "PASSWORD" ]
}
assert_raises(KeyError) { Kamal::Utils.env_file_with_secrets(env) }
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", \
Kamal::Utils.env_file_with_secrets(env)
ensure
ENV.delete "PASSWORD"
end
test "optionize" do test "optionize" do
assert_equal [ "--foo", "\"bar\"", "--baz", "\"qux\"", "--quux" ], \ assert_equal [ "--foo", "\"bar\"", "--baz", "\"qux\"", "--quux" ], \
Kamal::Utils.optionize({ foo: "bar", baz: "qux", quux: true }) Kamal::Utils.optionize({ foo: "bar", baz: "qux", quux: true })