sparkle_formation 1.0.4 → 1.1.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.
@@ -0,0 +1,425 @@
1
+ ---
2
+ title: "Nested Stacks"
3
+ category: "dsl"
4
+ weight: 6
5
+ anchors:
6
+ - title: "Shallow Nesting"
7
+ url: "#shallow-nesting"
8
+ - title: "Deep Nesting"
9
+ url: "#deep-nesting"
10
+ ---
11
+
12
+ ## Nested Stacks
13
+
14
+ Most orchestration API templating systems provide support for a
15
+ "stack" resource which allows for a stack to define one or more
16
+ _nested_ stacks within its resources. SparkleFormation expands
17
+ stack nesting by adding extra functionality when compiling
18
+ SparkleFormation templates. Currently two styles of expanded
19
+ functionality are available and are explained in depth below:
20
+
21
+ - [Shallow Nesting](#shallow-nesting)
22
+ - [Deep Nesting](#deep-nesting)
23
+
24
+ The interface for using SparkleFormation's nested stack functionality
25
+ is via the `nest!` helper method. The method accepts a template
26
+ name and will insert the stack resource into the current template:
27
+
28
+ ~~~ruby
29
+ SparkleFormation.new(:root_template) do
30
+ nest!(:networking)
31
+ nest!(:applications)
32
+ end
33
+ ~~~
34
+
35
+ ### Shallow Nesting
36
+
37
+ Shallow stack nesting is the original style of nesting functionality
38
+ implemented within SparkleFormation. Key features/restrictions of
39
+ shallow nesting:
40
+
41
+ * Support nesting _one_ level deep
42
+ * Automatic parameter bubbling to root stack
43
+ * Automatic output mapping
44
+
45
+ #### Shallow Nesting Depth
46
+
47
+ Shallow nesting is restricted to single level nesting. This restriction
48
+ is in place due to the shallow nesting style being the first successfully
49
+ implemented nesting strategy. The restriction remains due to the unique
50
+ behavior this style of nesting provides which does not work well past
51
+ a single level of nesting.
52
+
53
+ #### Nested Parameter Bubbling
54
+
55
+ On compilation SparkleFormation will process nested stacks in a top-down
56
+ order. It will first extract parameter names from the nested stack. If
57
+ the root stack has no matching parameter, the parameter will automatically
58
+ be added to the root stack. For example:
59
+
60
+ ~~~ruby
61
+ SparkleFormation.new(:template_a) do
62
+ ...
63
+ parameters.fubar do
64
+ type 'String'
65
+ default 'FOOBAR'
66
+ end
67
+ end
68
+ ~~~
69
+
70
+ ~~~ruby
71
+ SparkleFormation.new(:root) do
72
+ nest!(:template_a)
73
+ end
74
+ ~~~
75
+
76
+ when compiled would result in:
77
+
78
+ ~~~json
79
+ ...
80
+ "Parameters": {
81
+ "Fubar": {
82
+ "Type": "String",
83
+ "Default": "FOOBAR"
84
+ }
85
+ },
86
+ "Resources": {
87
+ "TemplateA": {
88
+ "Type": "AWS::CloudFormation::Stack",
89
+ "Properties": {
90
+ "Parameters": {
91
+ "Fubar": {
92
+ "Ref": "Fubar"
93
+ }
94
+
95
+ ...
96
+ ~~~
97
+
98
+ If a second stack is nested and it defines a parameter
99
+ with the same name as a previously defined parameter,
100
+ only the original parameter will be used and both stacks
101
+ will reference it.
102
+
103
+ This _parameter bubbling_ behavior allows all contained stacks
104
+ to be controlled from the root stack providing a single point
105
+ of interaction.
106
+
107
+ #### Nested Output Mapping
108
+
109
+ During compilation and processing of nested stacks, SparkleFormation
110
+ will also keep list of outputs available from previously processed
111
+ nested stack resources. If a parameter name on a nested stack
112
+ matches the name of an output defined in a nested stack, SparkleFormation
113
+ will automatically update the nested stack resource parameter to
114
+ use the output value. For example:
115
+
116
+ ~~~ruby
117
+ SparkleFormation.new(:template_a) do
118
+ ...
119
+ outputs.address do
120
+ description 'Address of thing'
121
+ value ref!(:thing)
122
+ end
123
+ ...
124
+ end
125
+ ~~~
126
+
127
+ ~~~ruby
128
+ SparkleFormation.new(:template_b) do
129
+ ...
130
+ parameters.address do
131
+ type 'String'
132
+ end
133
+ ...
134
+ end
135
+ ~~~
136
+
137
+ ~~~ruby
138
+ SparkleFormation.new(:root_template) do
139
+ nest!(:template_a)
140
+ nest!(:template_b)
141
+ end
142
+ ~~~
143
+
144
+ When the final template file is compiled SparkleFormation will not
145
+ bubble the `Address` parameter to the root stack. Because `template_b`
146
+ defines an output with a matching name, SparkleFormation automatically
147
+ uses that output value:
148
+
149
+ ~~~json
150
+ ...
151
+ "TemplateB": {
152
+ "Type": "AWS::CloudFormation::Stack",
153
+ "Properties": {
154
+ "Parameters": {
155
+ "Address": {
156
+ "Fn:GetAtt": [
157
+ "TemplateA",
158
+ "Outputs.Address"
159
+ ]
160
+ }
161
+ ...
162
+ ~~~
163
+
164
+ Shallow nesting easily exposes the power of nesting stack resources
165
+ while maintaining a single point of access for managing a stack. This
166
+ is important to note when looking at the ease of use for updating
167
+ running stacks. Unless a template change is required, parameter changes
168
+ can be made via a single update call to the root stack. It also means
169
+ that parameter based updates can be provided from any acceptable interface,
170
+ be it a CLI tool, or web based UI.
171
+
172
+ _NOTE: One issue quickly encountered with parameter heavy nested stacks
173
+ is resource limits on the number of parameters allowed within a single
174
+ stack. Using deep stack nesting prevents this issue._
175
+
176
+ #### Shallow Nesting Usage
177
+
178
+ Shallow nesting is performed by calling `SparkleFormation#apply_nesting`.
179
+ The method expects a block to be provided. This block handles storage
180
+ of the nested stack template (if required) and any updates to the
181
+ original stack resource.
182
+
183
+ ~~~ruby
184
+ sfn = SparkleFormation.compile(template_path, :sparkle)
185
+
186
+ sfn.apply_nesting(:shallow) do |stack_name, nested_stack_sfn, original_stack_resource|
187
+ template_content = nested_stack_cfn.compile.dump!
188
+ # store the template content as required, set remote location as `template_url`
189
+ original_stack_resource.properites.delete!(:stack)
190
+ original_stack_resource.properties.set!('TemplateURL', template_url)
191
+ end
192
+ ~~~
193
+
194
+ ### Deep Nesting
195
+
196
+ Deep stack nesting is an expansion of the shallow stack nesting functionality.
197
+ It loses some ease of use but gains greater functionality. Key features/
198
+ restrictions of deep stack nesting:
199
+
200
+ * No parameter bubbling
201
+ * Supports unlimited nesting depths
202
+ * Automatic output mapping
203
+ * Automatic output bubbling
204
+
205
+ #### Deep Nested Parameters
206
+
207
+ Deep stack nesting does not provide parameter bubbling. The biggest issue
208
+ in providing this type of behavior for deeply nested stacks are the limits
209
+ applied by the API. It also introduces more complexity to the implementation
210
+ since parameters would have to be propagated from the root stack to the
211
+ leaf stacks requiring the parameters.
212
+
213
+ Instead of bubbling parameters to the root stack, deep nesting behavior
214
+ does nothing with the parameters defined for nested stacks. It shifts that
215
+ responsibility to the application which can update resource's parameters
216
+ as it decides using its registered callback handler.
217
+
218
+ #### Unlimited Nesting Depth
219
+
220
+ Deep stack nesting does not enforce a limit on the number of levels deep
221
+ stacks may be nested. This _may_ not be true for the targeted API.
222
+ Supporting multiple levels of nesting makes it easy to logically
223
+ compartmentalize related resources into stacks, which can then be
224
+ collected and compartmentalized into category-style stacks which can be
225
+ nested into the root stack. This can make it easier to not only develop
226
+ stacks but easier to reason about as well.
227
+
228
+ #### Automatic Output Mapping and Bubbling
229
+
230
+ Much like the shallow nesting behavior, deep nesting provides automatic
231
+ output value mapping to parameters of a matching name. This behavior is
232
+ more challenging when using deep nesting behavior due to the possibility
233
+ of outputs being defined in a resource tree that is isolated from a
234
+ nested stack requiring its value. To solve this problem SparkleFormation
235
+ will automatically add an output entry to the parent stack(s) "bubbling"
236
+ the value until the output is available at the same level requesting stack.
237
+ If the requesting stack is nested from the common depth, then parameters
238
+ are added to the stacks to "push" the value down.
239
+
240
+ ##### Output Bubbling Behavior
241
+
242
+ This example will illustrate the behavior seen when outputs are "bubbled":
243
+
244
+ ~~~ruby
245
+ SparkleFormation.new(:networking) do
246
+ ...
247
+ outputs.subnet do
248
+ description 'Networking subnet'
249
+ value ref!(:subnet_resource)
250
+ end
251
+ ...
252
+ end
253
+ ~~~
254
+
255
+ ~~~ruby
256
+ SparkleFormation.new(:infrastructure) do
257
+ ...
258
+ nest!(:networking)
259
+ ...
260
+ end
261
+ ~~~
262
+
263
+ ~~~ruby
264
+ SparkleFormation.new(:applications) do
265
+ ...
266
+ nest!(:moneymaker)
267
+ ...
268
+ end
269
+ ~~~
270
+
271
+ ~~~ruby
272
+ SparkleFormation.new(:moneymaker) do
273
+ parameters.subnet do
274
+ type 'String'
275
+ end
276
+ ...
277
+ end
278
+ ~~~
279
+
280
+ ~~~ruby
281
+ SparkleFormation.new(:root) do
282
+ nest!(:infrastructure)
283
+ nest!(:applications)
284
+ end
285
+ ~~~
286
+
287
+ When the `root` stack is compiled, it will first process the `infrastructure`
288
+ nesting, which will in turn process the `networking` nesting. After processing
289
+ those stacks, SparkleFormation will know the location of the `Subnet` output.
290
+ It will then process the `application` nesting, which has a `Subnet` parameter
291
+ matching a known output. Because the `Subnet` output from `networking` stack
292
+ is not accessible from the root stack to provide to the `application` stack,
293
+ SparkleFormation will add an output to the `infrastructure` stack "bubbling"
294
+ the output to the root stack. Once it is available at the root stack, it can
295
+ be passed to the `application` stack resource:
296
+
297
+ _NOTE: The below example includes the nested stack contents. A real template
298
+ will simply include a URL endpoint for fetching the document._
299
+
300
+ ~~~json
301
+ {
302
+ "Resources": {
303
+ "Infrastructure": {
304
+ "Type": "AWS::CloudFormation::Stack",
305
+ "Properties": {
306
+ "Stack": {
307
+ "Resources": {
308
+ "Networking": {
309
+ "Type": "AWS::CloudFormation::Stack",
310
+ "Properties": {
311
+ "Stack": {
312
+ ...
313
+ "Outputs": {
314
+ "Subnet": {
315
+ "Description": "Networking subnet",
316
+ "Value": {
317
+ "Ref": "SubnetResource"
318
+ }
319
+ }
320
+ }
321
+ }
322
+ }
323
+ },
324
+ "TemplateURL": "http://example.com/Networking.json"
325
+ },
326
+ "Outputs": {
327
+ "Subnet": {
328
+ "Value": {
329
+ "Fn::Att": [
330
+ "Networking",
331
+ "Outputs.Subnet"
332
+ ]
333
+ }
334
+ }
335
+ }
336
+ },
337
+ "TemplateURL": "http://example.com/Infrastructure.json"
338
+ }
339
+ },
340
+ "Applications": {
341
+ "Type": "AWS::CloudFormation::Stack",
342
+ "Properties": {
343
+ "Stack": {
344
+ "Parameters": {
345
+ "Subnet": {
346
+ "Type": "String"
347
+ }
348
+ },
349
+ "Resources": {
350
+ "Moneymaker": {
351
+ "Type": "AWS::CloudFormation::Stack",
352
+ "Properties": {
353
+ "Stack": {
354
+ "Parameters": {
355
+ "Subnet": {
356
+ "Type": "String"
357
+ }
358
+ }
359
+ ...
360
+ },
361
+ "Parameters": {
362
+ "Subnet": {
363
+ "Ref": "Subnet"
364
+ }
365
+ },
366
+ "TemplateURL": "http://example.com/Moneymaker.json"
367
+ }
368
+ }
369
+ }
370
+ },
371
+ "Parameters": {
372
+ "Subnet": {
373
+ "Fn::Att": [
374
+ "Infrastructure",
375
+ "Outputs.Subnet"
376
+ ]
377
+ }
378
+ },
379
+ "TemplateURL": "http://example.com/Applications.json"
380
+ }
381
+ }
382
+ }
383
+ }
384
+ ~~~
385
+
386
+ When the `root` template is compiled, it nests the `infrastructure` template, which in turn
387
+ nests the `networking` template. The `Subnet` output is found, registered, and the compilation
388
+ continues. At this point the `networking` template is the last of this "branch", so compilation
389
+ returns to the `root` template and starts on the nested `applications` template. It has
390
+ `moneymaker` nested and when the `moneymaker` template is processed, the parameter `Subnet` is
391
+ checked in the registered outputs. Since a match is found, two things happen:
392
+
393
+ 1. The `Subnet` output is "bubbled" to the `infrastructure` template
394
+ 2. The `Subnet` output from the `infrastructure` template is "dripped" into the `applications`
395
+ template and passed to the `moneymaker` template
396
+
397
+ When a parameter is encountered and a matching output has been registered, SparkleFormation will
398
+ add stack outputs to parent templates until a common context can be found between the requesting
399
+ template (template with the parameter) and the providing template (template with the output). The
400
+ common context for the two templates may not make it accessible to the requesting template, which is
401
+ where the "dripping" method is employed.
402
+
403
+ Since the requesting template may not have access to the common context (as the example above illustrates),
404
+ SparkleFormation will "drip" the value down to the template. It does this by injecting a `Subnet` parameter
405
+ into child templates and passing the value in the stack resource until it reaches a common depth with
406
+ the requesting template. Once this is completed, the requesting template will have access to the output
407
+ from the provider template.
408
+
409
+ #### Deep Nesting Usage
410
+
411
+ Deep nesting is performed by calling `SparkleFormation#apply_nesting`.
412
+ The method expects a block to be provided. This block handles storage
413
+ of the nested stack template (if required) and any updates to the
414
+ original stack resource.
415
+
416
+ ~~~ruby
417
+ sfn = SparkleFormation.compile(template_path, :sparkle)
418
+
419
+ sfn.apply_nesting(:deep) do |stack_name, nested_stack_sfn, original_stack_resource|
420
+ template_content = nested_stack_cfn.compile.dump!
421
+ # store the template content as required, set remote location as `template_url`
422
+ original_stack_resource.properites.delete!(:stack)
423
+ original_stack_resource.properties.set!('TemplateURL', template_url)
424
+ end
425
+ ~~~
data/docs/overview.md ADDED
@@ -0,0 +1,54 @@
1
+ ---
2
+ title: "Overview"
3
+ category: "dsl"
4
+ weight: 1
5
+ next:
6
+ label: "SparkleFormation DSL"
7
+ url: "sparkleformation-dsl.md"
8
+ ---
9
+
10
+ # Overview
11
+
12
+ SparkleFormation is a Ruby DSL library that assists in programmatically
13
+ composing template files commonly used by orchestration APIs. The library
14
+ has specific helper methods defined targeting the [AWS CloudFormation][cfn]
15
+ API, but the library is _not_ restricted to generating only
16
+ [AWS CloudFormation][cfn] templates.
17
+
18
+ SparkleFormation templates describe the state of infrastructure resources
19
+ as code. This allows for provisioning and updating of isolated stacks of
20
+ resources in a predictable and repeatable manner. These stacks can be
21
+ mangaged as single independent or interdependent collection which allow
22
+ for creation, modification, or deletion via a single API call.
23
+
24
+ SparkleFormation can be used to compose templates for any orchestration
25
+ API that accepts serialized documents to describe resources. This includes
26
+ AWS, Rackspace, OpenStack, GCE, and other similar services.
27
+
28
+ ## Getting Started
29
+
30
+ SparkleFormation on its own is simply a library used to generate serialized
31
+ templates. This documentation is focused mainly on the library specific
32
+ features and functionality. For user documentation focused on building and
33
+ generating infrastructure with SparkleFormation, please refer to the
34
+ [sfn][sfn] documentation.
35
+
36
+ [cfn]: https://aws.amazon.com/cloudformation/
37
+ [sfn]: http://www.sparkleformation.io/docs/sfn
38
+
39
+ ### Installation
40
+
41
+ SparkleFormation is available from [Ruby Gems](https://rubygems.org/gems/sparkle_formation). To install, simply execute:
42
+
43
+ ~~~sh
44
+ $ gem install sparkle_formation
45
+ ~~~
46
+
47
+ or, if you use [Bundler](http://bundler.io/), add the following to your Gemfile:
48
+
49
+ ~~~sh
50
+ gem 'sparkle_formation', '~> 1.0.4'
51
+ ~~~
52
+
53
+ This will install the SparkleFormation library. To install the `sfn`
54
+ CLI tool, please [refer to its documentation](http://www.sparkleformation.io/docs/sfn/overview.html#installation)
@@ -0,0 +1,211 @@
1
+ ---
2
+ title: "SparklePacks"
3
+ category: "dsl"
4
+ weight: 7
5
+ anchors:
6
+ - title: "Cheatsheet"
7
+ url: "#cheatsheet-breakdown"
8
+ - title: "Requirements"
9
+ url: "#requirements"
10
+ - title: "Layout"
11
+ url: "#layout"
12
+ - title: "Usage"
13
+ url: "#usage"
14
+ ---
15
+
16
+ ## SparklePacks
17
+
18
+ SparklePacks are a way to package and ship SparkleFormation collections
19
+ for direct use, or to extend in customized usage. A SparklePack can be
20
+ composed of all the building blocks defined by SparkleFormation. Once
21
+ a SparklePack is built, it can then be loaded and registered making
22
+ its building blocks available in the current usage context. Multiple
23
+ SparklePacks can be loaded, and SparkleFormation performs its lookup
24
+ action based on load order (last loaded retains highest precedence).
25
+
26
+ ### Cheatsheet Breakdown
27
+
28
+ * Composed of SparkleFormation any/all building blocks:
29
+ * Components
30
+ * Dynamics
31
+ * Registry
32
+ * Templates
33
+ * Packaged and distributed for reuse
34
+ * Supports standalone usage _and_ project integration
35
+ * Allows loading of multiple SparklePacks
36
+ * SparklePacks affect building block lookup behavior of SparkleFormation
37
+ * Last loaded SparklePack retains highest precedence
38
+
39
+ ### Requirements
40
+
41
+ #### Explicit Building Block Methods
42
+
43
+ The [explicit building block methods](building-blocks.md#name-based-components)
44
+ must be used when creating a SparklePack. Usage of implicit methods (like
45
+ `SparkleFormation.build` instead of `SparkleFormation.component`) is currently working but
46
+ should be considered un-supported. The explicit methods also allow
47
+ more flexibility on the layout of files since the file system structure
48
+ and file naming are decoupled.
49
+
50
+ ### Layout
51
+
52
+ A SparklePack is simply a directory containing a `sparkleformation`
53
+ subdirectory which contains all distributed building blocks:
54
+
55
+ ~~~
56
+ > tree
57
+ .
58
+ |____sparkleformation
59
+ | |____dynamics
60
+ | |____components
61
+ | |____registry
62
+ ~~~
63
+
64
+ ### Usage
65
+
66
+ On instantiation, `SparkleFormation` will automatically generate a
67
+ SparklePack based on global configuration and current working directory.
68
+ A customized pack can be provided on instantiation to override this
69
+ behavior:
70
+
71
+ ~~~ruby
72
+ root_pack = SparkleFormation::SparklePack.new(
73
+ :root => PATH_TO_PACK
74
+ )
75
+ sfn = SparkleFormation.new(:my_template, :sparkle => root_pack) do
76
+ # Define template
77
+ end
78
+ ~~~
79
+
80
+ It is also possible to add additional SparklePacks to an existing
81
+ SparkleFormation.
82
+
83
+ > NOTE: The SparklePack used on instantiation of a SparkleFormation
84
+ > instance is considered the *root* SparklePack and will _*always*_
85
+ > have the highest precedence.
86
+
87
+ Building from the previous example, adding a additional pack:
88
+
89
+ ~~~ruby
90
+ root_pack = SparkleFormation::SparklePack.new(
91
+ :root => PATH_TO_PACK
92
+ )
93
+ custom_pack = SparkleFormation::SparklePack.new(
94
+ :root => PATH_TO_PACK
95
+ )
96
+
97
+ sfn = SparkleFormation.new(
98
+ :my_template,
99
+ :sparkle => root_pack
100
+ )
101
+ sfn.sparkle.add_sparkle(custom_pack)
102
+ ~~~
103
+
104
+ With this `custom_pack` added to the collection, the SparkleFormation
105
+ lookup for building blocks will follow the order:
106
+
107
+ 1. `root_pack`
108
+ 2. `custom_pack`
109
+
110
+ By default new packs added will retain higher precedence than existing
111
+ packs already added:
112
+
113
+ ~~~ruby
114
+ root_pack = SparkleFormation::SparklePack.new(
115
+ :root => PATH_TO_PACK
116
+ )
117
+ custom_pack = SparkleFormation::SparklePack.new(
118
+ :root => PATH_TO_PACK
119
+ )
120
+ override_pack = SparkleFormation::SparklePack.new(
121
+ :root => PATH_TO_PACK
122
+ )
123
+
124
+ sfn = SparkleFormation.new(
125
+ :my_template,
126
+ :sparkle => root_pack
127
+ )
128
+ sfn.sparkle.add_sparkle(custom_pack)
129
+ sfn.sparkle.add_sparkle(override_pack)
130
+ ~~~
131
+
132
+
133
+ In the above example `override_pack` holds the second highest precedence
134
+ (the `root_pack` always holding the highest). Lookups will now have the
135
+ following order:
136
+
137
+ 1. `root_pack`
138
+ 2. `override_pack`
139
+ 3. `custom_pack`
140
+
141
+ It is possible to force a pack to the lowest precedence level when
142
+ adding:
143
+
144
+ ~~~ruby
145
+ root_pack = SparkleFormation::SparklePack.new(
146
+ :root => PATH_TO_PACK
147
+ )
148
+ custom_pack = SparkleFormation::SparklePack.new(
149
+ :root => PATH_TO_PACK
150
+ )
151
+ base_pack = SparkleFormation::SparklePack.new(
152
+ :root => PATH_TO_PACK
153
+ )
154
+
155
+ sfn = SparkleFormation.new(
156
+ :my_template,
157
+ :sparkle => root_pack
158
+ )
159
+ sfn.sparkle.add_sparkle(custom_pack)
160
+ sfn.sparkle.add_sparkle(base_pack, :low)
161
+ ~~~
162
+
163
+ This example demonstrates how to add a pack at the lowest precedence
164
+ level allowing currently registered SparklePacks to retain their
165
+ existing precedence. Lookups in this example will have the
166
+ following order:
167
+
168
+ 1. `root_pack`
169
+ 2. `custom_pack`
170
+ 3. `base_pack`
171
+
172
+ This behavior is _non-default_ so ensure it is the expected behavior
173
+ within an implementation.
174
+
175
+ ### Distribution
176
+
177
+ SparklePacks are structured such that it is easy to package and
178
+ distrbute them via RubyGems. An example file structure for `my-pack`
179
+ gem:
180
+
181
+ ~~~
182
+ > tree
183
+ .
184
+ |____my-pack.gemspec
185
+ |____lib
186
+ | |____sparkleformation
187
+ | | |____dynamics
188
+ | | |____components
189
+ | | |____registry
190
+ | |____my-pack.rb
191
+ ~~~
192
+
193
+ Then register the pack:
194
+
195
+ ~~~ruby
196
+ # ./lib/my-pack.rb
197
+
198
+ SparkleFormation::SparklePack.register!
199
+ ~~~
200
+
201
+ Once registered, packs can be loaded via name:
202
+
203
+ ~~~ruby
204
+ require 'my-pack'
205
+ root_pack = SparkleFormation::SparklePack.new(:name => 'my-pack')
206
+
207
+ sfn = SparkleFormation.new(
208
+ :my_template,
209
+ :sparkle => root_pack
210
+ )
211
+ ~~~