dnsmadeeasy 0.3.5 → 1.0.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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +31 -0
  3. data/.gitignore +2 -0
  4. data/.rubocop.yml +6 -0
  5. data/.rubocop_todo.yml +272 -107
  6. data/.travis.yml +2 -1
  7. data/CLAUDE.md +67 -0
  8. data/Gemfile +9 -1
  9. data/README.md +506 -310
  10. data/Rakefile +4 -4
  11. data/dnsmadeeasy.gemspec +44 -27
  12. data/docs/badges/coverage_badge.svg +21 -0
  13. data/docs/dry-cli-based-tools.webloc +8 -0
  14. data/docs/plan-zone-management.md +756 -0
  15. data/docs/plans/01-plan-ruby4-baseline.md +38 -0
  16. data/docs/plans/02-plan-dry-cli-launcher.md +44 -0
  17. data/docs/plans/03-plan-account-command.md +43 -0
  18. data/docs/plans/04-plan-zone-record-model.md +48 -0
  19. data/docs/plans/05-plan-zone-parser.md +41 -0
  20. data/docs/plans/06-plan-zone-formatter.md +43 -0
  21. data/docs/plans/07-plan-zone-export.md +58 -0
  22. data/docs/plans/08-plan-zone-diff.md +42 -0
  23. data/docs/plans/09-plan-zone-apply.md +69 -0
  24. data/docs/plans/10-plan-docs-cleanup.md +55 -0
  25. data/docs/spec-zone-management.md +242 -0
  26. data/exe/dme +13 -2
  27. data/exe/dmez +6 -0
  28. data/lib/dme.rb +6 -6
  29. data/lib/dnsmadeeasy/api/client.rb +31 -32
  30. data/lib/dnsmadeeasy/cli/box_output.rb +9 -0
  31. data/lib/dnsmadeeasy/cli/commands/account.rb +222 -0
  32. data/lib/dnsmadeeasy/cli/commands/base.rb +119 -0
  33. data/lib/dnsmadeeasy/cli/commands/legacy_operation.rb +30 -0
  34. data/lib/dnsmadeeasy/cli/commands/version.rb +22 -0
  35. data/lib/dnsmadeeasy/cli/commands/zone.rb +326 -0
  36. data/lib/dnsmadeeasy/cli/commands.rb +19 -0
  37. data/lib/dnsmadeeasy/cli/input.rb +14 -0
  38. data/lib/dnsmadeeasy/cli/launcher.rb +53 -0
  39. data/lib/dnsmadeeasy/cli/message_helpers.rb +81 -0
  40. data/lib/dnsmadeeasy/cli/reported_error.rb +10 -0
  41. data/lib/dnsmadeeasy/credentials/api_keys.rb +11 -9
  42. data/lib/dnsmadeeasy/credentials/yaml_file.rb +28 -27
  43. data/lib/dnsmadeeasy/credentials.rb +3 -4
  44. data/lib/dnsmadeeasy/runner.rb +102 -98
  45. data/lib/dnsmadeeasy/types.rb +19 -0
  46. data/lib/dnsmadeeasy/version.rb +2 -1
  47. data/lib/dnsmadeeasy/zone/aname_flattener.rb +63 -0
  48. data/lib/dnsmadeeasy/zone/apply_executor.rb +189 -0
  49. data/lib/dnsmadeeasy/zone/apply_result.rb +22 -0
  50. data/lib/dnsmadeeasy/zone/diff.rb +152 -0
  51. data/lib/dnsmadeeasy/zone/file.rb +26 -0
  52. data/lib/dnsmadeeasy/zone/parser.rb +172 -0
  53. data/lib/dnsmadeeasy/zone/plan.rb +28 -0
  54. data/lib/dnsmadeeasy/zone/plan_action.rb +29 -0
  55. data/lib/dnsmadeeasy/zone/plan_renderer.rb +91 -0
  56. data/lib/dnsmadeeasy/zone/provider_record.rb +18 -0
  57. data/lib/dnsmadeeasy/zone/record.rb +44 -0
  58. data/lib/dnsmadeeasy/zone/record_set.rb +23 -0
  59. data/lib/dnsmadeeasy/zone/remote_adapter.rb +94 -0
  60. data/lib/dnsmadeeasy/zone/remote_records.rb +26 -0
  61. data/lib/dnsmadeeasy/zone/serializer.rb +115 -0
  62. data/lib/dnsmadeeasy.rb +61 -31
  63. metadata +184 -23
@@ -13,7 +13,7 @@ module DnsMadeEasy
13
13
  # Immutable instance with key and secret.
14
14
  #
15
15
  class ApiKeys
16
- API_KEY_REGEX = /^([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})$/.freeze
16
+ API_KEY_REGEX = /^([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})$/
17
17
 
18
18
  attr_reader :api_key,
19
19
  :api_secret,
@@ -24,7 +24,7 @@ module DnsMadeEasy
24
24
  include Sym
25
25
 
26
26
  def initialize(key, secret, encryption_key = nil, default: false, account: nil)
27
- raise InvalidCredentialKeys, "Key and Secret can not be nil" if key.nil? || secret.nil?
27
+ raise InvalidCredentialKeys, 'Key and Secret can not be nil' if key.nil? || secret.nil?
28
28
 
29
29
  @default = default
30
30
  @account = account
@@ -38,17 +38,19 @@ module DnsMadeEasy
38
38
  @api_secret = secret
39
39
  end
40
40
 
41
- raise InvalidCredentialKeys, "Key [#{api_key}] or Secret [#{api_secret}] has failed validation for its format" unless valid?
41
+ return if valid?
42
+
43
+ raise InvalidCredentialKeys,
44
+ "Key [#{api_key}] or Secret [#{api_secret}] has failed validation for its format"
42
45
  end
43
46
 
44
47
  def sym_resolve(encryption_key)
45
48
  null_output = ::File.open('/dev/null', 'w')
46
- result = Sym::Application.new({ cache_passwords: true, key: encryption_key }, $stdin, null_output, null_output).execute
47
- if result.is_a?(Hash)
48
- raise InvalidCredentialKeys, "Unable to decrypt the data, error is: #{result[:exception]}"
49
- else
50
- result
51
- end
49
+ result = Sym::Application.new({ cache_passwords: true, key: encryption_key }, $stdin, null_output,
50
+ null_output).execute
51
+ raise InvalidCredentialKeys, "Unable to decrypt the data, error is: #{result[:exception]}" if result.is_a?(Hash)
52
+
53
+ result
52
54
  end
53
55
 
54
56
  def to_s
@@ -21,28 +21,7 @@ module DnsMadeEasy
21
21
  return nil if mash.nil?
22
22
 
23
23
  creds = if mash.accounts.is_a?(Array)
24
- account = if account
25
- mash.accounts.find { |a| a.name == account.to_s }
26
- elsif
27
- # rubocop:todo Layout/ConditionPosition
28
- mash.accounts.size == 1
29
- # rubocop:enable Layout/ConditionPosition
30
- mash.accounts.first
31
- else
32
- mash.accounts.find(&:default_account)
33
- end
34
-
35
- unless account
36
- raise DnsMadeEasy::APIKeyAndSecretMissingError,
37
- (account ? "account #{account} was not found" : 'default account does not exist')
38
- end
39
-
40
- unless account.credentials
41
- raise DnsMadeEasy::InvalidCredentialsFormatError,
42
- 'Expected account entry to have "credentials" key'
43
- end
44
-
45
- account.credentials
24
+ credentials_from_array(mash.accounts, account&.to_s)
46
25
 
47
26
  elsif mash.credentials
48
27
  mash.credentials
@@ -52,11 +31,11 @@ module DnsMadeEasy
52
31
  'expected either "accounts" or "credentials" as the top-level key'
53
32
  end
54
33
 
55
- # rubocop:todo Style/MultilineTernaryOperator
56
- creds ? ApiKeys.new(creds.api_key,
57
- creds.api_secret,
58
- encryption_key || creds.encryption_key) : nil
59
- # rubocop:enable Style/MultilineTernaryOperator
34
+ return nil unless creds
35
+
36
+ ApiKeys.new creds.api_key,
37
+ creds.api_secret,
38
+ encryption_key || creds.encryption_key
60
39
  end
61
40
 
62
41
  def to_s
@@ -65,6 +44,28 @@ module DnsMadeEasy
65
44
 
66
45
  private
67
46
 
47
+ def credentials_from_array(accounts, account_name = nil)
48
+ account = if account_name
49
+ accounts.find { |a| a.name == account_name }
50
+ elsif accounts.size == 1
51
+ accounts.first
52
+ else
53
+ accounts.find(&:default_account)
54
+ end
55
+
56
+ unless account
57
+ raise DnsMadeEasy::APIKeyAndSecretMissingError,
58
+ (account ? "account #{account} was not found" : 'Default account does not exist')
59
+ end
60
+
61
+ unless account.credentials
62
+ raise DnsMadeEasy::InvalidCredentialsFormatError,
63
+ 'Expected an account entry to have the "credentials" key'
64
+ end
65
+
66
+ account.credentials
67
+ end
68
+
68
69
  def parse!
69
70
  self.mash = Hashie::Extensions::SymbolizeKeys.symbolize_keys(load_hash)
70
71
  end
@@ -82,16 +82,15 @@ module DnsMadeEasy
82
82
  def keys_from_file(file: default_credentials_path,
83
83
  account: nil,
84
84
  encryption_key: nil)
85
-
86
85
  YamlFile.new(file: file).keys(account: account,
87
86
  encryption_key: encryption_key)
88
87
  end
89
88
 
90
89
  # @return String path to the default credentials file.
91
90
  def default_credentials_path(user: nil)
92
- user ? # rubocop:todo Style/MultilineTernaryOperator
93
- File.expand_path(Dir.home(user) + '/.dnsmadeeasy/credentials.yml').freeze :
94
- File.expand_path('~/.dnsmadeeasy/credentials.yml').freeze
91
+ File.expand_path(
92
+ user ? Dir.home(user) + '/.dnsmadeeasy/credentials.yml' : '~/.dnsmadeeasy/credentials.yml'
93
+ )
95
94
  end
96
95
  end
97
96
  end
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
+ # vim: ft=ruby
5
+
4
6
  require 'colored2'
5
7
  require 'awesome_print'
6
8
  require 'dnsmadeeasy'
@@ -9,12 +11,13 @@ require 'etc'
9
11
 
10
12
  module DnsMadeEasy
11
13
  class Runner
12
- SUPPORTED_FORMATS = %w(json json_pretty yaml pp).freeze
14
+ SUPPORTED_FORMATS = %w[json json_pretty yaml pp].freeze
15
+ SUPPORTED_FORMAT_FLAGS = SUPPORTED_FORMATS.map { |f| "--#{f}" }.freeze
13
16
 
14
17
  attr_accessor :format, :argv, :operation
15
18
 
16
19
  def initialize(argv = nil)
17
- self.argv = argv || ARGV.dup
20
+ self.argv = argv || ARGV.dup
18
21
  self.format = process_flags_format
19
22
  end
20
23
 
@@ -44,76 +47,73 @@ module DnsMadeEasy
44
47
  end
45
48
  0
46
49
  rescue ArgumentError => e
50
+ warn(e.backtrace.join("\n").yellow)
47
51
  sig = method_signature(e, method)
48
- # rubocop:todo Style/MultilineTernaryOperator
49
- sig.shift == method.to_s ?
50
- print_signature(method, sig) :
51
- print_error('Action', method.to_s.bold.yellow.to_s, 'has generated an error'.red, exception: e)
52
- # rubocop:enable Style/MultilineTernaryOperator
52
+ print_signature(method, sig) if sig.shift == method.to_s
53
+ print_error('Action', method.to_s.bold.yellow.to_s, 'has generated an error'.red, exception: e)
53
54
  1
54
- rescue NoMethodError => e
55
+ rescue NoMethodError
55
56
  print_error('Action', method.to_s.bold.yellow.to_s, 'is not valid.'.red)
56
57
  puts 'HINT: try running ' + 'dme operations'.bold.green + ' to see the list of valid operations.'
57
58
  2
58
- rescue Net::HTTPServerException => e
59
+ rescue Net::HTTPClientException => e
59
60
  print_error(exception: e)
60
61
  3
61
62
  end
62
63
 
63
64
  def print_error(*args, exception: nil)
64
65
  unless args.empty?
65
- # rubocop:todo Naming/HeredocDelimiterNaming
66
66
  puts <<~EOF
67
- # rubocop:enable Naming/HeredocDelimiterNaming
68
- #{'Error:'.bold.red} #{args.join(' ').red}
67
+ #{'Error:'.bold.red} #{args.join(' ').red}
69
68
  EOF
70
69
  end
71
70
 
72
- if exception
73
- # rubocop:todo Naming/HeredocDelimiterNaming
74
- puts <<~EOF
75
- # rubocop:enable Naming/HeredocDelimiterNaming
76
- #{'Exception: '.bold.red}#{exception.inspect.red}
77
- EOF
78
- end
71
+ return unless exception
72
+
73
+ puts <<~EOF
74
+ #{'Exception: '.bold.red}#{exception.inspect.red}
75
+ EOF
79
76
  end
80
77
 
81
78
  def print_signature(method, sig)
82
- # rubocop:todo Naming/HeredocDelimiterNaming
83
- puts <<~EOF # rubocop:todo Naming/HeredocDelimiterNaming
84
- # rubocop:enable Naming/HeredocDelimiterNaming
85
- #{'Error: '.bold.yellow}
86
- #{'You are missing some arguments for this operation:'.red}
79
+ puts <<~EOF
80
+ #{'Error: '.bold.yellow}
81
+ #{'You are missing some arguments for this operation:'.red}
87
82
 
88
- #{'Correct Usage: '.bold.yellow}
89
- #{method.to_s.bold.green} #{sig.join(' ').blue}
83
+ #{'Correct Usage: '.bold.yellow}
84
+ #{method.to_s.bold.green} #{sig.join(' ').blue}
90
85
 
91
86
  EOF
92
87
  end
93
88
 
94
89
  def process_flags_format
95
- if argv.first&.start_with?('--')
96
- format = argv.shift.gsub(/^--/, '')
97
- if format =~ /^h(elp)?$/i
98
- print_help_message
99
- end
90
+ if argv.first =~ /^op(erations)?$/i
91
+ print_supported_operations
92
+
93
+ elsif argv.include?('--help') || argv.include?('-h')
94
+ print_help_message
95
+
96
+ elsif !(argv & SUPPORTED_FORMAT_FLAGS).empty?
97
+ format_flag = (argv & SUPPORTED_FORMAT_FLAGS).first
98
+ format = format_flag.delete '--'
99
+ argv.delete(format_flag)
100
100
  unless SUPPORTED_FORMATS.include?(format)
101
- puts "Error: format #{format.bold.red} is not supported."
102
- puts "Supported values are: #{SUPPORTED_FORMATS.join(', ')}"
101
+ warn "Error: format #{format.bold.red} is not supported."
102
+ warn "Supported values are: #{SUPPORTED_FORMATS.join(', ')}"
103
103
  exit 1
104
104
  end
105
- elsif argv.first =~ /^op(erations)?$/i
106
- print_supported_operations
107
- exit 0
105
+ format
108
106
  end
109
- format
110
107
  end
111
108
 
112
109
  def configure_authentication
113
- credentials_file = ENV['DNSMADEEASY_CREDENTIALS_FILE'] || DnsMadeEasy::Credentials.default_credentials_path(user: Etc.getlogin)
110
+ credentials_file = ENV['DNSMADEEASY_CREDENTIALS_FILE'] ||
111
+ DnsMadeEasy::Credentials.default_credentials_path(user: Etc.getlogin)
112
+
114
113
  if ENV['DNSMADEEASY_API_KEY'] && ENV['DNSMADEEASY_API_SECRET']
115
114
  DnsMadeEasy.api_key = ENV['DNSMADEEASY_API_KEY']
116
115
  DnsMadeEasy.api_secret = ENV['DNSMADEEASY_API_SECRET']
116
+
117
117
  elsif credentials_file && ::File.exist?(credentials_file)
118
118
  keys = DnsMadeEasy::Credentials.keys_from_file(file: credentials_file)
119
119
  if keys
@@ -122,64 +122,73 @@ module DnsMadeEasy
122
122
  end
123
123
  end
124
124
 
125
- if DnsMadeEasy.api_key.nil?
126
- print_error('API Key/Secret was not detected or read from file')
127
- puts('You can also set two environment variables: ')
128
- puts(' DNSMADEEASY_API_KEY and DNSMADEEASY_API_SECRET')
129
- exit 123
130
- end
125
+ return unless DnsMadeEasy.api_key.nil?
126
+
127
+ print_error('API Key/Secret was not detected or read from file')
128
+ puts('You can also set two environment variables: ')
129
+ puts(' • DNSMADEEASY_API_KEY and ')
130
+ puts(' • DNSMADEEASY_API_SECRET')
131
+ exit 123
131
132
  end
132
133
 
133
134
  def print_usage_message
134
- # rubocop:todo Naming/HeredocDelimiterNaming
135
135
  puts <<~EOF
136
- # rubocop:enable Naming/HeredocDelimiterNaming
137
- #{'Usage:'.bold.yellow}
138
- #{'# Execute an API call:'.dark}
139
- #{"dme [ #{SUPPORTED_FORMATS.map { |f| "--#{f}" }.join(' | ')} ] operation [ arg1 arg2 ... ] ".bold.green}
140
-
141
- #{'# Print suported operations:'.dark}
142
- #{'dme op[erations]'.bold.green}
136
+ #{'Usage:'.bold.yellow}
137
+ #{'# Execute an API call:'.dark}
138
+ #{"dme [ #{SUPPORTED_FORMATS.map { |f| "--#{f}" }.join(' | ')} ] operation [ arg1 arg2 ... ] ".bold.green}
143
139
 
140
+ #{'# Print suported operations:'.dark}
141
+ #{'dme op[erations]'.bold.green}
144
142
  EOF
145
143
  end
146
144
 
147
145
  def print_help_message
148
146
  print_usage_message
149
-
150
- # rubocop:todo Naming/HeredocDelimiterNaming
151
147
  puts <<~EOF
152
- # rubocop:enable Naming/HeredocDelimiterNaming
153
- #{header 'Credentials'}
154
- Store your credentials in a YAML file
155
- #{DnsMadeEasy::Credentials.default_credentials_path(user: ENV['USER'])} as follows:
156
148
 
149
+ #{header 'Credentials'}
150
+ Store your credentials in a YAML file #{DnsMadeEasy::Credentials.default_credentials_path(user: ENV.fetch('USER', nil)).blue}
151
+ as follows:
152
+
153
+ #{'# ~/.dnsmadeeasy/credentials.yml'.bold.black}
157
154
  #{'credentials:
158
- api_key: XXXX
159
- api_secret: YYYY'.bold.magenta}
160
-
161
- Or a multi-account version:
162
-
163
- #{'accounts:
164
- - name: production
165
- credentials:
166
- api_key: XXXX
167
- api_secret: YYYY
168
- encryption_key: my_key
169
- - name: development
170
- default_account: true
171
- credentials:
172
- api_key: ZZZZ
173
- api_secret: WWWW'.bold.magenta}
174
-
175
- #{header 'Examples:'}
176
- #{'dme domain moo.com
177
- dme --json domain moo.com
178
- dme find_all moo.com A www
179
- dme find_first moo.com CNAME vpn-west
180
- dme --yaml find_first moo.com CNAME vpn-west'.green}
155
+ api_key: XXXX
156
+ api_secret: YYYY'.cyan}
157
+
158
+ Or, you can use a multi-account version, but in that case it helps if you
159
+ specify which of the accounts is the default:
181
160
 
182
161
  EOF
162
+
163
+ puts <<-YAML
164
+ #{'# ~/.dnsmadeeasy/credentials.yml'.bold.black}
165
+ #{'accounts:
166
+ - name: production
167
+ credentials:
168
+ api_key: XXXX
169
+ api_secret: YYYY
170
+ encryption_key: my_key
171
+ - name: development
172
+ default_account: true
173
+ credentials:
174
+ api_key: ZZZZ
175
+ api_secret: WWWW'.cyan}
176
+ YAML
177
+
178
+ puts header 'Examples:'
179
+
180
+ puts '
181
+ $ dme domain moo.com
182
+ $ dme domain moo.com --yaml > moo.yml
183
+
184
+ $ dme all moo.com
185
+ $ dme find_all moo.com A www
186
+ $ dme find_first moo.com CNAME vpn-west
187
+
188
+ $ dme update_record moo.com www A 11.3.43.56
189
+ $ dme create_record moo.com ftp CNAME www.moo.com.
190
+
191
+ $ dme --yaml find_first moo.com CNAME vpn-west'.green
183
192
  exit 1
184
193
  end
185
194
 
@@ -188,17 +197,14 @@ module DnsMadeEasy
188
197
  end
189
198
 
190
199
  def print_supported_operations
191
- # rubocop:todo Naming/HeredocDelimiterNaming
192
200
  puts <<~EOF
193
- # rubocop:enable Naming/HeredocDelimiterNaming
194
- #{header 'Actions:'}
195
- Checkout the README and RubyDoc for the arguments to each operation,
196
- which is basically a method on a DnsMadeEasy::Api::Client instance.
197
- #{'http://www.rubydoc.info/gems/dnsmadeeasy/DnsMadeEasy/Api/Client'.blue.bold.underlined}
198
-
199
- #{header 'Valid Operations Are:'}
200
- #{DnsMadeEasy::Api::Client.public_operations.join("\n ").green.bold}
201
+ #{header 'Actions:'}
202
+ Checkout the README and RubyDoc for the arguments to each operation,
203
+ which is basically a method on a DnsMadeEasy::Api::Client instance.
204
+ #{'http://www.rubydoc.info/gems/dnsmadeeasy/DnsMadeEasy/Api/Client'.blue.bold.underlined}
201
205
 
206
+ #{header 'Valid Operations Are:'}
207
+ #{DnsMadeEasy::Api::Client.public_operations.join("\n ").green.bold}
202
208
  EOF
203
209
  end
204
210
 
@@ -210,7 +216,7 @@ module DnsMadeEasy
210
216
  require 'pp'
211
217
  pp result
212
218
  else
213
- m = "to_#{format}".to_sym
219
+ m = :"to_#{format}"
214
220
  puts result.send(m) if result.respond_to?(m)
215
221
  end
216
222
  else
@@ -220,26 +226,24 @@ module DnsMadeEasy
220
226
 
221
227
  # e.backtrack.first looks like this:
222
228
  # ..../dnsmadeeasy/lib/dnsmadeeasy/api/client.rb:143:in `create_a_record'
223
- # rubocop:todo Naming/MethodParameterName
224
229
  def method_signature(e, method)
225
230
  file, line, call_method = e.backtrace.first.split(':')
226
- call_method = call_method.gsub(/[']/, '').split('`').last
231
+ call_method = call_method.gsub('\'', '').split('`').last
227
232
  if call_method && call_method.to_sym == method.to_sym
228
233
  source_line = File.open(file).to_a[line.to_i - 1].chomp!
229
234
  if source_line =~ /def #{method}/
230
- signature = source_line.strip.gsub(/,/, '').split(/[ ()]/)
235
+ signature = source_line.strip.gsub(',', '').split(/[ ()]/)
231
236
  signature.shift # remove def
232
- return signature.reject { |a| a =~ /^([={}\)\(])*$/ }
237
+ return signature.reject { |a| a =~ /^([={})(])*$/ }
233
238
  end
234
239
  end
235
240
  []
236
241
  rescue StandardError
237
242
  []
238
243
  end
239
- # rubocop:enable Naming/MethodParameterName
240
244
 
241
- def puts(*args)
242
- super(*args)
245
+ def puts(*)
246
+ super
243
247
  end
244
248
  end
245
249
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/types'
4
+
5
+ module DnsMadeEasy
6
+ # Shared dry-rb types used by value objects.
7
+ module Types
8
+ include Dry.Types()
9
+
10
+ DNS_RECORD_TYPES = %w[A AAAA CNAME ANAME MX NS PTR SPF SRV TXT].freeze
11
+
12
+ StrictString = Strict::String
13
+ NonEmptyString = StrictString.constrained(min_size: 1)
14
+ RecordType = StrictString.enum(*DNS_RECORD_TYPES)
15
+ Ttl = Coercible::Integer.constrained(gteq: 0)
16
+ OptionalInteger = Coercible::Integer.optional
17
+ OptionalString = StrictString.optional
18
+ end
19
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DnsMadeEasy
4
- VERSION = '0.3.5'
4
+ # Version 1.0+ is supporting zone file manipulation
5
+ VERSION = '1.0.1'
5
6
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/monads'
4
+ require 'resolv'
5
+ require 'dnsmadeeasy/zone/record'
6
+
7
+ module DnsMadeEasy
8
+ module Zone
9
+ # Flattens ANAME records into resolved A records for RFC-portable
10
+ # exports. Resolution is a point-in-time snapshot, so every conversion
11
+ # is reported as a notice.
12
+ class AnameFlattener
13
+ include Dry::Monads[:result]
14
+
15
+ def initialize(records, resolver: nil)
16
+ @records = records
17
+ @resolver = resolver || default_resolver
18
+ end
19
+
20
+ def call
21
+ flattened = []
22
+ notices = []
23
+
24
+ records.each do |record|
25
+ if record.type == 'ANAME'
26
+ addresses = resolve(record)
27
+ return Failure(["Unable to resolve ANAME target #{record.value} for --strict-rfc export"]) if addresses.empty?
28
+
29
+ addresses.each { |address| flattened << record.new(type: 'A', value: address) }
30
+ notices << flatten_notice(record, addresses)
31
+ else
32
+ flattened << record
33
+ end
34
+ end
35
+
36
+ Success([flattened, notices])
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :records,
42
+ :resolver
43
+
44
+ def resolve(record)
45
+ resolver.call(record.value)
46
+ rescue Resolv::ResolvError
47
+ []
48
+ end
49
+
50
+ def flatten_notice(record, addresses)
51
+ "Flattened ANAME #{record.owner} -> #{record.value} into A #{addresses.join(', ')} (point-in-time snapshot)"
52
+ end
53
+
54
+ def default_resolver
55
+ lambda do |target|
56
+ Resolv::DNS.open do |dns|
57
+ dns.getresources(target, Resolv::DNS::Resource::IN::A).map { |resource| resource.address.to_s }
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end