Single fetch command
This commit is contained in:
@@ -1,36 +1,19 @@
|
||||
class Kamal::Cli::Secrets < Kamal::Cli::Base
|
||||
desc "login", "Login to a secrets vault"
|
||||
desc "fetch [ITEM] [FIELDS...]", "Fetch secrets from a vault"
|
||||
option :adapter, type: :string, aliases: "-a", required: true, desc: "Which vault adapter to use"
|
||||
option :adapter_options, type: :hash, aliases: "-O", required: false, desc: "Options to pass to the vault adapter"
|
||||
def login
|
||||
puts adapter(options).login(**adapter_options(options))
|
||||
option :account, type: :string, aliases: "-a", required: true, desc: "The account identifier or username"
|
||||
def fetch(item, *fields)
|
||||
ENV["KAMAL_SECRETS_KILL_PARENT"] = "1"
|
||||
puts JSON.dump(adapter(options[:adapter]).fetch(item, fields, account: options[:account])).shellescape
|
||||
end
|
||||
|
||||
desc "fetch", "Fetch a secret from a vault"
|
||||
option :adapter, type: :string, aliases: "-a", required: true, desc: "Which vault adapter to use"
|
||||
option :adapter_options, type: :hash, aliases: "-O", required: false, desc: "Options to pass to the vault adapter"
|
||||
def fetch(name)
|
||||
puts adapter(options).fetch(name, **adapter_options(options))
|
||||
end
|
||||
|
||||
desc "fetch_all", "Fetch multiple secrets from a vault"
|
||||
option :adapter, type: :string, aliases: "-a", required: true, desc: "Which vault adapter to use"
|
||||
option :adapter_options, type: :hash, aliases: "-O", required: false, desc: "Options to pass to the vault adapter"
|
||||
def fetch_all(*names)
|
||||
puts JSON.dump(adapter(options).fetch_all(*names, **adapter_options(options))).shellescape
|
||||
end
|
||||
|
||||
desc "extract", "Extract a single secret from the results of a fetch_all call"
|
||||
desc "extract", "Extract a single secret from the results of a fetch call"
|
||||
def extract(name, secrets)
|
||||
puts JSON.parse(secrets).fetch(name)
|
||||
end
|
||||
|
||||
private
|
||||
def adapter(options)
|
||||
Kamal::Secrets::Adapters.lookup(options[:adapter])
|
||||
end
|
||||
|
||||
def adapter_options(options)
|
||||
options.fetch(:adapter_options, {}).transform_keys(&:to_sym)
|
||||
def adapter(adapter)
|
||||
Kamal::Secrets::Adapters.lookup(adapter)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,55 +1,39 @@
|
||||
class Kamal::Secrets::Adapters::OnePassword
|
||||
delegate :optionize, to: Kamal::Utils
|
||||
|
||||
def login(account:)
|
||||
`op signin #{to_options(account: account, force: true, raw: true)}`.tap do
|
||||
raise RuntimeError, "Failed to login to 1Password: #{output}" unless $?.success?
|
||||
def fetch(item, fields, account: nil)
|
||||
# session may be nil if logging in with the app CLI integration
|
||||
session = signin(account)
|
||||
vault, vault_item = item.split("/")
|
||||
labels = fields.map { |field| "label=#{field}" }.join(",")
|
||||
options = to_options(vault: vault, fields: labels, format: "json", account: account, session: session.presence)
|
||||
|
||||
secrets_json = `op item get #{vault_item} #{options}`.tap do
|
||||
raise RuntimeError, "Could not read #{labels} from #{vault_item} in the #{vault} 1Password vault" unless $?.success?
|
||||
end
|
||||
end
|
||||
|
||||
def fetch(name, account:, session: nil)
|
||||
`op read #{name} #{to_options(account: account, session: session)}`.tap do
|
||||
raise RuntimeError, "Could not read #{name} from 1Password" unless $?.success?
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_all(*names, account:, session: nil)
|
||||
secrets = {}
|
||||
|
||||
vaults_items_fields(names).each do |vault, items|
|
||||
items.each do |item, fields|
|
||||
labels = fields.map { |field| "label=#{field}" }.join(",")
|
||||
secrets_json = `op item get #{item} #{to_options(vault: vault, fields: labels, format: "json", account: account, session: session.presence)}`.tap do
|
||||
raise RuntimeError, "Could not read #{labels} from #{item} in the #{vault} 1Password vault" unless $?.success?
|
||||
end
|
||||
|
||||
JSON.parse(secrets_json).each do |secret_json|
|
||||
secrets[secret_json["reference"]] = secret_json["value"]
|
||||
end
|
||||
{}.tap do |secrets|
|
||||
JSON.parse(secrets_json).each do |secret_json|
|
||||
# The reference is in the form `op://vault/item/field[/field]`
|
||||
field = secret_json["reference"].delete_prefix("op://#{item}/")
|
||||
secrets[field] = secret_json["value"]
|
||||
secrets[field.split("/").last] = secret_json["value"]
|
||||
end
|
||||
end
|
||||
rescue => e
|
||||
$stderr.puts " \e[31mERROR (#{e.class}): #{e.message}\e[0m"
|
||||
|
||||
secrets
|
||||
Process.kill("INT", Process.ppid) if ENV["KAMAL_SECRETS_KILL_PARENT"]
|
||||
exit 1
|
||||
end
|
||||
|
||||
private
|
||||
def vaults_items_fields(names)
|
||||
{}.tap do |vaults|
|
||||
names.each do |name|
|
||||
vault, item, field = vault_item_field(name)
|
||||
vaults[vault] ||= {}
|
||||
vaults[vault][item] ||= []
|
||||
vaults[vault][item] << field
|
||||
end
|
||||
def signin(account)
|
||||
`op signin #{to_options(account: account, force: true, raw: true)}`.tap do
|
||||
raise RuntimeError, "Failed to login to 1Password" unless $?.success?
|
||||
end
|
||||
end
|
||||
|
||||
def vault_item_field(name)
|
||||
parts = name.delete_prefix("op://").split("/")
|
||||
|
||||
[ parts[0], parts[1], parts[2..-1].join(".") ]
|
||||
end
|
||||
|
||||
def to_options(**options)
|
||||
optionize(options.compact).join(" ")
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user