kennel 1.127.0 → 1.129.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec72f834db1c82d19ba21eabd482e5f46a540b0c9a3e0e51ecc20a80c192a6d7
4
- data.tar.gz: c5b549fb934ce3e274d88149df509b4dcf16e1adc6be3271e94b3101dbee1ad7
3
+ metadata.gz: c7690d961a150ea0ba0649edcab513c22ec828988fec2ab631636497274305c7
4
+ data.tar.gz: c5bc7244c4168466f5167e5c322f3a2cdb8e6b9b8ec23a2d12fb343a7648ae06
5
5
  SHA512:
6
- metadata.gz: e0bff1a22428c19da30025f79e8e7904675ea27d79eb2cfdf1a6cf0638a33efbdee44c10f898480b9e7d91d73296368e3bab58ce495282beb43cb13c975c9153
7
- data.tar.gz: 445cf62d6e525c42808a42ba80e941829cab5c679ef74f71590799ec46e93f3d41ff8162bed775342510c042249a2acfaf10dea06507a2b5f7a7b0c8a450fb3c
6
+ metadata.gz: 51b47377a8cf688dafd17600b543cc1734eb8511a6ed1b627d8b1180bf70fc66c86f9d4a8e4b2a3ff5e7223edf366e5483d983d9465c5907aaa07f5b9c36ef46
7
+ data.tar.gz: 698c23a4de558382467342b90e7d3eac143a0279653efe9f1b711b3e114a37d8d72f4a9c5d97e652a6646007298f5e93f296c1125caaa4c498195089b0cac730
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "diff/lcs"
4
+
5
+ module Kennel
6
+ class AttributeDiffer
7
+ def initialize
8
+ # min '2' because: -1 makes no sense, 0 does not work with * 2 math, 1 says '1 lines'
9
+ @max_diff_lines = [Integer(ENV.fetch("MAX_DIFF_LINES", "50")), 2].max
10
+ super
11
+ end
12
+
13
+ def format(type, field, old, new = nil)
14
+ multiline = false
15
+ if type == "+"
16
+ temp = pretty_inspect(new)
17
+ new = pretty_inspect(old)
18
+ old = temp
19
+ elsif old.is_a?(String) && new.is_a?(String) && (old.include?("\n") || new.include?("\n"))
20
+ multiline = true
21
+ else # ~ and -
22
+ old = pretty_inspect(old)
23
+ new = pretty_inspect(new)
24
+ end
25
+
26
+ message =
27
+ if multiline
28
+ " #{type}#{field}\n" +
29
+ multiline_diff(old, new).map { |l| " #{l}" }.join("\n")
30
+ elsif (old + new).size > 100
31
+ " #{type}#{field}\n" \
32
+ " #{old} ->\n" \
33
+ " #{new}"
34
+ else
35
+ " #{type}#{field} #{old} -> #{new}"
36
+ end
37
+
38
+ truncate(message)
39
+ end
40
+
41
+ private
42
+
43
+ # display diff for multi-line strings
44
+ # must stay readable when color is off too
45
+ def multiline_diff(old, new)
46
+ Diff::LCS.sdiff(old.split("\n", -1), new.split("\n", -1)).flat_map do |diff|
47
+ case diff.action
48
+ when "-"
49
+ Utils.color(:red, "- #{diff.old_element}")
50
+ when "+"
51
+ Utils.color(:green, "+ #{diff.new_element}")
52
+ when "!"
53
+ [
54
+ Utils.color(:red, "- #{diff.old_element}"),
55
+ Utils.color(:green, "+ #{diff.new_element}")
56
+ ]
57
+ else
58
+ " #{diff.old_element}"
59
+ end
60
+ end
61
+ end
62
+
63
+ def truncate(message)
64
+ warning = Utils.color(
65
+ :magenta,
66
+ " (Diff for this item truncated after #{@max_diff_lines} lines. " \
67
+ "Rerun with MAX_DIFF_LINES=#{@max_diff_lines * 2} to see more)"
68
+ )
69
+ Utils.truncate_lines(message, to: @max_diff_lines, warning: warning)
70
+ end
71
+
72
+ # TODO: use awesome-print or similar, but it has too many monkey-patches
73
+ # https://github.com/amazing-print/amazing_print/issues/36
74
+ def pretty_inspect(object)
75
+ string = object.inspect.dup
76
+ string.gsub!(/:([a-z_]+)=>/, "\\1: ")
77
+ 10.times do
78
+ string.gsub!(/{(\S.*?\S)}/, "{ \\1 }") || break
79
+ end
80
+ string
81
+ end
82
+ end
83
+ end
@@ -189,7 +189,7 @@ module Kennel
189
189
 
190
190
  # important to the front and rest deterministic
191
191
  def sort_hash(hash)
192
- Hash[hash.sort_by { |k, _| [SORT_ORDER.index(k) || 999, k] }]
192
+ hash.sort_by { |k, _| [SORT_ORDER.index(k) || 999, k] }.to_h
193
193
  end
194
194
  end
195
195
  end
@@ -35,8 +35,8 @@ module Kennel
35
35
  :klass, :tracking_id # added by syncer.rb
36
36
  ].freeze
37
37
  ALLOWED_KENNEL_ID_CHARS = "a-zA-Z_\\d.-"
38
- ALLOWED_KENNEL_ID_FULL = "[#{ALLOWED_KENNEL_ID_CHARS}]+:[#{ALLOWED_KENNEL_ID_CHARS}]+"
39
- ALLOWED_KENNEL_ID_REGEX = /\A#{ALLOWED_KENNEL_ID_FULL}\z/.freeze
38
+ ALLOWED_KENNEL_ID_FULL = "[#{ALLOWED_KENNEL_ID_CHARS}]+:[#{ALLOWED_KENNEL_ID_CHARS}]+".freeze
39
+ ALLOWED_KENNEL_ID_REGEX = /\A#{ALLOWED_KENNEL_ID_FULL}\z/
40
40
 
41
41
  settings :id, :kennel_id
42
42
 
@@ -52,7 +52,7 @@ module Kennel
52
52
  end
53
53
 
54
54
  def api_resource_map
55
- subclasses.map { |s| [s.api_resource, s] }.to_h
55
+ subclasses.to_h { |s| [s.api_resource, s] }
56
56
  end
57
57
 
58
58
  def parse_tracking_id(a)
@@ -98,6 +98,8 @@ module Kennel
98
98
 
99
99
  self.class.send(:normalize, expected, actual)
100
100
 
101
+ return [] if actual == expected # Hashdiff is slow, this is fast
102
+
101
103
  # strict: ignore Integer vs Float
102
104
  # similarity: show diff when not 100% similar
103
105
  # use_lcs: saner output
@@ -3,8 +3,7 @@ module Kennel
3
3
  module Models
4
4
  class SyntheticTest < Record
5
5
  TRACKING_FIELD = :message
6
- DEFAULTS = {
7
- }.freeze
6
+ DEFAULTS = {}.freeze
8
7
  READONLY_ATTRIBUTES = superclass::READONLY_ATTRIBUTES + [:status, :monitor_id]
9
8
  LOCATIONS = ["aws:ca-central-1", "aws:eu-north-1", "aws:eu-west-1", "aws:eu-west-3", "aws:eu-west-2", "aws:ap-south-1", "aws:us-west-2", "aws:us-west-1", "aws:sa-east-1", "aws:us-east-2", "aws:ap-northeast-1", "aws:ap-northeast-2", "aws:eu-central-1", "aws:ap-southeast-2", "aws:ap-southeast-1"].freeze
10
9
 
@@ -8,13 +8,9 @@ module Kennel
8
8
 
9
9
  def write(parts)
10
10
  Progress.progress "Storing" do
11
- if filter.tracking_id_filter
12
- write_changed(parts)
13
- else
14
- old = old_paths
15
- used = write_changed(parts)
16
- (old - used).uniq.each { |p| FileUtils.rm_rf(p) }
17
- end
11
+ existing = existing_files_and_folders
12
+ used = write_changed(parts)
13
+ FileUtils.rm_rf(existing - used)
18
14
  end
19
15
  end
20
16
 
@@ -26,31 +22,36 @@ module Kennel
26
22
  used = []
27
23
 
28
24
  Utils.parallel(parts, max: 2) do |part|
29
- path = "generated/#{part.tracking_id.tr("/", ":").sub(":", "/")}.json"
25
+ path = path_for_tracking_id(part.tracking_id)
30
26
 
31
- used << File.dirname(path) # only 1 level of sub folders, so this is enough
27
+ used << File.dirname(path) # we have 1 level of sub folders, so this is enough
32
28
  used << path
33
29
 
34
- payload = part.as_json.merge(api_resource: part.class.api_resource)
35
- write_file_if_necessary(path, JSON.pretty_generate(payload) << "\n")
30
+ content = part.as_json.merge(api_resource: part.class.api_resource)
31
+ write_file_if_necessary(path, content)
36
32
  end
37
33
 
38
34
  used
39
35
  end
40
36
 
41
- def directories_to_clean_up
42
- if filter.project_filter
43
- filter.project_filter.map { |project| "generated/#{project}" }
37
+ def existing_files_and_folders
38
+ if filter.tracking_id_filter
39
+ filter.tracking_id_filter.map { |tracking_id| path_for_tracking_id(tracking_id) }
40
+ elsif filter.project_filter
41
+ filter.project_filter.flat_map { |project| Dir["generated/#{project}/*"] }
44
42
  else
45
- ["generated"]
43
+ Dir["generated/**/*"] # also includes folders so we clean up empty directories
46
44
  end
47
45
  end
48
46
 
49
- def old_paths
50
- Dir["{#{directories_to_clean_up.join(",")}}/**/*"]
47
+ def path_for_tracking_id(tracking_id)
48
+ "generated/#{tracking_id.tr("/", ":").sub(":", "/")}.json"
51
49
  end
52
50
 
53
51
  def write_file_if_necessary(path, content)
52
+ # NOTE: always generating is faster than JSON.load-ing and comparing
53
+ content = JSON.pretty_generate(content) << "\n"
54
+
54
55
  # 99% case
55
56
  begin
56
57
  return if File.read(path) == content
@@ -4,8 +4,8 @@ require "benchmark"
4
4
  module Kennel
5
5
  class Progress
6
6
  # print what we are doing and a spinner until it is done ... then show how long it took
7
- def self.progress(name, interval: 0.2, &block)
8
- return progress_no_tty(name, &block) unless Kennel.err.tty?
7
+ def self.progress(name, interval: 0.2, plain: false, &block)
8
+ return progress_no_tty(name, &block) if plain || !Kennel.err.tty?
9
9
 
10
10
  Kennel.err.print "#{name} ... "
11
11
 
data/lib/kennel/syncer.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "diff/lcs"
4
-
5
3
  module Kennel
6
4
  class Syncer
7
5
  DELETE_ORDER = ["dashboard", "slo", "monitor", "synthetics/tests"].freeze # dashboards references monitors + slos, slos reference monitors
@@ -10,34 +8,41 @@ module Kennel
10
8
  Plan = Struct.new(:changes, keyword_init: true)
11
9
  Change = Struct.new(:type, :api_resource, :tracking_id, :id)
12
10
 
13
- def initialize(api, expected, kennel:, project_filter: nil, tracking_id_filter: nil)
11
+ def initialize(api, expected, actual, strict_imports: true, project_filter: nil, tracking_id_filter: nil)
14
12
  @api = api
15
- @kennel = kennel
13
+ @expected = Set.new expected # need Set to speed up deletion
14
+ @actual = actual
15
+ @strict_imports = strict_imports
16
16
  @project_filter = project_filter
17
17
  @tracking_id_filter = tracking_id_filter
18
- @expected = Set.new expected # need set to speed up deletion
19
- calculate_diff
20
- validate_plan
18
+
19
+ @attribute_differ = AttributeDiffer.new
20
+
21
+ calculate_changes
22
+ validate_changes
21
23
  prevent_irreversible_partial_updates
24
+
25
+ @warnings.each { |message| Kennel.out.puts Utils.color(:yellow, "Warning: #{message}") }
22
26
  end
23
27
 
24
28
  def plan
29
+ Plan.new(
30
+ changes:
31
+ @create.map { |_id, e, _a| Change.new(:create, e.class.api_resource, e.tracking_id, nil) } +
32
+ @update.map { |id, e, _a| Change.new(:update, e.class.api_resource, e.tracking_id, id) } +
33
+ @delete.map { |id, _e, a| Change.new(:delete, a.fetch(:klass).api_resource, a.fetch(:tracking_id), id) }
34
+ )
35
+ end
36
+
37
+ def print_plan
25
38
  Kennel.out.puts "Plan:"
26
39
  if noop?
27
40
  Kennel.out.puts Utils.color(:green, "Nothing to do")
28
41
  else
29
- @warnings.each { |message| Kennel.out.puts Utils.color(:yellow, "Warning: #{message}") }
30
- print_plan "Create", @create, :green
31
- print_plan "Update", @update, :yellow
32
- print_plan "Delete", @delete, :red
42
+ print_changes "Create", @create, :green
43
+ print_changes "Update", @update, :yellow
44
+ print_changes "Delete", @delete, :red
33
45
  end
34
-
35
- Plan.new(
36
- changes:
37
- @create.map { |_id, e, _a| Change.new(:create, e.class.api_resource, e.tracking_id, nil) } +
38
- @update.map { |id, e, _a| Change.new(:update, e.class.api_resource, e.tracking_id, id) } +
39
- @delete.map { |id, _e, a| Change.new(:delete, a.fetch(:klass).api_resource, a.fetch(:tracking_id), id) }
40
- )
41
46
  end
42
47
 
43
48
  def confirm
@@ -53,7 +58,7 @@ module Kennel
53
58
  message = "#{e.class.api_resource} #{e.tracking_id}"
54
59
  Kennel.out.puts "Creating #{message}"
55
60
  reply = @api.create e.class.api_resource, e.as_json
56
- cache_metadata reply, e.class
61
+ Utils.inline_resource_metadata reply, e.class
57
62
  id = reply.fetch(:id)
58
63
  changes << Change.new(:create, e.class.api_resource, e.tracking_id, id)
59
64
  populate_id_map [], [reply] # allow resolving ids we could previously no resolve
@@ -82,8 +87,6 @@ module Kennel
82
87
 
83
88
  private
84
89
 
85
- attr_reader :kennel
86
-
87
90
  # loop over items until everything is resolved or crash when we get stuck
88
91
  # this solves cases like composite monitors depending on each other or monitor->monitor slo->slo monitor chains
89
92
  def each_resolved(list)
@@ -119,73 +122,67 @@ module Kennel
119
122
  @create.empty? && @update.empty? && @delete.empty?
120
123
  end
121
124
 
122
- def calculate_diff
125
+ def calculate_changes
123
126
  @warnings = []
124
- @update = []
125
- @delete = []
126
127
  @id_map = IdMap.new
127
128
 
128
- actual = Progress.progress("Downloading definitions") { download_definitions }
129
-
130
129
  Progress.progress "Diffing" do
131
- populate_id_map @expected, actual
132
- filter_actual! actual
130
+ populate_id_map @expected, @actual
131
+ filter_actual! @actual
133
132
  resolve_linked_tracking_ids! @expected # resolve dependencies to avoid diff
133
+ @expected.each(&:add_tracking_id) # avoid diff with actual, which has tracking_id
134
134
 
135
- @expected.each(&:add_tracking_id) # avoid diff with actual
135
+ # see which expected match the actual
136
+ matching, unmatched_expected, unmatched_actual = partition_matched_expected
137
+ validate_expected_id_not_missing unmatched_expected
138
+ fill_details! matching # need details to diff later
136
139
 
137
- lookup_map = matching_expected_lookup_map
138
- items = actual.map do |a|
139
- e = matching_expected(a, lookup_map)
140
- if e && @expected.delete?(e)
141
- [e, a]
142
- else
143
- [nil, a]
144
- end
145
- end
140
+ # update matching if needed
141
+ @update = matching.map do |e, a|
142
+ id = a.fetch(:id)
143
+ diff = e.diff(a)
144
+ [id, e, a, diff] if diff.any?
145
+ end.compact
146
146
 
147
- # fill details of things we need to compare
148
- details = items.map { |e, a| a if e && e.class.api_resource == "dashboard" }.compact
149
- @api.fill_details! "dashboard", details
147
+ # delete previously managed
148
+ @delete = unmatched_actual.map { |a| [a.fetch(:id), nil, a] if a.fetch(:tracking_id) }.compact
150
149
 
151
- # pick out things to update or delete
152
- items.each do |e, a|
153
- id = a.fetch(:id)
154
- if e
155
- diff = e.diff(a) # slow ...
156
- if diff.any?
157
- @update << [id, e, a, diff]
158
- end
159
- elsif a.fetch(:tracking_id) # was previously managed
160
- @delete << [id, nil, a]
161
- end
162
- end
150
+ # unmatched expected need to be created
151
+ @create = unmatched_expected.map { |e| [nil, e] }
163
152
 
164
- ensure_all_ids_found
165
- @create = @expected.map { |e| [nil, e] }
153
+ # order to avoid deadlocks
166
154
  @delete.sort_by! { |_, _, a| DELETE_ORDER.index a.fetch(:klass).api_resource }
167
155
  @update.sort_by! { |_, e, _| DELETE_ORDER.index e.class.api_resource } # slo needs to come before slo alert
168
156
  end
169
157
  end
170
158
 
171
- def download_definitions
172
- Utils.parallel(Models::Record.subclasses) do |klass|
173
- results = @api.list(klass.api_resource, with_downtimes: false) # lookup monitors without adding unnecessary downtime information
174
- results = results[results.keys.first] if results.is_a?(Hash) # dashboards are nested in {dashboards: []}
175
- results.each { |a| cache_metadata(a, klass) }
176
- end.flatten(1)
159
+ def partition_matched_expected
160
+ lookup_map = matching_expected_lookup_map
161
+ unmatched_expected = @expected.dup
162
+ unmatched_actual = []
163
+ matched = []
164
+ @actual.each do |a|
165
+ e = matching_expected(a, lookup_map)
166
+ if e && unmatched_expected.delete?(e)
167
+ matched << [e, a]
168
+ else
169
+ unmatched_actual << a
170
+ end
171
+ end.compact
172
+ [matched, unmatched_expected, unmatched_actual]
177
173
  end
178
174
 
179
- def cache_metadata(a, klass)
180
- a[:klass] = klass
181
- a[:tracking_id] = a.fetch(:klass).parse_tracking_id(a)
175
+ # fill details of things we need to compare
176
+ def fill_details!(details_needed)
177
+ details_needed = details_needed.map { |e, a| a if e && e.class.api_resource == "dashboard" }.compact
178
+ @api.fill_details! "dashboard", details_needed
182
179
  end
183
180
 
184
- def ensure_all_ids_found
185
- @expected.each do |e|
181
+ def validate_expected_id_not_missing(expected)
182
+ expected.each do |e|
186
183
  next unless id = e.id
187
184
  resource = e.class.api_resource
188
- if kennel.strict_imports
185
+ if @strict_imports
189
186
  raise "Unable to find existing #{resource} with id #{id}\nIf the #{resource} was deleted, remove the `id: -> { #{id} }` line."
190
187
  else
191
188
  @warnings << "#{resource} #{e.tracking_id} specifies id #{id}, but no such #{resource} exists. 'id' will be ignored. Remove the `id: -> { #{id} }` line."
@@ -210,79 +207,18 @@ module Kennel
210
207
  map["#{klass.api_resource}:#{a.fetch(:id)}"] || map[a.fetch(:tracking_id)]
211
208
  end
212
209
 
213
- def print_plan(step, list, color)
210
+ def print_changes(step, list, color)
214
211
  return if list.empty?
215
212
  list.each do |_, e, a, diff|
216
213
  klass = (e ? e.class : a.fetch(:klass))
217
214
  Kennel.out.puts Utils.color(color, "#{step} #{klass.api_resource} #{e&.tracking_id || a.fetch(:tracking_id)}")
218
- print_diff(diff) if diff # only for update
215
+ diff&.each { |args| Kennel.out.puts @attribute_differ.format(*args) } # only for update
219
216
  end
220
217
  end
221
218
 
222
- def print_diff(diff)
223
- diff.each do |type, field, old, new|
224
- use_diff = false
225
- if type == "+"
226
- temp = Utils.pretty_inspect(new)
227
- new = Utils.pretty_inspect(old)
228
- old = temp
229
- elsif old.is_a?(String) && new.is_a?(String) && (old.include?("\n") || new.include?("\n"))
230
- use_diff = true
231
- else # ~ and -
232
- old = Utils.pretty_inspect(old)
233
- new = Utils.pretty_inspect(new)
234
- end
235
-
236
- message =
237
- if use_diff
238
- " #{type}#{field}\n" +
239
- diff(old, new).map { |l| " #{l}" }.join("\n")
240
- elsif (old + new).size > 100
241
- " #{type}#{field}\n" \
242
- " #{old} ->\n" \
243
- " #{new}"
244
- else
245
- " #{type}#{field} #{old} -> #{new}"
246
- end
247
-
248
- Kennel.out.puts truncate_diff(message)
249
- end
250
- end
251
-
252
- # display diff for multi-line strings
253
- # must stay readable when color is off too
254
- def diff(old, new)
255
- Diff::LCS.sdiff(old.split("\n", -1), new.split("\n", -1)).flat_map do |diff|
256
- case diff.action
257
- when "-"
258
- Utils.color(:red, "- #{diff.old_element}")
259
- when "+"
260
- Utils.color(:green, "+ #{diff.new_element}")
261
- when "!"
262
- [
263
- Utils.color(:red, "- #{diff.old_element}"),
264
- Utils.color(:green, "+ #{diff.new_element}")
265
- ]
266
- else
267
- " #{diff.old_element}"
268
- end
269
- end
270
- end
271
-
272
- def truncate_diff(message)
273
- # min '2' because: -1 makes no sense, 0 does not work with * 2 math, 1 says '1 lines'
274
- @max_diff_lines ||= [Integer(ENV.fetch("MAX_DIFF_LINES", "50")), 2].max
275
- warning = Utils.color(
276
- :magenta,
277
- " (Diff for this item truncated after #{@max_diff_lines} lines. " \
278
- "Rerun with MAX_DIFF_LINES=#{@max_diff_lines * 2} to see more)"
279
- )
280
- Utils.truncate_lines(message, to: @max_diff_lines, warning: warning)
281
- end
282
-
283
219
  # We've already validated the desired objects ('generated') in isolation.
284
220
  # Now that we have made the plan, we can perform some more validation.
285
- def validate_plan
221
+ def validate_changes
286
222
  @update.each do |_, expected, actuals, diffs|
287
223
  expected.validate_update!(actuals, diffs)
288
224
  end
@@ -290,11 +226,10 @@ module Kennel
290
226
 
291
227
  # - do not add tracking-id when working with existing ids on a branch,
292
228
  # so resource do not get deleted when running an update on master (for example merge->CI)
293
- # - make sure the diff is clean, by kicking out the now noop-update
294
229
  # - ideally we'd never add tracking in the first place, but when adding tracking we do not know the diff yet
295
230
  def prevent_irreversible_partial_updates
296
- return unless @project_filter
297
- @update.select! do |_, e, _, diff|
231
+ return unless @project_filter # full update, so we are not on a branch
232
+ @update.select! do |_, e, _, diff| # ensure clean diff, by removing noop-update
298
233
  next true unless e.id # safe to add tracking when not having id
299
234
 
300
235
  diff.select! do |field_diff|
@@ -307,7 +242,7 @@ module Kennel
307
242
  actual != field_diff[3] # discard diff if now nothing changes
308
243
  end
309
244
 
310
- !diff.empty?
245
+ diff.any?
311
246
  end
312
247
  end
313
248
 
data/lib/kennel/tasks.rb CHANGED
@@ -101,8 +101,11 @@ namespace :kennel do
101
101
  end
102
102
 
103
103
  # also generate parts so users see and commit updated generated automatically
104
+ # (generate must run after plan to enable parallel .download+.generate inside of .plan)
104
105
  desc "show planned datadog changes (scope with PROJECT=name)"
105
- task plan: :generate do
106
+ task plan: :environment do
107
+ Kennel::Tasks.kennel.preload
108
+ Kennel::Tasks.kennel.generate
106
109
  Kennel::Tasks.kennel.plan
107
110
  end
108
111
 
data/lib/kennel/utils.rb CHANGED
@@ -5,6 +5,7 @@ module Kennel
5
5
 
6
6
  class TeeIO < IO
7
7
  def initialize(ios)
8
+ super(0) # called with fake file descriptor 0, so we can call super and get a proper class
8
9
  @ios = ios
9
10
  end
10
11
 
@@ -150,15 +151,9 @@ module Kennel
150
151
  end
151
152
  end
152
153
 
153
- # TODO: use awesome-print or similar, but it has too many monkey-patches
154
- # https://github.com/amazing-print/amazing_print/issues/36
155
- def pretty_inspect(object)
156
- string = object.inspect.dup
157
- string.gsub!(/:([a-z_]+)=>/, "\\1: ")
158
- 10.times do
159
- string.gsub!(/{(\S.*?\S)}/, "{ \\1 }") || break
160
- end
161
- string
154
+ def inline_resource_metadata(resource, klass)
155
+ resource[:klass] = klass
156
+ resource[:tracking_id] = klass.parse_tracking_id(resource)
162
157
  end
163
158
  end
164
159
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Kennel
3
- VERSION = "1.127.0"
3
+ VERSION = "1.129.0"
4
4
  end
data/lib/kennel.rb CHANGED
@@ -10,6 +10,7 @@ require "kennel/progress"
10
10
  require "kennel/filter"
11
11
  require "kennel/parts_serializer"
12
12
  require "kennel/projects_provider"
13
+ require "kennel/attribute_differ"
13
14
  require "kennel/syncer"
14
15
  require "kennel/id_map"
15
16
  require "kennel/api"
@@ -42,7 +43,6 @@ module Kennel
42
43
  UnresolvableIdError = Class.new(StandardError)
43
44
  DisallowedUpdateError = Class.new(StandardError)
44
45
  GenerationAbortedError = Class.new(StandardError)
45
- UpdateResult = Struct.new(:plan, :update, keyword_init: true)
46
46
 
47
47
  class << self
48
48
  attr_accessor :out, :err
@@ -52,24 +52,30 @@ module Kennel
52
52
  self.err = $stderr
53
53
 
54
54
  class Engine
55
+ attr_accessor :strict_imports
56
+
55
57
  def initialize
56
58
  @strict_imports = true
57
59
  end
58
60
 
59
- attr_accessor :strict_imports
61
+ # start generation and download in parallel to make planning faster
62
+ def preload
63
+ Utils.parallel([:generated, :definitions]) { |m| send m, plain: true }
64
+ end
60
65
 
61
66
  def generate
62
67
  parts = generated
63
- parts_serializer.write(parts) if ENV["STORE"] != "false" # quicker when debugging
68
+ PartsSerializer.new(filter: filter).write(parts) if ENV["STORE"] != "false" # quicker when debugging
64
69
  parts
65
70
  end
66
71
 
67
72
  def plan
73
+ syncer.print_plan
68
74
  syncer.plan
69
75
  end
70
76
 
71
77
  def update
72
- syncer.plan
78
+ syncer.print_plan
73
79
  syncer.update if syncer.confirm
74
80
  end
75
81
 
@@ -80,25 +86,25 @@ module Kennel
80
86
  end
81
87
 
82
88
  def syncer
83
- @syncer ||= Syncer.new(api, generated, kennel: self, project_filter: filter.project_filter, tracking_id_filter: filter.tracking_id_filter)
89
+ @syncer ||= begin
90
+ preload
91
+ Syncer.new(
92
+ api, generated, definitions,
93
+ strict_imports: strict_imports,
94
+ project_filter: filter.project_filter,
95
+ tracking_id_filter: filter.tracking_id_filter
96
+ )
97
+ end
84
98
  end
85
99
 
86
100
  def api
87
101
  @api ||= Api.new
88
102
  end
89
103
 
90
- def projects_provider
91
- @projects_provider ||= ProjectsProvider.new
92
- end
93
-
94
- def parts_serializer
95
- @parts_serializer ||= PartsSerializer.new(filter: filter)
96
- end
97
-
98
- def generated
104
+ def generated(**kwargs)
99
105
  @generated ||= begin
100
- parts = Progress.progress "Finding parts" do
101
- projects = projects_provider.projects
106
+ parts = Progress.progress "Finding parts", **kwargs do
107
+ projects = ProjectsProvider.new.projects
102
108
  projects = filter.filter_projects projects
103
109
 
104
110
  parts = Utils.parallel(projects, &:validated_parts).flatten(1)
@@ -123,5 +129,14 @@ module Kennel
123
129
  parts
124
130
  end
125
131
  end
132
+
133
+ def definitions(**kwargs)
134
+ @definitions ||= Progress.progress("Downloading definitions", **kwargs) do
135
+ Utils.parallel(Models::Record.subclasses) do |klass|
136
+ results = api.list(klass.api_resource, with_downtimes: false) # lookup monitors without adding unnecessary downtime information
137
+ results.each { |a| Utils.inline_resource_metadata(a, klass) }
138
+ end.flatten(1)
139
+ end
140
+ end
126
141
  end
127
142
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kennel
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.127.0
4
+ version: 1.129.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Grosser
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-12-05 00:00:00.000000000 Z
11
+ date: 2022-12-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs
@@ -80,7 +80,7 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '2.4'
83
- description:
83
+ description:
84
84
  email: michael@grosser.it
85
85
  executables: []
86
86
  extensions: []
@@ -89,6 +89,7 @@ files:
89
89
  - Readme.md
90
90
  - lib/kennel.rb
91
91
  - lib/kennel/api.rb
92
+ - lib/kennel/attribute_differ.rb
92
93
  - lib/kennel/file_cache.rb
93
94
  - lib/kennel/filter.rb
94
95
  - lib/kennel/github_reporter.rb
@@ -119,7 +120,7 @@ homepage: https://github.com/grosser/kennel
119
120
  licenses:
120
121
  - MIT
121
122
  metadata: {}
122
- post_install_message:
123
+ post_install_message:
123
124
  rdoc_options: []
124
125
  require_paths:
125
126
  - lib
@@ -127,15 +128,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
127
128
  requirements:
128
129
  - - ">="
129
130
  - !ruby/object:Gem::Version
130
- version: 2.6.0
131
+ version: 3.1.0
131
132
  required_rubygems_version: !ruby/object:Gem::Requirement
132
133
  requirements:
133
134
  - - ">="
134
135
  - !ruby/object:Gem::Version
135
136
  version: '0'
136
137
  requirements: []
137
- rubygems_version: 3.0.3
138
- signing_key:
138
+ rubygems_version: 3.3.26
139
+ signing_key:
139
140
  specification_version: 4
140
141
  summary: Keep datadog monitors/dashboards/etc in version control, avoid chaotic management
141
142
  via UI