tfdsl 0.1.10 → 0.2.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 +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +24 -21
- data/CLAUDE.md +86 -0
- data/Gemfile +7 -3
- data/README.md +144 -35
- data/Rakefile +4 -5
- data/bin/console +3 -9
- data/lib/terraform_dsl/action.rb +7 -0
- data/lib/terraform_dsl/block.rb +64 -0
- data/lib/terraform_dsl/check.rb +7 -0
- data/lib/terraform_dsl/data_source.rb +7 -0
- data/lib/terraform_dsl/ephemeral.rb +7 -0
- data/lib/terraform_dsl/formatter.rb +26 -0
- data/lib/terraform_dsl/import.rb +8 -0
- data/lib/terraform_dsl/json_syntax.rb +33 -0
- data/lib/terraform_dsl/locals.rb +7 -0
- data/lib/terraform_dsl/moved.rb +8 -0
- data/lib/terraform_dsl/output.rb +7 -0
- data/lib/terraform_dsl/provider.rb +7 -0
- data/lib/terraform_dsl/removed.rb +8 -0
- data/lib/terraform_dsl/resource.rb +7 -0
- data/lib/terraform_dsl/stack.rb +40 -0
- data/lib/terraform_dsl/template.rb +74 -0
- data/lib/terraform_dsl/terraform.rb +7 -0
- data/lib/terraform_dsl/tfmodule.rb +7 -0
- data/lib/terraform_dsl/variable.rb +8 -0
- data/lib/terraform_dsl/version.rb +5 -0
- data/lib/terraform_dsl.rb +34 -0
- data/lib/tfdsl.rb +5 -29
- data/tfdsl.gemspec +9 -28
- metadata +32 -155
- data/.travis.yml +0 -8
- data/Gemfile.lock +0 -73
- data/lib/tfdsl/block.rb +0 -80
- data/lib/tfdsl/data_formatter.rb +0 -63
- data/lib/tfdsl/data_source.rb +0 -4
- data/lib/tfdsl/formatter.rb +0 -77
- data/lib/tfdsl/kind_translator.rb +0 -20
- data/lib/tfdsl/locals.rb +0 -4
- data/lib/tfdsl/output.rb +0 -4
- data/lib/tfdsl/provider.rb +0 -4
- data/lib/tfdsl/resource.rb +0 -4
- data/lib/tfdsl/stack.rb +0 -36
- data/lib/tfdsl/template.rb +0 -46
- data/lib/tfdsl/terraform.rb +0 -4
- data/lib/tfdsl/tfmodule.rb +0 -4
- data/lib/tfdsl/variable.rb +0 -4
- data/lib/tfdsl/version.rb +0 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 53dc500c87b4a8775477610f94c90265c74a8b0fb065855c1f6914dca2dcbbaa
|
|
4
|
+
data.tar.gz: 5d5c9b8187223be0b79da605dde02e987ac0b7316c37aefdf046f86ec82170c0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a77d38c0cf2c1546a8bbf3fe37bc3a8c7383bb46fec86b7797cac517bba012738a5b180586fc4dd3d3c657f339d4b7c2296e55268145ec4ef44c38f1d4549642
|
|
7
|
+
data.tar.gz: efb9ed85d08976da942ea83b32851a0ff6f05b6d608bf848d8395d1d506daf2ff23d78b6cdfbb3bd878e6a2edbcde40f6314360543df1a5f2c9c0697765d34a1
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
|
@@ -1,27 +1,30 @@
|
|
|
1
|
-
|
|
1
|
+
AllCops:
|
|
2
|
+
TargetRubyVersion: 2.7
|
|
3
|
+
NewCops: enable
|
|
4
|
+
SuggestExtensions: false
|
|
5
|
+
|
|
6
|
+
Style/Documentation:
|
|
2
7
|
Enabled: false
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
8
|
+
|
|
9
|
+
Layout/LineLength:
|
|
10
|
+
Max: 120
|
|
11
|
+
|
|
12
|
+
Metrics/BlockLength:
|
|
6
13
|
Exclude:
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- lib/**/formatter.rb
|
|
11
|
-
- lib/**/data_formatter.rb
|
|
12
|
-
- lib/**/block.rb
|
|
14
|
+
- '*.gemspec'
|
|
15
|
+
- test/**/*
|
|
16
|
+
|
|
13
17
|
Metrics/MethodLength:
|
|
14
|
-
Max: 35
|
|
15
|
-
Metrics/LineLength:
|
|
16
|
-
Max: 120
|
|
17
18
|
Exclude:
|
|
18
19
|
- test/**/*
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
|
|
21
|
+
Style/ClassAndModuleChildren:
|
|
22
|
+
Exclude:
|
|
23
|
+
- test/**/*
|
|
24
|
+
|
|
25
|
+
Metrics/AbcSize:
|
|
26
|
+
Max: 20
|
|
27
|
+
|
|
28
|
+
Metrics:
|
|
26
29
|
Exclude:
|
|
27
|
-
-
|
|
30
|
+
- test/**/*
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## What this is
|
|
6
|
+
|
|
7
|
+
`tfdsl` is a small Ruby gem (no runtime dependencies) that renders a Chef-like Ruby DSL as Terraform
|
|
8
|
+
HCL text. The output is meant to be byte-identical to what `terraform fmt` produces. The gem is
|
|
9
|
+
published as `tfdsl` (the name `terraform_dsl` on rubygems.org belongs to someone else); the code lives
|
|
10
|
+
under `lib/terraform_dsl/` in the `TerraformDSL` module, and `lib/tfdsl.rb` adds the legacy `TFDSL`
|
|
11
|
+
alias. Releases go out with `bundle exec rake release` after bumping `lib/terraform_dsl/version.rb`.
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bin/setup # bundle install
|
|
17
|
+
bundle exec rake # rubocop + full test suite (the default task, what CI runs)
|
|
18
|
+
bundle exec rake test # tests only
|
|
19
|
+
bundle exec rubocop # lint only (RuboCop 1.x, NewCops enabled, TargetRubyVersion 2.7)
|
|
20
|
+
bundle exec rubocop -a # autocorrect safe offenses
|
|
21
|
+
|
|
22
|
+
# Single test file / single test
|
|
23
|
+
bundle exec ruby -Ilib -Itest test/terraform_dsl/template_test.rb
|
|
24
|
+
bundle exec ruby -Ilib -Itest test/terraform_dsl/block_test.rb -n test_assignment_syntax_and_multiple_arguments
|
|
25
|
+
|
|
26
|
+
bin/console # IRB with the gem loaded
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
SimpleCov enforces 95% minimum line coverage; the suite fails below that.
|
|
30
|
+
|
|
31
|
+
`test/terraform_dsl/terraform_test.rb` shells out to the real `terraform` binary (`fmt -check`,
|
|
32
|
+
`init -backend=false`, `validate`) and is skipped when `terraform` is not on the PATH. CI installs
|
|
33
|
+
Terraform, so locally install it too if you touch rendering. The validate fixture deliberately uses only
|
|
34
|
+
the builtin `terraform_data` resource so no provider download is needed.
|
|
35
|
+
|
|
36
|
+
`Gemfile.lock` is intentionally not committed. Dev dependencies live in the `Gemfile`, not the gemspec.
|
|
37
|
+
|
|
38
|
+
## Architecture
|
|
39
|
+
|
|
40
|
+
HCL rendering flows `Stack -> Block -> Formatter -> Template`; JSON rendering flows
|
|
41
|
+
`Stack#to_h -> Block#to_h -> JsonSyntax`:
|
|
42
|
+
|
|
43
|
+
- **`Block`** (`lib/terraform_dsl/block.rb`) is the whole DSL. `method_missing` turns a call with
|
|
44
|
+
arguments into an entry in the ordered `@__attrs__` hash and a call with a Ruby block into a child
|
|
45
|
+
`Block` in `@__blocks__` (its name is `__type__`, extra args are `__labels__`). Attribute order is
|
|
46
|
+
declaration order. So that almost any HCL attribute name works, the class body **undefines every
|
|
47
|
+
inherited Object method except `KEEP`**, plus a few private Kernel methods (`format`, `test`,
|
|
48
|
+
`select`, `open`, `system`). Consequences: anything you need to call on a Block instance from
|
|
49
|
+
library or test code (e.g. `instance_of?` for Minitest's `assert_instance_of`) must be in `KEEP`;
|
|
50
|
+
and Block is a plain `Object`, not `BasicObject`, precisely so that `puts`, `raise` and top-level
|
|
51
|
+
helper methods still work inside DSL blocks.
|
|
52
|
+
- **Marker subclasses** (`Resource`, `Variable`, `Moved`, `Check`, `Action`, ...) each only set
|
|
53
|
+
`KEYWORD`. `Formatter.header` uses `block.class::KEYWORD` to decide top-level vs nested rendering:
|
|
54
|
+
top-level quotes `__type__` and labels after the keyword (`resource "aws_vpc" "main"`), nested
|
|
55
|
+
prints `__type__` bare and quotes only labels (`backend "local"`). Adding a Terraform top-level
|
|
56
|
+
block type = new 7-line subclass file + `require_relative` in `lib/terraform_dsl.rb` + an entry in
|
|
57
|
+
`Stack::BLOCKS`. Nested block kinds (`lifecycle`, `precondition`, `dynamic`, ...) need no code.
|
|
58
|
+
- **`Stack`** defines one method per `BLOCKS` entry and joins the blocks' `to_tf` with blank lines.
|
|
59
|
+
`tfmodule` exists because `module` is a Ruby keyword; `data`/`datasource` are aliases.
|
|
60
|
+
- **`Formatter`** (stateless module, depth passed as a parameter) renders a block's header,
|
|
61
|
+
attributes, children and closing brace with 2-space indentation.
|
|
62
|
+
- **`Template`** maps Ruby values to HCL: String -> quoted/escaped (heredoc `<<-EOT` when it ends
|
|
63
|
+
with `\n`), **Symbol -> raw expression** (this is how references, functions and types are
|
|
64
|
+
written), Numeric/true/false bare, nil -> `null`, Array/Hash recursive. A Hash argument is a map
|
|
65
|
+
*attribute* (`tags = {}`); a Ruby block is a nested *block*. `Template.assignments` reproduces
|
|
66
|
+
`terraform fmt`'s `=` alignment: consecutive single-line values share a column, a multi-line
|
|
67
|
+
`{`/`[` value breaks the run and is itself unaligned. Blank lines inside heredocs must stay empty
|
|
68
|
+
(Terraform ignores them when computing `<<-` indentation; padding them changes the value).
|
|
69
|
+
- **`JsonSyntax`** backs `Block#to_h`, `Stack#to_h` and `Stack#to_json` (Terraform JSON syntax, used by
|
|
70
|
+
consumers that write `.tf.json`). `JsonSyntax.value` wraps Symbols as `"${...}"`, except for attributes
|
|
71
|
+
listed in the block class's `STATIC_REFS` (`provider`, `depends_on`, `ignore_changes`, a variable's
|
|
72
|
+
`type`, `moved`/`import`/`removed` addresses), which JSON syntax requires as bare reference strings.
|
|
73
|
+
`terraform validate` rejects `${}` there, so extend `STATIC_REFS` when adding such an argument. `JsonSyntax.insert` nests a body under
|
|
74
|
+
`[keyword, type, *labels]` and turns a repeated path into an array, which is how repeated blocks
|
|
75
|
+
(`provider "aws"` twice, two `ingress` blocks) are expressed. `terraform_test.rb` validates a generated
|
|
76
|
+
`.tf.json` as well as the `.tf`.
|
|
77
|
+
|
|
78
|
+
## Testing conventions
|
|
79
|
+
|
|
80
|
+
- `test/terraform_dsl/stack_test.rb` holds one golden `EXPECTED` string covering every block kind,
|
|
81
|
+
exposed as `StackTest.stack` so `terraform_test.rb` can run `terraform fmt -check` on the same
|
|
82
|
+
output. Any change to rendering must keep that fixture passing `fmt -check`; update `EXPECTED` to
|
|
83
|
+
the canonical `terraform fmt` form, never the other way round.
|
|
84
|
+
- Fixtures must be valid Terraform 0.12+ (e.g. `provisioner`, not `provider`, inside a resource).
|
|
85
|
+
- `alias` is a Ruby keyword; in the DSL it is written `self.alias = 'west'` (the `=` suffix is stripped
|
|
86
|
+
by `method_missing`).
|
data/Gemfile
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
source 'https://rubygems.org'
|
|
4
4
|
|
|
5
|
-
# Specify your gem's dependencies in tfdsl.gemspec
|
|
6
5
|
gemspec
|
|
6
|
+
|
|
7
|
+
gem 'minitest', '~> 5.0'
|
|
8
|
+
gem 'rake', '~> 13.0'
|
|
9
|
+
gem 'rubocop', '~> 1.50'
|
|
10
|
+
gem 'simplecov', '~> 0.22'
|
data/README.md
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
[](https://travis-ci.org/tlopo-ruby/tfdsl)
|
|
3
|
-
[](https://codeclimate.com/github/tlopo-ruby/tfdsl)
|
|
4
|
-
[](https://coveralls.io/github/tlopo-ruby/tfdsl?branch=master)
|
|
1
|
+
# tfdsl
|
|
5
2
|
|
|
6
|
-
|
|
3
|
+
A Chef like Ruby DSL that renders Terraform (HCL) configuration.
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
HCL is a fine configuration language, but it is not a programming language. This gem lets
|
|
6
|
+
you describe Terraform blocks in Ruby, with loops, helper methods and everything else Ruby
|
|
7
|
+
gives you, and renders them as `terraform fmt` formatted HCL.
|
|
11
8
|
|
|
12
9
|
## Installation
|
|
13
10
|
|
|
@@ -23,68 +20,178 @@ And then execute:
|
|
|
23
20
|
|
|
24
21
|
Or install it yourself as:
|
|
25
22
|
|
|
26
|
-
$ gem install
|
|
23
|
+
$ gem install tfdsl
|
|
27
24
|
|
|
28
25
|
## Usage
|
|
29
26
|
|
|
30
|
-
A quick usage example
|
|
31
27
|
```ruby
|
|
32
|
-
require '
|
|
33
|
-
|
|
34
|
-
stack =
|
|
28
|
+
require 'terraform_dsl'
|
|
29
|
+
|
|
30
|
+
stack = TerraformDSL.stack do
|
|
35
31
|
resource 'aws_vpc', 'main' do
|
|
36
32
|
cidr_block '10.0.0.0/16'
|
|
37
33
|
instance_tenancy 'dedicated'
|
|
38
|
-
tags
|
|
39
|
-
|
|
34
|
+
tags Name: 'main'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
resource 'aws_subnet', 'public' do
|
|
38
|
+
vpc_id :'aws_vpc.main.id'
|
|
39
|
+
cidr_block :'cidrsubnet(aws_vpc.main.cidr_block, 8, 1)'
|
|
40
|
+
lifecycle do
|
|
41
|
+
precondition do
|
|
42
|
+
condition :'aws_vpc.main.enable_dns_support'
|
|
43
|
+
error_message 'DNS support must be enabled'
|
|
44
|
+
end
|
|
40
45
|
end
|
|
41
46
|
end
|
|
42
47
|
end
|
|
43
48
|
|
|
44
49
|
puts stack
|
|
45
50
|
```
|
|
46
|
-
|
|
51
|
+
|
|
52
|
+
The code above produces:
|
|
53
|
+
|
|
47
54
|
```hcl
|
|
48
55
|
resource "aws_vpc" "main" {
|
|
49
|
-
cidr_block
|
|
56
|
+
cidr_block = "10.0.0.0/16"
|
|
50
57
|
instance_tenancy = "dedicated"
|
|
51
|
-
|
|
52
58
|
tags = {
|
|
53
59
|
Name = "main"
|
|
54
60
|
}
|
|
55
61
|
}
|
|
62
|
+
|
|
63
|
+
resource "aws_subnet" "public" {
|
|
64
|
+
vpc_id = aws_vpc.main.id
|
|
65
|
+
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, 1)
|
|
66
|
+
lifecycle {
|
|
67
|
+
precondition {
|
|
68
|
+
condition = aws_vpc.main.enable_dns_support
|
|
69
|
+
error_message = "DNS support must be enabled"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Rules
|
|
76
|
+
|
|
77
|
+
Inside a block, every method call is either an **attribute** (when given arguments) or a
|
|
78
|
+
**nested block** (when given a Ruby block). Nested blocks can carry labels, so
|
|
79
|
+
`backend 'local' do ... end` renders `backend "local" { ... }`. Any nested block Terraform
|
|
80
|
+
knows works this way: `lifecycle`, `precondition`, `postcondition`, `validation`, `dynamic`,
|
|
81
|
+
`content`, `provisioner`, `connection`, `assert`, `required_providers`, and so on.
|
|
82
|
+
|
|
83
|
+
Ruby values map to HCL values:
|
|
84
|
+
|
|
85
|
+
| Ruby | HCL |
|
|
86
|
+
|------------------------|---------------------------------------|
|
|
87
|
+
| `'text'` | `"text"`, escaped as needed |
|
|
88
|
+
| `"multi\nline\n"` | `<<-EOT` heredoc (string ends in `\n`)|
|
|
89
|
+
| `:'aws_vpc.main.id'` | raw expression `aws_vpc.main.id` |
|
|
90
|
+
| `:string`, `:'list(string)'` | type expression |
|
|
91
|
+
| `1`, `1.5`, `true` | `1`, `1.5`, `true` |
|
|
92
|
+
| `nil` | `null` |
|
|
93
|
+
| `[80, 443]` | `[80, 443]` list |
|
|
94
|
+
| `{ Name: 'main' }` | `{ Name = "main" }` map attribute |
|
|
95
|
+
|
|
96
|
+
So a Hash argument gives a map **attribute** (`tags = { ... }`) while a Ruby block gives a
|
|
97
|
+
nested **block** (`lifecycle { ... }`). Symbols are written verbatim, which is how you
|
|
98
|
+
reference resources, variables, functions and types.
|
|
99
|
+
|
|
100
|
+
### Top level blocks
|
|
101
|
+
|
|
102
|
+
| DSL | HCL |
|
|
103
|
+
|--------------------------------------|-----------------------------------|
|
|
104
|
+
| `terraform do ... end` | `terraform { }` |
|
|
105
|
+
| `provider 'aws' do ... end` | `provider "aws" { }` |
|
|
106
|
+
| `variable 'name' do ... end` | `variable "name" { }` |
|
|
107
|
+
| `locals do ... end` | `locals { }` |
|
|
108
|
+
| `tfmodule 'vpc' do ... end` | `module "vpc" { }` |
|
|
109
|
+
| `data 'aws_ami', 'web' do ... end` | `data "aws_ami" "web" { }` |
|
|
110
|
+
| `resource 'aws_vpc', 'main' do ... end` | `resource "aws_vpc" "main" { }` |
|
|
111
|
+
| `output 'id' do ... end` | `output "id" { }` |
|
|
112
|
+
| `moved do ... end` | `moved { }` |
|
|
113
|
+
| `import do ... end` | `import { }` |
|
|
114
|
+
| `removed do ... end` | `removed { }` |
|
|
115
|
+
| `check 'health' do ... end` | `check "health" { }` |
|
|
116
|
+
| `ephemeral 'random_password', 'db' do ... end` | `ephemeral "random_password" "db" { }` |
|
|
117
|
+
| `action 'aws_lambda_invoke', 'x' do ... end` | `action "aws_lambda_invoke" "x" { }` |
|
|
118
|
+
|
|
119
|
+
`module` is a Ruby keyword, hence `tfmodule`. `datasource` is kept as an alias of `data`.
|
|
120
|
+
|
|
121
|
+
### Using Ruby inside blocks
|
|
122
|
+
|
|
123
|
+
Blocks are evaluated with `instance_eval`, so local variables, constants and top level
|
|
124
|
+
helper methods are all available inside them:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
def ami_for(region)
|
|
128
|
+
AMIS.fetch(region)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
stack = TerraformDSL.stack do
|
|
132
|
+
REGIONS.each do |region|
|
|
133
|
+
resource 'aws_instance', "web_#{region}" do
|
|
134
|
+
ami ami_for(region)
|
|
135
|
+
instance_type 'm1.small'
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
56
139
|
```
|
|
57
140
|
|
|
58
|
-
|
|
141
|
+
Nearly every name works as an attribute, including Ruby method names such as `hash`,
|
|
142
|
+
`method`, `format` or `test`. The exceptions are `class`, `inspect`, `to_s` and a few other
|
|
143
|
+
basics a Ruby object needs, plus private Kernel methods that you would expect to keep working
|
|
144
|
+
inside a block, such as `puts`, `p`, `raise`, `require`, `lambda` or `loop`. Methods of a
|
|
145
|
+
surrounding class are not reachable from inside a block because `self` is the block.
|
|
146
|
+
|
|
147
|
+
Ruby keywords such as `alias` cannot be called bare, so use the assignment form:
|
|
148
|
+
`self.alias = 'west'`.
|
|
59
149
|
|
|
60
|
-
|
|
61
|
-
|
|
150
|
+
`require 'tfdsl'` also works and defines `TFDSL` as an alias of `TerraformDSL`, for code written
|
|
151
|
+
against tfdsl 0.1.x. Note that 0.2.0 changed the output format: values are typed, references are
|
|
152
|
+
written as Symbols, and `to_json` renders bare strings for static references.
|
|
62
153
|
|
|
63
|
-
|
|
154
|
+
Each block class can also be used on its own:
|
|
64
155
|
|
|
65
|
-
In ruby
|
|
66
156
|
```ruby
|
|
67
|
-
|
|
68
|
-
...
|
|
69
|
-
end
|
|
157
|
+
TerraformDSL::Resource.new('aws_vpc', 'main') { cidr_block '10.0.0.0/16' }.to_tf
|
|
70
158
|
```
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
159
|
+
|
|
160
|
+
### JSON output
|
|
161
|
+
|
|
162
|
+
A stack also renders in
|
|
163
|
+
[Terraform JSON syntax](https://developer.hashicorp.com/terraform/language/syntax/json),
|
|
164
|
+
for writing a `.tf.json` file:
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
stack.to_json # pretty printed JSON string
|
|
168
|
+
stack.to_h # the same as a Hash
|
|
76
169
|
```
|
|
77
170
|
|
|
171
|
+
Symbols become `"${...}"` template strings, except where JSON syntax wants a plain reference
|
|
172
|
+
string: a variable's `type`, `provider`, `providers`, `depends_on`, `ignore_changes`,
|
|
173
|
+
`replace_triggered_by`, and the addresses in `moved`, `import` and `removed` blocks. Repeated
|
|
174
|
+
blocks, such as two `provider "aws"` blocks or two `ingress` blocks, become arrays.
|
|
78
175
|
|
|
79
176
|
## Development
|
|
80
177
|
|
|
81
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then
|
|
178
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then run `rake` to
|
|
179
|
+
run RuboCop and the tests. You can also run `bin/console` for an interactive prompt.
|
|
180
|
+
|
|
181
|
+
When the `terraform` binary is on the PATH the test suite also checks the generated
|
|
182
|
+
configuration with `terraform fmt -check` and `terraform validate`.
|
|
82
183
|
|
|
83
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To release a new
|
|
184
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new
|
|
185
|
+
version, update the version number in `version.rb`, and then run `bundle exec rake release`,
|
|
186
|
+
which will create a git tag for the version, push git commits and tags, and push the `.gem`
|
|
187
|
+
file to [rubygems.org](https://rubygems.org).
|
|
84
188
|
|
|
85
189
|
## Contributing
|
|
86
190
|
|
|
87
|
-
Bug reports and pull requests are welcome on GitHub at
|
|
191
|
+
Bug reports and pull requests are welcome on GitHub at
|
|
192
|
+
https://github.com/tlopo-ruby/terraform-dsl. This project is intended to be a safe, welcoming
|
|
193
|
+
space for collaboration, and contributors are expected to adhere to the
|
|
194
|
+
[Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
|
88
195
|
|
|
89
196
|
## License
|
|
90
197
|
|
|
@@ -92,4 +199,6 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
|
92
199
|
|
|
93
200
|
## Code of Conduct
|
|
94
201
|
|
|
95
|
-
Everyone interacting in the
|
|
202
|
+
Everyone interacting in the TerraformDSL project's codebases, issue trackers, chat rooms and
|
|
203
|
+
mailing lists is expected to follow the
|
|
204
|
+
[code of conduct](https://github.com/tlopo-ruby/terraform-dsl/blob/master/CODE_OF_CONDUCT.md).
|
data/Rakefile
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'bundler/gem_tasks'
|
|
2
4
|
require 'rake/testtask'
|
|
3
5
|
require 'rubocop/rake_task'
|
|
4
|
-
require 'coveralls/rake/task'
|
|
5
6
|
|
|
6
7
|
Rake::TestTask.new(:test) do |t|
|
|
7
8
|
t.libs << 'test'
|
|
@@ -9,8 +10,6 @@ Rake::TestTask.new(:test) do |t|
|
|
|
9
10
|
t.test_files = FileList['test/**/*_test.rb']
|
|
10
11
|
end
|
|
11
12
|
|
|
12
|
-
task default: %i[rubocop test]
|
|
13
|
-
task test_with_coveralls: [:default, 'coveralls:push']
|
|
14
|
-
|
|
15
13
|
RuboCop::RakeTask.new
|
|
16
|
-
|
|
14
|
+
|
|
15
|
+
task default: %i[rubocop test]
|
data/bin/console
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
require 'bundler/setup'
|
|
4
|
-
require '
|
|
5
|
-
|
|
6
|
-
# You can add fixtures and/or initialization code here to make experimenting
|
|
7
|
-
# with your gem easier. You can also use a different console, if you like.
|
|
8
|
-
|
|
9
|
-
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
10
|
-
# require "pry"
|
|
11
|
-
# Pry.start
|
|
12
|
-
|
|
5
|
+
require 'terraform_dsl'
|
|
13
6
|
require 'irb'
|
|
7
|
+
|
|
14
8
|
IRB.start(__FILE__)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TerraformDSL
|
|
4
|
+
# A terraform configuration block: https://developer.hashicorp.com/terraform/language/syntax/configuration
|
|
5
|
+
#
|
|
6
|
+
# Inside the DSL block every bare method call becomes either an attribute
|
|
7
|
+
# (when called with arguments) or a nested block (when called with a Ruby
|
|
8
|
+
# block). Subclasses set KEYWORD to the top-level block keyword.
|
|
9
|
+
class Block
|
|
10
|
+
KEYWORD = nil
|
|
11
|
+
# Arguments that take static references, which JSON syntax wants as plain
|
|
12
|
+
# strings rather than "${...}" expressions. Subclasses extend the list.
|
|
13
|
+
STATIC_REFS = %w[provider providers depends_on ignore_changes replace_triggered_by].freeze
|
|
14
|
+
|
|
15
|
+
# Ruby methods a Block keeps. Everything else Object provides is undefined
|
|
16
|
+
# so that it can be used as an attribute name (class, hash, method, ...).
|
|
17
|
+
KEEP = %i[instance_eval instance_exec class is_a? kind_of? instance_of? respond_to? object_id
|
|
18
|
+
__send__ __id__ equal? == != ! inspect to_s freeze frozen? nil?].freeze
|
|
19
|
+
(instance_methods - KEEP).each { |m| undef_method m }
|
|
20
|
+
# Private Kernel methods that are plausible attribute names.
|
|
21
|
+
%i[format test select open system].each { |m| undef_method m }
|
|
22
|
+
|
|
23
|
+
attr_accessor :__type__, :__labels__
|
|
24
|
+
attr_reader :__attrs__, :__blocks__
|
|
25
|
+
|
|
26
|
+
def initialize(type = nil, *labels, &block)
|
|
27
|
+
@__type__ = type
|
|
28
|
+
@__labels__ = labels
|
|
29
|
+
@__attrs__ = {}
|
|
30
|
+
@__blocks__ = []
|
|
31
|
+
instance_eval(&block) if block
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def method_missing(name, *args, &block)
|
|
35
|
+
name = name.to_s.chomp('=')
|
|
36
|
+
return (@__blocks__ << Block.new(name, *args, &block)).last if block
|
|
37
|
+
return @__attrs__[name] if args.empty?
|
|
38
|
+
|
|
39
|
+
@__attrs__[name] = args.size == 1 ? args.first : args
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def respond_to_missing?(_name, _include_private = false)
|
|
43
|
+
false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def to_tf
|
|
47
|
+
"#{Formatter.format(self)}\n"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The block body in Terraform JSON syntax: attributes plus nested blocks
|
|
51
|
+
# keyed by type and labels.
|
|
52
|
+
def to_h
|
|
53
|
+
body = @__attrs__.to_h do |name, value|
|
|
54
|
+
[name, JsonSyntax.value(value, static: self.class::STATIC_REFS.include?(name))]
|
|
55
|
+
end
|
|
56
|
+
@__blocks__.each { |child| JsonSyntax.insert(body, [child.__type__, *child.__labels__], child.to_h) }
|
|
57
|
+
body
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def to_s
|
|
61
|
+
to_tf
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TerraformDSL
|
|
4
|
+
# Renders a Block (and its nested blocks) as HCL text.
|
|
5
|
+
module Formatter
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def format(block, depth = 0)
|
|
9
|
+
pad = INDENT * depth
|
|
10
|
+
lines = ["#{pad}#{header(block)} {"]
|
|
11
|
+
attrs = block.__attrs__.map { |name, value| [name, Template.value(value, depth + 1)] }
|
|
12
|
+
lines.concat Template.assignments(attrs, pad + INDENT)
|
|
13
|
+
block.__blocks__.each { |child| lines << format(child, depth + 1) }
|
|
14
|
+
lines << "#{pad}}"
|
|
15
|
+
lines.join("\n")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Top level blocks: resource "aws_vpc" "main"
|
|
19
|
+
# Nested blocks: backend "local"
|
|
20
|
+
def header(block)
|
|
21
|
+
keyword = block.class::KEYWORD
|
|
22
|
+
name, *labels = keyword ? [keyword, block.__type__, *block.__labels__] : [block.__type__, *block.__labels__]
|
|
23
|
+
[name, *labels.compact.map { |label| Template.string(label.to_s) }].join(' ')
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TerraformDSL
|
|
4
|
+
# Helpers for rendering blocks in Terraform JSON syntax:
|
|
5
|
+
# https://developer.hashicorp.com/terraform/language/syntax/json
|
|
6
|
+
module JsonSyntax
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# Symbols (raw HCL expressions) become "${...}" template strings, or bare
|
|
10
|
+
# strings for static references such as depends_on or a variable's type.
|
|
11
|
+
def value(obj, static: false)
|
|
12
|
+
case obj
|
|
13
|
+
when Hash then obj.to_h { |k, v| [k.to_s, value(v, static: static)] }
|
|
14
|
+
when Array then obj.map { |v| value(v, static: static) }
|
|
15
|
+
when Symbol then static ? obj.to_s : "${#{obj}}"
|
|
16
|
+
else obj
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Stores body under the nested keys. A repeated key turns into an array of
|
|
21
|
+
# bodies, which is how JSON syntax represents repeated blocks.
|
|
22
|
+
def insert(hash, keys, body)
|
|
23
|
+
key, *rest = keys.map(&:to_s)
|
|
24
|
+
return insert(hash[key] ||= {}, rest, body) unless rest.empty?
|
|
25
|
+
|
|
26
|
+
hash[key] = case (existing = hash[key])
|
|
27
|
+
when nil then body
|
|
28
|
+
when Array then existing + [body]
|
|
29
|
+
else [existing, body]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|