zonesync 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 799ab804db56a8f9b6882b1dd64ebdcad5f88310bb25ca6261ab997ecb9934d9
4
+ data.tar.gz: 3410be337d22be94eaae226aadf34d064f9f66289306023dce0d4d26ecaa31b2
5
+ SHA512:
6
+ metadata.gz: 06c7e0f79883aaa040aa9d98b61793495adcc48ce565f1072d14cf22c98e81aea2877d4e4917371f72d26d6b952a1f8daed443e71c51d32003464b9b850a61e2
7
+ data.tar.gz: 1112ee6ef2d185e6d21cea7284035960e71d9dc6b23dc21a8768c08da4b831c1780452fbb1d93e2436ded427c4559b70643a3c7eeca3050a0ee10741090a0fa6
data/.envrc ADDED
@@ -0,0 +1,2 @@
1
+ layout ruby
2
+
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require debug
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in zonesync.gemspec
4
+ gemspec
5
+
6
+ gem "debug"
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 James Ottaway
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # zonesync
2
+
3
+ Sync your DNS host with your DNS zone file, making it easy to version your zone file and sync changes.
4
+
5
+ ## Why?
6
+
7
+ Configuration management is important, and switched-on technical types now agree that "configuration is code". This means that your DNS configuration should be treated with the same degree of respect you would give to any other code you would write.
8
+
9
+ In order to live up to this standard, there needs to be an easy way to manage your DNS host file in a SCM tool like Git, allowing you to feed it into a continuous integration pipeline. This library enables this very ideal, making DNS management no different to source code management.
10
+
11
+ ## How?
12
+
13
+ ### Install
14
+
15
+ Add `zonesync` to your Gemfile:
16
+
17
+ ```ruby
18
+ source 'https://rubygems.org'
19
+
20
+ gem 'zonesync'
21
+ ```
22
+
23
+ or run:
24
+
25
+ `gem install zonesync`
26
+
27
+ ### DNS zone file
28
+
29
+ The following is an example DNS zone file for `example.com`:
30
+
31
+ ```
32
+ $ORIGIN example.com.
33
+ $TTL 1h
34
+ example.com. IN SOA ns.example.com. username.example.com. (2007120710; 1d; 2h; 4w; 1h)
35
+ example.com. NS ns
36
+ example.com. NS ns.somewhere.example.
37
+ example.com. MX 10 mail.example.com.
38
+ @ MX 20 mail2.example.com.
39
+ @ MX 50 mail3
40
+ example.com. A 192.0.2.1
41
+ AAAA 2001:db8:10::1
42
+ ns A 192.0.2.2
43
+ AAAA 2001:db8:10::2
44
+ www CNAME example.com.
45
+ wwwtest CNAME www
46
+ mail A 192.0.2.3
47
+ mail2 A 192.0.2.4
48
+ mail3 A 192.0.2.5
49
+ ```
50
+
51
+ ### DNS Host
52
+
53
+ We need to tell `zonesync` about our DNS host by building a small YAML file. The structure of this file will depend on your DNS host, so here are some examples:
54
+
55
+ **Cloudflare**
56
+
57
+ ```
58
+ provider: Cloudflare
59
+ email: <CLOUDFLARE_EMAIL>
60
+ key: <CLOUDFLARE_API_KEY>
61
+ ```
62
+
63
+ **Route 53**
64
+
65
+ ```
66
+ provider: AWS
67
+ aws_access_key_id: <AWS_ACCESS_KEY_ID>
68
+ aws_secret_access_key: <AWS_SECRET_ACCESS_KEY>
69
+ ```
70
+
71
+ ### Usage
72
+
73
+ Assuming your zone file lives in `hostfile.txt` and your DNS provider credentials are configured in `provider.yml`:
74
+
75
+ ```ruby
76
+ require 'zonesync'
77
+ Zonesync.call(zonefile: 'hostfile.txt', credentials: YAML.load('provider.yml'))
78
+ ```
79
+
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+ task :default => :spec
8
+
@@ -0,0 +1,64 @@
1
+ require "zonesync/record"
2
+ require "zonesync/http"
3
+
4
+ module Zonesync
5
+ class Cloudflare < Provider
6
+ def read
7
+ http.get("/export")
8
+ end
9
+
10
+ def remove record
11
+ id = records.fetch(record)
12
+ http.delete("/#{id}")
13
+ end
14
+
15
+ def change old_record, new_record
16
+ id = records.fetch(old_record)
17
+ http.patch("/#{id}", {
18
+ name: new_record[:name],
19
+ type: new_record[:type],
20
+ ttl: new_record[:ttl],
21
+ content: new_record[:rdata],
22
+ })
23
+ end
24
+
25
+ def add record
26
+ http.post(nil, {
27
+ name: record[:name],
28
+ type: record[:type],
29
+ ttl: record[:ttl],
30
+ content: record[:rdata],
31
+ })
32
+ end
33
+
34
+ def records
35
+ @records ||= begin
36
+ response = http.get(nil)
37
+ response["result"].reduce({}) do |map, attrs|
38
+ map.merge attrs["id"] => Record.new(
39
+ attrs["name"],
40
+ attrs["type"],
41
+ attrs["ttl"].to_i,
42
+ attrs["content"],
43
+ ).to_h
44
+ end.invert
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def http
51
+ return @http if @http
52
+ @http = HTTP.new("https://api.cloudflare.com/client/v4/zones/#{credentials[:zone_id]}/dns_records")
53
+ @http.before_request do |request|
54
+ request["Content-Type"] = "application/json"
55
+ request["X-Auth-Email"] = credentials[:email]
56
+ request["X-Auth-Key"] = credentials[:key]
57
+ end
58
+ @http.after_response do |response|
59
+ end
60
+ @http
61
+ end
62
+ end
63
+ end
64
+
@@ -0,0 +1,24 @@
1
+ require "diff/lcs"
2
+
3
+ module Zonesync
4
+ class Diff < Struct.new(:from, :to)
5
+ def self.call(from:, to:)
6
+ new(from, to).call
7
+ end
8
+
9
+ def call
10
+ changes = ::Diff::LCS.sdiff(from.diffable_records, to.diffable_records)
11
+ changes.map do |change|
12
+ case change.action
13
+ when "-"
14
+ [:remove, change.old_element.to_h]
15
+ when "!"
16
+ [:change, [change.old_element.to_h, change.new_element.to_h]]
17
+ when "+"
18
+ [:add, change.new_element.to_h]
19
+ end
20
+ end.compact
21
+ end
22
+ end
23
+ end
24
+
@@ -0,0 +1,50 @@
1
+ require "net/http"
2
+
3
+ module Zonesync
4
+ class HTTP < Struct.new(:base)
5
+ def get path
6
+ request("get", path)
7
+ end
8
+
9
+ def post path, body
10
+ request("post", path, body)
11
+ end
12
+
13
+ def patch path, body
14
+ request("patch", path, body)
15
+ end
16
+
17
+ def delete path
18
+ request("delete", path)
19
+ end
20
+
21
+ def before_request &block
22
+ @before_request = block
23
+ end
24
+
25
+ def after_response &block
26
+ @after_response = block
27
+ end
28
+
29
+ def request method, path, body=nil
30
+ uri = URI.parse("#{base}#{path}")
31
+ request = Net::HTTP.const_get(method.to_s.capitalize).new(uri.path)
32
+
33
+ @before_request.call(request) if @before_request
34
+
35
+ response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
36
+ body = JSON.dump(body) if request["Content-Type"].include?("application/json")
37
+ http.request(request, body)
38
+ end
39
+
40
+ @after_response.call(response) if @after_response
41
+
42
+ raise response.body unless response.code =~ /^20.$/
43
+ if response["Content-Type"].include?("application/json")
44
+ JSON.parse(response.body)
45
+ else
46
+ response.body
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,64 @@
1
+ require "dns/zonefile"
2
+ require "zonesync/record"
3
+
4
+ module Zonesync
5
+ class Provider < Struct.new(:credentials)
6
+ def self.from credentials
7
+ Zonesync.const_get(credentials[:provider]).new(credentials)
8
+ end
9
+
10
+ def diffable_records
11
+ DNS::Zonefile.load(read).records.map do |record|
12
+ rdata = case record
13
+ when DNS::Zonefile::A, DNS::Zonefile::AAAA
14
+ record.address
15
+ when DNS::Zonefile::CNAME
16
+ record.domainname
17
+ when DNS::Zonefile::MX
18
+ record.domainname
19
+ when DNS::Zonefile::TXT
20
+ record.data
21
+ else
22
+ next
23
+ end
24
+ Record.new(
25
+ record.host,
26
+ record.class.name.split("::").last,
27
+ record.ttl,
28
+ rdata,
29
+ )
30
+ end.compact
31
+ end
32
+
33
+ def read record
34
+ raise NotImplementedError
35
+ end
36
+
37
+ def remove record
38
+ raise NotImplementedError
39
+ end
40
+
41
+ def change old_record, new_record
42
+ raise NotImplementedError
43
+ end
44
+
45
+ def add record
46
+ raise NotImplementedError
47
+ end
48
+ end
49
+
50
+ require "zonesync/cloudflare"
51
+
52
+ class Memory < Provider
53
+ def read
54
+ credentials[:string]
55
+ end
56
+ end
57
+
58
+ class Filesystem < Provider
59
+ def read
60
+ File.read(credentials[:path])
61
+ end
62
+ end
63
+ end
64
+
@@ -0,0 +1,5 @@
1
+ module Zonesync
2
+ class Record < Struct.new(:name, :type, :ttl, :rdata)
3
+ end
4
+ end
5
+
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zonesync
4
+ VERSION = "0.1.0"
5
+ end
data/lib/zonesync.rb ADDED
@@ -0,0 +1,18 @@
1
+ require "zonesync/provider"
2
+ require "zonesync/diff"
3
+
4
+ module Zonesync
5
+ def self.call zonefile:, credentials:
6
+ Sync.new(zonefile, credentials).call
7
+ end
8
+
9
+ class Sync < Struct.new(:zonefile, :credentials)
10
+ def call
11
+ local = Provider.from({ provider: "Filesystem", path: zonefile })
12
+ remote = Provider.from(credentials)
13
+ operations = Diff.call(from: remote, to: local)
14
+ operations.each { |method, args| puts [method, *args].inspect }
15
+ end
16
+ end
17
+ end
18
+
data/zonesync.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/zonesync/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "zonesync"
7
+ spec.version = Zonesync::VERSION
8
+ spec.authors = ["Micah Geisel", "James Ottaway"]
9
+ spec.email = ["micah@botandrose.com", "git@james.ottaway.io"]
10
+
11
+ spec.summary = %q{Sync your Zone file with your DNS host}
12
+ spec.description = spec.summary
13
+ spec.homepage = "https://github.com/botandrose/zonesync"
14
+ spec.license = "MIT"
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = spec.homepage
18
+
19
+ # Specify which files should be added to the gem when it is released.
20
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
21
+ spec.files = Dir.chdir(__dir__) do
22
+ `git ls-files -z`.split("\x0").reject do |f|
23
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
24
+ end
25
+ end
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_dependency "dns-zonefile", "~>1.0"
31
+ spec.add_dependency "diff-lcs", "~>1.4"
32
+
33
+ spec.add_development_dependency "rspec"
34
+ spec.add_development_dependency "webmock"
35
+ end
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: zonesync
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Micah Geisel
8
+ - James Ottaway
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2024-01-28 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: dns-zonefile
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '1.0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '1.0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: diff-lcs
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '1.4'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '1.4'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rspec
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: webmock
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ description: Sync your Zone file with your DNS host
71
+ email:
72
+ - micah@botandrose.com
73
+ - git@james.ottaway.io
74
+ executables: []
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - ".envrc"
79
+ - ".rspec"
80
+ - Gemfile
81
+ - LICENSE.txt
82
+ - README.md
83
+ - Rakefile
84
+ - lib/zonesync.rb
85
+ - lib/zonesync/cloudflare.rb
86
+ - lib/zonesync/diff.rb
87
+ - lib/zonesync/http.rb
88
+ - lib/zonesync/provider.rb
89
+ - lib/zonesync/record.rb
90
+ - lib/zonesync/version.rb
91
+ - zonesync.gemspec
92
+ homepage: https://github.com/botandrose/zonesync
93
+ licenses:
94
+ - MIT
95
+ metadata:
96
+ homepage_uri: https://github.com/botandrose/zonesync
97
+ source_code_uri: https://github.com/botandrose/zonesync
98
+ post_install_message:
99
+ rdoc_options: []
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ requirements: []
113
+ rubygems_version: 3.5.1
114
+ signing_key:
115
+ specification_version: 4
116
+ summary: Sync your Zone file with your DNS host
117
+ test_files: []