sfn 0.5.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -10,71 +10,33 @@ module Sfn
10
10
 
11
11
  # cloudformation directories that should be ignored
12
12
  TEMPLATE_IGNORE_DIRECTORIES = %w(components dynamics registry)
13
- # maximum number of attempts to get valid parameter value
14
- MAX_PARAMETER_ATTEMPTS = 5
15
13
 
16
14
  module InstanceMethods
17
15
 
18
- # Request compile time parameter value
19
- #
20
- # @param p_name [String, Symbol] name of parameter
21
- # @param p_config [Hash] parameter meta information
22
- # @param cur_val [Object, NilClass] current value assigned to parameter
23
- # @param nested [TrueClass, FalseClass] template is nested
24
- # @option p_config [String, Symbol] :type
25
- # @option p_config [String, Symbol] :default
26
- # @option p_config [String, Symbol] :description
27
- # @option p_config [String, Symbol] :multiple
28
- # @return [Object]
29
- def request_compile_parameter(p_name, p_config, cur_val, nested=false)
30
- result = nil
31
- attempts = 0
32
- unless(cur_val || p_config[:default].nil?)
33
- cur_val = p_config[:default]
34
- end
35
- if(cur_val.is_a?(Array))
36
- cur_val = cur_val.map(&:to_s).join(',')
37
- end
38
- until(result && (!result.respond_to?(:empty?) || !result.empty?))
39
- attempts += 1
40
- if(config[:interactive_parameters] && (!nested || !p_config.key?(:prompt_when_nested) || p_config[:prompt_when_nested] == true))
41
- result = ui.ask_question(
42
- p_name.to_s.split('_').map(&:capitalize).join,
43
- :default => cur_val.to_s.empty? ? nil : cur_val.to_s
44
- )
45
- else
46
- result = cur_val.to_s
16
+ # @return [Array<SparkleFormation::SparklePack>]
17
+ def sparkle_packs
18
+ memoize(:sparkle_packs) do
19
+ config.fetch(:sparkle_pack, []).map do |sparkle_name|
20
+ SparkleFormation::Sparkle.new(:name => sparkle_name)
47
21
  end
48
- case p_config.fetch(:type, 'string').to_s.downcase.to_sym
49
- when :string
50
- if(p_config[:multiple])
51
- result = result.split(',').map(&:strip)
52
- end
53
- when :number
54
- if(p_config[:multiple])
55
- result = result.split(',').map(&:strip)
56
- new_result = result.map do |item|
57
- new_item = item.to_i
58
- new_item if new_item.to_s == item
59
- end
60
- result = new_result.size == result.size ? new_result : []
61
- else
62
- new_result = result.to_i
63
- result = new_result.to_s == result ? new_result : nil
64
- end
65
- else
66
- raise ArgumentError.new "Unknown compile time parameter type provided: `#{p_config[:type].inspect}` (Parameter: #{p_name})"
22
+ end
23
+ end
24
+
25
+ # @return [SparkleFormation::SparkleCollection]
26
+ def sparkle_collection
27
+ memoize(:sparkle_collection) do
28
+ collection = SparkleFormation::SparkleCollection.new
29
+ begin
30
+ root_pack = SparkleFormation::SparklePack.new(:root => config[:base_directory])
31
+ collection.set_root(root_pack)
32
+ rescue Errno::ENOENT
33
+ ui.warn 'No local SparkleFormation files detected'
67
34
  end
68
- if(result.nil? || (result.respond_to?(:empty?) && result.empty?))
69
- if(attempts > MAX_PARAMETER_ATTEMPTS)
70
- ui.fatal 'Failed to receive allowed parameter!'
71
- exit 1
72
- else
73
- ui.error "Invalid value provided for parameter. Must be type: `#{p_config[:type].to_s.capitalize}`"
74
- end
35
+ sparkle_packs.each do |pack|
36
+ collection.add_sparkle(pack)
75
37
  end
38
+ collection
76
39
  end
77
- result
78
40
  end
79
41
 
80
42
  # Load the template file
@@ -82,9 +44,10 @@ module Sfn
82
44
  # @param args [Symbol] options (:allow_missing)
83
45
  # @return [Hash] loaded template
84
46
  def load_template_file(*args)
47
+ c_stack = (args.detect{|i| i.is_a?(Hash)} || {})[:stack]
85
48
  unless(config[:template])
86
49
  set_paths_and_discover_file!
87
- unless(File.exists?(config[:file].to_s))
50
+ unless(config[:file])
88
51
  unless(args.include?(:allow_missing))
89
52
  ui.fatal "Invalid formation file path provided: #{config[:file]}"
90
53
  raise IOError.new "Failed to locate file: #{config[:file]}"
@@ -95,59 +58,110 @@ module Sfn
95
58
  config[:template]
96
59
  elsif(config[:file])
97
60
  if(config[:processing])
98
- compile_state = config.fetch(:compile_parameters, Smash.new)
99
61
  sf = SparkleFormation.compile(config[:file], :sparkle)
100
- if(name_args.first)
101
- sf.name = name_args.first
102
- end
103
- sf.compile_time_parameter_setter do |formation|
104
- f_name = []
105
- f_form = formation
106
- while(f_form)
107
- f_name.push f_form.name
108
- f_form = f_form.parent
109
- end
110
- f_name = f_name.reverse.map(&:to_s).join('_')
111
- current_state = compile_state.fetch(f_name, Smash.new)
112
- if(formation.compile_state)
113
- current_state = current_state.merge(formation.compile_state)
114
- end
115
- ui.info "#{ui.color('Compile time parameters:', :bold)} - template: #{ui.color(formation.name, :green, :bold)}"
116
- formation.parameters.each do |k,v|
117
- current_state[k] = request_compile_parameter(k, v, current_state[k], !!formation.parent)
118
- end
119
- formation.compile_state = current_state
62
+ sparkle_packs.each do |pack|
63
+ sf.sparkle.add_sparkle(pack)
120
64
  end
121
65
  if(sf.nested? && !sf.isolated_nests?)
122
- raise TypeError.new('Template does not contain isolated stack nesting! Cannot process in existing state.')
66
+ raise TypeError.new('Template does not contain isolated stack nesting! Sfn does not support mixed mixed resources within root stack!')
123
67
  end
68
+ run_callbacks_for(:template, :stack_name => arguments.first, :sparkle_stack => sf)
124
69
  if(sf.nested? && config[:apply_nesting])
125
- sf.apply_nesting do |stack_name, stack_definition|
126
- if(config[:print_only])
127
- # "http://example.com/bucket/#{name_args.first}_#{stack_name}.json"
128
- stack_definition
129
- else
130
- bucket = provider.connection.api_for(:storage).buckets.get(
131
- config[:nesting_bucket]
132
- )
133
- unless(bucket)
134
- raise "Failed to locate configured bucket for stack template storage (#{bucket})!"
135
- end
136
- file = bucket.files.build
137
- file.name = "#{name_args.first}_#{stack_name}.json"
138
- file.content_type = 'text/json'
139
- file.body = MultiJson.dump(Sfn::Utils::StackParameterScrubber.scrub!(stack_definition))
140
- file.save
141
- # TODO: what if we need extra params?
142
- url = URI.parse(file.url)
143
- "#{url.scheme}://#{url.host}#{url.path}"
144
- end
70
+ if(config[:apply_nesting] == true)
71
+ config[:apply_nesting] = :deep
72
+ end
73
+ case config[:apply_nesting].to_sym
74
+ when :deep
75
+ process_nested_stack_deep(sf, c_stack)
76
+ when :shallow
77
+ process_nested_stack_shallow(sf, c_stack)
78
+ else
79
+ raise ArgumentError.new "Unknown nesting style requested: #{config[:apply_nesting].inspect}!"
145
80
  end
146
81
  else
147
82
  sf.dump.merge('sfn_nested_stack' => !!sf.nested?)
148
83
  end
149
84
  else
150
- _from_json(File.read(config[:file]))
85
+ template = _from_json(File.read(config[:file]))
86
+ run_callbacks_for(:template, :stack_name => arguments.first, :hash_stack => template)
87
+ template
88
+ end
89
+ else
90
+ raise ArgumentError.new 'Failed to locate template for processing!'
91
+ end
92
+ end
93
+
94
+ # Processes template using the original shallow workflow
95
+ #
96
+ # @param sf [SparkleFormation] stack formation
97
+ # @param c_stack [Miasma::Models::Orchestration::Stack] existing stack
98
+ # @return [Hash] dumped stack
99
+ def process_nested_stack_shallow(sf, c_stack=nil)
100
+ sf.apply_nesting(:shallow) do |stack_name, stack, resource|
101
+ run_callbacks_for(:template, :stack_name => stack_name, :sparkle_stack => stack)
102
+ stack_definition = stack.compile.dump!
103
+ bucket = provider.connection.api_for(:storage).buckets.get(
104
+ config[:nesting_bucket]
105
+ )
106
+ if(config[:print_only])
107
+ template_url = "http://example.com/bucket/#{name_args.first}_#{stack_name}.json"
108
+ else
109
+ resource.properties.delete!(:stack)
110
+ unless(bucket)
111
+ raise "Failed to locate configured bucket for stack template storage (#{bucket})!"
112
+ end
113
+ file = bucket.files.build
114
+ file.name = "#{name_args.first}_#{stack_name}.json"
115
+ file.content_type = 'text/json'
116
+ file.body = MultiJson.dump(Sfn::Utils::StackParameterScrubber.scrub!(stack_definition))
117
+ file.save
118
+ url = URI.parse(file.url)
119
+ template_url = "#{url.scheme}://#{url.host}#{url.path}"
120
+ end
121
+ resource.properties.set!('TemplateURL', template_url)
122
+ end
123
+ end
124
+
125
+ # Processes template using new deep workflow
126
+ #
127
+ # @param sf [SparkleFormation] stack
128
+ # @param c_stack [Miasma::Models::Orchestration::Stack] existing stack
129
+ # @return [Hash] dumped stack
130
+ def process_nested_stack_deep(sf, c_stack=nil)
131
+ sf.apply_nesting(:deep) do |stack_name, stack, resource|
132
+ run_callbacks_for(:template, :stack_name => stack_name, :sparkle_stack => stack)
133
+ stack_definition = stack.compile.dump!
134
+ stack_resource = resource._dump
135
+ unless(config[:print_only])
136
+ result = Smash.new(
137
+ 'Parameters' => populate_parameters!(stack,
138
+ :stack => c_stack ? c_stack.nested_stacks.detect{|s| s.data[:logical_id] == stack_name} : nil,
139
+ :current_parameters => c_stack ? c_stack.template.fetch('Resources', stack_name, 'Properties', 'Parameters', Smash.new) : stack_resource['Properties'].fetch('Parameters', {})
140
+ )
141
+ )
142
+ resource.properties.delete!(:stack)
143
+ bucket = provider.connection.api_for(:storage).buckets.get(
144
+ config[:nesting_bucket]
145
+ )
146
+ unless(bucket)
147
+ raise "Failed to locate configured bucket for stack template storage (#{bucket})!"
148
+ end
149
+ file = bucket.files.build
150
+ file.name = "#{name_args.first}_#{stack_name}.json"
151
+ file.content_type = 'text/json'
152
+ file.body = MultiJson.dump(Sfn::Utils::StackParameterScrubber.scrub!(stack_definition))
153
+ file.save
154
+ url = URI.parse(file.url)
155
+ result.merge!(
156
+ 'TemplateURL' => "#{url.scheme}://#{url.host}#{url.path}"
157
+ )
158
+ else
159
+ result = Smash.new(
160
+ 'TemplateURL' => "http://example.com/bucket/#{name_args.first}_#{stack_name}.json"
161
+ )
162
+ end
163
+ result.each do |k,v|
164
+ resource.properties.set!(k, v)
151
165
  end
152
166
  end
153
167
  end
@@ -181,40 +195,98 @@ module Sfn
181
195
  #
182
196
  # @return [TrueClass]
183
197
  def set_paths_and_discover_file!
184
- if(config[:base_directory])
185
- SparkleFormation.sparkle_path = config[:base_directory]
186
- end
187
- if(!config[:file] && config[:file_path_prompt])
188
- root = File.expand_path(
189
- config.fetch(:base_directory,
190
- File.join(Dir.pwd, 'cloudformation')
191
- )
192
- ).split('/')
193
- bucket = root.pop
194
- root = root.join('/')
195
- directory = File.join(root, bucket)
196
- config[:file] = prompt_for_file(directory,
197
- :directories_name => 'Collections',
198
- :files_name => 'Templates',
199
- :ignore_directories => TEMPLATE_IGNORE_DIRECTORIES
200
- )
198
+ if(config[:processing])
199
+ if(!config[:file] && config[:file_path_prompt])
200
+ config[:file] = prompt_for_template
201
+ else
202
+ config[:file] = sparkle_collection.get(:template, config[:file])[:path]
203
+ end
201
204
  else
202
- unless(Pathname(config[:file].to_s).absolute?)
203
- base_dir = config[:base_directory].to_s
204
- file = config[:file].to_s
205
- pwd = Dir.pwd
206
- config[:file] = [
207
- File.join(base_dir, file),
208
- File.join(pwd, file),
209
- File.join(pwd, 'cloudformation', file)
210
- ].detect do |file_path|
211
- File.file?(file_path)
205
+ if(config[:file])
206
+ unless(File.exists?(config[:file]))
207
+ raise Errno::ENOENT.new("No such file - #{config[:file]}")
212
208
  end
209
+ else
210
+ raise "Template processing is disabled. Path to serialized template via `--file` required!"
213
211
  end
214
212
  end
215
213
  true
216
214
  end
217
215
 
216
+ # Prompt user for template selection
217
+ #
218
+ # @param prefix [String] prefix filter for names
219
+ # @return [String] path to template
220
+ def prompt_for_template(prefix=nil)
221
+ if(prefix)
222
+ collection_name = prefix.split('__').map do |c_name|
223
+ c_name.split('_').map(&:capitalize).join(' ')
224
+ end.join(' / ')
225
+ ui.info "Viewing collection: #{ui.color(collection_name, :bold)}"
226
+ template_names = sparkle_collection.templates.keys.find_all do |t_name|
227
+ t_name.to_s.start_with?(prefix.to_s)
228
+ end
229
+ else
230
+ template_names = sparkle_collection.templates.keys
231
+ end
232
+ collections = template_names.map do |t_name|
233
+ t_name = t_name.to_s.sub(/^#{Regexp.escape(prefix.to_s)}/, '')
234
+ if(t_name.include?('__'))
235
+ c_name = t_name.split('__').first
236
+ [[prefix, c_name].compact.join('') + '__', c_name]
237
+ end
238
+ end.compact.uniq(&:first)
239
+ templates = template_names.map do |t_name|
240
+ t_name = t_name.to_s.sub(/^#{Regexp.escape(prefix.to_s)}/, '')
241
+ unless(t_name.include?('__'))
242
+ [[prefix, t_name].compact.join(''), t_name]
243
+ end
244
+ end.compact
245
+ if(collections.empty? && templates.empty?)
246
+ ui.error 'Failed to locate any templates!'
247
+ return nil
248
+ end
249
+ ui.info "Please select an entry#{ '(or collection to list)' unless collections.empty?}:"
250
+ output = []
251
+ idx = 1
252
+ valid = {}
253
+ unless(collections.empty?)
254
+ output << ui.color('Collections:', :bold)
255
+ collections.each do |full_name, part_name|
256
+ valid[idx] = {:name => full_name, :type => :collection}
257
+ output << [idx, part_name.split('_').map(&:capitalize).join(' ')]
258
+ idx += 1
259
+ end
260
+ end
261
+ unless(templates.empty?)
262
+ output << ui.color('Templates:', :bold)
263
+ templates.each do |full_name, part_name|
264
+ valid[idx] = {:name => full_name, :type => :template}
265
+ output << [idx, part_name.split('_').map(&:capitalize).join(' ')]
266
+ idx += 1
267
+ end
268
+ end
269
+ max = idx.to_s.length
270
+ output.map! do |line|
271
+ if(line.is_a?(Array))
272
+ " #{line.first}.#{' ' * (max - line.first.to_s.length)} #{line.last}"
273
+ else
274
+ line
275
+ end
276
+ end
277
+ ui.puts "#{output.join("\n")}\n"
278
+ response = nil
279
+ until(valid[response])
280
+ response = ui.ask_question('Enter selection').to_i
281
+ end
282
+ entry = valid[response]
283
+ if(entry[:type] == :collection)
284
+ prompt_for_template(entry[:name])
285
+ else
286
+ sparkle_collection.get(:template, entry[:name])[:path]
287
+ end
288
+ end
289
+
218
290
  end
219
291
 
220
292
  module ClassMethods
@@ -3,6 +3,7 @@ require 'sfn'
3
3
  module Sfn
4
4
  module CommandModule
5
5
  autoload :Base, 'sfn/command_module/base'
6
+ autoload :Callbacks, 'sfn/command_module/callbacks'
6
7
  autoload :Stack, 'sfn/command_module/stack'
7
8
  autoload :Template, 'sfn/command_module/template'
8
9
  end
@@ -5,10 +5,6 @@ module Sfn
5
5
  # Update command configuration
6
6
  class Update < Validate
7
7
 
8
- attribute(
9
- :print_only, [TrueClass, FalseClass],
10
- :description => 'Print the resulting stack template'
11
- )
12
8
  attribute(
13
9
  :apply_stack, String,
14
10
  :multiple => true,
@@ -37,14 +37,24 @@ module Sfn
37
37
  :coerce => lambda{|v| v.to_i}
38
38
  )
39
39
  attribute(
40
- :apply_nesting, [TrueClass, FalseClass],
41
- :default => true,
40
+ :apply_nesting, [String, Symbol],
41
+ :default => 'deep',
42
42
  :description => 'Apply stack nesting'
43
43
  )
44
44
  attribute(
45
45
  :nesting_bucket, String,
46
46
  :description => 'Bucket to use for storing nested stack templates'
47
47
  )
48
+ attribute(
49
+ :print_only, [TrueClass, FalseClass],
50
+ :description => 'Print the resulting stack template'
51
+ )
52
+ attribute(
53
+ :sparkle_pack, String,
54
+ :multiple => true,
55
+ :description => 'Load SparklePack gem',
56
+ :coerce => lambda{|s| require s; s}
57
+ )
48
58
 
49
59
  end
50
60
  end
@@ -187,6 +187,78 @@ module Sfn
187
187
  end
188
188
  end
189
189
 
190
+ # Return all stacks contained within this stack
191
+ #
192
+ # @param recurse [TrueClass, FalseClass] recurse to fetch _all_ stacks
193
+ # @return [Array<Miasma::Models::Orchestration::Stack>]
194
+ def nested_stacks(recurse=true)
195
+ resources.all.map do |resource|
196
+ if(self.api.class.const_get(:RESOURCE_MAPPING).fetch(resource.type, {})[:api] == :orchestration)
197
+ n_stack = resource.expand
198
+ if(n_stack)
199
+ n_stack.data[:logical_id] = resource.name
200
+ n_stack.data[:parent_stack] = self
201
+ if(recurse)
202
+ [n_stack] + n_stack.nested_stacks(recurse)
203
+ else
204
+ n_stack
205
+ end
206
+ end
207
+ end
208
+ end.flatten.compact
209
+ end
210
+
211
+ # @return [TrueClass, FalseClass] stack contains nested stacks
212
+ def nested?
213
+ !!resources.detect do |resource|
214
+ self.api.class.const_get(:RESOURCE_MAPPING).fetch(resource.type, {})[:api] == :orchestration
215
+ end
216
+ end
217
+
218
+ # Return stack policy if available
219
+ #
220
+ # @return [Smash, NilClass]
221
+ def policy
222
+ if(self.api.provider == :aws) # cause this is the only one
223
+ begin
224
+ result = self.api.request(
225
+ :path => '/',
226
+ :form => Smash.new(
227
+ 'Action' => 'GetStackPolicy',
228
+ 'StackName' => self.id
229
+ )
230
+ )
231
+ serialized_policy = result.get(:body, 'GetStackPolicyResult', 'StackPolicyBody')
232
+ MultiJson.load(serialized_policy).to_smash
233
+ rescue Miasma::Error::ApiError::RequestError => e
234
+ if(e.response.code == 404)
235
+ nil
236
+ else
237
+ raise
238
+ end
239
+ end
240
+ end
241
+ end
242
+
243
+ # Detect the nesting style in use by the stack
244
+ #
245
+ # @return [Symbol, NilClass] style of nesting (:shallow, :deep)
246
+ # or `nil` if no nesting detected
247
+ # @note in shallow nesting style, stack resources will not
248
+ # contain any direct values for parameters (which is what we
249
+ # are testing for)
250
+ def nesting_style
251
+ if(nested?)
252
+ self.template['Resources'].find_all do |t_resource|
253
+ t_resource['Type'] == self.api.class.const_get(:RESOURCE_MAPPING).key(self.class)
254
+ end.detect do |t_resource|
255
+ t_resource['Properties'].fetch('Parameters', {}).values.detect do |t_value|
256
+ !t_value.is_a?(Hash)
257
+ end
258
+ end ? :deep : :shallow
259
+ end
260
+ end
261
+
190
262
  end
191
263
  end
192
264
  end
@@ -1,4 +1,5 @@
1
1
  require 'sfn'
2
+ require 'pathname'
2
3
 
3
4
  module Sfn
4
5
  module Utils
@@ -87,6 +88,8 @@ module Sfn
87
88
  entry = valid[response.to_i]
88
89
  if(entry[:type] == :directory)
89
90
  prompt_for_file(entry[:path], opts)
91
+ elsif Pathname(entry[:path]).absolute?
92
+ entry[:path]
90
93
  else
91
94
  "/#{entry[:path]}"
92
95
  end
data/lib/sfn/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Sfn
2
2
  # Current library version
3
- VERSION = Gem::Version.new('0.5.0')
3
+ VERSION = Gem::Version.new('1.0.0')
4
4
  end
data/lib/sfn.rb CHANGED
@@ -4,6 +4,7 @@ require 'bogo'
4
4
 
5
5
  module Sfn
6
6
 
7
+ autoload :Callback, 'sfn/callback'
7
8
  autoload :Provider, 'sfn/provider'
8
9
  autoload :Cache, 'sfn/cache'
9
10
  autoload :Config, 'sfn/config'
data/sfn.gemspec CHANGED
@@ -11,24 +11,22 @@ Gem::Specification.new do |s|
11
11
  s.license = 'Apache-2.0'
12
12
  s.require_path = 'lib'
13
13
  s.add_dependency 'bogo-cli', '~> 0.1.21'
14
- s.add_dependency 'miasma', '~> 0.2.20'
14
+ s.add_dependency 'miasma', '~> 0.2.27'
15
15
  s.add_dependency 'miasma-aws', '~> 0.1.16'
16
16
  s.add_dependency 'net-ssh'
17
- s.add_dependency 'sparkle_formation', '>= 0.4.0', '< 1.0'
17
+ s.add_dependency 'sparkle_formation', '~> 1.0'
18
18
  s.executables << 'sfn'
19
19
  s.files = Dir['{lib,bin}/**/*'] + %w(sfn.gemspec README.md CHANGELOG.md LICENSE)
20
20
  s.post_install_message = <<-EOF
21
21
 
22
- This version of sfn restricts the SparkleFormation library to versions prior to the
23
- 1.0 release. That's great for now but it means many features will not be available
24
- and only fixes will be backported and applied to this gem.
22
+ This is an install of the sfn gem from the 1.0 release tree. If you
23
+ are upgrading from a pre-1.0 version, please review the CHANGELOG and
24
+ test your environment _before_ continuing on!
25
25
 
26
- It is highly suggested that you upgrade to the 1.0 version of sfn or later to take
27
- advantage of new features and new development. This gem will continue on in
28
- maintenance mode for the near term future. Once EOL has been reached, this message
29
- will be updated.
26
+ * https://github.com/sparkleformation/sfn/blob/master/CHANGELOG.md
27
+
28
+ Happy stacking!
30
29
 
31
- Thanks and happy stacking!
32
30
  EOF
33
31
 
34
32
  end