record_store 0.3.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|