Class: Stax::Stack

Inherits:
Base
  • Object
show all
Defined in:
lib/stax/stack.rb,
lib/stax/cli/info.rb,
lib/stax/stack/cfn.rb,
lib/stax/stack/crud.rb,
lib/stax/stack/drift.rb,
lib/stax/stack/exports.rb,
lib/stax/stack/outputs.rb,
lib/stax/stack/template.rb,
lib/stax/stack/changeset.rb,
lib/stax/stack/resources.rb,
lib/stax/stack/parameters.rb,
lib/stax/generators/new/templates/lib/stack.rb,
lib/stax/cfer.rb

Constant Summary collapse

COLORS =
{
  IN_SYNC:  :green,
  MODIFIED: :red,
  DELETED:  :red,
  ADD:      :green,
  REMOVE:   :red,
}

Instance Method Summary collapse

Instance Method Details

#cancelObject



224
225
226
227
228
229
230
# File 'lib/stax/stack/crud.rb', line 224

def cancel
  debug("Cancelling update for #{stack_name}")
  Aws::Cfn.cancel(stack_name)
  tail
rescue ::Aws::CloudFormation::Errors::ValidationError => e
  fail_task(e.message)
end

#changeObject



104
105
106
107
108
109
110
111
112
# File 'lib/stax/stack/changeset.rb', line 104

def change
  id = change_set_update
  change_set_complete?(id) || fail_task(change_set_reason(id))
  change_set_changes(id)
  change_set_unlock
  change_set_execute(id) && tail && update_warn_imports
ensure
  change_set_lock
end

#continueObject



239
240
241
242
243
244
245
246
247
# File 'lib/stax/stack/crud.rb', line 239

def continue
  Aws::Cfn.client.continue_update_rollback(
    stack_name: stack_name,
    resources_to_skip: options[:skip],
  )
  tail
rescue ::Aws::CloudFormation::Errors::ValidationError => e
  fail_task(e.message)
end

#createObject



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
# File 'lib/stax/stack/crud.rb', line 164

def create
  debug("Creating stack #{stack_name}")

  ## ensure stacks we import exist
  ensure_stack(*stack_imports)

  ## create the stack
  Aws::Cfn.create(
    stack_name: stack_name,
    template_body: cfn_template_body,
    template_url: cfn_template_url,
    parameters: cfn_parameters_create,
    capabilities: cfn_capabilities,
    stack_policy_body: stack_policy,
    notification_arns: cfn_notification_arns,
    enable_termination_protection: cfn_termination_protection,
    tags: cfn_tags_array,
  )

  ## show stack events
  tail
rescue ::Aws::CloudFormation::Errors::AlreadyExistsException => e
  fail_task(e.message)
rescue ::Aws::CloudFormation::Errors::ValidationError => e
  warn(e.message)
end

#deleteObject



213
214
215
216
217
218
219
220
221
# File 'lib/stax/stack/crud.rb', line 213

def delete
  delete_warn_imports
  if yes? "Really delete stack #{stack_name}?", :yellow
    Aws::Cfn.delete(stack_name)
    tail unless options[:notail]
  end
rescue ::Aws::CloudFormation::Errors::ValidationError => e
  fail_task(e.message)
end

#driftsObject



49
50
51
52
53
# File 'lib/stax/stack/drift.rb', line 49

def drifts
  run_drift_detection
  drifts = show_drifts
  show_drifts_details(drifts)
end

#eventsObject



34
35
36
37
38
# File 'lib/stax/stack/cfn.rb', line 34

def events
  print_events(Aws::Cfn.events(stack_name)[0..options[:number]-1])
rescue ::Aws::CloudFormation::Errors::ValidationError => e
  puts e.message
end

#existsObject



47
48
49
# File 'lib/stax/stack.rb', line 47

def exists
  puts exists?
end

#exportsObject



25
26
27
28
29
30
31
# File 'lib/stax/stack/exports.rb', line 25

def exports
  debug("Stacks that import from #{stack_name}")
  print_table Aws::Cfn.exports(stack_name).map { |e|
    imports = (i = Aws::Cfn.imports(e.export_name)).empty? ? '-' : i.join('  ')
    [e.output_key, imports]
  }.sort
end

#generateObject



233
234
235
# File 'lib/stax/stack/crud.rb', line 233

def generate
  puts cfn_template
end

#id(resource) ⇒ Object



30
31
32
# File 'lib/stax/stack/resources.rb', line 30

def id(resource)
  puts Aws::Cfn.id(stack_name, resource)
end

#importObject



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/stax/stack/changeset.rb', line 119

def import
  ## prompt for missing options
  %i[type id key value].each do |opt|
    options[opt] ||= ask("Resource #{opt}?", :yellow)
  end

  ## create import changeset
  debug("Creating import change set for #{stack_name}")
  id = change_set_import(
    resource_type: options[:type],
    logical_resource_id: options[:id],
    resource_identifier: {
      options[:key] => options[:value]
    }
  )

  ## wait for changeset, prompt for changes, and execute
  change_set_complete?(id) || fail_task(change_set_reason(id))
  change_set_changes(id)
  change_set_execute(id) && tail && update_warn_imports
end

#importsObject



34
35
36
37
# File 'lib/stax/stack/exports.rb', line 34

def imports
  warn("deprecated method: please use 'exports' instead")
  exports
end

#infoObject



11
12
13
14
15
16
17
18
19
20
# File 'lib/stax/cli/info.rb', line 11

def info
  ## get mixins in the order we declared them
  self.class.subcommands.reverse.each do |cmd|
    begin
      invoke cmd, [:info]
    rescue Thor::UndefinedCommandError => e
      # no info no problem
    end
  end
end

#lintObject



290
# File 'lib/stax/stack/crud.rb', line 290

desc 'lint', 'run cfn-lint on template'

#outputs(key = nil) ⇒ Object



15
16
17
18
19
20
21
22
23
# File 'lib/stax/stack/outputs.rb', line 15

def outputs(key = nil)
  if key
    puts stack_output(key)
  else
    print_table Aws::Cfn.describe(stack_name).outputs.map { |o|
      [o.output_key, o.output_value, o.description, o.export_name]
    }.sort
  end
end

#parametersObject



17
18
19
20
21
# File 'lib/stax/stack/parameters.rb', line 17

def parameters
  print_table stack_parameters.each_with_object({}) { |p, h|
    h[p.parameter_key] = p.parameter_value
  }.sort
end

#policy(json = nil) ⇒ Object



263
264
265
266
267
268
269
# File 'lib/stax/stack/crud.rb', line 263

def policy(json = nil)
  if json
    Aws::Cfn.set_policy(stack_name: stack_name, stack_policy_body: json)
  else
    puts Aws::Cfn.get_policy(stack_name: stack_name)
  end
end

#protectionObject



252
253
254
255
256
257
258
259
260
# File 'lib/stax/stack/crud.rb', line 252

def protection
  if options[:enable]
    Aws::Cfn.protection(stack_name, true)
  elsif options[:disable]
    Aws::Cfn.protection(stack_name, false)
  end
  debug("Termination protection for #{stack_name}")
  puts Aws::Cfn.describe(stack_name)&.enable_termination_protection
end

#resourcesObject



18
19
20
21
22
23
24
25
26
27
# File 'lib/stax/stack/resources.rb', line 18

def resources
  print_table stack_resources.tap { |resources|
    if options[:match]
      m = Regexp.new(options[:match], Regexp::IGNORECASE)
      resources.select! { |r| m.match(r.resource_type) }
    end
  }.map { |r|
    [r.logical_resource_id, r.resource_type, color(r.resource_status, Aws::Cfn::COLORS), r.physical_resource_id]
  }
end

#skeletonObject



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/stax/stack/crud.rb', line 273

def skeleton
  skel = {
    StackName: stack_name,
    TemplateBody: cfn_template_body,
    TemplateURL: cfn_template_url,
    Parameters: cfn_parameters_create,
    Capabilities: cfn_capabilities,
    StackPolicyBody: stack_policy,
    NotificationARNs: cfn_notification_arns,
    EnableTerminationProtection: cfn_termination_protection,
    Tags: cfn_tags_array,
  }.compact
  method = options[:pretty] ? :pretty_generate : :generate
  puts JSON.send(method, skel)
end

#tagsObject



73
74
75
76
77
# File 'lib/stax/stack/cfn.rb', line 73

def tags
  print_table Aws::Cfn.describe(stack_name).tags.map { |t|
    [ t.key, t.value ]
  }
end

#tailObject



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/stax/stack/cfn.rb', line 42

def tail
  trap('SIGINT', 'EXIT')    # clean exit with ctrl-c

  ## print some historical events
  events = Aws::Cfn.events(stack_name).first(options[:number] || 1)
  return unless events
  print_events(events)
  last_seen = events&.first&.event_id

  loop do
    sleep(1)
    events = []

    Aws::Cfn.events(stack_name).each do |e|
      (last_seen == e.event_id) ? break : events << e
    end

    unless events.empty?
      print_events(events)
      last_seen = events.first.event_id
    end

    ## get stack status and break if stack gone, or delete complete/failed
    s = Aws::Cfn.describe(stack_name)
    break if s.nil? || s.stack_status.end_with?('COMPLETE', 'FAILED')
  end
rescue ::Aws::CloudFormation::Errors::ValidationError => e
  puts e.message
end

#templateObject



20
21
22
23
24
25
26
27
28
29
30
# File 'lib/stax/stack/cfn.rb', line 20

def template
  body = Aws::Cfn.template(stack_name)
  if options[:pretty]
    begin
      body = JSON.pretty_generate(JSON.parse(body))
    rescue JSON::ParserError
      ## not valid json, may be yaml
    end
  end
  puts body
end

#updateObject



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/stax/stack/crud.rb', line 192

def update
  return change if stack_force_changeset
  debug("Updating stack #{stack_name}")
  Aws::Cfn.update(
    stack_name: stack_name,
    template_body: cfn_template_body,
    template_url: cfn_template_url,
    parameters: cfn_parameters_update,
    capabilities: cfn_capabilities,
    stack_policy_during_update_body: stack_policy_during_update,
    notification_arns: cfn_notification_arns,
    tags: cfn_tags_array,
  )
  tail
  update_warn_imports
rescue ::Aws::CloudFormation::Errors::ValidationError => e
  warn(e.message)
end

#validateObject



154
155
156
157
158
159
160
161
# File 'lib/stax/stack/crud.rb', line 154

def validate
  Aws::Cfn.validate(
    template_body: cfn_template_body,
    template_url:  cfn_template_url,
  )
rescue ::Aws::CloudFormation::Errors::ValidationError => e
  fail_task(e.message)
end