sfn 0.5.0 → 1.0.0
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.
- 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
|