customresource-route53 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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