terradactyl 0.13.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,61 @@
1
+ require 'bundler'
2
+ require 'open3'
3
+ require 'uri'
4
+ require 'bundler/gem_tasks'
5
+ require 'rspec/core/rake_task'
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ RSpec::Core::RakeTask.new(:doc) do |t|
10
+ t.rspec_opts = "--format doc"
11
+ end
12
+
13
+ task :default => :spec
14
+
15
+ BUILD_DIR = 'pkg'
16
+
17
+ def bundler
18
+ @bundler ||= Bundler::GemHelper.new
19
+ end
20
+
21
+ def execute(cmd)
22
+ Open3.popen2e(ENV, cmd) do |stdin, stdout_err, wait_thru|
23
+ puts $_ while stdout_err.gets
24
+ wait_thru.value.exitstatus
25
+ end
26
+ end
27
+
28
+ def name
29
+ bundler.gemspec.name
30
+ end
31
+
32
+ def version
33
+ bundler.gemspec.version
34
+ end
35
+
36
+ def allowed_push_host
37
+ bundler.gemspec.metadata['allowed_push_host'] || String.new
38
+ end
39
+
40
+ def gem_server
41
+ URI.parse(allowed_push_host).host
42
+ end
43
+
44
+ def resultant_gem
45
+ "#{BUILD_DIR}/#{name}-#{version}.gem"
46
+ end
47
+
48
+ desc "Lint gem"
49
+ task :lint do
50
+ exit(execute('bundle exec rubocop lib'))
51
+ end
52
+
53
+ desc "Build gem"
54
+ task :build do
55
+ bundler.build_gem
56
+ end
57
+
58
+ desc "Clean all builds"
59
+ task :clean do
60
+ FileUtils.rm_rf BUILD_DIR if File.exist? BUILD_DIR
61
+ end
@@ -0,0 +1 @@
1
+ resource "null_resource" "foo" {}
@@ -0,0 +1,3 @@
1
+ terradactyl:
2
+ terraform:
3
+ version: '0.11.14' # an explicit version
@@ -0,0 +1 @@
1
+ resource "null_resource" "bar" {}
@@ -0,0 +1,3 @@
1
+ terradactyl:
2
+ terraform:
3
+ version: 0.12.29
@@ -0,0 +1 @@
1
+ resource "null_resource" "baz" {}
@@ -0,0 +1,3 @@
1
+ terradactyl:
2
+ terraform:
3
+ version: '~> 0.13.5' # >= 0.13.5 && < 0.14.0
@@ -0,0 +1,3 @@
1
+ terradactyl:
2
+ terraform:
3
+ version: 0.12.29
@@ -0,0 +1 @@
1
+ resource "null_resource" "demo" {}
@@ -0,0 +1 @@
1
+ terradactyl:
data/exe/td ADDED
@@ -0,0 +1 @@
1
+ exe/terradactyl
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'terradactyl'
4
+
5
+ trap 'SIGINT' do
6
+ puts
7
+ puts 'Caught Interrupt. Exiting ...'
8
+ exit 1
9
+ end
10
+
11
+ Terradactyl::CLI.start
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'rake'
5
+ require 'open3'
6
+ require 'yaml'
7
+ require 'json'
8
+ require 'ostruct'
9
+ require 'digest'
10
+ require 'singleton'
11
+ require 'colorize'
12
+ require 'deepsort'
13
+ require 'deep_merge'
14
+ require 'terradactyl/terraform'
15
+
16
+ require_relative 'terradactyl/version'
17
+ require_relative 'terradactyl/config'
18
+ require_relative 'terradactyl/commands'
19
+ require_relative 'terradactyl/common'
20
+ require_relative 'terradactyl/stack'
21
+ require_relative 'terradactyl/stacks'
22
+ require_relative 'terradactyl/filters'
23
+ require_relative 'terradactyl/cli'
@@ -0,0 +1,335 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Terradactyl
4
+ # rubocop:disable Metrics/ClassLength
5
+ class CLI < Thor
6
+ include Common
7
+ def self.exit_on_failure?
8
+ true
9
+ end
10
+
11
+ def initialize(*args)
12
+ # Hook ensures abort on stack errors
13
+ at_exit { abort if Stacks.error? }
14
+ super
15
+ end
16
+
17
+ no_commands do
18
+ # Monkey-patch Thor internal method to break out of nested calls
19
+ def invoke_command(command, *args)
20
+ catch(:error) { super }
21
+ end
22
+
23
+ def validate_smartplan(stacks)
24
+ if stacks.empty?
25
+ print_message 'No Stacks Modified ...'
26
+ print_line 'Did you forget to `git add` your selected changes?'
27
+ end
28
+ stacks
29
+ end
30
+
31
+ def validate_planpr(stacks)
32
+ if stacks.empty?
33
+ print_message 'No Stacks Modified ...'
34
+ print_line 'Skipping plan ...'
35
+ end
36
+ stacks
37
+ end
38
+
39
+ def generate_report(report)
40
+ data_file = "#{config.base_folder}.audit.json"
41
+ print_warning "Writing Report: #{data_file} ..."
42
+ report[:error] = Stacks.error.map { |s| "#{config.base_folder}/#{s.name}" }.sort
43
+ File.write data_file, JSON.pretty_generate(report)
44
+ print_ok 'Done!'
45
+ end
46
+ end
47
+
48
+ #################################################################
49
+ # GENERIC TASKS
50
+ # * These tasks are used regularly against stacks, by name.
51
+ #################################################################
52
+
53
+ desc 'defaults', 'Print the compiled configuration'
54
+ def defaults
55
+ puts config.to_h.to_yaml
56
+ end
57
+
58
+ desc 'stacks', 'List the stacks'
59
+ def stacks
60
+ print_ok 'Stacks:'
61
+ Stacks.load.each do |name|
62
+ print_dot name.to_s
63
+ end
64
+ end
65
+
66
+ desc 'version', 'Print version'
67
+ def version
68
+ print_message format('version: %<semver>s', semver: Terradactyl::VERSION)
69
+ end
70
+
71
+ #################################################################
72
+ # SPECIAL TASKS
73
+ # * These tasks are related to Git state and PR planning ops.
74
+ # * Some are useful only in pipelines. These are hidden.
75
+ #################################################################
76
+
77
+ desc 'planpr', 'Plan stacks against origin/HEAD (used for PRs)', hide: true
78
+ def planpr
79
+ print_header 'SmartPlanning PR ...'
80
+ stacks = Stacks.load(filter: StacksPlanFilterGitDiffOriginBranch.new)
81
+ validate_planpr(stacks).each do |name|
82
+ clean(name)
83
+ init(name)
84
+ plan(name)
85
+ @stack = nil
86
+ end
87
+ end
88
+
89
+ desc 'smartplan', 'Plan any stacks that differ from Git HEAD'
90
+ def smartplan
91
+ print_header 'SmartPlanning Stacks ...'
92
+ stacks = Stacks.load(filter: StacksPlanFilterGitDiffHead.new)
93
+ validate_smartplan(stacks).each do |name|
94
+ clean(name)
95
+ init(name)
96
+ plan(name)
97
+ @stack = nil
98
+ end
99
+ end
100
+
101
+ desc 'smartapply', 'Apply any stacks that contain plan files', hide: true
102
+ def smartapply
103
+ print_header 'SmartApplying Stacks ...'
104
+ stacks = Stacks.load(filter: StacksApplyFilterPrePlanned.new)
105
+ print_warning 'No stacks contain plan files ...' unless stacks.any?
106
+ stacks.each do |name|
107
+ apply(name)
108
+ @stack = nil
109
+ end
110
+ print_message "Total Stacks Modified: #{stacks.size}"
111
+ end
112
+
113
+ desc 'smartrefresh', 'Refresh any stacks that contain plan files', hide: true
114
+ def smartrefresh
115
+ print_header 'SmartRefreshing Stacks ...'
116
+ stacks = Stacks.load(filter: StacksApplyFilterPrePlanned.new)
117
+ print_warning 'No stacks contain plan files ...' unless stacks.any?
118
+ stacks.each do |name|
119
+ refresh(name)
120
+ @stack = nil
121
+ end
122
+ print_message "Total Stacks Refreshed: #{stacks.size}"
123
+ end
124
+
125
+ #################################################################
126
+ # META-STACK TASKS
127
+ # * These tasks are used regularly against groups of stacks, but
128
+ # the `quickplan` task is an exception to this rule.
129
+ #################################################################
130
+
131
+ desc 'quickplan NAME', 'Clean, init and plan a stack, by name'
132
+ def quickplan(name)
133
+ print_header "Quick planning #{name} ..."
134
+ clean(name)
135
+ init(name)
136
+ plan(name)
137
+ end
138
+
139
+ desc 'clean-all', 'Clean all stacks'
140
+ def clean_all
141
+ print_header 'Cleaning ALL Stacks ...'
142
+ Stacks.load.each do |name|
143
+ clean(name)
144
+ @stack = nil
145
+ end
146
+ end
147
+
148
+ desc 'plan-all', 'Plan all stacks'
149
+ def plan_all
150
+ print_header 'Planning ALL Stacks ...'
151
+ Stacks.load.each do |name|
152
+ catch(:error) do
153
+ clean(name)
154
+ init(name)
155
+ plan(name)
156
+ end
157
+ @stack = nil
158
+ end
159
+ end
160
+
161
+ desc 'audit-all', 'Audit all stacks'
162
+ options report: :optional
163
+ method_option :report, type: :boolean
164
+ # rubocop:disable Metrics/AbcSize
165
+ def audit_all
166
+ report = { start: Time.now.to_json }
167
+ print_header 'Auditing ALL Stacks ...'
168
+ Stacks.load.each do |name|
169
+ catch(:error) do
170
+ clean(name)
171
+ init(name)
172
+ audit(name)
173
+ end
174
+ @stack = nil
175
+ end
176
+ report[:finish] = Time.now.to_json
177
+ if options[:report]
178
+ print_header 'Audit Report ...'
179
+ generate_report(report)
180
+ end
181
+ end
182
+ # rubocop:enable Metrics/AbcSize
183
+
184
+ desc 'validate-all', 'Validate all stacks'
185
+ def validate_all
186
+ print_header 'Validating ALL Stacks ...'
187
+ Stacks.load.each do |name|
188
+ catch(:error) do
189
+ clean(name)
190
+ init(name)
191
+ validate(name)
192
+ end
193
+ @stack = nil
194
+ end
195
+ end
196
+
197
+ #################################################################
198
+ # TARGETED STACK TASKS
199
+ # * These tasks are used regularly against stacks, by name.
200
+ #################################################################
201
+
202
+ desc 'lint NAME', 'Lint an individual stack, by name'
203
+ def lint(name)
204
+ @stack ||= Stack.new(name)
205
+ print_ok "Linting: #{@stack.name}"
206
+ if @stack.lint.zero?
207
+ print_ok "Formatting OK: #{@stack.name}"
208
+ else
209
+ Stacks.error!(@stack)
210
+ print_warning "Bad Formatting: #{@stack.name}"
211
+ end
212
+ end
213
+
214
+ desc 'fmt NAME', 'Format an individual stack, by name'
215
+ def fmt(name)
216
+ @stack ||= Stack.new(name)
217
+ print_warning "Formatting: #{@stack.name}"
218
+ if @stack.fmt.zero?
219
+ print_ok "Formatted: #{@stack.name}"
220
+ else
221
+ Stacks.error!(@stack)
222
+ print_crit "Formatting failed: #{@stack.name}"
223
+ end
224
+ end
225
+
226
+ desc 'init NAME', 'Init an individual stack, by name'
227
+ def init(name)
228
+ @stack ||= Stack.new(name)
229
+ print_ok "Initializing: #{@stack.name}"
230
+ if @stack.init.zero?
231
+ print_ok "Initialized: #{@stack.name}"
232
+ else
233
+ Stacks.error!(@stack)
234
+ print_crit "Initialization failed: #{@stack.name}"
235
+ throw :error
236
+ end
237
+ end
238
+
239
+ desc 'plan NAME', 'Plan an individual stack, by name'
240
+ # rubocop:disable Metrics/AbcSize
241
+ def plan(name)
242
+ @stack ||= Stack.new(name)
243
+ print_ok "Planning: #{@stack.name}"
244
+ case @stack.plan
245
+ when 0
246
+ print_ok "No changes: #{@stack.name}"
247
+ when 1
248
+ Stacks.error!(@stack)
249
+ print_crit "Plan failed: #{@stack.name}"
250
+ @stack.print_plan
251
+ throw :error
252
+ when 2
253
+ Stacks.dirty!(@stack)
254
+ print_warning "Changes detected: #{@stack.name}"
255
+ @stack.print_plan
256
+ else
257
+ raise
258
+ end
259
+ end
260
+ # rubocop:enable Metrics/AbcSize
261
+
262
+ desc 'audit NAME', 'Audit an individual stack, by name'
263
+ def audit(name)
264
+ plan(name)
265
+ if (@stack = Stacks.dirty?(name))
266
+ Stacks.error!(@stack)
267
+ print_crit "Dirty stack: #{@stack.name}"
268
+ end
269
+ end
270
+
271
+ desc 'validate NAME', 'Validate an individual stack, by name'
272
+ def validate(name)
273
+ @stack ||= Stack.new(name)
274
+ print_ok "Validating: #{@stack.name}"
275
+ if @stack.validate.zero?
276
+ print_ok "Validated: #{@stack.name}"
277
+ else
278
+ Stacks.error!(@stack)
279
+ print_crit "Validation failed: #{@stack.name}"
280
+ throw :error
281
+ end
282
+ end
283
+
284
+ desc 'clean NAME', 'Clean an individual stack, by name'
285
+ def clean(name)
286
+ @stack ||= Stack.new(name)
287
+ print_warning "Cleaning: #{@stack.name}"
288
+ @stack.clean
289
+ print_ok "Cleaned: #{@stack.name}"
290
+ end
291
+
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
299
+ def apply(name)
300
+ @stack ||= Stack.new(name)
301
+ print_warning "Applying: #{@stack.name}"
302
+ if @stack.apply.zero?
303
+ print_ok "Applied: #{@stack.name}"
304
+ else
305
+ Stacks.error!(@stack)
306
+ print_crit "Failed to apply changes: #{@stack.name}"
307
+ end
308
+ end
309
+
310
+ desc 'refresh NAME', 'Refresh state on an individual stack, by name', hide: true
311
+ def refresh(name)
312
+ @stack ||= Stack.new(name)
313
+ print_crit "Refreshing: #{@stack.name}"
314
+ if @stack.refresh.zero?
315
+ print_warning "Refreshed: #{@stack.name}"
316
+ else
317
+ Stacks.error!(@stack)
318
+ print_crit "Failed to refresh stack: #{@stack.name}"
319
+ end
320
+ end
321
+
322
+ desc 'destroy NAME', 'Destroy an individual stack, by name', hide: true
323
+ def destroy(name)
324
+ @stack ||= Stack.new(name)
325
+ print_crit "Destroying: #{@stack.name}"
326
+ if @stack.destroy.zero?
327
+ print_warning "Destroyed: #{@stack.name}"
328
+ else
329
+ Stacks.error!(@stack)
330
+ print_crit "Failed to apply changes: #{@stack.name}"
331
+ end
332
+ end
333
+ end
334
+ # rubocop:enable Metrics/ClassLength
335
+ end