class SecretConfig::CLI

Constants

PROVIDERS

Attributes

console[R]
copy_path[R]
delete_key[R]
delete_tree[R]
diff[R]
diff_path[R]
export[R]
fetch_key[R]
file_name[R]
force[R]
import[R]
import_path[R]
interpolate[R]
key_alias[R]
key_id[R]
no_filter[R]
path[R]
provider[R]
prune[R]
random_size[R]
set_key[R]
set_value[R]
show_version[R]

Public Class Methods

new(argv) click to toggle source
# File lib/secret_config/cli.rb, line 44
def initialize(argv)
  @export       = false
  @import       = false
  @path         = nil
  @key_id       = nil
  @key_alias    = nil
  @provider     = :ssm
  @random_size  = 32
  @no_filter    = false
  @prune        = false
  @replace      = false
  @copy_path    = nil
  @show_version = false
  @console      = false
  @diff         = false
  @set_key      = nil
  @set_value    = nil
  @fetch_key    = nil
  @delete_key   = nil
  @delete_tree  = nil
  @diff_path    = nil
  @import_path  = nil
  @force        = false
  @interpolate  = false

  if argv.empty?
    puts parser
    exit(-10)
  end
  parser.parse!(argv)
end
run!(argv) click to toggle source
# File lib/secret_config/cli.rb, line 40
def self.run!(argv)
  new(argv).run!
end

Public Instance Methods

parser() click to toggle source
# File lib/secret_config/cli.rb, line 110
    def parser
      @parser ||= OptionParser.new do |opts|
        opts.banner = <<~BANNER
          Secret Config v#{VERSION}

            For more information, see: https://rocketjob.github.io/secret_config/

          secret-config [options]
        BANNER

        opts.on "-e", "--export SOURCE_PATH", "Export configuration. Use --file to specify the file name, otherwise stdout is used." do |path|
          @export = path
        end

        opts.on "-i", "--import TARGET_PATH", "Import configuration. Use --file to specify the file name, --path for the SOURCE_PATH, otherwise stdin is used." do |path|
          @import = path
        end

        opts.on "-f", "--file FILE_NAME", "Import/Export/Diff to/from this file." do |file_name|
          @file_name = file_name
        end

        opts.on "-p", "--path PATH", "Import/Export/Diff to/from this path." do |path|
          @path = path
        end

        opts.on "--diff TARGET_PATH", "Compare configuration to this path. Use --file to specify the source file name, --path for the SOURCE_PATH, otherwise stdin is used." do |file_name|
          @diff = file_name
        end

        opts.on "-s", "--set KEY=VALUE", "Set one key to value. Example: --set mysql/database=localhost" do |param|
          @set_key, @set_value = param.split("=")
          unless @set_key && @set_value
            raise(ArgumentError, "Supply key and value separated by '='. Example: --set mysql/database=localhost")
          end
        end

        opts.on "-f", "--fetch KEY", "Fetch the value for one setting. Example: --fetch mysql/database." do |key|
          @fetch_key = key
        end

        opts.on "-d", "--delete KEY", "Delete one specific key." do |key|
          @delete_key = key
        end

        opts.on "-r", "--delete-tree PATH", "Recursively delete all keys under the specified path." do |path|
          @delete_tree = path
        end

        opts.on "-c", "--console", "Start interactive console." do
          @console = true
        end

        opts.on "--provider PROVIDER", "Provider to use. [ssm | file]. Default: ssm" do |provider|
          @provider = provider.to_sym
        end

        opts.on "--no-filter", "For --export only. Do not filter passwords and keys." do
          @no_filter = true
        end

        opts.on "--interpolate", "For --export only. Evaluate string interpolation and __import__." do
          @interpolate = true
        end

        opts.on "--prune", "For --import only. During import delete all existing keys for which there is no key in the import file. Only works with --import." do
          @prune = true
        end

        opts.on "--force", "For --import only. Overwrite all values, not just the changed ones. Useful for changing the KMS key." do
          @force = true
        end

        opts.on "--key_id KEY_ID", "For --import only. Encrypt config settings with this AWS KMS key id. Default: AWS Default key." do |key_id|
          @key_id = key_id
        end

        opts.on "--key_alias KEY_ALIAS", "For --import only. Encrypt config settings with this AWS KMS alias." do |key_alias|
          @key_alias = key_alias
        end

        opts.on "--random_size INTEGER", Integer, "For --import only. Size to use when generating random values when $(random) is encountered in the source. Default: 32" do |random_size|
          @random_size = random_size
        end

        opts.on "-v", "--version", "Display Secret Config version." do
          @show_version = true
        end

        opts.on("-h", "--help", "Prints this help.") do
          puts opts
          exit
        end
      end
    end
run!() click to toggle source
# File lib/secret_config/cli.rb, line 76
def run!
  if show_version
    puts "Secret Config v#{VERSION}"
  elsif console
    run_console
  elsif export
    raise(ArgumentError, "--path option is not valid for --export") if path

    run_export(export, file_name || STDOUT, filtered: !no_filter)
  elsif import
    if path
      run_import_path(import, path, prune, force)
    else
      run_import(import, file_name || STDIN, prune, force)
    end
  elsif diff
    if path
      run_diff_path(diff, path)
    else
      run_diff(diff, file_name || STDIN)
    end
  elsif set_key
    run_set(set_key, set_value)
  elsif fetch_key
    run_fetch(fetch_key)
  elsif delete_key
    run_delete(delete_key)
  elsif delete_tree
    run_delete_tree(delete_tree)
  else
    puts parser
  end
end

Private Instance Methods

current_values(path) click to toggle source
# File lib/secret_config/cli.rb, line 307
def current_values(path)
  Utils.flatten(fetch_config(path, filtered: false), path)
end
diff_config(target, source) click to toggle source

Diffs two configs and displays the results

# File lib/secret_config/cli.rb, line 354
def diff_config(target, source)
  (source.keys + target.keys).sort.uniq.each do |key|
    if target.key?(key)
      if source.key?(key)
        value = source[key].to_s
        # Ignore filtered values
        if (value != target[key].to_s) && (value != FILTERED)
          puts "#{Colors::KEY}#{key}:"
          puts "#{Colors::REMOVE}#{prefix_lines('- ', target[key])}"
          puts "#{Colors::ADD}#{prefix_lines('+ ', source[key])}#{Colors::CLEAR}\n\n"
        end
      else
        puts "#{Colors::KEY}#{key}:"
        puts "#{Colors::REMOVE}#{prefix_lines('- ', target[key])}\n\n"
      end
    elsif source.key?(key)
      puts "#{Colors::KEY}#{key}:"
      puts "#{Colors::ADD}#{prefix_lines('+ ', source[key])}#{Colors::CLEAR}\n\n"
    end
  end
end
fetch_config(path, filtered: true) click to toggle source
# File lib/secret_config/cli.rb, line 347
def fetch_config(path, filtered: true)
  registry = Registry.new(path: path, provider: provider_instance, interpolate: interpolate)
  config   = filtered ? registry.configuration : registry.configuration(filters: nil)
  sort_hash_by_key!(config)
end
file_format(file_name) click to toggle source
# File lib/secret_config/cli.rb, line 437
def file_format(file_name)
  return :yml unless file_name.is_a?(String)

  case File.extname(file_name).downcase
  when ".yml", ".yaml"
    :yml
  when ".json"
    :json
  else
    raise ArgumentError, "Import/Export file name must end with '.yml' or '.json'"
  end
end
import_config(config, path, prune, force) click to toggle source
# File lib/secret_config/cli.rb, line 380
def import_config(config, path, prune, force)
  current     = current_values(path)
  delete_keys = prune ? current.keys - Utils.flatten(config, path).keys : []

  unless delete_keys.empty?
    puts "Going to delete the following keys:"
    delete_keys.each { |key| puts "  #{key}" }
    sleep(5)
  end

  set_config(config, path, force ? {} : current)

  delete_keys.each do |key|
    puts "#{Colors::REMOVE}- #{key}#{Colors::CLEAR}"
    provider_instance.delete(key)
  end
end
parse(data, format) click to toggle source
# File lib/secret_config/cli.rb, line 424
def parse(data, format)
  config =
    case format
    when :yml
      YAML.safe_load(ERB.new(data).result)
    when :json
      JSON.parse(data)
    else
      raise ArgumentError, "Invalid format: #{format.inspect}"
    end
  sort_hash_by_key!(config)
end
prefix_lines(prefix, value) click to toggle source
# File lib/secret_config/cli.rb, line 376
def prefix_lines(prefix, value)
  value.to_s.lines.collect { |line| "#{prefix}#{line}" }.join("")
end
provider_instance() click to toggle source
# File lib/secret_config/cli.rb, line 208
def provider_instance
  @provider_instance ||=
    begin
      case provider
      when :ssm
        if key_alias
          Providers::Ssm.new(key_alias: key_alias)
        elsif key_id
          Providers::Ssm.new(key_id: key_id)
        else
          Providers::Ssm.new
        end
      else
        raise ArgumentError, "Invalid provider: #{provider}"
      end
    end
end
random_password() click to toggle source
# File lib/secret_config/cli.rb, line 450
def random_password
  SecureRandom.urlsafe_base64(random_size)
end
read_config_file(file_name) click to toggle source
# File lib/secret_config/cli.rb, line 311
def read_config_file(file_name)
  format = file_format(file_name)
  data   = read_file(file_name)
  parse(data, format)
end
read_file(file_name_or_io) click to toggle source
# File lib/secret_config/cli.rb, line 398
def read_file(file_name_or_io)
  return file_name_or_io.read unless file_name_or_io.is_a?(String)

  ::File.new(file_name_or_io).read
end
render(hash, format) click to toggle source
# File lib/secret_config/cli.rb, line 413
def render(hash, format)
  case format
  when :yml
    hash.to_yaml
  when :json
    hash.to_json
  else
    raise ArgumentError, "Invalid format: #{format.inspect}"
  end
end
run_console() click to toggle source
# File lib/secret_config/cli.rb, line 277
def run_console
  IRB.start
end
run_delete(key) click to toggle source
# File lib/secret_config/cli.rb, line 281
def run_delete(key)
  puts "#{Colors::TITLE}--- #{provider}:#{path}"
  puts "#{Colors::REMOVE}- #{key}#{Colors::CLEAR}"
  provider_instance.delete(key)
end
run_delete_tree(path) click to toggle source
# File lib/secret_config/cli.rb, line 287
def run_delete_tree(path)
  source_config = fetch_config(path)
  puts "#{Colors::TITLE}--- #{provider}:#{path}#{Colors::CLEAR}"

  source = Utils.flatten(source_config, path)
  source.each_key do |key|
    puts "#{Colors::REMOVE}- #{key}#{Colors::CLEAR}"
    provider_instance.delete(key)
  end
end
run_diff(target_path, file_name) click to toggle source
# File lib/secret_config/cli.rb, line 250
def run_diff(target_path, file_name)
  source_config = read_config_file(file_name)
  source        = Utils.flatten(source_config, target_path)

  target_config = fetch_config(target_path, filtered: false)
  target        = Utils.flatten(target_config, target_path)

  if file_name.is_a?(String)
    puts "#{Colors::TITLE}--- #{provider}:#{target_path}"
    puts "+++ #{file_name}#{Colors::CLEAR}"
  end
  diff_config(target, source)
end
run_diff_path(target_path, source_path) click to toggle source
# File lib/secret_config/cli.rb, line 264
def run_diff_path(target_path, source_path)
  source_config = fetch_config(source_path, filtered: false)
  source        = Utils.flatten(source_config)

  target_config = fetch_config(target_path, filtered: false)
  target        = Utils.flatten(target_config)

  puts "#{Colors::TITLE}--- #{provider}:#{target_path}"
  puts "+++ #{provider}:#{source_path}#{Colors::CLEAR}"

  diff_config(target, source)
end
run_export(source_path, file_name, filtered: true) click to toggle source
# File lib/secret_config/cli.rb, line 226
def run_export(source_path, file_name, filtered: true)
  puts("Exporting #{provider}:#{source_path} to #{file_name}") if file_name.is_a?(String)

  config = fetch_config(source_path, filtered: filtered)
  write_config_file(file_name, config)
end
run_fetch(key) click to toggle source
# File lib/secret_config/cli.rb, line 298
def run_fetch(key)
  value = provider_instance.fetch(key)
  puts value if value
end
run_import(target_path, file_name, prune, force) click to toggle source
# File lib/secret_config/cli.rb, line 233
def run_import(target_path, file_name, prune, force)
  puts "#{Colors::TITLE}--- #{provider}:#{target_path}"
  puts "+++ #{file_name}#{Colors::CLEAR}"
  config = read_config_file(file_name)
  import_config(config, target_path, prune, force)
end
run_import_path(target_path, source_path, prune, force) click to toggle source
# File lib/secret_config/cli.rb, line 240
def run_import_path(target_path, source_path, prune, force)
  puts "#{Colors::TITLE}--- #{provider}:#{target_path}"
  puts "+++ #{provider}:#{source_path}#{Colors::CLEAR}"

  config = fetch_config(source_path, filtered: false)
  import_config(config, target_path, prune, force)

  puts("Imported #{target_path} from #{source_path} on provider: #{provider}")
end
run_set(key, value) click to toggle source
# File lib/secret_config/cli.rb, line 303
def run_set(key, value)
  provider_instance.set(key, value)
end
set_config(config, path, current_values = {}) click to toggle source
# File lib/secret_config/cli.rb, line 323
def set_config(config, path, current_values = {})
  Utils.flatten_each(config, path) do |key, value|
    next if value.nil?
    next if current_values[key].to_s == value.to_s

    if value.to_s.strip == RANDOM
      next if current_values[key]

      value = random_password
    elsif value == FILTERED
      # Ignore filtered values
      next
    end

    if current_values.key?(key)
      puts "#{Colors::KEY}* #{key}#{Colors::CLEAR}"
    else
      puts "#{Colors::ADD}+ #{key}#{Colors::CLEAR}"
    end

    provider_instance.set(key, value)
  end
end
sort_hash_by_key!(h) click to toggle source
# File lib/secret_config/cli.rb, line 454
def sort_hash_by_key!(h)
  h.keys.sort.each do |key|
    value = h[key] = h.delete(key)
    sort_hash_by_key!(value) if value.is_a?(Hash)
  end
  h
end
write_config_file(file_name, config) click to toggle source
# File lib/secret_config/cli.rb, line 317
def write_config_file(file_name, config)
  format = file_format(file_name)
  data   = render(config, format)
  write_file(file_name, data)
end
write_file(file_name_or_io, data) click to toggle source
# File lib/secret_config/cli.rb, line 404
def write_file(file_name_or_io, data)
  return file_name_or_io.write(data) unless file_name_or_io.is_a?(String)

  output_path = ::File.dirname(file_name_or_io)
  FileUtils.mkdir_p(output_path) unless ::File.exist?(output_path)

  ::File.open(file_name_or_io, "w") { |io| io.write(data) }
end