Class: Hygroscope::Cli

Inherits:
Thor
  • Object
show all
Includes:
Thor::Actions
Defined in:
lib/hygroscope/cli.rb

Instance Method Summary collapse

Constructor Details

#initialize(*args) ⇒ Cli

Returns a new instance of Cli.



7
8
9
# File 'lib/hygroscope/cli.rb', line 7

def initialize(*args)
  super(*args)
end

Instance Method Details

#createObject



222
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
# File 'lib/hygroscope/cli.rb', line 222

def create
  check_path
  validate

  # Prepare task takes care of shared logic between "create" and "update"
  template, paramset = prepare

  stack = Hygroscope::Stack.new(options[:name], options[:region], options[:profile])
  stack.parameters = paramset.parameters
  stack.template = template.compress

  stack.tags['X-Hygroscope-Template'] = hygro_name
  options[:tags].each do |tag|
    if paramset.get(tag)
      stack.tags[tag] = paramset.get(tag)
    else
      say_status('info', "Skipping tag #{tag} because it does not exist", :blue)
    end
  end

  stack.capabilities = ['CAPABILITY_IAM']
  stack.timeout = 60

  stack.create!

  status
end

#deleteObject



294
295
296
297
298
299
300
301
302
303
# File 'lib/hygroscope/cli.rb', line 294

def delete
  check_path
  abort unless options[:force] ||
               yes?("Really delete stack #{options[:name]} [y/N]?")

  say('Deleting stack!')
  stack = Hygroscope::Stack.new(options[:name], options[:region], options[:profile])
  stack.delete!
  status
end

#generateObject



377
378
379
380
381
382
383
384
385
386
# File 'lib/hygroscope/cli.rb', line 377

def generate
  check_path
  t = Hygroscope::Template.new(template_path, options[:region], options[:profile])
  if options[:color]
    require 'json_color'
    puts JsonColor.colorize(t.process)
  else
    puts t.process
  end
end

#paramsetObject



411
412
413
414
415
416
417
# File 'lib/hygroscope/cli.rb', line 411

def paramset
  if options[:name]
    say_paramset(options[:name])
  else
    say_paramset_list
  end
end

#prepareObject



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
133
134
135
136
137
138
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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/hygroscope/cli.rb', line 95

def prepare
  # Generate the template
  t = Hygroscope::Template.new(template_path, options[:region], options[:profile])

  # If the paramset exists load it, otherwise instantiate an empty one
  p = Hygroscope::ParamSet.new(options[:paramset])

  if options[:paramset]
    # User provided a paramset, so load it and determine which parameters
    # are set and which need to be prompted.
    paramset_keys = p.parameters.keys
    template_keys = t.parameters.keys

    # Reject any keys in paramset that are not requested by template
    rejected_keys = paramset_keys - template_keys
    say_status('info', "Keys in paramset not requested by template: #{rejected_keys.join(', ')}", :blue) unless rejected_keys.empty?

    # Prompt for any key that is missing. If "ask" option was passed,
    # prompt for every key.
    missing = options[:ask] ? template_keys : template_keys - paramset_keys
  else
    # No paramset provided, so every parameter is missing!
    missing = t.parameters.keys
  end

  options[:existing].each do |existing|
    # User specified an existing stack from which to pull outputs and
    # translate into parameters. Load the existing stack.
    e = Hygroscope::Stack.new(existing, options[:region], options[:profile])
    say_status('info', "Populating parameters from #{existing} stack", :blue)

    # Fill any template parameter that matches an output from the existing
    # stack, overwriting values from the paramset object. The user will
    # be prompted to change these if they were not in the paramset or the
    # --ask option was passed.
    e.describe.outputs.each do |o|
      p.set(o.output_key, o.output_value) if t.parameters.keys.include?(o.output_key)
    end
  end if options[:existing].is_a?(Array)

  # Prompt for each missing parameter and save it in the paramset object
  missing.each do |key|
    # Do not prompt for keys prefixed with the "Hygroscope" reserved word.
    # These parameters are populated internally without user input.
    next if key =~ /^Hygroscope/

    type = t.parameters[key]['Type']
    default = p.get(key) ? p.get(key) : t.parameters[key]['Default'] || ''
    description = t.parameters[key]['Description'] || false
    values = t.parameters[key]['AllowedValues'] || false
    no_echo = t.parameters[key]['NoEcho'] || false

    # Thor conveniently provides some nice logic for formatting,
    # allowing defaults, and validating user input
    ask_opts = {}
    ask_opts[:default] = default unless default.to_s.empty?
    ask_opts[:limited_to] = values if values
    ask_opts[:echo] = false if no_echo

    puts
    say("#{description} (#{type})") if description
    # Make sure user enters a value
    # TODO: Better input validation
    answer = ''
    answer = ask(key, :cyan, ask_opts) until answer != ''

    # Save answer to paramset object
    p.set(key, answer)

    # Add a line break
    say if no_echo
  end

  # Offer to save paramset if it was modified
  # Filter out keys beginning with "Hygroscope" since they are not visible
  # to the user and may be modified on each invocation.
  unless missing.reject { |k| k =~ /^Hygroscope/ }.empty?
    puts
    if yes?('Save changes to paramset?')
      unless options[:paramset]
        p.name = ask('Paramset name', :cyan, default: options[:name])
      end
      p.save!
    end
  end

  # Upload payload
  payload_path = File.join(Dir.pwd, 'payload')
  if File.directory?(payload_path)
    payload = Hygroscope::Payload.new(payload_path, options[:region], options[:profile])
    payload.prefix = options[:name]
    payload.upload!
    p.set('HygroscopePayloadBucket', payload.bucket) if missing.include?('HygroscopePayloadBucket')
    p.set('HygroscopePayloadKey', payload.key) if missing.include?('HygroscopePayloadKey')
    p.set('HygroscopePayloadSignedUrl', payload.generate_url) if missing.include?('HygroscopePayloadSignedUrl')
    say_status('ok', 'Payload uploaded to:', :green)
    say_status('', "s3://#{payload.bucket}/#{payload.key}")
  end

  [t, p]
end

#statusObject



310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
# File 'lib/hygroscope/cli.rb', line 310

def status
  check_path
  stack = Hygroscope::Stack.new(options[:name], options[:region], options[:profile])

  # Query and display the status of the stack and its resources. Refresh
  # every 10 seconds until the user aborts or an error is encountered.
  begin
    s = stack.describe

    system('clear') || system('cls')

    header = {
      'Name:'    => s.stack_name,
      'Created:' => s.creation_time,
      'Status:'  => colorize_status(s.stack_status)
    }

    print_table header
    puts

    # Fancy acrobatics to fit output to terminal width. If the terminal
    # window is too small, fallback to something appropriate for ~80 chars
    term_width = `stty size 2>/dev/null`.split[1].to_i || `tput cols 2>/dev/null`.to_i
    type_width   = term_width < 80 ? 30 : term_width - 50
    output_width = term_width < 80 ? 54 : term_width - 31

    # Header row
    puts set_color(sprintf(' %-28s %-*s %-18s ', 'Resource', type_width, 'Type', 'Status'), :white, :on_blue)
    resources = stack.list_resources
    resources.each do |r|
      puts sprintf(' %-28s %-*s %-18s ', r[:name][0..26], type_width, r[:type][0..type_width], colorize_status(r[:status]))
    end

    if s.stack_status.downcase =~ /complete$/
      # If the stack is complete display any available outputs and stop refreshing
      puts
      puts set_color(sprintf(' %-28s %-*s ', 'Output', output_width, 'Value'), :white, :on_yellow)
      s.outputs.each do |o|
        puts sprintf(' %-28s %-*s ', o.output_key, output_width, o.output_value)
      end

      puts "\nMore information: https://console.aws.amazon.com/cloudformation/home"
      break
    elsif s.stack_status.downcase =~ /failed$/
      # If the stack failed to create, stop refreshing
      puts "\nMore information: https://console.aws.amazon.com/cloudformation/home"
      break
    else
      puts "\nMore information: https://console.aws.amazon.com/cloudformation/home"
      countdown('Updating in', 9)
      puts
    end
  rescue Aws::CloudFormation::Errors::ValidationError
    say_fail('Stack not found')
  rescue Interrupt
    abort
  end while true
end

#updateObject



264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/hygroscope/cli.rb', line 264

def update
  # TODO: Right now update just does the same thing as create, not taking
  # into account the complications of updating (which params to keep,
  # whether to re-upload the payload, etc.)
  check_path
  validate

  # Prepare task takes care of shared logic between "create" and "update"
  template, paramset = prepare

  s = Hygroscope::Stack.new(options[:name], options[:region], options[:profile])
  s.parameters = paramset.parameters
  s.template = template.compress
  s.capabilities = ['CAPABILITY_IAM']
  s.timeout = 60
  s.update!

  status
end

#validateObject



389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
# File 'lib/hygroscope/cli.rb', line 389

def validate
  check_path

  begin
    t = Hygroscope::Template.new(template_path, options[:region], options[:profile])
    t.validate
  rescue Aws::CloudFormation::Errors::ValidationError => e
    say_fail("Validation error: #{e.message}")
  rescue Hygroscope::TemplateYamlParseError => e
    say_fail("YAML parsing error: #{e.message}")
  rescue => e
    say_fail(e.message)
  else
    say_status('ok', 'Template is valid', :green)
  end
end