kennel 1.131.0 → 1.132.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: ac3eeb3c1ec0e2b03c7acbf2b2d7c68335207a7e79408775e9b876c6c4acd400
4
- data.tar.gz: db96fddb04bce543efcea1a505a1d360447161259e9e9fdd481acfe785bad194
3
+ metadata.gz: 3d0c423b6658afcd8aa09a7e80169f85312540c03aad0bc8c3543e76c5a9420b
4
+ data.tar.gz: 71ab44e7dc1ed76114f6a3aadfd70d79d905d673d0090f85f96e40e6c8967620
5
5
  SHA512:
6
- metadata.gz: a710b2e2ae6932cf36bdd12fbb06c34f8b538944e693a484b294e7614b7d9941e7c2da498cf7cdcb56bda255a0b2c874383306437ee5b71456d900ae1e9a5c2f
7
- data.tar.gz: 90e767722863bb640f3bd96436937c390d40d8c8b868d3ee51a444ff6e042e36f03048cc1b5c9cec49d96207d53d99df6f529318658c3881fd9078ed55c2b7d8
6
+ metadata.gz: a4b6217e61f1282dd003bd2feea590b92f7dd8ddd2c7e9efc693ee58d0735baad7ea69d260ad376e581a7ff4b6364c51ddd2d42defd978928326183e5c8c5de3
7
+ data.tar.gz: ed60a47513aa4db4e1a1082cee541dbf9a1b345a512a058810eb4ad8763cc0505d3158b91f5b9df97e33a788cde84511fa0ad8b4de19aa14ca907f6944de8a2b
data/lib/kennel/filter.rb CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  module Kennel
4
4
  class Filter
5
- attr_reader :project_filter, :tracking_id_filter
6
-
7
5
  def initialize
8
6
  # build early so we fail fast on invalid user input
9
7
  @tracking_id_filter = build_tracking_id_filter
@@ -18,8 +16,25 @@ module Kennel
18
16
  filter_resources(parts, :tracking_id, tracking_id_filter, "resources", "TRACKING_ID")
19
17
  end
20
18
 
19
+ def filtering?
20
+ !project_filter.nil?
21
+ end
22
+
23
+ def matches_project_id?(project_id)
24
+ !filtering? || project_filter.include?(project_id)
25
+ end
26
+
27
+ def matches_tracking_id?(tracking_id)
28
+ return true unless filtering?
29
+ return tracking_id_filter.include?(tracking_id) if tracking_id_filter
30
+
31
+ project_filter.include?(tracking_id.split(":").first)
32
+ end
33
+
21
34
  private
22
35
 
36
+ attr_reader :project_filter, :tracking_id_filter
37
+
23
38
  def build_project_filter
24
39
  project_names = ENV["PROJECT"]&.split(",")&.sort&.uniq
25
40
  tracking_project_names = tracking_id_filter&.map { |id| id.split(":", 2).first }&.sort&.uniq
@@ -35,7 +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}]+".freeze
38
+ ALLOWED_KENNEL_ID_SEGMENT = /[#{ALLOWED_KENNEL_ID_CHARS}]+/
39
+ ALLOWED_KENNEL_ID_FULL = "#{ALLOWED_KENNEL_ID_SEGMENT}:#{ALLOWED_KENNEL_ID_SEGMENT}".freeze
39
40
  ALLOWED_KENNEL_ID_REGEX = /\A#{ALLOWED_KENNEL_ID_FULL}\z/
40
41
 
41
42
  settings :id, :kennel_id
@@ -35,13 +35,16 @@ module Kennel
35
35
  end
36
36
 
37
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}/*"] }
42
- else
43
- Dir["generated/**/*"] # also includes folders so we clean up empty directories
38
+ paths = Dir["generated/**/*"]
39
+
40
+ if filter.filtering?
41
+ paths.select! do |path|
42
+ tracking_id = path.split("/")[1..2].to_a.join(":")
43
+ filter.matches_tracking_id?(tracking_id)
44
+ end
44
45
  end
46
+
47
+ paths
45
48
  end
46
49
 
47
50
  def path_for_tracking_id(tracking_id)
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kennel
4
+ class Syncer
5
+ module MatchedExpected
6
+ class << self
7
+ def partition(expected, actual)
8
+ lookup_map = matching_expected_lookup_map(expected)
9
+ unmatched_expected = Set.new(expected) # for efficient deletion
10
+ unmatched_actual = []
11
+ matched = []
12
+ actual.each do |a|
13
+ e = matching_expected(a, lookup_map)
14
+ if e && unmatched_expected.delete?(e)
15
+ matched << [e, a]
16
+ else
17
+ unmatched_actual << a
18
+ end
19
+ end.compact
20
+ [matched, unmatched_expected.to_a, unmatched_actual]
21
+ end
22
+
23
+ private
24
+
25
+ # index list by all the thing we look up by: tracking id and actual id
26
+ def matching_expected_lookup_map(expected)
27
+ expected.each_with_object({}) do |e, all|
28
+ keys = [e.tracking_id]
29
+ keys << "#{e.class.api_resource}:#{e.id}" if e.id
30
+ keys.compact.each do |key|
31
+ raise "Lookup #{key} is duplicated" if all[key]
32
+ all[key] = e
33
+ end
34
+ end
35
+ end
36
+
37
+ def matching_expected(a, map)
38
+ klass = a.fetch(:klass)
39
+ map["#{klass.api_resource}:#{a.fetch(:id)}"] || map[a.fetch(:tracking_id)]
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kennel
4
+ class Syncer
5
+ class PlanDisplayer
6
+ def initialize
7
+ @attribute_differ = AttributeDiffer.new
8
+ end
9
+
10
+ def display(internal_plan)
11
+ Kennel.out.puts "Plan:"
12
+ if internal_plan.empty?
13
+ Kennel.out.puts Console.color(:green, "Nothing to do")
14
+ else
15
+ print_changes "Create", internal_plan.creates, :green
16
+ print_changes "Update", internal_plan.updates, :yellow
17
+ print_changes "Delete", internal_plan.deletes, :red
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def print_changes(step, list, color)
24
+ return if list.empty?
25
+ list.each do |item|
26
+ Kennel.out.puts Console.color(color, "#{step} #{item.api_resource} #{item.tracking_id}")
27
+ if item.class::TYPE == :update
28
+ item.diff.each { |args| Kennel.out.puts @attribute_differ.format(*args) } # only for update
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../id_map"
4
+
5
+ module Kennel
6
+ class Syncer
7
+ class Resolver
8
+ def initialize(expected:, filter:)
9
+ @id_map = IdMap.new
10
+ @filter = filter
11
+
12
+ # mark everything as new
13
+ expected.each do |e|
14
+ id_map.set(e.class.api_resource, e.tracking_id, IdMap::NEW)
15
+ if e.class.api_resource == "synthetics/tests"
16
+ id_map.set(Kennel::Models::Monitor.api_resource, e.tracking_id, IdMap::NEW)
17
+ end
18
+ end
19
+ end
20
+
21
+ def add_actual(actual)
22
+ # override resources that exist with their id
23
+ actual.each do |a|
24
+ # ignore when not managed by kennel
25
+ next unless tracking_id = a.fetch(:tracking_id)
26
+
27
+ # ignore when deleted from the codebase
28
+ # (when running with filters we cannot see the other resources in the codebase)
29
+ api_resource = a.fetch(:klass).api_resource
30
+ next if !id_map.get(api_resource, tracking_id) && filter.matches_tracking_id?(tracking_id)
31
+
32
+ id_map.set(api_resource, tracking_id, a.fetch(:id))
33
+ if a.fetch(:klass).api_resource == "synthetics/tests"
34
+ id_map.set(Kennel::Models::Monitor.api_resource, tracking_id, a.fetch(:monitor_id))
35
+ end
36
+ end
37
+ end
38
+
39
+ def resolve_as_much_as_possible(expected)
40
+ expected.each do |e|
41
+ e.resolve_linked_tracking_ids!(id_map, force: false)
42
+ end
43
+ end
44
+
45
+ # loop over items until everything is resolved or crash when we get stuck
46
+ # this solves cases like composite monitors depending on each other or monitor->monitor slo->slo monitor chains
47
+ def each_resolved(list)
48
+ list = list.dup
49
+ loop do
50
+ return if list.empty?
51
+ list.reject! do |item|
52
+ if resolved?(item.expected)
53
+ yield item
54
+ true
55
+ else
56
+ false
57
+ end
58
+ end ||
59
+ assert_resolved(list[0].expected) # resolve something or show a circular dependency error
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ attr_reader :id_map, :filter
66
+
67
+ # TODO: optimize by storing an instance variable if already resolved
68
+ def resolved?(e)
69
+ assert_resolved e
70
+ true
71
+ rescue UnresolvableIdError
72
+ false
73
+ end
74
+
75
+ # raises UnresolvableIdError when not resolved
76
+ def assert_resolved(e)
77
+ e.resolve_linked_tracking_ids!(id_map, force: true)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kennel
4
+ class Syncer
5
+ module Types
6
+ class PlannedChange
7
+ def initialize(klass, tracking_id)
8
+ @klass = klass
9
+ @tracking_id = tracking_id
10
+ end
11
+
12
+ def api_resource
13
+ klass.api_resource
14
+ end
15
+
16
+ def url(id = nil)
17
+ klass.url(id || self.id)
18
+ end
19
+
20
+ def change(id = nil)
21
+ Change.new(self.class::TYPE, api_resource, tracking_id, id)
22
+ end
23
+
24
+ attr_reader :klass, :tracking_id
25
+ end
26
+
27
+ class PlannedCreate < PlannedChange
28
+ TYPE = :create
29
+
30
+ def initialize(expected)
31
+ super(expected.class, expected.tracking_id)
32
+ @expected = expected
33
+ end
34
+
35
+ attr_reader :expected
36
+ end
37
+
38
+ class PlannedUpdate < PlannedChange
39
+ TYPE = :update
40
+
41
+ def initialize(expected, actual, diff)
42
+ super(expected.class, expected.tracking_id)
43
+ @expected = expected
44
+ @actual = actual
45
+ @diff = diff
46
+ @id = actual.fetch(:id)
47
+ end
48
+
49
+ def change
50
+ super(id)
51
+ end
52
+
53
+ attr_reader :expected, :actual, :diff, :id
54
+ end
55
+
56
+ class PlannedDelete < PlannedChange
57
+ TYPE = :delete
58
+
59
+ def initialize(actual)
60
+ super(actual.fetch(:klass), actual.fetch(:tracking_id))
61
+ @actual = actual
62
+ @id = actual.fetch(:id)
63
+ end
64
+
65
+ def change
66
+ super(id)
67
+ end
68
+
69
+ attr_reader :actual, :id
70
+ end
71
+ end
72
+ end
73
+ end
data/lib/kennel/syncer.rb CHANGED
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "./syncer/matched_expected"
4
+ require_relative "./syncer/plan_displayer"
5
+ require_relative "./syncer/resolver"
6
+ require_relative "./syncer/types"
7
+
3
8
  module Kennel
4
9
  class Syncer
5
10
  DELETE_ORDER = ["dashboard", "slo", "monitor", "synthetics/tests"].freeze # dashboards references monitors + slos, slos reference monitors
@@ -15,13 +20,12 @@ module Kennel
15
20
 
16
21
  Change = Struct.new(:type, :api_resource, :tracking_id, :id)
17
22
 
18
- def initialize(api, expected, actual, strict_imports: true, project_filter: nil, tracking_id_filter: nil)
23
+ def initialize(api, expected, actual, filter:, strict_imports: true)
19
24
  @api = api
20
25
  @strict_imports = strict_imports
21
- @project_filter = project_filter
22
- @tracking_id_filter = tracking_id_filter
26
+ @filter = filter
23
27
 
24
- @resolver = Resolver.new(expected: expected, project_filter: @project_filter, tracking_id_filter: @tracking_id_filter)
28
+ @resolver = Resolver.new(expected: expected, filter: filter)
25
29
 
26
30
  internal_plan = calculate_changes(expected: expected, actual: actual)
27
31
  validate_changes(internal_plan)
@@ -81,7 +85,7 @@ module Kennel
81
85
 
82
86
  private
83
87
 
84
- attr_reader :resolver, :internal_plan
88
+ attr_reader :filter, :resolver, :internal_plan
85
89
 
86
90
  def calculate_changes(expected:, actual:)
87
91
  @warnings = []
@@ -101,7 +105,7 @@ module Kennel
101
105
  # Refuse to "adopt" existing items into kennel while running with a filter (i.e. on a branch).
102
106
  # Without this, we'd adopt an item, then the next CI run would delete it
103
107
  # (instead of "unadopting" it).
104
- e.add_tracking_id unless @project_filter && a.fetch(:tracking_id).nil?
108
+ e.add_tracking_id unless filter.filtering? && a.fetch(:tracking_id).nil?
105
109
  id = a.fetch(:id)
106
110
  diff = e.diff(a)
107
111
  a[:id] = id
@@ -150,233 +154,11 @@ module Kennel
150
154
  end
151
155
 
152
156
  def filter_actual!(actual)
153
- if @tracking_id_filter
154
- actual.select! do |a|
155
- tracking_id = a.fetch(:tracking_id)
156
- !tracking_id || @tracking_id_filter.include?(tracking_id)
157
- end
158
- elsif @project_filter
159
- project_prefixes = @project_filter.map { |p| "#{p}:" }
160
- actual.select! do |a|
161
- tracking_id = a.fetch(:tracking_id)
162
- !tracking_id || tracking_id.start_with?(*project_prefixes)
163
- end
164
- end
165
- end
166
-
167
- class PlanDisplayer
168
- def initialize
169
- @attribute_differ = AttributeDiffer.new
170
- end
171
-
172
- def display(internal_plan)
173
- Kennel.out.puts "Plan:"
174
- if internal_plan.empty?
175
- Kennel.out.puts Console.color(:green, "Nothing to do")
176
- else
177
- print_changes "Create", internal_plan.creates, :green
178
- print_changes "Update", internal_plan.updates, :yellow
179
- print_changes "Delete", internal_plan.deletes, :red
180
- end
181
- end
182
-
183
- private
184
-
185
- def print_changes(step, list, color)
186
- return if list.empty?
187
- list.each do |item|
188
- Kennel.out.puts Console.color(color, "#{step} #{item.api_resource} #{item.tracking_id}")
189
- if item.class::TYPE == :update
190
- item.diff.each { |args| Kennel.out.puts @attribute_differ.format(*args) } # only for update
191
- end
192
- end
193
- end
194
- end
195
-
196
- class Resolver
197
- def initialize(expected:, project_filter:, tracking_id_filter:)
198
- @id_map = IdMap.new
199
- @project_filter = project_filter
200
- @tracking_id_filter = tracking_id_filter
201
-
202
- # mark everything as new
203
- expected.each do |e|
204
- id_map.set(e.class.api_resource, e.tracking_id, IdMap::NEW)
205
- if e.class.api_resource == "synthetics/tests"
206
- id_map.set(Kennel::Models::Monitor.api_resource, e.tracking_id, IdMap::NEW)
207
- end
208
- end
209
- end
210
-
211
- def add_actual(actual)
212
- # override resources that exist with their id
213
- project_prefixes = project_filter&.map { |p| "#{p}:" }
214
-
215
- actual.each do |a|
216
- # ignore when not managed by kennel
217
- next unless tracking_id = a.fetch(:tracking_id)
218
-
219
- # ignore when deleted from the codebase
220
- # (when running with filters we cannot see the other resources in the codebase)
221
- api_resource = a.fetch(:klass).api_resource
222
- next if
223
- !id_map.get(api_resource, tracking_id) &&
224
- (!project_prefixes || tracking_id.start_with?(*project_prefixes)) &&
225
- (!tracking_id_filter || tracking_id_filter.include?(tracking_id))
226
-
227
- id_map.set(api_resource, tracking_id, a.fetch(:id))
228
- if a.fetch(:klass).api_resource == "synthetics/tests"
229
- id_map.set(Kennel::Models::Monitor.api_resource, tracking_id, a.fetch(:monitor_id))
230
- end
231
- end
232
- end
233
-
234
- def resolve_as_much_as_possible(expected)
235
- expected.each do |e|
236
- e.resolve_linked_tracking_ids!(id_map, force: false)
237
- end
238
- end
239
-
240
- # loop over items until everything is resolved or crash when we get stuck
241
- # this solves cases like composite monitors depending on each other or monitor->monitor slo->slo monitor chains
242
- def each_resolved(list)
243
- list = list.dup
244
- loop do
245
- return if list.empty?
246
- list.reject! do |item|
247
- if resolved?(item.expected)
248
- yield item
249
- true
250
- else
251
- false
252
- end
253
- end ||
254
- assert_resolved(list[0].expected) # resolve something or show a circular dependency error
255
- end
256
- end
257
-
258
- private
259
-
260
- attr_reader :id_map, :project_filter, :tracking_id_filter
261
-
262
- # TODO: optimize by storing an instance variable if already resolved
263
- def resolved?(e)
264
- assert_resolved e
265
- true
266
- rescue UnresolvableIdError
267
- false
268
- end
269
-
270
- # raises UnresolvableIdError when not resolved
271
- def assert_resolved(e)
272
- e.resolve_linked_tracking_ids!(id_map, force: true)
273
- end
274
- end
275
-
276
- module MatchedExpected
277
- class << self
278
- def partition(expected, actual)
279
- lookup_map = matching_expected_lookup_map(expected)
280
- unmatched_expected = Set.new(expected) # for efficient deletion
281
- unmatched_actual = []
282
- matched = []
283
- actual.each do |a|
284
- e = matching_expected(a, lookup_map)
285
- if e && unmatched_expected.delete?(e)
286
- matched << [e, a]
287
- else
288
- unmatched_actual << a
289
- end
290
- end.compact
291
- [matched, unmatched_expected.to_a, unmatched_actual]
292
- end
293
-
294
- private
295
-
296
- # index list by all the thing we look up by: tracking id and actual id
297
- def matching_expected_lookup_map(expected)
298
- expected.each_with_object({}) do |e, all|
299
- keys = [e.tracking_id]
300
- keys << "#{e.class.api_resource}:#{e.id}" if e.id
301
- keys.compact.each do |key|
302
- raise "Lookup #{key} is duplicated" if all[key]
303
- all[key] = e
304
- end
305
- end
306
- end
307
-
308
- def matching_expected(a, map)
309
- klass = a.fetch(:klass)
310
- map["#{klass.api_resource}:#{a.fetch(:id)}"] || map[a.fetch(:tracking_id)]
311
- end
312
- end
313
- end
314
-
315
- module Types
316
- class PlannedChange
317
- def initialize(klass, tracking_id)
318
- @klass = klass
319
- @tracking_id = tracking_id
320
- end
321
-
322
- def api_resource
323
- klass.api_resource
324
- end
325
-
326
- def url(id = nil)
327
- klass.url(id || self.id)
328
- end
329
-
330
- def change(id = nil)
331
- Change.new(self.class::TYPE, api_resource, tracking_id, id)
332
- end
333
-
334
- attr_reader :klass, :tracking_id
335
- end
336
-
337
- class PlannedCreate < PlannedChange
338
- TYPE = :create
339
-
340
- def initialize(expected)
341
- super(expected.class, expected.tracking_id)
342
- @expected = expected
343
- end
344
-
345
- attr_reader :expected
346
- end
347
-
348
- class PlannedUpdate < PlannedChange
349
- TYPE = :update
350
-
351
- def initialize(expected, actual, diff)
352
- super(expected.class, expected.tracking_id)
353
- @expected = expected
354
- @actual = actual
355
- @diff = diff
356
- @id = actual.fetch(:id)
357
- end
358
-
359
- def change
360
- super(id)
361
- end
362
-
363
- attr_reader :expected, :actual, :diff, :id
364
- end
365
-
366
- class PlannedDelete < PlannedChange
367
- TYPE = :delete
368
-
369
- def initialize(actual)
370
- super(actual.fetch(:klass), actual.fetch(:tracking_id))
371
- @actual = actual
372
- @id = actual.fetch(:id)
373
- end
374
-
375
- def change
376
- super(id)
377
- end
157
+ return unless filter.filtering? # minor optimization
378
158
 
379
- attr_reader :actual, :id
159
+ actual.select! do |a|
160
+ tracking_id = a.fetch(:tracking_id)
161
+ tracking_id.nil? || filter.matches_tracking_id?(tracking_id)
380
162
  end
381
163
  end
382
164
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Kennel
3
- VERSION = "1.131.0"
3
+ VERSION = "1.132.0"
4
4
  end
data/lib/kennel.rb CHANGED
@@ -92,9 +92,8 @@ module Kennel
92
92
  preload
93
93
  Syncer.new(
94
94
  api, generated, definitions,
95
- strict_imports: strict_imports,
96
- project_filter: filter.project_filter,
97
- tracking_id_filter: filter.tracking_id_filter
95
+ filter: filter,
96
+ strict_imports: strict_imports
98
97
  )
99
98
  end
100
99
  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.131.0
4
+ version: 1.132.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Grosser
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-01-09 00:00:00.000000000 Z
11
+ date: 2023-01-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs
@@ -112,6 +112,10 @@ files:
112
112
  - lib/kennel/string_utils.rb
113
113
  - lib/kennel/subclass_tracking.rb
114
114
  - lib/kennel/syncer.rb
115
+ - lib/kennel/syncer/matched_expected.rb
116
+ - lib/kennel/syncer/plan_displayer.rb
117
+ - lib/kennel/syncer/resolver.rb
118
+ - lib/kennel/syncer/types.rb
115
119
  - lib/kennel/tasks.rb
116
120
  - lib/kennel/template_variables.rb
117
121
  - lib/kennel/unmuted_alerts.rb