kennel 1.118.2 → 1.120.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: 73ea25369567c46bcfd2af7ce3ac6c5b64754f9a16f5506d46c23edf9f8fbadf
4
- data.tar.gz: b07ae565f4c905e60b784688d15fa704bcbce470283632ff249e26ca51924a1a
3
+ metadata.gz: 5cbbb48418612701154d8a4aef95ef0267ca8993425b23977f550b91f185a0cb
4
+ data.tar.gz: d50f6890094e8af69346047cdae827ea5ca63d28e119c0de58792f7dc969a11d
5
5
  SHA512:
6
- metadata.gz: '079bf981abd354955807478b3e7ee4e90cdeb418053b1054271c5460421ba7b175afeb77c2b512e835890eff5cbbb121abf2be9ae3f4897376ff6d4cb2f0c772'
7
- data.tar.gz: 51444f447bec09ff09b5b854b0d759d270b8b72f9145d9e5d1bb40647215841384cad9a7d4e4079678fdedf7485dcea83c2b91a1af9de577ebdddf06d0b27424
6
+ metadata.gz: eac3443d6277ff33da2c1b4dec30ad389ecae8b6c211b65eb6e5b4c19404d2b32adc1299d6d207de58839b321f1343ee3186f2b488d375db92db411735e3bf69
7
+ data.tar.gz: 83e9083b77bbe269985986478a2a021ee32f9ac88712e662b3e817c6a70f1020ac68f0484944d8c9e7e4920c88440baf5e4cd111cc7be80bb943fdfd17f3a854
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kennel
4
+ class Filter
5
+ attr_reader :project_filter, :tracking_id_filter
6
+
7
+ def initialize
8
+ # build early so we fail fast on invalid user input
9
+ @tracking_id_filter = build_tracking_id_filter
10
+ @project_filter = build_project_filter
11
+ end
12
+
13
+ def filter_projects(projects)
14
+ filter_resources(projects, :kennel_id, project_filter, "projects", "PROJECT")
15
+ end
16
+
17
+ def filter_parts(parts)
18
+ filter_resources(parts, :tracking_id, tracking_id_filter, "resources", "TRACKING_ID")
19
+ end
20
+
21
+ private
22
+
23
+ def build_project_filter
24
+ project_names = ENV["PROJECT"]&.split(",")&.sort&.uniq
25
+ tracking_project_names = tracking_id_filter&.map { |id| id.split(":", 2).first }&.sort&.uniq
26
+ if project_names && tracking_project_names && project_names != tracking_project_names
27
+ raise "do not set PROJECT= when using TRACKING_ID="
28
+ end
29
+ (project_names || tracking_project_names)
30
+ end
31
+
32
+ def build_tracking_id_filter
33
+ (tracking_id = ENV["TRACKING_ID"]) && tracking_id.split(",").sort.uniq
34
+ end
35
+
36
+ def filter_resources(resources, by, expected, name, env)
37
+ return resources unless expected
38
+
39
+ expected = expected.uniq
40
+ before = resources.dup
41
+ resources = resources.select { |p| expected.include?(p.send(by)) }
42
+ keeping = resources.uniq(&by).size
43
+ return resources if keeping == expected.size
44
+
45
+ raise <<~MSG.rstrip
46
+ #{env}=#{expected.join(",")} matched #{keeping} #{name}, try any of these:
47
+ #{before.map(&by).sort.uniq.join("\n")}
48
+ MSG
49
+ end
50
+ end
51
+ end
@@ -27,11 +27,12 @@ module Kennel
27
27
  }.freeze
28
28
  DEFAULT_ESCALATION_MESSAGE = ["", nil].freeze
29
29
  ALLOWED_PRIORITY_CLASSES = [NilClass, Integer].freeze
30
+ ALLOWED_UNLINKED = [] # rubocop:disable Style/MutableConstant placeholder for custom overrides
30
31
 
31
32
  settings(
32
33
  :query, :name, :message, :escalation_message, :critical, :type, :renotify_interval, :warning, :timeout_h, :evaluation_delay,
33
34
  :ok, :no_data_timeframe, :notify_no_data, :notify_audit, :tags, :critical_recovery, :warning_recovery, :require_full_window,
34
- :threshold_windows, :new_host_delay, :new_group_delay, :priority
35
+ :threshold_windows, :new_host_delay, :new_group_delay, :priority, :validate_using_links
35
36
  )
36
37
 
37
38
  defaults(
@@ -238,6 +239,8 @@ module Kennel
238
239
  validate_message_variables(data)
239
240
  end
240
241
 
242
+ validate_using_links(data)
243
+
241
244
  if type == "service check" && !data[:query].to_s.include?(".by(")
242
245
  invalid! "query must include a .by() at least .by(\"*\")"
243
246
  end
@@ -286,6 +289,21 @@ module Kennel
286
289
  Group or filter the query by #{forbidden.map { |f| f.sub(".name", "") }.join(", ")} to use it.
287
290
  MSG
288
291
  end
292
+
293
+ def validate_using_links(data)
294
+ case data[:type]
295
+ when "composite" # TODO: add slo to mirror resolve_linked_tracking_ids! logic
296
+ ids = data[:query].tr("-", "_").scan(/\b\d+\b/)
297
+ ids.reject! { |id| ALLOWED_UNLINKED.include?([tracking_id, id]) }
298
+ if ids.any?
299
+ invalid! <<~MSG.rstrip
300
+ Used #{ids} in the query, but should only use links in the form of %{<project id>:<monitor id>}
301
+ If that is not possible, add `validate_using_links: ->(*){} # linked monitors are not in kennel
302
+ MSG
303
+ end
304
+ else # do nothing
305
+ end
306
+ end
289
307
  end
290
308
  end
291
309
  end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kennel
4
+ class PartsSerializer
5
+ def initialize(filter:)
6
+ @filter = filter
7
+ end
8
+
9
+ def write(parts)
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
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :filter
24
+
25
+ def write_changed(parts)
26
+ used = []
27
+
28
+ Utils.parallel(parts, max: 2) do |part|
29
+ path = "generated/#{part.tracking_id.tr("/", ":").sub(":", "/")}.json"
30
+
31
+ used << File.dirname(path) # only 1 level of sub folders, so this is enough
32
+ used << path
33
+
34
+ payload = part.as_json.merge(api_resource: part.class.api_resource)
35
+ write_file_if_necessary(path, JSON.pretty_generate(payload) << "\n")
36
+ end
37
+
38
+ used
39
+ end
40
+
41
+ def directories_to_clean_up
42
+ if filter.project_filter
43
+ filter.project_filter.map { |project| "generated/#{project}" }
44
+ else
45
+ ["generated"]
46
+ end
47
+ end
48
+
49
+ def old_paths
50
+ Dir["{#{directories_to_clean_up.join(",")}}/**/*"]
51
+ end
52
+
53
+ def write_file_if_necessary(path, content)
54
+ # 99% case
55
+ begin
56
+ return if File.read(path) == content
57
+ rescue Errno::ENOENT
58
+ FileUtils.mkdir_p(File.dirname(path))
59
+ end
60
+
61
+ # slow 1% case
62
+ File.write(path, content)
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+ module Kennel
3
+ class ProjectsProvider
4
+ def projects
5
+ load_all
6
+ Models::Project.recursive_subclasses.map(&:new)
7
+ end
8
+
9
+ private
10
+
11
+ def load_all
12
+ # load_all's purpose is to "require" all the .rb files under './projects',
13
+ # also with reference to ./teams and ./parts. What happens if you call it
14
+ # more than once?
15
+ #
16
+ # For a reason yet to be investigated, Zeitwerk rejects second and subsequent calls.
17
+ # But even if we skip over the Zeitwerk part, the nature of 'require' is
18
+ # somewhat one-way: we're not providing any mechanism to *un*load things.
19
+ # As long as the contents of `./projects`, `./teams` and `./parts` doesn't
20
+ # change between calls, then simply by no-op'ing subsequent calls to `load_all`
21
+ # we can have `load_all` appear to be idempotent.
22
+ loader = Zeitwerk::Loader.new
23
+ Dir.exist?("teams") && loader.push_dir("teams", namespace: Teams)
24
+ Dir.exist?("parts") && loader.push_dir("parts")
25
+ loader.setup
26
+ loader.eager_load # TODO: this should not be needed but we see hanging CI processes when it's not added
27
+
28
+ # TODO: also auto-load projects and update expected path too
29
+ ["projects"].each do |folder|
30
+ Dir["#{folder}/**/*.rb"].sort.each { |f| require "./#{f}" }
31
+ end
32
+ rescue NameError => e
33
+ message = e.message
34
+ raise unless klass = message[/uninitialized constant (.*)/, 1]
35
+
36
+ # inverse of zeitwerk lib/zeitwerk/inflector.rb
37
+ path = klass.gsub("::", "/").gsub(/([a-z])([A-Z])/, "\\1_\\2").downcase + ".rb"
38
+ expected_path = (path.start_with?("teams/") ? path : "parts/#{path}")
39
+
40
+ # TODO: prefer to raise a new exception with the old backtrace attacked
41
+ e.define_singleton_method(:message) do
42
+ "\n" + <<~MSG.gsub(/^/, " ")
43
+ #{message}
44
+ Unable to load #{klass} from #{expected_path}
45
+ - Option 1: rename the constant or the file it lives in, to make them match
46
+ - Option 2: Use `require` or `require_relative` to load the constant
47
+ MSG
48
+ end
49
+
50
+ raise
51
+ end
52
+ end
53
+ end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Kennel
3
- VERSION = "1.118.2"
3
+ VERSION = "1.120.0"
4
4
  end
data/lib/kennel.rb CHANGED
@@ -8,6 +8,9 @@ require "kennel/version"
8
8
  require "kennel/compatibility"
9
9
  require "kennel/utils"
10
10
  require "kennel/progress"
11
+ require "kennel/filter"
12
+ require "kennel/parts_serializer"
13
+ require "kennel/projects_provider"
11
14
  require "kennel/syncer"
12
15
  require "kennel/id_map"
13
16
  require "kennel/api"
@@ -54,9 +57,9 @@ module Kennel
54
57
  attr_accessor :out, :err, :strict_imports
55
58
 
56
59
  def generate
57
- out = generated
58
- store out if ENV["STORE"] != "false" # quicker when debugging
59
- out
60
+ parts = generated
61
+ parts_serializer.write(parts) if ENV["STORE"] != "false" # quicker when debugging
62
+ parts
60
63
  end
61
64
 
62
65
  def plan
@@ -74,63 +77,34 @@ module Kennel
74
77
 
75
78
  private
76
79
 
77
- def store(parts)
78
- Progress.progress "Storing" do
79
- old = Dir[[
80
- "generated",
81
- if project_filter || tracking_id_filter
82
- [
83
- "{" + (project_filter || ["*"]).join(",") + "}",
84
- "{" + (tracking_id_filter || ["*"]).join(",") + "}.json"
85
- ]
86
- else
87
- "**"
88
- end
89
- ].join("/")]
90
- used = []
91
-
92
- Utils.parallel(parts, max: 2) do |part|
93
- path = "generated/#{part.tracking_id.tr("/", ":").sub(":", "/")}.json"
94
- used.concat [File.dirname(path), path] # only 1 level of sub folders, so this is safe
95
- payload = part.as_json.merge(api_resource: part.class.api_resource)
96
- write_file_if_necessary(path, JSON.pretty_generate(payload) << "\n")
97
- end
98
-
99
- # deleting all is slow, so only delete the extras
100
- (old - used).each { |p| FileUtils.rm_rf(p) }
101
- end
102
- end
103
-
104
- def write_file_if_necessary(path, content)
105
- # 99% case
106
- begin
107
- return if File.read(path) == content
108
- rescue Errno::ENOENT
109
- FileUtils.mkdir_p(File.dirname(path))
110
- end
111
-
112
- # slow 1% case
113
- File.write(path, content)
80
+ def filter
81
+ @filter ||= Filter.new
114
82
  end
115
83
 
116
84
  def syncer
117
- @syncer ||= Syncer.new(api, generated, project_filter: project_filter, tracking_id_filter: tracking_id_filter)
85
+ @syncer ||= Syncer.new(api, generated, project_filter: filter.project_filter, tracking_id_filter: filter.tracking_id_filter)
118
86
  end
119
87
 
120
88
  def api
121
89
  @api ||= Api.new(ENV.fetch("DATADOG_APP_KEY"), ENV.fetch("DATADOG_API_KEY"))
122
90
  end
123
91
 
92
+ def projects_provider
93
+ @projects_provider ||= ProjectsProvider.new
94
+ end
95
+
96
+ def parts_serializer
97
+ @parts_serializer ||= PartsSerializer.new(filter: filter)
98
+ end
99
+
124
100
  def generated
125
101
  @generated ||= begin
126
102
  Progress.progress "Generating" do
127
- load_all
128
-
129
- projects = Models::Project.recursive_subclasses.map(&:new)
130
- filter_resources!(projects, :kennel_id, project_filter, "projects", "PROJECT")
103
+ projects = projects_provider.projects
104
+ projects = filter.filter_projects projects
131
105
 
132
106
  parts = Utils.parallel(projects, &:validated_parts).flatten(1)
133
- filter_resources!(parts, :tracking_id, tracking_id_filter, "resources", "TRACKING_ID")
107
+ parts = filter.filter_parts parts
134
108
 
135
109
  parts.group_by(&:tracking_id).each do |tracking_id, same|
136
110
  next if same.size == 1
@@ -147,74 +121,5 @@ module Kennel
147
121
  end
148
122
  end
149
123
  end
150
-
151
- def project_filter
152
- projects = ENV["PROJECT"]&.split(",")
153
- tracking_projects = tracking_id_filter&.map { |id| id.split(":", 2).first }
154
- if projects && tracking_projects && projects != tracking_projects
155
- raise "do not set PROJECT= when using TRACKING_ID="
156
- end
157
- projects || tracking_projects
158
- end
159
-
160
- def tracking_id_filter
161
- (tracking_id = ENV["TRACKING_ID"]) && tracking_id.split(",")
162
- end
163
-
164
- def filter_resources!(resources, by, against, name, env)
165
- return unless against
166
-
167
- before = resources.dup
168
- resources.select! { |p| against.include?(p.send(by)) }
169
- keeping = resources.uniq(&by).size
170
- return if keeping == against.size
171
-
172
- raise <<~MSG.rstrip
173
- #{env}=#{against.join(",")} matched #{keeping} #{name}, try any of these:
174
- #{before.map(&by).sort.uniq.join("\n")}
175
- MSG
176
- end
177
-
178
- def load_all
179
- # load_all's purpose is to "require" all the .rb files under './projects',
180
- # also with reference to ./teams and ./parts. What happens if you call it
181
- # more than once?
182
- #
183
- # For a reason yet to be investigated, Zeitwerk rejects second and subsequent calls.
184
- # But even if we skip over the Zeitwerk part, the nature of 'require' is
185
- # somewhat one-way: we're not providing any mechanism to *un*load things.
186
- # As long as the contents of `./projects`, `./teams` and `./parts` doesn't
187
- # change between calls, then simply by no-op'ing subsequent calls to `load_all`
188
- # we can have `load_all` appear to be idempotent.
189
- loader = Zeitwerk::Loader.new
190
- Dir.exist?("teams") && loader.push_dir("teams", namespace: Teams)
191
- Dir.exist?("parts") && loader.push_dir("parts")
192
- loader.setup
193
- loader.eager_load # TODO: this should not be needed but we see hanging CI processes when it's not added
194
-
195
- # TODO: also auto-load projects and update expected path too
196
- ["projects"].each do |folder|
197
- Dir["#{folder}/**/*.rb"].sort.each { |f| require "./#{f}" }
198
- end
199
- rescue NameError => e
200
- message = e.message
201
- raise unless klass = message[/uninitialized constant (.*)/, 1]
202
-
203
- # inverse of zeitwerk lib/zeitwerk/inflector.rb
204
- path = klass.gsub("::", "/").gsub(/([a-z])([A-Z])/, "\\1_\\2").downcase + ".rb"
205
- expected_path = (path.start_with?("teams/") ? path : "parts/#{path}")
206
-
207
- # TODO: prefer to raise a new exception with the old backtrace attacked
208
- e.define_singleton_method(:message) do
209
- "\n" + <<~MSG.gsub(/^/, " ")
210
- #{message}
211
- Unable to load #{klass} from #{expected_path}
212
- - Option 1: rename the constant or the file it lives in, to make them match
213
- - Option 2: Use `require` or `require_relative` to load the constant
214
- MSG
215
- end
216
-
217
- raise
218
- end
219
124
  end
220
125
  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.118.2
4
+ version: 1.120.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-09-23 00:00:00.000000000 Z
11
+ date: 2022-09-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs
@@ -91,6 +91,7 @@ files:
91
91
  - lib/kennel/api.rb
92
92
  - lib/kennel/compatibility.rb
93
93
  - lib/kennel/file_cache.rb
94
+ - lib/kennel/filter.rb
94
95
  - lib/kennel/github_reporter.rb
95
96
  - lib/kennel/id_map.rb
96
97
  - lib/kennel/importer.rb
@@ -103,7 +104,9 @@ files:
103
104
  - lib/kennel/models/synthetic_test.rb
104
105
  - lib/kennel/models/team.rb
105
106
  - lib/kennel/optional_validations.rb
107
+ - lib/kennel/parts_serializer.rb
106
108
  - lib/kennel/progress.rb
109
+ - lib/kennel/projects_provider.rb
107
110
  - lib/kennel/settings_as_methods.rb
108
111
  - lib/kennel/subclass_tracking.rb
109
112
  - lib/kennel/syncer.rb