record_store 0.3.0 → 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 +4 -4
- data/.gitignore +3 -9
- data/Gemfile +19 -2
- data/Gemfile.lock +178 -0
- data/{LICENSE.txt → LICENSE} +5 -5
- data/README.md +131 -44
- data/Rakefile +18 -1
- data/bin/record-store +7 -0
- data/bin/setup +8 -2
- data/bin/test +5 -0
- data/circle.yml +8 -0
- data/lib/record_store.rb +48 -104
- data/lib/record_store/changeset.rb +85 -0
- data/lib/record_store/cli.rb +255 -0
- data/lib/record_store/provider.rb +102 -0
- data/lib/record_store/provider/dnsimple.rb +158 -0
- data/lib/record_store/provider/dynect.rb +97 -0
- data/lib/record_store/record.rb +70 -0
- data/lib/record_store/record/a.rb +32 -0
- data/lib/record_store/record/aaaa.rb +32 -0
- data/lib/record_store/record/alias.rb +20 -0
- data/lib/record_store/record/cname.rb +20 -0
- data/lib/record_store/record/mx.rb +25 -0
- data/lib/record_store/record/ns.rb +20 -0
- data/lib/record_store/record/spf.rb +20 -0
- data/lib/record_store/record/srv.rb +27 -0
- data/lib/record_store/record/txt.rb +29 -0
- data/lib/record_store/version.rb +3 -0
- data/lib/record_store/zone.rb +193 -0
- data/lib/record_store/zone/config.rb +24 -0
- data/record_store.gemspec +30 -14
- data/template/Gemfile +3 -0
- data/template/bin/record-store +7 -0
- data/template/bin/setup +3 -0
- data/template/bin/test +5 -0
- data/template/config.yml +5 -0
- data/template/secrets.json +11 -0
- data/template/zones/dnsimple.example.com.yml +37 -0
- data/template/zones/dynect.example.com.yml +37 -0
- metadata +208 -22
- data/.travis.yml +0 -3
- data/bin/console +0 -14
data/Rakefile
CHANGED
@@ -1 +1,18 @@
|
|
1
|
-
require
|
1
|
+
require 'rake/testtask'
|
2
|
+
require 'bundler/gem_tasks'
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift(__dir__ + "/lib")
|
5
|
+
require 'record_store'
|
6
|
+
|
7
|
+
Rake::TestTask.new do |t|
|
8
|
+
t.libs << "test"
|
9
|
+
t.test_files = FileList['test/**/*_test.rb']
|
10
|
+
end
|
11
|
+
|
12
|
+
task :validate do
|
13
|
+
record_store = RecordStore::CLI.new
|
14
|
+
record_store.validate_records
|
15
|
+
record_store.validate_all_present
|
16
|
+
end
|
17
|
+
|
18
|
+
task default: [:test]
|
data/bin/record-store
ADDED
data/bin/setup
CHANGED
@@ -2,6 +2,12 @@
|
|
2
2
|
set -euo pipefail
|
3
3
|
IFS=$'\n\t'
|
4
4
|
|
5
|
-
|
5
|
+
cd "$(dirname "$0")/.."
|
6
|
+
ROOT_DIR=$(pwd)
|
6
7
|
|
7
|
-
|
8
|
+
bundle check || bundle install
|
9
|
+
|
10
|
+
if [ ! -d $ROOT_DIR/dev ]; then
|
11
|
+
echo 'Created development directory dev/'
|
12
|
+
cp -R template dev
|
13
|
+
fi
|
data/bin/test
ADDED
data/circle.yml
ADDED
data/lib/record_store.rb
CHANGED
@@ -1,122 +1,66 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
require 'json'
|
2
|
+
require 'yaml'
|
3
|
+
require 'active_support'
|
4
|
+
require 'active_support/core_ext/hash/keys'
|
5
|
+
require 'active_support/core_ext/string'
|
6
|
+
require 'active_model'
|
7
|
+
require 'active_model/validations'
|
8
|
+
require 'ipaddr'
|
9
|
+
require 'thor'
|
10
|
+
require 'pathname'
|
11
|
+
|
12
|
+
require 'record_store/record'
|
13
|
+
require 'record_store/record/a'
|
14
|
+
require 'record_store/record/aaaa'
|
15
|
+
require 'record_store/record/alias'
|
16
|
+
require 'record_store/record/cname'
|
17
|
+
require 'record_store/record/mx'
|
18
|
+
require 'record_store/record/ns'
|
19
|
+
require 'record_store/record/txt'
|
20
|
+
require 'record_store/record/spf'
|
21
|
+
require 'record_store/record/srv'
|
22
|
+
require 'record_store/zone'
|
23
|
+
require 'record_store/zone/config'
|
24
|
+
require 'record_store/changeset'
|
25
|
+
require 'record_store/provider'
|
26
|
+
require 'record_store/provider/dynect'
|
27
|
+
require 'record_store/provider/dnsimple'
|
28
|
+
require 'record_store/cli'
|
29
|
+
|
30
|
+
module RecordStore
|
31
|
+
MAXIMUM_REMOVALS = 20
|
3
32
|
|
4
33
|
class << self
|
5
|
-
|
6
|
-
|
7
|
-
end
|
8
|
-
|
9
|
-
def dataset_name
|
10
|
-
@dataset_name ||= name.sub(/Store$/,'').tableize.to_sym
|
11
|
-
end
|
12
|
-
|
13
|
-
def dataset
|
14
|
-
database[dataset_name]
|
15
|
-
end
|
34
|
+
attr_writer :secrets_path
|
35
|
+
attr_writer :zones_path
|
16
36
|
|
17
|
-
def
|
18
|
-
|
37
|
+
def secrets_path
|
38
|
+
@secrets_path ||= File.expand_path(config.fetch('secrets_path'), File.dirname(config_path))
|
19
39
|
end
|
20
|
-
alias_method :[], :get
|
21
40
|
|
22
|
-
def
|
23
|
-
new(
|
41
|
+
def zones_path
|
42
|
+
@zones_path ||= Pathname.new(File.expand_path(config.fetch('zones_path'), File.dirname(config_path))).realpath.to_s
|
24
43
|
end
|
25
|
-
alias_method :<<, :put
|
26
44
|
|
27
|
-
def
|
28
|
-
@
|
29
|
-
map[metadata[:db_type]] ||= []
|
30
|
-
map[metadata[:db_type]] << column
|
31
|
-
end
|
45
|
+
def config_path
|
46
|
+
@config_path ||= File.expand_path('config.yml', Dir.pwd)
|
32
47
|
end
|
33
48
|
|
34
|
-
def
|
35
|
-
@
|
49
|
+
def config_path=(config_path)
|
50
|
+
@config = @zones_path = @secrets_path = nil
|
51
|
+
@config_path = config_path
|
36
52
|
end
|
37
53
|
|
38
|
-
def
|
39
|
-
|
40
|
-
validations << proc do
|
41
|
-
errors << "#{attribute.to_s.titleize} is required" unless @record[attribute].present?
|
42
|
-
end
|
43
|
-
end
|
54
|
+
def config
|
55
|
+
@config ||= YAML.load_file(config_path)
|
44
56
|
end
|
45
57
|
|
46
|
-
def
|
47
|
-
|
48
|
-
if @record[attribute].blank?
|
49
|
-
errors << "#{attribute} is required"
|
50
|
-
elsif database[table].where(id: @record[attribute]).get(:id).blank?
|
51
|
-
errors << "no #{table.to_s.singularize} with id=#{@record[attribute]}"
|
52
|
-
end
|
53
|
-
end
|
58
|
+
def defined_zones
|
59
|
+
@defined_zones ||= Zone.all.map(&:name)
|
54
60
|
end
|
55
|
-
end
|
56
|
-
|
57
|
-
def initialize(record)
|
58
|
-
@record = record
|
59
|
-
end
|
60
61
|
|
61
|
-
|
62
|
-
|
63
|
-
transform
|
64
|
-
return { errors: 'no record provided' } if @record.blank?
|
65
|
-
validate
|
66
|
-
return { errors: errors } if errors.present?
|
67
|
-
|
68
|
-
save
|
69
|
-
end
|
70
|
-
|
71
|
-
def save
|
72
|
-
if exists?
|
73
|
-
update
|
74
|
-
else
|
75
|
-
insert
|
62
|
+
def expected_zones
|
63
|
+
config.fetch('zones')
|
76
64
|
end
|
77
|
-
@record
|
78
|
-
end
|
79
|
-
|
80
|
-
def database
|
81
|
-
self.class.database
|
82
|
-
end
|
83
|
-
|
84
|
-
def dataset
|
85
|
-
self.class.dataset
|
86
|
-
end
|
87
|
-
|
88
|
-
def errors
|
89
|
-
@errors ||= []
|
90
|
-
end
|
91
|
-
|
92
|
-
def typecast
|
93
|
-
if database.database_type == :postgres
|
94
|
-
Array(self.class.type_column_map(database)['json']).each do |json_column|
|
95
|
-
if @record[json_column]
|
96
|
-
@record[json_column] = Sequel.pg_json(@record[json_column])
|
97
|
-
end
|
98
|
-
end
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
|
-
def transform
|
103
|
-
end
|
104
|
-
|
105
|
-
def validate
|
106
|
-
self.class.validations.each do |validation|
|
107
|
-
instance_eval(&validation)
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
def exists?
|
112
|
-
!dataset.select(1).where(id: @record[:id]).empty?
|
113
|
-
end
|
114
|
-
|
115
|
-
def insert
|
116
|
-
@record[:id] = dataset.insert(@record.except(:errors))
|
117
|
-
end
|
118
|
-
|
119
|
-
def update
|
120
|
-
dataset.where(id: @record[:id]).update(@record.except(:id, :errors))
|
121
65
|
end
|
122
66
|
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module RecordStore
|
2
|
+
class Changeset
|
3
|
+
class Change
|
4
|
+
attr_accessor :type, :record, :id
|
5
|
+
|
6
|
+
def initialize(type: nil, record: nil, id: nil)
|
7
|
+
@type, @record, @id = type, record, id
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.addition(record)
|
11
|
+
new(type: :addition, record: record)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.removal(record)
|
15
|
+
new(type: :removal, record: record)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.update(id, record)
|
19
|
+
raise ArgumentError.new('id cannot be nil') if id.nil?
|
20
|
+
new(type: :update, record: record, id: id)
|
21
|
+
end
|
22
|
+
|
23
|
+
def removal?
|
24
|
+
type == :removal
|
25
|
+
end
|
26
|
+
|
27
|
+
def addition?
|
28
|
+
type == :addition
|
29
|
+
end
|
30
|
+
|
31
|
+
def update?
|
32
|
+
type == :update
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
attr_reader :current_records, :desired_records, :removals, :additions, :updates
|
37
|
+
|
38
|
+
def initialize(current_records: [], desired_records: [])
|
39
|
+
@current_records, @desired_records = Set.new(current_records), Set.new(desired_records)
|
40
|
+
@additions, @removals, @updates = [], [], []
|
41
|
+
|
42
|
+
build_changeset
|
43
|
+
end
|
44
|
+
|
45
|
+
def unchanged
|
46
|
+
current_records & desired_records
|
47
|
+
end
|
48
|
+
|
49
|
+
def changes
|
50
|
+
updates.to_a + removals.to_a + additions.to_a
|
51
|
+
end
|
52
|
+
|
53
|
+
def empty?
|
54
|
+
changes.empty?
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def build_changeset
|
60
|
+
current_records_set = (current_records - unchanged).group_by(&:key)
|
61
|
+
desired_records_set = (desired_records - unchanged).group_by(&:key)
|
62
|
+
current_records_set.default_proc = desired_records_set.default_proc = Proc.new{Array.new}
|
63
|
+
|
64
|
+
record_keys = current_records_set.keys | desired_records_set.keys
|
65
|
+
|
66
|
+
diff = record_keys.flat_map do |key|
|
67
|
+
# the array being zipped over must be bigger then or equal to the other array for zip to work
|
68
|
+
buffer = [0, desired_records_set[key].size - current_records_set[key].size].max
|
69
|
+
temp_diff = current_records_set[key] + [nil] * buffer
|
70
|
+
|
71
|
+
temp_diff.zip(desired_records_set[key])
|
72
|
+
end
|
73
|
+
|
74
|
+
diff.each do |before_rr, after_rr|
|
75
|
+
if before_rr.nil?
|
76
|
+
@additions << Change.addition(after_rr)
|
77
|
+
elsif after_rr.nil?
|
78
|
+
@removals << Change.removal(before_rr)
|
79
|
+
else
|
80
|
+
@updates << Change.update(before_rr.id, after_rr)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,255 @@
|
|
1
|
+
module RecordStore
|
2
|
+
class CLI < Thor
|
3
|
+
class_option :config, desc: 'Path to config.yml', aliases: '-c'
|
4
|
+
|
5
|
+
def initialize(*args)
|
6
|
+
super
|
7
|
+
RecordStore.config_path = options.fetch('config', "#{Dir.pwd}/config.yml")
|
8
|
+
end
|
9
|
+
|
10
|
+
desc 'thaw', 'Thaws all zones under management to allow manual edits'
|
11
|
+
def thaw
|
12
|
+
Zone.each do |_, zone|
|
13
|
+
zone.provider.thaw if zone.provider.thawable?
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
desc 'freeze', 'Freezes all zones under management to prevent manual edits'
|
18
|
+
def freeze
|
19
|
+
Zone.each do |_, zone|
|
20
|
+
zone.provider.freeze_zone if zone.provider.freezable?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
desc 'list', 'Lists out records in YAML zonefiles'
|
25
|
+
def list
|
26
|
+
Zone.each do |name, zone|
|
27
|
+
puts "Zone: #{name}"
|
28
|
+
zone.records.each do |record|
|
29
|
+
record.log!
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
option :verbose, desc: 'Print records that haven\'t diverged', aliases: '-v', type: :boolean, default: false
|
35
|
+
desc 'diff', 'Displays the DNS differences between the zone files in this repo and production'
|
36
|
+
def diff
|
37
|
+
Zone.each do |name, zone|
|
38
|
+
puts "Zone: #{name}"
|
39
|
+
|
40
|
+
diff = zone.changeset
|
41
|
+
|
42
|
+
if !diff.additions.empty? || options.fetch('verbose')
|
43
|
+
puts "Add:"
|
44
|
+
diff.additions.map(&:record).each do |record|
|
45
|
+
puts " - #{record.to_s}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
if !diff.removals.empty? || options.fetch('verbose')
|
50
|
+
puts "Remove:"
|
51
|
+
diff.removals.map(&:record).each do |record|
|
52
|
+
puts " - #{record.to_s}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
if !diff.updates.empty? || options.fetch('verbose')
|
57
|
+
puts "Update:"
|
58
|
+
diff.updates.map(&:record).each do |record|
|
59
|
+
puts " - #{record.to_s}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
if options.fetch('verbose')
|
64
|
+
puts "Unchanged:"
|
65
|
+
diff.unchanged.each do |record|
|
66
|
+
puts " - #{record.to_s}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
puts "Empty diff" if diff.changes.empty?
|
71
|
+
puts '=' * 20
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
desc 'apply', 'Applies the DNS changes'
|
76
|
+
def apply
|
77
|
+
zones = zones_modified
|
78
|
+
|
79
|
+
if zones.empty?
|
80
|
+
puts "No changes to sync"
|
81
|
+
exit
|
82
|
+
end
|
83
|
+
|
84
|
+
zones.each do |zone|
|
85
|
+
abort "Attempted to apply invalid zone: #{zone.name}" unless zone.valid?
|
86
|
+
provider = zone.provider
|
87
|
+
provider.apply_changeset(zone.changeset)
|
88
|
+
end
|
89
|
+
|
90
|
+
puts "All zone changes deployed"
|
91
|
+
end
|
92
|
+
|
93
|
+
option :name, desc: 'Zone to download', aliases: '-n', type: :string, required: true
|
94
|
+
option :provider, desc: 'Provider in which this zone exists', aliases: '-p', type: :string
|
95
|
+
desc 'download', 'Downloads all records from zone and creates YAML zone definition in zones/ e.g. record-store download --name=shopify.io'
|
96
|
+
def download
|
97
|
+
name = options.fetch('name')
|
98
|
+
abort 'Please omit the period at the end of the zone' if name.ends_with?('.')
|
99
|
+
abort 'Zone with this name already exists in zones/' if File.exists?("#{RecordStore.zones_path}/#{name}.yml")
|
100
|
+
|
101
|
+
provider = options.fetch('provider', Provider.provider_for(name))
|
102
|
+
if provider.nil?
|
103
|
+
puts "Could not find valid provider from #{name} SOA record"
|
104
|
+
provider = ask("Please enter the provider in which #{name} exists")
|
105
|
+
else
|
106
|
+
puts "Identified #{provider} as the DNS provider"
|
107
|
+
end
|
108
|
+
|
109
|
+
puts "Downloading records for #{name}"
|
110
|
+
Zone.download(name, provider)
|
111
|
+
puts "Records have been downloaded & can be found in zones/#{name}.yml"
|
112
|
+
end
|
113
|
+
|
114
|
+
option :name, desc: 'Zone to sort', aliases: '-n', type: :string, required: true
|
115
|
+
desc 'sort', 'Sorts the zonefile alphabetically e.g. record-store sort --name=shopify.io'
|
116
|
+
def sort
|
117
|
+
name = options.fetch('name')
|
118
|
+
abort "Please omit the period at the end of the zone" if name.ends_with?('.')
|
119
|
+
|
120
|
+
yaml = YAML.load_file("#{RecordStore.zones_path}/#{name}.yml")
|
121
|
+
yaml.fetch(name).fetch('records').sort_by! { |r| [r.fetch('fqdn'), r.fetch('type'), r['nsdname'] || r['address']] }
|
122
|
+
|
123
|
+
File.write("#{RecordStore.zones_path}/#{name}.yml", yaml.deep_stringify_keys.to_yaml.gsub("---\n", ''))
|
124
|
+
end
|
125
|
+
|
126
|
+
desc 'secrets', 'Decrypts DynECT credentials'
|
127
|
+
def secrets
|
128
|
+
environment = begin
|
129
|
+
if ENV['PRODUCTION']
|
130
|
+
'production'
|
131
|
+
elsif ENV['CI']
|
132
|
+
'ci'
|
133
|
+
else
|
134
|
+
'dev'
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
secrets = `ejson decrypt #{RecordStore.secrets_path.sub(/\.json\z/, ".#{environment}.ejson")}`
|
139
|
+
if $?.exitstatus == 0
|
140
|
+
File.write(RecordStore.secrets_path, secrets)
|
141
|
+
else
|
142
|
+
abort secrets
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
desc 'assert_empty_diff', 'Asserts there is no divergence between DynECT & the zone files'
|
147
|
+
def assert_empty_diff
|
148
|
+
zones = zones_modified.map(&:name)
|
149
|
+
|
150
|
+
unless zones.empty?
|
151
|
+
abort "The following zones have diverged: #{zones.join(', ')}"
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
desc 'validate_records', 'Validates that all DNS records have valid definitions'
|
156
|
+
def validate_records
|
157
|
+
invalid_zones = []
|
158
|
+
Zone.each do |name, zone|
|
159
|
+
if !zone.valid?
|
160
|
+
invalid_zones << name
|
161
|
+
|
162
|
+
puts "#{name} definition is not valid:"
|
163
|
+
zone.errors.each do |field, msg|
|
164
|
+
puts " - #{field}: #{msg}"
|
165
|
+
end
|
166
|
+
|
167
|
+
invalid_records = zone.records.reject(&:valid?)
|
168
|
+
puts ' Invalid records' if invalid_records.size > 0
|
169
|
+
|
170
|
+
invalid_records.each do |record|
|
171
|
+
puts " #{record.to_s}"
|
172
|
+
record.errors.each do |field, msg|
|
173
|
+
puts " - #{field}: #{msg}"
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
if invalid_zones.size > 0
|
180
|
+
abort "The following zones were invalid: #{invalid_zones.join(', ')}"
|
181
|
+
else
|
182
|
+
puts "All records have valid definitions."
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
desc 'validate_change_size', "Validates no more then particular limit of DNS records are removed per zone at a time"
|
187
|
+
def validate_change_size
|
188
|
+
zones = zones_modified
|
189
|
+
|
190
|
+
unless zones.empty?
|
191
|
+
removals = zones.select do |zone|
|
192
|
+
zone.changeset.removals.size > MAXIMUM_REMOVALS
|
193
|
+
end
|
194
|
+
|
195
|
+
unless removals.empty?
|
196
|
+
abort "As a safety measure, you cannot remove more than #{MAXIMUM_REMOVALS} records at a time per zone. (zones failing this: #{removals.map(&:name).join(', ')})"
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
desc 'validate_all_present', 'Validates that all the zones that are expected are defined'
|
202
|
+
def validate_all_present
|
203
|
+
defined_zones = Set.new(RecordStore.defined_zones)
|
204
|
+
expected_zones = Set.new(RecordStore.expected_zones)
|
205
|
+
|
206
|
+
missing_zones = expected_zones - defined_zones
|
207
|
+
extra_zones = defined_zones - expected_zones
|
208
|
+
|
209
|
+
unless missing_zones.empty?
|
210
|
+
abort "The following zones are missing: #{missing_zones.to_a.join(', ')}"
|
211
|
+
end
|
212
|
+
|
213
|
+
unless extra_zones.empty?
|
214
|
+
abort "The following unexpected zones are defined: #{extra_zones.to_a.join(', ')}"
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
SKIP_CHECKS = 'SKIP_DEPLOY_VALIDATIONS'
|
219
|
+
desc 'validate_initial_state', "Validates state hasn't diverged since the last deploy"
|
220
|
+
def validate_initial_state
|
221
|
+
begin
|
222
|
+
assert_empty_diff
|
223
|
+
puts "Deploy will cause no changes, no need to validate initial state"
|
224
|
+
rescue SystemExit
|
225
|
+
if File.exists?(File.expand_path(SKIP_CHECKS, Dir.pwd))
|
226
|
+
puts "Found '#{SKIP_CHECKS}', skipping predeploy validations"
|
227
|
+
else
|
228
|
+
puts "Checkout git SHA #{ENV['LAST_DEPLOYED_SHA']}"
|
229
|
+
`git checkout #{ENV['LAST_DEPLOYED_SHA']}`
|
230
|
+
abort "Checkout of old commit failed" if $?.exitstatus != 0
|
231
|
+
|
232
|
+
`record-store assert_empty_diff`
|
233
|
+
abort "Dyn status has diverged!" if $?.exitstatus != 0
|
234
|
+
|
235
|
+
puts "Checkout git SHA #{ENV['REVISION']}"
|
236
|
+
`git checkout #{ENV['REVISION']}`
|
237
|
+
abort "Checkout of new commit failed" if $?.exitstatus != 0
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
private
|
243
|
+
|
244
|
+
def zones_modified
|
245
|
+
modified_zones, mutex = [], Mutex.new
|
246
|
+
Zone.all.map do |zone|
|
247
|
+
thread = Thread.new do
|
248
|
+
mutex.synchronize {modified_zones << zone} unless zone.unchanged?
|
249
|
+
end
|
250
|
+
end.each(&:join)
|
251
|
+
|
252
|
+
modified_zones
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|