sprinkle_dns 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +76 -0
- data/LICENSE +21 -0
- data/README.md +181 -0
- data/Rakefile +1 -0
- data/examples/example01.rb +47 -0
- data/examples/example02.rb +61 -0
- data/examples/example03.rb +27 -0
- data/examples/example04.rb +22 -0
- data/lib/sprinkle_dns/cli/hosted_zone_diff.rb +206 -0
- data/lib/sprinkle_dns/cli/interactive_change_request_printer.rb +32 -0
- data/lib/sprinkle_dns/cli/propagated_change_request_printer.rb +33 -0
- data/lib/sprinkle_dns/client.rb +169 -0
- data/lib/sprinkle_dns/config.rb +43 -0
- data/lib/sprinkle_dns/core_ext/array_wrap.rb +11 -0
- data/lib/sprinkle_dns/core_ext/zonify.rb +4 -0
- data/lib/sprinkle_dns/entry_policy_service.rb +100 -0
- data/lib/sprinkle_dns/exceptions.rb +9 -0
- data/lib/sprinkle_dns/hosted_zone.rb +36 -0
- data/lib/sprinkle_dns/hosted_zone_alias.rb +91 -0
- data/lib/sprinkle_dns/hosted_zone_domain.rb +18 -0
- data/lib/sprinkle_dns/hosted_zone_entry.rb +97 -0
- data/lib/sprinkle_dns/providers/mock_client.rb +60 -0
- data/lib/sprinkle_dns/providers/route53_client.rb +155 -0
- data/lib/sprinkle_dns/version.rb +3 -0
- data/lib/sprinkle_dns.rb +5 -0
- data/logos/SDNS.png +0 -0
- data/logos/SDNS.svg +1 -0
- data/readme_files/delete_true_and_diff.png +0 -0
- data/readme_files/dry_run_and_diff.png +0 -0
- data/readme_files/force_false.png +0 -0
- data/spec/spec_helper.rb +110 -0
- data/spec/support/entry_helpers.rb +18 -0
- data/spec/unit/cli_hosted_zone_diff_spec.rb +30 -0
- data/spec/unit/hosted_zone_domain_spec.rb +12 -0
- data/spec/unit/hosted_zone_spec.rb +343 -0
- data/spec/unit/mock_client_spec.rb +59 -0
- data/spec/unit/sprinkle_dns_spec.rb +235 -0
- data/sprinkle_dns.gemspec +29 -0
- data/test_perms.rb.example +2 -0
- metadata +192 -0
@@ -0,0 +1,60 @@
|
|
1
|
+
module SprinkleDNS
|
2
|
+
MockChangeRequest = Struct.new(:hosted_zone, :tries, :tries_needed, :in_sync)
|
3
|
+
|
4
|
+
class MockClient
|
5
|
+
attr_reader :hosted_zones
|
6
|
+
|
7
|
+
def initialize(hosted_zones = [])
|
8
|
+
@hosted_zones = hosted_zones
|
9
|
+
end
|
10
|
+
|
11
|
+
def fetch_hosted_zones(filter: [])
|
12
|
+
hosted_zones = []
|
13
|
+
|
14
|
+
if filter.empty?
|
15
|
+
return []
|
16
|
+
end
|
17
|
+
|
18
|
+
@hosted_zones.each do |hz|
|
19
|
+
hz.resource_record_sets.each do |entry|
|
20
|
+
entry.persisted!
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
@hosted_zones.select{|hz| filter.include?(hz.name)}
|
25
|
+
end
|
26
|
+
|
27
|
+
def create_hosted_zones(hosted_zones)
|
28
|
+
change_requests = []
|
29
|
+
|
30
|
+
hosted_zones.each do |hosted_zone|
|
31
|
+
change_requests << MockChangeRequest.new(hosted_zone, 0, rand(3..15), false)
|
32
|
+
end
|
33
|
+
|
34
|
+
change_requests
|
35
|
+
end
|
36
|
+
|
37
|
+
def change_hosted_zones(hosted_zones, configuration)
|
38
|
+
change_requests = []
|
39
|
+
|
40
|
+
hosted_zones.each do |hosted_zone|
|
41
|
+
changes = EntryPolicyService.new(hosted_zone, configuration).compile
|
42
|
+
|
43
|
+
if changes.any?
|
44
|
+
change_requests << MockChangeRequest.new(hosted_zone, 0, rand(3..15), false)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
change_requests
|
49
|
+
end
|
50
|
+
|
51
|
+
def check_change_requests(change_requests)
|
52
|
+
change_requests.reject{|cr| cr.in_sync}.each do |change_request|
|
53
|
+
change_request.tries += 1
|
54
|
+
change_request.in_sync = change_request.tries >= change_request.tries_needed
|
55
|
+
end
|
56
|
+
|
57
|
+
change_requests
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
require 'aws-sdk-route53'
|
2
|
+
require 'sprinkle_dns/version'
|
3
|
+
|
4
|
+
module SprinkleDNS
|
5
|
+
Route53ChangeRequest = Struct.new(:hosted_zone, :change_info_id, :tries, :in_sync)
|
6
|
+
|
7
|
+
class Route53Client
|
8
|
+
attr_reader :hosted_zones
|
9
|
+
|
10
|
+
def initialize(aws_access_key_id, aws_secret_access_key)
|
11
|
+
@api_client = Aws::Route53::Client.new(
|
12
|
+
access_key_id: aws_access_key_id,
|
13
|
+
secret_access_key: aws_secret_access_key,
|
14
|
+
region: 'us-east-1',
|
15
|
+
)
|
16
|
+
@hosted_zone_to_api_mapping = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def fetch_hosted_zones(filter: [])
|
20
|
+
hosted_zones = []
|
21
|
+
more_pages = true
|
22
|
+
next_marker = nil
|
23
|
+
|
24
|
+
if filter.empty?
|
25
|
+
return []
|
26
|
+
end
|
27
|
+
|
28
|
+
while(more_pages)
|
29
|
+
begin
|
30
|
+
data = @api_client.list_hosted_zones({:max_items => nil, :marker => next_marker})
|
31
|
+
rescue Aws::Route53::Errors::AccessDenied
|
32
|
+
# TODO extract this to custom exceptions
|
33
|
+
raise
|
34
|
+
end
|
35
|
+
|
36
|
+
more_pages = data.is_truncated
|
37
|
+
next_marker = data.next_marker
|
38
|
+
|
39
|
+
data.hosted_zones.each do |hosted_zone_data|
|
40
|
+
if filter.include?(hosted_zone_data.name)
|
41
|
+
|
42
|
+
if hosted_zones.map(&:name).include?(hosted_zone_data.name)
|
43
|
+
raise DuplicatedHostedZones, "Whooops, seems like you have the same hosted zone duplicated on your Route53 account!\nIt's the following: #{hz.name}"
|
44
|
+
end
|
45
|
+
|
46
|
+
hosted_zone = HostedZone.new(hosted_zone_data.name)
|
47
|
+
hosted_zone.resource_record_sets = get_resource_record_set!(hosted_zone, hosted_zone_data.id)
|
48
|
+
@hosted_zone_to_api_mapping[hosted_zone.name] = hosted_zone_data.id
|
49
|
+
|
50
|
+
hosted_zones << hosted_zone
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
hosted_zones
|
56
|
+
end
|
57
|
+
|
58
|
+
def create_hosted_zones(hosted_zones)
|
59
|
+
change_requests = []
|
60
|
+
|
61
|
+
hosted_zones.each do |hosted_zone|
|
62
|
+
change_request = @api_client.create_hosted_zone({
|
63
|
+
name: hosted_zone.name,
|
64
|
+
caller_reference: "#{hosted_zone.name}.#{Time.now.to_i}",
|
65
|
+
hosted_zone_config: {
|
66
|
+
comment: "Created by SprinkleDNS #{SprinkleDNS::VERSION}",
|
67
|
+
},
|
68
|
+
})
|
69
|
+
@hosted_zone_to_api_mapping[hosted_zone.name] = change_request.hosted_zone.id
|
70
|
+
change_requests << Route53ChangeRequest.new(hosted_zone, change_request.change_info.id, 0, false)
|
71
|
+
end
|
72
|
+
|
73
|
+
change_requests
|
74
|
+
end
|
75
|
+
|
76
|
+
def change_hosted_zones(hosted_zones, configuration)
|
77
|
+
change_requests = []
|
78
|
+
|
79
|
+
hosted_zones.each do |hosted_zone|
|
80
|
+
changes = EntryPolicyService.new(hosted_zone, configuration).compile
|
81
|
+
|
82
|
+
if changes.any?
|
83
|
+
change_request = @api_client.change_resource_record_sets({
|
84
|
+
hosted_zone_id: @hosted_zone_to_api_mapping[hosted_zone.name],
|
85
|
+
change_batch: {
|
86
|
+
changes: changes,
|
87
|
+
}
|
88
|
+
})
|
89
|
+
|
90
|
+
change_requests << Route53ChangeRequest.new(hosted_zone, change_request.change_info.id, 0, false)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
change_requests
|
95
|
+
end
|
96
|
+
|
97
|
+
def check_change_requests(change_requests)
|
98
|
+
change_requests.reject{|cr| cr.in_sync}.each do |change_request|
|
99
|
+
resp = @api_client.get_change({id: change_request.change_info_id})
|
100
|
+
change_request.in_sync = resp.change_info.status == 'INSYNC'
|
101
|
+
change_request.tries += 1
|
102
|
+
end
|
103
|
+
|
104
|
+
change_requests
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def ignored_record_types
|
110
|
+
['NS','SOA']
|
111
|
+
end
|
112
|
+
|
113
|
+
def get_resource_record_set!(hosted_zone, hosted_zone_id)
|
114
|
+
existing_resource_record_sets = []
|
115
|
+
more_pages = true
|
116
|
+
next_record_name = nil
|
117
|
+
next_record_type = nil
|
118
|
+
next_record_identifier = nil
|
119
|
+
|
120
|
+
while(more_pages)
|
121
|
+
data = @api_client.list_resource_record_sets(
|
122
|
+
hosted_zone_id: hosted_zone_id,
|
123
|
+
max_items: nil,
|
124
|
+
start_record_name: next_record_name,
|
125
|
+
start_record_type: next_record_type,
|
126
|
+
start_record_identifier: next_record_identifier,
|
127
|
+
)
|
128
|
+
more_pages = data.is_truncated
|
129
|
+
|
130
|
+
next_record_name = data.next_record_name
|
131
|
+
next_record_type = data.next_record_type
|
132
|
+
next_record_identifier = data.next_record_identifier
|
133
|
+
|
134
|
+
data.resource_record_sets.each do |rrs|
|
135
|
+
rrs_name = rrs.name
|
136
|
+
rrs_name = rrs_name.gsub('\\052', '*')
|
137
|
+
rrs_name = rrs_name.gsub('\\100', '@')
|
138
|
+
|
139
|
+
next if ignored_record_types.include?(rrs.type) && rrs_name == hosted_zone.name
|
140
|
+
|
141
|
+
entry = if rrs.alias_target
|
142
|
+
HostedZoneAlias.new(rrs.type, rrs_name, rrs.alias_target.hosted_zone_id, rrs.alias_target.dns_name, hosted_zone.name)
|
143
|
+
else
|
144
|
+
HostedZoneEntry.new(rrs.type, rrs_name, rrs.resource_records.map(&:value), rrs.ttl, hosted_zone.name)
|
145
|
+
end
|
146
|
+
|
147
|
+
existing_resource_record_sets << entry
|
148
|
+
entry.persisted! # TODO test if all entries from are persisted = true
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
existing_resource_record_sets
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
data/lib/sprinkle_dns.rb
ADDED
data/logos/SDNS.png
ADDED
Binary file
|
data/logos/SDNS.svg
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 200 200"><defs><style>.cls-1{fill:none;}.cls-2{isolation:isolate;}.cls-3{fill:url(#linear-gradient);}.cls-4{fill:url(#linear-gradient-2);}.cls-5{clip-path:url(#clip-path);}.cls-6{fill:url(#radial-gradient);}.cls-11,.cls-7{opacity:0.2;}.cls-11,.cls-18,.cls-19,.cls-7,.cls-8{mix-blend-mode:multiply;}.cls-7{fill:url(#linear-gradient-3);}.cls-9{fill:#ffd297;}.cls-10{fill:url(#radial-gradient-2);}.cls-11{fill:url(#linear-gradient-4);}.cls-12{fill:url(#radial-gradient-3);}.cls-13{fill:url(#New_Gradient_Swatch_1);}.cls-14{fill:url(#New_Gradient_Swatch_1-2);}.cls-15{fill:url(#New_Gradient_Swatch_1-3);}.cls-16{fill:url(#New_Gradient_Swatch_1-4);}.cls-17{fill:url(#New_Gradient_Swatch_1-5);}.cls-18{opacity:0.8;}.cls-19{opacity:0.1;}.cls-20{fill:url(#linear-gradient-5);}.cls-21{fill:url(#linear-gradient-6);}.cls-22{fill:url(#linear-gradient-7);}.cls-23{fill:url(#linear-gradient-8);}.cls-24{fill:url(#linear-gradient-9);}.cls-25{fill:url(#New_Gradient_Swatch_2);}.cls-26{fill:url(#New_Gradient_Swatch_2-2);}.cls-27{fill:url(#New_Gradient_Swatch_2-3);}.cls-28{fill:url(#New_Gradient_Swatch_2-4);}.cls-29{fill:url(#New_Gradient_Swatch_2-5);}</style><linearGradient id="linear-gradient" x1="100" y1="200" x2="100" gradientUnits="userSpaceOnUse"><stop offset="0"/><stop offset="1" stop-color="#2f2f2f"/></linearGradient><linearGradient id="linear-gradient-2" x1="100" y1="14.24" x2="100" y2="185.76" xlink:href="#linear-gradient"/><clipPath id="clip-path"><circle class="cls-1" cx="100" cy="100" r="85.76"/></clipPath><radialGradient id="radial-gradient" cx="100" cy="158.93" r="48.1" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f0cf81"/><stop offset=".37" stop-color="#edcc7f"/><stop offset=".68" stop-color="#e3c378"/><stop offset=".95" stop-color="#d2b46d"/><stop offset="1" stop-color="#ceb06a"/></radialGradient><linearGradient id="linear-gradient-3" x1="100" y1="175.03" x2="100" y2="122.31" gradientUnits="userSpaceOnUse"><stop offset="0" stop-opacity="0"/><stop offset=".91" stop-opacity=".8"/><stop offset="1"/></linearGradient><radialGradient id="radial-gradient-2" cx="100" cy="118.09" r="47.52" xlink:href="#radial-gradient"/><linearGradient id="linear-gradient-4" x1="100.11" y1="126.18" x2="100.11" y2="106.89" xlink:href="#linear-gradient-3"/><radialGradient id="radial-gradient-3" cx="102.85" cy="60.49" r="51.17" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff"/><stop offset=".31" stop-color="#fcfeff"/><stop offset=".53" stop-color="#f3fbff"/><stop offset=".72" stop-color="#e4f5ff"/><stop offset=".89" stop-color="#ceedff"/><stop offset="1" stop-color="#bbe6ff"/></radialGradient><radialGradient id="New_Gradient_Swatch_1" cx="43.72" cy="97.08" r="16.97" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff"/><stop offset=".49" stop-color="#fdfeff"/><stop offset=".66" stop-color="#f6fcff"/><stop offset=".79" stop-color="#ebf7ff"/><stop offset=".89" stop-color="#daf1ff"/><stop offset=".97" stop-color="#c4e9ff"/><stop offset="1" stop-color="#bbe6ff"/></radialGradient><radialGradient id="New_Gradient_Swatch_1-2" cx="65.01" cy="103.55" r="19.08" xlink:href="#New_Gradient_Swatch_1"/><radialGradient id="New_Gradient_Swatch_1-3" cx="156.28" cy="97.08" r="16.97" xlink:href="#New_Gradient_Swatch_1"/><radialGradient id="New_Gradient_Swatch_1-4" cx="134.99" cy="103.55" r="19.08" xlink:href="#New_Gradient_Swatch_1"/><radialGradient id="New_Gradient_Swatch_1-5" cx="100" cy="104.2" r="23.55" xlink:href="#New_Gradient_Swatch_1"/><linearGradient id="linear-gradient-5" x1="104.86" y1="48.6" x2="130.39" y2="48.6" gradientTransform="translate(-8.54 65.32) rotate(-30)" gradientUnits="userSpaceOnUse"><stop offset="0"/><stop offset=".91"/><stop offset="1"/></linearGradient><linearGradient id="linear-gradient-6" x1="63.16" y1="78.21" x2="83.87" y2="78.21" gradientTransform="translate(57.7 184.38) rotate(-128.3)" xlink:href="#linear-gradient-5"/><linearGradient id="linear-gradient-7" x1="99.18" y1="77.7" x2="122.57" y2="77.7" gradientTransform="translate(-23.77 91.89) rotate(-41.04)" xlink:href="#linear-gradient-5"/><linearGradient id="linear-gradient-8" x1="139.01" y1="70.14" x2="158.06" y2="70.14" gradientTransform="translate(9.41 157.48) rotate(-57.4)" xlink:href="#linear-gradient-5"/><linearGradient id="linear-gradient-9" x1="55.29" y1="52.19" x2="79.55" y2="52.19" gradientTransform="matrix(-.8 -.6 .6 -.8 89.82 134.45)" xlink:href="#linear-gradient-5"/><linearGradient id="New_Gradient_Swatch_2" x1="104.9" y1="47.61" x2="130.43" y2="47.61" gradientTransform="translate(-8.04 65.21) rotate(-30)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ff6601"/><stop offset=".29" stop-color="#ff6301"/><stop offset=".53" stop-color="#ff5901"/><stop offset=".75" stop-color="#ff4801"/><stop offset=".95" stop-color="#ff3001"/><stop offset="1" stop-color="#ff2901"/></linearGradient><linearGradient id="New_Gradient_Swatch_2-2" x1="63.21" y1="77.22" x2="83.92" y2="77.22" gradientTransform="translate(58.55 182.82) rotate(-128.3)" xlink:href="#New_Gradient_Swatch_2"/><linearGradient id="New_Gradient_Swatch_2-3" x1="99.22" y1="76.71" x2="122.62" y2="76.71" gradientTransform="translate(-23.11 91.68) rotate(-41.04)" xlink:href="#New_Gradient_Swatch_2"/><linearGradient id="New_Gradient_Swatch_2-4" x1="139.05" y1="69.16" x2="158.1" y2="69.16" gradientTransform="translate(10.26 157.06) rotate(-57.4)" xlink:href="#New_Gradient_Swatch_2"/><linearGradient id="New_Gradient_Swatch_2-5" x1="55.34" y1="51.2" x2="79.59" y2="51.2" gradientTransform="translate(90.5 132.7) rotate(-142.98)" xlink:href="#New_Gradient_Swatch_2"/></defs><title>Artboard 1</title><g id="Layer_1" class="cls-2"><rect class="cls-3" width="200" height="200" rx="46.3" ry="46.3"/><circle class="cls-4" cx="100" cy="100" r="85.76"/><g class="cls-5"><path class="cls-6" d="M42.67 122.31l11.78 70a315 315 0 0 0 45.55 3.24 315 315 0 0 0 45.55-3.25l11.78-70z"/><path class="cls-7" d="M51.36 173.94l97.1 1.09 8.87-52.72h-114.66l8.69 51.63z"/><g class="cls-8"><path class="cls-9" d="M130.19 194.13c1.48-.14 3-.28 4.41-.44l12.88-12.88.8-4.78zM48.52 125.33l23.75 23.75-21.47 21.48.57 3.4 22.89-22.89 23.74 23.76-20 20 3.74.24 18.26-18.26 18.22 18.19 3.74-.24-20-20 23.75-23.75 22.92 22.99.57-3.4-21.48-21.48 23.75-23.75 4.53 4.53.57-3.4-3.15-3.15 1-1h-6.06l1 1-23.62 23.75-23.74-23.76 1-1h-6l1 1-23.74 23.76-23.76-23.76 1-1h-6.01l1 1-3.15 3.15.57 3.4zm51.48 0l23.75 23.75-23.75 23.76-23.75-23.75zM51.72 176l.8 4.78 12.88 12.92c1.46.16 2.94.3 4.41.44z"/></g><path class="cls-10" d="M35.27 100l2.33 29.94a304.77 304.77 0 0 0 62.4 6.25 304.77 304.77 0 0 0 62.4-6.25l2.33-29.94z"/><path class="cls-11" d="M37.3 126.18h125.35l1.77-19.29h-128.61l1.49 19.29z"/><image width="192" height="86" transform="translate(4 57)" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAABWCAYAAACZ45lZAAAACXBIWXMAAAsSAAALEgHS3X78AAAFAklEQVR4Xu3cT6jlYxzH8bljxn/NMChJd/Inm0lZyLCQnYWpCQvbu2Ihf8pullJEKCsLViRhaTXUJMmSkkKxkEJRIn92j8+75zw5Hbc+R91z7+9872fx2n0393k/z+93zu+ccw+01g5E7Fd2IKIyOxBRmR2IqMwORFRmByIqswMRldmBiMrsQERldiCiMjsQUZkdiKjMDkRUZgciKrMDEZXZgYjK7EBEZXYgojI7EFGZHYiozA5EVGYHIiqzAxGV2YGIyuxARGV2IKIyOxBRmR2YkI1YK67nJNiBXbbdQh6MtTbpw2EHdsHiZj9v5tDM4Vhro+Poungo3P5YKTuwYosbnwU7Xy6Ui+TimUtiLY1+tKQpbWm8eBDcPlkZO7BCixv/gtYX6lI5IpfLFXKlXDXn6pi0+Va0oyEtaUpbGtN68SC4/bISdmBFxskfm5+rA4tztPWFu0auk025Xm6QG+WmWAu0ohntNltvSVPa0pjWND/c+h7YszuBHViRcfVnAViIy+RY64t0XG6WE3Kr3Ca3y0m5Y+bOmKTRh1Y0ox0NaUlT2tKY1jQfh2DP7gJ2YEXG1Z9bIVcDFuTa1q8ct7S+gHfLPXJKTst9cr88EJNGI1rRjHY0pCVNaUtjWh9rvT17YNwF3L7ZcXZgBeav/rwe5JbIVYGF4Wpxl9wrD8qWPCyPyGPyuDwRk0YjWtGMdjSkJU1pS2Na0/xo63tgz+4CdmAFxtWfJwJcAXhdeLz1qwMLxJVjq/VFPCNPybPyvLwgL8pLMUm0oRGtaEY7GtKSpqdbb0xrmtOePcBe2JO7gB1YgXEAeP13pPUrAa8PuUVyldiSJ+VpeVlekzfkLXlb3pF3Y5JoQyNa0Yx2NKQlTWlLY1rTnPbsAfbCvjoAfDDCrY/HYzwhONH660RulVwtWLBXWl/I9+QD+VA+ko9j0mhEK5rRjoa0pCltaUxrmtOePcBeYE/sqwPAByQ8I95s/XUhb5a2Wr9lctVg4c7KJ/KZfCFfylfydUwSbWhEK5rR7mzrLWl6pvXGtKb5Zut7gL2wbw7AeAPMH80HJTwr5nHZKXmo9deNr7Z+9WABWcxv5Xv5QX6Un2KSaEMjWtGMdjSkJU1pS2Na05z27AH2AnuCveH2z46yAyswDgAflfMmiA9MeGbMGySeHDwjr8v78ql80/qi/iK/ym8zv8ekjC40ohXNaEdDWtKUtjSmNc1pzx5gL+yLAzD/CHQcAB6J8aaIZ8ePynPyppyTz+U7+bn1xf1D/pS/Zv6OSRg9aEMjWtGMdjQ813pT2tKY1idbb794AHb1ZZAd2GE5ADXlAPwPeQlUU14CLSlvguvKm+Al5DFoXXkMuoR8EFZbPggz8lWIuvJViCXky3B15ctwS8jXoWujUb4ObeQHMXXlBzFLmL8L5CeRdeQnkUvaaP/eBcYh4GrALZHXhflR/HqjVX4Ub4y7wDgE3Ap5PcjiHGn5tyjrar5V/i2KMU7+/EHIP8aqI/8Yawkb7b8HAYda/jViBaPj6Dq/8fd088MO7LKNbRyMtbZdU7cPdo0dmJDtFjKmy/WcBDsQUZkdiKjMDkRUZgciKrMDEZXZgYjK7EBEZXYgojI7EFGZHYiozA5EVGYHIiqzAxGV2YGIyuxARGV2IKIyOxBRmR2IqMwORFRmByIqswMRldmBiMrsQERldiCiMjsQUZkdiKjMDkRU9g+3u1JaB07ChQAAAABJRU5ErkJggg=="/><image width="162" height="87" transform="translate(19 35)" xlink:href="data:image/png;base64,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"/><path class="cls-12" d="M102.85 28.12a64.73 64.73 0 0 0-64.73 64.73h129.46a64.73 64.73 0 0 0-64.73-64.73z"/><circle class="cls-13" cx="43.72" cy="97.08" r="16.97"/><circle class="cls-14" cx="65.01" cy="103.55" r="19.08"/><circle class="cls-15" cx="156.28" cy="97.08" r="16.97"/><circle class="cls-16" cx="134.99" cy="103.55" r="19.08"/><circle class="cls-17" cx="100" cy="104.2" r="23.55"/><image width="180" height="101" transform="translate(10 37)" xlink:href="data:image/png;base64,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"/><image width="174" height="81" transform="translate(13 40)" xlink:href="data:image/png;base64,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" class="cls-18"/><g class="cls-19"><rect class="cls-20" x="103.92" y="44.84" width="27.4" height="7.51" rx="2.67" ry="2.67" transform="translate(40.06 -52.3) rotate(30)"/><rect class="cls-21" x="59.82" y="74.46" width="27.4" height="7.51" rx="2.67" ry="2.67" transform="translate(180.46 68.98) rotate(128.3)"/><rect class="cls-22" x="97.17" y="73.94" width="27.4" height="7.51" rx="2.67" ry="2.67" transform="translate(78.26 -53.7) rotate(41.04)"/><rect class="cls-23" x="134.83" y="66.39" width="27.4" height="7.51" rx="2.67" ry="2.67" transform="translate(127.6 -92.78) rotate(57.4)"/><rect class="cls-24" x="53.72" y="48.43" width="27.4" height="7.51" rx="2.67" ry="2.67" transform="translate(152.68 53.26) rotate(142.98)"/></g><rect class="cls-25" x="103.97" y="43.85" width="27.4" height="7.51" rx="2.67" ry="2.67" transform="translate(39.57 -52.46) rotate(30)"/><rect class="cls-26" x="59.86" y="73.47" width="27.4" height="7.51" rx="2.67" ry="2.67" transform="translate(179.76 67.35) rotate(128.3)"/><rect class="cls-27" x="97.22" y="72.95" width="27.4" height="7.51" rx="2.67" ry="2.67" transform="translate(77.62 -53.98) rotate(41.04)"/><rect class="cls-28" x="134.88" y="65.4" width="27.4" height="7.51" rx="2.67" ry="2.67" transform="translate(126.78 -93.27) rotate(57.4)"/><rect class="cls-29" x="53.77" y="47.44" width="27.4" height="7.51" rx="2.67" ry="2.67" transform="matrix(-.8 .6 -.6 -.8 152.16 51.45)"/></g></g></svg>
|
Binary file
|
Binary file
|
Binary file
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
|
+
# The generated `.rspec` file contains `--require spec_helper` which will cause
|
4
|
+
# this file to always be loaded, without a need to explicitly require it in any
|
5
|
+
# files.
|
6
|
+
#
|
7
|
+
# Given that it is always loaded, you are encouraged to keep this file as
|
8
|
+
# light-weight as possible. Requiring heavyweight dependencies from this file
|
9
|
+
# will add to the boot time of your test suite on EVERY test run, even for an
|
10
|
+
# individual file that may not need all of that loaded. Instead, consider making
|
11
|
+
# a separate helper file that requires the additional dependencies and performs
|
12
|
+
# the additional setup, and require it from the spec files that actually need
|
13
|
+
# it.
|
14
|
+
#
|
15
|
+
# The `.rspec` file also contains a few flags that are not defaults but that
|
16
|
+
# users commonly want.
|
17
|
+
#
|
18
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
19
|
+
|
20
|
+
require 'rubygems'
|
21
|
+
require 'bundler/setup'
|
22
|
+
|
23
|
+
require 'simplecov'
|
24
|
+
SimpleCov.start
|
25
|
+
|
26
|
+
require 'vcr'
|
27
|
+
|
28
|
+
require './spec/support/entry_helpers'
|
29
|
+
|
30
|
+
VCR.configure do |c|
|
31
|
+
c.allow_http_connections_when_no_cassette = false
|
32
|
+
c.hook_into :webmock
|
33
|
+
c.cassette_library_dir = 'spec/cassettes'
|
34
|
+
end
|
35
|
+
|
36
|
+
require 'sprinkle_dns'
|
37
|
+
require 'sprinkle_dns/providers/mock_client'
|
38
|
+
|
39
|
+
RSpec.configure do |config|
|
40
|
+
# rspec-expectations config goes here. You can use an alternate
|
41
|
+
# assertion/expectation library such as wrong or the stdlib/minitest
|
42
|
+
# assertions if you prefer.
|
43
|
+
config.expect_with :rspec do |expectations|
|
44
|
+
# This option will default to `true` in RSpec 4. It makes the `description`
|
45
|
+
# and `failure_message` of custom matchers include text for helper methods
|
46
|
+
# defined using `chain`, e.g.:
|
47
|
+
# be_bigger_than(2).and_smaller_than(4).description
|
48
|
+
# # => "be bigger than 2 and smaller than 4"
|
49
|
+
# ...rather than:
|
50
|
+
# # => "be bigger than 2"
|
51
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
52
|
+
end
|
53
|
+
|
54
|
+
# rspec-mocks config goes here. You can use an alternate test double
|
55
|
+
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
56
|
+
config.mock_with :rspec do |mocks|
|
57
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
58
|
+
# a real object. This is generally recommended, and will default to
|
59
|
+
# `true` in RSpec 4.
|
60
|
+
mocks.verify_partial_doubles = true
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
# These two settings work together to allow you to limit a spec run
|
65
|
+
# to individual examples or groups you care about by tagging them with
|
66
|
+
# `:focus` metadata. When nothing is tagged with `:focus`, all examples
|
67
|
+
# get run.
|
68
|
+
config.filter_run :focus
|
69
|
+
config.run_all_when_everything_filtered = true
|
70
|
+
|
71
|
+
# Limits the available syntax to the non-monkey patched syntax that is
|
72
|
+
# recommended. For more details, see:
|
73
|
+
# - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
|
74
|
+
# - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
75
|
+
# - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
|
76
|
+
config.disable_monkey_patching!
|
77
|
+
|
78
|
+
# This setting enables warnings. It's recommended, but in some cases may
|
79
|
+
# be too noisy due to issues in dependencies.
|
80
|
+
config.warnings = true
|
81
|
+
|
82
|
+
# Many RSpec users commonly either run the entire suite or an individual
|
83
|
+
# file, and it's useful to allow more verbose output when running an
|
84
|
+
# individual spec file.
|
85
|
+
if config.files_to_run.one?
|
86
|
+
# Use the documentation formatter for detailed output,
|
87
|
+
# unless a formatter has already been configured
|
88
|
+
# (e.g. via a command-line flag).
|
89
|
+
config.default_formatter = 'doc'
|
90
|
+
end
|
91
|
+
|
92
|
+
# Print the 10 slowest examples and example groups at the
|
93
|
+
# end of the spec run, to help surface which specs are running
|
94
|
+
# particularly slow.
|
95
|
+
config.profile_examples = 10
|
96
|
+
|
97
|
+
# Run specs in random order to surface order dependencies. If you find an
|
98
|
+
# order dependency and want to debug it, you can fix the order by providing
|
99
|
+
# the seed, which is printed after each run.
|
100
|
+
# --seed 1234
|
101
|
+
config.order = :random
|
102
|
+
|
103
|
+
# Seed global randomization in this process using the `--seed` CLI option.
|
104
|
+
# Setting this allows you to use `--seed` to deterministically reproduce
|
105
|
+
# test failures related to randomization by passing the same `--seed` value
|
106
|
+
# as the one that triggered the failure.
|
107
|
+
Kernel.srand config.seed
|
108
|
+
|
109
|
+
config.include EntryHelpers
|
110
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module EntryHelpers
|
2
|
+
def sprinkle_entry(type, name, value, ttl = 3600, hosted_zone_name = nil)
|
3
|
+
name = zonify!(name)
|
4
|
+
|
5
|
+
if ['CNAME', 'MX'].include?(type)
|
6
|
+
value = Array.wrap(value)
|
7
|
+
value.map!{|v| zonify!(v)}
|
8
|
+
end
|
9
|
+
SprinkleDNS::HostedZoneEntry.new(type, name, Array.wrap(value), ttl, hosted_zone_name)
|
10
|
+
end
|
11
|
+
|
12
|
+
def sprinkle_alias(type, name, hosted_zone_id, dns_name, hosted_zone_name = nil)
|
13
|
+
name = zonify!(name)
|
14
|
+
dns_name = zonify!(dns_name)
|
15
|
+
|
16
|
+
SprinkleDNS::HostedZoneAlias.new(type, name, hosted_zone_id, dns_name, hosted_zone_name)
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe SprinkleDNS::CLI::HostedZoneDiff do
|
4
|
+
it 'should print' do
|
5
|
+
hz = SprinkleDNS::HostedZone.new('test.colourful.com.')
|
6
|
+
pe01 = SprinkleDNS::HostedZoneEntry.new('A', 'noref.test.colourful.com.', Array.wrap('80.80.80.80'), 3600, hz.name)
|
7
|
+
pe02 = SprinkleDNS::HostedZoneEntry.new('A', 'updateme.test.colourful.com.', Array.wrap('80.80.80.80'), 3600, hz.name)
|
8
|
+
pe03 = SprinkleDNS::HostedZoneEntry.new('TXT', 'txt.test.colourful.com.', %Q{"#{Time.now.to_i}"}, 60, hz.name)
|
9
|
+
pe04 = SprinkleDNS::HostedZoneEntry.new('A', 'nochange.test.colourful.com.', Array.wrap('80.80.80.80'), 60, hz.name)
|
10
|
+
|
11
|
+
# We are emulating that these records are already live, mark them as persisted
|
12
|
+
[pe01, pe02, pe03, pe04].each do |persisted|
|
13
|
+
persisted.persisted!
|
14
|
+
hz.resource_record_sets << persisted
|
15
|
+
end
|
16
|
+
|
17
|
+
client = SprinkleDNS::MockClient.new([hz])
|
18
|
+
sdns = SprinkleDNS::Client.new(client, dry_run: true, diff: false)
|
19
|
+
|
20
|
+
sdns.entry('A', 'updateme.test.colourful.com.', '90.90.90.90', 7200, 'test.colourful.com')
|
21
|
+
sdns.entry('TXT', 'txt.test.colourful.com', %Q{"#{Time.now.to_i+10}"}, 60, 'test.colourful.com')
|
22
|
+
sdns.entry('A', 'nochange.test.colourful.com.', '80.80.80.80', 60, 'test.colourful.com')
|
23
|
+
|
24
|
+
existing_hosted_zones, _ = sdns.sprinkle!
|
25
|
+
|
26
|
+
SprinkleDNS::CLI::HostedZoneDiff.new.diff(existing_hosted_zones, [], sdns.config).each do |line|
|
27
|
+
puts line.join(' ')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe SprinkleDNS::HostedZoneDomain do
|
4
|
+
it "should zone properly" do
|
5
|
+
expect( SprinkleDNS::HostedZoneDomain::parse('servnice.com') ).to eq 'servnice.com.'
|
6
|
+
expect( SprinkleDNS::HostedZoneDomain::parse('servnice.com.') ).to eq 'servnice.com.'
|
7
|
+
expect( SprinkleDNS::HostedZoneDomain::parse('servnice.co.uk') ).to eq 'servnice.co.uk.'
|
8
|
+
expect( SprinkleDNS::HostedZoneDomain::parse('www.billetto.co.uk') ).to eq 'billetto.co.uk.'
|
9
|
+
expect( SprinkleDNS::HostedZoneDomain::parse('*.billetto.co.uk') ).to eq 'billetto.co.uk.'
|
10
|
+
expect( SprinkleDNS::HostedZoneDomain::parse('mesmtp._domainkey.servnice.com') ).to eq 'servnice.com.'
|
11
|
+
end
|
12
|
+
end
|