carinadigital-hiera-cloudformation 0.0.2.1 → 0.0.2.2

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.
data/README.md CHANGED
@@ -1,13 +1,17 @@
1
- # Hiera::Cloudformation
1
+ # Multiregion Hiera::Cloudformation
2
2
 
3
- This backend for Hiera can retrieve information from:
3
+ This a a fork of the hiera-cloudformaiton project (https://github.com/fanduel/hiera-cloudformation).
4
+ It adds the ability to lookup stack outputs in multiple regions from a single install along with other
5
+ small fixes.
6
+
7
+ This a backend for Hiera can retrieve information from:
4
8
 
5
9
  * the outputs of a CloudFormation stack
6
10
  * the metadata of a resource in a stack
7
11
 
8
12
  ## Installation
9
13
 
10
- gem install hiera-cloudformation
14
+ gem install carinadigital-hiera-cloudformation
11
15
 
12
16
  ## Usage
13
17
 
@@ -29,16 +33,15 @@ If you do not add these keys to your configuration file, the access keys will be
29
33
  the `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables, or from an IAM
30
34
  instance role (if you are running Hiera on an EC2 instance with an IAM role assigned).
31
35
 
32
- The AWS region to use can also be configured in the `:cloudformation` section of hiera.yaml.
33
36
  You can also tell the backend to convert string literals "true", "false", "3.14", etc to Boolean
34
37
  or Number types with the `:parse_metadata` configuration option; this may be useful as
35
38
  CloudFormation will convert Booleans and Numbers in the template JSON metadata into Strings when
36
39
  retrieved from a stack resource:
37
40
 
38
41
  :cloudformation:
39
- :region: 'us-west-1'
40
42
  :parse_metadata: true
41
43
 
44
+ The AWS region to use can also be configured in the `:cloudformation` section of hiera.yaml.
42
45
  For use in multiple AWS regions, the region can be set by an interpolated variable.
43
46
 
44
47
  :cloudformation:
data/Rakefile CHANGED
@@ -20,10 +20,10 @@ require 'rake/testtask'
20
20
 
21
21
  spec = Gem::Specification.new do |gem|
22
22
  gem.name = "carinadigital-hiera-cloudformation"
23
- gem.version = '0.0.2.1'
23
+ gem.version = '0.0.2.2'
24
24
  gem.authors = ["carinadigital"]
25
25
  gem.email = ["carinadigital@gmail.com"]
26
- gem.summary = %q{CloudFormation backend for Hiera}
26
+ gem.summary = %q{Multiregion CloudFormation backend for Hiera}
27
27
  gem.description = %q{Forked from hiera-cloudformation. Multiregion cloudFormation quieries of stack outputs or resource metadata for Hiera.}
28
28
  gem.homepage = "https://github.com/carinadigital/hiera-cloudformation"
29
29
  gem.license = "Apache License (2.0)"
@@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
6
6
  You may obtain a copy of the License at
7
7
 
8
- http://www.apache.org/licenses/LICENSE-2.0
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
9
 
10
10
  Unless required by applicable law or agreed to in writing, software
11
11
  distributed under the License is distributed on an "AS IS" BASIS,
@@ -15,217 +15,265 @@ limitations under the License.
15
15
  =end
16
16
 
17
17
  class Hiera
18
- module Backend
19
- class Cloudformation_backend
20
- TIMEOUT = 60 # 1 minute timeout for AWS API response caching
21
-
22
- def initialize
23
- begin
24
- require 'aws'
25
- require 'timedcache'
26
- require 'json'
27
- rescue LoadError
28
- require 'rubygems'
29
- require 'aws'
30
- require 'timedcache'
31
- require 'json'
32
- end
33
-
34
- if Config.include?(:cloudformation) && !Config[:cloudformation].nil? then
35
- if Config[:cloudformation].fetch(:parse_metadata, false) then
36
- debug("Will convert CloudFormation stringified metadata back to numbers or booleans.")
37
- @parse_metadata = true
38
- else
39
- @parse_metadata = false
40
- end
41
-
42
- @aws_config = {}
43
- if Config[:cloudformation].include?(:access_key_id) && Config[:cloudformation].include?(:secret_access_key) then
44
- debug("Found AWS access key #{Config[:cloudformation][:access_key_id]} from configuration")
45
- @aws_config[:access_key_id] = Config[:cloudformation][:access_key_id]
46
- @aws_config[:secret_access_key] = Config[:cloudformation][:secret_access_key]
47
- end
48
- if Config[:cloudformation].include?(:region) then
49
- debug("Found AWS region #{Config[:cloudformation][:region]} from configuration")
50
- @aws_config[:region] = Config[:cloudformation][:region]
51
- end
52
-
53
- else
54
- debug("No configuration found, will fall back to env variables or IAM role")
55
- @cf = AWS::CloudFormation.new
56
- end
57
-
58
- @output_cache = TimedCache.new
59
- @resource_cache = TimedCache.new
60
-
61
- debug("Hiera cloudformation backend loaded")
62
- end
63
-
64
-
65
- def create_connection(scope)
66
-
67
- # If we already have a connection object then return early.
68
- if defined? @cf then
69
- return
70
- end
71
-
72
- # Interpolate the value from hiera.yaml
73
- if @aws_config.include?(:region)
74
- @aws_config[:region] = Backend.parse_answer(@aws_config[:region], scope)
75
-
76
- # Make an array of valid AWS regions.
77
- aws_region_names = []
78
- AWS.regions.each do |region|
79
- aws_region_names.push(region.name)
18
+ module Backend
19
+ class Cloudformation_backend
20
+ TIMEOUT = 60 # 1 minute timeout for AWS API response caching
21
+
22
+ def initialize
23
+ begin
24
+ require 'aws'
25
+ require 'timedcache'
26
+ require 'json'
27
+ rescue LoadError
28
+ require 'rubygems'
29
+ require 'aws'
30
+ require 'timedcache'
31
+ require 'json'
32
+ end
33
+
34
+ # Class variables
35
+ # Data shared amongst all instance of this class.
36
+ @@aws_config = {} # AWS access credentials from yaml.
37
+ # Caches are shared, so that multiple AWS instances from seperate threads can share cache data
38
+ # e.g. When a scaling event occurs.
39
+ @@output_cache = TimedCache.new(:default_timeout => 60) #Default timeout in 60 seconds.
40
+ @@resource_cache = TimedCache.new(:default_timeout => 60)
41
+
42
+ # Class instance variables
43
+ # We want don't want two instances trying to reuse the same connection objects, and possible
44
+ # the same connection, so give each class it's own connection objects.
45
+ @cf = Hash.new # Variable for hash of connection options, keyed by region.
46
+
47
+
48
+ # Check our config key is present in hiera.yaml
49
+ if not Config.include?(:cloudformation) || Config[:cloudformation].nil? then
50
+ error_message = "[cloudformation_backend]: No configuration found."
51
+ Hiera.warn(error_message)
52
+ raise Exception, error_message
53
+ end
54
+
55
+ # Check we have the AWS access_key_id
56
+ # TODO we should have a fallback for access credentials as per documentaion.
57
+ if not Config[:cloudformation].include?(:access_key_id) then
58
+ error_message = "[cloudformation_backend]: :access_key_id missing in configuration."
59
+ Hiera.warn(error_message)
60
+ raise Exception, error_message
61
+ end
62
+
63
+ #Check we have the AWS secret_access_key
64
+ if not Config[:cloudformation].include?(:secret_access_key) then
65
+ error_message = "[cloudformation_backend]: :secret_access_key missing in configuration."
66
+ Hiera.warn(error_message)
67
+ raise Exception, error_message
68
+ end
69
+
70
+ # Check we have the AWS region.
71
+ # This can be a static region name of an interpolated fact.
72
+ # TODO: This should be improved so it can fallback to default environment or
73
+ # dot file values.
74
+ if not Config[:cloudformation].include?(:region) then
75
+ error_message = "[cloudformation_backend]: :region missing in configuration."
76
+ Hiera.warn(error_message)
77
+ raise Exception, error_message
78
+ end
79
+
80
+ if Config[:cloudformation].fetch(:parse_metadata, false) then
81
+ debug("Will convert CloudFormation stringified metadata back to numbers or booleans.")
82
+ @parse_metadata = true
83
+ else
84
+ @parse_metadata = false
85
+ end
86
+
87
+ debug("Using AWS access key #{Config[:cloudformation][:access_key_id]}")
88
+ @@aws_config['access_key_id'] = Config[:cloudformation][:access_key_id]
89
+ @@aws_config['secret_access_key'] = Config[:cloudformation][:secret_access_key]
90
+ debug("Using AWS region #{Config[:cloudformation][:region]}")
91
+ @@aws_config['region'] = Config[:cloudformation][:region]
92
+ debug("Hiera cloudformation backend loaded")
93
+ end
94
+
95
+
96
+ def lookup(key, scope, order_override, resolution_type)
97
+ answer = nil
98
+
99
+ # Lookups can potentially come from different agents in different AWS regions.
100
+ # Interpolate the value from hiera.yaml for this agent's region.
101
+ if @@aws_config.include?('region')
102
+ agent_region = Backend.parse_answer(@@aws_config['region'], scope)
103
+ end
104
+
105
+ # Idempotent connection creation of AWS connections for reuse.
106
+ create_connection(agent_region)
107
+
108
+ Backend.datasources(scope, order_override) do |elem|
109
+ case elem
110
+ when /cfstack\/([^\/]+)\/outputs/
111
+ debug("Looking up #{agent_region} #{$1} #{key} as an output of stack.")
112
+ raw_answer = stack_output_query($1, key, agent_region)
113
+ when /cfstack\/([^\/]+)\/resources\/([^\/]+)/
114
+ debug("Looking up #{agent_region} #{$1} #{$2} #{key} in metadata of stack resource")
115
+ raw_answer = stack_resource_query($1, $2, key,agent_region)
116
+ else
117
+ #debug("#{elem} doesn't seem to be a CloudFormation hierarchy element")
118
+ next
80
119
  end
81
120
 
82
- # Check this is a valid aws region.
83
- if not aws_region_names.include? @aws_config[:region]
84
- # If we don't have a region specified then the cloudformation endpoint will be malformed
85
- # resulting in networking errors.
86
- # Fail now with a proper error mesage.
87
- error_message = "[cloudformation_backend]: AWS Region #{@aws_config[:region]} is invalid."
88
- Hiera.warn(error_message)
89
- raise Exception, error_message
121
+ next if raw_answer.nil?
122
+
123
+ if @parse_metadata then
124
+ raw_answer = convert_metadata(raw_answer)
90
125
  end
91
126
 
92
- debug("Using lookups from region #{@aws_config[:region]} for this run.")
93
- end
94
-
95
- if @aws_config.length != 0 then
96
- @cf = AWS::CloudFormation.new(@aws_config)
97
- else
98
- debug("No AWS configuration found, will fall back to env variables or IAM role")
99
- @cf = AWS::CloudFormation.new
100
- end
101
- end
102
-
103
-
104
- def lookup(key, scope, order_override, resolution_type)
105
- answer = nil
106
-
107
- # Idempotent connection creation.
108
- create_connection(scope)
109
-
110
- Backend.datasources(scope, order_override) do |elem|
111
- case elem
112
- when /cfstack\/([^\/]+)\/outputs/
113
- debug("Looking up #{key} as an output of stack #{$1}")
114
- raw_answer = stack_output_query($1, key)
115
- when /cfstack\/([^\/]+)\/resources\/([^\/]+)/
116
- debug("Looking up #{key} in metadata of stack #{$1} resource #{$2}")
117
- raw_answer = stack_resource_query($1, $2, key)
118
- else
119
- debug("#{elem} doesn't seem to be a CloudFormation hierarchy element")
120
- next
121
- end
122
-
123
- next if raw_answer.nil?
124
-
125
- if @parse_metadata then
126
- raw_answer = convert_metadata(raw_answer)
127
- end
128
-
129
- new_answer = Backend.parse_answer(raw_answer, scope)
130
-
131
- case resolution_type
132
- when :array
133
- raise Exception, "Hiera type mismatch: expected Array and got #{new_answer.class}" unless new_answer.kind_of? Array or new_answer.kind_of? String
134
- answer ||= []
135
- answer << new_answer
136
- when :hash
137
- raise Exception, "Hiera type mismatch: expected Hash and got #{new_answer.class}" unless new_answer.kind_of? Hash
138
- answer ||= {}
139
- answer = Backend.merge_answer(new_answer, answer)
140
- else
141
- answer = new_answer
142
- break
143
- end
144
- end
145
-
146
- return answer
147
- end
148
-
149
- def stack_output_query(stack_name, key)
150
- outputs = @output_cache.get(stack_name)
151
-
152
- if outputs.nil? then
153
- debug("#{stack_name} outputs not cached, fetching...")
154
- begin
155
- outputs = @cf.stacks[stack_name].outputs
156
- rescue AWS::CloudFormation::Errors::ValidationError
157
- debug("Stack #{stack_name} outputs can't be retrieved")
158
- outputs = [] # this is just a non-nil value to serve as marker in cache
159
- end
160
- @output_cache.put(stack_name, outputs, TIMEOUT)
161
- end
162
-
163
- output = outputs.select { |item| item.key == key }
164
-
165
- return output.empty? ? nil : output.shift.value
166
- end
167
-
168
- def stack_resource_query(stack_name, resource_id, key)
169
- metadata = @resource_cache.get({:stack => stack_name, :resource => resource_id})
170
-
171
- if metadata.nil? then
172
- debug("#{stack_name} #{resource_id} metadata not cached, fetching")
173
- begin
174
- metadata = @cf.stacks[stack_name].resources[resource_id].metadata
175
- rescue AWS::CloudFormation::Errors::ValidationError
176
- # Stack or resource doesn't exist
177
- debug("Stack #{stack_name} resource #{resource_id} can't be retrieved")
178
- metadata = "{}" # This is just a non-nil value to serve as marker in cache
179
- end
180
- @resource_cache.put({:stack => stack_name, :resource => resource_id}, metadata, TIMEOUT)
181
- end
182
-
183
- if metadata.respond_to?(:to_str) then
184
- data = JSON.parse(metadata)
185
-
186
- if data.include?('hiera') then
187
- return data['hiera'][key] if data['hiera'].include?(key)
188
- end
189
- end
190
-
191
- return nil
192
- end
193
-
194
- def convert_metadata(json_object)
195
- if json_object.is_a?(Hash) then
196
- # convert each value of a Hash
197
- converted_object = {}
198
- json_object.each do |key, value|
199
- converted_object[key] = convert_metadata(value)
200
- end
201
- return converted_object
202
- elsif json_object.is_a?(Array) then
203
- # convert each item in an Array
204
- return json_object.map { |item| convert_metadata(item) }
205
- elsif json_object == "true" then
206
- # Boolean literals
207
- return true
208
- elsif json_object == "false" then
209
- return false
210
- elsif json_object == "null" then
211
- return nil
212
- elsif /^-?([1-9]\d*|0)(.\d+)?([eE][+-]?\d+)?$/.match(json_object) then
213
- # Numeric literals
214
- if json_object.include?('.') then
215
- return json_object.to_f
216
- else
217
- return json_object.to_i
218
- end
219
- else
220
- return json_object
221
- end
222
- end
127
+ new_answer = Backend.parse_answer(raw_answer, scope)
128
+
129
+ case resolution_type
130
+ when :array
131
+ raise Exception, "Hiera type mismatch: expected Array and got #{new_answer.class}" unless new_answer.kind_of? Array or new_answer.kind_of? String
132
+ answer ||= []
133
+ answer << new_answer
134
+ when :hash
135
+ raise Exception, "Hiera type mismatch: expected Hash and got #{new_answer.class}" unless new_answer.kind_of? Hash
136
+ answer ||= {}
137
+ answer = Backend.merge_answer(new_answer, answer)
138
+ else
139
+ answer = new_answer
140
+ break
141
+ end
142
+ end
143
+
144
+ return answer
145
+ end
146
+
147
+
148
+ # Ensures that connetion is created for this region in the class variable for connection.
149
+ def create_connection(region)
150
+
151
+ # If we already have a connection object then return early.
152
+ if @cf.has_key?(region) then
153
+ return
154
+ end
155
+
156
+ debug("Creating new persistent aws connection for region #{region}.")
157
+
158
+ # Check this is a valid aws region.
159
+ if not is_aws_region_name?(region)
160
+ # If we don't have a region specified then the cloudformation endpoint will be malformed
161
+ # resulting in networking errors.
162
+ # Fail now with a proper error mesage.
163
+ error_message = "[cloudformation_backend]: AWS Region #{region} is invalid."
164
+ Hiera.warn(error_message)
165
+ raise Exception, error_message
166
+ end
167
+
168
+ # Create an AWS connecton object for this region.
169
+ @cf[region] = AWS::CloudFormation.new(
170
+ :access_key_id => @@aws_config['access_key_id'],
171
+ :secret_access_key => @@aws_config['secret_access_key'],
172
+ :region => region
173
+ )
174
+ end
175
+
176
+
177
+ def stack_output_query(stack_name, key, region)
178
+ outputs = @@output_cache.get(region+stack_name)
179
+
180
+ if outputs.nil? then
181
+ debug("#{stack_name} outputs not cached, fetching...")
182
+ begin
183
+ outputs = @cf[region].stacks[stack_name].outputs
184
+ rescue AWS::CloudFormation::Errors::ValidationError
185
+ debug("Stack #{stack_name} outputs can't be retrieved")
186
+ outputs = [] # this is just a non-nil value to serve as marker in cache
187
+ end
188
+ @@output_cache.put(region+stack_name, outputs, TIMEOUT)
189
+ end
223
190
 
224
- # Custom function to wrap our debug messages to make them easier to find in the output.
225
- def debug(message)
226
- Hiera.debug("[cloudformation_backend]: #{message}")
227
- end
191
+ output = outputs.select { |item| item.key == key }
192
+
193
+ return output.empty? ? nil : output.shift.value
194
+ end
195
+
196
+
197
+ def stack_resource_query(stack_name, resource_id, key, region)
198
+ metadata = @@resource_cache.get({:stack => region+stack_name, :resource => resource_id})
199
+
200
+ if metadata.nil? then
201
+ debug("#{stack_name} #{resource_id} metadata not cached, fetching")
202
+ begin
203
+ metadata = @cf[region].stacks[stack_name].resources[resource_id].metadata
204
+ rescue AWS::CloudFormation::Errors::ValidationError
205
+ # Stack or resource doesn't exist
206
+ debug("Stack #{stack_name} resource #{resource_id} can't be retrieved")
207
+ metadata = "{}" # This is just a non-nil value to serve as marker in cache
208
+ end
209
+ @@resource_cache.put({:stack => region+stack_name, :resource => resource_id}, metadata, TIMEOUT)
210
+ end
211
+
212
+ if metadata.respond_to?(:to_str) then
213
+ data = JSON.parse(metadata)
228
214
 
229
- end
230
- end
215
+ if data.include?('hiera') then
216
+ return data['hiera'][key] if data['hiera'].include?(key)
217
+ end
218
+ end
219
+
220
+ return nil
221
+ end
222
+
223
+
224
+ def convert_metadata(json_object)
225
+ if json_object.is_a?(Hash) then
226
+ # convert each value of a Hash
227
+ converted_object = {}
228
+ json_object.each do |key, value|
229
+ converted_object[key] = convert_metadata(value)
230
+ end
231
+ return converted_object
232
+ elsif json_object.is_a?(Array) then
233
+ # convert each item in an Array
234
+ return json_object.map { |item| convert_metadata(item) }
235
+ elsif json_object == "true" then
236
+ # Boolean literals
237
+ return true
238
+ elsif json_object == "false" then
239
+ return false
240
+ elsif json_object == "null" then
241
+ return nil
242
+ elsif /^-?([1-9]\d*|0)(.\d+)?([eE][+-]?\d+)?$/.match(json_object) then
243
+ # Numeric literals
244
+ if json_object.include?('.') then
245
+ return json_object.to_f
246
+ else
247
+ return json_object.to_i
248
+ end
249
+ else
250
+ return json_object
251
+ end
252
+ end
253
+
254
+
255
+ # Custom function to wrap our debug messages to make them easier to find in the output.
256
+ def debug(message)
257
+ Hiera.debug("[cloudformation_backend]: #{message}")
258
+ end
259
+
260
+
261
+ # Check region name is a valid AWS regoion
262
+ def is_aws_region_name?(name)
263
+ # Make an array of valid AWS regions.
264
+ aws_region_names = []
265
+ AWS.regions.each do |aws_region|
266
+ aws_region_names.push(aws_region.name)
267
+ end
268
+
269
+ # Check this is a valid aws region.
270
+ if aws_region_names.include?(name)
271
+ return true
272
+ else
273
+ return false
274
+ end
275
+ end
276
+
277
+ end
278
+ end
231
279
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: carinadigital-hiera-cloudformation
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2.1
4
+ version: 0.0.2.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-09-19 00:00:00.000000000 Z
12
+ date: 2014-10-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
@@ -112,6 +112,6 @@ rubyforge_project:
112
112
  rubygems_version: 1.8.23
113
113
  signing_key:
114
114
  specification_version: 3
115
- summary: CloudFormation backend for Hiera
115
+ summary: Multiregion CloudFormation backend for Hiera
116
116
  test_files:
117
117
  - test/convert_metadata_test.rb