From 79731da6195b7f6d1606e64bc78401c72adb5af8 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Mon, 26 Aug 2024 15:20:13 +0100 Subject: [PATCH] Single fetch command --- lib/kamal/cli/secrets.rb | 33 +++--------- lib/kamal/secrets/adapters/one_password.rb | 60 ++++++++-------------- 2 files changed, 30 insertions(+), 63 deletions(-) diff --git a/lib/kamal/cli/secrets.rb b/lib/kamal/cli/secrets.rb index 5becfdc5..0c9dd5cb 100644 --- a/lib/kamal/cli/secrets.rb +++ b/lib/kamal/cli/secrets.rb @@ -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 diff --git a/lib/kamal/secrets/adapters/one_password.rb b/lib/kamal/secrets/adapters/one_password.rb index 36a439c2..f5f1f8e1 100644 --- a/lib/kamal/secrets/adapters/one_password.rb +++ b/lib/kamal/secrets/adapters/one_password.rb @@ -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