terradactyl 0.13.2 → 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
  SHA256:
3
- metadata.gz: f0bcbb7e3e6b90e0099b5e9619a58e804382de149a01b8125eb66b05a2124e7f
4
- data.tar.gz: 33653dc380bc7177bc32de91cf1e1c1d87a668d33a28e06d3cdd6c3bfaf77f72
3
+ metadata.gz: a7426368caebddb37fb99a029fe1868678b88e0fce83bcd57237ec6cd2b78436
4
+ data.tar.gz: 66d09971c29d06d319217b8bc9f42e1ad0af72e368e305a3903d140336f56e6d
5
5
  SHA512:
6
- metadata.gz: 88ab36f3422ecb3ce7e4898d94de40fdb745e725ecefbf9c01bda7b9324e68b049fa872072cada1d3ca942ed909798032be310cc8c01dc1d333de26e4c4abfce
7
- data.tar.gz: 5e20eb83847c02b02a5d92029135232906abd40ffb4327ce4fde71f7b038361e0da49b463a665298bf7d40683829edc3c8b8c2ddde36632be924299880e2281b
6
+ metadata.gz: 5782c0b917da8da15b6dab9dbe539de241b72b692ef52a9c9c328b34fd3a79999cac64ad0420db3fa4eaff9b208f5eddccdc201829164ff98f985d52e2a5afb2
7
+ data.tar.gz: 986f5d0e48a595ff800fa98cc542c7d337be11cf6bc22fdca6c1f4419938e88da4e54b64f2d8002abdc87d0c423417b2745f3c536c4c38f642476249f6c07766
data/.rubocop.yml CHANGED
@@ -10,6 +10,8 @@ Metrics/CyclomaticComplexity:
10
10
  Max: 10
11
11
  Metrics/ClassLength:
12
12
  Max: 115
13
+ Naming/ClassAndModuleCamelCase:
14
+ Enabled: false
13
15
  Naming/UncommunicativeMethodParamName:
14
16
  MinNameLength: 2
15
17
  Style/IfUnlessModifier:
data/CHANGELOG.md CHANGED
@@ -1,5 +1,61 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 1.0.0 (2021-06-09)
4
+
5
+ NEW FEATURES:
6
+
7
+ * add support for Terraform version `~> 1.0.0`
8
+
9
+ BUG FIXES:
10
+
11
+ * fix bad test matrix
12
+
13
+ ## 0.15.3 (2021-05-14)
14
+
15
+ BUG FIXES:
16
+
17
+ * fix `auto-approve` on `destroy` subcommand for Terraform version 0.15
18
+ * update all tests to use latest minor rev
19
+
20
+ ## 0.15.2 (2021-05-02)
21
+
22
+ NEW FEATURES:
23
+
24
+ * make all stacks upgradeable, regardless of binary version
25
+ * add warning after upgrading to Terrafrom version 0.13
26
+ * expanded testing
27
+
28
+ BUG FIXES:
29
+
30
+ * do not init backend during upgrade
31
+ * fix edge case on stacks with no `versions.tf` file
32
+
33
+ ## 0.15.1 (2021-04-28)
34
+
35
+ BUG FIXES:
36
+
37
+ * repair broken `upgrade` subcommand
38
+ * fix malformed HCL substitution
39
+ * fix regex match order of operations bug
40
+ * make the feature more robust
41
+ * add better feedback
42
+
43
+ ## 0.15.0 (2021-04-27)
44
+
45
+ NEW FEATURES:
46
+
47
+ * add support for Terraform version `0.14.x`
48
+ * add support for Terraform version `0.15.x`
49
+ * add new subcommand `install`
50
+ * generic component installation; presently only supports `terraform`
51
+ - permits on-demand installation of any available Terraform binary
52
+ * add new subcommand `upgrade`
53
+ * performs a Terraform upgrade of the target stack
54
+ * add support for native HCL Terraform contraints
55
+ * terradactyl will now search for Terraform version contraints in the following files: `settings.tf`, `versions.tf` and `backend.tf`
56
+ * update version expression parsing to match Terraform's own
57
+ * terradactyl version expression parsing should now operate the same way Terraform's own does, including support for version ranges
58
+
3
59
  ## 0.13.2 (2020-12-09)
4
60
 
5
61
  BUG FIXES:
data/README.md CHANGED
@@ -19,7 +19,7 @@ Terradactyl simplifies managing large heterogeneous Terraform monorepos by intro
19
19
 
20
20
  Requires Ruby 2.5 or greater.
21
21
 
22
- NOTE: Terraform sub-command operations are only supported between stable versions `~> 0.11.x` and `~> 0.13.x`.
22
+ NOTE: Terraform sub-command operations are only supported between stable versions `>= 0.11.x` to `~> 0.15.x`.
23
23
 
24
24
  ## Installation
25
25
 
@@ -83,6 +83,14 @@ The Terradactyl CLI is installed with a symlink so it may be called by its full
83
83
  $ terradactyl help
84
84
  $ td help
85
85
 
86
+ #### install terraform
87
+
88
+ ##### NOTE: You do not need to explicitly install Terraform, it will be downloaded and installed automatically, if your stack is configured to do so -- this is just to demonstrate on-demand installs ...
89
+
90
+ This is optional!
91
+
92
+ $ terradactyl install terraform --version=0.15.1
93
+
86
94
  #### quickplan a single stack
87
95
 
88
96
  You can specify the relative path to the stack OR the just the stack name. These two commands are equivalent:
@@ -110,6 +118,18 @@ When complete, you should have a JSON report that you can pass to other processe
110
118
 
111
119
  $ terradactyl smartapply
112
120
 
121
+ #### upgrade a legacy stack
122
+
123
+ Running this one time will upgrade it to next minor revision of Terraform ...
124
+
125
+ # Take me to Terraform v12
126
+ $ terradactyl upgrade stacks/tfv11
127
+
128
+ Running it again will bump it again!
129
+
130
+ # Take me to Terraform v13
131
+ $ terradactyl upgrade stacks/tfv11
132
+
113
133
  #### clean all the stacks
114
134
 
115
135
  $ terradactyl clean-all
@@ -122,7 +142,7 @@ See the [Configuration](#configuration) section for more info on how to control
122
142
 
123
143
  ## Operation
124
144
 
125
- NOTE: `terradactyl` (symlinked as `td`) ONLY operates in the root of your monorepo. In order to execute any sub-commands, your working directory must contain your project-level configuration file, otherwise you will receive this:
145
+ NOTE: `terradactyl` (symlinked as `td`) ONLY operates in the root of your monorepo. In order to execute any subcommands, your working directory must contain your project-level configuration file, otherwise you will receive this:
126
146
 
127
147
  FATAL: Could not load project file: `terradactyl.yaml`, No such file or directory @ rb_sysopen - terradactyl.yaml
128
148
 
@@ -135,9 +155,9 @@ Generally speaking, Terradactyl operates on the principle of **plan file** (`*.t
135
155
 
136
156
  In some cases, this might seem onerous, but it pays dividends in team workflow and CI/CD contexts.
137
157
 
138
- ### Supported sub-commands
158
+ ### Supported subcommands
139
159
 
140
- Terradactyl was created to facilitate the using Terraform in a CI environment. As such, some of the more exotic ad hoc user-focused sub-commands have not received any effort in integration. The following is a list of the supported Terraform sub-commands:
160
+ Terradactyl was created to facilitate the using Terraform in a CI environment. As such, some of the more exotic ad hoc user-focused subcommands have not received any effort in integration. The following is a list of the supported Terraform subcommands:
141
161
 
142
162
  * apply
143
163
  * destroy
@@ -147,6 +167,32 @@ Terradactyl was created to facilitate the using Terraform in a CI environment. A
147
167
  * refresh
148
168
  * validate
149
169
 
170
+ ### Special utility subcommands
171
+
172
+ Terradactyl add some unique utility commands that permit you to more readily manage your Terraform stacks.
173
+
174
+ #### install
175
+
176
+ Installs supporting components, namely Terraform itself...
177
+
178
+ # Install the latest terraform binary
179
+ terradactly install terraform
180
+
181
+ # Install pessimistic version
182
+ terradactyl install terraform --version="~> 0.13.0"
183
+
184
+ # Install ranged version
185
+ terradactyl install terraform --version=">= 0.14.5, <= 0.14.7"
186
+
187
+ # Install explicit version
188
+ terradactyl install terraform --version=0.15.0-beta2
189
+
190
+ #### upgrade
191
+
192
+ Upgrade abstracts the various Terraform subcommands related to upgrading individual stacks.
193
+
194
+ terradactyl upgrade <stack>
195
+
150
196
  ### Meta-commands
151
197
 
152
198
  Terradactyl provides a few useful meta-commands that can help you avoid repetitive multi-phase Terraform operations. Here are a few ...
@@ -166,7 +212,7 @@ Apply or Refresh _ANY_ stack containing a plan file.
166
212
 
167
213
  ### Getting Help
168
214
 
169
- For a list of available sub-commands do:
215
+ For a list of available subcommands do:
170
216
 
171
217
  $ terradactyl help
172
218
 
@@ -195,7 +241,7 @@ You can dump the compiled configuration for your project using the `defaults` su
195
241
  ```yaml
196
242
  terradactyl: <Object, Terradactyl config>
197
243
  base_folder: <String, the sub-directory for all your Terraform stacks, default=stacks>
198
- terraform: <Object, configuration to Terraform sub-commands and binaries>
244
+ terraform: <Object, configuration to Terraform subcommands and binaries>
199
245
  binary: <String, path to the Terraform binary you wish to use, default=nil>
200
246
  version: <String, explicit or implict Terraform version, default=nil>
201
247
  autoinstall: <Bool, perform automatic Terraform installations, default=true>
@@ -228,7 +274,7 @@ terradactyl: <Object, Terradactyl config>
228
274
 
229
275
  ### Terraform sub-command arguments
230
276
 
231
- Note that the config above contains config for Terraform sub-commands. for example:
277
+ Note that the config above contains config for Terraform subcommands. for example:
232
278
 
233
279
  ```yaml
234
280
  terradactyl:
@@ -243,12 +289,12 @@ Each of the keys in the `plan` object correspond to an argument passed to the `t
243
289
 
244
290
  terraform -lock=false -parallelism=5 -detailed-exitcode
245
291
 
246
- There are two conventions to keep in mind when configuring sub-commands:
292
+ There are two conventions to keep in mind when configuring subcommands:
247
293
 
248
294
  1. any sub-command option which toggles behaviour (i.e. `-detailed-exitcode`) requires a specific Boolean value of `true` OR `false`
249
295
  2. any sub-command option that is hyphenated (i.e. `-detailed-exitcode`) is set in the config using an **underscore** (i.e `detailed_exitcode`)
250
296
 
251
- If you need to tweak or augment any of the default arguments passed to any of the supported Terraform sub-commands, you can do so by adding them to the config.
297
+ If you need to tweak or augment any of the default arguments passed to any of the supported Terraform subcommands, you can do so by adding them to the config.
252
298
 
253
299
  Example:
254
300
 
@@ -260,7 +306,7 @@ terradactyl:
260
306
  backup: /tmp/tfbackup
261
307
  ```
262
308
 
263
- In addition, you can override the `echo` and `quiet` settings for any of the Terraform sub-commands:
309
+ In addition, you can override the `echo` and `quiet` settings for any of the Terraform subcommands:
264
310
 
265
311
  ```yaml
266
312
  terradactyl:
@@ -279,9 +325,29 @@ This can assist in debugging.
279
325
 
280
326
  ### Terraform version management
281
327
 
328
+ Terradactyl gives you some powerful ways to manage which versions of Terraform you support and where.
329
+
330
+ You may set **project-wide** OR **stack-explicit** versions, by using a config file (`terradactyl.yaml`, see [Configuration](#configuration)).
331
+
332
+ In addition, Terradactyl will also, search for a stack's desired Terraform version from one of **your HCL files**.
333
+
334
+ terraform {
335
+ required_version = "~> 0.13.0"
336
+ }
337
+
338
+ If a configuration like the one above is discovered in your stack's ...
339
+
340
+ * `settings.tf`
341
+ * `versions.tf`
342
+ * `backend.tf`
343
+
344
+ ... file, Terradactyl will download and use that version as required.
345
+
346
+ NOTE: These files are searched in the order you see above. If you specify `required_version` multiple times, the last one discovered is used.
347
+
282
348
  #### Explicit versions
283
349
 
284
- By default, Terradactyl will always use the **latest** stable version of Terraform. If you do not specify a version, you will always get the latest stable version of Terraform available.
350
+ By default, Terradactyl will always use the **latest** stable version of Terraform. So, if you don't specify a version, you will always get the latest stable version of Terraform available.
285
351
 
286
352
  But, as part of Terradactyl's configuration, you can specify a **project** Terraform version, making it the default for _your_ monorepo:
287
353
 
@@ -291,7 +357,9 @@ terradactyl:
291
357
  version: 0.12.29
292
358
  ```
293
359
 
294
- Still, because Terradactyl's configuration is hierarchic, in addition the default version you specify at the project level, **each stack** may also specify a different version of Terraform.
360
+ Still, because Terradactyl's configuration is hierarchic, you can also specify a version at the project level ...
361
+
362
+ Yes! **Each stack may use a different version of Terradactyl independent of any other.**
295
363
 
296
364
  See [examples/multi-tf-version](examples/multi-tf-version) for this setup.
297
365
 
@@ -1,3 +1,3 @@
1
1
  terradactyl:
2
2
  terraform:
3
- version: '0.11.14' # an explicit version
3
+ version: ~> 0.11.14
@@ -1 +1,3 @@
1
- resource "null_resource" "bar" {}
1
+ resource "null_resource" "bar" {
2
+ }
3
+
@@ -0,0 +1,3 @@
1
+ terraform {
2
+ required_version = "~> 0.12.0"
3
+ }
@@ -0,0 +1,8 @@
1
+ terraform {
2
+ required_providers {
3
+ null = {
4
+ source = "hashicorp/null"
5
+ }
6
+ }
7
+ required_version = "~> 0.13.0"
8
+ }
@@ -0,0 +1 @@
1
+ resource "null_resource" "baz" {}
@@ -0,0 +1,8 @@
1
+ terraform {
2
+ required_providers {
3
+ null = {
4
+ source = "hashicorp/null"
5
+ }
6
+ }
7
+ required_version = "~> 0.14.0"
8
+ }
@@ -0,0 +1 @@
1
+ resource "null_resource" "baz" {}
@@ -0,0 +1,8 @@
1
+ terraform {
2
+ required_providers {
3
+ null = {
4
+ source = "hashicorp/null"
5
+ }
6
+ }
7
+ required_version = "~> 0.15.0"
8
+ }
@@ -1,3 +1,3 @@
1
1
  terradactyl:
2
2
  terraform:
3
- version: 0.12.29
3
+ version: 0.15.1
@@ -1,5 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Fix for https://github.com/erikhuda/thor/issues/398
4
+ class Thor
5
+ module Shell
6
+ class Basic
7
+ def print_wrapped(message, options = {})
8
+ indent = (options[:indent] || 0).to_i
9
+ if indent.zero?
10
+ stdout.puts message
11
+ else
12
+ message.each_line do |message_line|
13
+ stdout.print ' ' * indent
14
+ stdout.puts message_line.chomp
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+
3
22
  module Terradactyl
4
23
  # rubocop:disable Metrics/ClassLength
5
24
  class CLI < Thor
@@ -14,6 +33,7 @@ module Terradactyl
14
33
  super
15
34
  end
16
35
 
36
+ # rubocop:disable Metrics/BlockLength
17
37
  no_commands do
18
38
  # Monkey-patch Thor internal method to break out of nested calls
19
39
  def invoke_command(command, *args)
@@ -43,7 +63,27 @@ module Terradactyl
43
63
  File.write data_file, JSON.pretty_generate(report)
44
64
  print_ok 'Done!'
45
65
  end
66
+
67
+ def terraform_latest
68
+ Terradactyl::Terraform::VersionManager.latest
69
+ end
70
+
71
+ def upgrade_stack(name)
72
+ @stack ||= Stack.new(name)
73
+ print_warning "Upgrading: #{@stack.name}"
74
+ if @stack.upgrade.zero?
75
+ print_ok "Upgraded: #{@stack.name}"
76
+ else
77
+ Stacks.error!(@stack)
78
+ print_crit "Failed to upgrade: #{@stack.name}"
79
+ throw :error
80
+ end
81
+ rescue Terradactyl::Terraform::VersionManager::VersionManagerError => e
82
+ print_crit "Error: #{e.message}"
83
+ exit 1
84
+ end
46
85
  end
86
+ # rubocop:enable Metrics/BlockLength
47
87
 
48
88
  #################################################################
49
89
  # GENERIC TASKS
@@ -128,6 +168,14 @@ module Terradactyl
128
168
  # the `quickplan` task is an exception to this rule.
129
169
  #################################################################
130
170
 
171
+ desc 'upgrade NAME', 'Cleans, inits, upgrades and formats an individual stack, by name'
172
+ def upgrade(name)
173
+ clean(name)
174
+ init(name, backend: false)
175
+ upgrade_stack(name)
176
+ fmt(name)
177
+ end
178
+
131
179
  desc 'quickplan NAME', 'Clean, init and plan a stack, by name'
132
180
  def quickplan(name)
133
181
  print_header "Quick planning #{name} ..."
@@ -224,8 +272,10 @@ module Terradactyl
224
272
  end
225
273
 
226
274
  desc 'init NAME', 'Init an individual stack, by name'
227
- def init(name)
275
+ def init(name, backend: true)
228
276
  @stack ||= Stack.new(name)
277
+ @stack.config.terraform.init.backend = backend
278
+
229
279
  print_ok "Initializing: #{@stack.name}"
230
280
  if @stack.init.zero?
231
281
  print_ok "Initialized: #{@stack.name}"
@@ -247,7 +297,7 @@ module Terradactyl
247
297
  when 1
248
298
  Stacks.error!(@stack)
249
299
  print_crit "Plan failed: #{@stack.name}"
250
- @stack.print_plan
300
+ @stack.print_error
251
301
  throw :error
252
302
  when 2
253
303
  Stacks.dirty!(@stack)
@@ -289,13 +339,7 @@ module Terradactyl
289
339
  print_ok "Cleaned: #{@stack.name}"
290
340
  end
291
341
 
292
- #################################################################
293
- # HIDDEN TARGETED STACK TASKS
294
- # * These tasks are destructive in nature and do not require
295
- # regular use.
296
- #################################################################
297
-
298
- desc 'apply NAME', 'Apply an individual stack, by name', hide: true
342
+ desc 'apply NAME', 'Apply an individual stack, by name'
299
343
  def apply(name)
300
344
  @stack ||= Stack.new(name)
301
345
  print_warning "Applying: #{@stack.name}"
@@ -307,7 +351,7 @@ module Terradactyl
307
351
  end
308
352
  end
309
353
 
310
- desc 'refresh NAME', 'Refresh state on an individual stack, by name', hide: true
354
+ desc 'refresh NAME', 'Refresh state on an individual stack, by name'
311
355
  def refresh(name)
312
356
  @stack ||= Stack.new(name)
313
357
  print_crit "Refreshing: #{@stack.name}"
@@ -319,7 +363,7 @@ module Terradactyl
319
363
  end
320
364
  end
321
365
 
322
- desc 'destroy NAME', 'Destroy an individual stack, by name', hide: true
366
+ desc 'destroy NAME', 'Destroy an individual stack, by name'
323
367
  def destroy(name)
324
368
  @stack ||= Stack.new(name)
325
369
  print_crit "Destroying: #{@stack.name}"
@@ -330,6 +374,53 @@ module Terradactyl
330
374
  print_crit "Failed to apply changes: #{@stack.name}"
331
375
  end
332
376
  end
377
+
378
+ #################################################################
379
+ # PROJECT-LEVEL UTILITY TASKS
380
+ # * These tasks are managing project-wide characteristics or
381
+ # invoking useful commands.
382
+ #################################################################
383
+
384
+ desc 'install COMPONENT', 'Installs specified component'
385
+ long_desc <<~LONGDESC
386
+ The `terradactyl install COMPONENT` subcommand perfoms installations of
387
+ prerequisties. At present, only Terraform binaries are supported.
388
+
389
+ Here are a few examples:
390
+
391
+ # Install latest
392
+ `terradactyl install terraform`
393
+
394
+ # Install pessimistic version
395
+ `terradactyl install terraform --version="~> 0.13.0"`
396
+
397
+ # Install ranged version
398
+ `terradactyl install terraform --version=">= 0.14.5, <= 0.14.7"`
399
+
400
+ # Install explicit version
401
+ `terradactyl install terraform --version=0.15.0-beta2`
402
+
403
+ LONGDESC
404
+ option :version, type: :string, default: 'latest'
405
+ # rubocop:disable Metrics/AbcSize
406
+ def install(component)
407
+ case component.to_sym
408
+ when :terraform
409
+ print_warning "Installing: #{component}, version: #{options[:version]}"
410
+ version = options[:version] == 'latest' ? terraform_latest : options[:version]
411
+ Terradactyl::Terraform::VersionManager.reset!
412
+ Terradactyl::Terraform::VersionManager.version = version
413
+ Terradactyl::Terraform::VersionManager.install
414
+ if Terradactyl::Terraform::VersionManager.binary
415
+ print_ok "Installed: #{Terradactyl::Terraform::VersionManager.binary}"
416
+ end
417
+ else
418
+ msg = %(Operation not supported -- I don't know how to install: #{component})
419
+ print_crit msg
420
+ exit 1
421
+ end
422
+ end
423
+ # rubocop:enable Metrics/AbcSize
333
424
  end
334
425
  # rubocop:enable Metrics/ClassLength
335
426
  end
@@ -1,13 +1,70 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Terradactyl
4
+ module Terraform
5
+ module Subcommands
6
+ module Upgrade
7
+ def defaults
8
+ {
9
+ 'yes' => false
10
+ }
11
+ end
12
+
13
+ def switches
14
+ %w[
15
+ yes
16
+ ]
17
+ end
18
+ end
19
+ end
20
+
21
+ module Commands
22
+ class Upgrade < Base
23
+ def execute
24
+ VersionManager.install
25
+ return 0 unless revision.upgradeable?
26
+
27
+ super
28
+ end
29
+
30
+ def next_version
31
+ @next_version ||= compute_upgrade
32
+ end
33
+
34
+ private
35
+
36
+ def revision
37
+ Terradactyl::Stack.revision
38
+ end
39
+
40
+ def compute_upgrade
41
+ maj, min, _rev = version.split('.')
42
+ resolution = VersionManager.resolve("~> #{maj}.#{min.to_i + 1}.0")
43
+ VersionManager.version = resolution
44
+ VersionManager.version
45
+ end
46
+
47
+ def subcmd
48
+ pre = version.slice(/\d+\.\d+/)
49
+ sig = self.class.name.split('::').last.downcase
50
+ sig == 'base' ? '' : "#{pre}#{sig}"
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ # rubocop:disable Metrics/ModuleLength
4
57
  module Commands
5
58
  class << self
6
59
  def extend_by_revision(tf_version, object)
7
60
  anon_module = revision_module
61
+ revision = revision_constant(tf_version)
8
62
 
9
63
  anon_module.include(self)
10
- anon_module.prepend(revision_constant(tf_version))
64
+ anon_module.prepend(revision)
65
+
66
+ object.class.define_singleton_method(:revision) { revision }
67
+ object.define_singleton_method(:revision) { revision }
11
68
 
12
69
  object.extend(anon_module)
13
70
  end
@@ -41,8 +98,8 @@ module Terradactyl
41
98
  end
42
99
 
43
100
  def revision_constant(tf_version)
44
- revision_name = ['Rev', *tf_version.split('.').take(2)].join
45
- const_get(revision_name)
101
+ revision = Terradactyl::Terraform.calc_revision(tf_version)
102
+ const_get(revision)
46
103
  end
47
104
  end
48
105
 
@@ -63,18 +120,17 @@ module Terradactyl
63
120
  options: options,
64
121
  capture: true)
65
122
 
66
- output = case captured.exitstatus
67
- when 0
68
- 'No changes. Infrastructure is up-to-date.'
69
- when 1
70
- captured.stderr
71
- when 2
72
- captured.stdout
73
- end
123
+ @plan_file_obj = load_plan_file
74
124
 
75
- @plan_file_obj = load_plan_file
76
- @plan_file_obj.plan_output = output
77
- @plan_file_obj.save
125
+ case captured.exitstatus
126
+ when 0
127
+ 'No changes. Infrastructure is up-to-date.'
128
+ when 1
129
+ @plan_file_obj.error_output = captured.stderr
130
+ when 2
131
+ @plan_file_obj.plan_output = captured.stdout
132
+ @plan_file_obj.save
133
+ end
78
134
 
79
135
  captured.exitstatus
80
136
  end
@@ -118,19 +174,149 @@ module Terradactyl
118
174
  end
119
175
  # rubocop:enable Metrics/AbcSize
120
176
 
177
+ def upgrade
178
+ perform_upgrade
179
+ end
180
+
121
181
  private
122
182
 
183
+ def versions_file
184
+ 'versions.tf'
185
+ end
186
+
187
+ def settings_files
188
+ Dir.glob('*.tf').each_with_object([]) do |file, memo|
189
+ File.open(file, 'r').each_line do |line|
190
+ if line.match(Common.required_versions_re)
191
+ memo << file
192
+ break
193
+ end
194
+ end
195
+ end
196
+ end
197
+
198
+ def sanitize_terraform_settings
199
+ settings_files.each do |file|
200
+ next if file == versions_file
201
+
202
+ write_stream = Tempfile.new(Common.tag)
203
+ File.open(file, 'r').each_line do |line|
204
+ write_stream.puts line unless line.match(Common.required_versions_re)
205
+ end
206
+ write_stream.close
207
+ FileUtils.mv(write_stream.path, file)
208
+ end
209
+ end
210
+
211
+ def update_required_version(upgrade_version)
212
+ if File.exist?(versions_file)
213
+ settings = File.read(versions_file)
214
+ if (req_version = settings.match(Common.required_versions_re))
215
+ substitution = %(#{req_version[:assignment]}"~> #{upgrade_version}")
216
+ settings.sub!(Common.required_versions_re, substitution)
217
+ end
218
+ else
219
+ # This is ugly, so let's explain ...
220
+ #
221
+ # When the versions.tf is present, but the stack is ~> 0.11.0, the
222
+ # `terraform 0.12upgrade` subcommand will FAIL because it uses the
223
+ # presence of this file as the sole gauge as to whether or not
224
+ # the stack can be upgraded. So, why not just use `-force`? Haha yes ...
225
+ #
226
+ # When the `versions.tf` file exists and the `-force` flag is passed,
227
+ # it will create a `versions-1.tf` file ... FML :facepalm:
228
+ #
229
+ # So, make the creation of a de facto versions.tf contingent upon the
230
+ # Terraform upgrade_version. Yay.
231
+ unless upgrade_version =~ /0\.12/
232
+ settings = <<~VERSIONS
233
+ terraform {
234
+ required_version = "~> #{upgrade_version}"
235
+ }
236
+ VERSIONS
237
+ end
238
+ end
239
+
240
+ File.write(versions_file, settings) if settings
241
+ end
242
+
243
+ def upgrade_notice
244
+ output = File.read('versions.tf')
245
+ insert = output.strip.split("\n").map { |l| " #{l}" }.join($INPUT_RECORD_SEPARATOR)
246
+
247
+ <<~NOTICE
248
+ This stack has been upgraded to version the described below and its
249
+ Terradactly config file (if it existed) has been removed.
250
+
251
+ #{insert}
252
+
253
+ NOTES:
254
+
255
+ • ALL Terraform version constraints are now specified in `versions.tf` using
256
+ the `required_version` directive.
257
+
258
+ • If your stack already contained one or more `required_version` directives,
259
+ they have been consolidated into a single directive in `versions.tf`.
260
+
261
+ • Terraform provider version contraints ARE NOT upgraded automatically. You
262
+ will need to edit these MANUALLY.
263
+
264
+ • Before proceeding. please perform a `terradactyl quickplan` on your stack
265
+ to ensure the upgraded stack functions as intended.
266
+ NOTICE
267
+ end
268
+
269
+ def upgrade_notice_rev013
270
+ <<~NOTICE
271
+ STOP UPGRADING!
272
+
273
+ Upgrading from Terraform 0.12 to 0.13 requires an apply to be performed
274
+ before continuing ...
275
+
276
+ DO NOT attempt to upgrade any further without first committing the existing
277
+ changes and seeing they are applied.
278
+
279
+ See the documentation here if you require more infomation ...
280
+
281
+ https://www.terraform.io/upgrade-guides/0-13.html
282
+ NOTICE
283
+ end
284
+
285
+ # rubocop:disable Metrics/AbcSize
286
+ def perform_upgrade
287
+ options = command_options.tap { |dat| dat.yes = true }
288
+ upgrade = Upgrade.new(dir_or_plan: nil, options: options)
289
+
290
+ sanitize_terraform_settings
291
+
292
+ update_required_version(upgrade.next_version)
293
+
294
+ if (result = upgrade.execute).zero?
295
+ update_required_version(upgrade.next_version)
296
+ FileUtils.rm_rf('terradactyl.yaml') if File.exist?('terradactyl.yaml')
297
+ end
298
+
299
+ print_content(upgrade_notice) if result.zero?
300
+
301
+ print_crit(upgrade_notice_rev013) if upgrade.next_version =~ /0\.13/
302
+
303
+ result
304
+ end
305
+ # rubocop:enable Metrics/AbcSize
306
+
123
307
  def load_plan_file
124
308
  Terraform::PlanFile.new(plan_path: plan_file, parser: parser)
125
309
  end
126
310
 
127
311
  module Rev011
128
- include Terraform::Commands
129
-
130
- def checklist
131
- Checklist.execute(dir_or_plan: nil, options: command_options)
312
+ class << self
313
+ def upgradeable?
314
+ true
315
+ end
132
316
  end
133
317
 
318
+ include Terraform::Commands
319
+
134
320
  private
135
321
 
136
322
  def parser
@@ -139,6 +325,12 @@ module Terradactyl
139
325
  end
140
326
 
141
327
  module Rev012
328
+ class << self
329
+ def upgradeable?
330
+ true
331
+ end
332
+ end
333
+
142
334
  include Terraform::Commands
143
335
 
144
336
  private
@@ -149,13 +341,68 @@ module Terradactyl
149
341
  end
150
342
 
151
343
  module Rev013
344
+ class << self
345
+ def upgradeable?
346
+ false
347
+ end
348
+ end
349
+
152
350
  include Terraform::Commands
153
351
 
154
352
  private
155
353
 
156
354
  def parser
157
- Terraform::Rev012::PlanFileParser
355
+ Terraform::Rev013::PlanFileParser
356
+ end
357
+ end
358
+
359
+ module Rev014
360
+ class << self
361
+ def upgradeable?
362
+ false
363
+ end
364
+ end
365
+
366
+ include Terraform::Commands
367
+
368
+ private
369
+
370
+ def parser
371
+ Terraform::Rev014::PlanFileParser
372
+ end
373
+ end
374
+
375
+ module Rev015
376
+ class << self
377
+ def upgradeable?
378
+ false
379
+ end
380
+ end
381
+
382
+ include Terraform::Commands
383
+
384
+ private
385
+
386
+ def parser
387
+ Terraform::Rev015::PlanFileParser
388
+ end
389
+ end
390
+
391
+ module Rev1_00
392
+ class << self
393
+ def upgradeable?
394
+ false
395
+ end
396
+ end
397
+
398
+ include Terraform::Commands
399
+
400
+ private
401
+
402
+ def parser
403
+ Terraform::Rev1_00::PlanFileParser
158
404
  end
159
405
  end
160
406
  end
407
+ # rubocop:enable Metrics/ModuleLength
161
408
  end
@@ -7,6 +7,14 @@ module Terradactyl
7
7
 
8
8
  module_function
9
9
 
10
+ def required_versions_re
11
+ /(?<assignment>(?:\n\s)*required_version\s+=\s+)(?<value>".*?")/m
12
+ end
13
+
14
+ def supported_revisions
15
+ Terradactyl::Commands.constants.select { |c| c =~ /Rev/ }.sort
16
+ end
17
+
10
18
  def config
11
19
  @config ||= ConfigProject.instance
12
20
  end
@@ -26,7 +26,7 @@ module Terradactyl
26
26
  input: false
27
27
  destroy:
28
28
  parallelism: 5
29
- force: true
29
+ auto_approve: true
30
30
  environment:
31
31
  TF_PLUGIN_CACHE_DIR: ~/.terraform.d/plugins
32
32
  misc:
@@ -132,6 +132,12 @@ module Terradactyl
132
132
  end
133
133
 
134
134
  class ConfigStack < ConfigApplication
135
+ TERRAFORM_SETTINGS_FILES = %w[
136
+ settings.tf
137
+ versions.tf
138
+ backend.tf
139
+ ].freeze
140
+
135
141
  attr_reader :stack_name, :stack_path, :base_folder
136
142
 
137
143
  def initialize(stack_name)
@@ -163,5 +169,50 @@ module Terradactyl
163
169
  def plan_path
164
170
  "#{stack_path}/#{plan_file}"
165
171
  end
172
+
173
+ def versions_file
174
+ "#{stack_path}/versions.tf"
175
+ end
176
+
177
+ private
178
+
179
+ def terraform_required_version
180
+ matches = TERRAFORM_SETTINGS_FILES.each_with_object([]) do |file, memo|
181
+ path = File.join(stack_path, file)
182
+ next unless File.exist?(path)
183
+
184
+ File.readlines(path).each do |line|
185
+ next if line =~ /(?:\s*#\s*)/
186
+
187
+ if (match = line.match(Common.required_versions_re))
188
+ memo << match
189
+ end
190
+ end
191
+ end
192
+
193
+ return {} unless matches.any?
194
+
195
+ {
196
+ 'terradactyl' => {
197
+ 'terraform' => {
198
+ 'version' => matches.last[:value].delete('"')
199
+ }
200
+ }
201
+ }
202
+ end
203
+
204
+ def load_overlay(config_file)
205
+ overlay = super(config_file)
206
+
207
+ unless overlay_specifies_version?(overlay)
208
+ overlay.merge!(terraform_required_version)
209
+ end
210
+
211
+ overlay
212
+ end
213
+
214
+ def overlay_specifies_version?(overlay)
215
+ overlay['terradactyl']&.fetch('terraform', {})&.fetch('version', nil)
216
+ end
166
217
  end
167
218
  end
@@ -48,6 +48,10 @@ module Terradactyl
48
48
  print_content(plan_file_obj.plan_output)
49
49
  end
50
50
 
51
+ def print_error
52
+ print_content(plan_file_obj.error_output)
53
+ end
54
+
51
55
  def plan_file_obj
52
56
  @plan_file_obj ||= load_plan_file
53
57
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Terradactyl
4
- VERSION = '0.13.2'
4
+ VERSION = '1.0.0'
5
5
  end
data/terradactyl.gemspec CHANGED
@@ -33,11 +33,10 @@ Gem::Specification.new do |spec|
33
33
  spec.add_dependency 'deep_merge', '~> 1.2'
34
34
  spec.add_dependency 'bundler', '>= 1.16'
35
35
  spec.add_dependency 'rake', '>= 10.0'
36
- spec.add_dependency 'terradactyl-terraform', '>= 0.3.1'
36
+ spec.add_dependency 'terradactyl-terraform', '>= 1.0.0'
37
37
 
38
38
  spec.add_development_dependency 'rspec', '~> 3.0'
39
39
  spec.add_development_dependency 'pry', '~> 0.12'
40
40
  spec.add_development_dependency 'pry-remote', '~> 0.1.8'
41
41
  spec.add_development_dependency 'rubocop', '~> 0.71.0'
42
42
  end
43
-
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: terradactyl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.2
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Warsing
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-12-09 00:00:00.000000000 Z
11
+ date: 2021-06-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -100,14 +100,14 @@ dependencies:
100
100
  requirements:
101
101
  - - ">="
102
102
  - !ruby/object:Gem::Version
103
- version: 0.3.1
103
+ version: 1.0.0
104
104
  type: :runtime
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - ">="
109
109
  - !ruby/object:Gem::Version
110
- version: 0.3.1
110
+ version: 1.0.0
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: rspec
113
113
  requirement: !ruby/object:Gem::Requirement
@@ -186,9 +186,13 @@ files:
186
186
  - examples/multi-tf-version/stacks/tfv11/example.tf
187
187
  - examples/multi-tf-version/stacks/tfv11/terradactyl.yaml
188
188
  - examples/multi-tf-version/stacks/tfv12/example.tf
189
- - examples/multi-tf-version/stacks/tfv12/terradactyl.yaml
189
+ - examples/multi-tf-version/stacks/tfv12/versions.tf
190
190
  - examples/multi-tf-version/stacks/tfv13/example.tf
191
- - examples/multi-tf-version/stacks/tfv13/terradactyl.yaml
191
+ - examples/multi-tf-version/stacks/tfv13/versions.tf
192
+ - examples/multi-tf-version/stacks/tfv14/example.tf
193
+ - examples/multi-tf-version/stacks/tfv14/versions.tf
194
+ - examples/multi-tf-version/stacks/tfv15/example.tf
195
+ - examples/multi-tf-version/stacks/tfv15/versions.tf
192
196
  - examples/multi-tf-version/terradactyl.yaml
193
197
  - examples/simple/stacks/demo/example.tf
194
198
  - examples/simple/terradactyl.yaml
@@ -227,7 +231,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
227
231
  - !ruby/object:Gem::Version
228
232
  version: '0'
229
233
  requirements: []
230
- rubygems_version: 3.1.4
234
+ rubygems_version: 3.1.6
231
235
  signing_key:
232
236
  specification_version: 4
233
237
  summary: Manage a Terraform monorepo
@@ -1,3 +0,0 @@
1
- terradactyl:
2
- terraform:
3
- version: 0.12.29
@@ -1,3 +0,0 @@
1
- terradactyl:
2
- terraform:
3
- version: '~> 0.13.5' # >= 0.13.5 && < 0.14.0