sfn 3.0.30 → 3.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -0
  3. data/bin/sfn +16 -14
  4. data/lib/chef/knife/knife_plugin_seed.rb +12 -12
  5. data/lib/sfn.rb +17 -17
  6. data/lib/sfn/api_provider.rb +3 -3
  7. data/lib/sfn/api_provider/google.rb +2 -2
  8. data/lib/sfn/api_provider/terraform.rb +2 -2
  9. data/lib/sfn/cache.rb +9 -9
  10. data/lib/sfn/callback.rb +6 -6
  11. data/lib/sfn/callback/aws_assume_role.rb +5 -5
  12. data/lib/sfn/callback/aws_mfa.rb +8 -6
  13. data/lib/sfn/callback/stack_policy.rb +15 -15
  14. data/lib/sfn/command.rb +37 -36
  15. data/lib/sfn/command/conf.rb +12 -12
  16. data/lib/sfn/command/create.rb +9 -9
  17. data/lib/sfn/command/describe.rb +6 -6
  18. data/lib/sfn/command/destroy.rb +8 -8
  19. data/lib/sfn/command/diff.rb +31 -31
  20. data/lib/sfn/command/events.rb +6 -6
  21. data/lib/sfn/command/export.rb +8 -8
  22. data/lib/sfn/command/graph.rb +21 -21
  23. data/lib/sfn/command/graph/aws.rb +34 -34
  24. data/lib/sfn/command/graph/provider.rb +1 -1
  25. data/lib/sfn/command/graph/terraform.rb +41 -41
  26. data/lib/sfn/command/import.rb +17 -17
  27. data/lib/sfn/command/init.rb +15 -15
  28. data/lib/sfn/command/inspect.rb +16 -16
  29. data/lib/sfn/command/lint.rb +6 -6
  30. data/lib/sfn/command/list.rb +2 -2
  31. data/lib/sfn/command/plan.rb +227 -0
  32. data/lib/sfn/command/print.rb +4 -4
  33. data/lib/sfn/command/promote.rb +2 -2
  34. data/lib/sfn/command/update.rb +19 -144
  35. data/lib/sfn/command/validate.rb +17 -13
  36. data/lib/sfn/command_module.rb +6 -5
  37. data/lib/sfn/command_module/base.rb +8 -8
  38. data/lib/sfn/command_module/callbacks.rb +5 -5
  39. data/lib/sfn/command_module/planning.rb +151 -0
  40. data/lib/sfn/command_module/stack.rb +34 -34
  41. data/lib/sfn/command_module/template.rb +50 -50
  42. data/lib/sfn/config.rb +46 -44
  43. data/lib/sfn/config/conf.rb +3 -3
  44. data/lib/sfn/config/create.rb +9 -9
  45. data/lib/sfn/config/describe.rb +7 -7
  46. data/lib/sfn/config/destroy.rb +1 -1
  47. data/lib/sfn/config/diff.rb +3 -3
  48. data/lib/sfn/config/events.rb +9 -9
  49. data/lib/sfn/config/export.rb +5 -5
  50. data/lib/sfn/config/graph.rb +10 -10
  51. data/lib/sfn/config/import.rb +4 -4
  52. data/lib/sfn/config/init.rb +1 -1
  53. data/lib/sfn/config/inspect.rb +16 -16
  54. data/lib/sfn/config/lint.rb +5 -5
  55. data/lib/sfn/config/list.rb +6 -6
  56. data/lib/sfn/config/plan.rb +28 -0
  57. data/lib/sfn/config/print.rb +5 -5
  58. data/lib/sfn/config/promote.rb +4 -4
  59. data/lib/sfn/config/update.rb +18 -18
  60. data/lib/sfn/config/validate.rb +30 -30
  61. data/lib/sfn/lint.rb +5 -5
  62. data/lib/sfn/lint/definition.rb +3 -3
  63. data/lib/sfn/lint/rule.rb +3 -3
  64. data/lib/sfn/lint/rule_set.rb +2 -2
  65. data/lib/sfn/monkey_patch.rb +2 -2
  66. data/lib/sfn/monkey_patch/stack.rb +27 -27
  67. data/lib/sfn/monkey_patch/stack/azure.rb +1 -1
  68. data/lib/sfn/monkey_patch/stack/google.rb +5 -5
  69. data/lib/sfn/planner.rb +4 -4
  70. data/lib/sfn/planner/aws.rb +114 -70
  71. data/lib/sfn/provider.rb +13 -13
  72. data/lib/sfn/utils.rb +10 -10
  73. data/lib/sfn/utils/debug.rb +2 -2
  74. data/lib/sfn/utils/json.rb +1 -1
  75. data/lib/sfn/utils/object_storage.rb +3 -3
  76. data/lib/sfn/utils/output.rb +4 -4
  77. data/lib/sfn/utils/path_selector.rb +15 -15
  78. data/lib/sfn/utils/ssher.rb +4 -4
  79. data/lib/sfn/utils/stack_exporter.rb +16 -16
  80. data/lib/sfn/utils/stack_parameter_scrubber.rb +6 -6
  81. data/lib/sfn/utils/stack_parameter_validator.rb +22 -22
  82. data/lib/sfn/version.rb +1 -1
  83. data/sfn.gemspec +32 -32
  84. metadata +16 -13
@@ -1,5 +1,5 @@
1
- require 'stringio'
2
- require 'sfn'
1
+ require "stringio"
2
+ require "sfn"
3
3
 
4
4
  module Sfn
5
5
  class Command
@@ -12,27 +12,27 @@ module Sfn
12
12
 
13
13
  # Run the import action
14
14
  def execute!
15
- raise NotImplementedError.new 'Implementation updates required'
15
+ raise NotImplementedError.new "Implementation updates required"
16
16
  stack_name, json_file = name_args
17
- ui.info "#{ui.color('Stack Import:', :bold)} #{stack_name}"
17
+ ui.info "#{ui.color("Stack Import:", :bold)} #{stack_name}"
18
18
  unless json_file
19
19
  entries = [].tap do |_entries|
20
- _entries.push('s3') if config[:bucket]
21
- _entries.push('fs') if config[:path]
20
+ _entries.push("s3") if config[:bucket]
21
+ _entries.push("fs") if config[:path]
22
22
  end
23
23
  if entries.size > 1
24
24
  valid = false
25
25
  until valid
26
- answer = ui.ask_question('Import via file system (fs) or remote bucket (remote)?', :default => 'remote')
26
+ answer = ui.ask_question("Import via file system (fs) or remote bucket (remote)?", :default => "remote")
27
27
  valid = true if %w(remote fs).include?(answer)
28
28
  entries = [answer]
29
29
  end
30
30
  elsif entries.size < 1
31
- ui.fatal 'No path or bucket set. Unable to perform dynamic lookup!'
31
+ ui.fatal "No path or bucket set. Unable to perform dynamic lookup!"
32
32
  exit 1
33
33
  end
34
34
  case entries.first
35
- when 'remote'
35
+ when "remote"
36
36
  json_file = remote_discovery
37
37
  else
38
38
  json_file = local_discovery
@@ -49,9 +49,9 @@ module Sfn
49
49
  ),
50
50
  [stack_name]
51
51
  )
52
- ui.info ' - Starting creation of import'
52
+ ui.info " - Starting creation of import"
53
53
  creator.execute!
54
- ui.info "#{ui.color('Stack Import', :bold)} (#{json_file}): #{ui.color('complete', :green)}"
54
+ ui.info "#{ui.color("Stack Import", :bold)} (#{json_file}): #{ui.color("complete", :green)}"
55
55
  rescue => e
56
56
  ui.fatal "Failed to import stack: #{e}"
57
57
  debug "#{e.class}: #{e}\n#{e.backtrace.join("\n")}"
@@ -84,8 +84,8 @@ module Sfn
84
84
  directory = storage.directories.get(config[:bucket])
85
85
  file = prompt_for_file(
86
86
  directory,
87
- :directories_name => 'Collections',
88
- :files_names => 'Exports',
87
+ :directories_name => "Collections",
88
+ :files_names => "Exports",
89
89
  :filter_prefix => bucket_prefix,
90
90
  )
91
91
  if file
@@ -98,15 +98,15 @@ module Sfn
98
98
  #
99
99
  # @return [IO] stack export IO
100
100
  def local_discovery
101
- _, bucket = config[:path].split('/', 2)
101
+ _, bucket = config[:path].split("/", 2)
102
102
  storage = provider.service_for(:storage,
103
103
  :provider => :local,
104
- :local_root => '/')
104
+ :local_root => "/")
105
105
  directory = storage.directories.get(bucket)
106
106
  prompt_for_file(
107
107
  directory,
108
- :directories_name => 'Collections',
109
- :files_names => 'Exports',
108
+ :directories_name => "Collections",
109
+ :files_names => "Exports",
110
110
  )
111
111
  end
112
112
  end
@@ -1,5 +1,5 @@
1
- require 'sfn'
2
- require 'fileutils'
1
+ require "sfn"
2
+ require "fileutils"
3
3
 
4
4
  module Sfn
5
5
  class Command
@@ -8,15 +8,15 @@ module Sfn
8
8
  include Sfn::CommandModule::Base
9
9
 
10
10
  INIT_DIRECTORIES = [
11
- 'sparkleformation/dynamics',
12
- 'sparkleformation/components',
13
- 'sparkleformation/registry',
11
+ "sparkleformation/dynamics",
12
+ "sparkleformation/components",
13
+ "sparkleformation/registry",
14
14
  ]
15
15
 
16
16
  # Run the init command to initialize new project
17
17
  def execute!
18
18
  unless name_args.size == 1
19
- raise ArgumentError.new 'Please provide path argument only for project initialization'
19
+ raise ArgumentError.new "Please provide path argument only for project initialization"
20
20
  else
21
21
  path = name_args.first
22
22
  end
@@ -25,33 +25,33 @@ module Sfn
25
25
  end
26
26
  if File.directory?(path)
27
27
  ui.warn "Project directory already exists at given path. (`#{path}`)"
28
- ui.confirm 'Overwrite existing files?'
28
+ ui.confirm "Overwrite existing files?"
29
29
  end
30
- run_action 'Creating base project directories' do
30
+ run_action "Creating base project directories" do
31
31
  INIT_DIRECTORIES.each do |new_dir|
32
32
  FileUtils.mkdir_p(File.join(path, new_dir))
33
33
  end
34
34
  nil
35
35
  end
36
- run_action 'Creating project bundle' do
37
- File.open(File.join(path, 'Gemfile'), 'w') do |file|
36
+ run_action "Creating project bundle" do
37
+ File.open(File.join(path, "Gemfile"), "w") do |file|
38
38
  file.puts "source 'https://rubygems.org'\n\ngem 'sfn'"
39
39
  end
40
40
  nil
41
41
  end
42
- ui.info 'Generating .sfn configuration file'
42
+ ui.info "Generating .sfn configuration file"
43
43
  Dir.chdir(path) do
44
44
  Conf.new({:generate => true}, []).execute!
45
45
  end
46
- ui.info 'Installing project bundle'
46
+ ui.info "Installing project bundle"
47
47
  Dir.chdir(path) do
48
48
  if defined?(Bundler)
49
- Bundler.clean_system('bundle install')
49
+ Bundler.clean_system("bundle install")
50
50
  else
51
- system('bundle install')
51
+ system("bundle install")
52
52
  end
53
53
  end
54
- ui.info 'Project initialization complete!'
54
+ ui.info "Project initialization complete!"
55
55
  ui.puts " Project path -> #{File.expand_path(path)}"
56
56
  end
57
57
  end
@@ -1,4 +1,4 @@
1
- require 'sfn'
1
+ require "sfn"
2
2
 
3
3
  module Sfn
4
4
  class Command
@@ -22,7 +22,7 @@ module Sfn
22
22
  end.compact
23
23
  end
24
24
  if outputs.empty?
25
- ui.info ' Stack dump:'
25
+ ui.info " Stack dump:"
26
26
  ui.puts MultiJson.dump(
27
27
  MultiJson.load(
28
28
  stack.reload.to_json
@@ -34,25 +34,25 @@ module Sfn
34
34
 
35
35
  def display_instance_failure(stack)
36
36
  instances = stack.resources.all.find_all do |resource|
37
- resource.state.to_s.end_with?('failed')
37
+ resource.state.to_s.end_with?("failed")
38
38
  end.map do |resource|
39
39
  # If compute instance, simply expand
40
40
  if resource.within?(:compute, :servers)
41
41
  resource.instance
42
42
  # If a waitcondition, check for instance ID
43
- elsif resource.type.to_s.downcase.end_with?('waitcondition')
44
- if resource.status_reason.to_s.include?('uniqueId')
45
- srv_id = resource.status_reason.split(' ').last.strip
43
+ elsif resource.type.to_s.downcase.end_with?("waitcondition")
44
+ if resource.status_reason.to_s.include?("uniqueId")
45
+ srv_id = resource.status_reason.split(" ").last.strip
46
46
  provider.connection.api_for(:compute).servers.get(srv_id)
47
47
  end
48
48
  end
49
49
  end.compact
50
50
  if instances.empty?
51
- ui.error 'Failed to locate any failed instances'
51
+ ui.error "Failed to locate any failed instances"
52
52
  else
53
53
  log_path = config[:failure_log_path]
54
54
  if log_path.to_s.empty?
55
- log_path = '/var/log/chef/client.log'
55
+ log_path = "/var/log/chef/client.log"
56
56
  end
57
57
  opts = ssh_key ? {:keys => [ssh_key]} : {}
58
58
  instances.each do |instance|
@@ -84,7 +84,7 @@ module Sfn
84
84
  #
85
85
  # @return [Array<String>] usernames for ssh connect attempt
86
86
  def ssh_attempt_users
87
- [config[:ssh_user], config[:ssh_attempt_users], ENV['USER']].flatten.compact.uniq
87
+ [config[:ssh_user], config[:ssh_attempt_users], ENV["USER"]].flatten.compact.uniq
88
88
  end
89
89
 
90
90
  def ssh_key
@@ -93,14 +93,14 @@ module Sfn
93
93
 
94
94
  def display_attribute(stack)
95
95
  [config[:attribute]].flatten.compact.each do |stack_attribute|
96
- attr = stack_attribute.split('.').inject(stack) do |memo, key|
96
+ attr = stack_attribute.split(".").inject(stack) do |memo, key|
97
97
  args = key.scan(/\(([^\)]*)\)/).flatten.first.to_s
98
98
  if args
99
- args = args.split(',').map { |a| a.to_i.to_s == a ? a.to_i : a }
100
- key = key.split('(').first
99
+ args = args.split(",").map { |a| a.to_i.to_s == a ? a.to_i : a }
100
+ key = key.split("(").first
101
101
  end
102
102
  if memo.public_methods.include?(key.to_sym)
103
- if args.size == 1 && args.first.to_s.start_with?('&')
103
+ if args.size == 1 && args.first.to_s.start_with?("&")
104
104
  memo.send(key, &args.first.slice(2, args.first.size).to_sym)
105
105
  else
106
106
  memo.send(*[key, args].flatten.compact)
@@ -152,11 +152,11 @@ module Sfn
152
152
  end.compact
153
153
  ]
154
154
  unless asg_nodes.empty?
155
- ui.info ' AutoScale Group Instances:'
155
+ ui.info " AutoScale Group Instances:"
156
156
  ui.puts MultiJson.dump(asg_nodes, :pretty => true)
157
157
  end
158
158
  unless compute_nodes.empty?
159
- ui.info ' Compute Instances:'
159
+ ui.info " Compute Instances:"
160
160
  ui.puts MultiJson.dump(compute_nodes, :pretty => true)
161
161
  end
162
162
  end
@@ -179,7 +179,7 @@ module Sfn
179
179
  end
180
180
  ]
181
181
  unless load_balancers.empty?
182
- ui.info ' Load Balancer Instances:'
182
+ ui.info " Load Balancer Instances:"
183
183
  ui.puts MultiJson.dump(load_balancers, :pretty => true)
184
184
  end
185
185
  end
@@ -1,4 +1,4 @@
1
- require 'sfn'
1
+ require "sfn"
2
2
 
3
3
  module Sfn
4
4
  class Command
@@ -12,7 +12,7 @@ module Sfn
12
12
  print_only_original = config[:print_only]
13
13
  config[:print_only] = true
14
14
  file = load_template_file
15
- ui.info "#{ui.color("Template Linting (#{provider.connection.provider}): ", :bold)} #{config[:file].sub(Dir.pwd, '').sub(%r{^/}, '')}"
15
+ ui.info "#{ui.color("Template Linting (#{provider.connection.provider}): ", :bold)} #{config[:file].sub(Dir.pwd, "").sub(%r{^/}, "")}"
16
16
  config[:print_only] = print_only_original
17
17
 
18
18
  raw_template = parameter_scrub!(template_content(file))
@@ -22,16 +22,16 @@ module Sfn
22
22
  else
23
23
  result = lint_template(raw_template)
24
24
  if result == true
25
- ui.info ui.color(' -> VALID', :green, :bold)
25
+ ui.info ui.color(" -> VALID", :green, :bold)
26
26
  else
27
- ui.info ui.color(' -> INVALID', :red, :bold)
27
+ ui.info ui.color(" -> INVALID", :red, :bold)
28
28
  result.each do |failure|
29
29
  ui.error "Result Set: #{ui.color(failure[:rule_set].name, :red, :bold)}"
30
30
  failure[:failures].each do |f_msg|
31
31
  ui.fatal f_msg
32
32
  end
33
33
  end
34
- raise 'Linting failure'
34
+ raise "Linting failure"
35
35
  end
36
36
  end
37
37
  end
@@ -54,7 +54,7 @@ module Sfn
54
54
  def rule_sets
55
55
  sets = [config[:lint_directory]].flatten.compact.map do |directory|
56
56
  if File.directory?(directory)
57
- files = Dir.glob(File.join(directory, '**', '**', '*.rb'))
57
+ files = Dir.glob(File.join(directory, "**", "**", "*.rb"))
58
58
  files.map do |path|
59
59
  begin
60
60
  Sfn::Lint.class_eval(
@@ -1,4 +1,4 @@
1
- require 'sfn'
1
+ require "sfn"
2
2
 
3
3
  module Sfn
4
4
  class Command
@@ -15,7 +15,7 @@ module Sfn
15
15
  allowed_attributes.each do |attr|
16
16
  width_val = stacks.map { |e| e[attr].to_s.length }.push(attr.length).max + 2
17
17
  width_val = width_val > 70 ? 70 : width_val < 20 ? 20 : width_val
18
- column attr.split('_').map(&:capitalize).join(' '), :width => width_val
18
+ column attr.split("_").map(&:capitalize).join(" "), :width => width_val
19
19
  end
20
20
  end
21
21
  get_stacks.each do |stack|
@@ -0,0 +1,227 @@
1
+ require "sfn"
2
+
3
+ module Sfn
4
+ class Command
5
+ # Plan command
6
+ class Plan < Command
7
+ include Sfn::CommandModule::Base
8
+ include Sfn::CommandModule::Planning
9
+ include Sfn::CommandModule::Stack
10
+ include Sfn::CommandModule::Template
11
+
12
+ # Run the stack planning command
13
+ def execute!
14
+ name_required!
15
+ name = name_args.first
16
+
17
+ stack_info = "#{ui.color("Name:", :bold)} #{name}"
18
+ begin
19
+ stack = provider.stacks.get(name)
20
+ rescue Miasma::Error::ApiError::RequestError
21
+ stack = provider.stacks.build(name: name)
22
+ end
23
+
24
+ return display_plan_lists(stack) if config[:list]
25
+
26
+ if config[:plan_name]
27
+ # ensure custom attribute is dirty so we can modify
28
+ stack.custom = stack.custom.dup
29
+ stack.custom[:plan_name] = config[:plan_name]
30
+ end
31
+
32
+ use_existing = false
33
+
34
+ unless config[:print_only]
35
+ ui.info "#{ui.color("SparkleFormation:", :bold)} #{ui.color("plan", :green)}"
36
+ if stack && stack.plan
37
+ ui.warn "Found existing plan for this stack"
38
+ begin
39
+ ui.confirm "Destroy existing plan?"
40
+ ui.info "Destroying existing plan to generate new plan"
41
+ stack.plan.destroy
42
+ rescue Bogo::Ui::ConfirmationDeclined
43
+ ui.info "Loading existing stack plan for #{ui.color(stack.name, :bold)}..."
44
+ use_existing = true
45
+ end
46
+ end
47
+ end
48
+
49
+ unless use_existing
50
+ config[:compile_parameters] ||= Smash.new
51
+
52
+ if config[:file]
53
+ s_name = [name]
54
+
55
+ c_setter = lambda do |c_stack|
56
+ if c_stack.outputs
57
+ compile_params = c_stack.outputs.detect do |output|
58
+ output.key == "CompileState"
59
+ end
60
+ end
61
+ if compile_params
62
+ compile_params = MultiJson.load(compile_params.value)
63
+ c_current = config[:compile_parameters].fetch(s_name.join("__"), Smash.new)
64
+ config[:compile_parameters][s_name.join("__")] = compile_params.merge(c_current)
65
+ end
66
+ c_stack.nested_stacks(false).each do |n_stack|
67
+ s_name.push(n_stack.data.fetch(:logical_id, n_stack.name))
68
+ c_setter.call(n_stack)
69
+ s_name.pop
70
+ end
71
+ end
72
+
73
+ if stack && stack.persisted?
74
+ c_setter.call(stack)
75
+ end
76
+
77
+ ui.debug "Compile parameters - #{config[:compile_parameters]}"
78
+ file = load_template_file(:stack => stack)
79
+ stack_info << " #{ui.color("Path:", :bold)} #{config[:file]}"
80
+ else
81
+ file = stack.template.dup
82
+ end
83
+
84
+ unless file
85
+ if config[:template]
86
+ file = config[:template]
87
+ stack_info << " #{ui.color("(template provided)", :green)}"
88
+ else
89
+ stack_info << " #{ui.color("(no template update)", :yellow)}"
90
+ end
91
+ end
92
+ unless config[:print_only]
93
+ ui.info " -> #{stack_info}"
94
+ end
95
+ if file
96
+ if config[:print_only]
97
+ ui.puts format_json(parameter_scrub!(template_content(file)))
98
+ return
99
+ end
100
+
101
+ original_parameters = stack.parameters
102
+
103
+ apply_stacks!(stack)
104
+
105
+ populate_parameters!(file, :current_parameters => stack.root_parameters)
106
+
107
+ stack.parameters = config_root_parameters
108
+
109
+ if config[:upload_root_template]
110
+ upload_result = store_template(name, file, Smash.new)
111
+ stack.template_url = upload_result[:url]
112
+ else
113
+ stack.template = parameter_scrub!(template_content(file, :scrub))
114
+ end
115
+ else
116
+ apply_stacks!(stack)
117
+ original_parameters = stack.parameters
118
+ populate_parameters!(stack.template, :current_parameters => stack.root_parameters)
119
+ stack.parameters = config_root_parameters
120
+ end
121
+
122
+ # Set options defined within config into stack instance for update request
123
+
124
+ ui.info " -> Generating plan information..."
125
+ else
126
+ ui.info " -> Loading plan information..."
127
+ end
128
+
129
+ plan = stack.plan || stack.plan_generate
130
+
131
+ begin
132
+ display_plan_information(plan)
133
+ rescue Bogo::Ui::ConfirmationDeclined
134
+ stack.reload
135
+ if (stack.template.nil? || stack.template.empty?) && stack.state == :unknown
136
+ ui.auto_confirm = false
137
+ ui.warn "Stack appears to be empty and should be destroyed"
138
+ ui.confirm "Destroy stack?"
139
+ stack.destroy
140
+ poll_stack(stack.name)
141
+ else
142
+ ui.confirm "Destroy generated plan?"
143
+ plan.destroy
144
+ end
145
+ raise
146
+ end
147
+
148
+ if config[:merge_api_options]
149
+ config.fetch(:options, Smash.new).each_pair do |key, value|
150
+ if stack.respond_to?("#{key}=")
151
+ stack.send("#{key}=", value)
152
+ end
153
+ end
154
+ end
155
+
156
+ begin
157
+ api_action!(:api_stack => stack) do
158
+ stack.plan_execute
159
+ if config[:poll]
160
+ poll_stack(stack.name)
161
+ if stack.reload.state == :update_complete || stack.reload.state == :create_complete
162
+ ui.info "Stack plan apply complete: #{ui.color("SUCCESS", :green)}"
163
+ namespace.const_get(:Describe).new({:outputs => true}, [name]).execute!
164
+ else
165
+ ui.fatal "Update of stack #{ui.color(name, :bold)}: #{ui.color("FAILED", :red, :bold)}"
166
+ raise "Stack did not reach a successful completion state."
167
+ end
168
+ else
169
+ ui.warn "Stack state polling has been disabled."
170
+ ui.info "Stack plan apply initialized for #{ui.color(name, :green)}"
171
+ end
172
+ end
173
+ rescue Miasma::Error::ApiError::RequestError => e
174
+ if e.message.downcase.include?("no updates")
175
+ ui.warn "No changes detected for stack (#{stack.name})"
176
+ else
177
+ raise
178
+ end
179
+ end
180
+ end
181
+
182
+ # Display plan list in table form
183
+ #
184
+ # @param [Miasma::Models::Orchestration::Stack]
185
+ def display_plan_lists(stack)
186
+ unless stack
187
+ raise "Failed to locate requested stack `#{name_args.first}`"
188
+ end
189
+ plans = stack.plans.all
190
+ if plans.empty?
191
+ ui.warn "No plans found for stack `#{stack.name}`"
192
+ return
193
+ end
194
+ ui.info "Plans for stack: #{ui.color(stack.name, :bold)}\n"
195
+ n_width = "Plan Name".length
196
+ i_width = "Plan ID".length
197
+ s_width = "Plan State".length
198
+ c_width = "Created".length
199
+ plan_info = plans.map do |plan|
200
+ plan_id = plan.id.to_s.split("/").last
201
+ n_width = plan.name.to_s.length if plan.name.to_s.length > n_width
202
+ i_width = plan_id.to_s.length if plan_id.length > i_width
203
+ s_width = plan.state.to_s.length if plan.state.to_s.length > s_width
204
+ c_width = plan.created_at.to_s.length if plan.created_at.to_s.length > c_width
205
+ [plan.name, plan_id, plan.state, plan.created_at]
206
+ end
207
+ table = ui.table(self) do
208
+ table(:border => false) do
209
+ row(:header => true) do
210
+ column "Plan Name", :width => n_width + 5
211
+ column "Plan ID", :width => i_width + 5
212
+ column "Plan State", :width => s_width + 5
213
+ column "Created", :width => c_width + 5
214
+ end
215
+ plan_info.sort_by(&:first).each do |plan|
216
+ row do
217
+ plan.each do |item|
218
+ column item
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end.display
224
+ end
225
+ end
226
+ end
227
+ end