terradactyl 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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