covalence 0.0.1 → 0.7.9.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +164 -0
  3. data/README.md +489 -19
  4. data/TODO.md +14 -0
  5. data/lib/covalence.rb +41 -0
  6. data/lib/covalence/core/bootstrap.rb +8 -0
  7. data/lib/covalence/core/cli_wrappers/packer.yml +9 -0
  8. data/lib/covalence/core/cli_wrappers/packer_cli.rb +27 -0
  9. data/lib/covalence/core/cli_wrappers/popen_wrapper.rb +123 -0
  10. data/lib/covalence/core/cli_wrappers/terraform.yml +39 -0
  11. data/lib/covalence/core/cli_wrappers/terraform_cli.rb +119 -0
  12. data/lib/covalence/core/data_stores/hiera.rb +50 -0
  13. data/lib/covalence/core/entities/context.rb +38 -0
  14. data/lib/covalence/core/entities/environment.rb +24 -0
  15. data/lib/covalence/core/entities/input.rb +112 -0
  16. data/lib/covalence/core/entities/stack.rb +74 -0
  17. data/lib/covalence/core/entities/state_store.rb +65 -0
  18. data/lib/covalence/core/repositories/context_repository.rb +30 -0
  19. data/lib/covalence/core/repositories/environment_repository.rb +92 -0
  20. data/lib/covalence/core/repositories/input_repository.rb +56 -0
  21. data/lib/covalence/core/repositories/stack_repository.rb +89 -0
  22. data/lib/covalence/core/repositories/state_store_repository.rb +31 -0
  23. data/lib/covalence/core/services/hiera_syntax_service.rb +19 -0
  24. data/lib/covalence/core/services/packer_stack_tasks.rb +104 -0
  25. data/lib/covalence/core/services/terraform_stack_tasks.rb +212 -0
  26. data/lib/covalence/core/state_stores/atlas.rb +157 -0
  27. data/lib/covalence/core/state_stores/consul.rb +153 -0
  28. data/lib/covalence/core/state_stores/s3.rb +147 -0
  29. data/lib/covalence/environment_tasks.rb +328 -0
  30. data/lib/covalence/helpers/shell_interpolation.rb +28 -0
  31. data/lib/covalence/helpers/spec_dependencies.rb +21 -0
  32. data/lib/covalence/rake/rspec/envs_spec.rb +75 -0
  33. data/lib/covalence/rake/rspec/yaml_spec.rb +14 -0
  34. data/lib/covalence/spec_tasks.rb +59 -0
  35. data/lib/covalence/version.rb +3 -0
  36. metadata +344 -26
  37. data/.gitignore +0 -9
  38. data/Gemfile +0 -4
  39. data/LICENSE.txt +0 -21
  40. data/Rakefile +0 -2
  41. data/bin/console +0 -14
  42. data/bin/setup +0 -8
  43. data/lib/prometheus_unifio.rb +0 -5
  44. data/lib/prometheus_unifio/version.rb +0 -3
  45. data/prometheus_unifio.gemspec +0 -32
data/TODO.md ADDED
@@ -0,0 +1,14 @@
1
+ - individual state-stores: would be nice if it had something to describe the backing input types
2
+ - consider httparty vs rest-client
3
+ - Use the invalid yaml syntax to figure out more things in rake tasks that needs to be lazy loaded
4
+ - Like the idea of maybe splitting out the syntax elements to a different gem. Gem installation & management would become easier
5
+ - missing spec around reports?
6
+ - Look into muting the reporter as dev default: https://github.com/ci-reporter/ci_reporter
7
+ - Look into getting HieraDB wrapper to read .yml files
8
+ - Terraform CLI in docker probably isn't going anywhere, might need to figure out a nicer way of handling this
9
+ - Use covalence to do verification on the open source stacks:
10
+ - terraform-aws-openvpn
11
+ - Plugin architecture
12
+ - rollup tasks in terraform need to check terraform only tasks
13
+ - stack_repository: backfill packer related tests
14
+ - packer: utils to gen uuids as inputs?
data/lib/covalence.rb ADDED
@@ -0,0 +1,41 @@
1
+ require "covalence/version"
2
+ require "logger"
3
+ require 'active_support/core_ext/object/blank'
4
+
5
+ if %w(development test).include?(ENV['RAKE_ENV'])
6
+ require 'byebug'
7
+ require 'awesome_print'
8
+ end
9
+
10
+ # :reek:TooManyConstants
11
+ module Covalence
12
+ # Configurable constants
13
+ #TODO: look into how WORKSPACE is being used, maybe this can just be an internal ROOT and make CONFIG not depend on WORKSPACE
14
+ WORKSPACE = File.absolute_path(ENV['COVALENCE_WORKSPACE'] || '.')
15
+ CONFIG = File.join(WORKSPACE, (ENV['COVALENCE_CONFIG'] || 'covalence.yaml'))
16
+ # TODO: could use better naming
17
+ PACKER = File.absolute_path(File.join(WORKSPACE, (ENV['COVALENCE_PACKER_DIR'] || '.')))
18
+ TERRAFORM = File.absolute_path(File.join(WORKSPACE, (ENV['COVALENCE_TERRAFORM_DIR'] || '.')))
19
+ TEST_ENVS = (ENV['COVALENCE_TEST_ENVS'] || "").split(',')
20
+ # Reserved namespace including default ci and spec
21
+ RESERVED_NS = [(ENV['COVALENCE_RESERVED_NAMESPACE'] || "").split(','), 'ci', 'spec']
22
+
23
+ TERRAFORM_CMD = ENV['TERRAFORM_CMD'] || "terraform"
24
+ TERRAFORM_VERSION = ENV['TERRAFORM_VERSION'] || `#{TERRAFORM_CMD} version`.split("\n", 2)[0].gsub('Terraform v','')
25
+ TERRAFORM_PLUGIN_CACHE = File.absolute_path("#{ENV['TF_PLUGIN_CACHE_DIR']}/linux_amd64" || "#{ENV['HOME']}/.terraform.d/plugin-cache/linus_amd64")
26
+
27
+ PACKER_CMD = ENV['PACKER_CMD'] || "packer"
28
+
29
+ # No-op shell command. Should not need to modify for most unix shells.
30
+ DRY_RUN_CMD = (ENV['COVALENCE_DRY_RUN_CMD'] || ":")
31
+ DEBUG_CLI = (ENV['COVALENCE_DEBUG'] || 'false') =~ (/(true|t|yes|y|1)$/i)
32
+
33
+ #DOCKER_ENV_FILE
34
+
35
+ # Internal constants
36
+ GEM_ROOT = File.expand_path('covalence', File.dirname(__FILE__)).freeze
37
+ # look into logger-colors
38
+ LOGGER = Logger.new(STDOUT)
39
+ LOG_LEVEL = String(ENV['COVALENCE_LOG'] || "warn").upcase
40
+ LOGGER.level = Logger.const_get(LOG_LEVEL)
41
+ end
@@ -0,0 +1,8 @@
1
+ require 'yaml'
2
+
3
+ require_relative '../../covalence'
4
+
5
+ Dir[File.expand_path("cli_wrappers/**/*.rb", File.dirname(__FILE__))].sort.each { |file| require file }
6
+ Dir[File.expand_path("entities/**/*.rb", File.dirname(__FILE__))].sort.each { |file| require file }
7
+ Dir[File.expand_path("repositories/**/*.rb", File.dirname(__FILE__))].sort.each { |file| require file }
8
+ Dir[File.expand_path("services/**/*.rb", File.dirname(__FILE__))].sort.each { |file| require file }
@@ -0,0 +1,9 @@
1
+ ---
2
+ version: Packer v1.0.0
3
+ commands:
4
+ build:
5
+ fix:
6
+ inspect:
7
+ push:
8
+ validate:
9
+ version:
@@ -0,0 +1,27 @@
1
+ module Covalence
2
+ class PackerCli
3
+ class << self
4
+ def require_init()
5
+ cmds_yml = File.expand_path("packer.yml", __dir__)
6
+ init_packer_cmds(cmds_yml)
7
+ end
8
+
9
+ private
10
+ def init_packer_cmds(file)
11
+ definition = YAML.load_file(file)
12
+
13
+ definition['commands'].each do |cmd, _|
14
+ packer_cmd = "packer_#{cmd}"
15
+
16
+ next if respond_to?(packer_cmd.to_sym)
17
+ define_singleton_method(packer_cmd) do |template, args: ''|
18
+ output = PopenWrapper.run([Covalence::PACKER_CMD, cmd], template, args)
19
+ (output == 0)
20
+ end #define_singleton_method
21
+ end # definition
22
+ end
23
+ end # class << self
24
+ end #PackerCli
25
+ end
26
+
27
+ Covalence::PackerCli.require_init
@@ -0,0 +1,123 @@
1
+ require 'open3'
2
+ require 'highline'
3
+
4
+ require_relative '../../../covalence'
5
+
6
+ module Covalence
7
+ class PopenWrapper
8
+ class << self
9
+
10
+ def run(cmds, path, args,
11
+ stdin_io: STDIN,
12
+ stdout_io: STDOUT,
13
+ stderr_io: STDERR,
14
+ debug: Covalence::DEBUG_CLI,
15
+ dry_run: false,
16
+ ignore_exitcode: false)
17
+
18
+ # TODO: implement path prefix for the docker runs, see @tf_cmd
19
+ cmd_string = [*cmds]
20
+ # TODO: cmd escape issues with -var.
21
+ cmd_string += [*args] unless args.blank?
22
+ cmd_string << path unless path.blank?
23
+
24
+ #TODO debug command string maybe
25
+ #TODO debug command args maybe
26
+
27
+ run_cmd = cmd_string.join(' ')
28
+ print_cmd_string(run_cmd)
29
+ if dry_run
30
+ run_cmd = Covalence::DRY_RUN_CMD
31
+ end
32
+
33
+ if debug
34
+ return 0 unless HighLine.new.agree('Execute? [y/n]')
35
+ end
36
+
37
+ spawn_subprocess(ENV, run_cmd, {
38
+ stdin_io: stdin_io,
39
+ stdout_io: stdout_io,
40
+ stderr_io: stderr_io,
41
+ ignore_exitcode: ignore_exitcode
42
+ })
43
+ end
44
+
45
+ private
46
+ def print_cmd_string(cmd_string)
47
+ Covalence::LOGGER.warn "---"
48
+ Covalence::LOGGER.warn cmd_string
49
+ end
50
+
51
+ def spawn_subprocess(env, run_cmd,
52
+ stdin_io: STDIN,
53
+ stdout_io: STDOUT,
54
+ stderr_io: STDERR,
55
+ ignore_exitcode: false)
56
+ Signal.trap("INT") {} #Disable parent process from exiting, orphaning the child fork below
57
+ wait_thread = nil
58
+
59
+ Open3.popen3(env, run_cmd) do |stdin, stdout, stderr, wait_thr|
60
+ mappings = { stdin_io => stdin, stdout => stdout_io, stderr => stderr_io }
61
+ wait_thread = wait_thr
62
+
63
+ Signal.trap("INT") {
64
+ Process.kill("INT", wait_thr.pid)
65
+ Process.wait(wait_thr.pid, Process::WNOHANG)
66
+
67
+ exit(wait_thr.value.exitstatus)
68
+ } # let SIGINT drop into the child process
69
+
70
+ handle_io_streams(mappings, stdin_io)
71
+ end
72
+
73
+ Signal.trap("INT") { exit } #Restore parent SIGINT
74
+
75
+ return 0 if ignore_exitcode
76
+ exit(wait_thread.value.exitstatus) unless wait_thread.value.success?
77
+ return wait_thread.value.exitstatus
78
+ end
79
+
80
+ def handle_io_streams(mappings, stdin_io)
81
+ inputs = mappings.keys
82
+ streams_ready_for_eof_check = []
83
+
84
+ until inputs.empty? || (inputs.size == 1 && inputs.first == stdin_io) do
85
+
86
+ readable_inputs, _ = IO.select(inputs, [], [])
87
+ streams_ready_for_eof_check = readable_inputs
88
+
89
+ streams_ready_for_eof_check.select(&:eof).each do |src|
90
+ Covalence::LOGGER.debug "Stopping redirection from an IO in EOF: " + src.inspect
91
+ # `select`ing an IO which has reached EOF blocks forever.
92
+ # So you have to delete such IO from the array of IOs to `select`.
93
+ inputs.delete src
94
+
95
+ # You must close the child process' STDIN immeditely after the parent's STDIN reached EOF,
96
+ # or some kinds of child processes never exit.
97
+ # e.g.) echo foobar | joumae run -- cat
98
+ # After the `echo` finished outputting `foobar`, you have to tell `cat` about that or `cat` will wait for more inputs forever.
99
+ mappings[src].close if src == stdin_io
100
+ end
101
+
102
+ break if inputs.empty? || (inputs.size == 1 && inputs.first == stdin_io)
103
+
104
+ readable_inputs.each do |input|
105
+ begin
106
+ data = input.read_nonblock(1024)
107
+ output = mappings[input]
108
+ output.write(data)
109
+ output.flush
110
+ rescue EOFError => e
111
+ Covalence::LOGGER.debug "Reached EOF: #{e}"
112
+ inputs.delete input
113
+ rescue Errno::EPIPE => e
114
+ Covalence::LOGGER.debug "Handled error: #{e}: io: #{input.inspect}"
115
+ inputs.delete input
116
+ end
117
+ end #readable_inputs
118
+ end #until inputs.empty?
119
+ end #handle_io_streams
120
+
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,39 @@
1
+ ---
2
+ version: Terraform v0.10.6
3
+ commands:
4
+ apply:
5
+ #console:
6
+ destroy:
7
+ #env:
8
+ #list:
9
+ #select:
10
+ #new:
11
+ #delete:
12
+ fmt:
13
+ #force-unlock
14
+ get:
15
+ graph:
16
+ #import
17
+ #init:
18
+ #output:
19
+ plan:
20
+ providers:
21
+ push:
22
+ refresh:
23
+ show:
24
+ state:
25
+ #list:
26
+ #mv:
27
+ #pull:
28
+ #push:
29
+ #rm:
30
+ #show:
31
+ #taint:
32
+ validate:
33
+ #untaint:
34
+ workspace:
35
+ list:
36
+ select:
37
+ new:
38
+ delete:
39
+ version:
@@ -0,0 +1,119 @@
1
+ require 'yaml'
2
+ require 'fileutils'
3
+ require 'tmpdir'
4
+ require 'active_support/core_ext/object/blank'
5
+ require 'open3'
6
+ require 'semantic'
7
+
8
+ require_relative '../../../covalence'
9
+ require_relative 'popen_wrapper'
10
+
11
+ module Covalence
12
+ class TerraformCli
13
+ def self.require_init()
14
+ if Semantic::Version.new(Covalence::TERRAFORM_VERSION) < Semantic::Version.new("0.9.0")
15
+ raise "Terraform v0.9.0 or newer required"
16
+ else
17
+ cmds_yml = File.expand_path("terraform.yml", __dir__)
18
+ end
19
+ init_terraform_cmds(cmds_yml)
20
+ end
21
+
22
+ # :reek:BooleanParameter
23
+ def self.terraform_clean(path, dry_run: false, verbose: true)
24
+ # standard run shouldn't need this since it does a chdir on a temp dir anyway
25
+ # something about cln_cmd when working with docker images
26
+ targets = [ File.join(File.expand_path(path), ".terraform") ] +
27
+ Dir.glob(File.join(File.expand_path(path), "*.tfstate*"))
28
+
29
+ FileUtils.rm_rf(targets, {
30
+ noop: dry_run,
31
+ verbose: verbose,
32
+ secure: true,
33
+ })
34
+ end
35
+
36
+ def self.terraform_check_style(path)
37
+ output, status = Open3.capture2e(ENV, Covalence::TERRAFORM_CMD, "fmt", "-write=false", path)
38
+ return false unless status.success?
39
+ output = output.split("\n")
40
+ (output.size == 0)
41
+ end
42
+
43
+ def self.terraform_init(path='', args: '', ignore_exitcode: false)
44
+ if ENV['TF_PLUGIN_LOCAL'] == 'true'
45
+ cmd = [Covalence::TERRAFORM_CMD, "init", "-get=false", "-input=false", "-plugin-dir=#{Covalence::TERRAFORM_PLUGIN_CACHE}"]
46
+ else
47
+ cmd = [Covalence::TERRAFORM_CMD, "init", "-get=false", "-input=false"]
48
+ end
49
+
50
+ output = PopenWrapper.run(
51
+ cmd,
52
+ path,
53
+ args,
54
+ ignore_exitcode: ignore_exitcode)
55
+ (output == 0)
56
+ end
57
+
58
+ def self.terraform_workspace(workspace, path='', args: '', ignore_exitcode: false)
59
+ cmd = [Covalence::TERRAFORM_CMD, "workspace", "new", workspace]
60
+
61
+ output = PopenWrapper.run(
62
+ cmd,
63
+ path,
64
+ args,
65
+ ignore_exitcode: ignore_exitcode)
66
+
67
+ (output == 0)
68
+ end
69
+
70
+ def self.terraform_output(output_var, args: '')
71
+ raise "TODO: implement me"
72
+ end
73
+
74
+ def self.terraform_taint(resource_name, args: '')
75
+ raise "TODO: implement me"
76
+ end
77
+
78
+ def self.terraform_untaint(resource_name, args: '')
79
+ raise "TODO: implement me"
80
+ end
81
+
82
+
83
+ class << self
84
+ private
85
+
86
+ # The only args that should be automated are ones that only expect some DIR/PATH as it's only
87
+ # required arg, most other things need a little bit more manual definition.
88
+ def init_terraform_cmds(file)
89
+ definition = YAML.load_file(file)
90
+
91
+ definition['commands'].each do |cmd, sub_hash|
92
+ if sub_hash.blank?
93
+ terraform_cmd = "terraform_#{cmd}"
94
+
95
+ next if respond_to?(terraform_cmd.to_sym)
96
+ define_singleton_method(terraform_cmd) do |path=Dir.pwd(), args: ''|
97
+ output = PopenWrapper.run([Covalence::TERRAFORM_CMD, cmd], path, args)
98
+ (output == 0)
99
+ end
100
+ elsif sub_hash.is_a?(Hash)
101
+ sub_hash.keys.each do |sub_command|
102
+ terraform_cmd = "terraform_#{cmd}_#{sub_command}"
103
+
104
+ next if respond_to?(terraform_cmd.to_sym)
105
+ define_singleton_method(terraform_cmd) do |path=Dir.pwd(), args: ''|
106
+ output = PopenWrapper.run([Covalence::TERRAFORM_CMD, cmd, sub_command], path, args)
107
+ (output == 0)
108
+ end
109
+ end
110
+ else
111
+ raise "Invalid yml context"
112
+ end
113
+ end
114
+ end #init_terraform_cmds
115
+ end #private
116
+ end #TerraformCli
117
+ end
118
+
119
+ Covalence::TerraformCli.require_init
@@ -0,0 +1,50 @@
1
+ require_relative '../../../covalence'
2
+ require 'yaml'
3
+ require 'hiera'
4
+
5
+ module Covalence
6
+ module HieraDB
7
+ # TODO: maybe HieraWrapper
8
+ # :reek:DataClump
9
+ class Client
10
+ attr_reader :scope
11
+
12
+ def initialize(config, scope = {})
13
+ @config = config
14
+ @scope = scope
15
+
16
+ begin
17
+ @client = Hiera.new(:config => config)
18
+ rescue RuntimeError => e
19
+ Covalence::LOGGER.error e.message
20
+ exit 1
21
+ end
22
+ end
23
+
24
+ def initialize_scope(scope)
25
+ self.class.new(@config, scope)
26
+ end
27
+
28
+ def set_scope(env, stack)
29
+ @scope = {
30
+ "environment" => env,
31
+ "stack" => stack
32
+ }
33
+ end
34
+
35
+ def lookup(key, default = nil, scope = @scope)
36
+ @client.lookup(key, default, scope)
37
+ end
38
+
39
+ def hash_lookup(key, default = nil, scope = @scope)
40
+ # https://github.com/puppetlabs/hiera/blob/d7ed74f4eec8f4fb1aa84cd0e158a595f86debd4/lib/hiera/backend.rb#L241
41
+ # def lookup(key, default, scope, order_override, resolution_type, context = {:recurse_guard => nil})
42
+ @client.lookup(key, default, scope, nil, :hash)
43
+ end
44
+
45
+ def array_lookup(key, default = nil, scope = @scope)
46
+ @client.lookup(key, default, scope, nil, :array)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,38 @@
1
+ require 'active_support/core_ext/object/blank'
2
+ require 'virtus'
3
+ require 'active_model'
4
+
5
+ module Covalence
6
+ # Maybe just call this targets
7
+ class Context
8
+ include Virtus.model
9
+ include ActiveModel::Validations
10
+
11
+ attribute :name, String, default: ''
12
+ attribute :values, Array, default: []
13
+
14
+ validates! :name, format: {
15
+ without: /(\s+|,)/,
16
+ message: "Context %{attribute}: \"%{value}\" cannot contain spaces or commas"
17
+ }
18
+
19
+ def initialize(attributes = {}, *args)
20
+ super
21
+ self.valid?
22
+ end
23
+
24
+ def namespace
25
+ return "" if name.blank?
26
+ "#{name}:"
27
+ end
28
+
29
+ def to_command_options
30
+ values.map { |value| "-target=\"#{value}\"" }
31
+ end
32
+
33
+ def to_packer_command_options
34
+ return "" if values.blank?
35
+ "-only=#{values.join(',')}"
36
+ end
37
+ end
38
+ end