customresource-route53 0.7.1

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.
@@ -0,0 +1,2 @@
1
+ ore-apachev2
2
+ ============
@@ -0,0 +1,30 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+
5
+ begin
6
+ require 'bundler'
7
+ rescue LoadError => e
8
+ warn e.message
9
+ warn "Run `gem install bundler` to install Bundler."
10
+ exit -1
11
+ end
12
+
13
+ begin
14
+ Bundler.setup(:development)
15
+ rescue Bundler::BundlerError => e
16
+ warn e.message
17
+ warn "Run `bundle install` to install missing gems."
18
+ exit e.status_code
19
+ end
20
+
21
+ require 'rake'
22
+
23
+ require 'rubygems/tasks'
24
+ Gem::Tasks.new
25
+
26
+ require 'cucumber/rake/task'
27
+
28
+ Cucumber::Rake::Task.new do |t|
29
+ t.cucumber_opts = %w[--format pretty]
30
+ end
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Adjust lib path
4
+ _lib=File.expand_path(File.dirname(__FILE__) + '/../lib')
5
+ $:.unshift(_lib) unless $:.include?(_lib)
6
+ require 'customresource/route53/cli'
7
+
8
+ # =====================================================================================================================
9
+ rc = CustomResource::Route53::Cli.start(ARGV)
10
+ if rc.is_a?(Fixnum)
11
+ exit rc
12
+ else
13
+ puts rc.ai
14
+ exit 0
15
+ end
16
+
@@ -0,0 +1,34 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require File.expand_path('../lib/customresource/route53/version', __FILE__)
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.name = 'customresource-route53'
7
+ gem.version = CustomResource::Route53::VERSION
8
+ gem.summary = %q{An action command for aws-cfn-resource-bridge to implement CloudFormation custom resources}
9
+ gem.description = %q{An action command for aws-cfn-resource-bridge to implement CloudFormation custom resources}
10
+ gem.license = 'Apachev2'
11
+ gem.authors = ['Christo De Lange']
12
+ gem.email = 'rubygems@dldinternet.com'
13
+ gem.homepage = 'https://rubygems.org/gems/customresource-route53'
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ['lib']
19
+
20
+ gem.add_development_dependency 'bundler', '~> 1.0'
21
+ gem.add_development_dependency 'rake', '~> 10'
22
+ gem.add_development_dependency 'rubygems-tasks', '~> 0.2'
23
+
24
+ gem.add_dependency 'thor', '~> 0.19'
25
+ gem.add_dependency 'awesome_print', '~> 1.2'
26
+ gem.add_dependency 'colorize', '~> 0.7.3'
27
+ gem.add_dependency 'aws-sdk-core', '~> 2.0'
28
+ gem.add_dependency 'inifile', '~> 3'
29
+ gem.add_dependency 'dldinternet-mixlib-thor-nocommands', '~> 0.2'
30
+ gem.add_dependency 'dldinternet-mixlib-logging', '~> 0.4'
31
+ gem.add_dependency 'ipaddress', '~> 0.8'
32
+ gem.add_dependency 'customresource-base', '~> 0'
33
+
34
+ end
File without changes
@@ -0,0 +1 @@
1
+ Feature: Blah blah blah
File without changes
@@ -0,0 +1,2 @@
1
+ require 'customresource/route53/version'
2
+ require 'customresource/route53/cli'
@@ -0,0 +1,59 @@
1
+ require 'thor'
2
+ require 'awesome_print'
3
+ require 'colorize'
4
+ require 'aws-sdk-core'
5
+ require 'yaml'
6
+ require 'customresource/route53/version'
7
+ require 'aws/ec2/instance_data'
8
+
9
+ module CustomResource
10
+ module Route53
11
+
12
+ class Cli < Thor
13
+ class_option :verbose, :type => :boolean
14
+ class_option :debug, :type => :boolean
15
+ class_option :trace, :type => :boolean
16
+ class_option :log_level, :type => :string, :banner => 'Log level ([:trace, :debug, :info, :step, :warn, :error, :fatal, :todo])'
17
+ class_option :log_file, :type => :string
18
+ class_option :inifile, :type => :string
19
+ class_option :input, :type => :string
20
+ class_option :ip_address, :type => :string
21
+
22
+ no_commands do
23
+
24
+ require 'dldinternet/mixlib/thor/no_commands'
25
+ include DLDInternet::MixLib::Thor::No_Commands
26
+
27
+ require 'customresource/route53/mixins/cli'
28
+ include CustomResource::Route53::MixIns::Cli
29
+
30
+ require 'customresource/route53/mixins/actions'
31
+ include CustomResource::Route53::MixIns::Actions
32
+
33
+ end # no_commands
34
+
35
+ def initialize(args = [], local_options = {}, config = {})
36
+ super(args,local_options,config)
37
+ @log_level = :step
38
+ end
39
+
40
+ desc 'version', 'display current version'
41
+ def version()
42
+ puts ::CustomResource::Route53::VERSION
43
+ exit 0
44
+ end
45
+
46
+ desc 'privatehostedzone', 'CRUD for privatehostedzones'
47
+ def privatehostedzone()
48
+ process('PrivateHostedZones')
49
+ end
50
+
51
+ desc 'reversednsentry', 'CRUD for reversednsentries'
52
+ def reversednsentry()
53
+ process('ReverseDNSEntries')
54
+ end
55
+
56
+ end
57
+ end
58
+ end
59
+
@@ -0,0 +1,339 @@
1
+ require 'customresource/base/mixins/actions'
2
+
3
+ module CustomResource
4
+ module Route53
5
+ module MixIns
6
+ module Actions
7
+ include CustomResource::Base::MixIns::Actions
8
+
9
+ WAIT_PERIOD_INSYNC = 30
10
+
11
+ class NoSuchHostedZone < StandardError ; end
12
+
13
+ def action(verb,section,proc)
14
+ prepare
15
+ # data = {}
16
+ restyp = section.gsub(/s$/,'').downcase
17
+ method = "#{verb}_#{restyp}"
18
+
19
+ resource = @event['ResourceProperties']
20
+ @logger.debug "#{method} #{resource.ai}"
21
+
22
+ # Convert the 'String' keys to :string symbols
23
+ params = properties_to_params(resource.dup)
24
+ # Weed out the invalid properties
25
+ invalid_keys = validate_params(section,params)
26
+ if invalid_keys.size > 0
27
+ @logger.warn "Invalid keys in #{params[:name] || params.ai}:\n#{invalid_keys.ai}"
28
+ end
29
+ params = delete_invalid_keys(params,invalid_keys)
30
+ # Restore these exceptions to the rule from the resource 'String' set e.g. :codec_options => { 'String': Value, }
31
+ params = restore_exception_params(section,params,resource)
32
+ params = infer_params(section,Mash.new(params))
33
+ @logger.debug params.ai
34
+
35
+ data = proc.call(params, restyp, method, resource)
36
+ @logger.debug data
37
+ respond('SUCCESS', data)
38
+ 0
39
+ end
40
+
41
+ def create(section)
42
+ case section
43
+ when /PrivateHostedZones/
44
+ action('create', section, lambda do |params, restyp, method, resource|
45
+ begin
46
+ require 'date'
47
+ data = nil
48
+ set = list_hosted_zones(params)
49
+ if set.size == 0
50
+ @logger.info "create #{params[:name]}"
51
+ resp = @awssdk.create_hosted_zone(
52
+ name: params[:hostedzone],
53
+ vpc: {
54
+ vpc_region: @region,
55
+ vpc_id: params[:vpcid]
56
+ },
57
+ caller_reference: "#{params[:name]}::#{DateTime::now.strftime('%Y%m%dT%H%M%S%z')}",
58
+ hosted_zone_config: {
59
+ comment: params[:comment],
60
+ # private_zone: true,
61
+ }
62
+ )
63
+ wait_for_insync(resp)
64
+ zone = get_hosted_zone(params)
65
+ else
66
+ zone = set[0] # get_hosted_zone(params)
67
+ @logger.info "Existing hosted_zone #{zone[:id]}::#{zone[:name]}"
68
+ resp = @awssdk.update_hosted_zone_comment( id: zone[:id], comment: params[:comment] )
69
+ unless resp and resp[:hosted_zone]
70
+ abort! "Failed to set comment on zone #{zone[:id]}::#{zone[:name]}"
71
+ end
72
+ zone = Mash.new(resp[:hosted_zone].to_hash)
73
+ end
74
+ if params[:ttl]
75
+ rrs = list_resource_record_sets(zone)
76
+ existing = rrs.select{ |rr| %w(NS SOA).include?(rr[:type]) }
77
+ changes = []
78
+ if existing.size > 0
79
+ existing.each do |rr|
80
+ rrc = rr.dup
81
+ rrc[:ttl] = params[:ttl]
82
+ changes << {
83
+ action: 'UPSERT',
84
+ resource_record_set: rrc
85
+ }
86
+ end
87
+ end
88
+ if changes.size > 0
89
+ resp = change_resource_record_sets(zone, changes)
90
+ end
91
+ end
92
+ data = get_item_data(zone, section, params)
93
+ data
94
+ rescue Exception => e
95
+ abort! "#{restyp}/#{params[:name]}: #{e.message}"
96
+ end
97
+ end
98
+ )
99
+ when /ReverseDNSEntries/
100
+ action('create',
101
+ section,
102
+ lambda do |params, restyp, method, resource|
103
+ begin
104
+ data = nil
105
+ zone = get_hosted_zone(params)
106
+ @logger.info "Found hosted_zone #{zone[:id]}::#{zone[:name]}"
107
+ rrs = list_resource_record_sets(zone)
108
+ existing = rrs.select{ |rr| rr[:name] == params[:name] }
109
+ changes = []
110
+ if existing.size > 0
111
+ existing.each do |rr|
112
+ changes << {
113
+ action: 'UPSERT',
114
+ resource_record_set: {
115
+ name: rr[:name],
116
+ type: rr[:type],
117
+ ttl: params[:ttl],
118
+ resource_records: params[:resourcerecords] || rr[:resource_records].map{ |r| { value: params[:value] } }
119
+ }
120
+ }
121
+ end
122
+ else
123
+ changes << {
124
+ action: 'CREATE',
125
+ resource_record_set: {
126
+ name: params[:name],
127
+ type: params[:type],
128
+ ttl: params[:ttl],
129
+ resource_records: params[:resourcerecords] || [ { value: params[:value] } ]
130
+ }
131
+ }
132
+ end
133
+ if changes.size > 0
134
+ resp = change_resource_record_sets(zone, changes)
135
+ end
136
+ data = get_item_data(params, section, params)
137
+ data
138
+ rescue SystemExit => e
139
+ raise e
140
+ rescue Exception => e
141
+ abort! "#{section}/#{params[:name]}: #{e.message}"
142
+ end
143
+ end
144
+ )
145
+ else
146
+ abort! "Unsupported section #{section}"
147
+ end
148
+ end
149
+
150
+ def update(section)
151
+ create section
152
+ end
153
+
154
+ def delete(section)
155
+ case section
156
+ when /PrivateHostedZones/
157
+ action('delete',
158
+ section,
159
+ lambda do |params, restyp, method, resource|
160
+ begin
161
+ data = nil
162
+ set = list_hosted_zones(params)
163
+ unless set.size == 0
164
+ zone = set[0]
165
+ @logger.info "Delete existing hosted_zone #{zone[:id]}::#{zone[:name]}"
166
+ # if zone[:resource_record_set_count] > 2
167
+ # rrs = list_resource_record_sets(zone)
168
+ # changes = []
169
+ # rrs.each do |rr|
170
+ # changes << {
171
+ # action: 'DELETE',
172
+ # resource_record_set: {
173
+ # name: rr[:name],
174
+ # type: rr[:type],
175
+ # ttl: rr[:ttl],
176
+ # resource_records: rr[:resource_records].map{ |r| { value: r[:value] } }
177
+ # }
178
+ # } unless %w(NS SOA).include?(rr[:type])
179
+ # end
180
+ # if changes.size > 0
181
+ # change_resource_record_sets(zone, changes)
182
+ # end
183
+ # end
184
+ @awssdk.delete_hosted_zone( { id: zone[:id] })
185
+ data = get_item_data(Mash.new({ id: zone[:id] }.merge(params)), section, params)
186
+ end
187
+ data
188
+ rescue Exception => e
189
+ abort! "#{restyp}/#{params[:name]}: #{e.message}"
190
+ end
191
+ end
192
+ )
193
+ when /ReverseDNSEntries/
194
+ action('delete',
195
+ section,
196
+ lambda do |params, restyp, method, resource|
197
+ begin
198
+ data = nil
199
+ zone = get_hosted_zone(params)
200
+ @logger.info "Found hosted_zone #{zone[:id]}::#{zone[:name]}"
201
+ changes = []
202
+ rrs = list_resource_record_sets(zone)
203
+ if params[:resourcerecords]
204
+ rrs.select! do |rr|
205
+ rr[:name] == params[:name]
206
+ end
207
+ changes << {
208
+ action: 'DELETE',
209
+ resource_record_set: {
210
+ name: params[:name],
211
+ type: params[:type],
212
+ ttl: params[:ttl],
213
+ resource_records: params[:resourcerecords]
214
+ }
215
+ } if rrs.size > 0
216
+ else
217
+ rrs.each do |rr|
218
+ rrd = rr.dup
219
+ changes << {
220
+ action: 'DELETE',
221
+ resource_record_set: rrd
222
+ } if rr[:name] == params[:name]
223
+ end
224
+ end
225
+ if changes.size > 0
226
+ # abort! "Cannot delete #{params[:name]} from #{zone[:id]}::#{zone[:name]}"
227
+ change_resource_record_sets(zone,changes)
228
+ end
229
+ data = get_item_data(params, section, params)
230
+ data
231
+ rescue NoSuchHostedZone => e
232
+ # [2014-12-15 Christo] TODO: May need to rethink considering the MIA ZoneId a successful deletion. The motivator for this is the fact that a stack will be stuck of someone manually deletes/recreates the zone ... :O
233
+ respond('SUCCESS', nil, e.message)
234
+ rescue SystemExit => e
235
+ raise e
236
+ rescue Exception => e
237
+ abort! "#{section}/#{params[:name]}: #{e.message}"
238
+ end
239
+ end
240
+ )
241
+ else
242
+ abort! "Unsupported section #{section}"
243
+ end
244
+ end
245
+
246
+ def get_hosted_zone(params)
247
+ zone = nil
248
+ begin
249
+ if params[:hostedzoneid]
250
+ resp = @awssdk.get_hosted_zone(id: params[:hostedzoneid])
251
+ if resp
252
+ zone = Mash.new(resp.hosted_zone.to_hash)
253
+ end
254
+ else
255
+ set = list_hosted_zones(params)
256
+ if set.size == 0
257
+ raise NoSuchHostedZone.new("Cannot find hosted zone #{params[:hostedzone]}")
258
+ else
259
+ zone = set[0]
260
+ end
261
+ end
262
+ zone
263
+ rescue Aws::Route53::Errors::NoSuchHostedZone => e
264
+ raise NoSuchHostedZone.new(e.message)
265
+ end
266
+ end
267
+
268
+ def wait_for_insync(resp)
269
+ unless resp and resp[:change_info]
270
+ abort! 'Bad response to change_resource_record_sets'
271
+ end
272
+ change_info = Mash.new(resp[:change_info].to_hash)
273
+ while resp
274
+ resp = @awssdk.get_change(id: change_info[:id])
275
+ unless resp and resp[:change_info]
276
+ abort! "Bad response to get_change[id: #{change_info[:id]}]"
277
+ end
278
+ change_info = Mash.new(resp[:change_info].to_hash)
279
+ if change_info[:status] != 'INSYNC'
280
+ @logger.info "Change #{change_info[:id]} is #{change_info[:status]} ... Waiting #{WAIT_PERIOD_INSYNC}s"
281
+ sleep WAIT_PERIOD_INSYNC
282
+ else
283
+ resp = nil
284
+ end
285
+ end
286
+ end
287
+
288
+ def list_hosted_zones(params)
289
+ set = []
290
+ resp = @awssdk.list_hosted_zones
291
+ if resp
292
+ resp.hosted_zones.map { |item| set << item.to_h }
293
+ while resp[:marker]
294
+ resp = @awssdk.list_hosted_zones(marker: resp[:marker])
295
+ if resp
296
+ resp.hosted_zones.map { |item| set << Mash.new(item.to_h) }
297
+ end
298
+ end
299
+ # Let's see if we have hostedzones which match this :name
300
+ set.select!{ |item| item[:name].match(/^#{params[:hostedzone]}$/) and item[:config][:private_zone] == true }
301
+ end
302
+ set
303
+ end
304
+
305
+ def change_resource_record_sets(zone, changes)
306
+ while true
307
+ begin
308
+ resp = @awssdk.change_resource_record_sets(
309
+ hosted_zone_id: zone[:id],
310
+ change_batch: {
311
+ changes: changes
312
+ }
313
+ )
314
+ wait_for_insync(resp)
315
+ return resp
316
+ rescue Aws::Route53::Errors::PriorRequestNotComplete
317
+ sleep WAIT_PERIOD_INSYNC
318
+ end
319
+ end
320
+ end
321
+
322
+ def list_resource_record_sets(zone)
323
+ rrs = []
324
+ resp = @awssdk.list_resource_record_sets(hosted_zone_id: zone[:id])
325
+ while resp
326
+ resp.resource_record_sets.map { |rr| rrs << Mash.new(rr.to_hash) }
327
+ resp = if resp[:is_truncated]
328
+ @awssdk.list_resource_record_sets(hosted_zone_id: zone[:id], marker: resp[:marker])
329
+ else
330
+ nil
331
+ end
332
+ end
333
+ rrs
334
+ end
335
+
336
+ end
337
+ end
338
+ end
339
+ end