covalence 0.0.1 → 0.7.9.rc1

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 (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
@@ -0,0 +1,24 @@
1
+ require 'virtus'
2
+ require 'active_model'
3
+
4
+ require_relative 'stack'
5
+
6
+ module Covalence
7
+ class Environment
8
+ include Virtus.model
9
+ include ActiveModel::Validations
10
+
11
+ attribute :name, String
12
+ attribute :stacks, Array[Stack]
13
+
14
+ validates! :name, format: {
15
+ without: /\s+/,
16
+ message: "Environment %{attribute}: \"%{value}\" cannot contain spaces"
17
+ }
18
+
19
+ def initialize(attributes = {}, *args)
20
+ super
21
+ self.valid?
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,112 @@
1
+ require 'virtus'
2
+ require 'active_support/core_ext/string/inflections'
3
+ require 'active_support/core_ext/hash'
4
+ require 'active_model'
5
+
6
+ require_relative '../../helpers/shell_interpolation'
7
+
8
+ module Covalence
9
+ class Input
10
+ include Virtus.model
11
+ include ActiveModel::Validations
12
+
13
+ attribute :name, String
14
+ # unprocessed value, could be remote
15
+ attribute :raw_value, Object
16
+
17
+ def initialize(attributes = {}, *args)
18
+ super
19
+ self.valid?
20
+ end
21
+
22
+ def value
23
+ return raw_value if !raw_value.is_a?(Hash)
24
+ get_value(raw_value)
25
+ end
26
+
27
+ def to_command_option
28
+ "#{name} = #{parse_input(value())}"
29
+ end
30
+
31
+ private
32
+
33
+ def get_value(input)
34
+ backend, type = parse_type(input)
35
+
36
+ if backend != "local"
37
+ remote_value = backend::lookup(type, input)
38
+ if remote_value.is_a?(Hash)
39
+ get_value(remote_value)
40
+ else
41
+ remote_value
42
+ end
43
+ elsif input.stringify_keys.has_key?('value')
44
+ input.stringify_keys.fetch('value')
45
+ else
46
+ input
47
+ end
48
+ end
49
+
50
+ def parse_array(input)
51
+ config = "[\n"
52
+ input.each do |v|
53
+ config += " #{parse_input(v)},\n"
54
+ end
55
+ config += "]"
56
+ end
57
+
58
+ def parse_hash(input)
59
+ config = "{\n"
60
+ input.each do |k,v|
61
+ config += " \"#{k}\" = #{parse_input(v)}\n"
62
+ end
63
+ config += "}"
64
+ end
65
+
66
+ def parse_input(input)
67
+ if input.nil?
68
+ "\"\""
69
+
70
+ elsif input.is_a?(Hash)
71
+ parse_hash(input)
72
+
73
+ elsif input.is_a?(Array)
74
+ parse_array(input)
75
+
76
+ elsif input.to_s.include?("$(")
77
+ "\"#{Covalence::Helpers::ShellInterpolation.parse_shell(input)}\""
78
+
79
+ else
80
+ "\"#{input}\""
81
+ end
82
+ end
83
+
84
+ # :reek:FeatureEnvy
85
+ def parse_type(input)
86
+ if input.stringify_keys.has_key?('type')
87
+ type = input.stringify_keys.fetch('type')
88
+
89
+ local_types = %w(
90
+ list
91
+ map
92
+ string
93
+ )
94
+
95
+ if local_types.any? {|local_type| type == local_type }
96
+ return [ "local", type ]
97
+ elsif type.include?('.')
98
+ pieces = type.split('.', 2)
99
+ return [ "Covalence::#{pieces.first.camelize}".constantize,
100
+ pieces[1] ]
101
+ else
102
+ errors.add(:base,
103
+ "invalid input type specified: #{type}",
104
+ strict: true)
105
+ end
106
+ else
107
+ return [ "local", "map" ]
108
+ end
109
+ end
110
+
111
+ end
112
+ end
@@ -0,0 +1,74 @@
1
+ require 'virtus'
2
+ require 'active_model'
3
+ require 'semantic'
4
+
5
+ require_relative '../../../covalence'
6
+ require_relative 'state_store'
7
+ require_relative 'input'
8
+ require_relative 'context'
9
+
10
+ module Covalence
11
+ class Stack
12
+ include Virtus.model
13
+ include ActiveModel::Validations
14
+
15
+ attribute :type, String
16
+ attribute :name, String
17
+ attribute :environment_name, String
18
+ attribute :module_path, String
19
+ attribute :dependencies, Array[String]
20
+ attribute :packer_template, String
21
+ attribute :state_stores, Array[StateStore]
22
+ attribute :contexts, Array[Context]
23
+ attribute :inputs, Hash[String => Input]
24
+ attribute :args, String
25
+ attribute :workspace, String
26
+
27
+ validates! :type, inclusion: {
28
+ in: %w(terraform packer)
29
+ }
30
+ validates! :name, format: {
31
+ without: /\s+/,
32
+ message: "Stack %{attribute}: \"%{value}\" cannot contain spaces"
33
+ }
34
+
35
+ def initialize(attributes = {}, *arguments)
36
+ super
37
+ self.valid?
38
+ end
39
+
40
+ def full_name
41
+ "#{environment_name}-#{name}"
42
+ end
43
+
44
+ def materialize_cmd_inputs
45
+ if type == "terraform"
46
+ config = ""
47
+ inputs.values.map(&:to_command_option).each do |input|
48
+ config += input + "\n"
49
+ end
50
+ logger.info "\nStack inputs:\n\n#{config}"
51
+ File.open('covalence-inputs.tfvars','w') {|f| f.write(config)}
52
+ elsif type == "packer"
53
+ config = Hash.new
54
+ inputs.each do |name, input|
55
+ config[name] = input.value
56
+ end
57
+ config_json = JSON.generate(config)
58
+ logger.info "\nStack inputs:\n\n#{config_json}"
59
+ File.open('covalence-inputs.json','w') {|f| f.write(config_json)}
60
+ end
61
+ end
62
+
63
+ def materialize_state_inputs(store: state_stores.first)
64
+ config = store.get_config
65
+ logger.info "\nState store configuration:\n\n#{config}"
66
+ File.open('covalence-state.tf','w') {|f| f.write(config)}
67
+ end
68
+
69
+ def logger
70
+ Covalence::LOGGER
71
+ end
72
+
73
+ end
74
+ end
@@ -0,0 +1,65 @@
1
+ require 'virtus'
2
+ require 'active_support/core_ext/string/inflections'
3
+ require 'active_support/core_ext/hash'
4
+ require 'active_model'
5
+
6
+ # check to see how this works with plugins
7
+ Dir[File.expand_path('../state_stores/*.rb', File.dirname(__FILE__))].each do |file|
8
+ require file
9
+ end
10
+
11
+ module Covalence
12
+ class StateStore
13
+ include Virtus.model
14
+ include ActiveModel::Validations
15
+
16
+ attribute :params, Hash, :writer => :private
17
+ attribute :backend, Object, :writer => :private
18
+ attribute :workspace_enabled, Boolean
19
+
20
+ validate :validate_params_has_name,
21
+ :backend_has_state_store
22
+
23
+ def initialize(attributes = {}, *args)
24
+ super
25
+ self.valid?
26
+ end
27
+
28
+ def name
29
+ params.fetch('name')
30
+ end
31
+
32
+ # :reek:FeatureEnvy
33
+ def params=(params)
34
+ super(params.stringify_keys)
35
+ end
36
+
37
+ #TODO: prep different backend for plugins
38
+ # :reek:FeatureEnvy
39
+ def backend=(backend_name)
40
+ super("Covalence::#{backend_name.camelize}".constantize)
41
+ end
42
+
43
+ def get_config
44
+ backend::get_state_store(@params, @workspace_enabled)
45
+ end
46
+
47
+ private
48
+ def validate_params_has_name
49
+ if !params.has_key?('name')
50
+ errors.add(:base,
51
+ "Params #{params} missing 'name' parameter for the #{backend} state store",
52
+ strict: true)
53
+ end
54
+ end
55
+
56
+ def backend_has_state_store
57
+ backend_has_no_state_store = !backend.has_state_store? rescue true
58
+ if backend_has_no_state_store
59
+ errors.add(:base,
60
+ "#{backend} backend module does not support state storage",
61
+ strict: true)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,30 @@
1
+ require 'active_support/core_ext/object/blank'
2
+ require_relative '../entities/context'
3
+
4
+ module Covalence
5
+ class ContextRepository
6
+ class << self
7
+ def query_by_namespace(data_store, namespace, tool)
8
+ if tool == 'terraform'
9
+ query_tool_by_namespace(data_store, namespace)
10
+ else
11
+ Array.new(1, Context.new())
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def query_tool_by_namespace(data_store, namespace)
18
+ targets = data_store.hash_lookup("#{namespace}::targets", {})
19
+ contexts = targets.map do |name, values|
20
+ next if name.blank?
21
+ Context.new(name: name, values: values)
22
+ end
23
+ contexts.compact!
24
+ # always append blank context at the end.
25
+ contexts << Context.new()
26
+ contexts
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,92 @@
1
+ require_relative '../../../covalence'
2
+ require_relative '../data_stores/hiera'
3
+ require_relative '../entities/environment'
4
+ require_relative 'stack_repository'
5
+
6
+ module Covalence
7
+ class EnvironmentRepository
8
+ class << self
9
+ def find_all(data_store = HieraDB::Client.new(Covalence::CONFIG))
10
+ environments_hash = lookup_environments(data_store)
11
+
12
+ environments_hash.map do |environment_name, stack_names|
13
+ stacks = stack_names.map do |stack_name|
14
+ StackRepository.find(data_store, environment_name, stack_name)
15
+ end.compact
16
+
17
+ check_all_stacks_valid!(environment_name,
18
+ stack_names,
19
+ stacks)
20
+
21
+ Environment.new(name: environment_name,
22
+ stacks: stacks)
23
+ end
24
+ end
25
+
26
+ def find_filtered(task, data_store = HieraDB::Client.new(Covalence::CONFIG))
27
+ environments_hash = lookup_environments(data_store)
28
+ env_request = task['environment']
29
+ stk_request = task['stack']
30
+
31
+ if (env_request.nil? || !environments_hash.has_key?(env_request))
32
+ if RESERVED_NS.include?(env_request)
33
+ return Array.new(1, Environment.new(name: env_request,
34
+ stacks: stk_request))
35
+ else
36
+ raise "'#{env_request}' not found in environments"
37
+ end
38
+ end
39
+
40
+ stacks = nil
41
+ if (!stk_request.nil? && environments_hash[env_request].include?(stk_request))
42
+ stack_list = Array.new(1, stk_request)
43
+ stacks = Array.new(1, StackRepository.find(data_store, env_request, stk_request))
44
+ else
45
+ stack_list = environments_hash[env_request]
46
+ stacks = stack_list.map do |stack_name|
47
+ StackRepository.find(data_store, env_request, stack_name)
48
+ end.compact
49
+ end
50
+
51
+ check_all_stacks_valid!(env_request,
52
+ stack_list,
53
+ stacks)
54
+
55
+ Array.new(1, Environment.new(name: env_request,
56
+ stacks: stacks))
57
+ end
58
+
59
+ def populate_stack(stack, data_store = HieraDB::Client.new(Covalence::CONFIG))
60
+ StackRepository.populate(data_store, stack)
61
+ end
62
+
63
+ private
64
+
65
+ def lookup_environments(data_store)
66
+ environments_hash = data_store.hash_lookup('environments')
67
+ raise "Missing 'environments' configuration hash" if environments_hash.empty?
68
+ environments_hash
69
+ end
70
+
71
+ # Stacks are valid if they map to at least one tool (packer or terraform)
72
+ def check_all_stacks_valid!(environment_name, stack_list, stacks)
73
+ stack_names = stacks.map(&:name)
74
+ logger.debug("All stacks: #{stack_list}")
75
+ logger.debug("Targeted stacks: #{stack_names}")
76
+
77
+ invalid_stacks = stack_list - stack_names
78
+
79
+ if invalid_stacks.size > 0
80
+ error_string = <<-eos
81
+ Invalid stack(s) #{invalid_stacks} for environment #{environment_name}.
82
+ eos
83
+ raise error_string.strip
84
+ end
85
+ end
86
+
87
+ def logger
88
+ Covalence::LOGGER
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,56 @@
1
+ require 'json'
2
+ require 'yaml'
3
+ require_relative '../../../covalence'
4
+ require_relative '../entities/input'
5
+
6
+ module Covalence
7
+ class InputRepository
8
+ class << self
9
+ def query_by_namespace(data_store, namespace, tool)
10
+ results = Hash.new
11
+ if tool == 'terraform'
12
+ results = parse_var_file('terraform', data_store, namespace)
13
+ else
14
+ results = parse_var_file('packer', data_store, namespace)
15
+ end
16
+
17
+ results.merge(query_tool_by_namespace(data_store, namespace))
18
+ end
19
+
20
+ private
21
+
22
+ #TODO: refactor nested conditional
23
+ def parse_var_file(tool, data_store, namespace)
24
+ yaml_ext = %w(yaml yml)
25
+ json_ext = %w(json)
26
+
27
+ varfile = data_store.lookup("#{namespace}::vars-file", nil)
28
+ return {} unless varfile
29
+ tool_module_path ="Covalence::#{tool.upcase}".constantize
30
+ varfile = File.expand_path(File.join(tool_module_path, varfile.to_s))
31
+ if (File.file?(varfile) &&
32
+ (yaml_ext + json_ext).include?(File.extname(varfile)[1..-1]))
33
+ if json_ext.include?(File.extname(varfile)[1..-1])
34
+ tmp_hash = JSON.parse(File.read(varfile))
35
+ else
36
+ tmp_hash = YAML.load_file(varfile)
37
+ end
38
+
39
+ tmp_hash = tmp_hash.map do |name, raw_value|
40
+ [ name, Input.new(name: name, raw_value: raw_value) ]
41
+ end
42
+ Hash[*tmp_hash.flatten]
43
+ else
44
+ raise ArgumentError, "cannot parse non-yaml or non-json file: #{varfile}"
45
+ end
46
+ end
47
+
48
+ def query_tool_by_namespace(data_store, namespace)
49
+ tmp_hash = data_store.hash_lookup("#{namespace}::vars", {}).map do |name, raw_value|
50
+ [ name, Input.new(name: name, raw_value: raw_value) ]
51
+ end
52
+ Hash[*tmp_hash.flatten]
53
+ end
54
+ end
55
+ end
56
+ end