sprinkle_dns 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +3 -0
  6. data/Gemfile.lock +76 -0
  7. data/LICENSE +21 -0
  8. data/README.md +181 -0
  9. data/Rakefile +1 -0
  10. data/examples/example01.rb +47 -0
  11. data/examples/example02.rb +61 -0
  12. data/examples/example03.rb +27 -0
  13. data/examples/example04.rb +22 -0
  14. data/lib/sprinkle_dns/cli/hosted_zone_diff.rb +206 -0
  15. data/lib/sprinkle_dns/cli/interactive_change_request_printer.rb +32 -0
  16. data/lib/sprinkle_dns/cli/propagated_change_request_printer.rb +33 -0
  17. data/lib/sprinkle_dns/client.rb +169 -0
  18. data/lib/sprinkle_dns/config.rb +43 -0
  19. data/lib/sprinkle_dns/core_ext/array_wrap.rb +11 -0
  20. data/lib/sprinkle_dns/core_ext/zonify.rb +4 -0
  21. data/lib/sprinkle_dns/entry_policy_service.rb +100 -0
  22. data/lib/sprinkle_dns/exceptions.rb +9 -0
  23. data/lib/sprinkle_dns/hosted_zone.rb +36 -0
  24. data/lib/sprinkle_dns/hosted_zone_alias.rb +91 -0
  25. data/lib/sprinkle_dns/hosted_zone_domain.rb +18 -0
  26. data/lib/sprinkle_dns/hosted_zone_entry.rb +97 -0
  27. data/lib/sprinkle_dns/providers/mock_client.rb +60 -0
  28. data/lib/sprinkle_dns/providers/route53_client.rb +155 -0
  29. data/lib/sprinkle_dns/version.rb +3 -0
  30. data/lib/sprinkle_dns.rb +5 -0
  31. data/logos/SDNS.png +0 -0
  32. data/logos/SDNS.svg +1 -0
  33. data/readme_files/delete_true_and_diff.png +0 -0
  34. data/readme_files/dry_run_and_diff.png +0 -0
  35. data/readme_files/force_false.png +0 -0
  36. data/spec/spec_helper.rb +110 -0
  37. data/spec/support/entry_helpers.rb +18 -0
  38. data/spec/unit/cli_hosted_zone_diff_spec.rb +30 -0
  39. data/spec/unit/hosted_zone_domain_spec.rb +12 -0
  40. data/spec/unit/hosted_zone_spec.rb +343 -0
  41. data/spec/unit/mock_client_spec.rb +59 -0
  42. data/spec/unit/sprinkle_dns_spec.rb +235 -0
  43. data/sprinkle_dns.gemspec +29 -0
  44. data/test_perms.rb.example +2 -0
  45. 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
@@ -0,0 +1,3 @@
1
+ module SprinkleDNS
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,5 @@
1
+ require 'sprinkle_dns/client'
2
+ require 'sprinkle_dns/providers/route53_client'
3
+
4
+ module SprinkleDNS
5
+ end
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,iVBORw0KGgoAAAANSUhEUgAAAKIAAABXCAYAAACHrZ6xAAAACXBIWXMAAAsSAAALEgHS3X78AAAGe0lEQVR4Xu3cWYgcZRQF4EwWY9wmGldcZtS4ICKKqOPKIIiCkWAUBUEYEBTcoigIAR8UQdEYFxAElwd3jQui+BAFFXcEFeIWN8QF465xSYwP13P4/58U7ZDbrV23p1Ln4Xu7XU1unXuruqY708xsmsiguQUiEdwCkQhugUgEt0AkglsgEsEtEIngFohEcAtEIrgFIhHcApEIboFIBLdAJIJbIBLBLRCJ4BaIRHALRCK4BSIR3AKRCG6BSAS3QCSCWyASwS0QieAWiERwC0QiuAUiEdwCkQhuwRQy9D94x5YBcwuCTRai6X3mhXZT4/V8SnALAlSbxqDMyGZms/poZsWMDl6Am25Kh9QtqFlnABmWzWBzmANbZFv2STnenPwes/P7VXlhbqrOAewMp3euauUW1KgzgAwFA7IVDMO2sB1sDztU7NiD6ut4nHn5mDz23Pw+28DW+X3JC3MTdQ5gGbjOQHrnrDZuQU3KP7yEkM1hCBgOhmYX2B1GYC/YG+bDPv/B/IzH2BNGYQ/YDXbN77Uz7GR+sJukcwjLAHL42GuGksPfGUjv3NXCLahJ2YYlhNxI3FYMxSjsBwfCIXAYHAFjcGR2VBdK7Vh2eD7WoXAwHJTf4wDYP7/nvuYHu0nKAHKYRywNN3vMcHLoGUj2f5alMA5sM7oFNSnbkBPJZjCE3E5sHAPC4IzDibAAFsKpsAhO69Gi/Foe4xQ4GU6CE+D4/D7HwjFwtPkBb4LqEHKIOYAcag4eB27UUiDZdy6BEsaBbUW3oAbVbcjLAyeTTWEI2azjLIXlTJiA8+ACuBgWwyU9WpxfeyGcn493Tj722XBWfq8z4PTMC3cTVAeQw8yhHrcUTg47+83hn2dpGXAplK3oncO+cwtqULYhb5jZAF4mRi01hyFk4yYshWcJXA3XwQ1wIyyDm7qwLONrlsL1+TjXwFVwZT7+FXA5XAaXZl64m6AMIIeYw8eecuA45Owzh55h5BKYa2kpDGwrugU1KEHk5YA3zmwELxecVDZpwlIoGJhb4S64Dx6CR2A5PNqF5Rlf8zA8CPfDPXA33AG3w235fW6Bm80PeBOUAeTwcvg4zBw6BpP9XWgpjBz+UUvLgEuBy2EgW9EtqAH/kXyexQnkpzjeQPPeZdzSxLJZDCFDwvA9Dc/Bi/ASvNKjl/Pr+Prn87FWwDPwFDwJT8Dj8Jj5AW+CMoDsH4eYw8xhY1855BOWhp7DzyXAZTBsaTm0Loh8rsVHCiOWLhO8h5mwNLlsGpvIwLwG78B78CGsgo+6tCrj6z7Ix1iZj/cWvAlvwOv5fV41P9hNUYaPg8dhZj853Awjh51DP25pCXAZcClwOfDctCKI5YMKg8jnW3y0wE91Cyzdy/Aywglm8xgOhucz+Aq+gdXwbQ9WZ3zt1/k4X8Dn+bifwifwsfnBbooyfOwdh459XGEpjBzyJZaGnsPPJTBiaSnwnLQuiHziz3sTPufiIwbet/DGmvc091qa5LcthYVB+gl+hTXwW4/W5NfSz/lYP8IP8D18l3mhbooyeBw69o+BZBg53HdaGvZzLQ0/lwCXAZcCg8hzw3Pknce+cgv6rPropgSRn9x4r8JHDRdZusHmB4sX4F340lJoGKg/YR381YN1FWvzMegP+D3zgtwka7JfLPWNgeTW51A/a2nIr7U09Bx+LgEuA54LnpPWB5HPvXjvstTSJYT3N+9bupxyizE4DNP67O8urJ/EZAHdVKy1DcPGfjGQ3Pq8FVlp6cPaA5YeZXHoOfxjls5BZxBDL89uQZ8NYiNubENuahTEHgziHrFNdGnuUvSn5jbSh5UuRD5HbCs9vulC9F9W2koPtB1Rf2tuM/2JrwtR375pM33poQvR30dsK/ZKXwNzlK042+r/hnZb6YuxXahuRd6X1PGblTYrfWLP9FOBjRiyDVuxhJFTyUsE71fYpH79iq/N2DP9eMpRtmIJIy8NvE9hc4atP79rbrNqz/RzUkeZwGog6/yfHtpKP7DvwpD9O5A00/r/f9+0Weln6W81gAMNIbkFwYYmMV36YrLeeucjjFswhUzWSPF5fZ0S3AKRCG6BSAS3QCSCWyASwS0QieAWiERwC0QiuAUiEdwCkQhugUgEt0AkglsgEsEtEIngFohEcAtEIrgFIhHcApEIboFIBLdAJIJbIBLBLRCJ4BaIRHALRCK4BSIR3AKRCG6BSAS3QCTCP5pZjhpKXV7ZAAAAAElFTkSuQmCC"/><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
@@ -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