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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +164 -0
- data/README.md +489 -19
- data/TODO.md +14 -0
- data/lib/covalence.rb +41 -0
- data/lib/covalence/core/bootstrap.rb +8 -0
- data/lib/covalence/core/cli_wrappers/packer.yml +9 -0
- data/lib/covalence/core/cli_wrappers/packer_cli.rb +27 -0
- data/lib/covalence/core/cli_wrappers/popen_wrapper.rb +123 -0
- data/lib/covalence/core/cli_wrappers/terraform.yml +39 -0
- data/lib/covalence/core/cli_wrappers/terraform_cli.rb +119 -0
- data/lib/covalence/core/data_stores/hiera.rb +50 -0
- data/lib/covalence/core/entities/context.rb +38 -0
- data/lib/covalence/core/entities/environment.rb +24 -0
- data/lib/covalence/core/entities/input.rb +112 -0
- data/lib/covalence/core/entities/stack.rb +74 -0
- data/lib/covalence/core/entities/state_store.rb +65 -0
- data/lib/covalence/core/repositories/context_repository.rb +30 -0
- data/lib/covalence/core/repositories/environment_repository.rb +92 -0
- data/lib/covalence/core/repositories/input_repository.rb +56 -0
- data/lib/covalence/core/repositories/stack_repository.rb +89 -0
- data/lib/covalence/core/repositories/state_store_repository.rb +31 -0
- data/lib/covalence/core/services/hiera_syntax_service.rb +19 -0
- data/lib/covalence/core/services/packer_stack_tasks.rb +104 -0
- data/lib/covalence/core/services/terraform_stack_tasks.rb +212 -0
- data/lib/covalence/core/state_stores/atlas.rb +157 -0
- data/lib/covalence/core/state_stores/consul.rb +153 -0
- data/lib/covalence/core/state_stores/s3.rb +147 -0
- data/lib/covalence/environment_tasks.rb +328 -0
- data/lib/covalence/helpers/shell_interpolation.rb +28 -0
- data/lib/covalence/helpers/spec_dependencies.rb +21 -0
- data/lib/covalence/rake/rspec/envs_spec.rb +75 -0
- data/lib/covalence/rake/rspec/yaml_spec.rb +14 -0
- data/lib/covalence/spec_tasks.rb +59 -0
- data/lib/covalence/version.rb +3 -0
- metadata +344 -26
- data/.gitignore +0 -9
- data/Gemfile +0 -4
- data/LICENSE.txt +0 -21
- data/Rakefile +0 -2
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/lib/prometheus_unifio.rb +0 -5
- data/lib/prometheus_unifio/version.rb +0 -3
- 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
|