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