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.
@@ -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