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