dh-proteus 0.1.3
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/.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
@@ -0,0 +1,57 @@
|
|
1
|
+
module Proteus
|
2
|
+
module Generators
|
3
|
+
module Context
|
4
|
+
def self.included(thor_class)
|
5
|
+
thor_class.class_eval do
|
6
|
+
|
7
|
+
desc "context CONTEXT", "Generate a new context"
|
8
|
+
def context(context)
|
9
|
+
|
10
|
+
if context !~ /^([a-z0-9])+(_{1}[a-z0-9]+)*$/
|
11
|
+
say "The name of your context has to be valid snake case. For example: 'foo_bar'", :red
|
12
|
+
exit 1
|
13
|
+
end
|
14
|
+
|
15
|
+
context_directory = File.join(context_path(context))
|
16
|
+
empty_directory(context_directory)
|
17
|
+
empty_directory(File.join(context_directory, 'environments'))
|
18
|
+
empty_directory(File.join(context_directory, 'modules'))
|
19
|
+
|
20
|
+
template_binding = Proteus::Templates::TemplateBinding.new(
|
21
|
+
context: context,
|
22
|
+
environment: nil,
|
23
|
+
module_name: nil
|
24
|
+
)
|
25
|
+
|
26
|
+
['main', 'variables'].each do |template_name|
|
27
|
+
template(
|
28
|
+
"context/#{template_name}.tf.erb",
|
29
|
+
File.join(
|
30
|
+
context_directory,
|
31
|
+
"#{template_name}.tf"
|
32
|
+
),
|
33
|
+
context: template_binding.get_binding
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
gitignore = File.join(destination_root, ".gitignore")
|
38
|
+
|
39
|
+
if File.file?(gitignore)
|
40
|
+
matches = File.readlines(gitignore).select do |line|
|
41
|
+
line.match(/\/contexts\/#{context}\/backend.tf/)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
if !File.file?(gitignore) || matches.none?
|
46
|
+
File.open(gitignore, 'a') do |file|
|
47
|
+
file.puts "/contexts/#{context}/backend.tf"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
say "Context #{context} created. Go ahead and create environments and modules.", :green
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Proteus
|
2
|
+
module Generators
|
3
|
+
module Environment
|
4
|
+
def self.included(thor_class)
|
5
|
+
thor_class.class_eval do
|
6
|
+
|
7
|
+
desc "environment ENVIRONMENT", "Generate configuration for a new environment / region"
|
8
|
+
option :context, type: :string, aliases: "-c", default: "default"
|
9
|
+
option :environment, type: :string, aliases: "-e", required: true
|
10
|
+
def environment
|
11
|
+
|
12
|
+
if options[:context] !~ /^([a-z0-9])+(_{1}[a-z0-9]+)*$/
|
13
|
+
say "The name of your context has to be valid snake case. For example: 'foo_bar'", :red
|
14
|
+
exit 1
|
15
|
+
end
|
16
|
+
|
17
|
+
if options[:environment] !~ /^([a-z0-9])+(_{1}[a-z0-9]+)*$/
|
18
|
+
say "The name of your environment has to be valid snake case. For example: 'foo_bar'", :red
|
19
|
+
exit 1
|
20
|
+
end
|
21
|
+
|
22
|
+
template_binding = Proteus::Templates::TemplateBinding.new(
|
23
|
+
environment: options[:environment],
|
24
|
+
context: options[:context],
|
25
|
+
module_name: nil
|
26
|
+
)
|
27
|
+
|
28
|
+
template(
|
29
|
+
'environment/terraform.tfvars.erb',
|
30
|
+
File.join(destination_root,
|
31
|
+
'contexts',
|
32
|
+
options[:context],
|
33
|
+
'environments',
|
34
|
+
"terraform.#{options[:environment]}.tfvars"),
|
35
|
+
context: template_binding.get_binding
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Proteus
|
2
|
+
module Generators
|
3
|
+
module Init
|
4
|
+
def self.included(thor_class)
|
5
|
+
thor_class.class_eval do
|
6
|
+
|
7
|
+
desc 'init', 'Initializes a new proteus root directory in the current working directory'
|
8
|
+
def init
|
9
|
+
|
10
|
+
say 'Creating config directory.', :green
|
11
|
+
empty_directory(config_dir)
|
12
|
+
|
13
|
+
say 'Creating sample config.', :green
|
14
|
+
template(
|
15
|
+
'config/config.yaml.erb',
|
16
|
+
File.join(
|
17
|
+
config_dir,
|
18
|
+
'config.yaml'
|
19
|
+
)
|
20
|
+
)
|
21
|
+
|
22
|
+
say 'Creating contexts directory.', :green
|
23
|
+
empty_directory(contexts_path)
|
24
|
+
|
25
|
+
confirm(question: 'Do you want to create a default proteus context?', color: :green, exit_on_no: false) do
|
26
|
+
invoke 'proteus:generate:context', ['default']
|
27
|
+
end
|
28
|
+
|
29
|
+
confirm(question: 'Do you want to create a sample proteus environment?', color: :green, exit_on_no: false) do
|
30
|
+
invoke 'proteus:generate:environment', [], context: 'default', environment: 'staging'
|
31
|
+
end
|
32
|
+
|
33
|
+
say "proteus root directory created.", :green
|
34
|
+
say "Please customize config/config.yaml. Then go ahead and create some modules.", :green
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Proteus
|
2
|
+
module Generators
|
3
|
+
module Module
|
4
|
+
def self.included(thor_class)
|
5
|
+
thor_class.class_eval do
|
6
|
+
|
7
|
+
desc "module", "Generate a new module"
|
8
|
+
option :context, type: :string, aliases: "-c", default: "default"
|
9
|
+
option :module_name, type: :string, aliases: "-m", required: true
|
10
|
+
def module
|
11
|
+
|
12
|
+
unless File.directory?(context_path(options[:context]))
|
13
|
+
say "The context #{options[:context]} does not exist.", :red
|
14
|
+
end
|
15
|
+
|
16
|
+
# check for valid module name
|
17
|
+
if options[:module_name] !~ /^([a-z0-9])+(_{1}[a-z0-9]+)*$/
|
18
|
+
say "The name of your module has to be valid snake case. For example: 'foo_bar'", :red
|
19
|
+
exit 1
|
20
|
+
end
|
21
|
+
|
22
|
+
template_binding = Proteus::Templates::TemplateBinding.new(
|
23
|
+
context: nil,
|
24
|
+
environment: nil,
|
25
|
+
module_name: options[:module_name],
|
26
|
+
)
|
27
|
+
|
28
|
+
module_directory = File.join(modules_path(options[:context]), options[:module_name])
|
29
|
+
empty_directory(module_directory)
|
30
|
+
|
31
|
+
template('module/io.tf.erb', File.join(module_directory, 'io.tf'), context: template_binding.get_binding)
|
32
|
+
template('module/module.tf.erb', File.join(module_directory, "#{options[:module_name]}.tf"), context: template_binding.get_binding)
|
33
|
+
|
34
|
+
|
35
|
+
confirm(question: "Will this module be included iteratively?", color: :green, exit_on_no: false) do
|
36
|
+
|
37
|
+
empty_directory(File.join(module_directory, 'config'))
|
38
|
+
empty_directory(File.join(module_directory, 'config', 'global_resources'))
|
39
|
+
empty_directory(File.join(module_directory, 'config', 'templates'))
|
40
|
+
|
41
|
+
confirm(question: "Do you want to implement validators for this module?", color: :green, exit_on_no: false) do
|
42
|
+
template_binding = Proteus::Templates::TemplateBinding.new(
|
43
|
+
context: nil,
|
44
|
+
environment: nil,
|
45
|
+
module_name: options[:module_name].split('_').collect(&:capitalize).join,
|
46
|
+
)
|
47
|
+
template('module/validator.rb.erb', File.join(module_directory, 'config', "validator.rb"), context: template_binding.get_binding)
|
48
|
+
end
|
49
|
+
|
50
|
+
say("Go ahead and create a configuration file for your environment in #{options[:module_name]}/config/your_environment.yaml", :green)
|
51
|
+
|
52
|
+
# add /contexts/context/module_name.tf to gitignore
|
53
|
+
gitignore = File.join(destination_root, ".gitignore")
|
54
|
+
matches = File.readlines(gitignore).select do |line|
|
55
|
+
line.match(/\/contexts\/#{options[:context]}\/#{options[:module_name]}/)
|
56
|
+
end
|
57
|
+
|
58
|
+
if matches.none?
|
59
|
+
File.open(gitignore, 'a') do |file|
|
60
|
+
file.puts "/contexts/#{options[:context]}/#{options[:module_name]}.tf"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end # #module
|
65
|
+
end # class_eval
|
66
|
+
end # self.included
|
67
|
+
end # module Module
|
68
|
+
end # module Generators
|
69
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
providers:
|
2
|
+
- name: 'aws'
|
3
|
+
environments:
|
4
|
+
- match: 'production' # must match the name of one of your proteus environments
|
5
|
+
profile: 'your_aws_profile'
|
6
|
+
- match: 'staging'
|
7
|
+
profile: 'your_aws_staging_profile'
|
8
|
+
|
9
|
+
slack_webhooks:
|
10
|
+
- match: 'production' # must match the name of one of your proteus environments
|
11
|
+
url: 'YOUR_PRODUCTION_SLACK_HOOK_URL'
|
12
|
+
- match: 'staging'
|
13
|
+
url: 'YOUR_STAGING_SLACK_HOOK_URL'
|
14
|
+
|
15
|
+
|
16
|
+
backend:
|
17
|
+
key_prefix: 'your-prefix-' # prefix for state files in the backend
|
18
|
+
# NOTE: Create this bucket in your AWS account and enable versioning
|
19
|
+
bucket:
|
20
|
+
name: 'your_bucket_name'
|
21
|
+
region: 'eu-west-1'
|
22
|
+
profile: 'your_aws_profile'
|
@@ -0,0 +1 @@
|
|
1
|
+
# place your input and output variables in this file
|
@@ -0,0 +1 @@
|
|
1
|
+
# Main config file for module "<%= @module_name %>"
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'proteus/modules/manager'
|
2
|
+
require 'proteus/helpers'
|
3
|
+
|
4
|
+
module Proteus
|
5
|
+
module GlobalCommands
|
6
|
+
module Validate
|
7
|
+
|
8
|
+
include Proteus::Helpers
|
9
|
+
|
10
|
+
def self.included(thor_class)
|
11
|
+
thor_class.class_eval do
|
12
|
+
desc "validate", "Renders templates for all contexts and environments"
|
13
|
+
|
14
|
+
long_desc <<-LONGDESC
|
15
|
+
Renders templates for all environments, reporting validation errors.
|
16
|
+
LONGDESC
|
17
|
+
def validate
|
18
|
+
self.class.contexts.each do |context|
|
19
|
+
context.environments.each do |environment|
|
20
|
+
module_manager = Proteus::Modules::Manager.new(context: context.name, environment: environment)
|
21
|
+
module_manager.render_modules
|
22
|
+
|
23
|
+
terraform = ENV.key?("TERRAFORM_BINARY") ? ENV["TERRAFORM_BINARY"] : "terraform"
|
24
|
+
|
25
|
+
validate_command = <<~VALIDATE_COMMAND
|
26
|
+
cd #{context_path(context.name)} \
|
27
|
+
&& #{terraform} init -backend=false \
|
28
|
+
&& #{terraform} get \
|
29
|
+
&& #{terraform} validate -var-file=#{var_file(context.name, environment)} -var 'aws_profile=needs_to_be_set'
|
30
|
+
VALIDATE_COMMAND
|
31
|
+
|
32
|
+
`#{validate_command.squeeze(' ')}`
|
33
|
+
|
34
|
+
say "Validated (context: #{context.name}, environment: #{environment}) #{"\u2714".encode('utf-8')}", :green
|
35
|
+
exit 1 if $?.exitstatus == 1
|
36
|
+
module_manager.clean
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'pty'
|
2
|
+
require 'json'
|
3
|
+
require 'net/http'
|
4
|
+
require 'proteus/helpers/path_helpers'
|
5
|
+
require 'date'
|
6
|
+
require 'etc'
|
7
|
+
|
8
|
+
module Proteus
|
9
|
+
module Helpers
|
10
|
+
include Proteus::Helpers::PathHelpers
|
11
|
+
|
12
|
+
ERROR = :red
|
13
|
+
DEFAULT = :green
|
14
|
+
|
15
|
+
def alert(message)
|
16
|
+
say "#{message}", :on_red
|
17
|
+
end
|
18
|
+
|
19
|
+
def limit(resources)
|
20
|
+
resources ? resources.inject("") {
|
21
|
+
|memo, resource| "#{memo} -target=#{resource}" } : ""
|
22
|
+
end
|
23
|
+
|
24
|
+
def confirm(question:, color:, exit_on_no: true, exit_code: 1)
|
25
|
+
if ask(question, color, limited_to: ["yes", "no"]) == "yes"
|
26
|
+
yield if block_given?
|
27
|
+
else
|
28
|
+
if exit_on_no
|
29
|
+
say "Exiting.", ERROR
|
30
|
+
exit exit_code
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def current_user
|
36
|
+
Etc.getpwnam(Etc.getlogin).gecos
|
37
|
+
end
|
38
|
+
|
39
|
+
def slack_webhook
|
40
|
+
hook = config[:slack_webhooks].find do |h|
|
41
|
+
environment =~ /#{h[:match]}/
|
42
|
+
end
|
43
|
+
|
44
|
+
hook ? hook[:url] : nil
|
45
|
+
end
|
46
|
+
|
47
|
+
def slack_notification(context:, environment:, message:)
|
48
|
+
webhook_url = slack_webhook
|
49
|
+
|
50
|
+
if webhook_url
|
51
|
+
time = DateTime.now.strftime("%Y/%m/%d - %H:%M")
|
52
|
+
slack_payload = {
|
53
|
+
text: "[#{context} - #{environment} - #{time}] #{current_user} #{message}"
|
54
|
+
}.to_json
|
55
|
+
|
56
|
+
uri = URI(webhook_url)
|
57
|
+
|
58
|
+
request = Net::HTTP::Post.new(uri)
|
59
|
+
request.body = slack_payload
|
60
|
+
|
61
|
+
request_options = {
|
62
|
+
use_ssl: uri.scheme == "https",
|
63
|
+
}
|
64
|
+
|
65
|
+
Net::HTTP::start(uri.hostname, uri.port, request_options) do |http|
|
66
|
+
http.request(request)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def syscall(command, dryrun: false, capture: false, suppress: false)
|
72
|
+
say "Executing: #{command}\n\n", :green
|
73
|
+
|
74
|
+
output = ""
|
75
|
+
|
76
|
+
if not dryrun
|
77
|
+
begin
|
78
|
+
PTY.spawn(command) do |stdout, stdin, pid|
|
79
|
+
stdout.each do |line|
|
80
|
+
output << line
|
81
|
+
puts line unless suppress
|
82
|
+
end
|
83
|
+
end
|
84
|
+
rescue Errno::EIO # GNU/Linux raises EIO.
|
85
|
+
nil
|
86
|
+
end
|
87
|
+
end
|
88
|
+
return output
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Proteus
|
2
|
+
module Helpers
|
3
|
+
module PathHelpers
|
4
|
+
include Thor::Shell
|
5
|
+
|
6
|
+
def root_path
|
7
|
+
if ARGV.size == 1 && ARGV[0] == 'init'
|
8
|
+
return Dir.pwd
|
9
|
+
end
|
10
|
+
|
11
|
+
path = ENV['PROTEUS_ROOT'].present? ? ENV['PROTEUS_ROOT'] : Dir.pwd
|
12
|
+
|
13
|
+
unless File.directory?(File.join(path, 'contexts'))
|
14
|
+
say "Can't find a contexts directory in #{path}. Exiting.", :red
|
15
|
+
exit 1
|
16
|
+
end
|
17
|
+
|
18
|
+
unless File.directory?(File.join(path, 'config'))
|
19
|
+
say "Can't find a config directory in #{path}. Exiting.", :red
|
20
|
+
exit 1
|
21
|
+
end
|
22
|
+
|
23
|
+
File.expand_path(path)
|
24
|
+
end
|
25
|
+
|
26
|
+
def config_dir
|
27
|
+
File.join(root_path, 'config')
|
28
|
+
end
|
29
|
+
|
30
|
+
def config_path
|
31
|
+
File.join(config_dir, 'config.yaml')
|
32
|
+
end
|
33
|
+
|
34
|
+
def contexts_path
|
35
|
+
File.join(root_path, 'contexts')
|
36
|
+
end
|
37
|
+
|
38
|
+
def context_path(context)
|
39
|
+
File.join(contexts_path, context)
|
40
|
+
end
|
41
|
+
|
42
|
+
def context_temp_directory(context)
|
43
|
+
File.join(context_path(context), '.tmp')
|
44
|
+
end
|
45
|
+
|
46
|
+
alias_method :context_root_path, :context_path
|
47
|
+
|
48
|
+
def environments_path(context)
|
49
|
+
File.join(context_path(context), 'environments')
|
50
|
+
end
|
51
|
+
|
52
|
+
def var_file(context, environment)
|
53
|
+
File.join(environments_path(context), "terraform.#{environment}.tfvars")
|
54
|
+
end
|
55
|
+
|
56
|
+
def plan_file(context, environment)
|
57
|
+
File.join(context_temp_directory(context), "terraform.#{environment}.tfplan")
|
58
|
+
end
|
59
|
+
|
60
|
+
def state_file(context, environment)
|
61
|
+
File.join(context_root_path(context), '.terraform', 'terraform.tfstate')
|
62
|
+
end
|
63
|
+
|
64
|
+
def module_path(context, module_name)
|
65
|
+
File.join(modules_path(context), module_name)
|
66
|
+
end
|
67
|
+
|
68
|
+
def modules_path(context)
|
69
|
+
File.join(context_path(context), 'modules')
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
def module_config_path(context, module_name)
|
74
|
+
File.join(module_path(context, module_name), 'config')
|
75
|
+
end
|
76
|
+
|
77
|
+
def module_templates_path(context, module_name)
|
78
|
+
File.join(module_path(context, module_name), 'config', 'templates')
|
79
|
+
end
|
80
|
+
|
81
|
+
def module_io_file(context, module_name)
|
82
|
+
File.read(File.join(module_path(context, module_name), 'io.tf'))
|
83
|
+
end
|
84
|
+
|
85
|
+
def module_hooks_path(context, module_name)
|
86
|
+
File.join(module_config_path(context, module_name), 'hooks')
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|