dnsmadeeasy 0.4.0 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +5 -10
- data/.gitignore +2 -0
- data/.rubocop.yml +5 -4
- data/.rubocop_todo.yml +251 -143
- data/CLAUDE.md +67 -0
- data/Gemfile +9 -1
- data/README.md +858 -0
- data/Rakefile +4 -4
- data/dnsmadeeasy.gemspec +42 -26
- data/docs/badges/coverage_badge.svg +21 -0
- data/docs/dry-cli-based-tools.webloc +8 -0
- data/docs/plan-zone-management.md +756 -0
- data/docs/plans/01-plan-ruby4-baseline.md +38 -0
- data/docs/plans/02-plan-dry-cli-launcher.md +44 -0
- data/docs/plans/03-plan-account-command.md +43 -0
- data/docs/plans/04-plan-zone-record-model.md +48 -0
- data/docs/plans/05-plan-zone-parser.md +41 -0
- data/docs/plans/06-plan-zone-formatter.md +43 -0
- data/docs/plans/07-plan-zone-export.md +58 -0
- data/docs/plans/08-plan-zone-diff.md +42 -0
- data/docs/plans/09-plan-zone-apply.md +69 -0
- data/docs/plans/10-plan-docs-cleanup.md +55 -0
- data/docs/spec-zone-management.md +242 -0
- data/exe/dme +13 -2
- data/exe/dmez +6 -0
- data/lib/dme.rb +7 -2
- data/lib/dnsmadeeasy/api/client.rb +32 -26
- data/lib/dnsmadeeasy/cli/box_output.rb +9 -0
- data/lib/dnsmadeeasy/cli/commands/account.rb +222 -0
- data/lib/dnsmadeeasy/cli/commands/base.rb +119 -0
- data/lib/dnsmadeeasy/cli/commands/legacy_operation.rb +30 -0
- data/lib/dnsmadeeasy/cli/commands/version.rb +22 -0
- data/lib/dnsmadeeasy/cli/commands/zone.rb +326 -0
- data/lib/dnsmadeeasy/cli/commands.rb +19 -0
- data/lib/dnsmadeeasy/cli/input.rb +14 -0
- data/lib/dnsmadeeasy/cli/launcher.rb +53 -0
- data/lib/dnsmadeeasy/cli/message_helpers.rb +81 -0
- data/lib/dnsmadeeasy/cli/reported_error.rb +10 -0
- data/lib/dnsmadeeasy/credentials/api_keys.rb +11 -9
- data/lib/dnsmadeeasy/credentials.rb +0 -1
- data/lib/dnsmadeeasy/runner.rb +24 -24
- data/lib/dnsmadeeasy/types.rb +19 -0
- data/lib/dnsmadeeasy/version.rb +2 -1
- data/lib/dnsmadeeasy/zone/aname_flattener.rb +63 -0
- data/lib/dnsmadeeasy/zone/apply_executor.rb +189 -0
- data/lib/dnsmadeeasy/zone/apply_result.rb +22 -0
- data/lib/dnsmadeeasy/zone/diff.rb +152 -0
- data/lib/dnsmadeeasy/zone/file.rb +26 -0
- data/lib/dnsmadeeasy/zone/parser.rb +172 -0
- data/lib/dnsmadeeasy/zone/plan.rb +28 -0
- data/lib/dnsmadeeasy/zone/plan_action.rb +29 -0
- data/lib/dnsmadeeasy/zone/plan_renderer.rb +91 -0
- data/lib/dnsmadeeasy/zone/provider_record.rb +18 -0
- data/lib/dnsmadeeasy/zone/record.rb +44 -0
- data/lib/dnsmadeeasy/zone/record_set.rb +23 -0
- data/lib/dnsmadeeasy/zone/remote_adapter.rb +94 -0
- data/lib/dnsmadeeasy/zone/remote_records.rb +26 -0
- data/lib/dnsmadeeasy/zone/serializer.rb +115 -0
- data/lib/dnsmadeeasy.rb +61 -27
- metadata +184 -25
- data/.dme-help.png +0 -0
- data/README.adoc +0 -690
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
# Plan: Ruby 4, dry-cli, and Zone File Management
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
This gem is currently a compact Ruby API client and CLI for DNS Made Easy. The CLI is implemented by `DnsMadeEasy::Runner`, which forwards arbitrary operation names directly to `DnsMadeEasy.client`.
|
|
6
|
+
|
|
7
|
+
The requested direction is a major-version refactor:
|
|
8
|
+
|
|
9
|
+
- drop Ruby 2 support
|
|
10
|
+
- support Ruby 4
|
|
11
|
+
- bump the gem to `1.0.0`
|
|
12
|
+
- replace the current CLI runner with `dry-cli`
|
|
13
|
+
- add stronger validation using focused dry-rb gems
|
|
14
|
+
- preserve existing API operations under an `account` command namespace
|
|
15
|
+
- add declarative zone-file workflows under a new `zone` command namespace
|
|
16
|
+
|
|
17
|
+
The zone-management specification lives in `docs/spec-zone-management.md`.
|
|
18
|
+
|
|
19
|
+
## Source Material Reviewed
|
|
20
|
+
|
|
21
|
+
- Local current implementation:
|
|
22
|
+
- `lib/dnsmadeeasy/api/client.rb`
|
|
23
|
+
- `lib/dnsmadeeasy/runner.rb`
|
|
24
|
+
- `lib/dnsmadeeasy/credentials*.rb`
|
|
25
|
+
- `exe/dme`
|
|
26
|
+
- current specs
|
|
27
|
+
- Local reference project:
|
|
28
|
+
- `../githuh/lib/githuh/cli/launcher.rb`
|
|
29
|
+
- `../githuh/lib/githuh/cli/commands/base.rb`
|
|
30
|
+
- `../githuh/spec/support/aruba_helper.rb`
|
|
31
|
+
- `../githuh/spec/spec_helper.rb`
|
|
32
|
+
- External references:
|
|
33
|
+
- dry-rb/Hanakai overview and dry CLI/type/validation docs index
|
|
34
|
+
- Konstantin Gredeskoul's CLI + Aruba article
|
|
35
|
+
- `aeden/dns-zonefile` README
|
|
36
|
+
|
|
37
|
+
## Design Principles
|
|
38
|
+
|
|
39
|
+
1. Keep `DnsMadeEasy::Api::Client` as the provider boundary.
|
|
40
|
+
2. Treat `dry-cli` as command dispatch only, not as business logic.
|
|
41
|
+
3. Introduce typed value objects before introducing diff/apply logic.
|
|
42
|
+
4. Make every command testable in process through Aruba.
|
|
43
|
+
5. Keep zone parsing isolated behind an adapter so `DNS::Zonefile::*` classes do not leak through the implementation.
|
|
44
|
+
6. Make `plan` the safety gate for `apply`.
|
|
45
|
+
7. Keep deletion opt-in and out of the first implementation unless explicitly requested.
|
|
46
|
+
8. Use `tty-spinner` for long-running groups of independent provider operations.
|
|
47
|
+
|
|
48
|
+
## Parallel Provider Operations
|
|
49
|
+
|
|
50
|
+
The gem includes `tty-spinner` for visual progress during multi-step provider work. Zone workflows should split independent API work into bounded chunks, execute those chunks with worker threads, and attach one `TTY::Spinner` to each active worker or chunk.
|
|
51
|
+
|
|
52
|
+
Use `TTY::Spinner::Multi` for managing multiple concurrent thread progress indicators:
|
|
53
|
+
https://github.com/piotrmurach/tty-spinner#5-ttyspinnermulti-api
|
|
54
|
+
|
|
55
|
+
Rules:
|
|
56
|
+
|
|
57
|
+
- Use bounded concurrency; do not start one unbounded thread per DNS record.
|
|
58
|
+
- Only parallelize independent API calls.
|
|
59
|
+
- Preserve deterministic final output by collecting results and sorting/rendering after workers finish.
|
|
60
|
+
- Keep `plan` rendering deterministic and mostly non-animated; spinners belong to long-running `export` and `apply` execution paths.
|
|
61
|
+
- Provide a no-op or fake spinner in specs so test output is stable.
|
|
62
|
+
- Aggregate worker errors and report which chunk/action failed.
|
|
63
|
+
- Avoid parallel delete behavior until destructive operations are explicitly enabled and tested.
|
|
64
|
+
|
|
65
|
+
## Zone File Compatibility and Normalization
|
|
66
|
+
|
|
67
|
+
The `aeden/dns-zonefile` gem is sufficient for version 1 parsing. We are comfortable using it to parse downloaded or standard RFC-style zone files, and we accept its current limitations because this feature does not need to preserve every possible BIND extension or provider-specific behavior.
|
|
68
|
+
|
|
69
|
+
Export output is not intended to round-trip arbitrary zone-file formatting. Instead, `dme zone export` and `dme zone fmt` produce a normalized, readable, deterministic zone file.
|
|
70
|
+
|
|
71
|
+
Normalized zone characteristics:
|
|
72
|
+
|
|
73
|
+
- one zone per file
|
|
74
|
+
- explicit owner on every record
|
|
75
|
+
- `$ORIGIN`
|
|
76
|
+
- `$TTL`
|
|
77
|
+
- `@` for zone apex
|
|
78
|
+
- deterministic ordering
|
|
79
|
+
- normalized, aligned whitespace
|
|
80
|
+
- provider-managed SOA omitted
|
|
81
|
+
- apex NS records omitted by default
|
|
82
|
+
- delegated NS records preserved
|
|
83
|
+
|
|
84
|
+
Example:
|
|
85
|
+
|
|
86
|
+
```dns
|
|
87
|
+
$ORIGIN example.com.
|
|
88
|
+
$TTL 300
|
|
89
|
+
|
|
90
|
+
@ IN A 203.0.113.10
|
|
91
|
+
www IN CNAME @
|
|
92
|
+
|
|
93
|
+
@ IN MX 10 mail.example.com.
|
|
94
|
+
|
|
95
|
+
@ IN TXT "v=spf1 include:_spf.google.com ~all"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`HTTPRED` cannot be represented in a standard RFC zone file.
|
|
99
|
+
|
|
100
|
+
Version 1 behavior:
|
|
101
|
+
|
|
102
|
+
- omit `HTTPRED` records from exports
|
|
103
|
+
- emit a warning listing omitted records
|
|
104
|
+
|
|
105
|
+
Future versions may support provider-specific comment directives for records such as `HTTPRED`, but version 1 should keep the exported zone file standard and predictable.
|
|
106
|
+
|
|
107
|
+
## Phase 0: Baseline and Version Policy
|
|
108
|
+
|
|
109
|
+
### Objective
|
|
110
|
+
|
|
111
|
+
Establish a clean Ruby 4 baseline before changing CLI architecture.
|
|
112
|
+
|
|
113
|
+
### Work
|
|
114
|
+
|
|
115
|
+
1. Decide the exact supported Ruby range.
|
|
116
|
+
- Recommended: `spec.required_ruby_version = '>= 3.2', '< 4.1'` initially.
|
|
117
|
+
- If the goal is Ruby 4 only, use `~> 4.0`, matching the `githuh` reference.
|
|
118
|
+
- Do not keep Ruby 2 compatibility constraints.
|
|
119
|
+
2. Bump `DnsMadeEasy::VERSION` from `0.4.0` to `1.0.0`.
|
|
120
|
+
3. Update the GitHub Actions workflow.
|
|
121
|
+
- Replace Ruby 2.7 with Ruby 4.0.
|
|
122
|
+
- Use current `ruby/setup-ruby`.
|
|
123
|
+
- Enable Bundler cache.
|
|
124
|
+
4. Add or update dependencies.
|
|
125
|
+
- Runtime:
|
|
126
|
+
- `dry-cli`
|
|
127
|
+
- `dry-struct`
|
|
128
|
+
- `dry-types`
|
|
129
|
+
- `dry-validation`
|
|
130
|
+
- `dns-zonefile`
|
|
131
|
+
- `tsort`, because `sym` warns that `tsort` will no longer be bundled by default in Ruby 4.1
|
|
132
|
+
- `tty-spinner`
|
|
133
|
+
- Development:
|
|
134
|
+
- `aruba`
|
|
135
|
+
5. Run the current suite.
|
|
136
|
+
|
|
137
|
+
### Acceptance Criteria
|
|
138
|
+
|
|
139
|
+
- `bundle exec rspec` passes on Ruby 4.
|
|
140
|
+
- CI targets Ruby 4.
|
|
141
|
+
- Version reports `1.0.0`.
|
|
142
|
+
- Ruby 2 support is explicitly removed from gem metadata and CI.
|
|
143
|
+
|
|
144
|
+
## Phase 1: Introduce dry-cli Shell Without Changing Behavior
|
|
145
|
+
|
|
146
|
+
### Objective
|
|
147
|
+
|
|
148
|
+
Add a dry-cli launcher and command registry while preserving the current user-visible behavior as much as possible.
|
|
149
|
+
|
|
150
|
+
### Proposed Files
|
|
151
|
+
|
|
152
|
+
```text
|
|
153
|
+
lib/dnsmadeeasy/cli/launcher.rb
|
|
154
|
+
lib/dnsmadeeasy/cli/commands/base.rb
|
|
155
|
+
lib/dnsmadeeasy/cli/commands/version.rb
|
|
156
|
+
lib/dnsmadeeasy/cli/commands/account.rb
|
|
157
|
+
spec/support/aruba_helper.rb
|
|
158
|
+
spec/lib/dnsmadeeasy/cli/launcher_spec.rb
|
|
159
|
+
spec/lib/dnsmadeeasy/cli/commands/version_spec.rb
|
|
160
|
+
spec/lib/dnsmadeeasy/cli/commands/account_spec.rb
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Work
|
|
164
|
+
|
|
165
|
+
1. Add `DnsMadeEasy::CLI::Commands` as a `Dry::CLI::Registry`.
|
|
166
|
+
2. Add `DnsMadeEasy::CLI::Launcher`.
|
|
167
|
+
- Accept `argv`, `stdin`, `stdout`, `stderr`, and optional `kernel`.
|
|
168
|
+
- Send all command output through injected streams.
|
|
169
|
+
- Do not call raw `STDOUT`, `STDERR`, or `Kernel.exit` from command classes.
|
|
170
|
+
3. Update `exe/dme` to instantiate the launcher:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
DnsMadeEasy::CLI::Launcher.new(ARGV.dup).execute!
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
4. Add `Base < Dry::CLI::Command`.
|
|
177
|
+
- Centralize authentication setup.
|
|
178
|
+
- Centralize output format helpers.
|
|
179
|
+
- Centralize client creation.
|
|
180
|
+
- Support common options:
|
|
181
|
+
- `--credentials-file`
|
|
182
|
+
- `--account`
|
|
183
|
+
- `--sandbox`
|
|
184
|
+
- `--format=json|json_pretty|yaml|pp`
|
|
185
|
+
- `--verbose`
|
|
186
|
+
5. Add Aruba in-process test setup based on `../githuh`.
|
|
187
|
+
6. Keep `DnsMadeEasy::Runner` temporarily.
|
|
188
|
+
- Mark it internal/deprecated.
|
|
189
|
+
- Use it as an implementation reference while migrating commands.
|
|
190
|
+
|
|
191
|
+
### Acceptance Criteria
|
|
192
|
+
|
|
193
|
+
- `dme --help` is handled by dry-cli.
|
|
194
|
+
- `dme version` prints `1.0.0`.
|
|
195
|
+
- Aruba can run `dme` in-process.
|
|
196
|
+
- Existing library specs still pass.
|
|
197
|
+
|
|
198
|
+
## Phase 2: Move Existing API Operations Under `account`
|
|
199
|
+
|
|
200
|
+
### Objective
|
|
201
|
+
|
|
202
|
+
Create the compatibility namespace for existing API operations:
|
|
203
|
+
|
|
204
|
+
```text
|
|
205
|
+
dme account <operation> [args...] [--format=...]
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Examples:
|
|
209
|
+
|
|
210
|
+
```text
|
|
211
|
+
dme account domains
|
|
212
|
+
dme account domain example.com
|
|
213
|
+
dme account records_for example.com
|
|
214
|
+
dme account create_a_record example.com www 203.0.113.10
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The user proposal used `dme account create_record A ...`; the existing API shape is `create_record DOMAIN NAME TYPE VALUE`. Keep the existing method shape first. Add friendlier aliases later if needed.
|
|
218
|
+
|
|
219
|
+
### Work
|
|
220
|
+
|
|
221
|
+
1. Implement `DnsMadeEasy::CLI::Commands::Account`.
|
|
222
|
+
2. Use `DnsMadeEasy::Api::Client.public_operations` to validate allowed operation names.
|
|
223
|
+
3. Treat the operation name as an argument, not a generated class per API method.
|
|
224
|
+
- This minimizes refactoring.
|
|
225
|
+
- It avoids generating dozens of tiny command classes with no separate behavior.
|
|
226
|
+
4. Forward remaining CLI arguments to the client method.
|
|
227
|
+
5. Preserve existing output formats.
|
|
228
|
+
6. Preserve useful failure behavior:
|
|
229
|
+
- unknown operation: print valid operations
|
|
230
|
+
- wrong arity: print method signature when discoverable
|
|
231
|
+
- API failures: non-zero exit and readable error
|
|
232
|
+
7. Add a hidden or explicit compatibility shim if needed:
|
|
233
|
+
- Option A: keep old `dme domains` behavior through a root fallback command.
|
|
234
|
+
- Option B: make `dme domains` fail with a clear migration hint.
|
|
235
|
+
- Recommended for `1.0.0`: fail with a migration hint because this is a major version.
|
|
236
|
+
|
|
237
|
+
### Acceptance Criteria
|
|
238
|
+
|
|
239
|
+
- `dme account operations` lists supported API operations.
|
|
240
|
+
- `dme account records_for example.com --format=json` works with a mocked client in specs.
|
|
241
|
+
- Existing runner specs are replaced by Aruba specs.
|
|
242
|
+
- `DnsMadeEasy::Runner` can be deleted after parity is covered.
|
|
243
|
+
|
|
244
|
+
## Phase 3: Add Typed Domain Model for DNS Records
|
|
245
|
+
|
|
246
|
+
### Objective
|
|
247
|
+
|
|
248
|
+
Create a provider-neutral internal record model used by export, parse, diff, plan, and apply.
|
|
249
|
+
|
|
250
|
+
### Proposed Files
|
|
251
|
+
|
|
252
|
+
```text
|
|
253
|
+
lib/dnsmadeeasy/types.rb
|
|
254
|
+
lib/dnsmadeeasy/zone/record.rb
|
|
255
|
+
lib/dnsmadeeasy/zone/record_key.rb
|
|
256
|
+
lib/dnsmadeeasy/zone/record_set.rb
|
|
257
|
+
lib/dnsmadeeasy/zone/domain_name.rb
|
|
258
|
+
lib/dnsmadeeasy/zone/ttl.rb
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Recommended dry-rb Usage
|
|
262
|
+
|
|
263
|
+
1. `dry-types`
|
|
264
|
+
- DNS name type
|
|
265
|
+
- TTL type
|
|
266
|
+
- record type enum
|
|
267
|
+
- non-empty string type
|
|
268
|
+
- priority/weight/port integer types
|
|
269
|
+
2. `dry-struct`
|
|
270
|
+
- immutable record value objects
|
|
271
|
+
- execution plan objects
|
|
272
|
+
3. `dry-validation`
|
|
273
|
+
- command input validation
|
|
274
|
+
- zone-level validation that needs cross-field rules
|
|
275
|
+
|
|
276
|
+
### Initial Record Types
|
|
277
|
+
|
|
278
|
+
Support the standard records already represented by the existing client:
|
|
279
|
+
|
|
280
|
+
- `A`
|
|
281
|
+
- `AAAA`
|
|
282
|
+
- `CNAME`
|
|
283
|
+
- `MX`
|
|
284
|
+
- `NS`
|
|
285
|
+
- `PTR`
|
|
286
|
+
- `SPF`
|
|
287
|
+
- `SRV`
|
|
288
|
+
- `TXT`
|
|
289
|
+
|
|
290
|
+
Explicitly omit `HTTPRED` from zone files because it is provider-specific and cannot be represented in a standard zone file.
|
|
291
|
+
|
|
292
|
+
### Internal Model Sketch
|
|
293
|
+
|
|
294
|
+
```ruby
|
|
295
|
+
module DnsMadeEasy
|
|
296
|
+
module Zone
|
|
297
|
+
class Record < Dry::Struct
|
|
298
|
+
attribute :owner, Types::String
|
|
299
|
+
attribute :type, Types::RecordType
|
|
300
|
+
attribute :value, Types::String
|
|
301
|
+
attribute :ttl, Types::Integer.constrained(gteq: 0)
|
|
302
|
+
attribute :priority, Types::Integer.optional
|
|
303
|
+
attribute :weight, Types::Integer.optional
|
|
304
|
+
attribute :port, Types::Integer.optional
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Do not include provider IDs in `Record`. Store remote metadata separately.
|
|
311
|
+
|
|
312
|
+
### Acceptance Criteria
|
|
313
|
+
|
|
314
|
+
- Unit tests can construct valid records.
|
|
315
|
+
- Invalid record types fail fast.
|
|
316
|
+
- Record equality ignores provider metadata.
|
|
317
|
+
- Records are sortable deterministically.
|
|
318
|
+
|
|
319
|
+
## Phase 4: Zone File Parser Adapter
|
|
320
|
+
|
|
321
|
+
### Objective
|
|
322
|
+
|
|
323
|
+
Use `dns-zonefile` only at the parsing boundary.
|
|
324
|
+
|
|
325
|
+
The parser adapter should rely on `dns-zonefile` as sufficient for downloaded or standard zone files. Any unsupported syntax should become an explicit validation error rather than leaking parser internals into the rest of the application.
|
|
326
|
+
|
|
327
|
+
### Proposed Files
|
|
328
|
+
|
|
329
|
+
```text
|
|
330
|
+
lib/dnsmadeeasy/zone/parser.rb
|
|
331
|
+
lib/dnsmadeeasy/zone/parser_adapter.rb
|
|
332
|
+
lib/dnsmadeeasy/zone/parse_result.rb
|
|
333
|
+
spec/fixtures/zones/example.com.zone
|
|
334
|
+
spec/lib/dnsmadeeasy/zone/parser_spec.rb
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Work
|
|
338
|
+
|
|
339
|
+
1. Add `dns-zonefile`.
|
|
340
|
+
2. Implement a parser adapter around:
|
|
341
|
+
|
|
342
|
+
```ruby
|
|
343
|
+
DNS::Zonefile.load(zone_string, origin)
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
3. Convert all returned records immediately into `DnsMadeEasy::Zone::Record`.
|
|
347
|
+
4. Normalize:
|
|
348
|
+
- empty owner to `@`
|
|
349
|
+
- origin-qualified owner names
|
|
350
|
+
- TTL inheritance
|
|
351
|
+
- TXT quoting
|
|
352
|
+
- MX priority
|
|
353
|
+
- SRV priority, weight, and port
|
|
354
|
+
5. Reject or warn on unsupported records.
|
|
355
|
+
6. Treat parser exceptions as command validation failures.
|
|
356
|
+
|
|
357
|
+
### Acceptance Criteria
|
|
358
|
+
|
|
359
|
+
- `zone validate FILE` can parse a fixture zone file.
|
|
360
|
+
- `DNS::Zonefile::*` objects never leave the parser adapter.
|
|
361
|
+
- Unsupported records produce actionable validation output.
|
|
362
|
+
|
|
363
|
+
## Phase 5: Canonical Zone Serializer and Formatter
|
|
364
|
+
|
|
365
|
+
### Objective
|
|
366
|
+
|
|
367
|
+
Produce deterministic, Git-friendly zone-file output.
|
|
368
|
+
|
|
369
|
+
### Proposed Files
|
|
370
|
+
|
|
371
|
+
```text
|
|
372
|
+
lib/dnsmadeeasy/zone/serializer.rb
|
|
373
|
+
lib/dnsmadeeasy/zone/formatter.rb
|
|
374
|
+
spec/lib/dnsmadeeasy/zone/serializer_spec.rb
|
|
375
|
+
spec/lib/dnsmadeeasy/zone/formatter_spec.rb
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### Rules
|
|
379
|
+
|
|
380
|
+
The serializer emits the normalized zone-file format described in `Zone File Compatibility and Normalization`.
|
|
381
|
+
|
|
382
|
+
1. Emit one zone per file.
|
|
383
|
+
2. Emit `$ORIGIN`.
|
|
384
|
+
3. Emit `$TTL`.
|
|
385
|
+
4. Emit explicit owner on every record.
|
|
386
|
+
5. Use `@` for apex records.
|
|
387
|
+
6. Omit provider-managed SOA.
|
|
388
|
+
7. Omit apex NS records by default.
|
|
389
|
+
8. Preserve delegated NS records.
|
|
390
|
+
9. Sort deterministically:
|
|
391
|
+
- owner
|
|
392
|
+
- type priority group
|
|
393
|
+
- priority
|
|
394
|
+
- value
|
|
395
|
+
- ttl
|
|
396
|
+
10. Normalize whitespace.
|
|
397
|
+
|
|
398
|
+
### Commands
|
|
399
|
+
|
|
400
|
+
```text
|
|
401
|
+
dme zone validate FILE
|
|
402
|
+
dme zone fmt FILE
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Use `fmt` as the canonical command name, matching the spec. Optionally add `format` as an alias.
|
|
406
|
+
|
|
407
|
+
### Acceptance Criteria
|
|
408
|
+
|
|
409
|
+
- Parse then serialize then parse returns equivalent records.
|
|
410
|
+
- Formatting an already formatted zone is byte-identical.
|
|
411
|
+
- `dme zone fmt FILE --check` can be added later without changing internals.
|
|
412
|
+
|
|
413
|
+
## Phase 6: Remote Record Adapter
|
|
414
|
+
|
|
415
|
+
### Objective
|
|
416
|
+
|
|
417
|
+
Convert DNS Made Easy API responses into the same internal record model used by zone files.
|
|
418
|
+
|
|
419
|
+
### Proposed Files
|
|
420
|
+
|
|
421
|
+
```text
|
|
422
|
+
lib/dnsmadeeasy/zone/provider_record.rb
|
|
423
|
+
lib/dnsmadeeasy/zone/remote_reader.rb
|
|
424
|
+
spec/lib/dnsmadeeasy/zone/remote_reader_spec.rb
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Work
|
|
428
|
+
|
|
429
|
+
1. Use the existing API client:
|
|
430
|
+
|
|
431
|
+
```ruby
|
|
432
|
+
client.records_for(domain)
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
2. Convert `Hashie::Mash` responses into internal records plus provider metadata.
|
|
436
|
+
3. Keep metadata such as record ID separate:
|
|
437
|
+
|
|
438
|
+
```ruby
|
|
439
|
+
ProviderRecord = Struct.new(:record, :provider_id, keyword_init: true)
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
or a `Dry::Struct` equivalent.
|
|
443
|
+
|
|
444
|
+
4. Omit `HTTPRED` from exports and emit warnings.
|
|
445
|
+
5. Filter provider-managed SOA and apex NS according to serializer rules.
|
|
446
|
+
|
|
447
|
+
### Acceptance Criteria
|
|
448
|
+
|
|
449
|
+
- Remote records convert to internal records with IDs preserved separately.
|
|
450
|
+
- HTTPRED records are reported but omitted from zone serialization.
|
|
451
|
+
- Export fixtures do not include provider IDs.
|
|
452
|
+
|
|
453
|
+
## Phase 7: `zone export`
|
|
454
|
+
|
|
455
|
+
### Objective
|
|
456
|
+
|
|
457
|
+
Download remote provider records and emit a canonical zone file.
|
|
458
|
+
|
|
459
|
+
### Command
|
|
460
|
+
|
|
461
|
+
```text
|
|
462
|
+
dme zone export DOMAIN [--output=FILE] [--ttl=300] [--include-apex-ns]
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Work
|
|
466
|
+
|
|
467
|
+
1. Fetch remote records.
|
|
468
|
+
2. Convert via remote adapter.
|
|
469
|
+
3. Serialize canonical zone file.
|
|
470
|
+
4. Write to stdout by default.
|
|
471
|
+
5. Write to file when `--output` is provided.
|
|
472
|
+
6. Print warnings to stderr, not stdout, so stdout remains pipe-safe.
|
|
473
|
+
7. Use threaded chunks and `TTY::Spinner` when export requires multiple independent provider calls.
|
|
474
|
+
|
|
475
|
+
### Acceptance Criteria
|
|
476
|
+
|
|
477
|
+
- Export of the same mocked records is byte-identical across runs.
|
|
478
|
+
- HTTPRED omission warning appears on stderr.
|
|
479
|
+
- Stdout contains only zone-file content.
|
|
480
|
+
|
|
481
|
+
## Phase 8: Diff Engine and Execution Plan
|
|
482
|
+
|
|
483
|
+
### Objective
|
|
484
|
+
|
|
485
|
+
Compare desired zone-file state with remote state and produce a human-readable plan.
|
|
486
|
+
|
|
487
|
+
### Proposed Files
|
|
488
|
+
|
|
489
|
+
```text
|
|
490
|
+
lib/dnsmadeeasy/zone/diff.rb
|
|
491
|
+
lib/dnsmadeeasy/zone/plan.rb
|
|
492
|
+
lib/dnsmadeeasy/zone/plan_action.rb
|
|
493
|
+
lib/dnsmadeeasy/zone/plan_renderer.rb
|
|
494
|
+
spec/lib/dnsmadeeasy/zone/diff_spec.rb
|
|
495
|
+
spec/lib/dnsmadeeasy/zone/plan_renderer_spec.rb
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### Semantics
|
|
499
|
+
|
|
500
|
+
Default behavior:
|
|
501
|
+
|
|
502
|
+
- create missing records
|
|
503
|
+
- update modified records
|
|
504
|
+
- never delete records
|
|
505
|
+
|
|
506
|
+
Future behavior:
|
|
507
|
+
|
|
508
|
+
- `--delete` allows removal of remote records absent from the zone file, excluding protected records.
|
|
509
|
+
|
|
510
|
+
### Record Identity
|
|
511
|
+
|
|
512
|
+
Use a stable identity key:
|
|
513
|
+
|
|
514
|
+
```text
|
|
515
|
+
owner + type + routing fields
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
For simple records, `owner + type` may not be sufficient because multiple TXT, MX, NS, and A records can coexist. The diff engine should model record sets, not assume one record per owner/type.
|
|
519
|
+
|
|
520
|
+
Recommended approach:
|
|
521
|
+
|
|
522
|
+
- compare sets by full record content for creates/deletes
|
|
523
|
+
- compare update candidates only where DNS Made Easy requires update instead of delete/create
|
|
524
|
+
- prefer create/delete pair over ambiguous update when multiple records share owner/type
|
|
525
|
+
|
|
526
|
+
This is the part where sloppy code becomes a DNS outage generator, so do not invent clever matching without tests.
|
|
527
|
+
|
|
528
|
+
### Command
|
|
529
|
+
|
|
530
|
+
```text
|
|
531
|
+
dme zone plan FILE [--domain=DOMAIN] [--format=text|json]
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
If `$ORIGIN` is present, `--domain` is optional. If no `$ORIGIN` exists, require `--domain`.
|
|
535
|
+
|
|
536
|
+
### Acceptance Criteria
|
|
537
|
+
|
|
538
|
+
- Plan output is deterministic.
|
|
539
|
+
- Creates, updates, and skipped deletes are separately listed.
|
|
540
|
+
- Ambiguous updates are either resolved safely or reported as requiring manual intervention.
|
|
541
|
+
- `plan --format=json` is machine-readable for later automation.
|
|
542
|
+
|
|
543
|
+
## Phase 9: `zone apply`
|
|
544
|
+
|
|
545
|
+
### Objective
|
|
546
|
+
|
|
547
|
+
Execute a reviewed plan using the existing API client.
|
|
548
|
+
|
|
549
|
+
### Command
|
|
550
|
+
|
|
551
|
+
```text
|
|
552
|
+
dme zone apply FILE [--domain=DOMAIN] [--yes] [--delete]
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### Work
|
|
556
|
+
|
|
557
|
+
1. Reuse the same planner used by `zone plan`.
|
|
558
|
+
2. Refuse to apply destructive actions unless `--delete` is present.
|
|
559
|
+
3. Require confirmation unless `--yes` is present.
|
|
560
|
+
4. Execute creates and updates through existing client methods.
|
|
561
|
+
5. Execute deletes only when explicitly enabled.
|
|
562
|
+
6. Print a final summary.
|
|
563
|
+
7. Split independent API operations into bounded worker-thread chunks and display a `TTY::Spinner` for each active worker/chunk.
|
|
564
|
+
|
|
565
|
+
### Safety Rules
|
|
566
|
+
|
|
567
|
+
- Never delete by default.
|
|
568
|
+
- Never delete provider-managed SOA.
|
|
569
|
+
- Never delete apex NS unless an explicit future option allows it.
|
|
570
|
+
- Fail before partial application if validation fails.
|
|
571
|
+
- Consider ordering:
|
|
572
|
+
- create replacement records before deleting old records where safe
|
|
573
|
+
- avoid CNAME conflicts by detecting incompatible desired state before apply
|
|
574
|
+
- Parallelize only independent actions after dependency/conflict checks.
|
|
575
|
+
- Keep execution summaries deterministic even when worker completion order varies.
|
|
576
|
+
|
|
577
|
+
### Acceptance Criteria
|
|
578
|
+
|
|
579
|
+
- `apply` uses exactly the plan generated from current remote state.
|
|
580
|
+
- `apply` can run against a mocked client in specs.
|
|
581
|
+
- Confirmation is tested through Aruba.
|
|
582
|
+
- Partial failures report completed and failed actions.
|
|
583
|
+
- Threaded execution has specs for successful chunks and failed chunks.
|
|
584
|
+
- Spinner behavior is covered through injected fake/no-op spinner objects.
|
|
585
|
+
|
|
586
|
+
## Phase 10: Documentation and Migration Notes
|
|
587
|
+
|
|
588
|
+
### Work
|
|
589
|
+
|
|
590
|
+
1. Update README from old flat CLI to namespaced CLI.
|
|
591
|
+
2. Document Ruby support and 1.0.0 breaking changes.
|
|
592
|
+
3. Add examples:
|
|
593
|
+
|
|
594
|
+
```text
|
|
595
|
+
dme account domains
|
|
596
|
+
dme account records_for example.com
|
|
597
|
+
dme zone export example.com > example.com.zone
|
|
598
|
+
dme zone validate example.com.zone
|
|
599
|
+
dme zone fmt example.com.zone
|
|
600
|
+
dme zone plan example.com.zone
|
|
601
|
+
dme zone apply example.com.zone --yes
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
4. Add a migration section:
|
|
605
|
+
|
|
606
|
+
```text
|
|
607
|
+
dme domains
|
|
608
|
+
# becomes
|
|
609
|
+
dme account domains
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
5. Document unsupported records:
|
|
613
|
+
- `HTTPRED` omitted from standard zone files
|
|
614
|
+
- future provider-specific directives may be added
|
|
615
|
+
|
|
616
|
+
### Acceptance Criteria
|
|
617
|
+
|
|
618
|
+
- README examples are covered by Aruba smoke specs where practical.
|
|
619
|
+
- The old command style is either removed with a clear message or documented as unsupported in 1.0.0.
|
|
620
|
+
|
|
621
|
+
## Proposed Implementation Order
|
|
622
|
+
|
|
623
|
+
1. Runtime baseline:
|
|
624
|
+
- version `1.0.0`
|
|
625
|
+
- Ruby 4 support
|
|
626
|
+
- dependencies
|
|
627
|
+
- CI
|
|
628
|
+
2. dry-cli launcher:
|
|
629
|
+
- command registry
|
|
630
|
+
- version command
|
|
631
|
+
- Aruba in-process setup
|
|
632
|
+
3. `account` command:
|
|
633
|
+
- existing API operation forwarding
|
|
634
|
+
- output formats
|
|
635
|
+
- delete `Runner` once parity exists
|
|
636
|
+
4. zone model:
|
|
637
|
+
- dry types
|
|
638
|
+
- dry structs
|
|
639
|
+
- validation contracts
|
|
640
|
+
5. parser and serializer:
|
|
641
|
+
- `validate`
|
|
642
|
+
- `fmt`
|
|
643
|
+
6. remote adapter and export:
|
|
644
|
+
- `export`
|
|
645
|
+
7. diff and plan:
|
|
646
|
+
- `plan`
|
|
647
|
+
8. apply:
|
|
648
|
+
- confirmation
|
|
649
|
+
- execution
|
|
650
|
+
- failure reporting
|
|
651
|
+
9. documentation and examples.
|
|
652
|
+
|
|
653
|
+
## Test Strategy
|
|
654
|
+
|
|
655
|
+
### Unit Specs
|
|
656
|
+
|
|
657
|
+
- typed record validation
|
|
658
|
+
- parser adapter
|
|
659
|
+
- serializer
|
|
660
|
+
- remote adapter
|
|
661
|
+
- diff engine
|
|
662
|
+
- plan renderer
|
|
663
|
+
|
|
664
|
+
### CLI Specs
|
|
665
|
+
|
|
666
|
+
Use Aruba in-process tests for:
|
|
667
|
+
|
|
668
|
+
- `dme --help`
|
|
669
|
+
- `dme version`
|
|
670
|
+
- `dme account operations`
|
|
671
|
+
- `dme account records_for example.com`
|
|
672
|
+
- `dme zone validate FILE`
|
|
673
|
+
- `dme zone fmt FILE`
|
|
674
|
+
- `dme zone export DOMAIN`
|
|
675
|
+
- `dme zone plan FILE`
|
|
676
|
+
- `dme zone apply FILE --yes`
|
|
677
|
+
|
|
678
|
+
### API Specs
|
|
679
|
+
|
|
680
|
+
Keep WebMock around existing client tests. Do not hit real DNS Made Easy in automated tests.
|
|
681
|
+
|
|
682
|
+
### Live Integration Specs
|
|
683
|
+
|
|
684
|
+
There is one real DNS Made Easy domain available for opt-in integration testing:
|
|
685
|
+
|
|
686
|
+
- Domain: `isdue.today`
|
|
687
|
+
- DNS Made Easy domain ID: `8218117`
|
|
688
|
+
|
|
689
|
+
This domain is real but not currently in use. Live specs may use it to verify actual provider behavior for export, plan, and apply.
|
|
690
|
+
|
|
691
|
+
Rules:
|
|
692
|
+
|
|
693
|
+
- Never run live integration specs in default CI.
|
|
694
|
+
- Gate them behind an explicit environment variable such as `DNSMADEEASY_LIVE_TESTS=1`.
|
|
695
|
+
- Require real credentials through the existing credential mechanisms.
|
|
696
|
+
- Scope mutations to predictable test records, for example `_dnsmadeeasy-test.isdue.today`.
|
|
697
|
+
- Clean up records created by the test.
|
|
698
|
+
- Never modify apex records, NS records, SOA records, MX records, or any record not created by the test run.
|
|
699
|
+
- Make live specs idempotent so a failed run can be retried safely.
|
|
700
|
+
|
|
701
|
+
### Golden Files
|
|
702
|
+
|
|
703
|
+
Use fixture zone files and expected output files for:
|
|
704
|
+
|
|
705
|
+
- canonical formatting
|
|
706
|
+
- export output
|
|
707
|
+
- plan text output
|
|
708
|
+
- plan JSON output
|
|
709
|
+
|
|
710
|
+
## Open Decisions
|
|
711
|
+
|
|
712
|
+
1. Ruby support:
|
|
713
|
+
- Ruby 4 only, or Ruby 3.2+ plus Ruby 4?
|
|
714
|
+
- Recommendation: Ruby 4 only if this gem is primarily for current internal use; Ruby 3.2+ if public gem compatibility matters.
|
|
715
|
+
2. Backward CLI compatibility:
|
|
716
|
+
- Should `dme domains` continue working as an alias for `dme account domains`?
|
|
717
|
+
- Recommendation: no, because this is a major version and the new command tree should be unambiguous.
|
|
718
|
+
3. Zone-file SOA handling:
|
|
719
|
+
- Should `validate` permit SOA but `fmt/export` omit it?
|
|
720
|
+
- Recommendation: parse and validate SOA if present, but never apply SOA changes in version 1.
|
|
721
|
+
4. Multiple records with same owner/type:
|
|
722
|
+
- Should updates be represented as delete/create unless there is a single unambiguous remote candidate?
|
|
723
|
+
- Recommendation: yes.
|
|
724
|
+
5. Apex NS behavior:
|
|
725
|
+
- Should `export --include-apex-ns` exist in version 1?
|
|
726
|
+
- Recommendation: yes for visibility, but default remains omit.
|
|
727
|
+
|
|
728
|
+
## Risks
|
|
729
|
+
|
|
730
|
+
1. `dns-zonefile` may not support every zone-file construct users expect.
|
|
731
|
+
- Mitigation: keep adapter isolated and surface precise validation errors.
|
|
732
|
+
2. DNS Made Easy record semantics may not map perfectly to RFC zone records.
|
|
733
|
+
- Mitigation: keep provider metadata separate and document omissions.
|
|
734
|
+
3. Diffing DNS records can be deceptively dangerous.
|
|
735
|
+
- Mitigation: default to no deletes, deterministic plans, and conservative ambiguity handling.
|
|
736
|
+
4. CLI refactor can accidentally break existing automation.
|
|
737
|
+
- Mitigation: major version bump, migration docs, and Aruba tests.
|
|
738
|
+
5. `sym` may produce Ruby 4.1 dependency warnings.
|
|
739
|
+
- Mitigation: add `tsort` explicitly and consider replacing `sym` later.
|
|
740
|
+
|
|
741
|
+
## Definition of Done
|
|
742
|
+
|
|
743
|
+
- Gem version is `1.0.0`.
|
|
744
|
+
- Ruby 2 support is removed.
|
|
745
|
+
- CI runs on Ruby 4.
|
|
746
|
+
- CLI is dry-cli based and testable with Aruba.
|
|
747
|
+
- Existing account operations are available under `dme account`.
|
|
748
|
+
- Zone commands exist:
|
|
749
|
+
- `export`
|
|
750
|
+
- `validate`
|
|
751
|
+
- `fmt`
|
|
752
|
+
- `plan`
|
|
753
|
+
- `apply`
|
|
754
|
+
- Zone files are canonical and deterministic.
|
|
755
|
+
- Apply is safe by default and never deletes without explicit opt-in.
|
|
756
|
+
- README documents the new command tree and migration path.
|