class VaultUpdate

Constants

VERSION

Public Instance Methods

run() click to toggle source
# File lib/vault-update.rb, line 15
def run
  if opts[:history]
    secret_history.sort_by { |ts, _data| ts }[-history_fetch_size..-1].each do |ts, data|
      puts "#{Time.at(ts.to_s.to_i)}:".colorize(:green)
      puts JSON.pretty_generate(data) + "\n\n"
    end
  elsif opts[:last]
    puts JSON.pretty_generate(
      (secret_history.sort_by { |ts, _data| ts }.last || fail(NoHistoryError))[1]
    )
  elsif opts[:rollback]
    rollback_secret
  elsif opts[:current]
    puts JSON.pretty_generate(vault_read(opts[:path]) || fail(NoValueError))
  else
    update
  end
rescue MissingInputError, TypeError => e
  raise e unless e.class == TypeError && e.message == 'no implicit conversion of nil into String'
  Trollop.die 'KEY and VALUE must be provided'
rescue NoUpdateError
  puts 'Nothing to do'
  exit 0
rescue NoHistoryError
  puts 'ERROR: '.colorize(:red) + "There is no history for #{opts[:path]}"
  exit 2
rescue NoValueError
  puts 'ERROR: '.colorize(:red) + "There is no current value for #{opts[:path]}"
  exit 3
end

Private Instance Methods

debug?() click to toggle source
# File lib/vault-update.rb, line 73
def debug?
  ENV['DEBUG']
end
history_fetch_size() click to toggle source
# File lib/vault-update.rb, line 48
def history_fetch_size
  opts[:history] > secret_history.keys.count ? secret_history.keys.count : opts[:history]
end
opts() click to toggle source
# File lib/vault-update.rb, line 143
def opts
  @opts ||= begin
    opts = Trollop.options do
      version "vault-update #{VaultUpdate::VERSION} (c) 2017 Evertrue"
      banner(
        "Safely update Vault secrets (with rollbacks and history!)\n\n" \
        "Usage:\n" \
        "       vault-update [options] -p SECRET_PATH KEY VALUE\n" \
        "\nEnvironment Variables:\n" \
        "    VAULT_ADDR (required)\n" \
        "    VAULT_TOKEN (required)\n" \
        "\nOptions:"
      )
      opt :rollback, 'Roll back to previous release', short: 'r'
      opt :path, 'Secret path to update', short: 'p', required: true, type: String
      opt :history, 'Show the last N entries of history', short: 's', type: Integer
      opt :last, 'Show the last value', short: 'l'
      opt :current, 'Show the current contents of the secret', short: 'c'
    end

    fail 'VAULT_ADDR and VAULT_TOKEN must be set' unless ENV['VAULT_ADDR'] && ENV['VAULT_TOKEN']

    opts
  end
end
previous_update() click to toggle source
# File lib/vault-update.rb, line 129
def previous_update
  @previous_update ||= begin
    return nil unless (r = secret_history).any?
    r[r.keys.sort.last] # Return the value with the highest key
  end
end
rollback_secret() click to toggle source
# File lib/vault-update.rb, line 77
def rollback_secret
  fail NoHistoryError unless previous_update
  current_secret_value = vault_read opts[:path]

  # Update history with {} if empty now
  secret_history[Time.now.to_i] = (current_secret_value || {})
  vault_write "#{opts[:path]}_history", secret_history

  puts "Writing to #{opts[:path]}:\n".bold + JSON.pretty_generate(previous_update) unless debug?
  vault_write opts[:path], previous_update
end
secret_history() click to toggle source
# File lib/vault-update.rb, line 136
def secret_history
  @secret_history ||= begin
    r = vault_read("#{opts[:path]}_history")
    r ? r.dup : {}
  end
end
update() click to toggle source
# File lib/vault-update.rb, line 52
def update
  update_value = ARGV.pop

  json_value = true

  # JSON is optional in the value field, so we have this funny business
  update_value = (
    begin
      JSON.parse update_value
    rescue JSON::ParserError
      json_value = false
      update_value
    end
  )

  update_key = ARGV.pop

  raise(MissingInputError) unless json_value || update_key
  update_secret(json_value ? update_value : { update_key.to_sym => update_value })
end
update_secret(update_hash) click to toggle source
# File lib/vault-update.rb, line 89
def update_secret(update_hash)
  data =
    if (current_secret_value = vault_read(opts[:path]))
      current_secret_value = current_secret_value.stringify_keys
      merged_value = current_secret_value.merge update_hash.stringify_keys

      if debug?
        puts "current_secret_value: ".colorize(:blue) + current_secret_value.inspect
        puts "merged_value: ".colorize(:blue) + merged_value.inspect
      end

      fail NoUpdateError if current_secret_value == merged_value

      secret_history[Time.now.to_i] = current_secret_value
      vault_write "#{opts[:path]}_history", secret_history

      current_secret_value
    else
      puts "update_hash: ".colorize(:blue) + update_hash.inspect
      update_hash
    end

  puts "data: ".colorize(:blue) + data.inspect if debug?

  puts "Applying changes to #{opts[:path]}:\n".bold

  puts Diffy::Diff.new(
    (
      if current_secret_value
        JSON.pretty_generate(current_secret_value) + "\n" #
      else
        "\n"
      end
    ),
    JSON.pretty_generate(data) + "\n"
  ).to_s(:color)

  vault_write opts[:path], data
end
vault() click to toggle source
# File lib/vault-update.rb, line 187
def vault
  @vault ||= Vault::Client.new
end
vault_read(path) click to toggle source
# File lib/vault-update.rb, line 177
def vault_read(path)
  r = vault.with_retries(Vault::HTTPConnectionError) do |attempt, e|
    puts "Received exception #{e} from Vault - attempt #{attempt}" if e
    vault.logical.read(path)
  end
  res = r ? r.data : nil
  puts "Read from #{path}:\n".colorize(:blue) + res.to_json if debug?
  res
end
vault_write(path, data) click to toggle source
# File lib/vault-update.rb, line 169
def vault_write(path, data)
  puts "Writing to #{path}:\n".colorize(:blue) + data.inspect if debug?
  vault.with_retries(Vault::HTTPConnectionError) do |attempt, e|
    puts "Received exception #{e} from Vault - attempt #{attempt}" if e
    vault.logical.write(path, data)
  end
end