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
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "proteus"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/proteus ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'proteus'
4
+
5
+ Proteus::App.start(ARGV)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
4
+
5
+ require 'pry'
6
+ require 'proteus'
7
+
8
+ Proteus::App.start(ARGV)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/build.sh ADDED
@@ -0,0 +1,28 @@
1
+ #!/bin/bash
2
+
3
+ set -eo pipefail
4
+
5
+ AWS_PROFILE=logistics-production
6
+ BUCKET=production-eu-gems
7
+ NEXUS_REPOSITORY=https://nexus.usehurrier.com/repository/proteus/
8
+ VERSION=$(ruby lib/proteus/version.rb)
9
+
10
+
11
+ if ! gem list --local | grep nexus; then
12
+ echo Please install the nexus gem in order to push to the gem repository: \"gem install nexus\"
13
+ exit 1
14
+ fi
15
+
16
+ echo Building version $VERSION
17
+
18
+ mkdir -p ./release/gems
19
+
20
+ rake build
21
+
22
+ cp ./pkg/*.gem ./release/gems/
23
+
24
+ gem generate_index --directory ./release/
25
+
26
+ aws --profile $AWS_PROFILE s3 sync ./release s3://$BUCKET
27
+
28
+ gem nexus release/gems/dh-proteus-$VERSION.gem --url $NEXUS_REPOSITORY
@@ -0,0 +1,50 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "proteus/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "dh-proteus"
8
+ spec.version = Proteus::VERSION
9
+ spec.authors = ["Simon Albrecht"]
10
+ spec.email = ["simon.albrecht@deliveryhero.com"]
11
+
12
+ spec.summary = %q{Proteus is a Terraform wrapper application.}
13
+ #spec.description = %q{Write a longer description or delete this line.}
14
+ spec.homepage = "https://github.com/deliveryhero/proteus"
15
+
16
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
17
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
18
+ if spec.respond_to?(:metadata)
19
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
20
+
21
+ spec.metadata["homepage_uri"] = spec.homepage
22
+ spec.metadata["source_code_uri"] = "https://github.com/deliveryhero/proteus"
23
+ spec.metadata["changelog_uri"] = "https://github.com/deliveryhero/proteus/CHANGELOG"
24
+ else
25
+ raise "RubyGems 2.0 or newer is required to protect against " \
26
+ "public gem pushes."
27
+ end
28
+
29
+ # Specify which files should be added to the gem when it is released.
30
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
31
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
32
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
33
+ end
34
+ spec.bindir = "bin"
35
+ spec.executables = ['proteus']
36
+ spec.require_paths = ["lib"]
37
+
38
+ spec.add_development_dependency "bundler", "~> 2.0"
39
+ spec.add_development_dependency "rake", "~> 10.0"
40
+ spec.add_development_dependency "rspec", "~> 3.0"
41
+ spec.add_development_dependency "pry"
42
+
43
+ spec.add_runtime_dependency 'activesupport', '~> 5.1.1'
44
+ spec.add_runtime_dependency 'thor', '~> 0.20.0'
45
+ spec.add_runtime_dependency 'erubis', '~> 2.7.0'
46
+ spec.add_runtime_dependency 'hcl-checker', '~> 1.0.5'
47
+ spec.add_runtime_dependency 'aws-sdk-rds', '~> 1.11.0'
48
+ spec.add_runtime_dependency 'aws-sdk-route53', '~> 1.7.0'
49
+ spec.add_runtime_dependency 'aws-sdk-elasticsearchservice', '~> 1.4.0'
50
+ end
@@ -0,0 +1,14 @@
1
+ class ::Hash
2
+ def deep_merge(second)
3
+ merger = proc do |_, v1, v2|
4
+ if v1.is_a?(Hash) && v2.is_a?(Hash)
5
+ v1.merge(v2, &merger)
6
+ elsif v1.is_a?(Array) && v2.is_a?(Array)
7
+ v1 | v2
8
+ else
9
+ [:undefined, nil, :nil].include?(v2) ? v1 : v2
10
+ end
11
+ end
12
+ merge(second.to_h, &merger)
13
+ end
14
+ end
data/lib/proteus.rb ADDED
@@ -0,0 +1,9 @@
1
+ require "proteus/version"
2
+ require 'erubis'
3
+ require 'thor'
4
+ require 'active_support/core_ext/hash'
5
+ require 'core_ext/hash'
6
+ require 'proteus/validators/base_validator'
7
+ require 'proteus/helpers'
8
+ require 'proteus/config/config'
9
+ require 'proteus/app'
@@ -0,0 +1,86 @@
1
+ require 'proteus/common'
2
+ require 'proteus/generate'
3
+ require 'proteus/init'
4
+ require 'proteus/global_commands/validate'
5
+ require 'proteus/context_management/context'
6
+ require 'proteus/context_management/helpers'
7
+ require 'proteus/templates/template_binding'
8
+ require 'proteus/templates/partial'
9
+
10
+ module Proteus
11
+
12
+ class Context < Thor; end
13
+
14
+ class App < Thor
15
+ extend Proteus::Helpers
16
+ extend Proteus::Helpers::StringHelpers
17
+ extend Proteus::ContextManagement::Helpers
18
+
19
+ class_option :dryrun, type: :boolean, default: false, aliases: '-d'
20
+ class_option :verbose, type: :boolean, default: false, aliases: '-v'
21
+
22
+ contexts.each do |context|
23
+
24
+ if context.name != 'default'
25
+
26
+ # generate class for subcommand within contexts subcommand
27
+ context_subcommand_class = Class.new(Thor)
28
+ context_subcommand_class_name = camel_case(context.name)
29
+ Object.const_set(context_subcommand_class_name, context_subcommand_class)
30
+
31
+ Proteus::Context.class_eval do
32
+ desc "#{context.name} SUBCOMMAND", "Manage the #{context.name} context."
33
+ subcommand(context.name, const_get(context_subcommand_class_name))
34
+ end
35
+ end
36
+
37
+ mod_name = Object.const_set("Module#{camel_case(context.name)}", Module.new)
38
+
39
+ context.environments.each do |environment|
40
+ class_name = camel_case(environment)
41
+
42
+ klass = Class.new(Proteus::Common)
43
+
44
+ mod_name.const_set(class_name, klass)
45
+
46
+ klass.class_eval do
47
+ include Config
48
+ @context = context.name
49
+ @environment = environment
50
+
51
+ def self.context
52
+ @context
53
+ end
54
+
55
+ def self.environment
56
+ @environment
57
+ end
58
+ end
59
+
60
+ if context.name == 'default'
61
+ # attach subcommands for the standard environments directly
62
+ # eg.:
63
+ # ./proteus production_eu SUBCOMMAND
64
+ desc "#{environment} SUBCOMMAND", "Manage the #{environment} environment in context default"
65
+ subcommand(environment, mod_name.const_get(class_name))
66
+ else
67
+ const_get(context_subcommand_class_name).class_eval do
68
+ desc "#{environment} SUBCOMMAND", "Manage the #{environment} environment in context #{context.name}"
69
+ subcommand(environment, mod_name.const_get(class_name))
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ desc 'generate SUBCOMMAND', 'Generators for modules and environments'
76
+ subcommand('generate', Proteus::Generate)
77
+
78
+ desc 'context SUBCOMMAND', 'Context subcommands'
79
+ subcommand('context', Context)
80
+
81
+ desc 'init', 'Initializes a new proteus root directory in the current working directory'
82
+ subcommand('init', Proteus::Init)
83
+
84
+ include Proteus::GlobalCommands::Validate
85
+ end
86
+ end
@@ -0,0 +1,41 @@
1
+ require 'proteus/helpers/path_helpers'
2
+
3
+ module Proteus
4
+ module Backend
5
+ class Backend
6
+ include Proteus::Helpers::PathHelpers
7
+ include Thor::Shell
8
+
9
+ def initialize(config:, context:, environment:)
10
+ @config = config
11
+ @context = context
12
+ @environment = environment
13
+ end
14
+
15
+ def render
16
+ File.open(File.join(context_path(@context), 'backend.tf'), 'w') do |file|
17
+ file.write(Erubis::Eruby.new(template).result(binding))
18
+ end
19
+ rescue StandardError => e
20
+ say 'Error rendering backend config.', :magenta
21
+ say e.message, :magenta
22
+ exit 1
23
+ end
24
+
25
+ protected
26
+
27
+ def template
28
+ <<~TEMPLATE
29
+ terraform {
30
+ backend "s3" {
31
+ bucket = "<%= @config[:backend][:bucket][:name] %>"
32
+ key = "<%= @config[:backend][:key_prefix] %>#{@context}-#{@environment}.tfstate"
33
+ region = "<%= @config[:backend][:bucket][:region] %>"
34
+ profile = "<%= @config[:backend][:bucket][:profile]%>"
35
+ }
36
+ }
37
+ TEMPLATE
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,53 @@
1
+ module Proteus
2
+ module Commands
3
+ module Apply
4
+ def self.included(thor_class)
5
+ thor_class.class_eval do
6
+
7
+ desc 'apply', 'Applies the current terraform code'
8
+ long_desc <<-LONGDESC
9
+ Applies the current terraform code
10
+
11
+ With --limit option, Terraform will only apply changes for the the specified resources
12
+ LONGDESC
13
+ option :limit, type: :array, aliases: "-l", required: false, default: nil
14
+ def apply
15
+ init(verbose: parent_options[:verbose])
16
+
17
+ confirm question: "Do you really want to run 'terraform apply' on environment '#{environment}' in context '#{context}'?", color: :on_red, exit_code: 0 do
18
+
19
+
20
+ if !options[:dryrun]
21
+ slack_notification(
22
+ context: context,
23
+ environment: environment,
24
+ message: 'is running terraform apply. Wait for it.'
25
+ )
26
+ end
27
+
28
+ apply_command = <<~APPLY_COMMAND
29
+ cd #{context_path(context)} && \
30
+ terraform apply \
31
+ -input=true \
32
+ -refresh=true \
33
+ #{limit(options[:limit])} \
34
+ #{plan_file(context, environment)}
35
+ APPLY_COMMAND
36
+
37
+ syscall apply_command.squeeze(' '), dryrun: dryrun
38
+
39
+ if !options[:dryrun]
40
+ slack_notification(
41
+ context: context,
42
+ environment: environment,
43
+ message: 'applied a new version of me!'
44
+ )
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,22 @@
1
+ require 'proteus/modules/manager'
2
+
3
+ module Proteus
4
+ module Commands
5
+ module Clean
6
+ def self.included(thor_class)
7
+ thor_class.class_eval do |klass|
8
+
9
+ desc "clean", "Deletes rendered module manifests"
10
+ long_desc <<-LONGDESC
11
+ Deletes rendered module manifests without running Terraform
12
+ LONGDESC
13
+ def clean
14
+ module_manager = Proteus::Modules::Manager.new(context: context, environment: environment)
15
+ module_manager.clean
16
+ end
17
+
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,51 @@
1
+ module Proteus
2
+ module Commands
3
+ module Destroy
4
+ def self.included(thor_class)
5
+ thor_class.class_eval do
6
+
7
+ desc "destroy", "Destroys AWS resources."
8
+ long_desc <<-LONGDESC
9
+ Destroys AWS resources.
10
+
11
+ With --limit option, Terraform will only destroy the specified resources
12
+ If --limit does not get passed as an argument, terraform will destroy all of the AWS assets in the state.
13
+ LONGDESC
14
+ option :limit, type: :array, aliases: "-l", required: false
15
+ def destroy
16
+ init(verbose: parent_options[:verbose])
17
+
18
+ destroy_command = <<~DESTROY_COMMAND
19
+ cd #{context_path(context)} && \
20
+ terraform destroy \
21
+ -var-file=#{var_file(context, environment)} \
22
+ #{aws_profile} \
23
+ ##{limit(options[:limit])}
24
+ DESTROY_COMMAND
25
+
26
+ plan_destroy_command = <<~PLAN_DESTROY_COMMAND
27
+ cd #{context_path(context)} && \
28
+ terraform plan \
29
+ -destroy \
30
+ -out=#{plan_file(context, environment)} \
31
+ -var-file=#{var_file(context, environment)} \
32
+ #{aws_profile} \
33
+ ##{limit(options[:limit])}
34
+ PLAN_DESTROY_COMMAND
35
+
36
+
37
+ if dryrun
38
+ puts destroy_command.squeeze(' ')
39
+ else
40
+ syscall plan_destroy_command.squeeze(' ')
41
+ confirm question: "Do you really want to run 'terraform destroy' on environment '#{environment}'?", color: :on_red, exit_code: 0 do
42
+ exec destroy_command.squeeze(' ')
43
+ end
44
+ end
45
+ end
46
+
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,22 @@
1
+ module Proteus
2
+ module Commands
3
+ module Graph
4
+ def self.included(thor_class)
5
+ thor_class.class_eval do
6
+
7
+ desc "graph", "Creates a graph showing the resources and their relations."
8
+ def graph
9
+ say "Generating graph. This might take a while"
10
+ graph_command = <<~GRAPH_COMMAND
11
+ cd #{context_path(context)} \
12
+ && terraform graph -draw-cycles -module-depth=1 | dot -Tpng > graph.png \
13
+ && open graph.png
14
+ GRAPH_COMMAND
15
+ `#{graph_command}`
16
+ end
17
+
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,75 @@
1
+ module Proteus
2
+ module Commands
3
+ module Import
4
+ def self.included(thor_class)
5
+ thor_class.class_eval do
6
+
7
+ desc "import", "Imports an existing resource into the terraform state"
8
+ long_desc <<-LONGDESC
9
+ Imports existing resources into the terraform state
10
+
11
+ --address Terraform internal address of the resource
12
+
13
+ --bulk Enables bulk import mode
14
+
15
+ --resource The resource to import into the terraform state
16
+
17
+ --resources_file File containing resource addresses and identifiers
18
+ LONGDESC
19
+ option :address, type: :string, aliases: "-a", default: nil
20
+ option :bulk, type: :boolean, aliases: "-b", required: false, default: false
21
+ option :resource, type: :string, aliases: "-r", default: nil
22
+ option :resources_file, type: :string, aliases: "-f", required: false, default: nil
23
+ def import
24
+ if options[:bulk]
25
+ if !options[:resources_file]
26
+ say "Supply a file containing resource identifiers and Terraform addresses for bulk operations", :red
27
+ exit 1
28
+ end
29
+ else
30
+ if !(options[:address] || !options[:resource])
31
+ say "You need to supply a resource address and a resource.", :red
32
+ exit 1
33
+ end
34
+ end
35
+
36
+ init(verbose: parent_options[:verbose])
37
+
38
+ confirm question: "Do you really want to run 'terraform import' in context '(#{context}, #{environment})'?", color: :on_red, exit_code: 0 do
39
+
40
+ import_command = <<~IMPORT_COMMAND
41
+ cd #{context_path(context)} && \
42
+ terraform import \
43
+ -var-file=#{var_file(context, environment)} \
44
+ #{aws_profile} \
45
+ %{address} \
46
+ %{resource}
47
+ IMPORT_COMMAND
48
+
49
+ if options[:bulk]
50
+ if File.file?(options[:resources_file])
51
+ File.open(options[:resources_file], "r") do |file|
52
+ resource_count = File.foreach(file).inject(0) {|count, line| count + 1}
53
+ index = 1
54
+ file.each_line do |line|
55
+ say "Processing resource #{index}/#{resource_count}", :green
56
+ resource = line.chomp.split(" = ")
57
+ syscall (import_command % { address: resource[0], resource: resource[1] }).squeeze(' ')
58
+ index += 1
59
+ end
60
+ end
61
+ else
62
+ say "File #{options[:resources_file]} does not exist.", :red
63
+ exit 1
64
+ end
65
+ else
66
+ syscall (import_command % { address: options[:address], resource: options[:resource] })
67
+ end
68
+ end
69
+ end
70
+
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end