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,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