sparkle_formation 0.4.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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