dh-proteus 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +80 -0
- data/README.md +414 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/proteus +5 -0
- data/bin/proteus_testing +8 -0
- data/bin/setup +8 -0
- data/build.sh +28 -0
- data/dh-proteus.gemspec +50 -0
- data/lib/core_ext/hash.rb +14 -0
- data/lib/proteus.rb +9 -0
- data/lib/proteus/app.rb +86 -0
- data/lib/proteus/backend/backend.rb +41 -0
- data/lib/proteus/commands/apply.rb +53 -0
- data/lib/proteus/commands/clean.rb +22 -0
- data/lib/proteus/commands/destroy.rb +51 -0
- data/lib/proteus/commands/graph.rb +22 -0
- data/lib/proteus/commands/import.rb +75 -0
- data/lib/proteus/commands/move.rb +28 -0
- data/lib/proteus/commands/output.rb +29 -0
- data/lib/proteus/commands/plan.rb +55 -0
- data/lib/proteus/commands/remove.rb +71 -0
- data/lib/proteus/commands/render.rb +36 -0
- data/lib/proteus/commands/taint.rb +35 -0
- data/lib/proteus/common.rb +72 -0
- data/lib/proteus/config/config.rb +47 -0
- data/lib/proteus/context_management/context.rb +31 -0
- data/lib/proteus/context_management/helpers.rb +14 -0
- data/lib/proteus/generate.rb +18 -0
- data/lib/proteus/generators/context.rb +57 -0
- data/lib/proteus/generators/environment.rb +42 -0
- data/lib/proteus/generators/init.rb +40 -0
- data/lib/proteus/generators/module.rb +69 -0
- data/lib/proteus/generators/templates/config/config.yaml.erb +22 -0
- data/lib/proteus/generators/templates/context/main.tf.erb +7 -0
- data/lib/proteus/generators/templates/context/variables.tf.erb +3 -0
- data/lib/proteus/generators/templates/environment/terraform.tfvars.erb +6 -0
- data/lib/proteus/generators/templates/module/io.tf.erb +1 -0
- data/lib/proteus/generators/templates/module/module.tf.erb +1 -0
- data/lib/proteus/generators/templates/module/validator.rb.erb +9 -0
- data/lib/proteus/global_commands/validate.rb +45 -0
- data/lib/proteus/helpers.rb +91 -0
- data/lib/proteus/helpers/path_helpers.rb +90 -0
- data/lib/proteus/helpers/string_helpers.rb +13 -0
- data/lib/proteus/init.rb +16 -0
- data/lib/proteus/modules/manager.rb +53 -0
- data/lib/proteus/modules/terraform_module.rb +184 -0
- data/lib/proteus/templates/partial.rb +123 -0
- data/lib/proteus/templates/template_binding.rb +62 -0
- data/lib/proteus/validators/base_validator.rb +24 -0
- data/lib/proteus/validators/validation_dsl.rb +172 -0
- data/lib/proteus/validators/validation_error.rb +9 -0
- data/lib/proteus/validators/validation_helpers.rb +9 -0
- data/lib/proteus/version.rb +7 -0
- metadata +260 -0
data/lib/proteus/init.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'proteus/generators/init'
|
2
|
+
|
3
|
+
module Proteus
|
4
|
+
class Init < Thor
|
5
|
+
include Thor::Actions
|
6
|
+
include Helpers
|
7
|
+
|
8
|
+
def self.source_root
|
9
|
+
File.expand_path(File.join(File.dirname(__FILE__), 'generators', 'templates'))
|
10
|
+
end
|
11
|
+
|
12
|
+
include Generators::Init
|
13
|
+
|
14
|
+
default_task :init
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'proteus/helpers/path_helpers'
|
2
|
+
require 'proteus/modules/terraform_module'
|
3
|
+
require 'proteus/backend/backend'
|
4
|
+
|
5
|
+
module Proteus
|
6
|
+
module Modules
|
7
|
+
|
8
|
+
class Manager
|
9
|
+
include Thor::Shell
|
10
|
+
include Proteus::Helpers::PathHelpers
|
11
|
+
|
12
|
+
def initialize(context:, environment:)
|
13
|
+
@context = context
|
14
|
+
@environment = environment
|
15
|
+
initialize_modules
|
16
|
+
end
|
17
|
+
|
18
|
+
def render_modules
|
19
|
+
@modules.map(&:process)
|
20
|
+
end
|
21
|
+
|
22
|
+
def clean
|
23
|
+
@modules.map(&:clean)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def initialize_modules
|
29
|
+
say "Initializing modules", :green
|
30
|
+
|
31
|
+
tfvars_content = File.read(File.join(environments_path(@context), "terraform.#{@environment}.tfvars"))
|
32
|
+
|
33
|
+
if tfvars_content.empty?
|
34
|
+
terraform_variables = []
|
35
|
+
else
|
36
|
+
terraform_variables = HCL::Checker.parse(tfvars_content).with_indifferent_access
|
37
|
+
end
|
38
|
+
|
39
|
+
@modules = []
|
40
|
+
|
41
|
+
Dir.glob("#{modules_path(@context)}/*").each do |directory|
|
42
|
+
@modules << TerraformModule.new(
|
43
|
+
name: File.basename(directory),
|
44
|
+
context: @context,
|
45
|
+
environment: @environment,
|
46
|
+
terraform_variables: terraform_variables
|
47
|
+
)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
require 'proteus/helpers/path_helpers'
|
2
|
+
require 'proteus/helpers/string_helpers'
|
3
|
+
require 'hcl/checker'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
module Proteus
|
7
|
+
module Modules
|
8
|
+
class TerraformModule
|
9
|
+
@@hooks = {}
|
10
|
+
|
11
|
+
include Thor::Shell
|
12
|
+
extend Thor::Shell
|
13
|
+
include Proteus::Helpers::PathHelpers
|
14
|
+
extend Proteus::Helpers::PathHelpers
|
15
|
+
include Proteus::Helpers::StringHelpers
|
16
|
+
|
17
|
+
attr_reader :name
|
18
|
+
|
19
|
+
def initialize(name:, context:, environment:, terraform_variables:)
|
20
|
+
@name = name
|
21
|
+
@context = context
|
22
|
+
@environment = environment
|
23
|
+
@terraform_variables = terraform_variables
|
24
|
+
@hook_variables = {}
|
25
|
+
end
|
26
|
+
|
27
|
+
def process
|
28
|
+
clean
|
29
|
+
run_hooks
|
30
|
+
load_data
|
31
|
+
return if !data?
|
32
|
+
validate
|
33
|
+
render
|
34
|
+
end
|
35
|
+
|
36
|
+
def clean
|
37
|
+
File.file?(manifest) ? FileUtils.rm(manifest) : nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.register_hook(module_name, hook)
|
41
|
+
@@hooks[module_name] ||= Array.new
|
42
|
+
@@hooks[module_name] << hook
|
43
|
+
end
|
44
|
+
|
45
|
+
def run_hooks
|
46
|
+
Dir.glob(File.join(module_hooks_path(@context, @name), '*')).each do |file|
|
47
|
+
require file
|
48
|
+
end
|
49
|
+
|
50
|
+
hooks_module = "Proteus::Modules::#{camel_case(@name)}::Hooks"
|
51
|
+
if Kernel.const_defined?(hooks_module)
|
52
|
+
say "Hooks present for module #{@name}", :green
|
53
|
+
Kernel.const_get(hooks_module).constants.each do |constant|
|
54
|
+
say "Found hook: #{constant}", :green
|
55
|
+
self.class.include(Kernel.const_get("#{hooks_module}::#{constant}"))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
if @@hooks.key?(@name)
|
60
|
+
@@hooks[@name].each do |hook|
|
61
|
+
hook_result = hook.call(@environment, @context, @name)
|
62
|
+
|
63
|
+
if hook_result.is_a?(Hash)
|
64
|
+
@hook_variables.merge!(hook_result)
|
65
|
+
end
|
66
|
+
|
67
|
+
@@hooks[@name].delete(hook)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
|
75
|
+
def load_data
|
76
|
+
@data = {}
|
77
|
+
|
78
|
+
if File.file?(config_file)
|
79
|
+
@data = YAML.load_file(config_file, {}).with_indifferent_access
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def data?
|
84
|
+
return false unless @data
|
85
|
+
@data.any?
|
86
|
+
end
|
87
|
+
|
88
|
+
def validate
|
89
|
+
if data? && File.file?(validator_file)
|
90
|
+
require validator_file
|
91
|
+
|
92
|
+
begin
|
93
|
+
validator_class = "Proteus::Validators::#{camel_case(@name)}Validator"
|
94
|
+
|
95
|
+
Kernel.const_get(validator_class).new(@data, @environment)
|
96
|
+
|
97
|
+
say "Ran #{camel_case(@name)}Validator for environment #{@environment} #{"\u2714".encode('utf-8')}", :green
|
98
|
+
rescue Proteus::Validators::ValidationError => validation_error
|
99
|
+
say "#{validator_class}: #{validation_error.message} [modules/#{@name}/config/#{@environment}.yaml] #{"\u2718".encode('utf-8')}", :red
|
100
|
+
exit 1
|
101
|
+
end
|
102
|
+
else
|
103
|
+
say "Module #{@name} has no validator.", :magenta
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def render
|
108
|
+
File.open(manifest, "w") do |manifest|
|
109
|
+
manifest << global_resources
|
110
|
+
|
111
|
+
@data.fetch('template_data', {}).each do |template, data|
|
112
|
+
manifest << render_template(File.join(module_templates_path(@context, @name), "#{template}.tf.erb"), data)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def render_template(template_file, data)
|
118
|
+
if File.file?(template_file)
|
119
|
+
template = File.read(template_file)
|
120
|
+
begin
|
121
|
+
return "#{Erubis::Eruby.new(template).result(template_binding(data).get_binding)}\n\n"
|
122
|
+
rescue Exception => e
|
123
|
+
say "Error in template: #{template_file}", :magenta
|
124
|
+
e.backtrace.each { |line| say line, :magenta }
|
125
|
+
say e.message, :magenta
|
126
|
+
exit 1
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def template_binding(data)
|
132
|
+
binding = Proteus::Templates::TemplateBinding.new(
|
133
|
+
context: @context,
|
134
|
+
environment: @environment,
|
135
|
+
module_name: @name,
|
136
|
+
data: data,
|
137
|
+
defaults: parse_defaults
|
138
|
+
)
|
139
|
+
|
140
|
+
binding.set(:terraform_variables, @terraform_variables)
|
141
|
+
|
142
|
+
@hook_variables.each do |key, value|
|
143
|
+
binding.set(key, value)
|
144
|
+
end
|
145
|
+
|
146
|
+
binding
|
147
|
+
end
|
148
|
+
|
149
|
+
def parse_defaults
|
150
|
+
defaults = []
|
151
|
+
|
152
|
+
return defaults unless File.file?(File.join(module_path(@context, @name), 'io.tf'))
|
153
|
+
HCL::Checker.parse(File.read(File.join(module_path(@context, @name), 'io.tf')))['variable'].each do |variable, values|
|
154
|
+
if values
|
155
|
+
defaults.push(variable) if values.has_key?('default')
|
156
|
+
end
|
157
|
+
end
|
158
|
+
return defaults
|
159
|
+
end
|
160
|
+
|
161
|
+
def global_resources
|
162
|
+
Dir.glob(File.join(module_config_path(@context, @name), 'global_resources/*')).inject("") do |memo, resource_file|
|
163
|
+
if resource_file =~ /tf\.erb/
|
164
|
+
"#{memo}\n#{render_template(resource_file, @data.dig('global_resources', File.basename(resource_file, '.tf.erb')))}"
|
165
|
+
else
|
166
|
+
"#{memo}#{File.read(resource_file)}\n"
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def config_file
|
172
|
+
File.join(module_config_path(@context, @name), "#{@environment}.yaml")
|
173
|
+
end
|
174
|
+
|
175
|
+
def validator_file
|
176
|
+
File.join(module_config_path(@context, @name), 'validator.rb')
|
177
|
+
end
|
178
|
+
|
179
|
+
def manifest
|
180
|
+
File.join(context_root_path(@context), "#{@name}.tf")
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'hcl/checker'
|
2
|
+
|
3
|
+
module Proteus
|
4
|
+
module Templates
|
5
|
+
class Partial < TemplateBinding
|
6
|
+
|
7
|
+
include Proteus::Helpers::PathHelpers
|
8
|
+
include Thor::Shell
|
9
|
+
|
10
|
+
def initialize(name:, context:, environment:, module_name:, data:, data_context: nil, force_rendering: true, deep_merge: false, terraform_variables:, scope_resources: nil)
|
11
|
+
@name = name
|
12
|
+
@context = context
|
13
|
+
@environment = environment
|
14
|
+
@module_name = module_name
|
15
|
+
@data = data
|
16
|
+
@data_context = data_context
|
17
|
+
@force_rendering = force_rendering
|
18
|
+
@deep_merge = deep_merge
|
19
|
+
@terraform_variables = terraform_variables
|
20
|
+
@scope_resources = scope_resources
|
21
|
+
|
22
|
+
@defaults = {}
|
23
|
+
set(@name.split('/').last, {})
|
24
|
+
|
25
|
+
if data.dig('partials', @name)
|
26
|
+
@partial_data_present = true
|
27
|
+
|
28
|
+
@data['partials'][@name].delete('render')
|
29
|
+
|
30
|
+
set(@name, @data['partials'][@name])
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
def render
|
36
|
+
if partial_data? || @force_rendering
|
37
|
+
defaults_file = File.join(module_templates_path(@context, @module_name), 'defaults', "#{@name}.yaml")
|
38
|
+
|
39
|
+
default_partial_path = File.join(module_templates_path(@context, @module_name), "_#{@name}.tf.erb")
|
40
|
+
sub_directory_partial_path = File.join(module_templates_path(@context, @module_name), "#{@name}.tf.erb")
|
41
|
+
|
42
|
+
if File.file?(default_partial_path)
|
43
|
+
partial_file = default_partial_path
|
44
|
+
else
|
45
|
+
partial_file = sub_directory_partial_path
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
if File.file?(partial_file)
|
50
|
+
partial_template = File.read(partial_file)
|
51
|
+
|
52
|
+
if File.file?(defaults_file)
|
53
|
+
@defaults = YAML.load_file(defaults_file, {}).with_indifferent_access
|
54
|
+
|
55
|
+
partial_data = instance_variable_get("@#{@name}")
|
56
|
+
|
57
|
+
if @deep_merge == false
|
58
|
+
@defaults.each do |key, value|
|
59
|
+
if !partial_data.key?(key)
|
60
|
+
partial_data[key] = value
|
61
|
+
end
|
62
|
+
end
|
63
|
+
elsif @deep_merge == :each
|
64
|
+
merged_data = []
|
65
|
+
|
66
|
+
partial_data.each do |item|
|
67
|
+
merged_data << @defaults.deep_merge(item)
|
68
|
+
end
|
69
|
+
|
70
|
+
instance_variable_set("@#{@name.split('/').last}", merged_data)
|
71
|
+
else
|
72
|
+
instance_variable_set("@#{@name.split('/').last}", @defaults.deep_merge(partial_data))
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
begin
|
78
|
+
if @scope_resources
|
79
|
+
return scope_resources(manifest: Erubis::Eruby.new(partial_template).result(get_binding), scope: @scope_resources)
|
80
|
+
else
|
81
|
+
return Erubis::Eruby.new(partial_template).result(get_binding)
|
82
|
+
end
|
83
|
+
rescue Exception => e
|
84
|
+
say "Error in partial: #{partial_file}", :magenta
|
85
|
+
e.backtrace.each { |line| say line, :magenta }
|
86
|
+
say e.message, :magenta
|
87
|
+
exit 1
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def partial_data?
|
94
|
+
@partial_data_present
|
95
|
+
end
|
96
|
+
|
97
|
+
def scope_resources(manifest:, scope:)
|
98
|
+
scoped = []
|
99
|
+
manifest.each_line do |line|
|
100
|
+
if matches = line.match(/resource( +)"(?<resource_type>[a-z0-9_]+)"( +)"(?<resource_name>[a-zA-Z0-9_\-]+)"( +)(\{)/)
|
101
|
+
say "MATCHED RESOURCE: #{line}", :green
|
102
|
+
matches = matches.named_captures.with_indifferent_access
|
103
|
+
scoped << "resource \"#{matches[:resource_type]}\" \"#{scope}_#{matches[:resource_name]}\" {"
|
104
|
+
say "CHANGED TO: #{scoped.last}", :green
|
105
|
+
elsif matches = line.match(/(?<pre>.*(\{|\())(?<data>data\.)?(?<resource_type>(?<provider>aws|kubernetes|helm|tls_private_key|null_resource)[a-z_0-9]*)\.(?<resource_name>[a-z0-9_]+)(?<attribute>(?<wildcard>.\*)?\.[a-z_]+)?(?<post>.*)?/)
|
106
|
+
matches = matches.named_captures.with_indifferent_access
|
107
|
+
|
108
|
+
if !matches[:data]
|
109
|
+
say "MATCHED REFERENCE: #{line}", :green
|
110
|
+
scoped << "#{matches[:pre]}#{matches[:resource_type]}.#{scope}_#{matches[:resource_name]}#{matches[:attribute]}#{matches[:post]}"
|
111
|
+
say "CHANGED TO: #{scoped.last}", :green
|
112
|
+
else
|
113
|
+
scoped << line
|
114
|
+
end
|
115
|
+
else
|
116
|
+
scoped << line
|
117
|
+
end
|
118
|
+
end
|
119
|
+
scoped.map!(&:chomp).join("\n")
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Proteus
|
2
|
+
module Templates
|
3
|
+
class TemplateBinding
|
4
|
+
|
5
|
+
include Proteus::Helpers::PathHelpers
|
6
|
+
include Proteus::Helpers::StringHelpers
|
7
|
+
|
8
|
+
def initialize(context:, environment:, module_name:, data: {}, defaults: [])
|
9
|
+
@context = context
|
10
|
+
@environment = environment
|
11
|
+
@module_name = module_name
|
12
|
+
|
13
|
+
data.each do |key, value|
|
14
|
+
set(key, value)
|
15
|
+
end
|
16
|
+
@defaults = defaults
|
17
|
+
end
|
18
|
+
|
19
|
+
def set(name, value)
|
20
|
+
instance_variable_set("@#{name}", value)
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_binding
|
24
|
+
binding
|
25
|
+
end
|
26
|
+
|
27
|
+
def render_defaults(context, demo: false)
|
28
|
+
@defaults.inject("") do |memo, default|
|
29
|
+
if context.has_key?(default)
|
30
|
+
memo << "#{demo ? "# " : ""}#{default} = \"#{context[default]}\"\n"
|
31
|
+
else
|
32
|
+
memo
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# return output of partial template
|
38
|
+
# name: symbol (template_name)
|
39
|
+
def render_partial(name:, data:, data_context: nil, force_rendering: true, deep_merge: false, scope_resources: nil)
|
40
|
+
partial = Partial.new(
|
41
|
+
name: name.to_s,
|
42
|
+
context: @context,
|
43
|
+
environment: @environment,
|
44
|
+
module_name: @module_name,
|
45
|
+
data: data,
|
46
|
+
data_context: data_context,
|
47
|
+
force_rendering: force_rendering,
|
48
|
+
deep_merge: deep_merge,
|
49
|
+
terraform_variables: @terraform_variables,
|
50
|
+
scope_resources: scope_resources
|
51
|
+
)
|
52
|
+
|
53
|
+
partial.render
|
54
|
+
end
|
55
|
+
|
56
|
+
def default_true(context, key)
|
57
|
+
return context.has_key?(key) ? context[key] : true
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|