kennel 1.127.0 → 1.129.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 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