sfn 3.0.30 → 3.0.32

Sign up to get free protection for your applications and to get access to all the features.
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