zonesync 0.9.0 → 0.11.0
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/CLAUDE.md +90 -0
- data/Gemfile +1 -0
- data/lib/zonesync/cli.rb +9 -0
- data/lib/zonesync/cloudflare.rb +64 -39
- data/lib/zonesync/diff.rb +10 -1
- data/lib/zonesync/errors.rb +35 -0
- data/lib/zonesync/generate.rb +14 -0
- data/lib/zonesync/http.rb +24 -8
- data/lib/zonesync/logger.rb +20 -15
- data/lib/zonesync/manifest.rb +68 -21
- data/lib/zonesync/parser.rb +337 -0
- data/lib/zonesync/provider.rb +105 -26
- data/lib/zonesync/record.rb +18 -23
- data/lib/zonesync/record_hash.rb +15 -0
- data/lib/zonesync/route53.rb +146 -28
- data/lib/zonesync/sync.rb +51 -0
- data/lib/zonesync/validator.rb +77 -19
- data/lib/zonesync/version.rb +1 -1
- data/lib/zonesync/zonefile.rb +22 -311
- data/lib/zonesync.rb +28 -60
- data/sorbet/config +4 -0
- data/sorbet/rbi/annotations/.gitattributes +1 -0
- data/sorbet/rbi/annotations/activesupport.rbi +457 -0
- data/sorbet/rbi/annotations/minitest.rbi +119 -0
- data/sorbet/rbi/annotations/webmock.rbi +9 -0
- data/sorbet/rbi/gems/.gitattributes +1 -0
- data/sorbet/rbi/gems/activesupport@8.0.1.rbi +18474 -0
- data/sorbet/rbi/gems/addressable@2.8.7.rbi +1994 -0
- data/sorbet/rbi/gems/base64@0.2.0.rbi +507 -0
- data/sorbet/rbi/gems/benchmark@0.4.0.rbi +618 -0
- data/sorbet/rbi/gems/bigdecimal@3.1.9.rbi +9 -0
- data/sorbet/rbi/gems/concurrent-ruby@1.3.4.rbi +11645 -0
- data/sorbet/rbi/gems/connection_pool@2.4.1.rbi +9 -0
- data/sorbet/rbi/gems/crack@1.0.0.rbi +145 -0
- data/sorbet/rbi/gems/date@3.4.1.rbi +75 -0
- data/sorbet/rbi/gems/diff-lcs@1.5.1.rbi +1131 -0
- data/sorbet/rbi/gems/drb@2.2.1.rbi +1347 -0
- data/sorbet/rbi/gems/erubi@1.13.1.rbi +155 -0
- data/sorbet/rbi/gems/hashdiff@1.1.2.rbi +353 -0
- data/sorbet/rbi/gems/i18n@1.14.6.rbi +2275 -0
- data/sorbet/rbi/gems/io-console@0.8.0.rbi +9 -0
- data/sorbet/rbi/gems/logger@1.6.4.rbi +940 -0
- data/sorbet/rbi/gems/minitest@5.25.4.rbi +1547 -0
- data/sorbet/rbi/gems/netrc@0.11.0.rbi +159 -0
- data/sorbet/rbi/gems/parallel@1.26.3.rbi +291 -0
- data/sorbet/rbi/gems/polyglot@0.3.5.rbi +42 -0
- data/sorbet/rbi/gems/prism@1.3.0.rbi +40040 -0
- data/sorbet/rbi/gems/psych@5.2.2.rbi +1785 -0
- data/sorbet/rbi/gems/public_suffix@6.0.1.rbi +936 -0
- data/sorbet/rbi/gems/rake@13.2.1.rbi +3028 -0
- data/sorbet/rbi/gems/rbi@0.2.2.rbi +4527 -0
- data/sorbet/rbi/gems/rdoc@6.10.0.rbi +12766 -0
- data/sorbet/rbi/gems/reline@0.6.0.rbi +9 -0
- data/sorbet/rbi/gems/rexml@3.4.0.rbi +4974 -0
- data/sorbet/rbi/gems/rspec-core@3.13.2.rbi +10896 -0
- data/sorbet/rbi/gems/rspec-expectations@3.13.3.rbi +8183 -0
- data/sorbet/rbi/gems/rspec-mocks@3.13.2.rbi +5341 -0
- data/sorbet/rbi/gems/rspec-support@3.13.2.rbi +1630 -0
- data/sorbet/rbi/gems/rspec@3.13.0.rbi +83 -0
- data/sorbet/rbi/gems/securerandom@0.4.1.rbi +75 -0
- data/sorbet/rbi/gems/spoom@1.5.0.rbi +4932 -0
- data/sorbet/rbi/gems/stringio@3.1.2.rbi +9 -0
- data/sorbet/rbi/gems/tapioca@0.16.6.rbi +3611 -0
- data/sorbet/rbi/gems/thor@1.3.2.rbi +4378 -0
- data/sorbet/rbi/gems/treetop@1.6.12.rbi +1895 -0
- data/sorbet/rbi/gems/tzinfo@2.0.6.rbi +5918 -0
- data/sorbet/rbi/gems/uri@1.0.2.rbi +2340 -0
- data/sorbet/rbi/gems/webmock@3.24.0.rbi +1780 -0
- data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +435 -0
- data/sorbet/rbi/gems/yard@0.9.37.rbi +18379 -0
- data/sorbet/rbi/todo.rbi +7 -0
- data/sorbet/tapioca/config.yml +13 -0
- data/sorbet/tapioca/require.rb +4 -0
- data/zonesync.gemspec +3 -0
- metadata +102 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3e2ac350c2fe58aaed3387c001755daba2e460747b09e12ca187e3643895cfec
|
4
|
+
data.tar.gz: b9c28c96bfcb6e4964060aa6fac6b05af5533d7903dd74b76850cc1d58904158
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8861b5bbcb843c30e3017eb2989fc3391913452246d5a6d798b8ccac677430ae845f5189271a6a07d8f4b7f72f07db34153f480281a3e6d398e135c3b0b28cf1
|
7
|
+
data.tar.gz: 6a471b920e9cd971a023eca9992894c65f43ef9e6fffe9d873abb32706f265db98043fdeb7e749cd7a87feeda0faefac8bd4b1477f27059e28b8e793f3f091fe
|
data/CLAUDE.md
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
# CLAUDE.md
|
2
|
+
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
4
|
+
|
5
|
+
## Project Overview
|
6
|
+
|
7
|
+
Zonesync is a Ruby gem that synchronizes DNS zone files with DNS providers (Cloudflare, Route53). It treats DNS configuration as code, enabling version control and CI/CD workflows for DNS records.
|
8
|
+
|
9
|
+
## Commands
|
10
|
+
|
11
|
+
### Development Commands
|
12
|
+
- `bundle install` - Install dependencies
|
13
|
+
- `bundle exec rake` - Run all tests (default task)
|
14
|
+
- `bundle exec rspec` - Run RSpec tests
|
15
|
+
- `bundle exec rspec spec/path/to/specific_spec.rb` - Run a single test file
|
16
|
+
- `bundle exec rspec spec/path/to/specific_spec.rb:123` - Run a specific test at line 123
|
17
|
+
|
18
|
+
### Application Commands
|
19
|
+
- `bundle exec zonesync` - Sync Zonefile to configured DNS provider
|
20
|
+
- `bundle exec zonesync --dry-run` - Preview changes without applying them
|
21
|
+
- `bundle exec zonesync --force` - Force sync ignoring checksum mismatches
|
22
|
+
- `bundle exec zonesync generate` - Generate Zonefile from DNS provider
|
23
|
+
|
24
|
+
### Type Checking (Sorbet)
|
25
|
+
- `bundle exec srb tc` - Run Sorbet type checker
|
26
|
+
- `bundle exec tapioca gem` - Generate RBI files for gems
|
27
|
+
|
28
|
+
## Architecture
|
29
|
+
|
30
|
+
### Core Components
|
31
|
+
|
32
|
+
**Provider Pattern**: Abstract base class `Provider` with concrete implementations:
|
33
|
+
- `Cloudflare` - Cloudflare DNS API integration
|
34
|
+
- `Route53` (AWS) - Route53 DNS API integration
|
35
|
+
- `Filesystem` - Local zone file operations
|
36
|
+
- `Memory` - In-memory provider for testing
|
37
|
+
|
38
|
+
**Key Classes**:
|
39
|
+
- `Sync` - Orchestrates synchronization between source and destination providers
|
40
|
+
- `Generate` - Generates zone files from DNS providers
|
41
|
+
- `Record` - Represents DNS records with type, name, content, TTL
|
42
|
+
- `Zonefile` - Parses and generates RFC-compliant zone files
|
43
|
+
- `Diff` - Calculates differences between record sets
|
44
|
+
- `Manifest` - Tracks which records are managed by zonesync
|
45
|
+
- `Validator` - Validates operations and handles conflicts
|
46
|
+
|
47
|
+
### Data Flow
|
48
|
+
|
49
|
+
1. **Sync Process**: `Zonefile` → `Provider.diff!()` → `operations[]` → `destination.apply()`
|
50
|
+
2. **Generate Process**: DNS Provider → `Zonefile.generate()` → local file
|
51
|
+
3. **Validation**: Checksum verification prevents conflicting external changes
|
52
|
+
4. **Manifest**: TXT records track zonesync-managed records vs external ones
|
53
|
+
|
54
|
+
### Safety Mechanisms
|
55
|
+
|
56
|
+
- **Checksum verification**: Detects external changes to managed records
|
57
|
+
- **Manifest tracking**: Distinguishes zonesync-managed vs external records
|
58
|
+
- **Force mode**: Bypass safety checks when needed
|
59
|
+
- **Dry-run mode**: Preview changes without applying them
|
60
|
+
|
61
|
+
### Configuration
|
62
|
+
|
63
|
+
Credentials stored in Rails-style encrypted configuration:
|
64
|
+
- `config/credentials.yml.enc` - Encrypted credentials file
|
65
|
+
- `config/master.key` - Encryption key
|
66
|
+
- `RAILS_MASTER_KEY` env var - Alternative key source
|
67
|
+
|
68
|
+
## Testing
|
69
|
+
|
70
|
+
- **RSpec** for testing framework
|
71
|
+
- **WebMock** for HTTP request stubbing
|
72
|
+
- **Feature specs** in `spec/features/` test end-to-end workflows
|
73
|
+
- **Unit specs** test individual classes and methods
|
74
|
+
- All tests should pass before committing changes
|
75
|
+
|
76
|
+
## Type Safety
|
77
|
+
|
78
|
+
Uses **Sorbet** for gradual typing:
|
79
|
+
- All files have `# typed: strict` or similar headers
|
80
|
+
- Method signatures use `sig { ... }` blocks
|
81
|
+
- `extend T::Sig` enables signature checking
|
82
|
+
- RBI files in `sorbet/rbi/` define external gem types
|
83
|
+
|
84
|
+
## Error Handling
|
85
|
+
|
86
|
+
Custom exceptions in `lib/zonesync/errors.rb`:
|
87
|
+
- `ConflictError` - Record conflicts
|
88
|
+
- `ChecksumMismatchError` - External changes detected
|
89
|
+
- `MissingManifestError` - Missing manifest record
|
90
|
+
- `DuplicateRecordError` - Duplicate record handling
|
data/Gemfile
CHANGED
data/lib/zonesync/cli.rb
CHANGED
@@ -1,12 +1,19 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require "sorbet-runtime"
|
3
|
+
|
1
4
|
require "thor"
|
2
5
|
|
3
6
|
module Zonesync
|
4
7
|
class CLI < Thor
|
8
|
+
extend T::Sig
|
9
|
+
|
5
10
|
default_command :sync
|
6
11
|
desc "sync --source=Zonefile --destination=zonesync", "syncs the contents of the Zonefile to the DNS server configured in Rails.application.credentials.zonesync"
|
7
12
|
option :source, default: "Zonefile", desc: "path to the zonefile"
|
8
13
|
option :destination, default: "zonesync", desc: "key to the DNS server configuration in Rails.application.credentials"
|
9
14
|
method_option :dry_run, type: :boolean, default: false, aliases: :n, desc: "log operations to STDOUT but don't perform the sync"
|
15
|
+
method_option :force, type: :boolean, default: false, desc: "ignore checksum mismatches and force the sync"
|
16
|
+
sig { void }
|
10
17
|
def sync
|
11
18
|
kwargs = options.to_hash.transform_keys(&:to_sym)
|
12
19
|
Zonesync.call(**kwargs)
|
@@ -18,11 +25,13 @@ module Zonesync
|
|
18
25
|
desc "generate --source=zonesync --destination=Zonefile", "generates a Zonefile from the DNS server configured in Rails.application.credentials.zonesync"
|
19
26
|
option :source, default: "zonesync", desc: "key to the DNS server configuration in Rails.application.credentials"
|
20
27
|
option :destination, default: "Zonefile", desc: "path to the zonefile"
|
28
|
+
sig { void }
|
21
29
|
def generate
|
22
30
|
kwargs = options.to_hash.transform_keys(&:to_sym)
|
23
31
|
Zonesync.generate(**kwargs)
|
24
32
|
end
|
25
33
|
|
34
|
+
sig { returns(TrueClass) }
|
26
35
|
def self.exit_on_failure? = true
|
27
36
|
end
|
28
37
|
end
|
data/lib/zonesync/cloudflare.rb
CHANGED
@@ -1,83 +1,107 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require "sorbet-runtime"
|
3
|
+
|
1
4
|
require "zonesync/record"
|
2
5
|
require "zonesync/http"
|
3
6
|
|
4
7
|
module Zonesync
|
5
8
|
class Cloudflare < Provider
|
9
|
+
sig { returns(String) }
|
6
10
|
def read
|
7
|
-
|
8
|
-
|
9
|
-
end).map(&:to_s).join("\n") + "\n"
|
11
|
+
records = [fake_soa] + all.keys
|
12
|
+
records.map(&:to_s).join("\n") + "\n"
|
10
13
|
end
|
11
14
|
|
15
|
+
sig { params(record: Record).void }
|
12
16
|
def remove record
|
13
|
-
id = all.fetch(record
|
17
|
+
id = all.fetch(record)
|
14
18
|
http.delete("/#{id}")
|
15
19
|
end
|
16
20
|
|
21
|
+
sig { params(old_record: Record, new_record: Record).void }
|
17
22
|
def change old_record, new_record
|
18
|
-
id = all.fetch(old_record
|
19
|
-
http.patch("/#{id}",
|
20
|
-
name: new_record[:name],
|
21
|
-
type: new_record[:type],
|
22
|
-
ttl: new_record[:ttl],
|
23
|
-
content: new_record[:rdata],
|
24
|
-
comment: new_record[:comment],
|
25
|
-
})
|
23
|
+
id = all.fetch(old_record)
|
24
|
+
http.patch("/#{id}", to_hash(new_record))
|
26
25
|
end
|
27
26
|
|
27
|
+
sig { params(record: Record).void }
|
28
28
|
def add record
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
29
|
+
add_with_duplicate_handling(record) do
|
30
|
+
begin
|
31
|
+
http.post("", to_hash(record))
|
32
|
+
rescue RuntimeError => e
|
33
|
+
# Convert CloudFlare-specific duplicate error to standard exception
|
34
|
+
if e.message.include?('"code":81058') && e.message.include?("An identical record already exists")
|
35
|
+
raise DuplicateRecordError.new(record, "CloudFlare error 81058")
|
36
|
+
else
|
37
|
+
# Re-raise other errors
|
38
|
+
raise
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
36
42
|
end
|
37
43
|
|
44
|
+
sig { returns(T::Hash[Record, String]) }
|
38
45
|
def all
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
map.merge to_record(attrs) => attrs["id"]
|
43
|
-
end
|
46
|
+
response = http.get("")
|
47
|
+
response["result"].reduce({}) do |map, attrs|
|
48
|
+
map.merge to_record(attrs) => attrs["id"]
|
44
49
|
end
|
45
50
|
end
|
46
51
|
|
47
52
|
private
|
48
53
|
|
54
|
+
sig { params(record: Record).returns(T::Hash[String, String]) }
|
55
|
+
def to_hash record
|
56
|
+
hash = record.to_h
|
57
|
+
content = hash.delete(:rdata)
|
58
|
+
|
59
|
+
if record.type == "MX"
|
60
|
+
# For MX records, split "priority hostname" into separate fields
|
61
|
+
priority, hostname = T.must(content).split(" ", 2)
|
62
|
+
hash[:priority] = priority.to_i
|
63
|
+
hash[:content] = hostname.sub(/\.$/, "") # remove trailing dot
|
64
|
+
else
|
65
|
+
hash[:content] = content
|
66
|
+
end
|
67
|
+
|
68
|
+
hash[:comment] = hash.delete(:comment) # maintain original order
|
69
|
+
hash
|
70
|
+
end
|
71
|
+
|
72
|
+
sig { params(attrs: T::Hash[String, String]).returns(Record) }
|
49
73
|
def to_record attrs
|
50
74
|
rdata = attrs["content"]
|
51
75
|
if %w[CNAME MX].include?(attrs["type"])
|
52
|
-
rdata = normalize_trailing_period(rdata)
|
76
|
+
rdata = normalize_trailing_period(T.must(rdata))
|
53
77
|
end
|
54
78
|
if attrs["type"] == "MX"
|
55
79
|
rdata = "#{attrs["priority"]} #{rdata}"
|
56
80
|
end
|
57
81
|
if %w[TXT SPF NAPTR].include?(attrs["type"])
|
58
|
-
rdata = normalize_quoting(rdata)
|
59
|
-
end
|
60
|
-
if attrs["type"] == "TXT"
|
61
|
-
rdata = normalize_quoting(rdata)
|
82
|
+
rdata = normalize_quoting(T.must(rdata))
|
62
83
|
end
|
63
84
|
Record.new(
|
64
|
-
name: normalize_trailing_period(attrs["name"]),
|
85
|
+
name: normalize_trailing_period(T.must(attrs["name"])),
|
65
86
|
type: attrs["type"],
|
66
87
|
ttl: attrs["ttl"].to_i,
|
67
88
|
rdata:,
|
68
89
|
comment: attrs["comment"],
|
69
|
-
)
|
90
|
+
)
|
70
91
|
end
|
71
92
|
|
93
|
+
sig { params(value: String).returns(String) }
|
72
94
|
def normalize_trailing_period value
|
73
95
|
value =~ /\.$/ ? value : value + "."
|
74
96
|
end
|
75
97
|
|
98
|
+
sig { params(value: String).returns(String) }
|
76
99
|
def normalize_quoting value
|
77
|
-
value =~ /^".+"$/ ? value : %("#{value}") # handle quote wrapping
|
100
|
+
value = value =~ /^".+"$/ ? value : %("#{value}") # handle quote wrapping
|
78
101
|
value.gsub('" "', "") # handle multiple txt record joining
|
79
102
|
end
|
80
103
|
|
104
|
+
sig { returns(Zonesync::Record) }
|
81
105
|
def fake_soa
|
82
106
|
zone_name = http.get("/..")["result"]["name"]
|
83
107
|
Record.new(
|
@@ -89,19 +113,20 @@ module Zonesync
|
|
89
113
|
)
|
90
114
|
end
|
91
115
|
|
116
|
+
sig { returns(HTTP) }
|
92
117
|
def http
|
93
118
|
return @http if @http
|
94
|
-
@http = HTTP.new("https://api.cloudflare.com/client/v4/zones/#{
|
95
|
-
@http.before_request do |request|
|
119
|
+
@http = T.let(HTTP.new("https://api.cloudflare.com/client/v4/zones/#{config.fetch(:zone_id)}/dns_records"), T.nilable(Zonesync::HTTP))
|
120
|
+
T.must(@http).before_request do |request|
|
96
121
|
request["Content-Type"] = "application/json"
|
97
|
-
if
|
98
|
-
request["Authorization"] = "Bearer #{
|
122
|
+
if config[:token]
|
123
|
+
request["Authorization"] = "Bearer #{config[:token]}"
|
99
124
|
else
|
100
|
-
request["X-Auth-Email"] =
|
101
|
-
request["X-Auth-Key"] =
|
125
|
+
request["X-Auth-Email"] = config.fetch(:email)
|
126
|
+
request["X-Auth-Key"] = config.fetch(:key)
|
102
127
|
end
|
103
128
|
end
|
104
|
-
@http
|
129
|
+
T.must(@http)
|
105
130
|
end
|
106
131
|
end
|
107
132
|
end
|
data/lib/zonesync/diff.rb
CHANGED
@@ -1,11 +1,20 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require "sorbet-runtime"
|
3
|
+
|
1
4
|
require "diff/lcs"
|
2
5
|
|
3
6
|
module Zonesync
|
4
|
-
|
7
|
+
Operation = T.type_alias { [Symbol, T::Array[Record]] }
|
8
|
+
|
9
|
+
Diff = Struct.new(:from, :to) do
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
sig { params(from: T::Array[Record], to: T::Array[Record]).returns(T.untyped) }
|
5
13
|
def self.call(from:, to:)
|
6
14
|
new(from, to).call
|
7
15
|
end
|
8
16
|
|
17
|
+
sig { returns(T::Array[[Symbol, T::Array[Record]]]) }
|
9
18
|
def call
|
10
19
|
changes = ::Diff::LCS.sdiff(from, to)
|
11
20
|
changes.map do |change|
|
data/lib/zonesync/errors.rb
CHANGED
@@ -1,10 +1,17 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require "sorbet-runtime"
|
3
|
+
|
1
4
|
module Zonesync
|
2
5
|
class ConflictError < StandardError
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
sig { params(existing: T.nilable(Record), new: Record).void }
|
3
9
|
def initialize existing, new
|
4
10
|
@existing = existing
|
5
11
|
@new = new
|
6
12
|
end
|
7
13
|
|
14
|
+
sig { returns(String) }
|
8
15
|
def message
|
9
16
|
<<~MSG
|
10
17
|
The following untracked DNS record already exists and would be overwritten.
|
@@ -15,10 +22,14 @@ module Zonesync
|
|
15
22
|
end
|
16
23
|
|
17
24
|
class MissingManifestError < StandardError
|
25
|
+
extend T::Sig
|
26
|
+
|
27
|
+
sig { params(manifest: Record).void }
|
18
28
|
def initialize manifest
|
19
29
|
@manifest = manifest
|
20
30
|
end
|
21
31
|
|
32
|
+
sig { returns(String) }
|
22
33
|
def message
|
23
34
|
<<~MSG
|
24
35
|
The zonesync_manifest TXT record is missing. If this is the very first sync, make sure the Zonefile matches what's on the DNS server exactly. Otherwise, someone else may have removed it.
|
@@ -28,11 +39,15 @@ module Zonesync
|
|
28
39
|
end
|
29
40
|
|
30
41
|
class ChecksumMismatchError < StandardError
|
42
|
+
extend T::Sig
|
43
|
+
|
44
|
+
sig { params(existing: T.nilable(Record), new: Record).void }
|
31
45
|
def initialize existing, new
|
32
46
|
@existing = existing
|
33
47
|
@new = new
|
34
48
|
end
|
35
49
|
|
50
|
+
sig { returns(String) }
|
36
51
|
def message
|
37
52
|
<<~MSG
|
38
53
|
The zonesync_checksum TXT record does not match the current state of the DNS records. This probably means that someone else has changed them.
|
@@ -41,5 +56,25 @@ module Zonesync
|
|
41
56
|
MSG
|
42
57
|
end
|
43
58
|
end
|
59
|
+
|
60
|
+
class DuplicateRecordError < StandardError
|
61
|
+
extend T::Sig
|
62
|
+
|
63
|
+
sig { params(record: Record, provider_message: T.nilable(String)).void }
|
64
|
+
def initialize record, provider_message = nil
|
65
|
+
@record = record
|
66
|
+
@provider_message = provider_message
|
67
|
+
end
|
68
|
+
|
69
|
+
sig { returns(String) }
|
70
|
+
def message
|
71
|
+
msg = "Record already exists: #{@record.name} #{@record.type}"
|
72
|
+
msg += " (#{@provider_message})" if @provider_message
|
73
|
+
msg
|
74
|
+
end
|
75
|
+
|
76
|
+
sig { returns(Record) }
|
77
|
+
attr_reader :record
|
78
|
+
end
|
44
79
|
end
|
45
80
|
|
data/lib/zonesync/http.rb
CHANGED
@@ -1,38 +1,51 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require "sorbet-runtime"
|
3
|
+
|
1
4
|
require "net/http"
|
2
5
|
require "json"
|
3
6
|
|
4
7
|
module Zonesync
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
8
|
+
HTTP = Struct.new(:base) do
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
sig { params(base: String).void }
|
12
|
+
def initialize(base)
|
13
|
+
super
|
14
|
+
@before_request = T.let([], T::Array[T.untyped])
|
15
|
+
@after_response = T.let([], T::Array[T.untyped])
|
10
16
|
end
|
11
17
|
|
18
|
+
sig { params(path: String).returns(T.untyped) }
|
12
19
|
def get path
|
13
20
|
request("get", path)
|
14
21
|
end
|
15
22
|
|
23
|
+
sig { params(path: String, body: T.untyped).returns(T.untyped) }
|
16
24
|
def post path, body
|
17
25
|
request("post", path, body)
|
18
26
|
end
|
19
27
|
|
28
|
+
sig { params(path: String, body: T.untyped).returns(T.untyped) }
|
20
29
|
def patch path, body
|
21
30
|
request("patch", path, body)
|
22
31
|
end
|
23
32
|
|
33
|
+
sig { params(path: String).returns(T.untyped) }
|
24
34
|
def delete path
|
25
35
|
request("delete", path)
|
26
36
|
end
|
27
37
|
|
38
|
+
sig { params(block: T.proc.params(arg0: T.untyped, arg1: T.untyped, arg2: T.untyped).void).void }
|
28
39
|
def before_request &block
|
29
40
|
@before_request << block
|
30
41
|
end
|
31
42
|
|
43
|
+
sig { params(block: T.proc.params(arg0: T.untyped).void).void }
|
32
44
|
def after_response &block
|
33
45
|
@after_response << block
|
34
46
|
end
|
35
47
|
|
48
|
+
sig { params(method: String, path: String, body: T.untyped).returns(T.untyped) }
|
36
49
|
def request method, path, body=nil
|
37
50
|
uri = URI.parse("#{base}#{path}")
|
38
51
|
request = Net::HTTP.const_get(method.to_s.capitalize).new(uri.path)
|
@@ -42,12 +55,15 @@ module Zonesync
|
|
42
55
|
end
|
43
56
|
|
44
57
|
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
45
|
-
|
46
|
-
|
58
|
+
if request.fetch("Content-Type", "").include?("application/json")
|
59
|
+
http.request(request, JSON.dump(body))
|
60
|
+
else
|
61
|
+
http.request(request, body)
|
62
|
+
end
|
47
63
|
end
|
48
64
|
|
49
65
|
@after_response.each do |block|
|
50
|
-
call(response)
|
66
|
+
block.call(response)
|
51
67
|
end
|
52
68
|
|
53
69
|
raise response.body unless response.code =~ /^20.$/
|
data/lib/zonesync/logger.rb
CHANGED
@@ -1,24 +1,29 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require "sorbet-runtime"
|
3
|
+
|
1
4
|
require "logger"
|
2
5
|
require "fileutils"
|
3
6
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
+
module Zonesync
|
8
|
+
class Logger
|
9
|
+
extend T::Sig
|
7
10
|
|
8
|
-
|
9
|
-
|
10
|
-
loggers
|
11
|
-
end
|
11
|
+
sig { params(method: Symbol, records: T::Array[Record], dry_run: T::Boolean).void }
|
12
|
+
def self.log method, records, dry_run: false
|
13
|
+
loggers = [::Logger.new(STDOUT)]
|
12
14
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
15
|
+
if !dry_run
|
16
|
+
FileUtils.mkdir_p("log")
|
17
|
+
loggers << ::Logger.new("log/zonesync.log")
|
18
|
+
end
|
19
|
+
|
20
|
+
loggers.each do |logger|
|
21
|
+
operation =
|
22
|
+
(records.length == 2 ? "\n" : "") +
|
23
|
+
records.map { |r| r.to_h.values.join(" ") }.join("->\n")
|
24
|
+
logger.info "Zonesync: #{method.capitalize} #{operation}"
|
20
25
|
end
|
21
|
-
logger.info "Zonesync: #{method.capitalize} #{operation}"
|
22
26
|
end
|
23
27
|
end
|
24
28
|
end
|
29
|
+
|