sfn 0.5.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -8
- data/README.md +85 -53
- data/bin/generate_sfn_docs +37 -0
- data/lib/sfn/callback/stack_policy.rb +94 -0
- data/lib/sfn/callback.rb +48 -0
- data/lib/sfn/command/create.rb +19 -20
- data/lib/sfn/command/describe.rb +16 -10
- data/lib/sfn/command/destroy.rb +8 -3
- data/lib/sfn/command/events.rb +24 -22
- data/lib/sfn/command/inspect.rb +8 -6
- data/lib/sfn/command/update.rb +49 -72
- data/lib/sfn/command/validate.rb +30 -4
- data/lib/sfn/command.rb +8 -5
- data/lib/sfn/command_module/base.rb +17 -2
- data/lib/sfn/command_module/callbacks.rb +69 -0
- data/lib/sfn/command_module/stack.rb +107 -9
- data/lib/sfn/command_module/template.rb +201 -129
- data/lib/sfn/command_module.rb +1 -0
- data/lib/sfn/config/update.rb +0 -4
- data/lib/sfn/config/validate.rb +12 -2
- data/lib/sfn/monkey_patch/stack.rb +72 -0
- data/lib/sfn/utils/path_selector.rb +3 -0
- data/lib/sfn/version.rb +1 -1
- data/lib/sfn.rb +1 -0
- data/sfn.gemspec +8 -10
- metadata +19 -22
data/lib/sfn/command/update.rb
CHANGED
@@ -18,100 +18,77 @@ module Sfn
|
|
18
18
|
end
|
19
19
|
|
20
20
|
stack_info = "#{ui.color('Name:', :bold)} #{name}"
|
21
|
-
|
22
|
-
|
23
|
-
|
21
|
+
begin
|
22
|
+
stack = provider.connection.stacks.get(name)
|
23
|
+
rescue Miasma::Error::ApiError::RequestError
|
24
|
+
stack = nil
|
25
|
+
end
|
24
26
|
|
25
27
|
if(config[:file])
|
26
|
-
|
27
|
-
c_setter = lambda do |c_stack|
|
28
|
-
compile_params = c_stack.outputs.detect do |output|
|
29
|
-
output.key == 'CompileState'
|
30
|
-
end
|
31
|
-
if(compile_params)
|
32
|
-
compile_params = MultiJson.load(compile_params.value)
|
33
|
-
c_current = config[:compile_parameters].fetch(s_name.join('_'), Smash.new)
|
34
|
-
config[:compile_parameters][s_name.join('_')] = compile_params.merge(c_current)
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
if(stack)
|
39
|
-
c_setter.call(stack)
|
40
|
-
stack.resources.all.each do |s_resource|
|
41
|
-
if(s_resource.type == 'AWS::CloudFormation::Stack')
|
42
|
-
s_name.push(s_resource.logical_id)
|
43
|
-
c_setter.call(s_resource.expand)
|
44
|
-
s_name.pop
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
file = load_template_file
|
28
|
+
file = load_template_file(:stack => stack)
|
50
29
|
stack_info << " #{ui.color('Path:', :bold)} #{config[:file]}"
|
51
30
|
nested_stacks = file.delete('sfn_nested_stack')
|
52
31
|
end
|
53
32
|
|
54
33
|
if(nested_stacks)
|
55
|
-
|
56
34
|
unpack_nesting(name, file, :update)
|
57
|
-
|
58
35
|
else
|
59
|
-
stack
|
36
|
+
unless(stack)
|
37
|
+
ui.fatal "Failed to locate requested stack: #{ui.color(name, :red, :bold)}"
|
38
|
+
raise "Failed to locate stack: #{name}"
|
39
|
+
end
|
60
40
|
|
61
|
-
|
62
|
-
ui.info "#{ui.color('SparkleFormation:', :bold)} #{ui.color('update', :green)}"
|
41
|
+
ui.info "#{ui.color('Cloud Formation:', :bold)} #{ui.color('update', :green)}"
|
63
42
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
end
|
43
|
+
unless(file)
|
44
|
+
if(config[:template])
|
45
|
+
file = config[:template]
|
46
|
+
stack_info << " #{ui.color('(template provided)', :green)}"
|
47
|
+
else
|
48
|
+
stack_info << " #{ui.color('(no template update)', :yellow)}"
|
71
49
|
end
|
72
|
-
|
50
|
+
end
|
51
|
+
ui.info " -> #{stack_info}"
|
73
52
|
|
53
|
+
apply_stacks!(stack)
|
74
54
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
stack.parameters = config[:parameters]
|
80
|
-
stack.template = Sfn::Utils::StackParameterScrubber.scrub!(stack.template)
|
81
|
-
else
|
82
|
-
apply_stacks!(stack)
|
83
|
-
populate_parameters!(stack.template, stack.parameters)
|
84
|
-
stack.parameters = config[:parameters]
|
55
|
+
if(file)
|
56
|
+
if(config[:print_only])
|
57
|
+
ui.puts _format_json(translate_template(file))
|
58
|
+
return
|
85
59
|
end
|
60
|
+
populate_parameters!(file, :current_parameters => stack.parameters)
|
61
|
+
stack.template = translate_template(file)
|
62
|
+
stack.parameters = config_root_parameters
|
63
|
+
stack.template = Sfn::Utils::StackParameterScrubber.scrub!(stack.template)
|
64
|
+
else
|
65
|
+
populate_parameters!(stack.template, :current_parameters => stack.parameters)
|
66
|
+
stack.parameters = config_root_parameters
|
67
|
+
end
|
86
68
|
|
87
|
-
|
69
|
+
begin
|
70
|
+
api_action!(:api_stack => stack) do
|
88
71
|
stack.save
|
89
|
-
|
90
|
-
|
91
|
-
|
72
|
+
if(config[:poll])
|
73
|
+
poll_stack(stack.name)
|
74
|
+
if(stack.reload.state == :update_complete)
|
75
|
+
ui.info "Stack update complete: #{ui.color('SUCCESS', :green)}"
|
76
|
+
namespace.const_get(:Describe).new({:outputs => true}, [name]).execute!
|
77
|
+
else
|
78
|
+
ui.fatal "Update of stack #{ui.color(name, :bold)}: #{ui.color('FAILED', :red, :bold)}"
|
79
|
+
raise
|
80
|
+
end
|
92
81
|
else
|
93
|
-
|
82
|
+
ui.warn 'Stack state polling has been disabled.'
|
83
|
+
ui.info "Stack update initialized for #{ui.color(name, :green)}"
|
94
84
|
end
|
95
85
|
end
|
96
|
-
|
97
|
-
if(
|
98
|
-
|
99
|
-
if(stack.reload.state == :update_complete)
|
100
|
-
ui.info "Stack update complete: #{ui.color('SUCCESS', :green)}"
|
101
|
-
namespace.const_get(:Describe).new({:outputs => true}, [name]).execute!
|
102
|
-
else
|
103
|
-
ui.fatal "Update of stack #{ui.color(name, :bold)}: #{ui.color('FAILED', :red, :bold)}"
|
104
|
-
ui.info ""
|
105
|
-
namespace.const_get(:Inspect).new({:instance_failure => true}, [name]).execute!
|
106
|
-
raise
|
107
|
-
end
|
86
|
+
rescue Miasma::Error::ApiError::RequestError => e
|
87
|
+
if(e.message.downcase.include?('no updates'))
|
88
|
+
ui.warn "No updates detected for stack (#{stack.name})"
|
108
89
|
else
|
109
|
-
|
110
|
-
ui.info "Stack update initialized for #{ui.color(name, :green)}"
|
90
|
+
raise
|
111
91
|
end
|
112
|
-
else
|
113
|
-
ui.fatal "Failed to locate requested stack: #{ui.color(name, :red, :bold)}"
|
114
|
-
raise
|
115
92
|
end
|
116
93
|
|
117
94
|
end
|
data/lib/sfn/command/validate.rb
CHANGED
@@ -8,23 +8,49 @@ module Sfn
|
|
8
8
|
|
9
9
|
include Sfn::CommandModule::Base
|
10
10
|
include Sfn::CommandModule::Template
|
11
|
+
include Sfn::CommandModule::Stack
|
11
12
|
|
12
13
|
def execute!
|
14
|
+
print_only_original = config[:print_only]
|
15
|
+
config[:print_only] = true
|
13
16
|
file = load_template_file
|
14
17
|
file.delete('sfn_nested_stack')
|
15
18
|
ui.info "#{ui.color("Template Validation (#{provider.connection.provider}): ", :bold)} #{config[:file].sub(Dir.pwd, '').sub(%r{^/}, '')}"
|
16
19
|
file = Sfn::Utils::StackParameterScrubber.scrub!(file)
|
17
20
|
file = translate_template(file)
|
21
|
+
config[:print_only] = print_only_original
|
22
|
+
|
23
|
+
if(config[:print_only])
|
24
|
+
ui.puts _format_json(file)
|
25
|
+
else
|
26
|
+
validate_stack(file, sparkle_collection.get(:template, config[:file])[:name])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def validate_stack(stack, name)
|
31
|
+
resources = stack.fetch('Resources', {})
|
32
|
+
nested_stacks = resources.find_all do |r_name, r_value|
|
33
|
+
r_value.is_a?(Hash) &&
|
34
|
+
provider.connection.class.const_get(:RESOURCE_MAPPING).fetch(r_value['Type'], {})[:api] == :orchestration
|
35
|
+
end
|
36
|
+
nested_stacks.each do |n_name, n_resource|
|
37
|
+
validate_stack(n_resource.fetch('Properties', {}).fetch('Stack', {}), "#{name} > #{n_name}")
|
38
|
+
n_resource['Properties'].delete('Stack')
|
39
|
+
end
|
18
40
|
begin
|
19
|
-
|
41
|
+
ui.info "Validating: #{ui.color(name, :bold)}"
|
42
|
+
stack = provider.connection.stacks.build(
|
20
43
|
:name => 'validation-stack',
|
21
|
-
:template =>
|
22
|
-
)
|
44
|
+
:template => Sfn::Utils::StackParameterScrubber.scrub!(stack)
|
45
|
+
)
|
46
|
+
result = api_action!(:api_stack => stack) do
|
47
|
+
stack.validate
|
48
|
+
end
|
23
49
|
ui.info ui.color(' -> VALID', :bold, :green)
|
24
50
|
rescue => e
|
25
51
|
ui.info ui.color(' -> INVALID', :bold, :red)
|
26
52
|
ui.fatal e.message
|
27
|
-
|
53
|
+
raise e
|
28
54
|
end
|
29
55
|
end
|
30
56
|
|
data/lib/sfn/command.rb
CHANGED
@@ -18,8 +18,7 @@ module Sfn
|
|
18
18
|
|
19
19
|
# Override to provide config file searching
|
20
20
|
def initialize(cli_opts, args)
|
21
|
-
unless(cli_opts[
|
22
|
-
cli_opts = cli_opts.to_hash.to_smash(:snake)
|
21
|
+
unless(cli_opts['config'])
|
23
22
|
discover_config(cli_opts)
|
24
23
|
end
|
25
24
|
super(cli_opts, args)
|
@@ -37,14 +36,18 @@ module Sfn
|
|
37
36
|
# Start with current working directory and traverse to root
|
38
37
|
# looking for a `.sfn` configuration file
|
39
38
|
#
|
40
|
-
# @param opts [
|
41
|
-
# @return [
|
39
|
+
# @param opts [Slop]
|
40
|
+
# @return [Slop]
|
42
41
|
def discover_config(opts)
|
43
42
|
cwd = Dir.pwd.split(File::SEPARATOR)
|
44
43
|
until(cwd.empty? || File.exists?(cwd.push('.sfn').join(File::SEPARATOR)))
|
45
44
|
cwd.pop(2)
|
46
45
|
end
|
47
|
-
opts
|
46
|
+
if(opts.respond_to?(:fetch_option))
|
47
|
+
opts.fetch_option('config').value = cwd.join(File::SEPARATOR) unless cwd.empty?
|
48
|
+
else
|
49
|
+
opts['config'] = cwd.join(File::SEPARATOR) unless cwd.empty?
|
50
|
+
end
|
48
51
|
opts
|
49
52
|
end
|
50
53
|
|
@@ -11,11 +11,16 @@ module Sfn
|
|
11
11
|
# @return [KnifeCloudformation::Provider]
|
12
12
|
def provider
|
13
13
|
memoize(:provider, :direct) do
|
14
|
-
Sfn::Provider.new(
|
14
|
+
result = Sfn::Provider.new(
|
15
15
|
:miasma => config[:credentials],
|
16
16
|
:async => false,
|
17
17
|
:fetch => false
|
18
18
|
)
|
19
|
+
result.connection.data[:retry_ui] = ui
|
20
|
+
result.connection.data[:retry_type] = config.fetch(:retry, :type, :exponential)
|
21
|
+
result.connection.data[:retry_interval] = config.fetch(:retry, :interval, 5)
|
22
|
+
result.connection.data[:retry_max] = config.fetch(:retry, :max_attempts, 20)
|
23
|
+
result
|
19
24
|
end
|
20
25
|
end
|
21
26
|
|
@@ -117,9 +122,18 @@ module Sfn
|
|
117
122
|
arguments
|
118
123
|
end
|
119
124
|
|
125
|
+
# Override config method to memoize the result allowing for
|
126
|
+
# modifications to the configuration during runtime
|
127
|
+
#
|
128
|
+
# @return [Smash]
|
129
|
+
# @note callback requires are also loaded here
|
120
130
|
def config
|
121
131
|
memoize(:config) do
|
122
|
-
super
|
132
|
+
result = super
|
133
|
+
result.fetch(:callbacks, :require, []).each do |c_loader|
|
134
|
+
require c_loader
|
135
|
+
end
|
136
|
+
result
|
123
137
|
end
|
124
138
|
end
|
125
139
|
|
@@ -130,6 +144,7 @@ module Sfn
|
|
130
144
|
klass.instance_eval do
|
131
145
|
|
132
146
|
include Sfn::CommandModule::Base::InstanceMethods
|
147
|
+
include Sfn::CommandModule::Callbacks
|
133
148
|
include Sfn::Utils::JSON
|
134
149
|
include Sfn::Utils::Output
|
135
150
|
include Bogo::AnimalStrings
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'sfn'
|
2
|
+
require 'sparkle_formation'
|
3
|
+
|
4
|
+
module Sfn
|
5
|
+
module CommandModule
|
6
|
+
# Callback processor helpers
|
7
|
+
module Callbacks
|
8
|
+
|
9
|
+
include Bogo::Memoization
|
10
|
+
|
11
|
+
# Run expected callbacks around action
|
12
|
+
#
|
13
|
+
# @yieldblock api action to run
|
14
|
+
# @yieldresult [Object] result from call
|
15
|
+
# @return [Object] result of yield block
|
16
|
+
def api_action!(*args)
|
17
|
+
type = self.class.name.split('::').last.downcase
|
18
|
+
run_callbacks_for(["before_#{type}", :before], *args)
|
19
|
+
result = yield if block_given?
|
20
|
+
run_callbacks_for(["after_#{type}", :after], *args)
|
21
|
+
result
|
22
|
+
end
|
23
|
+
|
24
|
+
# Process requested callbacks
|
25
|
+
#
|
26
|
+
# @param type [Symbol, String] name of callback type
|
27
|
+
# @return [NilClass]
|
28
|
+
def run_callbacks_for(type, *args)
|
29
|
+
types = [type].flatten.compact
|
30
|
+
type = types.first
|
31
|
+
clbks = types.map do |c_type|
|
32
|
+
callbacks_for(c_type)
|
33
|
+
end.flatten(1).compact.uniq.each do |item|
|
34
|
+
callback_name, callback = item
|
35
|
+
ui.info "Callback #{ui.color(type.to_s, :bold)} #{callback_name}: #{ui.color('starting', :yellow)}"
|
36
|
+
if(args.empty?)
|
37
|
+
callback.call
|
38
|
+
else
|
39
|
+
callback.call(*args)
|
40
|
+
end
|
41
|
+
ui.info "Callback #{ui.color(type.to_s, :bold)} #{callback_name}: #{ui.color('complete', :green)}"
|
42
|
+
end
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
|
46
|
+
# Fetch valid callbacks for given type
|
47
|
+
#
|
48
|
+
# @param type [Symbol, String] name of callback type
|
49
|
+
# @param responder [Array<String, Symbol>] matching response methods
|
50
|
+
# @return [Array<Method>]
|
51
|
+
def callbacks_for(type)
|
52
|
+
([config.fetch(:callbacks, type, [])].flatten.compact + [config.fetch(:callbacks, :default, [])].flatten.compact).map do |c_name|
|
53
|
+
instance = memoize(c_name) do
|
54
|
+
begin
|
55
|
+
klass = Sfn::Callback.const_get(Bogo::Utility.camel(c_name.to_s))
|
56
|
+
klass.new(ui, config, arguments, provider)
|
57
|
+
rescue NameError
|
58
|
+
raise "Unknown #{type} callback requested: #{c_name} (not found)"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
if(instance.respond_to?(type))
|
62
|
+
[c_name, instance.method(type)]
|
63
|
+
end
|
64
|
+
end.compact
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -104,10 +104,25 @@ module Sfn
|
|
104
104
|
|
105
105
|
# Prompt for parameter values and store result
|
106
106
|
#
|
107
|
-
# @param
|
107
|
+
# @param sparkle [SparkleFormation, Hash]
|
108
|
+
# @param opts [Hash]
|
109
|
+
# @option opts [Hash] :current_parameters current stack parameter values
|
110
|
+
# @option opts [Miasma::Models::Orchestration::Stack] :stack existing stack
|
108
111
|
# @return [Hash]
|
109
|
-
def populate_parameters!(
|
110
|
-
|
112
|
+
def populate_parameters!(sparkle, opts={})
|
113
|
+
current_parameters = opts.fetch(:current_parameters, {})
|
114
|
+
current_stack = opts[:stack]
|
115
|
+
if(sparkle.is_a?(SparkleFormation))
|
116
|
+
parameter_prefix = sparkle.root? ? [] : (sparkle.root_path - [sparkle.root]).map do |s|
|
117
|
+
Bogo::Utility.camel(s.name)
|
118
|
+
end
|
119
|
+
stack_parameters = sparkle.compile.parameters
|
120
|
+
stack_parameters = stack_parameters.nil? ? Smash.new : stack_parameters._dump
|
121
|
+
else
|
122
|
+
parameter_prefix = []
|
123
|
+
stack_parameters = sparkle.fetch('Parameters', Smash.new)
|
124
|
+
end
|
125
|
+
unless(stack_parameters.empty?)
|
111
126
|
if(config.get(:parameter).is_a?(Array))
|
112
127
|
config[:parameter] = Smash[
|
113
128
|
*config.get(:parameter).map(&:to_a).flatten
|
@@ -120,14 +135,37 @@ module Sfn
|
|
120
135
|
else
|
121
136
|
config.set(:parameters, config.fetch(:parameter, Smash.new))
|
122
137
|
end
|
123
|
-
|
124
|
-
|
125
|
-
|
138
|
+
stack_parameters.each do |k,v|
|
139
|
+
ns_k = (parameter_prefix + [k]).compact.join('__')
|
140
|
+
next if config[:parameters][ns_k]
|
126
141
|
valid = false
|
142
|
+
# When parameter is a hash type, it is being set via
|
143
|
+
# intrinsic function and we don't modify
|
144
|
+
if(current_parameters[k].is_a?(Hash))
|
145
|
+
if(current_stack)
|
146
|
+
enable_set = validate_stack_parameter(current_stack, k, ns_k, current_parameters[k])
|
147
|
+
else
|
148
|
+
enable_set = true
|
149
|
+
end
|
150
|
+
if(enable_set)
|
151
|
+
# NOTE: direct set dumps the stack (nfi). Smash will
|
152
|
+
# auto dup it, and works, so yay i guess.
|
153
|
+
config[:parameters][ns_k] = Smash.new(current_parameters[k])
|
154
|
+
valid = true
|
155
|
+
end
|
156
|
+
else
|
157
|
+
if(current_stack && current_stack.data[:parent_stack])
|
158
|
+
use_expected = validate_stack_parameter(current_stack, k, ns_k, current_parameters[k])
|
159
|
+
unless(use_expected)
|
160
|
+
current_parameters[k] = current_stack.parameters[k]
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
attempt = 0
|
127
165
|
until(valid)
|
128
166
|
attempt += 1
|
129
167
|
default = config[:parameters].fetch(
|
130
|
-
|
168
|
+
ns_k, current_parameters.fetch(
|
131
169
|
k, v['Default']
|
132
170
|
)
|
133
171
|
)
|
@@ -138,7 +176,7 @@ module Sfn
|
|
138
176
|
end
|
139
177
|
validation = Sfn::Utils::StackParameterValidator.validate(answer, v)
|
140
178
|
if(validation == true)
|
141
|
-
config[:parameters][
|
179
|
+
config[:parameters][ns_k] = answer
|
142
180
|
valid = true
|
143
181
|
else
|
144
182
|
validation.each do |validation_error|
|
@@ -152,7 +190,67 @@ module Sfn
|
|
152
190
|
end
|
153
191
|
end
|
154
192
|
end
|
155
|
-
|
193
|
+
Smash[
|
194
|
+
config.fetch(:parameters, {}).map do |k,v|
|
195
|
+
strip_key = parameter_prefix ? k.sub(/#{parameter_prefix.join('__')}_{2}?/, '') : k
|
196
|
+
unless(strip_key.include?('__'))
|
197
|
+
[strip_key, v]
|
198
|
+
end
|
199
|
+
end.compact
|
200
|
+
]
|
201
|
+
end
|
202
|
+
|
203
|
+
# @return [Hash] parameters for root stack create/update
|
204
|
+
def config_root_parameters
|
205
|
+
Hash[
|
206
|
+
config.fetch(:parameters, {}).find_all do |k,v|
|
207
|
+
!k.include?('__')
|
208
|
+
end
|
209
|
+
]
|
210
|
+
end
|
211
|
+
|
212
|
+
# Validate stack parameter is properly set via stack resource
|
213
|
+
# from parent stack. If not properly set, prompt user for
|
214
|
+
# expected behavior. This accounts for states encountered when
|
215
|
+
# a nested stack's parameters are adjusted directly but the
|
216
|
+
# resource sets value via intrinsic function.
|
217
|
+
#
|
218
|
+
# @param c_stack [Miasma::Models::Orchestration::Stack] current stack
|
219
|
+
# @param p_key [String] stack parameter key
|
220
|
+
# @param p_ns_key [String] namespaced stack parameter key
|
221
|
+
# @param c_value [Hash] currently set value (via intrinsic function)
|
222
|
+
# @return [TrueClass, FalseClass] value is validated
|
223
|
+
def validate_stack_parameter(c_stack, p_key, p_ns_key, c_value)
|
224
|
+
stack_value = c_stack.parameters[p_key]
|
225
|
+
p_stack = c_stack.data[:parent_stack]
|
226
|
+
if(c_value.is_a?(Hash))
|
227
|
+
case c_value.keys.first
|
228
|
+
when 'Ref'
|
229
|
+
current_value = p_stack.parameters[c_value.values.first]
|
230
|
+
when 'Fn::Att'
|
231
|
+
resource_name, output_name = c_value.values.first.split('.', 2)
|
232
|
+
ref_stack = p_stack.nested_stacks.detect{|i| i.data[:logical_id] == resource_name}
|
233
|
+
if(ref_stack)
|
234
|
+
output = ref_stack.outputs.detect do |o|
|
235
|
+
o.key == output_name
|
236
|
+
end
|
237
|
+
if(output)
|
238
|
+
current_value = output.value
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
else
|
243
|
+
current_value = c_value
|
244
|
+
end
|
245
|
+
if(current_value && current_value != stack_value)
|
246
|
+
ui.warn 'Nested stack has been altered directly! This update may cause unexpected modifications!'
|
247
|
+
ui.warn "Stack name: #{c_stack.name}. Parameter: #{p_key}. Current value: #{stack_value}. Expected value: #{current_value} (via: #{c_value.inspect})"
|
248
|
+
answer = ui.ask_question("Use current value or expected value for #{p_key} [current/expected]?", :valid => ['current', 'expected'])
|
249
|
+
answer == 'expected'
|
250
|
+
else
|
251
|
+
ui.warn "Unable to check #{p_key} for direct value modification. (Cannot auto-check expected value #{c_value.inspect})"
|
252
|
+
true
|
253
|
+
end
|
156
254
|
end
|
157
255
|
|
158
256
|
end
|