diff --git a/lib/kamal/cli/base.rb b/lib/kamal/cli/base.rb index d4cac48d..85815506 100644 --- a/lib/kamal/cli/base.rb +++ b/lib/kamal/cli/base.rb @@ -1,5 +1,4 @@ require "thor" -require "dotenv" require "kamal/sshkit_with_ext" module Kamal::Cli diff --git a/lib/kamal/secrets.rb b/lib/kamal/secrets.rb index 25d24934..dc135331 100644 --- a/lib/kamal/secrets.rb +++ b/lib/kamal/secrets.rb @@ -1,6 +1,10 @@ +require "dotenv" + class Kamal::Secrets attr_reader :secrets_file + Kamal::Secrets::Dotenv::InlineCommandSubstitution.install! + def initialize(destination: nil) @secrets_file = [ *(".kamal/secrets.#{destination}" if destination), ".kamal/secrets" ].find { |f| File.exist?(f) } end @@ -26,7 +30,7 @@ class Kamal::Secrets def parse_secrets if secrets_file - interrupting_parent_on_error { Dotenv.parse(secrets_file) } + interrupting_parent_on_error { ::Dotenv.parse(secrets_file) } else {} end diff --git a/lib/kamal/secrets/dotenv/inline_command_substitution.rb b/lib/kamal/secrets/dotenv/inline_command_substitution.rb new file mode 100644 index 00000000..e8e12d5c --- /dev/null +++ b/lib/kamal/secrets/dotenv/inline_command_substitution.rb @@ -0,0 +1,37 @@ +class Kamal::Secrets::Dotenv::InlineCommandSubstitution + class << self + def install! + ::Dotenv::Parser.substitutions.map! { |sub| sub == ::Dotenv::Substitutions::Command ? self : sub } + end + + def call(value, _env, overwrite: false) + # Process interpolated shell commands + value.gsub(Dotenv::Substitutions::Command.singleton_class::INTERPOLATED_SHELL_COMMAND) do |*| + # Eliminate opening and closing parentheses + command = $LAST_MATCH_INFO[:cmd][1..-2] + + if $LAST_MATCH_INFO[:backslash] + # Command is escaped, don't replace it. + $LAST_MATCH_INFO[0][1..] + else + if command =~ /\A\s*kamal\s*secrets\s+/ + # Inline the command + capture_stdout { Kamal::Cli::Main.start(command.shellsplit[1..]) }.chomp + else + # Execute the command and return the value + `#{command}`.chomp + end + end + end + end + + def capture_stdout + old_stdout = $stdout + $stdout = StringIO.new + yield + $stdout.string + ensure + $stdout = old_stdout + end + end +end diff --git a/test/secrets/dotenv_inline_command_substitution_test.rb b/test/secrets/dotenv_inline_command_substitution_test.rb new file mode 100644 index 00000000..041ada29 --- /dev/null +++ b/test/secrets/dotenv_inline_command_substitution_test.rb @@ -0,0 +1,15 @@ +require "test_helper" + +class SecretsInlineCommandSubstitution < SecretAdapterTestCase + test "inlines kamal secrets commands" do + Kamal::Cli::Main.expects(:start).with { |command| puts "results"; command == [ "secrets", "fetch", "..." ] } + substituted = Kamal::Secrets::Dotenv::InlineCommandSubstitution.call("FOO=$(kamal secrets fetch ...)", nil, overwrite: false) + assert_equal "FOO=results", substituted + end + + test "executes other commands" do + Kamal::Secrets::Dotenv::InlineCommandSubstitution.stubs(:`).with("blah").returns("results") + substituted = Kamal::Secrets::Dotenv::InlineCommandSubstitution.call("FOO=$(blah)", nil, overwrite: false) + assert_equal "FOO=results", substituted + end +end