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,157 @@
1
+ require 'json'
2
+ require 'rest-client'
3
+
4
+ module Covalence
5
+ module Atlas
6
+ AtlasTokenMissing = Class.new(StandardError)
7
+
8
+ # Default base URL for Atlas.
9
+ URL = "https://atlas.hashicorp.com"
10
+
11
+ def self.reset_cache()
12
+ @cache = Hash.new{|h,k| h[k] = Hash.new{|h,k| h[k] = Hash.new{|h,k| h[k] = Hash.new}}}
13
+ end
14
+
15
+ reset_cache
16
+
17
+ def self.get_artifact(slug, version, key, metadata: {})
18
+ ensure_atlas_token_set
19
+
20
+ @cache[slug][version][key][metadata] ||= begin
21
+ # Create and execute HTTP request
22
+ request = "#{URL}/api/v1/artifacts/#{slug}/search"
23
+
24
+ params = {}
25
+ params[:version] = version
26
+ if !metadata.empty?
27
+ i = 1
28
+ metadata.map do |k,v|
29
+ params["metadata.#{i}.key"] = k
30
+ params["metadata.#{i}.value"] = v
31
+ i += 1
32
+ end
33
+ end
34
+
35
+ headers = {:'X-Atlas-Token' => ENV['ATLAS_TOKEN']}
36
+ headers = headers.merge({:params => params})
37
+
38
+ begin
39
+ response = RestClient.get request, headers
40
+ rescue RestClient::ExceptionWithResponse => err
41
+ fail "Unable to retrieve ID for artifact '#{slug}': " + err.message
42
+ end
43
+
44
+ # Parse JSON response
45
+ parsed = JSON.parse(response)
46
+ latest = parsed["versions"].select {|version| version['metadata'].keys.include? "#{key}" }.first
47
+
48
+ # Return ID for the region specified
49
+ if latest != nil
50
+ latest["metadata"]["#{key}"]
51
+ else
52
+ fail "Requested key '#{key}' not found"
53
+ end
54
+ end
55
+ end
56
+
57
+ def self.get_output(name, stack)
58
+ ensure_atlas_token_set
59
+
60
+ @cache[stack][name][0][0] || begin
61
+ # Create and execute HTTP request
62
+ request = "#{URL}/api/v1/terraform/state/#{stack}"
63
+ headers = {:'X-Atlas-Token' => ENV['ATLAS_TOKEN']}
64
+
65
+ begin
66
+ response = RestClient.get request, headers
67
+ rescue RestClient::ExceptionWithResponse => err
68
+ fail "Unable to retrieve output '#{name}' from stack '#{stack}': " + err.message
69
+ end
70
+
71
+ # Parse JSON response
72
+ parsed = JSON.parse(response)
73
+ outputs = parsed.fetch("modules")[0].fetch("outputs")
74
+
75
+ # Populate the cache for subsequent calls
76
+ outputs.keys.each do |key|
77
+ @cache[stack][key][0][0] = outputs.fetch(key)
78
+ end
79
+
80
+ # Check outputs for requested key and return
81
+ if outputs.has_key?(name)
82
+ @cache[stack][name][0][0]
83
+ else
84
+ fail("Requested output '#{name}' not found")
85
+ end
86
+ end
87
+ end
88
+
89
+ def self.get_state_store(params, workspace_enabled=false)
90
+ raise "State store parameters must be a Hash" unless params.is_a?(Hash)
91
+ raise "Missing 'name' store parameter" unless params.has_key? 'name'
92
+
93
+ config = <<-CONF
94
+ terraform {
95
+ backend "atlas" {
96
+ name = "#{params['name']}"
97
+ }
98
+ }
99
+ CONF
100
+
101
+ return config
102
+ end
103
+
104
+ def self.ensure_atlas_token_set
105
+ raise AtlasTokenMissing.new("Missing ATLAS_TOKEN environment variable") unless ENV.key? 'ATLAS_TOKEN'
106
+ end
107
+
108
+ # Return module capabilities
109
+ # TODO: maybe a state_store mixin later
110
+ #def self.has_key_read?
111
+ #return true
112
+ #end
113
+
114
+ #def self.has_key_write?
115
+ #return false
116
+ #end
117
+
118
+ #def self.has_state_read?
119
+ #return true
120
+ #end
121
+
122
+ def self.has_state_store?
123
+ return true
124
+ end
125
+
126
+ # Key lookups
127
+ def self.lookup(type, params)
128
+ raise "Lookup parameters must be a Hash" unless params.is_a?(Hash)
129
+
130
+ case
131
+ when type == 'artifact'
132
+ required_params = [
133
+ 'slug',
134
+ 'version',
135
+ 'key',
136
+ ]
137
+ required_params.each do |param|
138
+ raise "Missing '#{param}' lookup parameter" unless params.has_key?(param)
139
+ end
140
+ metadata = {}
141
+ metadata = params['metadata'] unless !params['metadata']
142
+ self.get_artifact(params['slug'],params['version'],params['key'], metadata: metadata)
143
+ when type == 'state'
144
+ required_params = [
145
+ 'key',
146
+ 'stack'
147
+ ]
148
+ required_params.each do |param|
149
+ raise "Missing '#{param}' lookup parameter" unless params.has_key?(param)
150
+ end
151
+ self.get_output(params['key'],params['stack'])
152
+ else
153
+ raise "Atlas module does not support the '#{type}' lookup type"
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,153 @@
1
+ require 'json'
2
+ require 'rest-client'
3
+ require 'base64'
4
+
5
+ require_relative '../../helpers/shell_interpolation'
6
+
7
+ module Covalence
8
+ module Consul
9
+
10
+ ##### Consul ennvironment variables #####
11
+ #
12
+ # CONSUL_HTTP_ADDR - DNS name and port of your Consul endpoint specified in the format dnsname:port.
13
+ # Defaults to the local agent HTTP listener.
14
+ # CONSUL_HTTP_SSL - Specifies what protocol to use when talking to the given address, either http or https.
15
+ # CONSUL_HTTP_AUTH - HTTP Basic Authentication credentials to be used when communicating with Consul,
16
+ # in the format of either user or user:pass.
17
+ # CONSUL_HTTP_TOKEN - HTTP authentication token.
18
+
19
+ URL = ENV['CONSUL_HTTP_ADDR'] || 'localhost:8500'
20
+
21
+ def self.reset_cache()
22
+ @cache = Hash.new{|h,k| h[k] = Hash.new}
23
+ end
24
+
25
+ reset_cache
26
+
27
+ def self.get_key(name)
28
+
29
+ @cache['root'][name] ||= begin
30
+ # Create and execute HTTP request
31
+ request = "#{URL}/v1/kv/#{name}"
32
+
33
+ # Configure request headers
34
+ headers = {}
35
+ headers['X-Consul-Token'] = ENV['CONSUL_HTTP_TOKEN'] if ENV.has_key? 'CONSUL_HTTP_TOKEN'
36
+
37
+ begin
38
+ response = RestClient.get request, headers
39
+ rescue RestClient::ExceptionWithResponse => err
40
+ fail "Unable to retrieve key '#{name}': " + err.message
41
+ end
42
+
43
+ # Parse JSON response
44
+ begin
45
+ parsed = JSON.parse(response)
46
+ encoded = parsed.first['Value']
47
+ rescue JSON::ParserError => err
48
+ fail "No results or unable to parse response: " + err.message
49
+ end
50
+
51
+ # Return decoded value
52
+ if encoded != nil
53
+ Base64.decode64(encoded)
54
+ else
55
+ # TODO: not sure if this is the right failure to raise
56
+ fail "Requested key '#{name}' not found"
57
+ end
58
+ end
59
+ end
60
+
61
+ def self.get_output(name, stack)
62
+
63
+ @cache[stack][name] || begin
64
+ # Retrieve stack state
65
+ value = self.get_key(stack)
66
+
67
+ # Parse JSON
68
+ parsed = JSON.parse(value)
69
+ outputs = parsed.fetch("modules")[0].fetch("outputs")
70
+
71
+ # Populate the cache for subsequent calls
72
+ outputs.keys.each do |key|
73
+ @cache[stack][key] = outputs.fetch(key)
74
+ end
75
+
76
+ # Check outputs for requested key and return
77
+ if outputs.has_key?(name)
78
+ @cache[stack][name]
79
+ else
80
+ fail("Requested output '#{name}' not found")
81
+ end
82
+ end
83
+ end
84
+
85
+ # Return configuration for remote state store.
86
+ def self.get_state_store(params, workspace_enabled=false)
87
+ raise "State store parameters must be a Hash" unless params.is_a?(Hash)
88
+ required_params = [
89
+ 'access_token',
90
+ 'name'
91
+ ]
92
+ required_params.each do |param|
93
+ raise "Missing '#{param}' store parameter" unless params.has_key?(param)
94
+ end
95
+
96
+ config = <<-CONF
97
+ terraform {
98
+ backend "consul" {
99
+ path = "#{params['name']}"
100
+ CONF
101
+
102
+ params.delete('name')
103
+ params.each do |k,v|
104
+ v = Covalence::Helpers::ShellInterpolation.parse_shell(v) if v.include?("$(")
105
+ config += " #{k} = \"#{v}\"\n"
106
+ end
107
+
108
+ config += " }\n}\n"
109
+
110
+ return config
111
+ end
112
+
113
+ # Return module capabilities
114
+ # TODO: maybe a state_store mixin later
115
+ #def self.has_key_read?
116
+ #return true
117
+ #end
118
+
119
+ #def self.has_key_write?
120
+ #return false
121
+ #end
122
+
123
+ #def self.has_state_read?
124
+ #return true
125
+ #end
126
+
127
+ def self.has_state_store?
128
+ return true
129
+ end
130
+
131
+ # Key lookups
132
+ def self.lookup(type, params)
133
+ raise "Lookup parameters must be a Hash" unless params.is_a?(Hash)
134
+
135
+ case
136
+ when type == 'key'
137
+ raise "Missing 'key' lookup parameter" unless params.has_key? 'key'
138
+ self.get_key(params['key'])
139
+ when type == 'state'
140
+ required_params = [
141
+ 'key',
142
+ 'stack'
143
+ ]
144
+ required_params.each do |param|
145
+ raise "Missing '#{param}' lookup parameter" unless params.has_key?(param)
146
+ end
147
+ self.get_output(params['key'],params['stack'])
148
+ else
149
+ raise "Consul module does not support the '#{type}' lookup type"
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,147 @@
1
+ require 'json'
2
+ require 'aws-sdk-s3'
3
+
4
+ require_relative '../../helpers/shell_interpolation'
5
+
6
+ module Covalence
7
+ module S3
8
+
9
+ ##### AWS ennvironment variables #####
10
+ #
11
+ # AWS_ACCESS_KEY_ID – AWS access key.
12
+ # AWS_SECRET_ACCESS_KEY – AWS secret key. Access and secret key variables override
13
+ # credentials stored in credential and config files.
14
+ # AWS_REGION – AWS region. This variable overrides the default region of the in-use
15
+ # profile, if set.
16
+
17
+ REGION = ENV['AWS_REGION']
18
+
19
+ class Client
20
+ def initialize(region: REGION)
21
+ @s3 = Aws::S3::Client.new(region: region)
22
+ self.reset_cache
23
+ end
24
+
25
+ def reset_cache
26
+ @cache = Hash.new{|h,k| h[k] = Hash.new}
27
+ end
28
+
29
+ def get_cache
30
+ @cache.to_s
31
+ end
32
+
33
+ def get_doc(bucket, document)
34
+ @cache[bucket][document] ||= begin
35
+ @s3.get_object(bucket: bucket, key: document).body.read
36
+ rescue Aws::S3::Errors::ServiceError => err
37
+ fail "Unable to retrieve document '#{document}' from bucket '#{bucket}': " + err.message
38
+ end
39
+ end
40
+
41
+ def get_key(bucket, document, name)
42
+ doc = self.get_doc(bucket, document)
43
+
44
+ # Parse JSON response
45
+ begin
46
+ parsed = JSON.parse(doc)
47
+ rescue JSON::ParserError => err
48
+ fail "No results or unable to parse document '#{document}': " + err.message
49
+ end
50
+
51
+ # Determine whether the document is a Terraform state file
52
+ tf_state = true if parsed.has_key?('modules')
53
+
54
+ # Return ID for the key specified
55
+ if tf_state
56
+ outputs = parsed.fetch('modules')[0].fetch('outputs')
57
+ return outputs.fetch(name)
58
+ end
59
+ return parsed.fetch(name) if parsed.has_key?(name)
60
+ fail "Requested key '#{name}' not found"
61
+ end
62
+ end
63
+
64
+ # Return configuration for remote state store.
65
+ def self.get_state_store(params, workspace_enabled=false)
66
+ raise "State store parameters must be a Hash" unless params.is_a?(Hash)
67
+ required_params = [
68
+ 'bucket',
69
+ 'name'
70
+ ]
71
+ required_params.each do |param|
72
+ raise "Missing '#{param}' store parameter" unless params.has_key?(param)
73
+ end
74
+
75
+ config = <<-CONF
76
+ terraform {
77
+ backend "s3" {
78
+ CONF
79
+
80
+ if !params.has_key?('key')
81
+ Covalence::LOGGER.debug "'key' parameter not specified. Inferring value from 'name' parameter."
82
+
83
+ if workspace_enabled
84
+ config += " key = \"terraform.tfstate\"\n"
85
+ else
86
+ config += " key = \"#{params['name']}/terraform.tfstate\"\n"
87
+ end
88
+ end
89
+
90
+ if !params.has_key?('workspace_key_prefix')
91
+ if workspace_enabled
92
+ Covalence::LOGGER.debug "'workspace_key_prefix' parameter not specified. Inferring value from 'name' parameter."
93
+ config += " workspace_key_prefix = \"#{params['name']}\"\n"
94
+ end
95
+ end
96
+
97
+ params.delete('name')
98
+
99
+ params.each do |k,v|
100
+ v = Covalence::Helpers::ShellInterpolation.parse_shell(v) if v.to_s.include?("$(")
101
+ config += " #{k} = \"#{v}\"\n"
102
+ end
103
+
104
+ config += " }\n}\n"
105
+
106
+ return config
107
+ end
108
+
109
+ # Return module capabilities
110
+ # TODO: maybe a state_store mixin later
111
+ #def self.has_key_read?
112
+ #return true
113
+ #end
114
+
115
+ #def self.has_key_write?
116
+ #return false
117
+ #end
118
+
119
+ #def self.has_state_read?
120
+ #return true
121
+ #end
122
+
123
+ def self.has_state_store?
124
+ return true
125
+ end
126
+
127
+ # Key lookups
128
+ def self.lookup(type, params)
129
+ raise "Lookup parameters must be a Hash" unless params.is_a?(Hash)
130
+
131
+ case
132
+ when type == 'key' || type == 'state'
133
+ required_params = [
134
+ 'bucket',
135
+ 'document',
136
+ 'key'
137
+ ]
138
+ required_params.each do |param|
139
+ raise "Missing '#{param}' lookup parameter" unless params.has_key?(param)
140
+ end
141
+ raise "Missing 'key' lookup parameter" unless params.has_key? 'key'
142
+ client = S3::Client.new(region: REGION)
143
+ client.get_key(params['bucket'],params['document'],params['key'])
144
+ end
145
+ end
146
+ end
147
+ end