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