lib/hu/collab.rb in hu-0.1.0 vs lib/hu/collab.rb in hu-1.0.0

- old
+ new

@@ -1,51 +1,107 @@ require 'powerbar' require 'yaml' require 'hashdiff' +require 'set' require 'platform-api' module Hu class Cli < Optix::Cli class Collab < Optix::Cli - desc "Print current mapping to stdout" - opt :format, "yaml|json", :default => 'yaml' - parent "collab", "Application collaborators" - def export(cmd, opts, argv) - puts heroku_state.send("to_#{opts[:format]}".to_sym) - end + text "Manage application/collaborator mapping." + text "" + text "Start by exporting the current mapping," + text "edit to taste, then diff and import." + text "" + text "WARNING: If you remove yourself from an application" + text " then hu won't be able to see it anymore." + text "" + text "Please note that hu can only operate on applications and" + text "collaborators that it sees. It will stubbornly refuse to" + text "create new applications or collaborators." + text "" + text "Follow this procedure to introduce a new collaborator:" + text "" + text "1. Create collaborator on heroku (via web or toolbelt)" + text "2. Add them to at least one application that you have access to" + text "" + text "Now the new collaborator will show up in 'export'" + text "and may be assigned to applications." + def collab; end OP_COLORS = { '+' => "\e[0;1;32m", '-' => "\e[0;1;31m", '~' => "\e[0;32m", } desc "Read mapping from stdin and diff to heroku state" + text "Read mapping from stdin and diff to heroku state" parent "collab" def diff(cmd, opts, argv) parsed_state = parse_as_json_or_yaml(STDIN.read) - plan(HashDiff.diff(heroku_state, parsed_state)).each do |s| + plan(HashDiff.diff(heroku_state['apps'], parsed_state['apps'])).each do |s| color = OP_COLORS[s[:op]] msg = '' if s[:method].nil? color = "\e[0;41;33;1m" msg = "Can not resolve this." end - printf "%s%6s %-30s %-15s %-30s %s\e[0m\n", color, s[:op_name], s[:app_name], s[:role], s[:value], msg + STDERR.printf "%s%6s %-30s %-15s %-30s %s\e[0m\n", color, s[:op_name], s[:app_name], s[:role], s[:value], msg end end + desc "Print current mapping to stdout" + text "Print current mapping to stdout" + opt :format, "yaml|json", :default => 'yaml' + parent "collab", "Application collaborators" + def export(cmd, opts, argv) + puts heroku_state.send("to_#{opts[:format]}".to_sym) + end + + desc "Read mapping from stdin and push to heroku" + text "Read mapping from stdin and push to heroku" + parent "collab" + def import(cmd, opts, argv) + parsed_state = parse_as_json_or_yaml(STDIN.read) + plan(HashDiff.diff(heroku_state['apps'], parsed_state['apps'])).each do |s| + color = OP_COLORS[s[:op]] + msg = '' + icon = ' ' + eol = "\e[100D" + 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 + self.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 + end + OP_MAP = { '+' => 'add', '-' => 'remove', '~' => 'change', } def plan(diff) plan = [] - diff.each do |op, target, value| + diff.each do |op, target, lval, rval| + value = rval || lval app_name, role = target.split('.') - role = role.split('[')[0] + + role = role.split('[')[0] unless role.nil? op_name = OP_MAP[op] method_name = "op_#{op_name}_#{role}".to_sym plan << { app_name: app_name, op: op, @@ -56,16 +112,16 @@ } end plan end - def op_add_collaborators(name) - raise NotImplementedError + def op_add_collaborators(args) + h.collaborator.create(args[:app_name], :user => args[:value]) end - def op_remove_collaborators(name) - raise NotImplementedError + def op_remove_collaborators(args) + h.collaborator.delete(args[:app_name], args[:value]) end def parse_as_json_or_yaml(input) begin parsed = JSON.load(input) @@ -82,31 +138,53 @@ STDERR.puts "-- YAML Error --" STDERR.puts yex raise ArgumentError end end + normalize(parsed) + end + + def normalize(parsed) + unless parsed.include? 'apps' + raise ArgumentError, "Malformed input, key 'apps' not found." + end + 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!.sort!.each do |collab| + unless heroku_state['collaborators'].include? collab + raise ArgumentError, "Unknown collaborator: #{collab}" + end + end + end parsed end - def heroku_state - data = {} + def heroku_state(force_refresh=false) + return @heroku_state unless force_refresh or @heroku_state.nil? + all_collaborators = Set.new + data = { 'apps' => {} } app_names = h.app.list.map{|e| e['name']} app_names.each_with_index do |app_name,i| pb :msg => app_name, :total => app_names.length, :done => i+1 - data[app_name] = { 'collaborators' => [] } - h.collaborator.list(app_name).map{|e| - case e['role'] - when 'owner' - data[app_name]['owner'] = e['user']['email'] - when nil - data[app_name]['collaborators'] << e['user']['email'] - else - raise RuntimeError, "Unknown collaborator role: #{e['role']}" - end - } + d = data['apps'][app_name] = { 'collaborators' => [] } + h.collaborator.list(app_name).map{|e| + case e['role'] + when 'owner' + d['owner'] = e['user']['email'] + when nil + d['collaborators'] << e['user']['email'] + else + raise RuntimeError, "Unknown collaborator role: #{e['role']}" + end + all_collaborators << e['user']['email'] + } end pb :wipe - data + data['collaborators'] = all_collaborators.to_a.sort + @heroku_state = data end def h @h ||= PlatformAPI.connect_oauth(Hu::Cli::API_TOKEN) end