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
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Terradactyl
|
4
|
+
module Commands
|
5
|
+
class << self
|
6
|
+
def extend_by_revision(tf_version, object)
|
7
|
+
anon_module = revision_module
|
8
|
+
|
9
|
+
anon_module.include(self)
|
10
|
+
anon_module.prepend(revision_constant(tf_version))
|
11
|
+
|
12
|
+
object.extend(anon_module)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def revision_module
|
18
|
+
Module.new do
|
19
|
+
class << self
|
20
|
+
def extended(base)
|
21
|
+
terraform_methods.each { |method| decorate(base, method) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def decorate(base, method)
|
25
|
+
base.define_singleton_method(method) do |*args, &block|
|
26
|
+
setup_terraform
|
27
|
+
pushd(stack_path)
|
28
|
+
super(*args, &block)
|
29
|
+
ensure
|
30
|
+
popd
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def terraform_methods
|
37
|
+
public_instance_methods.reject { |m| m == :terraform_methods }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def revision_constant(tf_version)
|
44
|
+
revision_name = ['Rev', *tf_version.split('.').take(2)].join
|
45
|
+
const_get(revision_name)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
include Terraform::Commands
|
50
|
+
|
51
|
+
def init
|
52
|
+
Init.execute(dir_or_plan: nil, options: command_options)
|
53
|
+
end
|
54
|
+
|
55
|
+
def plan
|
56
|
+
options = command_options.tap do |dat|
|
57
|
+
dat.state = state_file
|
58
|
+
dat.out = plan_file
|
59
|
+
dat.no_color = true
|
60
|
+
end
|
61
|
+
|
62
|
+
captured = Plan.execute(dir_or_plan: nil,
|
63
|
+
options: options,
|
64
|
+
capture: true)
|
65
|
+
|
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
|
74
|
+
|
75
|
+
@plan_file_obj = load_plan_file
|
76
|
+
@plan_file_obj.plan_output = output
|
77
|
+
@plan_file_obj.save
|
78
|
+
|
79
|
+
captured.exitstatus
|
80
|
+
end
|
81
|
+
|
82
|
+
def apply
|
83
|
+
Apply.execute(dir_or_plan: plan_file, options: command_options)
|
84
|
+
end
|
85
|
+
|
86
|
+
def refresh
|
87
|
+
options = command_options.tap { |dat| dat.state = state_file }
|
88
|
+
Refresh.execute(dir_or_plan: nil, options: options)
|
89
|
+
end
|
90
|
+
|
91
|
+
def destroy
|
92
|
+
options = command_options.tap { |dat| dat.state = state_file }
|
93
|
+
Destroy.execute(dir_or_plan: nil, options: options)
|
94
|
+
end
|
95
|
+
|
96
|
+
def lint
|
97
|
+
options = command_options.tap { |dat| dat.check = true }
|
98
|
+
Fmt.execute(dir_or_plan: nil, options: options)
|
99
|
+
end
|
100
|
+
|
101
|
+
def fmt
|
102
|
+
Fmt.execute(dir_or_plan: nil, options: command_options)
|
103
|
+
end
|
104
|
+
|
105
|
+
def validate
|
106
|
+
Validate.execute(dir_or_plan: nil, options: command_options)
|
107
|
+
end
|
108
|
+
|
109
|
+
# rubocop:disable Metrics/AbcSize
|
110
|
+
def clean
|
111
|
+
removals = config.cleanup.match.map { |p| Dir.glob("**/#{p}") }
|
112
|
+
removals << `find . -type d -empty`.split if config.cleanup.empty
|
113
|
+
removals = removals.flatten.sort.uniq.each do |trash|
|
114
|
+
print_dot("Removing: #{trash}", :light_yellow)
|
115
|
+
FileUtils.rm_rf(trash)
|
116
|
+
end
|
117
|
+
puts unless removals.empty?
|
118
|
+
end
|
119
|
+
# rubocop:enable Metrics/AbcSize
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def load_plan_file
|
124
|
+
Terraform::PlanFile.new(plan_path: plan_file, parser: parser)
|
125
|
+
end
|
126
|
+
|
127
|
+
module Rev011
|
128
|
+
include Terraform::Commands
|
129
|
+
|
130
|
+
def checklist
|
131
|
+
Checklist.execute(dir_or_plan: nil, options: command_options)
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
def parser
|
137
|
+
Terraform::Rev011::PlanFileParser
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
module Rev012
|
142
|
+
include Terraform::Commands
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
def parser
|
147
|
+
Terraform::Rev012::PlanFileParser
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
module Rev013
|
152
|
+
include Terraform::Commands
|
153
|
+
|
154
|
+
private
|
155
|
+
|
156
|
+
def parser
|
157
|
+
Terraform::Rev012::PlanFileParser
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Terradactyl
|
4
|
+
module Common
|
5
|
+
COLUMN_WIDTH = 80
|
6
|
+
BORDER_CHAR = '#'
|
7
|
+
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def config
|
11
|
+
@config ||= ConfigProject.instance
|
12
|
+
end
|
13
|
+
|
14
|
+
def terraform_binary
|
15
|
+
config.terraform.binary || %(terraform)
|
16
|
+
end
|
17
|
+
|
18
|
+
def tag
|
19
|
+
'Terradactyl'
|
20
|
+
end
|
21
|
+
|
22
|
+
def border
|
23
|
+
BORDER_CHAR * COLUMN_WIDTH
|
24
|
+
end
|
25
|
+
|
26
|
+
def centre
|
27
|
+
COLUMN_WIDTH / 2
|
28
|
+
end
|
29
|
+
|
30
|
+
def dot_icon
|
31
|
+
config.misc.utf8 ? '•' : '*'
|
32
|
+
end
|
33
|
+
|
34
|
+
def stack_icon
|
35
|
+
config.misc.utf8 ? ' 𝓣 ' : ' ||| '
|
36
|
+
end
|
37
|
+
|
38
|
+
def print_crit(msg)
|
39
|
+
print_message(msg, :light_red)
|
40
|
+
end
|
41
|
+
|
42
|
+
def print_ok(msg)
|
43
|
+
print_message(msg, :light_green)
|
44
|
+
end
|
45
|
+
|
46
|
+
def print_warning(msg)
|
47
|
+
print_message(msg, :light_yellow)
|
48
|
+
end
|
49
|
+
|
50
|
+
def print_content(content)
|
51
|
+
content.split("\n").each do |line|
|
52
|
+
print_line line
|
53
|
+
end
|
54
|
+
puts
|
55
|
+
end
|
56
|
+
|
57
|
+
def print_dot(msg, color = :light_blue)
|
58
|
+
string = " #{dot_icon} #{msg}"
|
59
|
+
cputs(string, color)
|
60
|
+
end
|
61
|
+
|
62
|
+
def print_line(msg, color = :light_blue)
|
63
|
+
string = " #{msg}"
|
64
|
+
cputs(string, color)
|
65
|
+
end
|
66
|
+
|
67
|
+
def print_message(msg, color = :light_blue)
|
68
|
+
string = "#{stack_icon}[#{tag}] #{msg}"
|
69
|
+
cputs(string, color)
|
70
|
+
puts
|
71
|
+
end
|
72
|
+
|
73
|
+
def print_header(msg, color = :blue)
|
74
|
+
indent = centre + msg.size / 2 - 1
|
75
|
+
content = format("#%#{indent}s", "#{tag} | #{msg}")
|
76
|
+
string = [border, content, border].join("\n")
|
77
|
+
cputs(string, color)
|
78
|
+
puts
|
79
|
+
end
|
80
|
+
|
81
|
+
def cputs(msg, color)
|
82
|
+
puts config.misc.disable_color ? msg : msg.send(color.to_s)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Terradactyl
|
4
|
+
class ConfigApplication
|
5
|
+
CONFIG_DEFAULTS = <<~CONFIG_DEFAULTS
|
6
|
+
---
|
7
|
+
terradactyl:
|
8
|
+
base_folder: stacks
|
9
|
+
terraform:
|
10
|
+
binary:
|
11
|
+
version:
|
12
|
+
autoinstall: true
|
13
|
+
install_dir:
|
14
|
+
echo: false
|
15
|
+
quiet: true
|
16
|
+
init:
|
17
|
+
lock: false
|
18
|
+
force_copy: true
|
19
|
+
plan:
|
20
|
+
lock: false
|
21
|
+
parallelism: 5
|
22
|
+
detailed_exitcode: true
|
23
|
+
apply:
|
24
|
+
parallelism: 5
|
25
|
+
refresh:
|
26
|
+
input: false
|
27
|
+
destroy:
|
28
|
+
parallelism: 5
|
29
|
+
force: true
|
30
|
+
environment:
|
31
|
+
TF_PLUGIN_CACHE_DIR: ~/.terraform.d/plugins
|
32
|
+
misc:
|
33
|
+
utf8: true
|
34
|
+
disable_color: false
|
35
|
+
cleanup:
|
36
|
+
empty: true
|
37
|
+
match:
|
38
|
+
- "*.tfout"
|
39
|
+
- "*.tflock"
|
40
|
+
- "*.zip"
|
41
|
+
- ".terraform"
|
42
|
+
CONFIG_DEFAULTS
|
43
|
+
|
44
|
+
attr_reader :config_file, :terradactyl
|
45
|
+
|
46
|
+
def initialize(config_file = nil, defaults: nil)
|
47
|
+
@config_file = config_file
|
48
|
+
@defaults = load_defaults(defaults)
|
49
|
+
@overlay = load_overlay(config_file)
|
50
|
+
load_config
|
51
|
+
end
|
52
|
+
|
53
|
+
def reload
|
54
|
+
load_config
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_h
|
58
|
+
@config
|
59
|
+
end
|
60
|
+
alias to_hash to_h
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def load_config
|
65
|
+
@config = [
|
66
|
+
@defaults,
|
67
|
+
@overlay
|
68
|
+
].inject({}) do |memo, obj|
|
69
|
+
memo.deep_merge!(obj, overwrite_arrays: true)
|
70
|
+
Marshal.load(Marshal.dump(memo))
|
71
|
+
end
|
72
|
+
@terradactyl = structify(@config).terradactyl
|
73
|
+
configure_colorization
|
74
|
+
@terradactyl
|
75
|
+
end
|
76
|
+
|
77
|
+
def load_defaults(defaults)
|
78
|
+
defaults || YAML.safe_load(CONFIG_DEFAULTS)
|
79
|
+
end
|
80
|
+
|
81
|
+
def load_overlay(config_file)
|
82
|
+
YAML.load_file(config_file.to_s)
|
83
|
+
rescue Errno::ENOENT
|
84
|
+
load_empty
|
85
|
+
end
|
86
|
+
|
87
|
+
def load_empty
|
88
|
+
{ 'terradactyl' => {} }
|
89
|
+
end
|
90
|
+
|
91
|
+
def structify(hash)
|
92
|
+
OpenStruct.new(hash.each_with_object({}) do |(key, val), memo|
|
93
|
+
memo[key] = val.is_a?(Hash) ? structify(val) : val
|
94
|
+
end)
|
95
|
+
end
|
96
|
+
|
97
|
+
def configure_colorization
|
98
|
+
String.disable_colorization = terradactyl.misc.disable_color
|
99
|
+
end
|
100
|
+
|
101
|
+
def method_missing(sym, *args, &block)
|
102
|
+
terradactyl.send(sym.to_sym, *args, &block)
|
103
|
+
rescue NameError
|
104
|
+
super
|
105
|
+
end
|
106
|
+
|
107
|
+
def respond_to_missing?(sym, *args)
|
108
|
+
terradactyl.respond_to?(sym) || super
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
class ConfigProject < ConfigApplication
|
113
|
+
include Singleton
|
114
|
+
|
115
|
+
CONFIG_PROJECT_FILE = 'terradactyl.yaml'
|
116
|
+
|
117
|
+
def self.instance
|
118
|
+
@instance ||= new
|
119
|
+
end
|
120
|
+
|
121
|
+
private_class_method :new
|
122
|
+
|
123
|
+
def load_overlay(_overload)
|
124
|
+
YAML.load_file(config_file)
|
125
|
+
rescue Errno::ENOENT => e
|
126
|
+
abort "FATAL: Could not load project file: `#{config_file}`, #{e.message}"
|
127
|
+
end
|
128
|
+
|
129
|
+
def config_file
|
130
|
+
@config_file = CONFIG_PROJECT_FILE
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
class ConfigStack < ConfigApplication
|
135
|
+
attr_reader :stack_name, :stack_path, :base_folder
|
136
|
+
|
137
|
+
def initialize(stack_name)
|
138
|
+
@stack_name = stack_name
|
139
|
+
@project_config = ConfigProject.instance
|
140
|
+
@base_folder = @project_config.base_folder
|
141
|
+
@stack_path = "#{@base_folder}/#{@stack_name}"
|
142
|
+
@config_file = "#{@stack_path}/#{ConfigProject::CONFIG_PROJECT_FILE}"
|
143
|
+
@defaults = load_defaults(@project_config.to_h)
|
144
|
+
@overlay = load_overlay(@config_file)
|
145
|
+
load_config
|
146
|
+
end
|
147
|
+
|
148
|
+
alias name stack_name
|
149
|
+
alias path stack_path
|
150
|
+
|
151
|
+
def state_file
|
152
|
+
'terraform.tfstate'
|
153
|
+
end
|
154
|
+
|
155
|
+
def state_path
|
156
|
+
"#{stack_path}/terraform.tfstate"
|
157
|
+
end
|
158
|
+
|
159
|
+
def plan_file
|
160
|
+
"#{stack_name}.tfout"
|
161
|
+
end
|
162
|
+
|
163
|
+
def plan_path
|
164
|
+
"#{stack_path}/#{plan_file}"
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Terradactyl
|
4
|
+
class StacksPlanFilterDefault
|
5
|
+
include Common
|
6
|
+
|
7
|
+
def self.name
|
8
|
+
'default'
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.desc
|
12
|
+
'A list of all stacks in the basedir'
|
13
|
+
end
|
14
|
+
|
15
|
+
def git_cmd
|
16
|
+
`git ls-files .`
|
17
|
+
end
|
18
|
+
|
19
|
+
def base_dir
|
20
|
+
config.base_folder
|
21
|
+
end
|
22
|
+
|
23
|
+
def stack_name(path)
|
24
|
+
path.split('/')[1]
|
25
|
+
end
|
26
|
+
|
27
|
+
def sift(stacks)
|
28
|
+
stacks
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class StacksPlanFilterGitDiffHead < StacksPlanFilterDefault
|
33
|
+
def self.name
|
34
|
+
'diff-head'
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.desc
|
38
|
+
'A list of all stacks that differ from Git HEAD'
|
39
|
+
end
|
40
|
+
|
41
|
+
def git_cmd
|
42
|
+
`git --no-pager diff --name-only HEAD .`
|
43
|
+
end
|
44
|
+
|
45
|
+
def sift(stacks)
|
46
|
+
modified = git_cmd.split.each_with_object([]) do |path, memo|
|
47
|
+
memo << stack_name(path) if path =~ /#{base_dir}/
|
48
|
+
end
|
49
|
+
stacks & modified
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class StacksPlanFilterGitDiffFetchHead < StacksPlanFilterGitDiffHead
|
54
|
+
def self.name
|
55
|
+
'diff-fetch-head'
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.desc
|
59
|
+
'A list of all stacks that differ from Git FETCH_HEAD'
|
60
|
+
end
|
61
|
+
|
62
|
+
def git_cmd
|
63
|
+
`git --no-pager diff --name-only FETCH_HEAD ORIG_HEAD .`
|
64
|
+
rescue StandardError
|
65
|
+
String.new
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class StacksPlanFilterGitDiffOriginBranch < StacksPlanFilterGitDiffHead
|
70
|
+
def self.name
|
71
|
+
'diff-origin'
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.desc
|
75
|
+
'A list of all stacks that differ from Git origin/HEAD'
|
76
|
+
end
|
77
|
+
|
78
|
+
def current_branch
|
79
|
+
`git symbolic-ref -q --short HEAD`
|
80
|
+
end
|
81
|
+
|
82
|
+
def git_cmd
|
83
|
+
`git --no-pager diff --name-only origin/#{current_branch} .`
|
84
|
+
rescue StandardError
|
85
|
+
String.new
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class StacksApplyFilterDefault < StacksPlanFilterDefault
|
90
|
+
end
|
91
|
+
|
92
|
+
class StacksApplyFilterPrePlanned < StacksApplyFilterDefault
|
93
|
+
def sift(stacks)
|
94
|
+
targets = Dir.glob('**/*.tfout').each_with_object([]) do |path, memo|
|
95
|
+
memo << path.split('/')[1]
|
96
|
+
end
|
97
|
+
stacks & targets
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|