Class: Hu::Cli::Collab

Inherits:
Optix::Cli
  • Object
show all
Defined in:
lib/hu/collab.rb

Defined Under Namespace

Classes: InvalidOperation, InvalidPlan

Constant Summary collapse

OP_COLORS =
{
  '+' => "\e[0;1;32m",
  '-' => "\e[0;1;34m",
  '~' => "\e[0;32m"
}.freeze
OP_MAP =
{
  '+' => 'add',
  '-' => 'remove',
  '~' => 'change'
}.freeze

Instance Method Summary collapse

Instance Method Details

#collabObject



46
# File 'lib/hu/collab.rb', line 46

def collab; end

#diff(_cmd, opts, _argv) ⇒ Object



56
57
58
59
# 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) ⇒ Object



85
86
87
# File 'lib/hu/collab.rb', line 85

def export(_cmd, opts, _argv)
  puts heroku_state.send("to_#{opts[:format]}".to_sym)
end

#hObject



254
255
256
# File 'lib/hu/collab.rb', line 254

def h
  @h ||= PlatformAPI.connect_oauth(Hu::API_TOKEN)
end

#heroku_state(force_refresh = false) ⇒ Object



223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# 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) ⇒ Boolean

Returns:

  • (Boolean)


216
217
218
219
220
221
# 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) ⇒ Object



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# 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) ⇒ Object



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# 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) ⇒ Object



171
172
173
# 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) ⇒ Object



175
176
177
# 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) ⇒ Object



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# 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) ⇒ Object



258
259
260
261
262
# 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 = {}) ⇒ Object

Raises:



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# 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) ⇒ Object



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# 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