kennel 1.124.0 → 1.126.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: 31edba322106d5b2f1942c57640f19eff6a265ea970b5bb2cc7f4ef02d1889fd
4
- data.tar.gz: f47474f8b7d745714fb49231755ed2bf41fae62cf8b5ff3817017c8cd8277713
3
+ metadata.gz: d8254e03670c1874a6f9b85da5fe453200cdcd9aabe2f82dafc424a6301d8416
4
+ data.tar.gz: 2d1c0b444762941f79fcba893f5cddc9d0119138c5cf0fc04d6fc871f2caf7a8
5
5
  SHA512:
6
- metadata.gz: 3dc6b1b72e834b7b7663b577c830dfa23da87a197752b04988edd2366ac9089685ff01e19a1e3c8244ac9a41f4217c75ec34bc6198ca83cf545bf6079069d71e
7
- data.tar.gz: 1dffcf6adbd06fe80117d9a5f9bc3b55fac90bc21abb4fdd4ebeb5b726ade944c799edbbcca3eabbc5a28beff6a302011ecfcce18f041b4ad2228076e164c2d6
6
+ metadata.gz: 32c9cbe7957e0f0652180d9b743e4498f3624231e96907f2aaafe5889b9d2f5cfcac98b21dce058c7e06cd6b719ae21b91d01b478eb21028a333b6e0383732a1
7
+ data.tar.gz: '068566cb8e89e9c9d993ed451cd174a301ce3976c2240aa8c12c1ebf20b602b9cf245b0eb28267dc9263930ad4659163cb75fee67af1d9c1094a649129fb7e17'
@@ -230,7 +230,9 @@ module Kennel
230
230
 
231
231
  # Avoid diff from datadog presets sorting.
232
232
  presets = data[:template_variable_presets]
233
- invalid! "template_variable_presets must be sorted by name" if presets && presets != presets.sort_by { |p| p[:name] }
233
+ if presets && presets != presets.sort_by { |p| p[:name] }
234
+ invalid! :template_variable_presets_must_be_sorted, "template_variable_presets must be sorted by name"
235
+ end
234
236
  end
235
237
 
236
238
  def render_definitions(definitions)
@@ -21,7 +21,8 @@ module Kennel
21
21
  renotify_interval: 0,
22
22
  notify_audit: false,
23
23
  no_data_timeframe: nil, # this works out ok since if notify_no_data is on, it would never be nil
24
- groupby_simple_monitor: false
24
+ groupby_simple_monitor: false,
25
+ variables: nil
25
26
  }.freeze
26
27
  DEFAULT_ESCALATION_MESSAGE = ["", nil].freeze
27
28
  ALLOWED_PRIORITY_CLASSES = [NilClass, Integer].freeze
@@ -30,7 +31,7 @@ module Kennel
30
31
  settings(
31
32
  :query, :name, :message, :escalation_message, :critical, :type, :renotify_interval, :warning, :timeout_h, :evaluation_delay,
32
33
  :ok, :no_data_timeframe, :notify_no_data, :notify_audit, :tags, :critical_recovery, :warning_recovery, :require_full_window,
33
- :threshold_windows, :new_host_delay, :new_group_delay, :priority, :validate_using_links
34
+ :threshold_windows, :new_host_delay, :new_group_delay, :priority, :validate_using_links, :variables
34
35
  )
35
36
 
36
37
  defaults(
@@ -50,7 +51,8 @@ module Kennel
50
51
  critical_recovery: -> { nil },
51
52
  warning_recovery: -> { nil },
52
53
  threshold_windows: -> { nil },
53
- priority: -> { MONITOR_DEFAULTS.fetch(:priority) }
54
+ priority: -> { MONITOR_DEFAULTS.fetch(:priority) },
55
+ variables: -> { MONITOR_OPTION_DEFAULTS.fetch(:variables) }
54
56
  )
55
57
 
56
58
  def build_json
@@ -73,7 +75,8 @@ module Kennel
73
75
  escalation_message: Utils.presence(escalation_message.strip),
74
76
  evaluation_delay: evaluation_delay,
75
77
  locked: false, # setting this to true prevents any edit and breaks updates when using replace workflow
76
- renotify_interval: renotify_interval || 0
78
+ renotify_interval: renotify_interval || 0,
79
+ variables: variables
77
80
  }
78
81
  )
79
82
 
@@ -211,26 +214,26 @@ module Kennel
211
214
  super
212
215
 
213
216
  if data[:name]&.start_with?(" ")
214
- invalid! "name cannot start with a space"
217
+ invalid! :name_must_not_start_with_space, "name cannot start with a space"
215
218
  end
216
219
 
217
220
  type = data.fetch(:type)
218
221
 
219
222
  # do not allow deprecated type that will be coverted by datadog and then produce a diff
220
223
  if type == "metric alert"
221
- invalid! "type 'metric alert' is deprecated, please set to a different type (e.g. 'query alert')"
224
+ invalid! :metric_alert_is_deprecated, "type 'metric alert' is deprecated, please set to a different type (e.g. 'query alert')"
222
225
  end
223
226
 
224
227
  # verify query includes critical value
225
228
  if query_value = data.fetch(:query)[/\s*[<>]=?\s*(\d+(\.\d+)?)\s*$/, 1]
226
229
  if Float(query_value) != Float(data.dig(:options, :thresholds, :critical))
227
- invalid! "critical and value used in query must match"
230
+ invalid! :critical_does_not_match_query, "critical and value used in query must match"
228
231
  end
229
232
  end
230
233
 
231
234
  # verify renotify interval is valid
232
235
  unless RENOTIFY_INTERVALS.include? data.dig(:options, :renotify_interval)
233
- invalid! "renotify_interval must be one of #{RENOTIFY_INTERVALS.join(", ")}"
236
+ invalid! :invalid_renotify_interval, "renotify_interval must be one of #{RENOTIFY_INTERVALS.join(", ")}"
234
237
  end
235
238
 
236
239
  if ["query alert", "service check"].include?(type) # TODO: most likely more types need this
@@ -240,15 +243,15 @@ module Kennel
240
243
  validate_using_links(data)
241
244
 
242
245
  if type == "service check" && !data[:query].to_s.include?(".by(")
243
- invalid! "query must include a .by() at least .by(\"*\")"
246
+ invalid! :query_must_include_by, "query must include a .by() at least .by(\"*\")"
244
247
  end
245
248
 
246
249
  unless ALLOWED_PRIORITY_CLASSES.include?(priority.class)
247
- invalid! "priority needs to be an Integer"
250
+ invalid! :invalid_priority, "priority needs to be an Integer"
248
251
  end
249
252
 
250
253
  if data.dig(:options, :timeout_h)&.> 24
251
- invalid! "timeout_h must be <= 24"
254
+ invalid! :invalid_timeout_h, "timeout_h must be <= 24"
252
255
  end
253
256
  end
254
257
 
@@ -283,7 +286,7 @@ module Kennel
283
286
  forbidden = used - allowed
284
287
  return if forbidden.empty?
285
288
 
286
- invalid! <<~MSG.rstrip
289
+ invalid! :invalid_variable_used_in_message, <<~MSG.rstrip
287
290
  Used #{forbidden.join(", ")} in the message, but can only be used with #{allowed.join(", ")}.
288
291
  Group or filter the query by #{forbidden.map { |f| f.sub(".name", "") }.join(", ")} to use it.
289
292
  MSG
@@ -295,7 +298,7 @@ module Kennel
295
298
  ids = data[:query].tr("-", "_").scan(/\b\d+\b/)
296
299
  ids.reject! { |id| ALLOWED_UNLINKED.include?([tracking_id, id]) }
297
300
  if ids.any?
298
- invalid! <<~MSG.rstrip
301
+ invalid! :links_must_be_via_tracking_id, <<~MSG.rstrip
299
302
  Used #{ids} in the query, but should only use links in the form of %{<project id>:<monitor id>}
300
303
  If that is not possible, add `validate_using_links: ->(*){} # linked monitors are not in kennel
301
304
  MSG
@@ -211,8 +211,8 @@ module Kennel
211
211
  end
212
212
  end
213
213
 
214
- def invalid!(message)
215
- unfiltered_validation_errors << ValidationMessage.new(message)
214
+ def invalid!(tag, message)
215
+ unfiltered_validation_errors << ValidationMessage.new(tag || OptionalValidations::UNIGNORABLE, message)
216
216
  end
217
217
 
218
218
  def raise_with_location(error, message)
@@ -2,7 +2,7 @@
2
2
  module Kennel
3
3
  module Models
4
4
  class Slo < Record
5
- READONLY_ATTRIBUTES = superclass::READONLY_ATTRIBUTES + [:type_id, :monitor_tags]
5
+ READONLY_ATTRIBUTES = superclass::READONLY_ATTRIBUTES + [:type_id, :monitor_tags, :target_threshold, :timeframe, :warning_threshold]
6
6
  TRACKING_FIELD = :description
7
7
  DEFAULTS = {
8
8
  description: nil,
@@ -85,7 +85,7 @@ module Kennel
85
85
  super
86
86
 
87
87
  if data[:thresholds].any? { |t| t[:warning] && t[:warning].to_f <= t[:critical].to_f }
88
- invalid! "Threshold warning must be greater-than critical value"
88
+ invalid! :warning_must_be_gt_critical, "Threshold warning must be greater-than critical value"
89
89
  end
90
90
  end
91
91
  end
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
  module Kennel
3
3
  module OptionalValidations
4
- ValidationMessage = Struct.new(:text)
4
+ ValidationMessage = Struct.new(:tag, :text)
5
+
6
+ UNIGNORABLE = :unignorable
5
7
 
6
8
  def self.included(base)
7
- base.settings :validate
8
- base.defaults(validate: -> { true })
9
+ base.settings :ignored_errors
10
+ base.defaults(ignored_errors: -> { [] })
9
11
  end
10
12
 
11
13
  def self.valid?(parts)
@@ -15,14 +17,26 @@ module Kennel
15
17
 
16
18
  return true if parts_with_errors.empty?
17
19
 
20
+ example_tag = nil
21
+
18
22
  Kennel.err.puts
19
23
  parts_with_errors.sort_by(&:safe_tracking_id).each do |part|
20
24
  part.filtered_validation_errors.each do |err|
21
- Kennel.err.puts "#{part.safe_tracking_id} #{err.text}"
25
+ Kennel.err.puts "#{part.safe_tracking_id} [#{err.tag.inspect}] #{err.text.gsub("\n", " ")}"
26
+ example_tag = err.tag unless err.tag == :unignorable
22
27
  end
23
28
  end
24
29
  Kennel.err.puts
25
30
 
31
+ Kennel.err.puts <<~MESSAGE if example_tag
32
+ If a particular error cannot be fixed, it can be marked as ignored via `ignored_errors`, e.g.:
33
+ Kennel::Models::Monitor.new(
34
+ ...,
35
+ ignored_errors: [#{example_tag.inspect}]
36
+ )
37
+
38
+ MESSAGE
39
+
26
40
  false
27
41
  end
28
42
 
@@ -32,6 +46,7 @@ module Kennel
32
46
  bad = Kennel::Utils.all_keys(data).grep_v(Symbol).sort.uniq
33
47
  return if bad.empty?
34
48
  invalid!(
49
+ :hash_keys_must_be_symbols,
35
50
  "Only use Symbols as hash keys to avoid permanent diffs when updating.\n" \
36
51
  "Change these keys to be symbols (usually 'foo' => 1 --> 'foo': 1)\n" \
37
52
  "#{bad.map(&:inspect).join("\n")}"
@@ -39,39 +54,30 @@ module Kennel
39
54
  end
40
55
 
41
56
  def filter_validation_errors
42
- if validate
43
- unfiltered_validation_errors
44
- elsif unfiltered_validation_errors.empty?
45
- msg = "`validate` is set to false, but there are no validation errors to suppress. Remove `validate: false`"
46
-
47
- mode = ENV.fetch("UNNECESSARY_VALIDATE_FALSE") do
48
- if ENV.key?("PROJECT") || ENV.key?("TRACKING_ID")
49
- "fail"
50
- else
51
- nil
52
- end
53
- end
54
-
55
- if mode == "fail"
56
- [ValidationMessage.new(msg)]
57
- else
58
- Kennel.out.puts "#{safe_tracking_id} #{msg}" if mode == "show"
57
+ if unfiltered_validation_errors.empty?
58
+ if ignored_errors.empty?
59
59
  []
60
+ else
61
+ [ValidationMessage.new(UNIGNORABLE, "`ignored_errors` is non-empty, but there are no errors to ignore. Remove `ignored_errors`")]
60
62
  end
61
63
  else
62
- mode = ENV.fetch("SUPPRESSED_ERRORS", "ignore")
63
-
64
- if mode == "fail"
65
- unfiltered_validation_errors
66
- else
67
- if mode == "show"
68
- unfiltered_validation_errors.each do |err|
69
- Kennel.out.puts "#{safe_tracking_id} `validate: false` suppressing error: #{err.text.gsub("\n", " ")}"
64
+ to_report =
65
+ if ENV["NO_IGNORED_ERRORS"]
66
+ # Turn off all suppressions, to see what errors are actually being suppressed
67
+ unfiltered_validation_errors
68
+ else
69
+ unfiltered_validation_errors.reject do |err|
70
+ err.tag != UNIGNORABLE && ignored_errors.include?(err.tag)
70
71
  end
71
72
  end
72
73
 
73
- []
74
+ unused_ignores = ignored_errors - unfiltered_validation_errors.map(&:tag)
75
+
76
+ unless unused_ignores.empty?
77
+ to_report << ValidationMessage.new(UNIGNORABLE, "Unused ignores #{unused_ignores.map(&:inspect).sort.uniq.join(" ")}. Remove these from `ignored_errors`")
74
78
  end
79
+
80
+ to_report
75
81
  end
76
82
  end
77
83
  end
@@ -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
@@ -6,14 +6,17 @@ module Kennel
6
6
  class Syncer
7
7
  DELETE_ORDER = ["dashboard", "slo", "monitor", "synthetics/tests"].freeze # dashboards references monitors + slos, slos reference monitors
8
8
  LINE_UP = "\e[1A\033[K" # go up and clear
9
+
9
10
  Plan = Struct.new(:changes, keyword_init: true)
11
+ Change = Struct.new(:type, :api_resource, :tracking_id, :id)
10
12
 
11
- def initialize(api, expected, kennel:, project_filter: nil, tracking_id_filter: nil)
13
+ def initialize(api, expected, actual, kennel:, project_filter: nil, tracking_id_filter: nil)
12
14
  @api = api
13
15
  @kennel = kennel
14
16
  @project_filter = project_filter
15
17
  @tracking_id_filter = tracking_id_filter
16
18
  @expected = Set.new expected # need set to speed up deletion
19
+ @actual = actual
17
20
  calculate_diff
18
21
  validate_plan
19
22
  prevent_irreversible_partial_updates
@@ -32,9 +35,9 @@ module Kennel
32
35
 
33
36
  Plan.new(
34
37
  changes:
35
- @create.map { |_id, e| [:create, e.class.api_resource, e.tracking_id, nil] } +
36
- @update.map { |_id, e| [:create, e.class.api_resource, e.tracking_id, nil] } +
37
- @delete.map { |_id, _e, a| [:delete, a.fetch(:klass).api_resource, a.fetch(:tracking_id), a.fetch(:id)] }
38
+ @create.map { |_id, e, _a| Change.new(:create, e.class.api_resource, e.tracking_id, nil) } +
39
+ @update.map { |id, e, _a| Change.new(:update, e.class.api_resource, e.tracking_id, id) } +
40
+ @delete.map { |id, _e, a| Change.new(:delete, a.fetch(:klass).api_resource, a.fetch(:tracking_id), id) }
38
41
  )
39
42
  end
40
43
 
@@ -51,9 +54,9 @@ module Kennel
51
54
  message = "#{e.class.api_resource} #{e.tracking_id}"
52
55
  Kennel.out.puts "Creating #{message}"
53
56
  reply = @api.create e.class.api_resource, e.as_json
54
- cache_metadata reply, e.class
57
+ Utils.inline_resource_metadata reply, e.class
55
58
  id = reply.fetch(:id)
56
- changes << [:create, e.class.api_resource, e.tracking_id, id]
59
+ changes << Change.new(:create, e.class.api_resource, e.tracking_id, id)
57
60
  populate_id_map [], [reply] # allow resolving ids we could previously no resolve
58
61
  Kennel.out.puts "#{LINE_UP}Created #{message} #{e.class.url(id)}"
59
62
  end
@@ -62,7 +65,7 @@ module Kennel
62
65
  message = "#{e.class.api_resource} #{e.tracking_id} #{e.class.url(id)}"
63
66
  Kennel.out.puts "Updating #{message}"
64
67
  @api.update e.class.api_resource, id, e.as_json
65
- changes << [:update, e.class.api_resource, e.tracking_id, id]
68
+ changes << Change.new(:update, e.class.api_resource, e.tracking_id, id)
66
69
  Kennel.out.puts "#{LINE_UP}Updated #{message}"
67
70
  end
68
71
 
@@ -71,7 +74,7 @@ module Kennel
71
74
  message = "#{klass.api_resource} #{a.fetch(:tracking_id)} #{id}"
72
75
  Kennel.out.puts "Deleting #{message}"
73
76
  @api.delete klass.api_resource, id
74
- changes << [:delete, klass.api_resource, a.fetch(:tracking_id), id]
77
+ changes << Change.new(:delete, klass.api_resource, a.fetch(:tracking_id), id)
75
78
  Kennel.out.puts "#{LINE_UP}Deleted #{message}"
76
79
  end
77
80
 
@@ -123,17 +126,15 @@ module Kennel
123
126
  @delete = []
124
127
  @id_map = IdMap.new
125
128
 
126
- actual = Progress.progress("Downloading definitions") { download_definitions }
127
-
128
129
  Progress.progress "Diffing" do
129
- populate_id_map @expected, actual
130
- filter_actual! actual
130
+ populate_id_map @expected, @actual
131
+ filter_actual! @actual
131
132
  resolve_linked_tracking_ids! @expected # resolve dependencies to avoid diff
132
133
 
133
134
  @expected.each(&:add_tracking_id) # avoid diff with actual
134
135
 
135
136
  lookup_map = matching_expected_lookup_map
136
- items = actual.map do |a|
137
+ items = @actual.map do |a|
137
138
  e = matching_expected(a, lookup_map)
138
139
  if e && @expected.delete?(e)
139
140
  [e, a]
@@ -166,19 +167,6 @@ module Kennel
166
167
  end
167
168
  end
168
169
 
169
- def download_definitions
170
- Utils.parallel(Models::Record.subclasses) do |klass|
171
- results = @api.list(klass.api_resource, with_downtimes: false) # lookup monitors without adding unnecessary downtime information
172
- results = results[results.keys.first] if results.is_a?(Hash) # dashboards are nested in {dashboards: []}
173
- results.each { |a| cache_metadata(a, klass) }
174
- end.flatten(1)
175
- end
176
-
177
- def cache_metadata(a, klass)
178
- a[:klass] = klass
179
- a[:tracking_id] = a.fetch(:klass).parse_tracking_id(a)
180
- end
181
-
182
170
  def ensure_all_ids_found
183
171
  @expected.each do |e|
184
172
  next unless id = e.id
data/lib/kennel/tasks.rb CHANGED
@@ -102,8 +102,9 @@ namespace :kennel do
102
102
 
103
103
  # also generate parts so users see and commit updated generated automatically
104
104
  desc "show planned datadog changes (scope with PROJECT=name)"
105
- task plan: :generate do
105
+ task plan: :environment do
106
106
  Kennel::Tasks.kennel.plan
107
+ Kennel::Tasks.kennel.generate
107
108
  end
108
109
 
109
110
  desc "update datadog (scope with PROJECT=name)"
@@ -29,8 +29,8 @@ module Kennel
29
29
  return if queries.empty?
30
30
 
31
31
  invalid!(
32
- "queries #{queries.join(", ")} must use the template variables #{variables.join(", ")}\n" \
33
- "If that is not possible, add `validate: -> { false } # query foo in bar does not have baz tag`"
32
+ :queries_must_use_template_variables,
33
+ "queries #{queries.join(", ")} must use the template variables #{variables.join(", ")}"
34
34
  )
35
35
  end
36
36
 
data/lib/kennel/utils.rb CHANGED
@@ -160,6 +160,11 @@ module Kennel
160
160
  end
161
161
  string
162
162
  end
163
+
164
+ def inline_resource_metadata(resource, klass)
165
+ resource[:klass] = klass
166
+ resource[:tracking_id] = klass.parse_tracking_id(resource)
167
+ end
163
168
  end
164
169
  end
165
170
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Kennel
3
- VERSION = "1.124.0"
3
+ VERSION = "1.126.0"
4
4
  end
data/lib/kennel.rb CHANGED
@@ -59,7 +59,7 @@ module Kennel
59
59
  attr_accessor :strict_imports
60
60
 
61
61
  def generate
62
- parts = generated
62
+ parts = generated(plain: false)
63
63
  parts_serializer.write(parts) if ENV["STORE"] != "false" # quicker when debugging
64
64
  parts
65
65
  end
@@ -75,12 +75,24 @@ module Kennel
75
75
 
76
76
  private
77
77
 
78
+ def download_definitions
79
+ Progress.progress("Downloading definitions", plain: true) do
80
+ Utils.parallel(Models::Record.subclasses) do |klass|
81
+ results = api.list(klass.api_resource, with_downtimes: false) # lookup monitors without adding unnecessary downtime information
82
+ results.each { |a| Utils.inline_resource_metadata(a, klass) }
83
+ end.flatten(1)
84
+ end
85
+ end
86
+
78
87
  def filter
79
88
  @filter ||= Filter.new
80
89
  end
81
90
 
82
91
  def syncer
83
- @syncer ||= Syncer.new(api, generated, kennel: self, project_filter: filter.project_filter, tracking_id_filter: filter.tracking_id_filter)
92
+ @syncer ||= begin
93
+ expected, actual = Utils.parallel([:generated, :download_definitions]) { |m| send m }
94
+ Syncer.new(api, expected, actual, kennel: self, project_filter: filter.project_filter, tracking_id_filter: filter.tracking_id_filter)
95
+ end
84
96
  end
85
97
 
86
98
  def api
@@ -95,9 +107,9 @@ module Kennel
95
107
  @parts_serializer ||= PartsSerializer.new(filter: filter)
96
108
  end
97
109
 
98
- def generated
110
+ def generated(plain: true)
99
111
  @generated ||= begin
100
- parts = Progress.progress "Finding parts" do
112
+ parts = Progress.progress "Finding parts", plain: plain do
101
113
  projects = projects_provider.projects
102
114
  projects = filter.filter_projects projects
103
115
 
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.124.0
4
+ version: 1.126.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-11-14 00:00:00.000000000 Z
11
+ date: 2022-12-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs