Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion lib/kamal/secrets/dotenv/inline_command_substitution.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,19 @@ class Kamal::Secrets::Dotenv::InlineCommandSubstitution

class << self
def install!
::Dotenv::Parser.substitutions.map! { |sub| sub == ::Dotenv::Substitutions::Command ? self : sub }
substitutions = ::Dotenv::Parser.substitutions
substitutions.map! { |sub| sub == ::Dotenv::Substitutions::Command ? self : sub }

# Run variable substitution before command substitution, so command
# output is inserted literally. Otherwise a "$word" in the output --
# common in passwords and tokens -- is treated as a reference to a
# (usually undefined) variable and silently dropped. This matches
# POSIX shell semantics, where the output of command substitution is
# not subject to further expansion. Variable references inside the
# command itself are still substituted in #call before execution.
if substitutions.delete(::Dotenv::Substitutions::Variable)
substitutions.unshift(::Dotenv::Substitutions::Variable)
end
end

def call(value, env, overwrite: false)
Expand Down
23 changes: 23 additions & 0 deletions test/secrets_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,29 @@ class SecretsTest < ActiveSupport::TestCase
end
end

test "command interpolation preserves dollar signs in the output" do
# The command outputs pa$1wor$D. Built with tr because "$word" in the
# command text itself is substituted before running.
with_test_secrets("secrets" => "SECRET=$(echo 'paX1worXD' | tr X '$')") do
assert_equal "pa$1wor$D", Kamal::Secrets.new(secrets_path: ".kamal/secrets")["SECRET"]
end
end

test "command interpolation still substitutes variables in the command" do
with_test_secrets("secrets" => "SECRET1=ABC\nSECRET2=$(echo ${SECRET1}DEF)") do
assert_equal "ABCDEF", Kamal::Secrets.new(secrets_path: ".kamal/secrets")["SECRET2"]
end
end

test "inline kamal secrets commands preserve dollar signs in extracted values" do
# Single-quoted so the stored value matches what an inlined
# "kamal secrets fetch" returns: shell-escaped JSON.
secrets_json = { "vault/password" => "pa$1wor$D" }.to_json.shellescape
with_test_secrets("secrets" => "SECRETS='#{secrets_json}'\nPASSWORD=$(kamal secrets extract vault/password ${SECRETS})") do
assert_equal "pa$1wor$D", Kamal::Secrets.new(secrets_path: ".kamal/secrets")["PASSWORD"]
end
end

test "variable references" do
with_test_secrets("secrets" => "SECRET1=ABC\nSECRET2=${SECRET1}DEF") do
assert_equal "ABC", Kamal::Secrets.new(secrets_path: ".kamal/secrets")["SECRET1"]
Expand Down