kennel 2.2.1 → 2.22.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.
data/lib/kennel/syncer.rb CHANGED
@@ -13,14 +13,13 @@ module Kennel
13
13
 
14
14
  attr_reader :plan
15
15
 
16
- def initialize(api, expected, actual, filter:, strict_imports: true)
16
+ def initialize(api, expected:, actual:, filter:, strict_imports: true)
17
17
  @api = api
18
18
  @strict_imports = strict_imports
19
19
  @filter = filter
20
20
 
21
21
  @resolver = Resolver.new(expected: expected, filter: filter)
22
22
  @plan = Plan.new(*calculate_changes(expected: expected, actual: actual))
23
- validate_changes
24
23
  end
25
24
 
26
25
  def print_plan
@@ -94,7 +93,8 @@ module Kennel
94
93
  convert_replace_into_update!(matching, unmatched_actual, unmatched_expected)
95
94
 
96
95
  validate_expected_id_not_missing unmatched_expected
97
- fill_details! matching # need details to diff later
96
+ uncached = fill_details! matching # need details to diff later
97
+ Kennel.out.puts "Uncached dashboard gets to fill details: #{uncached}" if ENV["SHOW_UNCACHED_FILL_DETAILS"]
98
98
 
99
99
  # update matching if needed
100
100
  updates = matching.map do |e, a|
@@ -126,21 +126,19 @@ module Kennel
126
126
  # if there is a new item that has the same name or title as an "to be deleted" item,
127
127
  # update it instead to avoid old urls from becoming invalid
128
128
  # - careful with unmatched_actual being huge since it has all api resources
129
- # - don't do it when a monitor type is changing since that would block the update
130
- # - when using a filter and updating the kennel_id of an existing item, old and new must be in the filter
129
+ # - don't do it when update is not allowed
130
+ # - when using a filter and updating the kennel_id of an existing item, old and new must be in the filter (PROJECT= works, but not TRACKING_ID)
131
131
  def convert_replace_into_update!(matching, unmatched_actual, unmatched_expected)
132
132
  unmatched_expected.reject! do |e|
133
- e_field, e_value = Kennel::Models::Record::TITLE_FIELDS.detect do |field|
134
- next unless (value = e.as_json[field])
135
- break [field, value]
136
- end
137
- raise unless e_field # uncovered: should never happen ...
138
- e_monitor_type = e.as_json[:type]
139
-
133
+ # find actual by title
134
+ e_field, e_value = title_field_and_value(e)
140
135
  actual = unmatched_actual.detect do |a|
141
- a[:klass].api_resource == e.class.api_resource && a[e_field] == e_value && a[:type] == e_monitor_type
136
+ a[:klass].api_resource == e.class.api_resource &&
137
+ a[e_field] == e_value
142
138
  end
143
- next false unless actual # keep in unmatched
139
+
140
+ # keep unmatched if we could not find or can't update
141
+ next false if !actual || e.allowed_update_error(actual)
144
142
 
145
143
  # add as update and remove from unmatched
146
144
  unmatched_actual.delete(actual)
@@ -150,10 +148,18 @@ module Kennel
150
148
  end
151
149
  end
152
150
 
153
- # fill details of things we need to compare
154
- def fill_details!(details_needed)
155
- details_needed = details_needed.map { |e, a| a if e && e.class.api_resource == "dashboard" }.compact
156
- @api.fill_details! "dashboard", details_needed
151
+ def title_field_and_value(e)
152
+ Kennel::Models::Record::TITLE_FIELDS.detect do |field|
153
+ next unless (value = e.as_json[field])
154
+ return [field, value]
155
+ end
156
+ raise # uncovered: should never happen ...
157
+ end
158
+
159
+ # fill details of things we need to compare, so diff works even though we cannot mass-fetch definitions
160
+ def fill_details!(matching)
161
+ dashboards = matching.filter_map { |e, a| a if e && e.class.api_resource == "dashboard" }
162
+ @api.fill_details! "dashboard", dashboards
157
163
  end
158
164
 
159
165
  def validate_expected_id_not_missing(expected)
@@ -169,20 +175,12 @@ module Kennel
169
175
  end
170
176
  end
171
177
 
172
- # We've already validated the desired objects ('generated') in isolation.
173
- # Now that we have made the plan, we can perform some more validation.
174
- def validate_changes
175
- @plan.updates.each do |item|
176
- item.expected.validate_update!(item.diff)
177
- end
178
- end
179
-
180
178
  def filter_actual!(actual)
181
179
  return unless filter.filtering? # minor optimization
182
180
 
183
181
  actual.select! do |a|
184
182
  tracking_id = a.fetch(:tracking_id)
185
- tracking_id.nil? || filter.matches_tracking_id?(tracking_id)
183
+ tracking_id.nil? || filter.filters_tracking_id?(tracking_id)
186
184
  end
187
185
  end
188
186
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :kennel do
4
+ desc "Dump ALL of datadog config as raw json ... useful for grep/search [TYPE=slo|monitor|dashboard]"
5
+ task dump: :environment do
6
+ resources =
7
+ if (type = ENV["TYPE"])
8
+ [type]
9
+ else
10
+ Kennel::Models::Record.api_resource_map.keys
11
+ end
12
+ api = Kennel::Api.new
13
+ list = nil
14
+ first = true
15
+
16
+ Kennel.out.puts "["
17
+ resources.each do |resource|
18
+ Kennel::Progress.progress("Downloading #{resource}") do
19
+ list = api.list(resource)
20
+ api.fill_details!(resource, list) if resource == "dashboard"
21
+ end
22
+ list.each do |r|
23
+ r[:api_resource] = resource
24
+ if first
25
+ first = false
26
+ else
27
+ Kennel.out.puts ","
28
+ end
29
+ Kennel.out.print JSON.pretty_generate(r)
30
+ end
31
+ end
32
+ Kennel.out.puts "\n]"
33
+ end
34
+
35
+ desc "Find items from dump by pattern DUMP= PATTERN= [URLS=true]"
36
+ task dump_grep: :environment do
37
+ file = ENV.fetch("DUMP")
38
+ pattern = Regexp.new ENV.fetch("PATTERN")
39
+ items = File.read(file)[2..-2].gsub("},\n{", "}--SPLIT--{").split("--SPLIT--")
40
+ models = Kennel::Models::Record.api_resource_map
41
+ found = items.grep(pattern)
42
+ exit 1 if found.empty?
43
+ found.each do |resource|
44
+ if ENV["URLS"]
45
+ parsed = JSON.parse(resource)
46
+ url = models[parsed.fetch("api_resource")].url(parsed.fetch("id"))
47
+ title = parsed["title"] || parsed["name"]
48
+ Kennel.out.puts "#{url} # #{title}"
49
+ else
50
+ Kennel.out.puts resource
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :kennel do
4
+ desc "Convert existing resources to copy-pasteable definitions to import existing resources (call with URL= or call with RESOURCE= and ID=)"
5
+ task import: :environment do
6
+ if (id = ENV["ID"]) && (resource = ENV["RESOURCE"])
7
+ id = Integer(id) if id =~ /^\d+$/
8
+ elsif (url = ENV["URL"])
9
+ resource, id = Kennel::Models::Record.parse_any_url(url) || Kennel::Tasks.abort("Unable to parse url")
10
+ else
11
+ possible_resources = Kennel::Models::Record.subclasses.map(&:api_resource)
12
+ Kennel::Tasks.abort("Call with URL= or call with RESOURCE=#{possible_resources.join(" or ")} and ID=")
13
+ end
14
+
15
+ Kennel.out.puts Kennel::Importer.new(Kennel::Api.new).import(resource, id)
16
+ end
17
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :kennel do
4
+ desc "show monitors with no data by TAG, for example TAG=team:foo [THRESHOLD_DAYS=7] [FORMAT=json]"
5
+ task nodata: :environment do
6
+ tag = ENV["TAG"] || Kennel::Tasks.abort("Call with TAG=foo:bar")
7
+ monitors = Kennel::Api.new.list("monitor", monitor_tags: tag, group_states: "no data")
8
+ monitors.select! { |m| m[:overall_state] == "No Data" }
9
+ monitors.reject! { |m| m[:tags].include? "nodata:ignore" }
10
+ if monitors.any?
11
+ Kennel.err.puts <<~TEXT
12
+ To ignore monitors with expected nodata, tag it with "nodata:ignore"
13
+
14
+ TEXT
15
+ end
16
+
17
+ now = Time.now
18
+ monitors.each do |m|
19
+ m[:days_in_no_data] =
20
+ if m[:overall_state_modified]
21
+ since = Date.parse(m[:overall_state_modified]).to_time
22
+ ((now - since) / (24 * 60 * 60)).to_i
23
+ else
24
+ 999
25
+ end
26
+ end
27
+
28
+ if (threshold = ENV["THRESHOLD_DAYS"])
29
+ monitors.select! { |m| m[:days_in_no_data] > Integer(threshold) }
30
+ end
31
+
32
+ monitors.each { |m| m[:url] = Kennel::Utils.path_to_url("/monitors/#{m[:id]}") }
33
+
34
+ if ENV["FORMAT"] == "json"
35
+ report = monitors.map do |m|
36
+ match = m[:message].to_s.match(/-- #{Regexp.escape(Kennel::Models::Record::MARKER_TEXT)} (\S+:\S+) in (\S+), /) || []
37
+ m.slice(:url, :name, :tags, :days_in_no_data).merge(
38
+ kennel_tracking_id: match[1],
39
+ kennel_source: match[2]
40
+ )
41
+ end
42
+
43
+ Kennel.out.puts JSON.pretty_generate(report)
44
+ else
45
+ monitors.each do |m|
46
+ Kennel.out.puts m[:name]
47
+ Kennel.out.puts Kennel::Utils.path_to_url("/monitors/#{m[:id]}")
48
+ Kennel.out.puts "No data since #{m[:days_in_no_data]}d"
49
+ Kennel.out.puts
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :kennel do
4
+ desc "Resolve given id to kennel tracking-id RESOURCE= ID="
5
+ task tracking_id: "kennel:environment" do
6
+ resource = ENV.fetch("RESOURCE")
7
+ id = ENV.fetch("ID")
8
+ klass =
9
+ Kennel::Models::Record.subclasses.detect { |s| s.api_resource == resource } ||
10
+ raise("resource #{resource} not know")
11
+ object = Kennel::Api.new.show(resource, id)
12
+ Kennel.out.puts klass.parse_tracking_id(object)
13
+ end
14
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :kennel do
4
+ desc "Verify that all used monitor mentions are valid"
5
+ task validate_mentions: :environment do
6
+ known = []
7
+
8
+ # @slack- @team- @webhook- @sns- user-emails
9
+ known += Kennel::Api.new.send(:request, :get, "/api/v2/notifications/handles?group_limit=99999")
10
+ .fetch(:data)
11
+ .flat_map { |d| d.dig(:attributes, :handles) }
12
+ .map { |v| v.fetch(:value) }
13
+
14
+ # group emails or other 1-off things we know are valid
15
+ manual = ENV["KNOWN"].to_s.split(",")
16
+ dupes = (manual & known)
17
+ Kennel::Tasks.abort "KNOWN=#{dupes.join(",")} values are already known and should be removed" if dupes.any?
18
+ known += manual
19
+
20
+ # @sns- handles are randomly invalid so we need to ignore them without checking if the ignore is needed
21
+ # https://help.datadoghq.com/hc/en-us/requests/2310423
22
+ known += ENV["KNOWN_RANDOM"].to_s.split(",")
23
+
24
+ bad = []
25
+ Dir["generated/**/*.json"].each do |f|
26
+ next unless (message = JSON.parse(File.read(f))["message"])
27
+ used = message
28
+ .scan(/(?:^|\s)(@[^\s{,'"]+)/)
29
+ .flatten(1)
30
+ .grep(/^@.*@|^@.*-/) # ignore @here etc handles ... datadog uses @foo@bar.com for emails and @foo-bar for integrations
31
+ (used - known).each { |v| bad << [f, v] }
32
+ end
33
+
34
+ if bad.any?
35
+ url = Kennel::Utils.path_to_url "/account/settings"
36
+ Kennel.err.puts "Invalid mentions found, either ignore them by adding to `KNOWN` env var or add them via #{url}"
37
+ bad.each { |f, v| Kennel.err.puts "Invalid mention #{v} in monitor message of #{f}" }
38
+ Kennel::Tasks.abort ENV["KNOWN_WARNING"]
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+ module Kennel
3
+ module ValidatePlan
4
+ class MonitorValidator
5
+ COSMETIC_FIELDS = ["name", "message", "tags"].freeze
6
+
7
+ def initialize(item)
8
+ @item = item
9
+ end
10
+
11
+ def validate(api)
12
+ data = @item.expected.as_json
13
+
14
+ # ignore unresolved ids from yet to be created monitors
15
+ return nil if ["composite", "slo alert"].include?(data[:type]) && data[:query].include?("%")
16
+
17
+ api.send(:request, :post, "/api/v1/monitor/validate", body: data)
18
+ nil
19
+ rescue StandardError => e
20
+ "#{Kennel::Console.color(:yellow, "#{@item.api_resource} #{@item.tracking_id}:")}\n#{e.message}"
21
+ end
22
+ end
23
+
24
+ class DashboardValidator
25
+ COSMETIC_FIELDS = ["title", "description", "tags"].freeze
26
+
27
+ def initialize(item)
28
+ @item = item
29
+ end
30
+
31
+ # datadog does not offer a validation api for dashboards,
32
+ # so we insert an invalid widget at the end and see if that is the invalid widget it complains about
33
+ # this will break if they ever start from the back or return errors for everything that is invalid
34
+ #
35
+ # we do not need to worry about unresolved ids because:
36
+ # - alert_graph widgets allows kennel style ids
37
+ # - slo widgets allow kennel style ids
38
+ # - uptime widgets allow kennel style ids
39
+ def validate(api)
40
+ json = @item.expected.as_json
41
+ json = Marshal.load(Marshal.dump(json))
42
+
43
+ # add a semi-valid (does not fail immediately on missing definition) widget still blocks the request
44
+ placeholder = "invalid_metric_do_not_update"
45
+ json.fetch(:widgets) << {
46
+ definition: {
47
+ type: "timeseries", requests: [{
48
+ response_format: "timeseries",
49
+ queries: [{ data_source: "metrics", name: "restarts", query: placeholder }]
50
+ }]
51
+ },
52
+ layout: { x: 0, y: 0, height: 0, width: 0 } # needed for `layout_type: free` and valid for all
53
+ }
54
+
55
+ begin
56
+ if @item.class::TYPE == :update
57
+ api.update("dashboard", @item.actual.fetch(:id), json)
58
+ else
59
+ api.create("dashboard", json)
60
+ end
61
+ raise "Dashboard validation should have failed, live dashboard was update/created by accident"
62
+ rescue StandardError => e
63
+ # parse the JSON in the error message and see if there is anything except our error
64
+ raise "Unreadable error format: #{e.message}" unless (json = e.message[/^\{"errors":.*}$/m])
65
+ data =
66
+ begin
67
+ JSON.parse(json)
68
+ rescue JSON::ParserError
69
+ raise "Unreadable error format: #{json}"
70
+ end
71
+ raise "Unreadable error format: #{data}" unless (errors = data["errors"]) # uncovered
72
+ return if errors.size == 1 && errors.all? { |m| m.include?("unable to parse #{placeholder}") }
73
+ "#{@item.tracking_id}: #{e.message}"
74
+ end
75
+ end
76
+ end
77
+
78
+ VALIDATORS = {
79
+ "monitor" => MonitorValidator,
80
+ "dashboard" => DashboardValidator
81
+ }.freeze
82
+
83
+ def self.validate(plan)
84
+ changes = (plan.creates + plan.updates)
85
+
86
+ validators = changes.filter_map do |item|
87
+ next unless (validator = VALIDATORS[item.api_resource]&.new(item))
88
+
89
+ if item.class::TYPE == :update
90
+ # ignore if nothing can break
91
+ modified_fields = item.diff.map { |_, f, *| f }
92
+ next nil if modified_fields.all? { |f| validator.class::COSMETIC_FIELDS.include?(f) }
93
+ end
94
+
95
+ validator
96
+ end
97
+
98
+ api = Kennel::Api.new
99
+ errors = validators.filter_map { |v| v.validate(api) }
100
+ return if errors.empty?
101
+
102
+ abort "#{Kennel::Console.color(:red, "#{errors.size} validation(s) failed:")}\n#{errors.join("\n")}"
103
+ end
104
+ end
105
+ end
106
+
107
+ namespace :kennel do
108
+ desc "Validate planned changes against the Datadog API [PROJECT=]"
109
+ task "validate_plan" => "kennel:environment" do
110
+ kennel = Kennel::Tasks.kennel
111
+ kennel.preload
112
+ Kennel::ValidatePlan.validate(kennel.plan)
113
+ end
114
+ end
data/lib/kennel/tasks.rb CHANGED
@@ -5,6 +5,8 @@ require "kennel/unmuted_alerts"
5
5
  require "kennel/importer"
6
6
  require "json"
7
7
 
8
+ Dir.children("#{__dir__}/tasks").each { |f| require_relative "tasks/#{File.basename(f, ".rb")}" }
9
+
8
10
  module Kennel
9
11
  module Tasks
10
12
  class << self
@@ -25,6 +27,7 @@ module Kennel
25
27
  source = ".env"
26
28
 
27
29
  # warn when users have things like DATADOG_TOKEN already set and it will not be loaded from .env
30
+ # (KENNEL_SILENCE_UPDATED_ENV is intentionally not documented - users see it when needed)
28
31
  unless ENV["KENNEL_SILENCE_UPDATED_ENV"]
29
32
  updated = Dotenv.parse(source).select { |k, v| ENV[k] && ENV[k] != v }
30
33
  warn "Environment variables #{updated.keys.join(", ")} need to be unset to be sourced from #{source}" if updated.any?
@@ -48,7 +51,11 @@ module Kennel
48
51
 
49
52
  def on_default_branch?
50
53
  branch = (ENV["TRAVIS_BRANCH"] || ENV["GITHUB_REF"]).to_s.sub(/^refs\/heads\//, "")
51
- (branch == (ENV["DEFAULT_BRANCH"] || "master"))
54
+ if (default = ENV["DEFAULT_BRANCH"])
55
+ branch == default
56
+ else
57
+ ["main", "master"].include?(branch)
58
+ end
52
59
  end
53
60
 
54
61
  def git_push?
@@ -66,38 +73,7 @@ namespace :kennel do
66
73
  Kennel::Tasks.abort "Error during diffing" unless $CHILD_STATUS.success?
67
74
  end
68
75
 
69
- # ideally do this on every run, but it's slow (~1.5s) and brittle (might not find all + might find false-positives)
70
- # https://help.datadoghq.com/hc/en-us/requests/254114 for automatic validation
71
- desc "Verify that all used monitor mentions are valid"
72
- task validate_mentions: :environment do
73
- known = Kennel::Api.new
74
- .send(:request, :get, "/monitor/notifications")
75
- .fetch(:handles)
76
- .values
77
- .flatten(1)
78
- .map { |v| v.fetch(:value) }
79
-
80
- known += ENV["KNOWN"].to_s.split(",")
81
-
82
- bad = []
83
- Dir["generated/**/*.json"].each do |f|
84
- next unless (message = JSON.parse(File.read(f))["message"])
85
- used = message
86
- .scan(/(?:^|\s)(@[^\s{,'"]+)/)
87
- .flatten(1)
88
- .grep(/^@.*@|^@.*-/) # ignore @here etc handles ... datadog uses @foo@bar.com for emails and @foo-bar for integrations
89
- (used - known).each { |v| bad << [f, v] }
90
- end
91
-
92
- if bad.any?
93
- url = Kennel::Utils.path_to_url "/account/settings"
94
- Kennel.err.puts "Invalid mentions found, either ignore them by adding to `KNOWN` env var or add them via #{url}"
95
- bad.each { |f, v| Kennel.err.puts "Invalid mention #{v} in monitor message of #{f}" }
96
- Kennel::Tasks.abort ENV["KNOWN_WARNING"]
97
- end
98
- end
99
-
100
- desc "generate local definitions"
76
+ desc "store definitions in generated/"
101
77
  task generate: :environment do
102
78
  Kennel::Tasks.kennel.generate
103
79
  end
@@ -129,132 +105,6 @@ namespace :kennel do
129
105
  Kennel::UnmutedAlerts.print(Kennel::Api.new, tag)
130
106
  end
131
107
 
132
- desc "show monitors with no data by TAG, for example TAG=team:foo [THRESHOLD_DAYS=7] [FORMAT=json]"
133
- task nodata: :environment do
134
- tag = ENV["TAG"] || Kennel::Tasks.abort("Call with TAG=foo:bar")
135
- monitors = Kennel::Api.new.list("monitor", monitor_tags: tag, group_states: "no data")
136
- monitors.select! { |m| m[:overall_state] == "No Data" }
137
- monitors.reject! { |m| m[:tags].include? "nodata:ignore" }
138
- if monitors.any?
139
- Kennel.err.puts <<~TEXT
140
- To ignore monitors with expected nodata, tag it with "nodata:ignore"
141
-
142
- TEXT
143
- end
144
-
145
- now = Time.now
146
- monitors.each do |m|
147
- m[:days_in_no_data] =
148
- if m[:overall_state_modified]
149
- since = Date.parse(m[:overall_state_modified]).to_time
150
- ((now - since) / (24 * 60 * 60)).to_i
151
- else
152
- 999
153
- end
154
- end
155
-
156
- if (threshold = ENV["THRESHOLD_DAYS"])
157
- monitors.select! { |m| m[:days_in_no_data] > Integer(threshold) }
158
- end
159
-
160
- monitors.each { |m| m[:url] = Kennel::Utils.path_to_url("/monitors/#{m[:id]}") }
161
-
162
- if ENV["FORMAT"] == "json"
163
- report = monitors.map do |m|
164
- match = m[:message].to_s.match(/-- #{Regexp.escape(Kennel::Models::Record::MARKER_TEXT)} (\S+:\S+) in (\S+), /) || []
165
- m.slice(:url, :name, :tags, :days_in_no_data).merge(
166
- kennel_tracking_id: match[1],
167
- kennel_source: match[2]
168
- )
169
- end
170
-
171
- Kennel.out.puts JSON.pretty_generate(report)
172
- else
173
- monitors.each do |m|
174
- Kennel.out.puts m[:name]
175
- Kennel.out.puts Kennel::Utils.path_to_url("/monitors/#{m[:id]}")
176
- Kennel.out.puts "No data since #{m[:days_in_no_data]}d"
177
- Kennel.out.puts
178
- end
179
- end
180
- end
181
-
182
- desc "Convert existing resources to copy-pasteable definitions to import existing resources (call with URL= or call with RESOURCE= and ID=)"
183
- task import: :environment do
184
- if (id = ENV["ID"]) && (resource = ENV["RESOURCE"])
185
- id = Integer(id) if id =~ /^\d+$/ # dashboards can have alphanumeric ids
186
- elsif (url = ENV["URL"])
187
- resource, id = Kennel::Models::Record.parse_any_url(url) || Kennel::Tasks.abort("Unable to parse url")
188
- else
189
- possible_resources = Kennel::Models::Record.subclasses.map(&:api_resource)
190
- Kennel::Tasks.abort("Call with URL= or call with RESOURCE=#{possible_resources.join(" or ")} and ID=")
191
- end
192
-
193
- Kennel.out.puts Kennel::Importer.new(Kennel::Api.new).import(resource, id)
194
- end
195
-
196
- desc "Dump ALL of datadog config as raw json ... useful for grep/search [TYPE=slo|monitor|dashboard]"
197
- task dump: :environment do
198
- resources =
199
- if (type = ENV["TYPE"])
200
- [type]
201
- else
202
- Kennel::Models::Record.api_resource_map.keys
203
- end
204
- api = Kennel::Api.new
205
- list = nil
206
- first = true
207
-
208
- Kennel.out.puts "["
209
- resources.each do |resource|
210
- Kennel::Progress.progress("Downloading #{resource}") do
211
- list = api.list(resource)
212
- api.fill_details!(resource, list) if resource == "dashboard"
213
- end
214
- list.each do |r|
215
- r[:api_resource] = resource
216
- if first
217
- first = false
218
- else
219
- Kennel.out.puts ","
220
- end
221
- Kennel.out.print JSON.pretty_generate(r)
222
- end
223
- end
224
- Kennel.out.puts "\n]"
225
- end
226
-
227
- desc "Find items from dump by pattern DUMP= PATTERN= [URLS=true]"
228
- task dump_grep: :environment do
229
- file = ENV.fetch("DUMP")
230
- pattern = Regexp.new ENV.fetch("PATTERN")
231
- items = File.read(file)[2..-2].gsub("},\n{", "}--SPLIT--{").split("--SPLIT--")
232
- models = Kennel::Models::Record.api_resource_map
233
- found = items.grep(pattern)
234
- exit 1 if found.empty?
235
- found.each do |resource|
236
- if ENV["URLS"]
237
- parsed = JSON.parse(resource)
238
- url = models[parsed.fetch("api_resource")].url(parsed.fetch("id"))
239
- title = parsed["title"] || parsed["name"]
240
- Kennel.out.puts "#{url} # #{title}"
241
- else
242
- Kennel.out.puts resource
243
- end
244
- end
245
- end
246
-
247
- desc "Resolve given id to kennel tracking-id RESOURCE= ID="
248
- task tracking_id: "kennel:environment" do
249
- resource = ENV.fetch("RESOURCE")
250
- id = ENV.fetch("ID")
251
- klass =
252
- Kennel::Models::Record.subclasses.detect { |s| s.api_resource == resource } ||
253
- raise("resource #{resource} not know")
254
- object = Kennel::Api.new.show(resource, id)
255
- Kennel.out.puts klass.parse_tracking_id(object)
256
- end
257
-
258
108
  task :environment do
259
109
  Kennel::Tasks.load_environment
260
110
  end
data/lib/kennel/utils.rb CHANGED
@@ -24,13 +24,14 @@ module Kennel
24
24
  workers = Array.new(threads).map do
25
25
  Thread.new do
26
26
  loop do
27
- item, i = work.pop
27
+ item, i = work.shift
28
28
  break unless i
29
29
  done[i] =
30
30
  begin
31
31
  yield item
32
32
  rescue Exception => e # rubocop:disable Lint/RescueException
33
- work.clear
33
+ work.clear # prevent new work
34
+ (workers - [Thread.current]).each(&:kill) # stop ongoing work
34
35
  e
35
36
  end
36
37
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Kennel
3
- VERSION = "2.2.1"
3
+ VERSION = "2.22.0"
4
4
  end
data/lib/kennel.rb CHANGED
@@ -56,7 +56,7 @@ module Kennel
56
56
  self.err = $stderr
57
57
 
58
58
  class Engine
59
- attr_accessor :strict_imports
59
+ attr_accessor :strict_imports # TODO: rename to :enforce_expected_ids_exist
60
60
 
61
61
  def initialize
62
62
  @strict_imports = true
@@ -69,7 +69,9 @@ module Kennel
69
69
 
70
70
  def generate
71
71
  parts = generated
72
- PartsSerializer.new(filter: filter).write(parts) if ENV["STORE"] != "false" # quicker when debugging
72
+ if ENV["STORE"] != "false" # quicker when debugging
73
+ PartsSerializer.new(filter: filter).write(parts)
74
+ end
73
75
  parts
74
76
  end
75
77
 
@@ -93,7 +95,9 @@ module Kennel
93
95
  @syncer ||= begin
94
96
  preload
95
97
  Syncer.new(
96
- api, generated, definitions,
98
+ api,
99
+ expected: generated,
100
+ actual: definitions,
97
101
  filter: filter,
98
102
  strict_imports: strict_imports
99
103
  )
@@ -107,8 +111,7 @@ module Kennel
107
111
  def generated(**kwargs)
108
112
  @generated ||= begin
109
113
  projects = Progress.progress "Loading projects", **kwargs do
110
- projects = ProjectsProvider.new(filter: filter).projects
111
- filter.filter_projects projects
114
+ ProjectsProvider.new(filter: filter).projects
112
115
  end
113
116
 
114
117
  parts = Progress.progress "Finding parts", **kwargs do
@@ -129,7 +132,7 @@ module Kennel
129
132
  end
130
133
  end
131
134
 
132
- # performance: this takes ~100ms on large codebases, tried rewriting with Set or Hash but it was slower
135
+ # performance: this takes ~100ms on large codebases, tried rewriting with Set or Hash, but it was slower
133
136
  def validate_unique_tracking_ids(parts)
134
137
  bad = parts.group_by(&:tracking_id).select { |_, same| same.size > 1 }
135
138
  return if bad.empty?
@@ -143,7 +146,10 @@ module Kennel
143
146
  def definitions(**kwargs)
144
147
  @definitions ||= Progress.progress("Downloading definitions", **kwargs) do
145
148
  Utils.parallel(Models::Record.subclasses) do |klass|
146
- api.list(klass.api_resource, with_downtimes: false) # lookup monitors without adding unnecessary downtime information
149
+ # lookup monitors without adding unnecessary downtime information
150
+ params = (klass.api_resource == "monitor" ? { with_downtimes: false } : {})
151
+
152
+ api.list(klass.api_resource, params)
147
153
  end.flatten(1)
148
154
  end
149
155
  end