class Hu::Cli::Collab

Constants

OP_COLORS
OP_MAP

Public Instance Methods

collab() click to toggle source
# File lib/hu/collab.rb, line 46
def collab; end
diff(_cmd, opts, _argv) click to toggle source
# File lib/hu/collab.rb, line 56
def diff(_cmd, opts, _argv)
  parsed_state = parse_as_json_or_yaml(STDIN.read)
  show_plan(plan(HashDiff.diff(heroku_state['apps'], parsed_state['apps']), opts))
end
export(_cmd, opts, _argv) click to toggle source
# File lib/hu/collab.rb, line 85
def export(_cmd, opts, _argv)
  puts heroku_state.send("to_#{opts[:format]}".to_sym)
end
h() click to toggle source
# File lib/hu/collab.rb, line 254
def h
  @h ||= PlatformAPI.connect_oauth(Hu::API_TOKEN)
end
heroku_state(force_refresh = false) click to toggle source
# File lib/hu/collab.rb, line 223
def heroku_state(force_refresh = false)
  return @heroku_state unless force_refresh || @heroku_state.nil?
  all_collaborators = Set.new
  data = { 'apps' => {} }
  app_names = h.app.list.map { |e| e['name'] }.reject { |e| ignored_app?(e) }
  threads = []
  app_names.each_with_index do |app_name, _i|
    threads << Thread.new do
      d = data['apps'][app_name] = { 'collaborators' => [] }
      h.collaborator.list(app_name).map do |e|
        case e['role']
        when 'owner'
          d['owner'] = e['user']['email']
        when nil, 'member'
          d['collaborators'] << e['user']['email']
        else
          raise "Unknown collaborator role: #{e['role']}"
        end
        all_collaborators << e['user']['email']
      end
    end
  end
  threads.each_with_index do |t, i|
    t.join
    pb msg: app_names[i], total: app_names.length, done: i + 1
  end
  pb :wipe
  data['collaborators'] = all_collaborators.to_a.sort
  @heroku_state = data
end
ignored_app?(app_name) click to toggle source
# File lib/hu/collab.rb, line 216
def ignored_app?(app_name)
  ENV.fetch('HU_IGNORE_APPS', '').split(' ').each do |p|
    return true if File.fnmatch(p, app_name)
  end
  false
end
import(_cmd, opts, _argv) click to toggle source
# File lib/hu/collab.rb, line 94
def import(_cmd, opts, _argv)
  parsed_state = parse_as_json_or_yaml(STDIN.read)
  validators = {
    op_add_collaborators: proc do |op|
      unless heroku_state['collaborators'].include?(op[:value]) || opts[:allow_create]
        raise InvalidOperation, "Use -c to allow creation of new collaborator '#{op[:value]}'"
      end
    end
  }
  begin
    plan(HashDiff.diff(heroku_state['apps'], parsed_state['apps']), opts, validators).each do |s|
      color = OP_COLORS[s[:op]]
      msg = ''
      icon = ' '
      eol = "\e[1G"
      if s[:method].nil?
        color = "\e[0;41;33;1m"
        msg = 'Skipped.'
        icon = "\e[0;31;1m\u2718\e[0m" # X
        eol = "\n"
      end
      STDERR.printf "%s %s%6s %-30s %-15s %-30s %s\e[0m%s", icon, color, s[:op_name], s[:app_name], s[:role], s[:value], msg, eol
      next if s[:method].nil?
      begin
        send(s[:method], s)
        STDERR.puts "\e[0;32;1m\u2713\e[0m\n" # check
      rescue => e
        STDERR.puts "\e[0;31;1m\u2718\e[0m\n" # X
        puts e.inspect
        puts e.backtrace
        exit 1
      end
    end # /plan()
  rescue InvalidPlan => e
    STDERR.puts "\e[0;31;1m#{e}:\e[0m\n\n"
    show_plan(e.invalid_plan)
    exit 1
  end
end
normalize(parsed) click to toggle source
# File lib/hu/collab.rb, line 200
def normalize(parsed)
  unless parsed.include? 'apps'
    raise ArgumentError, "Malformed input, key 'apps' not found."
  end
  parsed['apps'].reject! { |e| ignored_app?(e) }
  parsed['apps'].each do |app_name, v|
    unless heroku_state['apps'].include? app_name
      raise ArgumentError, "Unknown application: #{app_name}"
    end
    next unless v['collaborators'].is_a? Array
    v['collaborators'].flatten!
    v['collaborators'].sort!
  end
  parsed
end
op_add_collaborators(args) click to toggle source
# File lib/hu/collab.rb, line 171
def op_add_collaborators(args)
  h.collaborator.create(args[:app_name], user: args[:value], silent: args[:env][:silent_create])
end
op_remove_collaborators(args) click to toggle source
# File lib/hu/collab.rb, line 175
def op_remove_collaborators(args)
  h.collaborator.delete(args[:app_name], args[:value])
end
parse_as_json_or_yaml(input) click to toggle source
# File lib/hu/collab.rb, line 179
def parse_as_json_or_yaml(input)
  begin
    parsed = JSON.load(input)
  rescue => jex
    begin
      parsed = YAML.load(input)
      if parsed.is_a? String
        raise ArgumentError, 'Input parsed as YAML yields a String'
      end
    rescue => yex
      STDERR.puts 'Error: Could neither parse stdin as YAML nor as JSON.'
      STDERR.puts '-- JSON Error --'
      STDERR.puts jex
      STDERR.puts '-- YAML Error --'
      STDERR.puts yex
      raise ArgumentError
    end
  end
  normalize(parsed)
end
pb(show_opts) click to toggle source
# File lib/hu/collab.rb, line 258
def pb(show_opts)
  return if $quiet
  @pb ||= PowerBar.new
  show_opts == :wipe ? @pb.wipe : @pb.show(show_opts)
end
plan(diff, env = {}, validators = {}) click to toggle source
# File lib/hu/collab.rb, line 139
def plan(diff, env = {}, validators = {})
  plan = []
  last_error = nil
  diff.each do |op, target, lval, rval|
    value = rval || lval
    app_name, role = target.split('.')

    role = role.split('[')[0] unless role.nil?
    op_name = OP_MAP[op]
    method_name = "op_#{op_name}_#{role}".to_sym
    operation = {
      app_name: app_name,
      op: op,
      op_name: op_name,
      method: respond_to?(method_name) ? method_name : nil,
      value: value,
      role: role,
      env: env
    }
    if validators.include? method_name
      begin
        validators[method_name].call(operation)
      rescue InvalidOperation => e
        last_error = operation[:invalid] = e
      end
    end
    plan << operation
  end
  raise InvalidPlan.new('Plan did not pass validation', plan) unless last_error.nil?
  plan
end
show_plan(plan) click to toggle source
# File lib/hu/collab.rb, line 61
def show_plan(plan)
  plan.each do |s|
    color = OP_COLORS[s[:op]]
    msg = ''
    icon = ' '
    if s[:method].nil?
      color = "\e[0m"
      msg = '<-- Can not resolve this (NO-OP)'
      icon = '⚠️'
    elsif s[:invalid]
      color = "\e[0m"
      icon = '⚠️'
    end
    STDERR.printf "%1s %s%6s %-30s %-15s %-30s %s\e[0m\n", icon, color, s[:op_name], s[:app_name], s[:role], s[:value], msg
    if s[:invalid]
      STDERR.puts "\e[31;1m         Error: #{s[:invalid]}\e[0m\n\n"
    end
  end
end