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