Add secrets command + 1password integration
This commit is contained in:
committed by
Donal McBreen
parent
b464c4fd4a
commit
5910249d02
@@ -211,6 +211,9 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
desc "registry", "Login and -out of the image registry"
|
desc "registry", "Login and -out of the image registry"
|
||||||
subcommand "registry", Kamal::Cli::Registry
|
subcommand "registry", Kamal::Cli::Registry
|
||||||
|
|
||||||
|
desc "secrets", "Helpers for extracting secrets", hide: true
|
||||||
|
subcommand "secrets", Kamal::Cli::Secrets
|
||||||
|
|
||||||
desc "server", "Bootstrap servers with curl and Docker"
|
desc "server", "Bootstrap servers with curl and Docker"
|
||||||
subcommand "server", Kamal::Cli::Server
|
subcommand "server", Kamal::Cli::Server
|
||||||
|
|
||||||
|
|||||||
36
lib/kamal/cli/secrets.rb
Normal file
36
lib/kamal/cli/secrets.rb
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
class Kamal::Cli::Secrets < Kamal::Cli::Base
|
||||||
|
desc "login", "Login to a secrets vault"
|
||||||
|
option :adapter, type: :string, aliases: "-a", required: true, desc: "Which vault adapter to use"
|
||||||
|
option :adapter_options, type: :hash, aliases: "-O", required: true, desc: "Options to pass to the vault adapter"
|
||||||
|
def login
|
||||||
|
puts adapter(options).login(**adapter_options(options))
|
||||||
|
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: true, 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: true, 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"
|
||||||
|
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[:adapter_options].transform_keys(&:to_sym)
|
||||||
|
end
|
||||||
|
end
|
||||||
12
lib/kamal/secrets/adapters.rb
Normal file
12
lib/kamal/secrets/adapters.rb
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
module Kamal::Secrets::Adapters
|
||||||
|
def self.lookup(name)
|
||||||
|
case name
|
||||||
|
when "1password"
|
||||||
|
Kamal::Secrets::Adapters::OnePassword.new
|
||||||
|
else
|
||||||
|
Object.const_get("Kamal::Secrets::Adapters::#{name.camelize}").new
|
||||||
|
end
|
||||||
|
rescue NameError
|
||||||
|
raise RuntimeError, "Unknown secrets adapter: #{name}"
|
||||||
|
end
|
||||||
|
end
|
||||||
56
lib/kamal/secrets/adapters/one_password.rb
Normal file
56
lib/kamal/secrets/adapters/one_password.rb
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
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?
|
||||||
|
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
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
secrets
|
||||||
|
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
|
||||||
|
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
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user