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.
- checksums.yaml +7 -0
- data/.github/workflows/build-and-release.yml +58 -0
- data/.github/workflows/build-status.yml +26 -0
- data/.github/workflows/validate-pullrequest.yml +29 -0
- data/.gitignore +18 -0
- data/.rubocop.yml +18 -0
- data/CHANGELOG.md +244 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +8 -0
- data/README.md +325 -0
- data/Rakefile +61 -0
- data/examples/multi-tf-version/stacks/tfv11/example.tf +1 -0
- data/examples/multi-tf-version/stacks/tfv11/terradactyl.yaml +3 -0
- data/examples/multi-tf-version/stacks/tfv12/example.tf +1 -0
- data/examples/multi-tf-version/stacks/tfv12/terradactyl.yaml +3 -0
- data/examples/multi-tf-version/stacks/tfv13/example.tf +1 -0
- data/examples/multi-tf-version/stacks/tfv13/terradactyl.yaml +3 -0
- data/examples/multi-tf-version/terradactyl.yaml +3 -0
- data/examples/simple/stacks/demo/example.tf +1 -0
- data/examples/simple/terradactyl.yaml +1 -0
- data/exe/td +1 -0
- data/exe/terradactyl +11 -0
- data/lib/terradactyl.rb +23 -0
- data/lib/terradactyl/cli.rb +335 -0
- data/lib/terradactyl/commands.rb +161 -0
- data/lib/terradactyl/common.rb +85 -0
- data/lib/terradactyl/config.rb +167 -0
- data/lib/terradactyl/filters.rb +100 -0
- data/lib/terradactyl/stack.rb +127 -0
- data/lib/terradactyl/stacks.rb +90 -0
- data/lib/terradactyl/version.rb +5 -0
- data/terradactyl.gemspec +43 -0
- metadata +234 -0
data/Rakefile
ADDED
@@ -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 @@
|
|
1
|
+
resource "null_resource" "bar" {}
|
@@ -0,0 +1 @@
|
|
1
|
+
resource "null_resource" "baz" {}
|
@@ -0,0 +1 @@
|
|
1
|
+
resource "null_resource" "demo" {}
|
@@ -0,0 +1 @@
|
|
1
|
+
terradactyl:
|
data/exe/td
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
exe/terradactyl
|
data/exe/terradactyl
ADDED
data/lib/terradactyl.rb
ADDED
@@ -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
|