dh-proteus 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +7 -0
  5. data/Gemfile +4 -0
  6. data/Gemfile.lock +80 -0
  7. data/README.md +414 -0
  8. data/Rakefile +6 -0
  9. data/bin/console +14 -0
  10. data/bin/proteus +5 -0
  11. data/bin/proteus_testing +8 -0
  12. data/bin/setup +8 -0
  13. data/build.sh +28 -0
  14. data/dh-proteus.gemspec +50 -0
  15. data/lib/core_ext/hash.rb +14 -0
  16. data/lib/proteus.rb +9 -0
  17. data/lib/proteus/app.rb +86 -0
  18. data/lib/proteus/backend/backend.rb +41 -0
  19. data/lib/proteus/commands/apply.rb +53 -0
  20. data/lib/proteus/commands/clean.rb +22 -0
  21. data/lib/proteus/commands/destroy.rb +51 -0
  22. data/lib/proteus/commands/graph.rb +22 -0
  23. data/lib/proteus/commands/import.rb +75 -0
  24. data/lib/proteus/commands/move.rb +28 -0
  25. data/lib/proteus/commands/output.rb +29 -0
  26. data/lib/proteus/commands/plan.rb +55 -0
  27. data/lib/proteus/commands/remove.rb +71 -0
  28. data/lib/proteus/commands/render.rb +36 -0
  29. data/lib/proteus/commands/taint.rb +35 -0
  30. data/lib/proteus/common.rb +72 -0
  31. data/lib/proteus/config/config.rb +47 -0
  32. data/lib/proteus/context_management/context.rb +31 -0
  33. data/lib/proteus/context_management/helpers.rb +14 -0
  34. data/lib/proteus/generate.rb +18 -0
  35. data/lib/proteus/generators/context.rb +57 -0
  36. data/lib/proteus/generators/environment.rb +42 -0
  37. data/lib/proteus/generators/init.rb +40 -0
  38. data/lib/proteus/generators/module.rb +69 -0
  39. data/lib/proteus/generators/templates/config/config.yaml.erb +22 -0
  40. data/lib/proteus/generators/templates/context/main.tf.erb +7 -0
  41. data/lib/proteus/generators/templates/context/variables.tf.erb +3 -0
  42. data/lib/proteus/generators/templates/environment/terraform.tfvars.erb +6 -0
  43. data/lib/proteus/generators/templates/module/io.tf.erb +1 -0
  44. data/lib/proteus/generators/templates/module/module.tf.erb +1 -0
  45. data/lib/proteus/generators/templates/module/validator.rb.erb +9 -0
  46. data/lib/proteus/global_commands/validate.rb +45 -0
  47. data/lib/proteus/helpers.rb +91 -0
  48. data/lib/proteus/helpers/path_helpers.rb +90 -0
  49. data/lib/proteus/helpers/string_helpers.rb +13 -0
  50. data/lib/proteus/init.rb +16 -0
  51. data/lib/proteus/modules/manager.rb +53 -0
  52. data/lib/proteus/modules/terraform_module.rb +184 -0
  53. data/lib/proteus/templates/partial.rb +123 -0
  54. data/lib/proteus/templates/template_binding.rb +62 -0
  55. data/lib/proteus/validators/base_validator.rb +24 -0
  56. data/lib/proteus/validators/validation_dsl.rb +172 -0
  57. data/lib/proteus/validators/validation_error.rb +9 -0
  58. data/lib/proteus/validators/validation_helpers.rb +9 -0
  59. data/lib/proteus/version.rb +7 -0
  60. 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,7 @@
1
+ # Main manifest for context <%= @context %>
2
+ #
3
+ provider "aws" {
4
+ profile = "${var.aws_profile}"
5
+ region = "${var.region}"
6
+ }
7
+
@@ -0,0 +1,3 @@
1
+ variable "region" {}
2
+
3
+ variable "aws_profile" {}
@@ -0,0 +1,6 @@
1
+ # Context <%= @context %>
2
+
3
+ # must not contain underscores
4
+ environment = "<%= @environment.gsub('_', '-') %>"
5
+
6
+ region = "eu-west-1"
@@ -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,9 @@
1
+ module Proteus
2
+ module Validators
3
+ class <%= @module_name %>Validator < BaseValidator
4
+ def validate
5
+ # put your validations here
6
+ end
7
+ end
8
+ end
9
+ end
@@ -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