Interrupting parent on error

This commit is contained in:
Donal McBreen
2024-09-04 12:14:47 +01:00
parent 9deb8af4a0
commit 5226d52f8a
11 changed files with 601 additions and 48 deletions

View File

@@ -2,21 +2,39 @@ class Kamal::Cli::Secrets < Kamal::Cli::Base
desc "fetch [SECRETS...]", "Fetch secrets from a vault"
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 :location, 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"
def fetch(*secrets)
ENV["KAMAL_SECRETS_KILL_PARENT"] = "1"
results = adapter(options[:adapter]).fetch(secrets, **options.slice(:account, :location).symbolize_keys)
results = adapter(options[:adapter]).fetch(secrets, **options.slice(:account, :from).symbolize_keys)
puts JSON.dump(results).shellescape
rescue => e
handle_error(e)
end
desc "extract", "Extract a single secret from the results of a fetch call"
def extract(name, secrets)
parsed_secrets = JSON.parse(secrets)
if (value = parsed_secrets[name]).nil?
value = parsed_secrets.find { |k, v| k.end_with?("/#{name}") }&.last
end
raise "Could not find secret #{name}" if value.nil?
puts JSON.parse(secrets).fetch(name)
rescue => e
handle_error(e)
end
private
def adapter(adapter)
Kamal::Secrets::Adapters.lookup(adapter)
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

@@ -6,7 +6,10 @@ class Kamal::Secrets
end
def [](key)
@secrets ||= secrets_file ? Dotenv.parse(*secrets_file) : {}
# If dot env interpolates any `kamal secrets` calls, this tells it to interrupt this process if there are errors
ENV["KAMAL_SECRETS_INT_PARENT"] = "1"
@secrets ||= secrets_file ? Dotenv.parse(secrets_file) : {}
@secrets.fetch(key)
rescue KeyError
if secrets_file
@@ -15,4 +18,20 @@ class Kamal::Secrets
raise Kamal::ConfigurationError, "Secret '#{key}' not found, no secret files provided"
end
end
private
def parse_secrets
if secrets_file
interrupting_parent_on_error { Dotenv.parse(secrets_file) }
else
{}
end
end
def interrupting_parent_on_error
ENV["KAMAL_SECRETS_INT_PARENT"] = "1"
yield
ensure
ENV.delete("KAMAL_SECRETS_INT_PARENT")
end
end

View File

@@ -1,15 +1,15 @@
class Kamal::Secrets::Adapters::Base
delegate :optionize, to: Kamal::Utils
def fetch(secrets, account:, location: nil)
def fetch(secrets, account:, from: nil)
session = login(account)
full_secrets = secrets.map { |secret| [ location, secret ].compact.join("/") }
full_secrets = secrets.map { |secret| [ from, secret ].compact.join("/") }
fetch_from_vault(full_secrets, account: account, session: session)
rescue => 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_KILL_PARENT"]
Process.kill("INT", Process.ppid) if ENV["KAMAL_SECRETS_INT_PARENT"]
exit 1
end

View File

@@ -9,13 +9,13 @@ class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base
end
if status["status"] == "locked"
session = run_command("unlock --raw", raw: true)
session = run_command("unlock --raw", raw: true).presence
status = run_command("status", session: session)
end
raise RuntimeError, "Failed to login to and unlock Bitwarden" unless status["status"] == "unlocked"
run_command("sync", raw: true)
run_command("sync", session: session, raw: true)
raise RuntimeError, "Failed to sync Bitwarden" unless $?.success?
session
@@ -23,25 +23,37 @@ class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base
def fetch_from_vault(secrets, account:, session:)
{}.tap do |results|
secrets.each do |secret|
item, field = secret.split("/")
item = run_command("get item #{item}", session: session)
raise RuntimeError, "Could not read #{item} from Bitwarden" unless $?.success?
if field
item_field = item["fields"].find { |f| f["name"] == field }
raise RuntimeError, "Could not find field #{field} in item #{item} in Bitwarden" unless item_field
value = item_field["value"]
results[secret] = value
results[field] = value
items_fields(secrets).each do |item, fields|
item_json = run_command("get item #{item}", session: session, raw: true)
raise RuntimeError, "Could not read #{secret} from Bitwarden" unless $?.success?
item_json = JSON.parse(item_json)
if fields.any?
fields.each do |field|
item_field = item_json["fields"].find { |f| f["name"] == field }
raise RuntimeError, "Could not find field #{field} in item #{item} in Bitwarden" unless item_field
value = item_field["value"]
results["#{item}/#{field}"] = value
end
else
results[secret] = item["login"]["password"]
results[item] = item_json["login"]["password"]
end
end
end
end
def items_fields(secrets)
{}.tap do |items|
secrets.each do |secret|
item, field = secret.split("/")
items[item] ||= []
items[item] << field
end
end
end
def signedin?(account)
JSON.parse(`bw status`.strip)["status"] != "unauthenticated"
run_command("status")["status"] != "unauthenticated"
end
def run_command(command, session: nil, raw: false)

View File

@@ -12,12 +12,13 @@ class Kamal::Secrets::Adapters::LastPass < Kamal::Secrets::Adapters::Base
end
def fetch_from_vault(secrets, account:, session:)
items = JSON.parse(`lpass show #{secrets.join(" ")} --json`
raise RuntimeError, "Could not read #{fields} from 1Password" unless $?.success?
items = `lpass show #{secrets.join(" ")} --json`
raise RuntimeError, "Could not read #{secrets} from 1Password" unless $?.success?
items = JSON.parse(items)
{}.tap do |results|
items.each do |item|
results[item["name"]] = item["password"]
results[item["fullname"]] = item["password"]
end

View File

@@ -24,9 +24,8 @@ class Kamal::Secrets::Adapters::OnePassword < Kamal::Secrets::Adapters::Base
fields_json.each do |field_json|
# The reference is in the form `op://vault/item/field[/field]`
field = field_json["reference"].delete_suffix("/password")
field = field_json["reference"].delete_prefix("op://").delete_suffix("/password")
results[field] = field_json["value"]
results[field.split("/").last] = field_json["value"]
end
end
end
@@ -40,6 +39,7 @@ class Kamal::Secrets::Adapters::OnePassword < Kamal::Secrets::Adapters::Base
def vaults_items_fields(secrets)
{}.tap do |vaults|
secrets.each do |secret|
secret = secret.delete_prefix("op://")
vault, item, *fields = secret.split("/")
fields << "password" if fields.empty?