dslimple 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d3524b1943dd7ea604e2dc0405d806500a4c5c3f
4
+ data.tar.gz: bbd8df2e9fa8193f6fa6941784503115335a0e9b
5
+ SHA512:
6
+ metadata.gz: b50b823159fdbcb21b32c6db9723a0728f951affc184e354b3e786d48098042c552ce1dbf74c150f93db37aedb51d1803b7c03d6d51f8163d91b0871017b83da
7
+ data.tar.gz: 4f1ea991bdbe56a6a6e8c58ccc444c4e22bacff0b0cc4da2f0a8796e49e897383720a2a0565414d50dcabb5908637b77a53901a1f63d73755c4522273b8a40da
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /Domainfile
11
+ /domainfiles
data/.rubocop.yml ADDED
@@ -0,0 +1,28 @@
1
+ AllCops:
2
+ Include:
3
+ - "*.gemspec"
4
+ Exclude:
5
+ - "vendor/**/*"
6
+ - "db/schema.rb"
7
+ DisplayCopNames: true
8
+
9
+ Style/Alias:
10
+ EnforcedStyle: prefer_alias_method
11
+
12
+ Style/ClassAndModuleChildren:
13
+ EnforcedStyle: compact
14
+
15
+ Style/CaseEquality:
16
+ Enabled: false
17
+
18
+ Style/Documentation:
19
+ Enabled: false
20
+
21
+ Metrics/LineLength:
22
+ Max: 160
23
+
24
+ Metrics/MethodLength:
25
+ Max: 20
26
+
27
+ Metrics/AbcSize:
28
+ Max: 25
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in dslimple.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Sho Kusano
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # DSLimple
2
+
3
+ __DSLimple__ is a tool to manage [DNSimple](https://dnsimple.com/).
4
+
5
+ It defines the state of DNSimple using DSL, and updates DNSimple according to DSL.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'dslimple'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install dslimple
22
+
23
+ ## Usage
24
+
25
+ ```shell
26
+ export DLSIMPLE_EMAIL="..."
27
+ export DLSIMPLE_API_TOKEN="..."
28
+ dslimple export -f Domainfile
29
+ vi Domainfile
30
+ dslimple apply --dry-run -f Domainfile
31
+ dslimple apply --yes -f Domainfile
32
+ ```
33
+
34
+ ### Help
35
+
36
+ ```
37
+ $ dslimple help
38
+ Commands:
39
+ dslimple apply # Apply domain specifications
40
+ dslimple export # Export domain specifications
41
+ dslimple help [COMMAND] # Describe available commands or one specific command
42
+
43
+ Options:
44
+ -e, [--email=EMAIL] # Your E-Mail address
45
+ -t, [--api-token=API_TOKEN] # Your API token
46
+ -dt, [--domain-token=DOMAIN_TOKEN] # Your Domain API token
47
+ [--sandbox], [--no-sandbox] # Use sandbox API(at sandbox.dnsimple.com)
48
+ # Default: true
49
+ [--debug], [--no-debug]
50
+ ```
51
+
52
+ #### help apply
53
+
54
+ ```
55
+ $ dslimple help apply
56
+ Usage:
57
+ dslimple apply
58
+
59
+ Options:
60
+ -o, [--only=one two three] # Specify domains for apply
61
+ -d, [--dry-run], [--no-dry-run]
62
+ -f, [--file=FILE] # Source Domainfile path
63
+ # Default: Domainfile
64
+ [--addition], [--no-addition] # Add specified records
65
+ # Default: true
66
+ [--modification], [--no-modification] # Modify specified records
67
+ # Default: true
68
+ [--deletion], [--no-deletion] # Delete unspecified records
69
+ # Default: true
70
+ -y, [--yes], [--no-yes] # Do not confirm on before apply
71
+ -e, [--email=EMAIL] # Your E-Mail address
72
+ -t, [--api-token=API_TOKEN] # Your API token
73
+ -dt, [--domain-token=DOMAIN_TOKEN] # Your Domain API token
74
+ [--sandbox], [--no-sandbox] # Use sandbox API(at sandbox.dnsimple.com)
75
+ # Default: true
76
+ [--debug], [--no-debug]
77
+
78
+ Apply domain specifications
79
+ ```
80
+
81
+ #### help export
82
+
83
+ ```
84
+ $ dslimple help export
85
+ Usage:
86
+ dslimple export
87
+
88
+ Options:
89
+ -o, [--only=one two three] # Specify domains for export
90
+ -f, [--file=FILE] # Export Domainfile path
91
+ # Default: Domainfile
92
+ -d, [--dir=DIR] # Export directory path for split
93
+ # Default: ./domainfiles
94
+ -s, [--split], [--no-split] # Export with split by domains
95
+ -m, [--modeline], [--no-modeline] # Export with modeline for Vim
96
+ [--soa-and-ns], [--no-soa-and-ns] # Export without SOA and NS records
97
+ -e, [--email=EMAIL] # Your E-Mail address
98
+ -t, [--api-token=API_TOKEN] # Your API token
99
+ -dt, [--domain-token=DOMAIN_TOKEN] # Your Domain API token
100
+ [--sandbox], [--no-sandbox] # Use sandbox API(at sandbox.dnsimple.com)
101
+ # Default: true
102
+ [--debug], [--no-debug]
103
+
104
+ Export domain specifications
105
+ ```
106
+
107
+ ## Domainfile Examples
108
+
109
+ ### Basic
110
+
111
+ The following defines are all the same meaning
112
+
113
+ ```ruby
114
+ domain "example.com" do
115
+ a_record ttl: 3600 do
116
+ "0.0.0.0"
117
+ end
118
+
119
+ record type: :a, ttl: 3600 do
120
+ "0.0.0.0"
121
+ end
122
+
123
+ a_record do
124
+ ttl 3600
125
+ content "0.0.0.0"
126
+ end
127
+ end
128
+ ```
129
+
130
+ ### Dynamic
131
+
132
+ DSLimple's DSL works on ruby.
133
+
134
+ ```ruby
135
+ require 'open-uri'
136
+ require 'json'
137
+
138
+ domain "example.internal" do
139
+ JSON.parse(open('http://my.internal.service/records.json', &:read)).each do |record_data|
140
+ recored record_data['name'], record_data['options'] { record_data['content'] }
141
+ end
142
+ end
143
+ ```
144
+
145
+ ## Contributing
146
+
147
+ Bug reports and pull requests are welcome on GitHub at https://github.com/zeny-io/dslimple.
148
+
149
+
150
+ ## License
151
+
152
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
153
+
154
+ ## Inspired by
155
+
156
+ - [roadworker](https://github.com/winebarrel/roadworker)
157
+
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler/gem_tasks'
2
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "dslimple"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/dslimple.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'dslimple/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'dslimple'
8
+ spec.version = Dslimple::VERSION
9
+ spec.authors = ['Sho Kusano']
10
+ spec.email = ['sho-kusano@zeny.io']
11
+
12
+ spec.summary = 'DSLimple is a tool to manage DNSimple.'
13
+ spec.description = 'DSLimple is a tool to manage DNSimple. It defines the state of DNSimple using DSL, and updates DNSimple according to DSL.'
14
+ spec.homepage = 'https://github.com/zeny-io/dslimple'
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = 'exe'
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.add_development_dependency 'bundler', '~> 1.11'
23
+ spec.add_development_dependency 'rake', '~> 10.0'
24
+ spec.add_development_dependency 'rubocop'
25
+
26
+ spec.add_dependency 'dnsimple', '~> 2.1'
27
+ spec.add_dependency 'thor', '~> 0.19'
28
+ end
data/exe/dslimple ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'dslimple/cli'
4
+ Dslimple::CLI.start(ARGV)
@@ -0,0 +1,68 @@
1
+ require 'dslimple'
2
+ require 'pp'
3
+
4
+ class Dslimple::Applier
5
+ OPERATION_COLORS = {
6
+ addition: :green,
7
+ modification: :yellow,
8
+ deletion: :red
9
+ }
10
+
11
+ attr_reader :api_client, :shell, :options
12
+
13
+ def initialize(api_client, shell, options = {})
14
+ @api_client = api_client
15
+ @shell = shell
16
+ @options = options
17
+ end
18
+
19
+ def execute
20
+ dsl = Dslimple::DSL.new(options[:file], options)
21
+
22
+ dsl.execute
23
+
24
+ expected_domains = dsl.transform
25
+ expected_domains.select! { |domain| options[:only].include?(domain.name) } if options[:only].any?
26
+
27
+ @buildler = Dslimple::QueryBuilder.new(fetch_domains, expected_domains)
28
+ @buildler.execute
29
+ queries = @buildler.filtered_queries(options)
30
+
31
+ if queries.empty?
32
+ shell.say('No Changes', :bold)
33
+ return
34
+ end
35
+
36
+ show_plan(queries)
37
+
38
+ return if options[:dry_run] || !(options[:yes] || shell.yes?("Apply #{queries.size} changes. OK?(y/n) >"))
39
+
40
+ apply(queries)
41
+ end
42
+
43
+ def fetch_domains
44
+ domains = api_client.domains.list.map { |domain| Dslimple::Domain.new(domain.name, api_client, id: domain.id) }
45
+ domains.each(&:fetch_records!)
46
+ domains.select! { |domain| options[:only].include?(domain.name) } if options[:only].any?
47
+ domains
48
+ end
49
+
50
+ def show_plan(queries)
51
+ shell.say("Changes", :bold)
52
+ queries.each do |query|
53
+ show_query(query)
54
+ end
55
+ end
56
+
57
+ def apply(queries)
58
+ shell.say('Apply', :bold)
59
+ queries.each do |query|
60
+ show_query(query)
61
+ query.execute(api_client)
62
+ end
63
+ end
64
+
65
+ def show_query(query)
66
+ shell.say("#{shell.set_color(query.operation.to_s[0..2], OPERATION_COLORS[query.operation])} #{query.to_s}")
67
+ end
68
+ end
@@ -0,0 +1,81 @@
1
+ require 'thor'
2
+ require 'dnsimple'
3
+ require 'json'
4
+ require 'dslimple'
5
+
6
+ class Dslimple::CLI < Thor
7
+ include Thor::Actions
8
+
9
+ SANDBOX_API_ENDPOINT = 'https://api.sandbox.dnsimple.com'.freeze
10
+ USER_AGENT = "DSLimple: Simple CLI DNSimple client(v#{Dslimple::VERSION})".freeze
11
+
12
+ class_option :email, type: :string, aliases: %w(-e), desc: 'Your E-Mail address'
13
+ class_option :api_token, type: :string, aliases: %w(-t), desc: 'Your API token'
14
+ class_option :domain_token, type: :string, aliases: %w(-dt), desc: 'Your Domain API token'
15
+ class_option :sandbox, type: :boolean, default: ENV['DLSIMPLE_ENV'] == 'test', desc: 'Use sandbox API(at sandbox.dnsimple.com)'
16
+ class_option :debug, type: :boolean, default: false
17
+
18
+ desc 'export', 'Export domain specifications'
19
+ method_option :only, type: :array, default: [], aliases: %w(-o), desc: 'Specify domains for export'
20
+ method_option :file, type: :string, default: 'Domainfile', aliases: %w(-f), desc: 'Export Domainfile path'
21
+ method_option :dir, type: :string, default: './domainfiles', aliases: %w(-d), desc: 'Export directory path for split'
22
+ method_option :split, type: :boolean, default: false, aliases: %w(-s), desc: 'Export with split by domains'
23
+ method_option :modeline, type: :boolean, default: false, aliases: %w(-m), desc: 'Export with modeline for Vim'
24
+ method_option :soa_and_ns, type: :boolean, default: false, desc: 'Export without SOA and NS records'
25
+ def export
26
+ exporter = Dslimple::Exporter.new(api_client, options)
27
+
28
+ exporter.execute
29
+ rescue => e
30
+ rescue_from(e)
31
+ end
32
+
33
+ desc 'apply', 'Apply domain specifications'
34
+ method_option :only, type: :array, default: [], aliases: %w(-o), desc: 'Specify domains for apply'
35
+ method_option :dry_run, type: :boolean, default: false, aliases: %w(-d)
36
+ method_option :file, type: :string, default: 'Domainfile', aliases: %w(-f), desc: 'Source Domainfile path'
37
+ method_option :addition, type: :boolean, default: true, desc: 'Add specified records'
38
+ method_option :modification, type: :boolean, default: true, desc: 'Modify specified records'
39
+ method_option :deletion, type: :boolean, default: true, desc: 'Delete unspecified records'
40
+ method_option :yes, type: :boolean, default: false, aliases: %w(-y), desc: 'Do not confirm on before apply'
41
+ def apply
42
+ applier = Dslimple::Applier.new(api_client, self, options)
43
+
44
+ applier.execute
45
+ rescue => e
46
+ rescue_from(e)
47
+ rescue Dslimple::DSL::Error => e
48
+ rescue_from(e)
49
+ end
50
+
51
+ private
52
+
53
+ def api_client
54
+ @api_client ||= Dnsimple::Client.new(
55
+ username: options[:email] || ENV['DLSIMPLE_EMAIL'],
56
+ api_token: options[:api_token] || ENV['DLSIMPLE_API_TOKEN'],
57
+ domain_api_token: options[:domain_token] || ENV['DLSIMPLE_DOMAIN_TOKEN'],
58
+ api_endpoint: options[:sandbox] ? SANDBOX_API_ENDPOINT : nil,
59
+ user_agent: USER_AGENT
60
+ )
61
+ end
62
+
63
+ def rescue_from(e)
64
+ raise e if options[:debug]
65
+
66
+ case e
67
+ when Dnsimple::AuthenticationError
68
+ error(set_color(e.message, :red, :bold))
69
+ when Dnsimple::RequestError
70
+ error(set_color("#{e.message}: #{JSON.parse(e.response.body)['message']}", :yellow, :bold))
71
+ when Dslimple::DSL::Error
72
+ error(set_color(e.message, :yellow, :bold))
73
+ else
74
+ error(set_color("#{e.class}: #{e.message}", :red, :bold))
75
+ end
76
+ e.backtrace.each do |bt|
77
+ say(" #{set_color('from', :green)} #{bt}")
78
+ end
79
+ exit 1
80
+ end
81
+ end
@@ -0,0 +1,48 @@
1
+ require 'dslimple'
2
+
3
+ class Dslimple::Domain
4
+ attr_reader :name, :id
5
+ attr_accessor :api_client, :records
6
+
7
+ def initialize(name, api_client, options = {})
8
+ @name = name
9
+ @id = options[:id]
10
+ @api_client = api_client
11
+ @records = []
12
+ end
13
+
14
+ def escaped_name
15
+ Dslimple.escape_single_quote(name)
16
+ end
17
+
18
+ def records_without_soa_ns
19
+ records.select { |record| record.type != :soa && record.type != :ns }
20
+ end
21
+
22
+ def fetch_records
23
+ api_client.domains.records(name).map do |record|
24
+ Dslimple::Record.new(self, record.record_type, record.name, record.content, ttl: record.ttl, priority: record.priority, id: record.id)
25
+ end
26
+ end
27
+
28
+ def fetch_records!
29
+ @records = fetch_records
30
+ cleanup_records!
31
+ end
32
+
33
+ def cleanup_records!
34
+ @records = Dslimple::Record.cleanup_records(records)
35
+ end
36
+
37
+ def ==(other)
38
+ other.is_a?(Dslimple::Domain) && other.name == name
39
+ end
40
+ alias_method :eql, :==
41
+
42
+ def to_dsl(options = {})
43
+ <<"EOD"
44
+ domain '#{escaped_name}' do
45
+ #{(options[:soa_and_ns] ? records : records_without_soa_ns).map(&:to_dsl).join("\n")}end
46
+ EOD
47
+ end
48
+ end
@@ -0,0 +1,29 @@
1
+ require 'dslimple/dsl'
2
+
3
+ class Dslimple::DSL::Domain
4
+ attr_reader :name, :records
5
+
6
+ def initialize(name, &block)
7
+ @name = name
8
+ @records = []
9
+
10
+ instance_eval(&block)
11
+ end
12
+
13
+ def record(name = {}, options = {}, &block)
14
+ if name.is_a?(Hash)
15
+ options = options.merge(name)
16
+ name = ''
17
+ end
18
+
19
+ @records << Dslimple::DSL::Record.new(name, options, &block)
20
+ end
21
+
22
+ Dslimple::Record::RECORD_TYPES.each do |type|
23
+ class_eval(<<-EOC)
24
+ def #{type}_record(name = {}, options = {}, &block)
25
+ record(name, options.merge(type: :#{type}), &block)
26
+ end
27
+ EOC
28
+ end
29
+ end
@@ -0,0 +1,4 @@
1
+ require 'dslimple/dsl'
2
+
3
+ class Dslimple::DSL::Error < Exception
4
+ end
@@ -0,0 +1,25 @@
1
+ require 'dslimple/dsl'
2
+
3
+ class Dslimple::DSL::Record
4
+ attr_reader :name, :content, :options
5
+
6
+ def initialize(name, options = {}, &block)
7
+ @name = name
8
+ @options = options
9
+
10
+ returned_content = instance_eval(&block)
11
+ @content ||= returned_content
12
+ end
13
+
14
+ def priority(n)
15
+ @options[:priority] = n.to_s.to_i
16
+ end
17
+
18
+ def ttl(n)
19
+ @options[:ttl] = n.to_s.to_i
20
+ end
21
+
22
+ def content(c = nil)
23
+ c ? @content = c : @content
24
+ end
25
+ end
@@ -0,0 +1,65 @@
1
+ require 'pathname'
2
+ require 'dslimple'
3
+
4
+ class Dslimple::DSL
5
+ def initialize(file, context = {})
6
+ @file = Pathname.new(file)
7
+ @dir = @file.dirname
8
+ @domains = []
9
+ @files = []
10
+
11
+ @context = context
12
+ end
13
+
14
+ def execute
15
+ evaluate(@file)
16
+ end
17
+
18
+ def require(path)
19
+ if @dir.join(path).exist?
20
+ evaluate(@dir.join(path))
21
+ elsif @dir.join("#{path}.rb").exist?
22
+ evaluate(@dir.join("#{path}.rb"))
23
+ else
24
+ Kernel.require(path)
25
+ end
26
+ end
27
+
28
+ def evaluate(file)
29
+ @files << file.to_s
30
+ instance_eval(File.read(file), file.to_s)
31
+ rescue ScriptError => e
32
+ raise Dslimple::DSL::Error, "#{e.class}: #{e.message}", cleanup_backtrace(e.backtrace)
33
+ rescue StandardError => e
34
+ raise Dslimple::DSL::Error, "#{e.class}: #{e.message}", cleanup_backtrace(e.backtrace)
35
+ end
36
+
37
+ def domain(name, &block)
38
+ @domains << Dslimple::DSL::Domain.new(name, &block)
39
+ end
40
+
41
+ def transform
42
+ @domains.map do |domain|
43
+ Dslimple::Domain.new(domain.name, nil).tap do |model|
44
+ model.records = domain.records.map do |record|
45
+ Dslimple::Record.new(model, record.options[:type], record.name, record.content, record.options)
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def cleanup_backtrace(backtrace)
54
+ return backtrace if @context[:debug]
55
+
56
+ backtrace.select do |bt|
57
+ path = bt.split(':')[0..-3].join(':')
58
+ @files.include?(path)
59
+ end
60
+ end
61
+ end
62
+
63
+ require 'dslimple/dsl/domain'
64
+ require 'dslimple/dsl/record'
65
+ require 'dslimple/dsl/error'
@@ -0,0 +1,55 @@
1
+ require 'pathname'
2
+ require 'dslimple'
3
+
4
+ class Dslimple::Exporter
5
+ attr_reader :api_client, :options, :domains
6
+
7
+ def initialize(api_client, options)
8
+ @api_client = api_client
9
+ @options = options
10
+ @domains = []
11
+ end
12
+
13
+ def execute
14
+ @domains = fetch_domains
15
+
16
+ if options[:split] && options[:dir]
17
+ split_export(options[:dir], options[:file])
18
+ else
19
+ export(options[:file], domains)
20
+ end
21
+ end
22
+
23
+ def fetch_domains
24
+ domains = api_client.domains.list.map { |domain| Dslimple::Domain.new(domain.name, api_client) }
25
+ domains.each(&:fetch_records!)
26
+ domains.select! { |domain| options[:only].include?(domain.name) } unless options[:only].empty?
27
+ domains
28
+ end
29
+
30
+ def export(file, export_domains)
31
+ File.open(file.to_s, 'w') do |fd|
32
+ write_modeline(fd)
33
+ fd.puts export_domains.map { |domain| domain.to_dsl(options) }.join("\n")
34
+ end
35
+ end
36
+
37
+ def split_export(dir, file)
38
+ dir = Pathname.new(dir)
39
+ file = Pathname.new(file)
40
+ Dir.mkdir(dir.to_s) unless dir.directory?
41
+
42
+ File.open(file.to_s, 'w') do |fd|
43
+ write_modeline(fd)
44
+ domains.each do |domain|
45
+ domainfile = dir.join(domain.name)
46
+ export(domainfile, [domain])
47
+ fd.puts "require '#{domainfile.relative_path_from(file.dirname)}'"
48
+ end
49
+ end
50
+ end
51
+
52
+ def write_modeline(fd)
53
+ fd << "# -*- mode: ruby -*-\n# vi: set ft=ruby :\n\n" if options[:modeline]
54
+ end
55
+ end
@@ -0,0 +1,60 @@
1
+ require 'dslimple'
2
+
3
+ class Dslimple::Query
4
+ attr_reader :operation, :target, :domain, :params
5
+
6
+ def initialize(operation, target, domain, params = {})
7
+ @operation = operation
8
+ @target = target
9
+ @domain = domain
10
+ @params = params
11
+ end
12
+
13
+ %i(addition modification deletion).each do |operation|
14
+ class_eval(<<-EOC)
15
+ def #{operation}?
16
+ operation == :#{operation}
17
+ end
18
+ EOC
19
+ end
20
+
21
+ def to_s
22
+ if target == :domain
23
+ "#{domain.name}"
24
+ else
25
+ %(#{params[:record_type].to_s.rjust(5)} #{params[:name].to_s.rjust(10)}.#{domain.name} (#{record_options.join(', ')}) "#{params[:content]}")
26
+ end
27
+ end
28
+
29
+ def record_options
30
+ options = []
31
+ params.each_pair do |k, v|
32
+ options << "#{k}: #{v}" if %i(ttl prio).include?(k) && v
33
+ end
34
+ options
35
+ end
36
+
37
+ def execute(api_client)
38
+ __send__("execute_#{target}", api_client)
39
+ end
40
+
41
+ def execute_domain(api_client)
42
+ case operation
43
+ when :addition
44
+ api_client.domains.create(name: domain.name)
45
+ when :deletion
46
+ api_client.domains.delete(domain.name)
47
+ end
48
+ end
49
+
50
+ def execute_record(api_client)
51
+ case operation
52
+ when :addition
53
+ api_client.domains.create_record(domain.name, params)
54
+ when :modification
55
+ api_client.domains.update_record(domain.name, params[:id], params)
56
+ when :deletion
57
+ api_client.domains.delete_record(domain.name, params[:id])
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,86 @@
1
+ require 'dslimple'
2
+
3
+ class Dslimple::QueryBuilder
4
+ attr_reader :queries
5
+ attr_reader :expected_domains, :current_domains
6
+ attr_reader :append_records, :change_records, :delete_records
7
+
8
+ def initialize(current_domains, expected_domains)
9
+ @current_domains = Hash[*current_domains.map { |domain| [domain.name, domain] }.flatten]
10
+ @expected_domains = Hash[*expected_domains.map { |domain| [domain.name, domain] }.flatten]
11
+ end
12
+
13
+ def append_domains
14
+ @append_domains ||= expected_domains.values.reject { |domain| current_domains.key?(domain.name) }
15
+ end
16
+
17
+ def delete_domains
18
+ @delete_domains ||= current_domains.values.reject { |domain| expected_domains.key?(domain.name) }
19
+ end
20
+
21
+ def execute
22
+ @append_records = append_domains.map(&:records_without_soa_ns).flatten
23
+ @change_records = []
24
+ @delete_records = delete_domains.map(&:records_without_soa_ns).flatten
25
+
26
+ expected_domains.each_pair do |name, domain|
27
+ execute_records(name, domain)
28
+ end
29
+
30
+ build_queries
31
+ end
32
+
33
+ def execute_records(domain_name, domain)
34
+ current_domain = current_domains[domain_name]
35
+ return unless current_domain
36
+
37
+ current_records = current_domain.records_without_soa_ns.dup
38
+ domain.records_without_soa_ns.each do |record|
39
+ at = current_records.index { |current| current == record }
40
+ current_record = at ? current_records.slice!(at) : nil
41
+ like_record = current_records.find { |current| current === record }
42
+
43
+ if !like_record && !current_record
44
+ @append_records << record
45
+ elsif like_record
46
+ @change_records << [like_record, record]
47
+ current_records.delete(like_record)
48
+ end
49
+ end
50
+ @delete_records.concat(current_records)
51
+ end
52
+
53
+ def build_queries
54
+ @queries = []
55
+
56
+ append_domains.each do |domain|
57
+ @queries << Dslimple::Query.new(:addition, :domain, domain)
58
+ end
59
+
60
+ append_records.each do |record|
61
+ @queries << Dslimple::Query.new(:addition, :record, record.domain, record.to_params)
62
+ end
63
+
64
+ change_records.each do |old, new|
65
+ @queries << Dslimple::Query.new(:modification, :record, new.domain, new.to_params.merge(id: old.id))
66
+ end
67
+
68
+ delete_records.each do |record|
69
+ @queries << Dslimple::Query.new(:deletion, :record, record.domain, record.to_params)
70
+ end
71
+
72
+ delete_domains.each do |domain|
73
+ @queries << Dslimple::Query.new(:deletion, :domain, domain)
74
+ end
75
+
76
+ @queries
77
+ end
78
+
79
+ def filtered_queries(options)
80
+ queries.select do |query|
81
+ (query.addition? && options[:addition]) ||
82
+ (query.modification? && options[:modification]) ||
83
+ (query.deletion? && options[:deletion])
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,114 @@
1
+ require 'dslimple'
2
+
3
+ class Dslimple::Record
4
+ RECORD_TYPES = %i(a alias cname mx spf url txt ns srv naptr ptr aaaa sshfp hinfo pool).freeze
5
+ ALIAS_PREFIX = /\AALIAS for /
6
+ SPF_PREFIX = /\Av=spf1 /
7
+
8
+ attr_reader :domain, :type, :name, :id, :ttl, :priority, :content
9
+
10
+ def self.cleanup_records(records)
11
+ records = records.dup
12
+ alias_records = records.select(&:alias?)
13
+ spf_records = records.select(&:spf?)
14
+ txt_records = records.select { |record| record.like_spf? || record.like_alias? }
15
+
16
+ txt_records.each do |record|
17
+ reject = record.like_spf? ? spf_records.any? { |r| record.eql_spf?(r) } : alias_records.any? { |r| record.eql_alias?(r) }
18
+
19
+ records.delete(record) if reject
20
+ end
21
+
22
+ records
23
+ end
24
+
25
+ RECORD_TYPES.each do |type|
26
+ class_eval(<<-EOC)
27
+ def #{type}?
28
+ type == :#{type}
29
+ end
30
+ EOC
31
+ end
32
+
33
+ def initialize(domain, type, name, content, options = {})
34
+ @domain = domain
35
+ @type = type.to_s.downcase.to_sym
36
+ @name = name
37
+ @content = content
38
+ @ttl = options[:ttl]
39
+ @priority = options[:priority]
40
+ @id = options[:id]
41
+ end
42
+
43
+ def escaped_name
44
+ Dslimple.escape_single_quote(name)
45
+ end
46
+
47
+ def escaped_content
48
+ Dslimple.escape_single_quote(content)
49
+ end
50
+
51
+ def like_spf?
52
+ txt? && content.match(SPF_PREFIX)
53
+ end
54
+
55
+ def like_alias?
56
+ txt? && content.match(ALIAS_PREFIX)
57
+ end
58
+
59
+ def eql_spf?(spf_record)
60
+ spf_record.ttl == ttl && spf_record.content == content
61
+ end
62
+
63
+ def eql_alias?(alias_record)
64
+ alias_record.ttl == ttl && content == "ALIAS for #{alias_record.content}"
65
+ end
66
+
67
+ def ==(other)
68
+ other.is_a?(Dslimple::Record) && other.domain == domain && other.hash == hash
69
+ end
70
+ alias_method :eql, :==
71
+
72
+ def ===(other)
73
+ other.is_a?(Dslimple::Record) && other.domain == domain && other.rough_hash == rough_hash
74
+ end
75
+
76
+ def hash
77
+ "#{type}:#{name}:#{content}:#{ttl}:#{priority}"
78
+ end
79
+
80
+ def rough_hash
81
+ "#{type}:#{name}:#{content}"
82
+ end
83
+
84
+ def to_dsl_options
85
+ options = []
86
+ options << "'#{escaped_name}'" unless escaped_name.empty?
87
+ options << "ttl: #{ttl}" if ttl
88
+ options << "priority: #{priority}" if priority
89
+ options.join(', ')
90
+ end
91
+
92
+ def to_dsl
93
+ <<"EOD"
94
+ #{type}_record #{to_dsl_options} do
95
+ '#{escaped_content}'
96
+ end
97
+ EOD
98
+ end
99
+
100
+ def to_params
101
+ {
102
+ id: id,
103
+ record_type: type.to_s.upcase,
104
+ name: name,
105
+ content: content,
106
+ ttl: ttl,
107
+ prio: priority
108
+ }
109
+ end
110
+
111
+ def inspect
112
+ "<Dslimple::Record #{type.to_s.upcase} #{name}: #{content}>"
113
+ end
114
+ end
@@ -0,0 +1,3 @@
1
+ module Dslimple
2
+ VERSION = '1.0.0'.freeze
3
+ end
data/lib/dslimple.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'dslimple/version'
2
+ require 'dslimple/domain'
3
+ require 'dslimple/record'
4
+ require 'dslimple/dsl'
5
+ require 'dslimple/query'
6
+ require 'dslimple/query_builder'
7
+ require 'dslimple/exporter'
8
+ require 'dslimple/applier'
9
+
10
+ module Dslimple
11
+ ESCAPE_SINGLE_QUOTE_REGEXP = /[^\\]'/
12
+
13
+ def self.escape_single_quote(string)
14
+ string.gsub(ESCAPE_SINGLE_QUOTE_REGEXP) do |match|
15
+ match[0] + "\\'"
16
+ end
17
+ end
18
+ end
metadata ADDED
@@ -0,0 +1,139 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dslimple
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Sho Kusano
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-02-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.11'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.11'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: dnsimple
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.1'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: thor
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.19'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.19'
83
+ description: DSLimple is a tool to manage DNSimple. It defines the state of DNSimple
84
+ using DSL, and updates DNSimple according to DSL.
85
+ email:
86
+ - sho-kusano@zeny.io
87
+ executables:
88
+ - dslimple
89
+ extensions: []
90
+ extra_rdoc_files: []
91
+ files:
92
+ - ".gitignore"
93
+ - ".rubocop.yml"
94
+ - Gemfile
95
+ - LICENSE.txt
96
+ - README.md
97
+ - Rakefile
98
+ - bin/console
99
+ - bin/setup
100
+ - dslimple.gemspec
101
+ - exe/dslimple
102
+ - lib/dslimple.rb
103
+ - lib/dslimple/applier.rb
104
+ - lib/dslimple/cli.rb
105
+ - lib/dslimple/domain.rb
106
+ - lib/dslimple/dsl.rb
107
+ - lib/dslimple/dsl/domain.rb
108
+ - lib/dslimple/dsl/error.rb
109
+ - lib/dslimple/dsl/record.rb
110
+ - lib/dslimple/exporter.rb
111
+ - lib/dslimple/query.rb
112
+ - lib/dslimple/query_builder.rb
113
+ - lib/dslimple/record.rb
114
+ - lib/dslimple/version.rb
115
+ homepage: https://github.com/zeny-io/dslimple
116
+ licenses:
117
+ - MIT
118
+ metadata: {}
119
+ post_install_message:
120
+ rdoc_options: []
121
+ require_paths:
122
+ - lib
123
+ required_ruby_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ requirements: []
134
+ rubyforge_project:
135
+ rubygems_version: 2.5.1
136
+ signing_key:
137
+ specification_version: 4
138
+ summary: DSLimple is a tool to manage DNSimple.
139
+ test_files: []