dh-proteus 0.1.3

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.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +7 -0
  5. data/Gemfile +4 -0
  6. data/Gemfile.lock +80 -0
  7. data/README.md +414 -0
  8. data/Rakefile +6 -0
  9. data/bin/console +14 -0
  10. data/bin/proteus +5 -0
  11. data/bin/proteus_testing +8 -0
  12. data/bin/setup +8 -0
  13. data/build.sh +28 -0
  14. data/dh-proteus.gemspec +50 -0
  15. data/lib/core_ext/hash.rb +14 -0
  16. data/lib/proteus.rb +9 -0
  17. data/lib/proteus/app.rb +86 -0
  18. data/lib/proteus/backend/backend.rb +41 -0
  19. data/lib/proteus/commands/apply.rb +53 -0
  20. data/lib/proteus/commands/clean.rb +22 -0
  21. data/lib/proteus/commands/destroy.rb +51 -0
  22. data/lib/proteus/commands/graph.rb +22 -0
  23. data/lib/proteus/commands/import.rb +75 -0
  24. data/lib/proteus/commands/move.rb +28 -0
  25. data/lib/proteus/commands/output.rb +29 -0
  26. data/lib/proteus/commands/plan.rb +55 -0
  27. data/lib/proteus/commands/remove.rb +71 -0
  28. data/lib/proteus/commands/render.rb +36 -0
  29. data/lib/proteus/commands/taint.rb +35 -0
  30. data/lib/proteus/common.rb +72 -0
  31. data/lib/proteus/config/config.rb +47 -0
  32. data/lib/proteus/context_management/context.rb +31 -0
  33. data/lib/proteus/context_management/helpers.rb +14 -0
  34. data/lib/proteus/generate.rb +18 -0
  35. data/lib/proteus/generators/context.rb +57 -0
  36. data/lib/proteus/generators/environment.rb +42 -0
  37. data/lib/proteus/generators/init.rb +40 -0
  38. data/lib/proteus/generators/module.rb +69 -0
  39. data/lib/proteus/generators/templates/config/config.yaml.erb +22 -0
  40. data/lib/proteus/generators/templates/context/main.tf.erb +7 -0
  41. data/lib/proteus/generators/templates/context/variables.tf.erb +3 -0
  42. data/lib/proteus/generators/templates/environment/terraform.tfvars.erb +6 -0
  43. data/lib/proteus/generators/templates/module/io.tf.erb +1 -0
  44. data/lib/proteus/generators/templates/module/module.tf.erb +1 -0
  45. data/lib/proteus/generators/templates/module/validator.rb.erb +9 -0
  46. data/lib/proteus/global_commands/validate.rb +45 -0
  47. data/lib/proteus/helpers.rb +91 -0
  48. data/lib/proteus/helpers/path_helpers.rb +90 -0
  49. data/lib/proteus/helpers/string_helpers.rb +13 -0
  50. data/lib/proteus/init.rb +16 -0
  51. data/lib/proteus/modules/manager.rb +53 -0
  52. data/lib/proteus/modules/terraform_module.rb +184 -0
  53. data/lib/proteus/templates/partial.rb +123 -0
  54. data/lib/proteus/templates/template_binding.rb +62 -0
  55. data/lib/proteus/validators/base_validator.rb +24 -0
  56. data/lib/proteus/validators/validation_dsl.rb +172 -0
  57. data/lib/proteus/validators/validation_error.rb +9 -0
  58. data/lib/proteus/validators/validation_helpers.rb +9 -0
  59. data/lib/proteus/version.rb +7 -0
  60. metadata +260 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b0d31f4cca8dbbd403180b503384d70fc284b2b95df51a095e71f629b4942210
4
+ data.tar.gz: 0b57912e38c3c4b3f80d57b4c865bd89113f2f0934fb0996582f4d3a4ed1efaf
5
+ SHA512:
6
+ metadata.gz: 2a3c50683b3358058ed6651459152bc166f6452533bd8ca5c6d5a4a1c03c1ec821a39ebc40d210c5dfe5d32f6055156e8af5617cea9579964f8de49d75279bc7
7
+ data.tar.gz: 1e9ea31e800c1ead1ce9ec95ef460365848dae61a2f34fb6584821a31a1745d0a6f78f255e0b328a062ce79f652c8036cd7005e3c1924ddfaac50128f6183be0
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+
13
+ terraform.tfvars
14
+ *.tfplan
15
+ terraform.tfstate*
16
+ graph.png
17
+ .terraform
18
+ .DS_Store
19
+ .idea
20
+
21
+ release
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.5.5
7
+ before_install: gem install bundler -v 2.0.1
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in dh-proteus.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,80 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ proteus (0.1.3)
5
+ activesupport (~> 5.1.1)
6
+ aws-sdk-elasticsearchservice (~> 1.4.0)
7
+ aws-sdk-rds (~> 1.11.0)
8
+ aws-sdk-route53 (~> 1.7.0)
9
+ erubis (~> 2.7.0)
10
+ hcl-checker (~> 1.0.5)
11
+ thor (~> 0.20.0)
12
+
13
+ GEM
14
+ remote: https://rubygems.org/
15
+ specs:
16
+ activesupport (5.1.1)
17
+ concurrent-ruby (~> 1.0, >= 1.0.2)
18
+ i18n (~> 0.7)
19
+ minitest (~> 5.1)
20
+ tzinfo (~> 1.1)
21
+ aws-partitions (1.60.0)
22
+ aws-sdk-core (3.15.0)
23
+ aws-partitions (~> 1.0)
24
+ aws-sigv4 (~> 1.0)
25
+ jmespath (~> 1.0)
26
+ aws-sdk-elasticsearchservice (1.4.0)
27
+ aws-sdk-core (~> 3)
28
+ aws-sigv4 (~> 1.0)
29
+ aws-sdk-rds (1.11.0)
30
+ aws-sdk-core (~> 3)
31
+ aws-sigv4 (~> 1.0)
32
+ aws-sdk-route53 (1.7.0)
33
+ aws-sdk-core (~> 3)
34
+ aws-sigv4 (~> 1.0)
35
+ aws-sigv4 (1.0.2)
36
+ coderay (1.1.1)
37
+ concurrent-ruby (1.0.5)
38
+ diff-lcs (1.3)
39
+ erubis (2.7.0)
40
+ hcl-checker (1.0.5)
41
+ i18n (0.8.1)
42
+ jmespath (1.3.1)
43
+ method_source (0.8.2)
44
+ minitest (5.10.3)
45
+ pry (0.10.4)
46
+ coderay (~> 1.1.0)
47
+ method_source (~> 0.8.1)
48
+ slop (~> 3.4)
49
+ rake (10.5.0)
50
+ rspec (3.8.0)
51
+ rspec-core (~> 3.8.0)
52
+ rspec-expectations (~> 3.8.0)
53
+ rspec-mocks (~> 3.8.0)
54
+ rspec-core (3.8.0)
55
+ rspec-support (~> 3.8.0)
56
+ rspec-expectations (3.8.3)
57
+ diff-lcs (>= 1.2.0, < 2.0)
58
+ rspec-support (~> 3.8.0)
59
+ rspec-mocks (3.8.0)
60
+ diff-lcs (>= 1.2.0, < 2.0)
61
+ rspec-support (~> 3.8.0)
62
+ rspec-support (3.8.0)
63
+ slop (3.6.0)
64
+ thor (0.20.0)
65
+ thread_safe (0.3.6)
66
+ tzinfo (1.2.3)
67
+ thread_safe (~> 0.1)
68
+
69
+ PLATFORMS
70
+ ruby
71
+
72
+ DEPENDENCIES
73
+ bundler (~> 2.0)
74
+ proteus!
75
+ pry
76
+ rake (~> 10.0)
77
+ rspec (~> 3.0)
78
+
79
+ BUNDLED WITH
80
+ 2.0.1
data/README.md ADDED
@@ -0,0 +1,414 @@
1
+ # Terraform tool
2
+
3
+ This repository contains a wrapper application (proteus) around Terraform that facilitates management of resources.
4
+
5
+ The incentive for a Terraform wrapper is that Terraform in its current state cannot iteratively
6
+ declare module includes in a loop. Writing all the module includes manually would inevitably lead to
7
+ large manifest files.
8
+ These files would be difficult if not impossible to maintain without human error.
9
+
10
+ Furthermore, the abstraction from Terraform that the configuration format of the wrapper provides, enables
11
+ people who are not familiar with Terraform's configuration format to configure resources easily.
12
+
13
+ ## Setup
14
+
15
+ ### Requirements
16
+ * Ruby >= 2.5.5
17
+ * Terraform >= 0.11.14
18
+ * AWS IAM profile credentials
19
+
20
+ ### Prerequisites
21
+
22
+ #### Install Terraform
23
+ ```
24
+ brew install terraform
25
+ ```
26
+
27
+ If you would still like to use Terraform `< 0.12`:
28
+ ```
29
+ # Install Terraform 0.11.14 (for Homebrew users)
30
+ mkdir -p /usr/local/Cellar/terraform/0.11.14/bin \
31
+ && curl -o /tmp/terraform.zip "https://releases.hashicorp.com/terraform/0.11.14/terraform_0.11.14_darwin_amd64.zip" \
32
+ && unzip -o /tmp/terraform.zip -d "/usr/local/Cellar/terraform/0.11.14/bin" \
33
+ && rm -f /tmp/terraform.zip \
34
+ && brew switch terraform 0.11.14
35
+ && brew pin terraform
36
+ ```
37
+
38
+ #### Install proteus
39
+ ```
40
+ gem source --add https://YOUR_USER:YOUR_PASSWORD@nexus.usehurrier.com/repository/infra-gems
41
+ gem install proteus
42
+ ```
43
+
44
+ #### Set up proteus root path
45
+ ```
46
+ cd /path/to/your/repository
47
+
48
+ # initialize a project scaffolding
49
+ proteus init
50
+ ```
51
+
52
+ The above directory will be a valid `proteus` root. Should you want to be able to call `proteus` from anywhere on your system,
53
+ set it as an environment variable like so:
54
+ ```
55
+ export PROTEUS_ROOT=/path/to/your/repository
56
+ ```
57
+
58
+ #### AWS profile configuration
59
+ Create profiles and credentials for your environments:
60
+
61
+ `$HOME/.aws/config`:
62
+
63
+ ```
64
+ [profile staging]
65
+ region = eu-west-1
66
+ [profile production]
67
+ region = eu-west-1
68
+ ```
69
+
70
+ `$HOME/.aws/credentials`:
71
+ ```
72
+ [staging]
73
+ aws_access_key_id = YOUR_ACCESS_KEY_ID
74
+ aws_secret_access_key = YOUR_SECRET_ACCESS_KEY
75
+
76
+ [production]
77
+ aws_access_key_id = YOUR_ACCESS_KEY_ID
78
+ aws_secret_access_key = YOUR_SECRET_ACCESS_KEY
79
+ ```
80
+
81
+ #### State
82
+ Terraform state is managed remotely in an S3 bucket. Make sure to create that bucket and enable versioning.
83
+
84
+ ### Customize configuration
85
+ Having set up all of the above, it is time to modify the configuration to your needs.
86
+
87
+ ```
88
+ vim $PROTEUS_ROOT/config/config.yaml
89
+ ```
90
+
91
+ Should the term "environments" be unclear, please keep on reading. Otherwise:
92
+
93
+ ## TL;DR usage
94
+ ```
95
+ # Run "plan"
96
+ ./proteus [environment] plan
97
+
98
+ # Validate output
99
+
100
+ # Run "apply"
101
+ ./proteus [environment] apply
102
+ ```
103
+
104
+ Check the `example` context and its demo module in `contexts/example`.
105
+
106
+ ## Contexts and Environments
107
+ Environments and contexts define a scope for Terraform configuration. While contexts are defined by creating a directory in the contexts directory,
108
+ environments are defined using a Terraform variables file in the `environments` directory of a context.
109
+
110
+ ### State
111
+ Any tuple of the form (context, environment) has its own state. No state will be shared between tuples.
112
+ That means: (default, production) will not have any shared resources with (default, staging). Neither will (foo, production) share any state with
113
+ (bar, production.)
114
+
115
+ ### Conventions
116
+ * Valid environment names are are [snake case](https://en.wikipedia.org/wiki/Snake_case) and lowercase.
117
+ * files in `environments` need to comply with the following format: `terraform.environment_name.tfvars`
118
+
119
+ Once an environment gets defined using the above conventions, `proteus` will pick it up as a scope for its subcommands.
120
+
121
+ **Note:** You do not need to touch any code for the environment to be available in the command line interface.
122
+
123
+ ## Modules
124
+ There are two types of modules: Standard Terraform modules and modules that are managed by `proteus`.
125
+ Each module without `proteus` configuration behaves as a standard module.
126
+
127
+ ### Conventions
128
+ * Module names are [snake case](https://en.wikipedia.org/wiki/Snake_case) and lowercase. `foo_bar` is a correct module name while `FooBar` and `Foo_Bar` are both invalid names.
129
+ * Input and output variables go inside of a file called `io.tf` within the root of the module
130
+ * Group resources in separate files in the root of the module: Route53 related resources should be described in a file called `route53.tf`; IAM
131
+ specific resources go in a file called `iam.tf`. This way resource declarations are easy to find.
132
+ * Be verbose: We're not using MS-DOS FAT here. There is no need to shorten resource names.
133
+
134
+ **Note:** Use the generators provided by `proteus` for creating a scaffolding for contexts, modules and environments.
135
+
136
+ ### Standard modules
137
+ Standard modules can be implemented exactly as described in the [Terraform documentation](https://www.terraform.io/docs/modules/index.html).
138
+ They need to be included in a Terraform manifest in the root of a context. `proteus` will not use these modules for generating any code.
139
+
140
+ #### Structure
141
+ ```
142
+ modules/route53
143
+ ├── io.tf # Definition of input and output variables
144
+ └── route53.tf # route53 resources
145
+ ```
146
+
147
+ ### Managed modules
148
+ Managed modules extend the functionality of standard modules with a YAML configuration format, validators and templates.
149
+
150
+ #### Conventions
151
+ * Standard module conventions apply
152
+ * Singular (that means non-repeated resources) go into Terraform manifests in `config/global_resources`. Manifests in this directory
153
+ can be either standard Terraform manifests or ERB templates
154
+ * YAML Configuration files have to named exactly as an existing environment (with `.yaml` suffix)
155
+ * Template names are snake case (lowercase)
156
+ * The validator is located in the module's config root and named `validator.rb`
157
+
158
+ **Note: If your module does not contain a configuration file for your environment, it will be ignored.**
159
+
160
+ #### Structure
161
+ ```
162
+ modules/rds
163
+ ├── config # proteus confguration directory
164
+ │   ├── README.md
165
+ │   ├── global_resources # resources which only get applied once
166
+ │   │   ├── parameter_groups.tf
167
+ │   │   ├── rds.tf
168
+ │   │   └── vpc.tf
169
+ │   ├── production_ap.yaml # data for environment production_ap
170
+ │   ├── production_eu.yaml # data for environment production_eu
171
+ │   ├── production_us.yaml # ...
172
+ │   ├── qa.yaml # ...
173
+ │   ├── staging.yaml # ...
174
+ │   ├── templates
175
+ │   │   ├── _parameter_group.tf.erb # partial template for parameter groups
176
+ │   │   ├── _route53.tf.erb # partial template for route53 configuration
177
+ │   │   ├── defaults
178
+ │   │   │   └── parameter_group.yaml # default data for paramater group partial
179
+ │   │   └── rds.tf.erb # main template of the module
180
+ │   └── validator.rb # Class ensuring correct format of data
181
+ ├── io.tf # Definition of input and output variables
182
+ └── rds.tf # rds resources of the module
183
+ ```
184
+
185
+ #### Configuration format and templates
186
+ Configuration is implemented in YAML files in the root of the `config` directory modules.
187
+ The configuration format has only one required key: `template_data`.
188
+
189
+ *Example:*
190
+ ```
191
+ global_resources:
192
+ # refers to module_name/config/global_resources/your_global_resource_template.tf.erb
193
+ your_global_resource_template:
194
+ key0: value0
195
+ key1: value1
196
+ template_data:
197
+ # refers to module_name/config/templates/your_template.tf.erb
198
+ your_template:
199
+ foo: bar
200
+ hue:
201
+ - hue
202
+ - hue
203
+ - hue
204
+ ```
205
+ Each key in `template_data` refers to a template name. Each key in a template section of the `template_data`
206
+ Hash is available as an instance variable in the corresponding ERB template.
207
+
208
+ For the above example:
209
+ The template file is `your_template.tf.erb`. In the template `@foo` is available as a String value and
210
+ `@hue` is available as an Array.
211
+ The same gets applied to the global resource template `your_global_resource_template.tf.erb`: `@key0` and `@key1` are available
212
+ as instance variables within the corresponding template.
213
+
214
+ #### Partial templates
215
+ In addition to standard templates which are used to render collections as a whole, modules support templates which can be used
216
+ on single records within collections. This comes in handy if data related to a single record has to be rendered as it keeps
217
+ templates short and YAML configuration logically structured.
218
+
219
+ An example use case for this is RDS hosts and Route 53 records where a single database can have multiple Route 53 records.
220
+
221
+ Consider the following configuration data for an RDS host within the RDS module:
222
+ ```
223
+ template_data:
224
+ rds:
225
+ instances:
226
+ - instance_identifier: "dashboard"
227
+ instance_class: "db.m4.xlarge"
228
+ engine_version: "9.6.5"
229
+ engine: "postgres"
230
+ allocated_storage: 500
231
+
232
+ partials:
233
+ route53:
234
+ - app: "dashboard"
235
+ countries:
236
+ - "au"
237
+ - "bd"
238
+ - "bn"
239
+ - "hk"
240
+ - "kr"
241
+ - "my"
242
+ - "ph"
243
+ - "pk"
244
+ - "th"
245
+ - "tw"
246
+ ```
247
+ For this configuration, a template called `rds.tf.erb` will be loaded. Within the template all data within the scope of the key `rds` will
248
+ be available as instance variables. That means, data within in `instances` will be present in the template as `@instances`.
249
+
250
+ When iterating over the instances in the main template, one can trigger rendering of a partial in the context of the current instance
251
+ if data for an existing partial template is present.
252
+ In the above example, the configuration for the instance with the identifier "dashboard" refers to a partial template called `route53` and defines some
253
+ data within the scope of the key `route53`.
254
+
255
+ The partial template can be rendered in the main template as follows:
256
+ ```
257
+ <% @instances.each do |instance| %>
258
+ module "rds-<%= instance['instance_identifier'] %>" {
259
+ source = "./modules/rds"
260
+ ...
261
+ instance_identifier = "<%= instance['instance_identifier'] %>"
262
+ }
263
+
264
+ # Render partial "_route53.tf.erb" if data is present
265
+ # within partials => route53
266
+ <%= render_partial(name: :route53, data: instance, force_rendering: false) %>
267
+ <% end %>
268
+ ```
269
+ **Note:** The parameter `force_rendering` defines whether or not the partial will be rendered regardless of data being present or absent. The parameter
270
+ defaults to `true`.
271
+
272
+ **Partial default values**
273
+
274
+ If the data for a partial template is a Hash, defaults can be loaded from a YAML file in the `templates/defaults` directory.
275
+ All of the default values will be injected into the partial template, respecting the values set in the main YAML configuration.
276
+ Thus, the values in the main configuration act as overrides to the defaults.
277
+
278
+ For further information about how partial defaults work, please refer to the `example` module. You can render the module and check its
279
+ output by running the following command:
280
+
281
+ `./proteus context example demo_env render && cat contexts/example/demo_module.tf`
282
+
283
+
284
+ ### Default values
285
+ Each of the modules in this repository contains a file called `io.tf` which defines the input and output variables of the module.
286
+ For input variables, default values can be defined. These defaults can be overridden using the YAML configuration.
287
+ The following example is based on the `elasticache` module:
288
+
289
+ The module defines a variable named `node_type` in its `io.tf` manifest:
290
+ ```
291
+ variable "node_type" {
292
+ default = "cache.t2.micro"
293
+ }
294
+
295
+ ```
296
+ The configuration for the `staging` environment sets an override as follows:
297
+ ```
298
+ ...
299
+ elasticache:
300
+ instances:
301
+ - replication_group_id: sidekiq
302
+ node_type: cache.m3.medium # override for node_type
303
+ engine: "redis"
304
+ engine_version: "3.2.4"
305
+ availability_zones:
306
+ - eu-west-1a
307
+ number_cache_clusters: 1
308
+ ...
309
+ ```
310
+
311
+ Inside of the template `elasticache.tf.erb`, the method `render_defaults` gets called:
312
+ ```
313
+ ...
314
+ engine = "<%= instance['engine'] %>"
315
+ engine_version = "<%= instance['engine_version'] %>"
316
+
317
+ <%= render_defaults(instance) %>
318
+ environment = "${var.environment}"
319
+
320
+ vpc_id = "${module.vpc.id}"
321
+ ...
322
+
323
+ ```
324
+ `render_defaults` internally checks if the given context (in this case the data for an ElastiCache instance) defines
325
+ overrides for defaults defined in `io.tf` and, if overrides are present, renders them into the template.
326
+
327
+
328
+ #### Validators
329
+ The `proteus` library provides a simple DSL for validating module configuration. The DSL is available for validator classes.
330
+
331
+ #### Validator classes
332
+ The following conventions apply for validator classes:
333
+ * contained in `validator.rb` in config root of the respective module
334
+ * Class name: module name in [upper camel case](https://en.wikipedia.org/wiki/Camel_case)
335
+ * Validators inherit from `Proteus::Validators::BaseValidator`
336
+ * Validators override (and implement) exactly one method: `validate`
337
+
338
+ #### Validation DSL
339
+ The DSL provides the following keywords:
340
+
341
+ | Keyword | Description |
342
+ | ------------- |-------------|
343
+ | `within(key) { block }` | Ensures presence of `key` and data below `key` |
344
+ | `ensure_unique_values` | Ensures unique values in collections |
345
+ | `ensure_data_type(type)` | Checks if the current context is of type `type` |
346
+ | `ensure_uniqueness_across(key)` | Ensures uniqueness across a hierarchy |
347
+ | `each_key { block }` | Iterates over keys in the current context |
348
+ | `ensure_keys(*keys)` | Checks for presence of all provided keys in the current context |
349
+ | `each { block }` | Iterates over elements of a collection |
350
+ | `ensure_presence(key)` | Checks if `key` is present in the current context |
351
+ | `ensure_value(key, options)` | Ensures a value is in a set of predefined values or range or matches a regular expression |
352
+ | `in_case(key, has_value: [...]) { block }` | Optionally executes `block` if the value of `key` is in `has_value` |
353
+
354
+ Here's an example for data and the corresponding validator:
355
+
356
+ **Data:**
357
+ ```yaml
358
+ template_data:
359
+ my_template:
360
+ countries:
361
+ country_a:
362
+ apps:
363
+ - foo
364
+ - bar
365
+ - baz
366
+ country_b:
367
+ apps:
368
+ - foo
369
+ - bar
370
+ - baz
371
+ ```
372
+
373
+ **Validator method:**
374
+ ```ruby
375
+
376
+ def validate
377
+ within :template_data do # fails if template_data is absent
378
+
379
+ ensure_data_type Hash # fails is template_data is not a Hash
380
+
381
+ within :my_template do # fails if my_template is absent
382
+
383
+ ensure_data_type Hash # fails if my_template is not a Hash
384
+
385
+ within: countries do # fails if countries is absent
386
+ each_key do # iterates over countries
387
+ within :apps do # fails if any country is missing the apps key
388
+ ensure_data_type Array # fails if apps is not an Array
389
+ ensure_unique_values # fails if apps has duplicate values
390
+ end
391
+ end
392
+ end
393
+ end
394
+ end
395
+ end
396
+
397
+ ```
398
+
399
+ ## Generators
400
+ TBD
401
+
402
+ ## Development
403
+
404
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
405
+
406
+ ## To be done
407
+ * Error message if running apply without plan being present
408
+ * Tests
409
+ * Code documentation (YARD)
410
+ * Slack deployment?
411
+ * Disable Slack notifications for dry runs
412
+ * Enforcing validators?
413
+ * Prettier output
414
+ * Rubocop