sparkle_formation 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f13e8abe07f3752130dc2655eba524647f390618
4
- data.tar.gz: 874980adb868101566eb4756170280a5a1faec61
3
+ metadata.gz: 40ae36e7f30ce66c8f2866c246b873014effd6e1
4
+ data.tar.gz: c542ab9daceb4df19c8dfc4b50f1a1f8ac68090e
5
5
  SHA512:
6
- metadata.gz: 5f1cb9bec94362ba83200fb0d9f47ce310799118d3903a565a9b1ac667bb28751a9fdfea0fb952fffce83e3eb6cfbd5af5120672ceb2332b5113547cacc3f724
7
- data.tar.gz: 5b53d67cb905a968cab3932096e4603b324c68d133d76e66fea82982421b8c96654b4bf09df2db84149634981c14103c811972f44404e597d3e118d7e95e6971
6
+ metadata.gz: f75fa1dd08bc0438ffb2a92fa59a157c4d045a09423fd782ac33f92bdfaab3b10172b4697aa4bb31071d7901a7e99c86ea52789c238c3553b18b02278730e405
7
+ data.tar.gz: 2deadc43737f86a136dd7de4169d88cd7c5afefcf065685fb64c385b63835f379f6453c4d5a00993cd4f45aa50400b9fa99f6b98f64ba6fe14ec99c84df2b871
data/CHANGELOG.md CHANGED
@@ -1,8 +1,13 @@
1
- ## v0.4.0
2
- * Add support for compile time parameters
3
- * Backport SparkleStruct updates
4
- * Track parent stack within nested stacks
5
- * Fix nested stack check method which assumed resources hash
1
+ ## v1.0.0
2
+
3
+ > NOTE: This is a major version release. It includes multiple
4
+ > implementation updates that may cause breakage.
5
+
6
+ * Add SparklePacks for isolation and distribution
7
+ * Add SparkleFormation.component method for defining components
8
+ * Support fully recursive nesting and parameter / output mapping
9
+ * Support previous nesting style (shallow) and new style (deep)
10
+ * Include support for in-line stack policy extraction
6
11
 
7
12
  ## v0.3.0
8
13
  * Update `or!` helper method to take multiple arguments
data/LICENSE CHANGED
@@ -186,7 +186,7 @@ APPENDIX: How to apply the Apache License to your work.
186
186
  same "printed page" as the copyright notice for easier
187
187
  identification within third-party archives.
188
188
 
189
- Copyright [yyyy] [name of copyright owner]
189
+ Copyright 2015 Chris Roberts
190
190
 
191
191
  Licensed under the Apache License, Version 2.0 (the "License");
192
192
  you may not use this file except in compliance with the License.
data/README.md CHANGED
@@ -1,26 +1,40 @@
1
+ ![SparkleFormation](img/sparkle-formation.png)
2
+
1
3
  # SparkleFormation
2
4
 
3
- AWS CloudFormation template building tools for Ruby. Yay!
5
+ Orchestration template building tools for Ruby.
4
6
 
5
7
  ## What's it do?
6
8
 
7
- Provides a very loose DSL to describe an AWS CloudFormation
8
- in Ruby.
9
+ Provides a very loose DSL to describe orchestration API templates
10
+ programmatically in Ruby.
9
11
 
10
12
  ## Is that it?
11
13
 
12
14
  Yes. Well, kinda. It also has some extra features, like defining
13
- components, dynamics, merging, AWS builtin function helpers, and
15
+ building blocks to facilitate code reuse in template creation,
16
+ helper functions for commonly generated data structures, builtin
17
+ logic for handling template nesting, and most importantly:
14
18
  conjouring magic (to get unicorns).
15
19
 
16
- ## Expanded User Docs
20
+ ## Documentation
21
+
22
+ * [Library Documentation](https://sparkleformation.github.io/sparkle_formation)
23
+ * [User Documentation](https://sparkleformation.github.io/sparkle_formation/UserDocs)
24
+
25
+ ## Overview
17
26
 
18
- New user documentation is now here! [User Documentation](http://sparkleformation.github.io/sparkle_formation/UserDocs/)
27
+ Many template orchestration APIs accept serialized templates defining
28
+ infrastructure resources and configurations. Interacting directly with
29
+ these services via data serialization formats (JSON, YAML, etc) can be
30
+ difficult for humans. SparkleFormation allows humans to programmatically
31
+ define templates in Ruby. These are then exported into the desired
32
+ serialization format, ready to send to the target orchestration API.
19
33
 
20
34
  ## What's it look like?
21
35
 
22
- Lets use one of the example CF templates that creates an EC2 instance. First
23
- we can just convert it into a single file (ec2_example.rb):
36
+ Below is a simple example of an AWS CloudFormation template defined within
37
+ SparkleFormation. It creates a single EC2 resource:
24
38
 
25
39
  ```ruby
26
40
  SparkleFormation.new('ec2_example') do
@@ -44,7 +58,7 @@ SparkleFormation.new('ec2_example') do
44
58
  dynamic!(:ec2_instance, :foobar) do
45
59
  properties do
46
60
  key_name ref!(:key_name)
47
- image_id map!(:region_map, 'AWS::Region', :ami)
61
+ image_id map!(:region_map, region!, :ami)
48
62
  user_data base64!('80')
49
63
  end
50
64
  end
@@ -79,7 +93,7 @@ end
79
93
  ```
80
94
 
81
95
  And once compiled we get a nice Hash that we can then convert to JSON which
82
- is ready for AWS. To print:
96
+ is ready to send to the AWS CloudFormation API:
83
97
 
84
98
  ```ruby
85
99
  require 'sparkle_formation'
@@ -92,184 +106,25 @@ puts JSON.pretty_generate(
92
106
 
93
107
  Easy!
94
108
 
95
- ## Why not just write JSON?
96
-
97
- Because, who in their right mind would want to write all of that in JSON? Also,
98
- we can start applying some of the underlying features in `SparkleFormation` to
99
- make this easier to maintain.
100
-
101
- # Components
102
-
103
- Lets say we have a handful of CFN templates we want to maintain, and all of those
104
- templates use the same AMI. Instead of copying that information into all the
105
- templates, lets create an AMI component instead, and then load it into the actual
106
- templates.
107
-
108
- First, create the component (components/ami.rb):
109
-
110
- ```ruby
111
- SparkleFormation.build do
112
-
113
- mappings.region_map do
114
- set!('us-east-1', :ami => 'ami-7f418316')
115
- set!('us-east-1', :ami => 'ami-7f418316')
116
- set!('us-west-1', :ami => 'ami-951945d0')
117
- set!('us-west-2', :ami => 'ami-16fd7026')
118
- set!('eu-west-1', :ami => 'ami-24506250')
119
- set!('sa-east-1', :ami => 'ami-3e3be423')
120
- set!('ap-southeast-1', :ami => 'ami-74dda626')
121
- set!('ap-northeast-1', :ami => 'ami-dcfa4edd')
122
- end
123
-
124
- end
125
- ```
126
-
127
- Now, we can modify our initial example to use this component (ec2_example.rb):
128
-
129
- ```ruby
130
- SparkleFormation.new('ec2_example').load(:ami).overrides do
131
-
132
- description "AWS CloudFormation Sample Template ..."
133
-
134
- parameters.key_name do
135
- description 'Name of EC2 key pair'
136
- type 'String'
137
- end
138
-
139
- dynamic!(:ec2_instance, :foobar) do
140
- properties do
141
- key_name ref!(:key_name)
142
- image_id map!(:region_map, 'AWS::Region', :ami)
143
- user_data base64!('80')
144
- end
145
- end
146
-
147
- outputs do
148
- instance_id do
149
- description 'InstanceId of the newly created EC2 instance'
150
- value ref!(:foobar_ec2_instance)
151
- end
152
- az do
153
- description 'Availability Zone of the newly created EC2 instance'
154
- value attr!(:foobar_ec2_instance, :availability_zone)
155
- end
156
- public_ip do
157
- description 'Public IP address of the newly created EC2 instance'
158
- value attr!(:foobar_ec2_instance, :public_ip)
159
- end
160
- private_ip do
161
- description 'Private IP address of the newly created EC2 instance'
162
- value attr!(:foobar_ec2_instance, :private_ip)
163
- end
164
- public_dns do
165
- description 'Public DNSName of the newly created EC2 instance'
166
- value attr!(:foobar_ec2_instance, :public_dns_name)
167
- end
168
- private_dns do
169
- description 'Private DNSName of the newly created EC2 instance'
170
- value attr!(:foobar_ec2_instance, :private_dns_name)
171
- end
172
- end
173
- end
174
- ```
175
-
176
- Now a few things have changed. Instead of passing a block directly to the
177
- instance instantiation, we are loading a component (the `ami` component)
178
- into the formation, and then applying an override block on top of the `ami`
179
- component. The result is the same as the initial example, but now we have
180
- a DRY component to use. Great!
109
+ ## Reusability features
181
110
 
182
- ## Dynamics
111
+ SparkleFormation provides a number of features facilitating code reuse and
112
+ logical structuring. These features help aid developers in applying DRY
113
+ concepts to infrastructure codebases easing maintainability.
183
114
 
184
- Okay, so lets say we want to have two ec2 instances. We could duplicate the
185
- resource and outputs, renaming where required. This would get ugly quick,
186
- especially as more instances are added. Making a component for the ec2 resource
187
- won't really help since components are static, used to apply the same common
188
- parts to multiple templates. So what do we use?
115
+ > [Learn more!](https://sparkleformation.github.io/sparkle_formation/UserDocs/building-blocks.html)
189
116
 
190
- Enter `dynamics`. These are much like components, except that instead of simply
191
- being merged, they allow passing of arguments which makes them reusable to create
192
- unique resources. So, from our last example, lets move the ec2 related items
193
- into a dynamic (dynamics/node.rb):
117
+ ## SparkleFormation Implementations
194
118
 
195
- ```ruby
196
- SparkleFormation.dynamic(:node,
197
- :parameters => {
198
- :key_name => {
199
- :type => 'String',
200
- :description => 'Optionally make keypair static'
201
- }
202
- }
203
- ) do |_name, _config|
204
-
205
- if(_config[:key_name])
206
- parameters.set!("#{_name}_key_name".to_sym) do
207
- description 'Name of EC2 key pair'
208
- type 'String'
209
- end
210
- end
211
-
212
- dynamic!(:ec2_instance, _name) do
213
- properties do
214
- key_name _config.fetch(:key_name, ref!("#{_name}_key_name".to_sym))
215
- image_id map!(:region_map, 'AWS::Region', :ami)
216
- user_data baes64!('80')
217
- end
218
- end
219
-
220
- outputs("#{_name}_instance_id".to_sym) do
221
- description 'InstanceId of the newly created EC2 instance'
222
- value ref!("#{_name}_ec2_instance".to_sym)
223
- end
224
- outputs("#{_name}_az".to_sym) do
225
- description 'Availability Zone of the newly created EC2 instance'
226
- value attr!("#{_name}_ec2_instance".to_sym, :availability_zone)
227
- end
228
- outputs("#{_name}_public_ip".to_sym) do
229
- description 'Public IP address of the newly created EC2 instance'
230
- value attr!("#{_name}_ec2_instance".to_sym, :public_ip)
231
- end
232
- outputs("#{_name}_private_ip".to_sym) do
233
- description 'Private IP address of the newly created EC2 instance'
234
- value attr!("#{_name}_ec2_instance".to_sym, :private_ip)
235
- end
236
- outputs("#{_name}_public_dns".to_sym) do
237
- description 'Public DNSName of the newly created EC2 instance'
238
- value attr!("#{_name}_ec2_instance".to_sym, :public_dns_name)
239
- end
240
- outputs("#{_name}_private_dns".to_sym) do
241
- description 'Private DNSName of the newly created EC2 instance'
242
- value attr!("#{_name}_ec2_instance".to_sym, :private_dns_name)
243
- end
244
- end
245
- ```
246
-
247
- Now we can put all of these together, and create multiple ec2 instance
248
- resource easily:
249
-
250
- ```ruby
251
- SparkleFormation.new('ec2_example').load(:ami).overrides do
252
-
253
- description "AWS CloudFormation Sample Template ..."
254
-
255
- %w(node1 node2 node3).each do |_node_name|
256
- dynamic!(:node, _node_name)
257
- end
258
-
259
- # and include one with predefined keypair
260
-
261
- dynamic!(:node, 'snowflake', :key_pair => 'snowkeys')
262
- end
263
- ```
119
+ SparkleFormation itself is simply a library for building complex template
120
+ documents in Ruby. It does not provide any integrations with remote API
121
+ services. For interacting with remote services using the SparkleFormation
122
+ library, see the SparkleFormation CLI application:
264
123
 
265
- ## TODO
266
- * Add information about symbol importance
267
- * Add examples of camel case control
268
- * Add examples of complex merge strategies
269
- * Add examples of accessing parent hash elements
124
+ * [SparkleFormation CLI (sfn)](https://github.com/sparkleformation/sfn)
270
125
 
271
126
  # Infos
272
127
  * Documentation: http://sparkleformation.github.io/sparkle_formation
273
128
  * User Documentation: http://sparkleformation.github.io/sparkle_formation/UserDocs/README.html
274
129
  * Repository: https://github.com/sparkleformation/sparkle_formation
275
- * IRC: Freenode @ #heavywater
130
+ * IRC: Freenode @ #sparkleformation
@@ -0,0 +1,25 @@
1
+ require 'sparkle_formation'
2
+
3
+ class SparkleFormation
4
+
5
+ class Error < StandardError
6
+
7
+ class NotFound < KeyError
8
+ attr_reader :name
9
+
10
+ def initialize(*args)
11
+ opts = args.detect{|o| o.is_a?(Hash)}
12
+ args.delete(opts) if opts
13
+ super(args)
14
+ @name = opts[:name] if opts
15
+ end
16
+
17
+ class Dynamic < NotFound; end
18
+ class Component < NotFound; end
19
+ class Registry < NotFound; end
20
+ class Template < NotFound; end
21
+
22
+ end
23
+ end
24
+
25
+ end
@@ -0,0 +1,344 @@
1
+ require 'sparkle_formation'
2
+
3
+ class SparkleFormation
4
+ class Sparkle
5
+
6
+ class << self
7
+
8
+ @@_pack_registry = Smash.new
9
+
10
+ # Register a SparklePack for short name access
11
+ #
12
+ # @param name [String, Symbol] name of pack
13
+ # @param path [String] path to pack
14
+ # @return [Array<String:name, String:path>]
15
+ def register!(name=nil, path=nil)
16
+ unless(path)
17
+ idx = caller.index do |item|
18
+ item.end_with?("`register!'")
19
+ end
20
+ if(idx)
21
+ file = caller[idx.next].split(':', 2).first
22
+ path = File.join(File.dirname(file), 'sparkleformation')
23
+ unless(File.directory?(path))
24
+ path = nil
25
+ end
26
+ unless(name)
27
+ name = File.basename(caller[idx.next].split(':', 2).first)
28
+ name.sub!(File.extname(name), '')
29
+ end
30
+ end
31
+ end
32
+ unless(name)
33
+ if(path)
34
+ name = path.split(File::PATH_SEPARATOR)[-3].to_s
35
+ end
36
+ end
37
+ unless(path)
38
+ raise ArgumentError.new('No SparklePack path provided and failed to auto-detect!')
39
+ end
40
+ unless(name)
41
+ raise ArgumentError.new('No SparklePack name provided and failed to auto-detect!')
42
+ end
43
+ @@_pack_registry[name] = path
44
+ [name, path]
45
+ end
46
+
47
+ # Return the path to the SparkePack registered with the given
48
+ # name
49
+ #
50
+ # @param name [String, Symbol] name of pack
51
+ # @return [String] path
52
+ def path(name)
53
+ if(@@_pack_registry[name])
54
+ @@_pack_registry[name]
55
+ else
56
+ raise KeyError.new "No pack registered with requested name: #{name}!"
57
+ end
58
+ end
59
+
60
+ end
61
+
62
+ # Wrapper for evaluating sfn files to store within sparkle
63
+ # container and remove global application
64
+ def eval_wrapper
65
+ klass = Class.new(BasicObject)
66
+ klass.class_eval(<<-EOS
67
+ def require(*args)
68
+ ::Kernel.require *args
69
+ end
70
+
71
+ class SparkleFormation
72
+
73
+ attr_accessor :sparkle_path
74
+
75
+ class << self
76
+
77
+ def part_data(data=nil)
78
+ if(data)
79
+ @data = data
80
+ else
81
+ @data
82
+ end
83
+ end
84
+
85
+ def dynamic(name, args={}, &block)
86
+ part_data[:dynamic].push(
87
+ ::Smash.new(
88
+ :name => name,
89
+ :block => block,
90
+ :args => Smash[
91
+ args.map(&:to_a)
92
+ ],
93
+ :type => :dynamic
94
+ )
95
+ ).last
96
+ end
97
+
98
+ def build(&block)
99
+ part_data[:component].push(
100
+ ::Smash.new(
101
+ :block => block,
102
+ :type => :component
103
+ )
104
+ ).last
105
+ end
106
+
107
+ def component(name, &block)
108
+ part_data[:component].push(
109
+ ::Smash.new(
110
+ :name => name,
111
+ :block => block,
112
+ :type => :component
113
+ )
114
+ ).last
115
+ end
116
+
117
+ def dynamic_info(*args)
118
+ Smash.new(:metadata => {}, :args => {})
119
+ end
120
+
121
+ end
122
+
123
+ def initialize(*args)
124
+ SparkleFormation.part_data[:template].push(
125
+ ::Smash.new(
126
+ :name => args.first
127
+ )
128
+ )
129
+ raise TypeError
130
+ end
131
+
132
+ class Registry
133
+
134
+ def self.register(name, &block)
135
+ SparkleFormation.part_data[:registry].push(
136
+ ::Smash.new(
137
+ :name => name,
138
+ :block => block,
139
+ :type => :registry
140
+ )
141
+ ).last
142
+ end
143
+
144
+ end
145
+ SfnRegistry = Registry
146
+
147
+ end
148
+ ::Object.constants.each do |const|
149
+ unless(self.const_defined?(const))
150
+ next if const == :Config # prevent warning output
151
+ self.const_set(const, ::Object.const_get(const))
152
+ end
153
+ end
154
+
155
+ def part_data(arg)
156
+ SparkleFormation.part_data(arg)
157
+ end
158
+ EOS
159
+ )
160
+ klass
161
+ end
162
+
163
+ include Bogo::Memoization
164
+
165
+ # Valid directories from cwd to set as root
166
+ VALID_ROOT_DIRS = [
167
+ 'sparkleformation',
168
+ 'sfn',
169
+ 'cloudformation',
170
+ 'cfn',
171
+ '.'
172
+ ]
173
+
174
+ # Reserved directories
175
+ DIRS = [
176
+ 'components',
177
+ 'registry',
178
+ 'dynamics'
179
+ ]
180
+
181
+ # Valid types
182
+ TYPES = Smash.new(
183
+ 'component' => 'components',
184
+ 'registry' => 'registries',
185
+ 'dynamic' => 'dynamics',
186
+ 'template' => 'templates'
187
+ )
188
+
189
+ # @return [String] path to sparkle directories
190
+ attr_reader :root
191
+ # @return [Smash] raw part data
192
+ attr_reader :raw_data
193
+
194
+ # Create new sparkle instance
195
+ #
196
+ # @param args [Hash]
197
+ # @option args [String] :root path to sparkle directories
198
+ # @option args [String, Symbol] :name registered pack name
199
+ # @return [self]
200
+ def initialize(args={})
201
+ if(args[:name])
202
+ @root = self.class.path(args[:name])
203
+ else
204
+ @root = args.fetch(:root, locate_root)
205
+ end
206
+ unless(File.directory?(@root))
207
+ raise Errno::ENOENT.new("No such directory - #{@root}")
208
+ end
209
+ @raw_data = Smash.new(
210
+ :dynamic => [],
211
+ :component => [],
212
+ :registry => []
213
+ )
214
+ @wrapper = eval_wrapper.new
215
+ wrapper.part_data(raw_data)
216
+ load_parts!
217
+ end
218
+
219
+ # @return [Smash<name:block>]
220
+ def components
221
+ memoize(:components) do
222
+ Smash.new
223
+ end
224
+ end
225
+
226
+ # @return [Smash<name:block>]
227
+ def dynamics
228
+ memoize(:dynamics) do
229
+ Smash.new
230
+ end
231
+ end
232
+
233
+ # @return [Smash<name:block>]
234
+ def registries
235
+ memoize(:registries) do
236
+ Smash.new
237
+ end
238
+ end
239
+
240
+ # @return [Smash<name:path>]
241
+ def templates
242
+ memoize(:templates) do
243
+ Smash.new.tap do |hash|
244
+ Dir.glob(File.join(root, '**', '**', '*.{json,rb}')) do |path|
245
+ slim_path = path.sub("#{root}/", '')
246
+ next if DIRS.include?(slim_path.split('/').first)
247
+ data = Smash.new(:template => [])
248
+ t_wrap = eval_wrapper.new
249
+ t_wrap.part_data(data)
250
+ begin
251
+ t_wrap.instance_eval(IO.read(path), path, 1)
252
+ rescue TypeError
253
+ end
254
+ data = data[:template].first
255
+ unless(data[:name])
256
+ data[:name] = slim_path.tr('/', '__')
257
+ end
258
+ hash[data[:name]] = data.merge(
259
+ Smash.new(
260
+ :type => :template,
261
+ :path => path
262
+ )
263
+ )
264
+ end
265
+ end
266
+ end
267
+ end
268
+
269
+ # Request item from the store
270
+ #
271
+ # @param type [String, Symbol] item type (see: TYPES)
272
+ # @param name [String, Symbol] name of item
273
+ # @return [Smash] requested item
274
+ # @raises [NameError, Error::NotFound]
275
+ def get(type, name)
276
+ unless(TYPES.keys.include?(type.to_s))
277
+ raise NameError.new "Invalid type requested (#{type})! Valid types: #{TYPES.join(', ')}"
278
+ end
279
+ result = send(TYPES[type])[name]
280
+ if(result.nil? && TYPES[type] == 'templates')
281
+ result = (
282
+ send(TYPES[type]).detect{|k,v|
283
+ name = name.to_s
284
+ short_name = v[:path].sub(/#{Regexp.escape(root)}\/?/, '')
285
+ v[:path] == name ||
286
+ short_name == name ||
287
+ short_name.sub('.rb', '').gsub(File::SEPARATOR, '__').tr('-', '_') == name ||
288
+ v[:path].end_with?(name)
289
+ } || []
290
+ ).last
291
+ end
292
+ unless(result)
293
+ klass = Error::NotFound.const_get(type.capitalize)
294
+ raise klass.new("No #{type} registered with requested name (#{name})!", :name => name)
295
+ end
296
+ result
297
+ end
298
+
299
+ # @return [String]
300
+ def inspect
301
+ "<SparkleFormation::Sparkle [root: #{root.inspect}]>"
302
+ end
303
+
304
+ private
305
+
306
+ attr_reader :wrapper
307
+
308
+ # Locate root directory. Defaults to current working directory if
309
+ # valid sub directory is not located
310
+ #
311
+ # @return [String] root path
312
+ def locate_root
313
+ VALID_ROOT_DIRS.map do |part|
314
+ path = File.expand_path(File.join(Dir.pwd, part))
315
+ if(File.exists?(path))
316
+ path
317
+ end
318
+ end.compact.first
319
+ end
320
+
321
+ # Load all sparkle parts
322
+ def load_parts!
323
+ memoize(:load_parts) do
324
+ Dir.glob(File.join(root, "{#{DIRS.join(',')}}", '*.rb')).each do |file|
325
+ wrapper.instance_eval(IO.read(file), file, 1)
326
+ end
327
+ raw_data.each do |key, items|
328
+ items.each do |item|
329
+ if(item[:name])
330
+ send(TYPES[key])[item.delete(:name)] = item
331
+ else
332
+ path = item[:block].source_location.first.sub('.rb', '').split(File::SEPARATOR)
333
+ type, name = path.slice(path.size - 2, 2)
334
+ send(type)[name] = item
335
+ end
336
+ end
337
+ end
338
+ end
339
+ end
340
+
341
+ end
342
+ # Alias for interfacing naming
343
+ SparklePack = Sparkle
344
+ end