kennel 1.130.0 → 1.132.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: d6ee3df09b69dd5a7af7285c219f4415c4e94ce2ab4f6533c5fff03be848e9c4
4
- data.tar.gz: d7f923b69ad9b63774141ac0e9c6c04de7a26a67abd732c41c5f19d478704656
3
+ metadata.gz: 3d0c423b6658afcd8aa09a7e80169f85312540c03aad0bc8c3543e76c5a9420b
4
+ data.tar.gz: 71ab44e7dc1ed76114f6a3aadfd70d79d905d673d0090f85f96e40e6c8967620
5
5
  SHA512:
6
- metadata.gz: 7c967bf54743cc757a0c0c911e981701d4b5b393f6ebac66538432541c7c6dfecef2cd8c1c0e5389f90c7d0b91a54e79a29e0e1abf9d16fd67adad4bcd6209fe
7
- data.tar.gz: 297239ea9305e4eadbe40a363160b0ca2aaf70dad237f95a448b00b30d3db9ed9f707b2736b2596ef73e8981d333b600af87da40978f5d1c9fc8e0dee3d7f9ea
6
+ metadata.gz: a4b6217e61f1282dd003bd2feea590b92f7dd8ddd2c7e9efc693ee58d0735baad7ea69d260ad376e581a7ff4b6364c51ddd2d42defd978928326183e5c8c5de3
7
+ data.tar.gz: ed60a47513aa4db4e1a1082cee541dbf9a1b345a512a058810eb4ad8763cc0505d3158b91f5b9df97e33a788cde84511fa0ad8b4de19aa14ca907f6944de8a2b
data/lib/kennel/api.rb CHANGED
@@ -7,6 +7,8 @@ module Kennel
7
7
 
8
8
  def self.tag(api_resource, reply)
9
9
  klass = Models::Record.api_resource_map[api_resource]
10
+ return reply unless klass # do not blow up on unknown models
11
+
10
12
  reply.merge(
11
13
  klass: klass,
12
14
  tracking_id: klass.parse_tracking_id(reply)
@@ -57,7 +57,9 @@ module Kennel
57
57
  # - delete expired keys
58
58
  # - delete what would be deleted anyway when updating
59
59
  def expire_old_data
60
- @data.reject! { |_, (_, (_, cv), expires)| expires < @now || cv != @cache_version }
60
+ @data.reject! do |(_api_resource, _id), (_value, (_key_version, cache_version), expires)|
61
+ expires < @now || cache_version != @cache_version
62
+ end
61
63
  end
62
64
  end
63
65
  end
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
@@ -202,7 +202,7 @@ module Kennel
202
202
  end
203
203
  end
204
204
 
205
- def validate_update!(_actuals, diffs)
205
+ def validate_update!(diffs)
206
206
  _, path, from, to = diffs.find { |diff| diff[1] == "layout_type" }
207
207
  invalid_update!(path, from, to) if path
208
208
  end
@@ -137,7 +137,7 @@ module Kennel
137
137
  end
138
138
  end
139
139
 
140
- def validate_update!(_actuals, diffs)
140
+ def validate_update!(diffs)
141
141
  # ensure type does not change, but not if it's metric->query which is supported and used by importer.rb
142
142
  _, path, from, to = diffs.detect { |_, path, _, _| path == "type" }
143
143
  if path && !(from == "metric alert" && to == "query alert")
@@ -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
@@ -167,7 +168,7 @@ module Kennel
167
168
  end
168
169
 
169
170
  # Can raise DisallowedUpdateError
170
- def validate_update!(*)
171
+ def validate_update!(_diffs)
171
172
  end
172
173
 
173
174
  def invalid_update!(field, old_value, new_value)
@@ -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,52 +1,52 @@
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
6
11
  LINE_UP = "\e[1A\033[K" # go up and clear
7
12
 
8
13
  Plan = Struct.new(:changes, keyword_init: true)
14
+
15
+ InternalPlan = Struct.new(:creates, :updates, :deletes) do
16
+ def empty?
17
+ creates.empty? && updates.empty? && deletes.empty?
18
+ end
19
+ end
20
+
9
21
  Change = Struct.new(:type, :api_resource, :tracking_id, :id)
10
22
 
11
- 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)
12
24
  @api = api
13
- @expected = Set.new expected # need Set to speed up deletion
14
- @actual = actual
15
25
  @strict_imports = strict_imports
16
- @project_filter = project_filter
17
- @tracking_id_filter = tracking_id_filter
26
+ @filter = filter
18
27
 
19
- @attribute_differ = AttributeDiffer.new
28
+ @resolver = Resolver.new(expected: expected, filter: filter)
20
29
 
21
- calculate_changes
22
- validate_changes
23
- prevent_irreversible_partial_updates
30
+ internal_plan = calculate_changes(expected: expected, actual: actual)
31
+ validate_changes(internal_plan)
32
+ @internal_plan = internal_plan
24
33
 
25
34
  @warnings.each { |message| Kennel.out.puts Console.color(:yellow, "Warning: #{message}") }
26
35
  end
27
36
 
28
37
  def plan
38
+ ip = @internal_plan
29
39
  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) }
40
+ changes: (ip.creates + ip.updates + ip.deletes).map(&:change)
34
41
  )
35
42
  end
36
43
 
37
44
  def print_plan
38
- Kennel.out.puts "Plan:"
39
- if noop?
40
- Kennel.out.puts Console.color(:green, "Nothing to do")
41
- else
42
- print_changes "Create", @create, :green
43
- print_changes "Update", @update, :yellow
44
- print_changes "Delete", @delete, :red
45
- end
45
+ PlanDisplayer.new.display(internal_plan)
46
46
  end
47
47
 
48
48
  def confirm
49
- return false if noop?
49
+ return false if internal_plan.empty?
50
50
  return true if ENV["CI"] || !STDIN.tty? || !Kennel.err.tty?
51
51
  Console.ask?("Execute Plan ?")
52
52
  end
@@ -54,121 +54,77 @@ module Kennel
54
54
  def update
55
55
  changes = []
56
56
 
57
- each_resolved @create do |_, e|
58
- message = "#{e.class.api_resource} #{e.tracking_id}"
57
+ internal_plan.deletes.each do |item|
58
+ message = "#{item.api_resource} #{item.tracking_id} #{item.id}"
59
+ Kennel.out.puts "Deleting #{message}"
60
+ @api.delete item.api_resource, item.id
61
+ changes << item.change
62
+ Kennel.out.puts "#{LINE_UP}Deleted #{message}"
63
+ end
64
+
65
+ resolver.each_resolved internal_plan.creates do |item|
66
+ message = "#{item.api_resource} #{item.tracking_id}"
59
67
  Kennel.out.puts "Creating #{message}"
60
- reply = @api.create e.class.api_resource, e.as_json
68
+ reply = @api.create item.api_resource, item.expected.as_json
61
69
  id = reply.fetch(:id)
62
- changes << Change.new(:create, e.class.api_resource, e.tracking_id, id)
63
- populate_id_map [], [reply] # allow resolving ids we could previously no resolve
64
- Kennel.out.puts "#{LINE_UP}Created #{message} #{e.class.url(id)}"
70
+ changes << item.change(id)
71
+ resolver.add_actual [reply] # allow resolving ids we could previously not resolve
72
+ Kennel.out.puts "#{LINE_UP}Created #{message} #{item.url(id)}"
65
73
  end
66
74
 
67
- each_resolved @update do |id, e|
68
- message = "#{e.class.api_resource} #{e.tracking_id} #{e.class.url(id)}"
75
+ resolver.each_resolved internal_plan.updates do |item|
76
+ message = "#{item.api_resource} #{item.tracking_id} #{item.url}"
69
77
  Kennel.out.puts "Updating #{message}"
70
- @api.update e.class.api_resource, id, e.as_json
71
- changes << Change.new(:update, e.class.api_resource, e.tracking_id, id)
78
+ @api.update item.api_resource, item.id, item.expected.as_json
79
+ changes << item.change
72
80
  Kennel.out.puts "#{LINE_UP}Updated #{message}"
73
81
  end
74
82
 
75
- @delete.each do |id, _, a|
76
- klass = a.fetch(:klass)
77
- message = "#{klass.api_resource} #{a.fetch(:tracking_id)} #{id}"
78
- Kennel.out.puts "Deleting #{message}"
79
- @api.delete klass.api_resource, id
80
- changes << Change.new(:delete, klass.api_resource, a.fetch(:tracking_id), id)
81
- Kennel.out.puts "#{LINE_UP}Deleted #{message}"
82
- end
83
-
84
83
  Plan.new(changes: changes)
85
84
  end
86
85
 
87
86
  private
88
87
 
89
- # loop over items until everything is resolved or crash when we get stuck
90
- # this solves cases like composite monitors depending on each other or monitor->monitor slo->slo monitor chains
91
- def each_resolved(list)
92
- list = list.dup
93
- loop do
94
- return if list.empty?
95
- list.reject! do |id, e|
96
- if resolved?(e)
97
- yield id, e
98
- true
99
- else
100
- false
101
- end
102
- end ||
103
- assert_resolved(list[0][1]) # resolve something or show a circular dependency error
104
- end
105
- end
106
-
107
- # TODO: optimize by storing an instance variable if already resolved
108
- def resolved?(e)
109
- assert_resolved e
110
- true
111
- rescue UnresolvableIdError
112
- false
113
- end
88
+ attr_reader :filter, :resolver, :internal_plan
114
89
 
115
- # raises UnresolvableIdError when not resolved
116
- def assert_resolved(e)
117
- resolve_linked_tracking_ids! [e], force: true
118
- end
119
-
120
- def noop?
121
- @create.empty? && @update.empty? && @delete.empty?
122
- end
123
-
124
- def calculate_changes
90
+ def calculate_changes(expected:, actual:)
125
91
  @warnings = []
126
- @id_map = IdMap.new
127
92
 
128
93
  Progress.progress "Diffing" do
129
- populate_id_map @expected, @actual
130
- filter_actual! @actual
131
- resolve_linked_tracking_ids! @expected # resolve dependencies to avoid diff
132
- @expected.each(&:add_tracking_id) # avoid diff with actual, which has tracking_id
94
+ resolver.add_actual actual
95
+ filter_actual! actual
96
+ resolver.resolve_as_much_as_possible(expected) # resolve as many dependencies as possible to reduce the diff
133
97
 
134
98
  # see which expected match the actual
135
- matching, unmatched_expected, unmatched_actual = partition_matched_expected
99
+ matching, unmatched_expected, unmatched_actual = MatchedExpected.partition(expected, actual)
136
100
  validate_expected_id_not_missing unmatched_expected
137
101
  fill_details! matching # need details to diff later
138
102
 
139
103
  # update matching if needed
140
- @update = matching.map do |e, a|
104
+ updates = matching.map do |e, a|
105
+ # Refuse to "adopt" existing items into kennel while running with a filter (i.e. on a branch).
106
+ # Without this, we'd adopt an item, then the next CI run would delete it
107
+ # (instead of "unadopting" it).
108
+ e.add_tracking_id unless filter.filtering? && a.fetch(:tracking_id).nil?
141
109
  id = a.fetch(:id)
142
110
  diff = e.diff(a)
143
- [id, e, a, diff] if diff.any?
111
+ a[:id] = id
112
+ Types::PlannedUpdate.new(e, a, diff) if diff.any?
144
113
  end.compact
145
114
 
146
115
  # delete previously managed
147
- @delete = unmatched_actual.map { |a| [a.fetch(:id), nil, a] if a.fetch(:tracking_id) }.compact
116
+ deletes = unmatched_actual.map { |a| Types::PlannedDelete.new(a) if a.fetch(:tracking_id) }.compact
148
117
 
149
118
  # unmatched expected need to be created
150
- @create = unmatched_expected.map { |e| [nil, e] }
119
+ unmatched_expected.each(&:add_tracking_id)
120
+ creates = unmatched_expected.map { |e| Types::PlannedCreate.new(e) }
151
121
 
152
122
  # order to avoid deadlocks
153
- @delete.sort_by! { |_, _, a| DELETE_ORDER.index a.fetch(:klass).api_resource }
154
- @update.sort_by! { |_, e, _| DELETE_ORDER.index e.class.api_resource } # slo needs to come before slo alert
155
- end
156
- end
123
+ deletes.sort_by! { |item| DELETE_ORDER.index item.api_resource }
124
+ updates.sort_by! { |item| DELETE_ORDER.index item.api_resource } # slo needs to come before slo alert
157
125
 
158
- def partition_matched_expected
159
- lookup_map = matching_expected_lookup_map
160
- unmatched_expected = @expected.dup
161
- unmatched_actual = []
162
- matched = []
163
- @actual.each do |a|
164
- e = matching_expected(a, lookup_map)
165
- if e && unmatched_expected.delete?(e)
166
- matched << [e, a]
167
- else
168
- unmatched_actual << a
169
- end
170
- end.compact
171
- [matched, unmatched_expected, unmatched_actual]
126
+ InternalPlan.new(creates, updates, deletes)
127
+ end
172
128
  end
173
129
 
174
130
  # fill details of things we need to compare
@@ -189,108 +145,20 @@ module Kennel
189
145
  end
190
146
  end
191
147
 
192
- # index list by all the thing we look up by: tracking id and actual id
193
- def matching_expected_lookup_map
194
- @expected.each_with_object({}) do |e, all|
195
- keys = [e.tracking_id]
196
- keys << "#{e.class.api_resource}:#{e.id}" if e.id
197
- keys.compact.each do |key|
198
- raise "Lookup #{key} is duplicated" if all[key]
199
- all[key] = e
200
- end
201
- end
202
- end
203
-
204
- def matching_expected(a, map)
205
- klass = a.fetch(:klass)
206
- map["#{klass.api_resource}:#{a.fetch(:id)}"] || map[a.fetch(:tracking_id)]
207
- end
208
-
209
- def print_changes(step, list, color)
210
- return if list.empty?
211
- list.each do |_, e, a, diff|
212
- klass = (e ? e.class : a.fetch(:klass))
213
- Kennel.out.puts Console.color(color, "#{step} #{klass.api_resource} #{e&.tracking_id || a.fetch(:tracking_id)}")
214
- diff&.each { |args| Kennel.out.puts @attribute_differ.format(*args) } # only for update
215
- end
216
- end
217
-
218
148
  # We've already validated the desired objects ('generated') in isolation.
219
149
  # Now that we have made the plan, we can perform some more validation.
220
- def validate_changes
221
- @update.each do |_, expected, actuals, diffs|
222
- expected.validate_update!(actuals, diffs)
223
- end
224
- end
225
-
226
- # - do not add tracking-id when working with existing ids on a branch,
227
- # so resource do not get deleted when running an update on master (for example merge->CI)
228
- # - ideally we'd never add tracking in the first place, but when adding tracking we do not know the diff yet
229
- def prevent_irreversible_partial_updates
230
- return unless @project_filter # full update, so we are not on a branch
231
- @update.select! do |_, e, _, diff| # ensure clean diff, by removing noop-update
232
- next true unless e.id # safe to add tracking when not having id
233
-
234
- diff.select! do |field_diff|
235
- (_, field, actual) = field_diff
236
- # TODO: refactor this so TRACKING_FIELD stays record-private
237
- next true if e.class::TRACKING_FIELD != field.to_sym # need to sym here because Hashdiff produces strings
238
- next true if e.class.parse_tracking_id(field.to_sym => actual) # already has tracking id
239
-
240
- field_diff[3] = e.remove_tracking_id # make `rake plan` output match what we are sending
241
- actual != field_diff[3] # discard diff if now nothing changes
242
- end
243
-
244
- diff.any?
150
+ def validate_changes(internal_plan)
151
+ internal_plan.updates.each do |item|
152
+ item.expected.validate_update!(item.diff)
245
153
  end
246
154
  end
247
155
 
248
- def populate_id_map(expected, actual)
249
- # mark everything as new
250
- expected.each do |e|
251
- @id_map.set(e.class.api_resource, e.tracking_id, IdMap::NEW)
252
- if e.class.api_resource == "synthetics/tests"
253
- @id_map.set(Kennel::Models::Monitor.api_resource, e.tracking_id, IdMap::NEW)
254
- end
255
- end
256
-
257
- # override resources that exist with their id
258
- project_prefixes = @project_filter&.map { |p| "#{p}:" }
259
- actual.each do |a|
260
- # ignore when not managed by kennel
261
- next unless tracking_id = a.fetch(:tracking_id)
262
-
263
- # ignore when deleted from the codebase
264
- # (when running with filters we cannot see the other resources in the codebase)
265
- api_resource = a.fetch(:klass).api_resource
266
- next if
267
- !@id_map.get(api_resource, tracking_id) &&
268
- (!project_prefixes || tracking_id.start_with?(*project_prefixes)) &&
269
- (!@tracking_id_filter || @tracking_id_filter.include?(tracking_id))
270
-
271
- @id_map.set(api_resource, tracking_id, a.fetch(:id))
272
- if a.fetch(:klass).api_resource == "synthetics/tests"
273
- @id_map.set(Kennel::Models::Monitor.api_resource, tracking_id, a.fetch(:monitor_id))
274
- end
275
- end
276
- end
277
-
278
- def resolve_linked_tracking_ids!(list, force: false)
279
- list.each { |e| e.resolve_linked_tracking_ids!(@id_map, force: force) }
280
- end
281
-
282
156
  def filter_actual!(actual)
283
- if @tracking_id_filter
284
- actual.select! do |a|
285
- tracking_id = a.fetch(:tracking_id)
286
- !tracking_id || @tracking_id_filter.include?(tracking_id)
287
- end
288
- elsif @project_filter
289
- project_prefixes = @project_filter.map { |p| "#{p}:" }
290
- actual.select! do |a|
291
- tracking_id = a.fetch(:tracking_id)
292
- !tracking_id || tracking_id.start_with?(*project_prefixes)
293
- end
157
+ return unless filter.filtering? # minor optimization
158
+
159
+ actual.select! do |a|
160
+ tracking_id = a.fetch(:tracking_id)
161
+ tracking_id.nil? || filter.matches_tracking_id?(tracking_id)
294
162
  end
295
163
  end
296
164
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Kennel
3
- VERSION = "1.130.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.130.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: 2022-12-30 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