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 :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 :inline, type: :boolean, required: false, hidden: true
def fetch(*secrets)
handle_output(inline: options[:inline]) do
results = adapter(options[:adapter]).fetch(secrets, **options.slice(:account, :from).symbolize_keys)
puts JSON.dump(results).shellescape
rescue => e
handle_error(e)
JSON.dump(results).shellescape
end
end
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)
handle_output(inline: options[:inline]) do
parsed_secrets = JSON.parse(secrets)
if (value = parsed_secrets[name]).nil?
value = parsed_secrets.find { |k, v| k.end_with?("/#{name}") }&.last
end
value = parsed_secrets[name] || parsed_secrets.find { |k, v| k.end_with?("/#{name}") }&.last
raise "Could not find secret #{name}" if value.nil?
puts value
rescue => e
handle_error(e)
value
end
end
private
@@ -30,11 +30,18 @@ class Kamal::Cli::Secrets < Kamal::Cli::Base
Kamal::Secrets::Adapters.lookup(adapter)
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)
$stderr.puts " \e[31mERROR (#{e.class}): #{e.message}\e[0m"
$stderr.puts e.backtrace if ENV["VERBOSE"]
Process.kill("INT", Process.ppid) if ENV["KAMAL_SECRETS_INT_PARENT"]
exit 1
end
end

View File

@@ -30,17 +30,9 @@ class Kamal::Secrets
def parse_secrets
if secrets_file
interrupting_parent_on_error { ::Dotenv.parse(secrets_file) }
::Dotenv.parse(secrets_file)
else
{}
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

View File

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

View File

@@ -2,7 +2,7 @@ require "test_helper"
class SecretsInlineCommandSubstitution < SecretAdapterTestCase
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)
assert_equal "FOO=results", substituted
end