Make the secrets commands inline aware

Rather than redirecting the global $stdout, which is not never clever in
a threaded program, we'll make the secrets commands aware they are
being inlined, so they return the value instead of printing it.

Additionally we no longer need to interrupt the parent process on error
as we've inlined the command - exit 1 is enough.
This commit is contained in:
Donal McBreen
2024-09-10 10:24:14 +01:00
parent 5aa3d1aeb0
commit 06f4caa866
4 changed files with 26 additions and 32 deletions

View File

@@ -3,26 +3,26 @@ class Kamal::Cli::Secrets < Kamal::Cli::Base
option :adapter, type: :string, aliases: "-a", required: true, desc: "Which vault adapter to use" option :adapter, type: :string, aliases: "-a", required: true, desc: "Which vault adapter to use"
option :account, type: :string, required: true, desc: "The account identifier or username" option :account, type: :string, required: true, desc: "The account identifier or username"
option :from, type: :string, required: false, desc: "A vault or folder to fetch the secrets from" option :from, type: :string, required: false, desc: "A vault or folder to fetch the secrets from"
option :inline, type: :boolean, required: false, hidden: true
def fetch(*secrets) def fetch(*secrets)
handle_output(inline: options[:inline]) do
results = adapter(options[:adapter]).fetch(secrets, **options.slice(:account, :from).symbolize_keys) results = adapter(options[:adapter]).fetch(secrets, **options.slice(:account, :from).symbolize_keys)
puts JSON.dump(results).shellescape JSON.dump(results).shellescape
rescue => e end
handle_error(e)
end end
desc "extract", "Extract a single secret from the results of a fetch call" desc "extract", "Extract a single secret from the results of a fetch call"
option :inline, type: :boolean, required: false, hidden: true
def extract(name, secrets) def extract(name, secrets)
handle_output(inline: options[:inline]) do
parsed_secrets = JSON.parse(secrets) parsed_secrets = JSON.parse(secrets)
if (value = parsed_secrets[name]).nil? value = parsed_secrets[name] || parsed_secrets.find { |k, v| k.end_with?("/#{name}") }&.last
value = parsed_secrets.find { |k, v| k.end_with?("/#{name}") }&.last
end
raise "Could not find secret #{name}" if value.nil? raise "Could not find secret #{name}" if value.nil?
puts value value
rescue => e end
handle_error(e)
end end
private private
@@ -30,11 +30,18 @@ class Kamal::Cli::Secrets < Kamal::Cli::Base
Kamal::Secrets::Adapters.lookup(adapter) Kamal::Secrets::Adapters.lookup(adapter)
end end
def handle_output(inline: nil)
yield.tap do |output|
puts output unless inline
end
rescue => e
handle_error(e)
end
def handle_error(e) def handle_error(e)
$stderr.puts " \e[31mERROR (#{e.class}): #{e.message}\e[0m" $stderr.puts " \e[31mERROR (#{e.class}): #{e.message}\e[0m"
$stderr.puts e.backtrace if ENV["VERBOSE"] $stderr.puts e.backtrace if ENV["VERBOSE"]
Process.kill("INT", Process.ppid) if ENV["KAMAL_SECRETS_INT_PARENT"]
exit 1 exit 1
end end
end end

View File

@@ -30,17 +30,9 @@ class Kamal::Secrets
def parse_secrets def parse_secrets
if secrets_file if secrets_file
interrupting_parent_on_error { ::Dotenv.parse(secrets_file) } ::Dotenv.parse(secrets_file)
else else
{} {}
end end
end end
def interrupting_parent_on_error
# Make any `kamal secrets` calls in dotenv interpolation interrupt this process if there are errors
ENV["KAMAL_SECRETS_INT_PARENT"] = "1"
yield
ensure
ENV.delete("KAMAL_SECRETS_INT_PARENT")
end
end end

View File

@@ -16,7 +16,7 @@ class Kamal::Secrets::Dotenv::InlineCommandSubstitution
else else
if command =~ /\A\s*kamal\s*secrets\s+/ if command =~ /\A\s*kamal\s*secrets\s+/
# Inline the command # Inline the command
capture_stdout { Kamal::Cli::Main.start(command.shellsplit[1..]) }.chomp inline_secrets_command(command)
else else
# Execute the command and return the value # Execute the command and return the value
`#{command}`.chomp `#{command}`.chomp
@@ -25,13 +25,8 @@ class Kamal::Secrets::Dotenv::InlineCommandSubstitution
end end
end end
def capture_stdout def inline_secrets_command(command)
old_stdout = $stdout Kamal::Cli::Main.start(command.shellsplit[1..] + [ "--inline" ]).chomp
$stdout = StringIO.new
yield
$stdout.string
ensure
$stdout = old_stdout
end end
end end
end end

View File

@@ -2,7 +2,7 @@ require "test_helper"
class SecretsInlineCommandSubstitution < SecretAdapterTestCase class SecretsInlineCommandSubstitution < SecretAdapterTestCase
test "inlines kamal secrets commands" do test "inlines kamal secrets commands" do
Kamal::Cli::Main.expects(:start).with { |command| puts "results"; command == [ "secrets", "fetch", "..." ] } Kamal::Cli::Main.expects(:start).with { |command| command == [ "secrets", "fetch", "...", "--inline" ] }.returns("results")
substituted = Kamal::Secrets::Dotenv::InlineCommandSubstitution.call("FOO=$(kamal secrets fetch ...)", nil, overwrite: false) substituted = Kamal::Secrets::Dotenv::InlineCommandSubstitution.call("FOO=$(kamal secrets fetch ...)", nil, overwrite: false)
assert_equal "FOO=results", substituted assert_equal "FOO=results", substituted
end end