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.
- checksums.yaml +7 -0
- data/.bundle/config +2 -0
- data/.rakeTasks +7 -0
- data/ChangeLog.md +4 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +78 -0
- data/LICENSE +201 -0
- data/LICENSE.txt +203 -0
- data/README.md +2 -0
- data/Rakefile +30 -0
- data/bin/cfn-customresource-route53 +16 -0
- data/customresource-route53.gemspec +34 -0
- data/features/.gitkeep +0 -0
- data/features/customresource-route53.feature +1 -0
- data/features/step_definitions/.gitkeep +0 -0
- data/features/step_definitions/customresource-route53_steps.rb +1 -0
- data/lib/customresource/route53.rb +2 -0
- data/lib/customresource/route53/cli.rb +59 -0
- data/lib/customresource/route53/mixins/actions.rb +339 -0
- data/lib/customresource/route53/mixins/cli.rb +158 -0
- data/lib/customresource/route53/version.rb +6 -0
- data/tests/route53.privatehostedzone.create.log +21 -0
- data/tests/route53.privatehostedzone.delete.log +23 -0
- data/tests/route53.privatehostedzone.update.log +22 -0
- data/tests/route53.reversednsentry.create.id.log +8 -0
- data/tests/route53.reversednsentry.create.name.log +9 -0
- data/tests/route53.reversednsentry.delete.id.log +16 -0
- data/tests/route53.reversednsentry.delete.name.log +17 -0
- data/tests/route53.reversednsentry.delete.name.rr.log +16 -0
- data/tests/route53.reversednsentry.update.name.log +9 -0
- data/tests/update.sh +17 -0
- metadata +248 -0
data/README.md
ADDED
data/Rakefile
ADDED
@@ -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
|
data/features/.gitkeep
ADDED
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
Feature: Blah blah blah
|
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
@wip
|
@@ -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
|