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,242 @@
|
|
|
1
|
+
# DNS Zone Management Specification
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Extend the existing `dnsmadeeasy` Ruby gem with first-class support for managing DNS zones using standard RFC 1034/1035 zone files.
|
|
6
|
+
|
|
7
|
+
The objective is to make zone files the source of truth while continuing to use the existing DNS Made Easy API client for all provider interactions.
|
|
8
|
+
|
|
9
|
+
This feature is intentionally **DNS Made Easy specific**. It does not attempt to become a generic multi-provider framework.
|
|
10
|
+
|
|
11
|
+
______________________________________________________________________
|
|
12
|
+
|
|
13
|
+
# Goals
|
|
14
|
+
|
|
15
|
+
- Use standard DNS zone files.
|
|
16
|
+
- Produce deterministic, Git-friendly output.
|
|
17
|
+
- Generate human-readable execution plans.
|
|
18
|
+
- Apply the minimum set of API changes.
|
|
19
|
+
- Be safe by default.
|
|
20
|
+
- Be easy for LLMs to generate and modify.
|
|
21
|
+
|
|
22
|
+
______________________________________________________________________
|
|
23
|
+
|
|
24
|
+
# Existing Foundation
|
|
25
|
+
|
|
26
|
+
The gem already provides:
|
|
27
|
+
|
|
28
|
+
- authentication
|
|
29
|
+
- domain lookup
|
|
30
|
+
- record enumeration
|
|
31
|
+
- create/update/delete operations
|
|
32
|
+
- YAML/JSON output
|
|
33
|
+
- CLI
|
|
34
|
+
|
|
35
|
+
The new functionality builds on these capabilities rather than replacing them.
|
|
36
|
+
|
|
37
|
+
______________________________________________________________________
|
|
38
|
+
|
|
39
|
+
# Architecture
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
Zone File
|
|
43
|
+
│
|
|
44
|
+
▼
|
|
45
|
+
DNS::Zonefile.load
|
|
46
|
+
(dns-zonefile gem)
|
|
47
|
+
│
|
|
48
|
+
▼
|
|
49
|
+
Internal Record Model
|
|
50
|
+
│
|
|
51
|
+
├─────────────┐
|
|
52
|
+
│ │
|
|
53
|
+
▼ ▼
|
|
54
|
+
dme all DOMAIN Zone File
|
|
55
|
+
(remote state) Serializer
|
|
56
|
+
│
|
|
57
|
+
▼
|
|
58
|
+
Diff Engine
|
|
59
|
+
│
|
|
60
|
+
▼
|
|
61
|
+
Execution Plan
|
|
62
|
+
│
|
|
63
|
+
▼
|
|
64
|
+
Existing API Client
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
______________________________________________________________________
|
|
68
|
+
|
|
69
|
+
# Dependency
|
|
70
|
+
|
|
71
|
+
Use the `dns-zonefile` gem solely for parsing RFC-compliant zone files.
|
|
72
|
+
|
|
73
|
+
The rest of the implementation should not depend directly on `DNS::Zonefile::*` classes.
|
|
74
|
+
|
|
75
|
+
Instead:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
Zone File
|
|
79
|
+
│
|
|
80
|
+
▼
|
|
81
|
+
Parser Adapter
|
|
82
|
+
│
|
|
83
|
+
▼
|
|
84
|
+
DnsMadeEasy::Zone::Record
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
This isolates the rest of the implementation from parser details.
|
|
88
|
+
|
|
89
|
+
______________________________________________________________________
|
|
90
|
+
|
|
91
|
+
# Canonical Zone File
|
|
92
|
+
|
|
93
|
+
Exports should produce a normalized zone file.
|
|
94
|
+
|
|
95
|
+
Characteristics:
|
|
96
|
+
|
|
97
|
+
- one zone per file
|
|
98
|
+
- explicit owner on every record
|
|
99
|
+
- `$ORIGIN`
|
|
100
|
+
- `$TTL`
|
|
101
|
+
- `@` for zone apex
|
|
102
|
+
- deterministic ordering
|
|
103
|
+
- normalized whitespace
|
|
104
|
+
- provider-managed SOA omitted
|
|
105
|
+
- apex NS records omitted by default
|
|
106
|
+
- delegated NS records preserved
|
|
107
|
+
|
|
108
|
+
Example:
|
|
109
|
+
|
|
110
|
+
```dns
|
|
111
|
+
$ORIGIN example.com.
|
|
112
|
+
$TTL 300
|
|
113
|
+
|
|
114
|
+
@ IN A 203.0.113.10
|
|
115
|
+
www IN CNAME @
|
|
116
|
+
|
|
117
|
+
@ IN MX 10 mail.example.com.
|
|
118
|
+
|
|
119
|
+
@ IN TXT "v=spf1 include:_spf.google.com ~all"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
______________________________________________________________________
|
|
123
|
+
|
|
124
|
+
# New Commands
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
dme zone export DOMAIN
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Download remote records and emit a canonical zone file.
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
dme zone validate FILE
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Validate syntax and supported record types.
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
dme zone fmt FILE
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Rewrite the file into canonical formatting.
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
dme zone plan FILE
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Compare desired state against the provider and display the planned actions.
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
dme zone apply FILE
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Execute the planned changes.
|
|
155
|
+
|
|
156
|
+
______________________________________________________________________
|
|
157
|
+
|
|
158
|
+
# Internal Record Model
|
|
159
|
+
|
|
160
|
+
The diff engine operates on an internal provider-neutral record model.
|
|
161
|
+
|
|
162
|
+
Provider-specific metadata (record IDs, source IDs, etc.) is stored separately and is never serialized into zone files.
|
|
163
|
+
|
|
164
|
+
______________________________________________________________________
|
|
165
|
+
|
|
166
|
+
# Diff Semantics
|
|
167
|
+
|
|
168
|
+
Default behavior:
|
|
169
|
+
|
|
170
|
+
- create missing records
|
|
171
|
+
- update modified records
|
|
172
|
+
- never delete records
|
|
173
|
+
|
|
174
|
+
Future option:
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
dme zone apply FILE --delete
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
removes remote records not present in the zone file (excluding protected provider-managed records).
|
|
181
|
+
|
|
182
|
+
______________________________________________________________________
|
|
183
|
+
|
|
184
|
+
# Export Rules
|
|
185
|
+
|
|
186
|
+
Export should normalize:
|
|
187
|
+
|
|
188
|
+
- empty owner → `@`
|
|
189
|
+
- relative owner names
|
|
190
|
+
- TTLs
|
|
191
|
+
- whitespace
|
|
192
|
+
- ordering
|
|
193
|
+
|
|
194
|
+
Repeated export of an unchanged zone should produce byte-identical output.
|
|
195
|
+
|
|
196
|
+
______________________________________________________________________
|
|
197
|
+
|
|
198
|
+
# Provider-specific Records
|
|
199
|
+
|
|
200
|
+
HTTPRED cannot be represented in a standard RFC zone file.
|
|
201
|
+
|
|
202
|
+
Version 1 behavior:
|
|
203
|
+
|
|
204
|
+
- omit HTTPRED records from exports
|
|
205
|
+
- emit a warning listing omitted records
|
|
206
|
+
|
|
207
|
+
Future versions may support provider-specific comment directives.
|
|
208
|
+
|
|
209
|
+
______________________________________________________________________
|
|
210
|
+
|
|
211
|
+
# LLM Workflow
|
|
212
|
+
|
|
213
|
+
The intended AI workflow is:
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
User request
|
|
217
|
+
│
|
|
218
|
+
▼
|
|
219
|
+
LLM edits zone file
|
|
220
|
+
│
|
|
221
|
+
▼
|
|
222
|
+
dme zone plan
|
|
223
|
+
│
|
|
224
|
+
▼
|
|
225
|
+
Human reviews plan
|
|
226
|
+
│
|
|
227
|
+
▼
|
|
228
|
+
dme zone apply
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The LLM never calls provider APIs directly.
|
|
232
|
+
|
|
233
|
+
______________________________________________________________________
|
|
234
|
+
|
|
235
|
+
# Non-goals
|
|
236
|
+
|
|
237
|
+
- Multi-provider abstraction
|
|
238
|
+
- Inventing a new configuration language
|
|
239
|
+
- Replacing existing DNS tooling
|
|
240
|
+
- Supporting every obscure BIND extension
|
|
241
|
+
|
|
242
|
+
The project embraces standard DNS zone files and extends the existing `dnsmadeeasy` gem with a modern declarative workflow.
|
data/exe/dme
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require 'dnsmadeeasy
|
|
4
|
+
require 'dnsmadeeasy'
|
|
5
|
+
require 'dnsmadeeasy/cli/message_helpers'
|
|
5
6
|
|
|
6
|
-
DnsMadeEasy
|
|
7
|
+
# CLI for DnsMadeEasy v1
|
|
8
|
+
# @deprecated use dmez instead
|
|
9
|
+
class DmeCli
|
|
10
|
+
def self.start
|
|
11
|
+
puts
|
|
12
|
+
::DnsMadeEasy::CLI::MessageHelpers.error("You've upgraded to the version #{DnsMadeEasy::VERSION} of the DnsMadeEasy Ruby Gem.\nPlease use the new 'dmez' CLI command instead.")
|
|
13
|
+
exit 1
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
DmeCli.start
|
data/exe/dmez
ADDED
data/lib/dme.rb
CHANGED
|
@@ -7,10 +7,15 @@ module DME
|
|
|
7
7
|
def [](key, secret)
|
|
8
8
|
::DnsMadeEasy::Api::Client.new(key, secret)
|
|
9
9
|
end
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
|
|
11
|
+
def method_missing(method, ...)
|
|
12
|
+
DnsMadeEasy.send(method, ...)
|
|
12
13
|
rescue NameError => e
|
|
13
14
|
puts "Error: #{e.message}"
|
|
14
15
|
end
|
|
16
|
+
|
|
17
|
+
def respond_to_missing?(method, include_private = false)
|
|
18
|
+
DnsMadeEasy.respond_to?(method, include_private) || super
|
|
19
|
+
end
|
|
15
20
|
end
|
|
16
21
|
end
|
|
@@ -13,7 +13,7 @@ module DnsMadeEasy
|
|
|
13
13
|
class Client
|
|
14
14
|
class << self
|
|
15
15
|
def public_operations
|
|
16
|
-
(new('a', 'b').methods - Object.methods).map(&:to_s).reject { |r| r =~
|
|
16
|
+
(new('a', 'b').methods - Object.methods).map(&:to_s).reject { |r| r =~ /=$|^(api_|on_|request)/ }.sort
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
|
|
@@ -24,8 +24,8 @@ module DnsMadeEasy
|
|
|
24
24
|
:api_secret
|
|
25
25
|
|
|
26
26
|
def initialize(api_key, api_secret, sandbox = false, options = {})
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
raise 'api_key is undefined' unless api_key
|
|
28
|
+
raise 'api_secret is undefined' unless api_secret
|
|
29
29
|
|
|
30
30
|
@api_key = api_key
|
|
31
31
|
@api_secret = api_secret
|
|
@@ -36,12 +36,12 @@ module DnsMadeEasy
|
|
|
36
36
|
sandbox ? on_sandbox : on_production
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
def on_sandbox(&
|
|
40
|
-
with_url(::DnsMadeEasy::API_BASE_URL_SANDBOX, &
|
|
39
|
+
def on_sandbox(&)
|
|
40
|
+
with_url(::DnsMadeEasy::API_BASE_URL_SANDBOX, &)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
def on_production(&
|
|
44
|
-
with_url(::DnsMadeEasy::API_BASE_URL_PRODUCTION, &
|
|
43
|
+
def on_production(&)
|
|
44
|
+
with_url(::DnsMadeEasy::API_BASE_URL_PRODUCTION, &)
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
# -----------------------------------
|
|
@@ -127,55 +127,63 @@ module DnsMadeEasy
|
|
|
127
127
|
|
|
128
128
|
def create_a_record(domain_name, name, value, options = {})
|
|
129
129
|
# TODO: match IPv4 for value
|
|
130
|
-
create_record domain_name, name, 'A', value, options
|
|
130
|
+
create_record domain_name, name, 'A', value, **options
|
|
131
131
|
end
|
|
132
132
|
|
|
133
133
|
def create_aaaa_record(domain_name, name, value, options = {})
|
|
134
134
|
# TODO: match IPv6 for value
|
|
135
|
-
create_record domain_name, name, 'AAAA', value, options
|
|
135
|
+
create_record domain_name, name, 'AAAA', value, **options
|
|
136
136
|
end
|
|
137
137
|
|
|
138
138
|
def create_ptr_record(domain_name, name, value, options = {})
|
|
139
139
|
# TODO: match PTR value
|
|
140
|
-
create_record domain_name, name, 'PTR', value, options
|
|
140
|
+
create_record domain_name, name, 'PTR', value, **options
|
|
141
141
|
end
|
|
142
142
|
|
|
143
143
|
def create_txt_record(domain_name, name, value, options = {})
|
|
144
144
|
# TODO: match TXT value
|
|
145
|
-
create_record domain_name, name, 'TXT', value, options
|
|
145
|
+
create_record domain_name, name, 'TXT', value, **options
|
|
146
146
|
end
|
|
147
147
|
|
|
148
148
|
def create_cname_record(domain_name, name, value, options = {})
|
|
149
149
|
# TODO: match CNAME value
|
|
150
|
-
create_record domain_name, name, 'CNAME', value, options
|
|
150
|
+
create_record domain_name, name, 'CNAME', value, **options
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def create_aname_record(domain_name, name, value, options = {})
|
|
154
|
+
create_record domain_name, name, 'ANAME', value, **options
|
|
151
155
|
end
|
|
152
156
|
|
|
153
157
|
def create_ns_record(domain_name, name, value, options = {})
|
|
154
158
|
# TODO: match domainname for value
|
|
155
|
-
create_record domain_name, name, 'NS', value, options
|
|
159
|
+
create_record domain_name, name, 'NS', value, **options
|
|
156
160
|
end
|
|
157
161
|
|
|
158
162
|
def create_spf_record(domain_name, name, value, options = {})
|
|
159
|
-
create_record domain_name, name, 'SPF', value, options
|
|
163
|
+
create_record domain_name, name, 'SPF', value, **options
|
|
160
164
|
end
|
|
161
165
|
|
|
162
166
|
def create_mx_record(domain_name, name, priority, value, options = {})
|
|
163
167
|
options.merge!('mxLevel' => priority)
|
|
164
168
|
|
|
165
|
-
create_record domain_name, name, 'MX', value, options
|
|
169
|
+
create_record domain_name, name, 'MX', value, **options
|
|
166
170
|
end
|
|
167
171
|
|
|
168
172
|
def create_srv_record(domain_name, name, priority, weight, port, value, options = {})
|
|
169
173
|
options.merge!('priority' => priority, 'weight' => weight, 'port' => port)
|
|
170
|
-
create_record domain_name, name, 'SRV', value, options
|
|
174
|
+
create_record domain_name, name, 'SRV', value, **options
|
|
171
175
|
end
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
176
|
+
|
|
177
|
+
def create_httpred_record(domain_name, name, value, redirectType = 'STANDARD - 302', description = '',
|
|
178
|
+
keywords = '', title = '', options = {})
|
|
179
|
+
options.merge!('redirectType' => redirectType, 'description' => description, 'keywords' => keywords,
|
|
180
|
+
'title' => title)
|
|
181
|
+
create_record domain_name, name, 'HTTPRED', value, **options
|
|
175
182
|
end
|
|
176
183
|
|
|
177
184
|
def update_record(domain, record_id, name, type, value, options = {})
|
|
178
|
-
body = { 'name' => name, 'type' => type, 'value' => value, 'ttl' => 3600, 'gtdLocation' => 'DEFAULT',
|
|
185
|
+
body = { 'name' => name, 'type' => type, 'value' => value, 'ttl' => 3600, 'gtdLocation' => 'DEFAULT',
|
|
186
|
+
'id' => record_id }
|
|
179
187
|
put "/dns/managed/#{get_id_by_domain(domain)}/records/#{record_id}/", body.merge(options)
|
|
180
188
|
end
|
|
181
189
|
|
|
@@ -314,12 +322,10 @@ module DnsMadeEasy
|
|
|
314
322
|
process_rate_limits(response)
|
|
315
323
|
unparsed_json = response.body.to_s.empty? ? '{}' : response.body
|
|
316
324
|
Hashie::Mash.new(JSON.parse(unparsed_json))
|
|
317
|
-
rescue Net::
|
|
318
|
-
if e.message =~ /403.*forbidden/i
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
raise e
|
|
322
|
-
end
|
|
325
|
+
rescue Net::HTTPClientException => e
|
|
326
|
+
raise ::DnsMadeEasy::AuthenticationError, e if e.message =~ /403.*forbidden/i
|
|
327
|
+
|
|
328
|
+
raise e
|
|
323
329
|
end
|
|
324
330
|
|
|
325
331
|
def process_rate_limits(response)
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'dnsmadeeasy/api/client'
|
|
4
|
+
require 'dnsmadeeasy/cli/commands/base'
|
|
5
|
+
|
|
6
|
+
module DnsMadeEasy
|
|
7
|
+
module CLI
|
|
8
|
+
# Registered dry-cli command classes.
|
|
9
|
+
module Commands
|
|
10
|
+
# Dispatches existing DNS Made Easy API operations under `dme account`.
|
|
11
|
+
class Account < Base
|
|
12
|
+
RECORD_TYPES = %w[A AAAA ANAME CNAME HTTPRED MX NS PTR SOA SPF SRV TXT].freeze
|
|
13
|
+
COMMON_OPTION_NAMES = %i[format credentials api_key api_secret record_type].freeze
|
|
14
|
+
RECORD_LIST_OPERATIONS = %w[all records_for].freeze
|
|
15
|
+
OPERATION_METADATA = {
|
|
16
|
+
'all' => {
|
|
17
|
+
usage: 'dme account all DOMAIN_NAME [--record-type=TYPE]',
|
|
18
|
+
arguments: ['DOMAIN_NAME # Domain name to list records for'],
|
|
19
|
+
options: ["--record-type=TYPE, -t TYPE # Optional record type filter: (#{RECORD_TYPES.join('/')})"],
|
|
20
|
+
summary: 'List records for a managed domain'
|
|
21
|
+
},
|
|
22
|
+
'records_for' => {
|
|
23
|
+
usage: 'dme account records_for DOMAIN_NAME [--record-type=TYPE]',
|
|
24
|
+
arguments: ['DOMAIN_NAME # Domain name to list records for'],
|
|
25
|
+
options: ["--record-type=TYPE, -t TYPE # Optional record type filter: (#{RECORD_TYPES.join('/')})"],
|
|
26
|
+
summary: 'List records for a managed domain'
|
|
27
|
+
},
|
|
28
|
+
'domains' => {
|
|
29
|
+
usage: 'dme account domains',
|
|
30
|
+
arguments: [],
|
|
31
|
+
options: [],
|
|
32
|
+
summary: 'List managed domains'
|
|
33
|
+
}
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
desc 'Execute DNS Made Easy account API operation'
|
|
37
|
+
|
|
38
|
+
option :list, aliases: ['l'], type: :boolean, default: false, desc: 'List account operations'
|
|
39
|
+
option :list_operations, type: :boolean, default: false, desc: 'List account operations'
|
|
40
|
+
|
|
41
|
+
def self.public_operation_names
|
|
42
|
+
DnsMadeEasy::Api::Client.public_operations
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.build_operation_command(operation_name)
|
|
46
|
+
operation_class = Class.new(AccountOperation)
|
|
47
|
+
operation_class.operation_name = operation_name
|
|
48
|
+
operation_class.operation_parameters = DnsMadeEasy::Api::Client.instance_method(operation_name).parameters
|
|
49
|
+
operation_class.desc(operation_metadata(operation_name).fetch(:summary))
|
|
50
|
+
operation_class.define_operation_arguments
|
|
51
|
+
operation_class.define_record_type_option if RECORD_LIST_OPERATIONS.include?(operation_name)
|
|
52
|
+
operation_class
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.operation_help(operation_name)
|
|
56
|
+
metadata = operation_metadata(operation_name)
|
|
57
|
+
return unknown_operation_help(operation_name) unless metadata
|
|
58
|
+
|
|
59
|
+
[
|
|
60
|
+
'Command:',
|
|
61
|
+
" #{metadata.fetch(:usage)}",
|
|
62
|
+
'',
|
|
63
|
+
'Description:',
|
|
64
|
+
" #{metadata.fetch(:summary)}",
|
|
65
|
+
*argument_help(metadata),
|
|
66
|
+
*option_help(metadata)
|
|
67
|
+
].join("\n")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.operation_metadata(operation_name)
|
|
71
|
+
normalized_operation_name = operation_name.to_s
|
|
72
|
+
OPERATION_METADATA[normalized_operation_name] || introspected_operation_metadata(normalized_operation_name)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.introspected_operation_metadata(operation_name)
|
|
76
|
+
return unless public_operation_names.include?(operation_name)
|
|
77
|
+
|
|
78
|
+
method_parameters = DnsMadeEasy::Api::Client.instance_method(operation_name).parameters
|
|
79
|
+
required_arguments = method_parameters.filter_map do |parameter_type, parameter_name|
|
|
80
|
+
parameter_name.to_s.upcase if parameter_type == :req
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
{
|
|
84
|
+
usage: (['dme account', operation_name] + required_arguments).join(' '),
|
|
85
|
+
arguments: required_arguments.map { |argument_name| "#{argument_name.ljust(35)} # Required" },
|
|
86
|
+
options: [],
|
|
87
|
+
summary: "Execute #{operation_name}"
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.unknown_operation_help(operation_name)
|
|
92
|
+
[
|
|
93
|
+
"Error: account operation #{operation_name.inspect} is not valid.",
|
|
94
|
+
'Hint: run `dme account --list` to see valid operations.'
|
|
95
|
+
].join("\n")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def self.argument_help(metadata)
|
|
99
|
+
arguments = metadata.fetch(:arguments)
|
|
100
|
+
return [] if arguments.empty?
|
|
101
|
+
|
|
102
|
+
['', 'Arguments:', *arguments.map { |argument| " #{argument}" }]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def self.option_help(metadata)
|
|
106
|
+
options = metadata.fetch(:options)
|
|
107
|
+
return [] if options.empty?
|
|
108
|
+
|
|
109
|
+
['', 'Options:', *options.map { |option| " #{option}" }]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def call(args: [], list: false, list_operations: false, **)
|
|
113
|
+
reject_unknown_subcommand!(args) unless list || list_operations
|
|
114
|
+
|
|
115
|
+
print_operations
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def reject_unknown_subcommand!(args)
|
|
121
|
+
unknown_subcommand = Array(args).first
|
|
122
|
+
return unless unknown_subcommand
|
|
123
|
+
|
|
124
|
+
warn "Error: account operation #{unknown_subcommand.inspect} is not valid."
|
|
125
|
+
warn 'Hint: run `dme account --help` to see valid operations.'
|
|
126
|
+
raise ArgumentError, "account operation #{unknown_subcommand.inspect} is not valid"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def public_operations
|
|
130
|
+
self.class.public_operation_names
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def print_operations
|
|
134
|
+
public_operations.each do |operation_name|
|
|
135
|
+
metadata = self.class.operation_metadata(operation_name)
|
|
136
|
+
summary = metadata.fetch(:summary)
|
|
137
|
+
puts "#{operation_name.ljust(28)} #{summary}"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Base command for generated account operation subcommands.
|
|
143
|
+
class AccountOperation < Base
|
|
144
|
+
class << self
|
|
145
|
+
attr_accessor :operation_name,
|
|
146
|
+
:operation_parameters
|
|
147
|
+
|
|
148
|
+
def define_operation_arguments
|
|
149
|
+
required_operation_parameters.each do |parameter_name|
|
|
150
|
+
argument parameter_name, required: true, desc: humanize_parameter_name(parameter_name)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def define_record_type_option
|
|
155
|
+
option :record_type, aliases: ['t'], values: Account::RECORD_TYPES, required: false,
|
|
156
|
+
desc: 'Filter returned records by type'
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def required_operation_parameters
|
|
160
|
+
operation_parameters.filter_map do |parameter_type, parameter_name|
|
|
161
|
+
parameter_name if parameter_type == :req
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def optional_operation_parameters
|
|
166
|
+
operation_parameters.filter_map do |parameter_type, parameter_name|
|
|
167
|
+
parameter_name if parameter_type == :opt && parameter_name != :options
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def humanize_parameter_name(parameter_name)
|
|
172
|
+
parameter_name.to_s.tr('_', ' ')
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def call(format: nil, credentials: nil, api_key: nil, api_secret: nil, record_type: nil, **arguments)
|
|
177
|
+
configure_authentication(credentials: credentials, api_key: api_key, api_secret: api_secret)
|
|
178
|
+
result = DnsMadeEasy.client.public_send(self.class.operation_name, *operation_arguments(arguments))
|
|
179
|
+
result = filter_records_by_type(result, record_type) if record_type && record_list_operation?
|
|
180
|
+
print_result(result, format)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private
|
|
184
|
+
|
|
185
|
+
def operation_arguments(arguments)
|
|
186
|
+
self.class.required_operation_parameters.map { |parameter_name| arguments.fetch(parameter_name) } +
|
|
187
|
+
self.class.optional_operation_parameters.filter_map { |parameter_name| arguments[parameter_name] }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def record_list_operation?
|
|
191
|
+
Account::RECORD_LIST_OPERATIONS.include?(self.class.operation_name)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def filter_records_by_type(result, record_type)
|
|
195
|
+
filtered_record_type = record_type.upcase
|
|
196
|
+
return result unless result.respond_to?(:key?) && result.key?('data')
|
|
197
|
+
|
|
198
|
+
filtered_result = result.dup
|
|
199
|
+
filtered_result['data'] = result['data'].select { |record| record['type'] == filtered_record_type }
|
|
200
|
+
filtered_result
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def print_result(result, format)
|
|
204
|
+
case result
|
|
205
|
+
when NilClass
|
|
206
|
+
puts 'No records returned.'
|
|
207
|
+
when Hashie::Mash
|
|
208
|
+
print_formatted(result.to_hash, format)
|
|
209
|
+
else
|
|
210
|
+
print_formatted(result, format)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
register 'account', Account do |prefix|
|
|
216
|
+
Account.public_operation_names.each do |operation_name|
|
|
217
|
+
prefix.register operation_name, Account.build_operation_command(operation_name)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|