kennel 1.122.0 → 1.124.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 +4 -4
- data/lib/kennel/api.rb +4 -4
- data/lib/kennel/file_cache.rb +11 -11
- data/lib/kennel/importer.rb +3 -1
- data/lib/kennel/models/dashboard.rb +8 -16
- data/lib/kennel/models/monitor.rb +12 -14
- data/lib/kennel/models/project.rb +1 -6
- data/lib/kennel/models/record.rb +73 -13
- data/lib/kennel/models/slo.rb +15 -16
- data/lib/kennel/models/synthetic_test.rb +4 -11
- data/lib/kennel/optional_validations.rb +58 -1
- data/lib/kennel/syncer.rb +41 -30
- data/lib/kennel/tasks.rb +16 -12
- data/lib/kennel/utils.rb +6 -0
- data/lib/kennel/version.rb +1 -1
- data/lib/kennel.rb +30 -28
- metadata +2 -3
- data/lib/kennel/compatibility.rb +0 -27
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 31edba322106d5b2f1942c57640f19eff6a265ea970b5bb2cc7f4ef02d1889fd
|
4
|
+
data.tar.gz: f47474f8b7d745714fb49231755ed2bf41fae62cf8b5ff3817017c8cd8277713
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3dc6b1b72e834b7b7663b577c830dfa23da87a197752b04988edd2366ac9089685ff01e19a1e3c8244ac9a41f4217c75ec34bc6198ca83cf545bf6079069d71e
|
7
|
+
data.tar.gz: 1dffcf6adbd06fe80117d9a5f9bc3b55fac90bc21abb4fdd4ebeb5b726ade944c799edbbcca3eabbc5a28beff6a302011ecfcce18f041b4ad2228076e164c2d6
|
data/lib/kennel/api.rb
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
# encapsulates knowledge around how the api works
|
3
|
-
# especially 1-off weirdness that should not
|
3
|
+
# especially 1-off weirdness that should not leak into other parts of the code
|
4
4
|
module Kennel
|
5
5
|
class Api
|
6
6
|
CACHE_FILE = "tmp/cache/details"
|
7
7
|
|
8
|
-
def initialize(app_key, api_key)
|
9
|
-
@app_key = app_key
|
10
|
-
@api_key = api_key
|
8
|
+
def initialize(app_key = nil, api_key = nil)
|
9
|
+
@app_key = app_key || ENV.fetch("DATADOG_APP_KEY")
|
10
|
+
@api_key = api_key || ENV.fetch("DATADOG_API_KEY")
|
11
11
|
url = Utils.path_to_url("", subdomain: "app")
|
12
12
|
@client = Faraday.new(url: url) { |c| c.adapter :net_http_persistent }
|
13
13
|
end
|
data/lib/kennel/file_cache.rb
CHANGED
@@ -15,11 +15,13 @@ module Kennel
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def open
|
18
|
-
load_data
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
18
|
+
@data = load_data || {}
|
19
|
+
begin
|
20
|
+
expire_old_data
|
21
|
+
yield self
|
22
|
+
ensure
|
23
|
+
persist
|
24
|
+
end
|
23
25
|
end
|
24
26
|
|
25
27
|
def fetch(key, key_version)
|
@@ -35,12 +37,9 @@ module Kennel
|
|
35
37
|
private
|
36
38
|
|
37
39
|
def load_data
|
38
|
-
@
|
39
|
-
|
40
|
-
|
41
|
-
rescue StandardError
|
42
|
-
{}
|
43
|
-
end
|
40
|
+
Marshal.load(File.read(@file)) # rubocop:disable Security/MarshalLoad
|
41
|
+
rescue Errno::ENOENT, TypeError, ArgumentError
|
42
|
+
nil
|
44
43
|
end
|
45
44
|
|
46
45
|
def persist
|
@@ -49,6 +48,7 @@ module Kennel
|
|
49
48
|
|
50
49
|
Tempfile.create "kennel-file-cache", dir do |tmp|
|
51
50
|
Marshal.dump @data, tmp
|
51
|
+
tmp.flush
|
52
52
|
File.rename tmp.path, @file
|
53
53
|
end
|
54
54
|
end
|
data/lib/kennel/importer.rb
CHANGED
@@ -38,6 +38,8 @@ module Kennel
|
|
38
38
|
|
39
39
|
case resource
|
40
40
|
when "monitor"
|
41
|
+
raise "Import the synthetic test page and not the monitor" if data[:type] == "synthetics alert"
|
42
|
+
|
41
43
|
# flatten monitor options so they are all on the base which is how Monitor builds them
|
42
44
|
data.merge!(data.delete(:options))
|
43
45
|
data.merge!(data.delete(:thresholds) || {})
|
@@ -96,7 +98,7 @@ module Kennel
|
|
96
98
|
def link_composite_monitors(data)
|
97
99
|
if data[:type] == "composite"
|
98
100
|
data[:query].gsub!(/\d+/) do |id|
|
99
|
-
object =
|
101
|
+
object = @api.show("monitor", id)
|
100
102
|
tracking_id = Kennel::Models::Monitor.parse_tracking_id(object)
|
101
103
|
tracking_id ? "%{#{tracking_id}}" : id
|
102
104
|
rescue StandardError # monitor not found
|
@@ -3,7 +3,6 @@ module Kennel
|
|
3
3
|
module Models
|
4
4
|
class Dashboard < Record
|
5
5
|
include TemplateVariables
|
6
|
-
include OptionalValidations
|
7
6
|
|
8
7
|
READONLY_ATTRIBUTES = superclass::READONLY_ATTRIBUTES + [
|
9
8
|
:author_handle, :author_name, :modified_at, :deleted_at, :url, :is_read_only, :notify_list, :restricted_roles
|
@@ -87,8 +86,7 @@ module Kennel
|
|
87
86
|
tags: -> do # not inherited by default to make onboarding to using dashboard tags simple
|
88
87
|
team = project.team
|
89
88
|
team.tag_dashboards ? team.tags : []
|
90
|
-
end
|
91
|
-
id: -> { nil }
|
89
|
+
end
|
92
90
|
)
|
93
91
|
|
94
92
|
class << self
|
@@ -152,29 +150,24 @@ module Kennel
|
|
152
150
|
end
|
153
151
|
end
|
154
152
|
|
155
|
-
def
|
156
|
-
return @json if @json
|
153
|
+
def build_json
|
157
154
|
all_widgets = render_definitions(definitions) + widgets
|
158
155
|
expand_q all_widgets
|
159
156
|
tags = tags()
|
160
157
|
tags_as_string = (tags.empty? ? "" : " (#{tags.join(" ")})")
|
161
158
|
|
162
|
-
|
159
|
+
json = super.merge(
|
163
160
|
layout_type: layout_type,
|
164
161
|
title: "#{title}#{tags_as_string}#{LOCK}",
|
165
162
|
description: description,
|
166
163
|
template_variables: render_template_variables,
|
167
164
|
template_variable_presets: template_variable_presets,
|
168
165
|
widgets: all_widgets
|
169
|
-
|
170
|
-
|
171
|
-
@json[:reflow_type] = reflow_type if reflow_type # setting nil breaks create with "ordered"
|
166
|
+
)
|
172
167
|
|
173
|
-
|
168
|
+
json[:reflow_type] = reflow_type if reflow_type # setting nil breaks create with "ordered"
|
174
169
|
|
175
|
-
|
176
|
-
|
177
|
-
@json
|
170
|
+
json
|
178
171
|
end
|
179
172
|
|
180
173
|
def self.url(id)
|
@@ -210,9 +203,8 @@ module Kennel
|
|
210
203
|
end
|
211
204
|
|
212
205
|
def validate_update!(_actuals, diffs)
|
213
|
-
|
214
|
-
|
215
|
-
end
|
206
|
+
_, path, from, to = diffs.find { |diff| diff[1] == "layout_type" }
|
207
|
+
invalid_update!(path, from, to) if path
|
216
208
|
end
|
217
209
|
|
218
210
|
private
|
@@ -2,8 +2,6 @@
|
|
2
2
|
module Kennel
|
3
3
|
module Models
|
4
4
|
class Monitor < Record
|
5
|
-
include OptionalValidations
|
6
|
-
|
7
5
|
RENOTIFY_INTERVALS = [0, 10, 20, 30, 40, 50, 60, 90, 120, 180, 240, 300, 360, 720, 1440].freeze # minutes
|
8
6
|
OPTIONAL_SERVICE_CHECK_THRESHOLDS = [:ok, :warning].freeze
|
9
7
|
READONLY_ATTRIBUTES = superclass::READONLY_ATTRIBUTES + [
|
@@ -41,8 +39,7 @@ module Kennel
|
|
41
39
|
renotify_interval: -> { project.team.renotify_interval },
|
42
40
|
warning: -> { nil },
|
43
41
|
ok: -> { nil },
|
44
|
-
|
45
|
-
notify_no_data: -> { true }, # datadog sets this to false by default, but true is the safer
|
42
|
+
notify_no_data: -> { true }, # datadog UI sets this to false by default, but true is safer
|
46
43
|
no_data_timeframe: -> { 60 },
|
47
44
|
notify_audit: -> { MONITOR_OPTION_DEFAULTS.fetch(:notify_audit) },
|
48
45
|
new_host_delay: -> { MONITOR_OPTION_DEFAULTS.fetch(:new_host_delay) },
|
@@ -56,9 +53,8 @@ module Kennel
|
|
56
53
|
priority: -> { MONITOR_DEFAULTS.fetch(:priority) }
|
57
54
|
)
|
58
55
|
|
59
|
-
def
|
60
|
-
|
61
|
-
data = {
|
56
|
+
def build_json
|
57
|
+
data = super.merge(
|
62
58
|
name: "#{name}#{LOCK}",
|
63
59
|
type: type,
|
64
60
|
query: query.strip,
|
@@ -79,9 +75,7 @@ module Kennel
|
|
79
75
|
locked: false, # setting this to true prevents any edit and breaks updates when using replace workflow
|
80
76
|
renotify_interval: renotify_interval || 0
|
81
77
|
}
|
82
|
-
|
83
|
-
|
84
|
-
data[:id] = id if id
|
78
|
+
)
|
85
79
|
|
86
80
|
options = data[:options]
|
87
81
|
if data.fetch(:type) != "composite"
|
@@ -105,6 +99,12 @@ module Kennel
|
|
105
99
|
end
|
106
100
|
end
|
107
101
|
|
102
|
+
# setting this via the api breaks the UI with
|
103
|
+
# "The no_data_timeframe option is not allowed for log alert monitors"
|
104
|
+
if data.fetch(:type) == "log alert"
|
105
|
+
options.delete :no_data_timeframe
|
106
|
+
end
|
107
|
+
|
108
108
|
if windows = threshold_windows
|
109
109
|
options[:threshold_windows] = windows
|
110
110
|
end
|
@@ -120,9 +120,7 @@ module Kennel
|
|
120
120
|
options[:renotify_statuses] = statuses
|
121
121
|
end
|
122
122
|
|
123
|
-
|
124
|
-
|
125
|
-
@as_json = data
|
123
|
+
data
|
126
124
|
end
|
127
125
|
|
128
126
|
def resolve_linked_tracking_ids!(id_map, **args)
|
@@ -140,7 +138,7 @@ module Kennel
|
|
140
138
|
# ensure type does not change, but not if it's metric->query which is supported and used by importer.rb
|
141
139
|
_, path, from, to = diffs.detect { |_, path, _, _| path == "type" }
|
142
140
|
if path && !(from == "metric alert" && to == "query alert")
|
143
|
-
|
141
|
+
invalid_update!(path, from, to)
|
144
142
|
end
|
145
143
|
end
|
146
144
|
|
@@ -20,7 +20,7 @@ module Kennel
|
|
20
20
|
def validated_parts
|
21
21
|
all = parts
|
22
22
|
unless all.is_a?(Array) && all.all? { |part| part.is_a?(Record) }
|
23
|
-
|
23
|
+
raise "Project #{kennel_id} #parts must return an array of Records"
|
24
24
|
end
|
25
25
|
|
26
26
|
validate_parts(all)
|
@@ -29,11 +29,6 @@ module Kennel
|
|
29
29
|
|
30
30
|
private
|
31
31
|
|
32
|
-
# let users know which project/resource failed when something happens during diffing where the backtrace is hidden
|
33
|
-
def invalid!(message)
|
34
|
-
raise ValidationError, "#{kennel_id} #{message}"
|
35
|
-
end
|
36
|
-
|
37
32
|
# hook for users to add custom validations via `prepend`
|
38
33
|
def validate_parts(parts)
|
39
34
|
end
|
data/lib/kennel/models/record.rb
CHANGED
@@ -2,6 +2,16 @@
|
|
2
2
|
module Kennel
|
3
3
|
module Models
|
4
4
|
class Record < Base
|
5
|
+
class PrepareError < StandardError
|
6
|
+
def initialize(tracking_id)
|
7
|
+
super("Error while preparing #{tracking_id}")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
UnvalidatedRecordError = Class.new(StandardError)
|
12
|
+
|
13
|
+
include OptionalValidations
|
14
|
+
|
5
15
|
# Apart from if you just don't like the default for some reason,
|
6
16
|
# overriding MARKER_TEXT allows for namespacing within the same
|
7
17
|
# Datadog account. If you run one Kennel setup with marker text
|
@@ -30,6 +40,8 @@ module Kennel
|
|
30
40
|
|
31
41
|
settings :id, :kennel_id
|
32
42
|
|
43
|
+
defaults(id: nil)
|
44
|
+
|
33
45
|
class << self
|
34
46
|
def parse_any_url(url)
|
35
47
|
subclasses.detect do |s|
|
@@ -72,7 +84,7 @@ module Kennel
|
|
72
84
|
end
|
73
85
|
end
|
74
86
|
|
75
|
-
attr_reader :project
|
87
|
+
attr_reader :project, :unfiltered_validation_errors, :filtered_validation_errors
|
76
88
|
|
77
89
|
def initialize(project, *args)
|
78
90
|
raise ArgumentError, "First argument must be a project, not #{project.class}" unless project.is_a?(Project)
|
@@ -96,7 +108,7 @@ module Kennel
|
|
96
108
|
@tracking_id ||= begin
|
97
109
|
id = "#{project.kennel_id}:#{kennel_id}"
|
98
110
|
unless id.match?(ALLOWED_KENNEL_ID_REGEX) # <-> parse_tracking_id
|
99
|
-
raise
|
111
|
+
raise "Bad kennel/tracking id: #{id.inspect} must match #{ALLOWED_KENNEL_ID_REGEX}"
|
100
112
|
end
|
101
113
|
id
|
102
114
|
end
|
@@ -108,7 +120,7 @@ module Kennel
|
|
108
120
|
def add_tracking_id
|
109
121
|
json = as_json
|
110
122
|
if self.class.parse_tracking_id(json)
|
111
|
-
|
123
|
+
raise "#{safe_tracking_id} Remove \"-- #{MARKER_TEXT}\" line from #{self.class::TRACKING_FIELD} to copy a resource"
|
112
124
|
end
|
113
125
|
json[self.class::TRACKING_FIELD] =
|
114
126
|
"#{json[self.class::TRACKING_FIELD]}\n" \
|
@@ -119,9 +131,54 @@ module Kennel
|
|
119
131
|
self.class.remove_tracking_id(as_json)
|
120
132
|
end
|
121
133
|
|
134
|
+
def build_json
|
135
|
+
{
|
136
|
+
id: id
|
137
|
+
}.compact
|
138
|
+
end
|
139
|
+
|
140
|
+
def build
|
141
|
+
@unfiltered_validation_errors = []
|
142
|
+
json = nil
|
143
|
+
|
144
|
+
begin
|
145
|
+
json = build_json
|
146
|
+
(id = json.delete(:id)) && json[:id] = id
|
147
|
+
validate_json(json)
|
148
|
+
rescue StandardError
|
149
|
+
if unfiltered_validation_errors.empty?
|
150
|
+
@unfiltered_validation_errors = nil
|
151
|
+
raise PrepareError, safe_tracking_id
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
@filtered_validation_errors = filter_validation_errors
|
156
|
+
@as_json = json # Only valid if filtered_validation_errors.empty?
|
157
|
+
end
|
158
|
+
|
159
|
+
def as_json
|
160
|
+
# A courtesy to those tests that still expect as_json to perform validation and raise on error
|
161
|
+
build if @unfiltered_validation_errors.nil?
|
162
|
+
raise UnvalidatedRecordError, "#{safe_tracking_id} as_json called on invalid part" unless filtered_validation_errors.empty?
|
163
|
+
|
164
|
+
@as_json
|
165
|
+
end
|
166
|
+
|
167
|
+
# Can raise DisallowedUpdateError
|
122
168
|
def validate_update!(*)
|
123
169
|
end
|
124
170
|
|
171
|
+
def invalid_update!(field, old_value, new_value)
|
172
|
+
raise DisallowedUpdateError, "#{safe_tracking_id} Datadog does not allow update of #{field} (#{old_value.inspect} -> #{new_value.inspect})"
|
173
|
+
end
|
174
|
+
|
175
|
+
# For use during error handling
|
176
|
+
def safe_tracking_id
|
177
|
+
tracking_id
|
178
|
+
rescue StandardError
|
179
|
+
"<unknown; #tracking_id crashed>"
|
180
|
+
end
|
181
|
+
|
125
182
|
private
|
126
183
|
|
127
184
|
def resolve(value, type, id_map, force:)
|
@@ -133,26 +190,29 @@ module Kennel
|
|
133
190
|
id.is_a?(String) && id.include?(":")
|
134
191
|
end
|
135
192
|
|
136
|
-
def resolve_link(
|
137
|
-
if id_map.new?(
|
193
|
+
def resolve_link(sought_tracking_id, sought_type, id_map, force:)
|
194
|
+
if id_map.new?(sought_type.to_s, sought_tracking_id)
|
138
195
|
if force
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
196
|
+
raise UnresolvableIdError, <<~MESSAGE
|
197
|
+
#{tracking_id} #{sought_type} #{sought_tracking_id} was referenced but is also created by the current run.
|
198
|
+
It could not be created because of a circular dependency. Try creating only some of the resources.
|
199
|
+
MESSAGE
|
143
200
|
else
|
144
201
|
nil # will be re-resolved after the linked object was created
|
145
202
|
end
|
146
|
-
elsif id = id_map.get(
|
203
|
+
elsif id = id_map.get(sought_type.to_s, sought_tracking_id)
|
147
204
|
id
|
148
205
|
else
|
149
|
-
|
206
|
+
raise UnresolvableIdError, <<~MESSAGE
|
207
|
+
#{tracking_id} Unable to find #{sought_type} #{sought_tracking_id}
|
208
|
+
This is either because it doesn't exist, and isn't being created by the current run;
|
209
|
+
or it does exist, but is being deleted.
|
210
|
+
MESSAGE
|
150
211
|
end
|
151
212
|
end
|
152
213
|
|
153
|
-
# let users know which project/resource failed when something happens during diffing where the backtrace is hidden
|
154
214
|
def invalid!(message)
|
155
|
-
|
215
|
+
unfiltered_validation_errors << ValidationMessage.new(message)
|
156
216
|
end
|
157
217
|
|
158
218
|
def raise_with_location(error, message)
|
data/lib/kennel/models/slo.rb
CHANGED
@@ -15,7 +15,6 @@ module Kennel
|
|
15
15
|
settings :type, :description, :thresholds, :query, :tags, :monitor_ids, :monitor_tags, :name, :groups
|
16
16
|
|
17
17
|
defaults(
|
18
|
-
id: -> { nil },
|
19
18
|
tags: -> { @project.tags },
|
20
19
|
query: -> { DEFAULTS.fetch(:query) },
|
21
20
|
description: -> { DEFAULTS.fetch(:description) },
|
@@ -24,35 +23,25 @@ module Kennel
|
|
24
23
|
groups: -> { DEFAULTS.fetch(:groups) }
|
25
24
|
)
|
26
25
|
|
27
|
-
def
|
28
|
-
super
|
29
|
-
if thresholds.any? { |t| t[:warning] && t[:warning].to_f <= t[:critical].to_f }
|
30
|
-
raise ValidationError, "Threshold warning must be greater-than critical value"
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
def as_json
|
35
|
-
return @as_json if @as_json
|
36
|
-
data = {
|
26
|
+
def build_json
|
27
|
+
data = super.merge(
|
37
28
|
name: "#{name}#{LOCK}",
|
38
29
|
description: description,
|
39
30
|
thresholds: thresholds,
|
40
31
|
monitor_ids: monitor_ids,
|
41
32
|
tags: tags.uniq,
|
42
33
|
type: type
|
43
|
-
|
34
|
+
)
|
44
35
|
|
45
36
|
if v = query
|
46
37
|
data[:query] = v
|
47
38
|
end
|
48
|
-
|
49
|
-
data[:id] = v
|
50
|
-
end
|
39
|
+
|
51
40
|
if v = groups
|
52
41
|
data[:groups] = v
|
53
42
|
end
|
54
43
|
|
55
|
-
|
44
|
+
data
|
56
45
|
end
|
57
46
|
|
58
47
|
def self.api_resource
|
@@ -89,6 +78,16 @@ module Kennel
|
|
89
78
|
|
90
79
|
ignore_default(expected, actual, DEFAULTS)
|
91
80
|
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def validate_json(data)
|
85
|
+
super
|
86
|
+
|
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"
|
89
|
+
end
|
90
|
+
end
|
92
91
|
end
|
93
92
|
end
|
94
93
|
end
|
@@ -11,15 +11,14 @@ module Kennel
|
|
11
11
|
settings :tags, :config, :message, :subtype, :type, :name, :locations, :options
|
12
12
|
|
13
13
|
defaults(
|
14
|
-
id: -> { nil },
|
15
14
|
tags: -> { @project.tags },
|
16
15
|
message: -> { "\n\n#{project.mention}" }
|
17
16
|
)
|
18
17
|
|
19
|
-
def
|
20
|
-
return @as_json if @as_json
|
18
|
+
def build_json
|
21
19
|
locations = locations()
|
22
|
-
|
20
|
+
|
21
|
+
super.merge(
|
23
22
|
message: message,
|
24
23
|
tags: tags,
|
25
24
|
config: config,
|
@@ -28,13 +27,7 @@ module Kennel
|
|
28
27
|
options: options,
|
29
28
|
name: "#{name}#{LOCK}",
|
30
29
|
locations: locations == :all ? LOCATIONS : locations
|
31
|
-
|
32
|
-
|
33
|
-
if v = id
|
34
|
-
data[:id] = v
|
35
|
-
end
|
36
|
-
|
37
|
-
@as_json = data
|
30
|
+
)
|
38
31
|
end
|
39
32
|
|
40
33
|
def self.api_resource
|
@@ -1,15 +1,35 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module Kennel
|
3
3
|
module OptionalValidations
|
4
|
+
ValidationMessage = Struct.new(:text)
|
5
|
+
|
4
6
|
def self.included(base)
|
5
7
|
base.settings :validate
|
6
8
|
base.defaults(validate: -> { true })
|
7
9
|
end
|
8
10
|
|
11
|
+
def self.valid?(parts)
|
12
|
+
parts_with_errors = parts.reject do |part|
|
13
|
+
part.filtered_validation_errors.empty?
|
14
|
+
end
|
15
|
+
|
16
|
+
return true if parts_with_errors.empty?
|
17
|
+
|
18
|
+
Kennel.err.puts
|
19
|
+
parts_with_errors.sort_by(&:safe_tracking_id).each do |part|
|
20
|
+
part.filtered_validation_errors.each do |err|
|
21
|
+
Kennel.err.puts "#{part.safe_tracking_id} #{err.text}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
Kennel.err.puts
|
25
|
+
|
26
|
+
false
|
27
|
+
end
|
28
|
+
|
9
29
|
private
|
10
30
|
|
11
31
|
def validate_json(data)
|
12
|
-
bad = Kennel::Utils.all_keys(data).grep_v(Symbol)
|
32
|
+
bad = Kennel::Utils.all_keys(data).grep_v(Symbol).sort.uniq
|
13
33
|
return if bad.empty?
|
14
34
|
invalid!(
|
15
35
|
"Only use Symbols as hash keys to avoid permanent diffs when updating.\n" \
|
@@ -17,5 +37,42 @@ module Kennel
|
|
17
37
|
"#{bad.map(&:inspect).join("\n")}"
|
18
38
|
)
|
19
39
|
end
|
40
|
+
|
41
|
+
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"
|
59
|
+
[]
|
60
|
+
end
|
61
|
+
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", " ")}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
[]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
20
77
|
end
|
21
78
|
end
|
data/lib/kennel/syncer.rb
CHANGED
@@ -6,12 +6,11 @@ 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
|
+
Plan = Struct.new(:changes, keyword_init: true)
|
9
10
|
|
10
|
-
|
11
|
-
Update = Struct.new(:update_log, keyword_init: true)
|
12
|
-
|
13
|
-
def initialize(api, expected, project_filter: nil, tracking_id_filter: nil)
|
11
|
+
def initialize(api, expected, kennel:, project_filter: nil, tracking_id_filter: nil)
|
14
12
|
@api = api
|
13
|
+
@kennel = kennel
|
15
14
|
@project_filter = project_filter
|
16
15
|
@tracking_id_filter = tracking_id_filter
|
17
16
|
@expected = Set.new expected # need set to speed up deletion
|
@@ -32,11 +31,10 @@ module Kennel
|
|
32
31
|
end
|
33
32
|
|
34
33
|
Plan.new(
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
delete: @delete
|
34
|
+
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)] }
|
40
38
|
)
|
41
39
|
end
|
42
40
|
|
@@ -47,7 +45,7 @@ module Kennel
|
|
47
45
|
end
|
48
46
|
|
49
47
|
def update
|
50
|
-
|
48
|
+
changes = []
|
51
49
|
|
52
50
|
each_resolved @create do |_, e|
|
53
51
|
message = "#{e.class.api_resource} #{e.tracking_id}"
|
@@ -55,7 +53,7 @@ module Kennel
|
|
55
53
|
reply = @api.create e.class.api_resource, e.as_json
|
56
54
|
cache_metadata reply, e.class
|
57
55
|
id = reply.fetch(:id)
|
58
|
-
|
56
|
+
changes << [:create, e.class.api_resource, e.tracking_id, id]
|
59
57
|
populate_id_map [], [reply] # allow resolving ids we could previously no resolve
|
60
58
|
Kennel.out.puts "#{LINE_UP}Created #{message} #{e.class.url(id)}"
|
61
59
|
end
|
@@ -64,7 +62,7 @@ module Kennel
|
|
64
62
|
message = "#{e.class.api_resource} #{e.tracking_id} #{e.class.url(id)}"
|
65
63
|
Kennel.out.puts "Updating #{message}"
|
66
64
|
@api.update e.class.api_resource, id, e.as_json
|
67
|
-
|
65
|
+
changes << [:update, e.class.api_resource, e.tracking_id, id]
|
68
66
|
Kennel.out.puts "#{LINE_UP}Updated #{message}"
|
69
67
|
end
|
70
68
|
|
@@ -73,15 +71,17 @@ module Kennel
|
|
73
71
|
message = "#{klass.api_resource} #{a.fetch(:tracking_id)} #{id}"
|
74
72
|
Kennel.out.puts "Deleting #{message}"
|
75
73
|
@api.delete klass.api_resource, id
|
76
|
-
|
74
|
+
changes << [:delete, klass.api_resource, a.fetch(:tracking_id), id]
|
77
75
|
Kennel.out.puts "#{LINE_UP}Deleted #{message}"
|
78
76
|
end
|
79
77
|
|
80
|
-
|
78
|
+
Plan.new(changes: changes)
|
81
79
|
end
|
82
80
|
|
83
81
|
private
|
84
82
|
|
83
|
+
attr_reader :kennel
|
84
|
+
|
85
85
|
# loop over items until everything is resolved or crash when we get stuck
|
86
86
|
# this solves cases like composite monitors depending on each other or monitor->monitor slo->slo monitor chains
|
87
87
|
def each_resolved(list)
|
@@ -104,11 +104,11 @@ module Kennel
|
|
104
104
|
def resolved?(e)
|
105
105
|
assert_resolved e
|
106
106
|
true
|
107
|
-
rescue
|
107
|
+
rescue UnresolvableIdError
|
108
108
|
false
|
109
109
|
end
|
110
110
|
|
111
|
-
# raises
|
111
|
+
# raises UnresolvableIdError when not resolved
|
112
112
|
def assert_resolved(e)
|
113
113
|
resolve_linked_tracking_ids! [e], force: true
|
114
114
|
end
|
@@ -121,7 +121,6 @@ module Kennel
|
|
121
121
|
@warnings = []
|
122
122
|
@update = []
|
123
123
|
@delete = []
|
124
|
-
@no_change = []
|
125
124
|
@id_map = IdMap.new
|
126
125
|
|
127
126
|
actual = Progress.progress("Downloading definitions") { download_definitions }
|
@@ -154,8 +153,6 @@ module Kennel
|
|
154
153
|
diff = e.diff(a) # slow ...
|
155
154
|
if diff.any?
|
156
155
|
@update << [id, e, a, diff]
|
157
|
-
else
|
158
|
-
@no_change << [id, e, a]
|
159
156
|
end
|
160
157
|
elsif a.fetch(:tracking_id) # was previously managed
|
161
158
|
@delete << [id, nil, a]
|
@@ -186,7 +183,7 @@ module Kennel
|
|
186
183
|
@expected.each do |e|
|
187
184
|
next unless id = e.id
|
188
185
|
resource = e.class.api_resource
|
189
|
-
if
|
186
|
+
if kennel.strict_imports
|
190
187
|
raise "Unable to find existing #{resource} with id #{id}\nIf the #{resource} was deleted, remove the `id: -> { #{id} }` line."
|
191
188
|
else
|
192
189
|
@warnings << "#{resource} #{e.tracking_id} specifies id #{id}, but no such #{resource} exists. 'id' will be ignored. Remove the `id: -> { #{id} }` line."
|
@@ -234,16 +231,19 @@ module Kennel
|
|
234
231
|
new = Utils.pretty_inspect(new)
|
235
232
|
end
|
236
233
|
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
234
|
+
message =
|
235
|
+
if use_diff
|
236
|
+
" #{type}#{field}\n" +
|
237
|
+
diff(old, new).map { |l| " #{l}" }.join("\n")
|
238
|
+
elsif (old + new).size > 100
|
239
|
+
" #{type}#{field}\n" \
|
240
|
+
" #{old} ->\n" \
|
241
|
+
" #{new}"
|
242
|
+
else
|
243
|
+
" #{type}#{field} #{old} -> #{new}"
|
244
|
+
end
|
245
|
+
|
246
|
+
Kennel.out.puts truncate_diff(message)
|
247
247
|
end
|
248
248
|
end
|
249
249
|
|
@@ -267,6 +267,17 @@ module Kennel
|
|
267
267
|
end
|
268
268
|
end
|
269
269
|
|
270
|
+
def truncate_diff(message)
|
271
|
+
# min '2' because: -1 makes no sense, 0 does not work with * 2 math, 1 says '1 lines'
|
272
|
+
@max_diff_lines ||= [Integer(ENV.fetch("MAX_DIFF_LINES", "50")), 2].max
|
273
|
+
warning = Utils.color(
|
274
|
+
:magenta,
|
275
|
+
" (Diff for this item truncated after #{@max_diff_lines} lines. " \
|
276
|
+
"Rerun with MAX_DIFF_LINES=#{@max_diff_lines * 2} to see more)"
|
277
|
+
)
|
278
|
+
Utils.truncate_lines(message, to: @max_diff_lines, warning: warning)
|
279
|
+
end
|
280
|
+
|
270
281
|
# We've already validated the desired objects ('generated') in isolation.
|
271
282
|
# Now that we have made the plan, we can perform some more validation.
|
272
283
|
def validate_plan
|
data/lib/kennel/tasks.rb
CHANGED
@@ -8,6 +8,10 @@ require "json"
|
|
8
8
|
module Kennel
|
9
9
|
module Tasks
|
10
10
|
class << self
|
11
|
+
def kennel
|
12
|
+
@kennel ||= Kennel::Engine.new
|
13
|
+
end
|
14
|
+
|
11
15
|
def abort(message = nil)
|
12
16
|
Kennel.err.puts message if message
|
13
17
|
raise SystemExit.new(1), message
|
@@ -35,10 +39,10 @@ module Kennel
|
|
35
39
|
load_environment
|
36
40
|
|
37
41
|
if on_default_branch? && git_push?
|
38
|
-
Kennel.strict_imports = false
|
39
|
-
Kennel.update
|
42
|
+
Kennel::Tasks.kennel.strict_imports = false
|
43
|
+
Kennel::Tasks.kennel.update
|
40
44
|
else
|
41
|
-
Kennel.plan # show plan in CI logs
|
45
|
+
Kennel::Tasks.kennel.plan # show plan in CI logs
|
42
46
|
end
|
43
47
|
end
|
44
48
|
|
@@ -66,7 +70,7 @@ namespace :kennel do
|
|
66
70
|
# https://help.datadoghq.com/hc/en-us/requests/254114 for automatic validation
|
67
71
|
desc "Verify that all used monitor mentions are valid"
|
68
72
|
task validate_mentions: :environment do
|
69
|
-
known = Kennel.
|
73
|
+
known = Kennel::Api.new
|
70
74
|
.send(:request, :get, "/monitor/notifications")
|
71
75
|
.fetch(:handles)
|
72
76
|
.values
|
@@ -93,18 +97,18 @@ namespace :kennel do
|
|
93
97
|
|
94
98
|
desc "generate local definitions"
|
95
99
|
task generate: :environment do
|
96
|
-
Kennel.generate
|
100
|
+
Kennel::Tasks.kennel.generate
|
97
101
|
end
|
98
102
|
|
99
103
|
# also generate parts so users see and commit updated generated automatically
|
100
104
|
desc "show planned datadog changes (scope with PROJECT=name)"
|
101
105
|
task plan: :generate do
|
102
|
-
Kennel.plan
|
106
|
+
Kennel::Tasks.kennel.plan
|
103
107
|
end
|
104
108
|
|
105
109
|
desc "update datadog (scope with PROJECT=name)"
|
106
110
|
task update_datadog: :environment do
|
107
|
-
Kennel.update
|
111
|
+
Kennel::Tasks.kennel.update
|
108
112
|
end
|
109
113
|
|
110
114
|
desc "update on push to the default branch, otherwise show plan"
|
@@ -115,13 +119,13 @@ namespace :kennel do
|
|
115
119
|
desc "show unmuted alerts filtered by TAG, for example TAG=team:foo"
|
116
120
|
task alerts: :environment do
|
117
121
|
tag = ENV["TAG"] || Kennel::Tasks.abort("Call with TAG=foo:bar")
|
118
|
-
Kennel::UnmutedAlerts.print(Kennel.
|
122
|
+
Kennel::UnmutedAlerts.print(Kennel::Api.new, tag)
|
119
123
|
end
|
120
124
|
|
121
125
|
desc "show monitors with no data by TAG, for example TAG=team:foo [THRESHOLD_DAYS=7] [FORMAT=json]"
|
122
126
|
task nodata: :environment do
|
123
127
|
tag = ENV["TAG"] || Kennel::Tasks.abort("Call with TAG=foo:bar")
|
124
|
-
monitors = Kennel.
|
128
|
+
monitors = Kennel::Api.new.list("monitor", monitor_tags: tag, group_states: "no data")
|
125
129
|
monitors.select! { |m| m[:overall_state] == "No Data" }
|
126
130
|
monitors.reject! { |m| m[:tags].include? "nodata:ignore" }
|
127
131
|
if monitors.any?
|
@@ -179,7 +183,7 @@ namespace :kennel do
|
|
179
183
|
Kennel::Tasks.abort("Call with URL= or call with RESOURCE=#{possible_resources.join(" or ")} and ID=")
|
180
184
|
end
|
181
185
|
|
182
|
-
Kennel.out.puts Kennel::Importer.new(Kennel.
|
186
|
+
Kennel.out.puts Kennel::Importer.new(Kennel::Api.new).import(resource, id)
|
183
187
|
end
|
184
188
|
|
185
189
|
desc "Dump ALL of datadog config as raw json ... useful for grep/search [TYPE=slo|monitor|dashboard]"
|
@@ -190,7 +194,7 @@ namespace :kennel do
|
|
190
194
|
else
|
191
195
|
Kennel::Models::Record.api_resource_map.keys
|
192
196
|
end
|
193
|
-
api = Kennel.
|
197
|
+
api = Kennel::Api.new
|
194
198
|
list = nil
|
195
199
|
first = true
|
196
200
|
|
@@ -240,7 +244,7 @@ namespace :kennel do
|
|
240
244
|
klass =
|
241
245
|
Kennel::Models::Record.subclasses.detect { |s| s.api_resource == resource } ||
|
242
246
|
raise("resource #{resource} not know")
|
243
|
-
object = Kennel.
|
247
|
+
object = Kennel::Api.new.show(resource, id)
|
244
248
|
Kennel.out.puts klass.parse_tracking_id(object)
|
245
249
|
end
|
246
250
|
|
data/lib/kennel/utils.rb
CHANGED
@@ -57,6 +57,12 @@ module Kennel
|
|
57
57
|
"\e[#{COLORS.fetch(color)}m#{text}\e[0m"
|
58
58
|
end
|
59
59
|
|
60
|
+
def truncate_lines(text, to:, warning:)
|
61
|
+
lines = text.split(/\n/, to + 1)
|
62
|
+
lines[-1] = warning if lines.size > to
|
63
|
+
lines.join("\n")
|
64
|
+
end
|
65
|
+
|
60
66
|
def capture_stdout
|
61
67
|
old = Kennel.out
|
62
68
|
Kennel.out = StringIO.new
|
data/lib/kennel/version.rb
CHANGED
data/lib/kennel.rb
CHANGED
@@ -5,7 +5,6 @@ require "zeitwerk"
|
|
5
5
|
require "English"
|
6
6
|
|
7
7
|
require "kennel/version"
|
8
|
-
require "kennel/compatibility"
|
9
8
|
require "kennel/utils"
|
10
9
|
require "kennel/progress"
|
11
10
|
require "kennel/filter"
|
@@ -40,21 +39,24 @@ module Teams
|
|
40
39
|
end
|
41
40
|
|
42
41
|
module Kennel
|
43
|
-
|
42
|
+
UnresolvableIdError = Class.new(StandardError)
|
43
|
+
DisallowedUpdateError = Class.new(StandardError)
|
44
|
+
GenerationAbortedError = Class.new(StandardError)
|
45
|
+
UpdateResult = Struct.new(:plan, :update, keyword_init: true)
|
46
|
+
|
47
|
+
class << self
|
48
|
+
attr_accessor :out, :err
|
44
49
|
end
|
45
50
|
|
46
|
-
|
47
|
-
|
48
|
-
UpdateResult = Struct.new(:plan, :update, keyword_init: true)
|
51
|
+
self.out = $stdout
|
52
|
+
self.err = $stderr
|
49
53
|
|
50
54
|
class Engine
|
51
55
|
def initialize
|
52
|
-
@out = $stdout
|
53
|
-
@err = $stderr
|
54
56
|
@strict_imports = true
|
55
57
|
end
|
56
58
|
|
57
|
-
attr_accessor :
|
59
|
+
attr_accessor :strict_imports
|
58
60
|
|
59
61
|
def generate
|
60
62
|
parts = generated
|
@@ -67,12 +69,8 @@ module Kennel
|
|
67
69
|
end
|
68
70
|
|
69
71
|
def update
|
70
|
-
|
71
|
-
|
72
|
-
UpdateResult.new(
|
73
|
-
plan: the_plan,
|
74
|
-
update: the_update
|
75
|
-
)
|
72
|
+
syncer.plan
|
73
|
+
syncer.update if syncer.confirm
|
76
74
|
end
|
77
75
|
|
78
76
|
private
|
@@ -82,11 +80,11 @@ module Kennel
|
|
82
80
|
end
|
83
81
|
|
84
82
|
def syncer
|
85
|
-
@syncer ||= Syncer.new(api, generated, project_filter: filter.project_filter, tracking_id_filter: filter.tracking_id_filter)
|
83
|
+
@syncer ||= Syncer.new(api, generated, kennel: self, project_filter: filter.project_filter, tracking_id_filter: filter.tracking_id_filter)
|
86
84
|
end
|
87
85
|
|
88
86
|
def api
|
89
|
-
@api ||= Api.new
|
87
|
+
@api ||= Api.new
|
90
88
|
end
|
91
89
|
|
92
90
|
def projects_provider
|
@@ -99,26 +97,30 @@ module Kennel
|
|
99
97
|
|
100
98
|
def generated
|
101
99
|
@generated ||= begin
|
102
|
-
Progress.progress "
|
100
|
+
parts = Progress.progress "Finding parts" do
|
103
101
|
projects = projects_provider.projects
|
104
102
|
projects = filter.filter_projects projects
|
105
103
|
|
106
104
|
parts = Utils.parallel(projects, &:validated_parts).flatten(1)
|
107
|
-
|
105
|
+
filter.filter_parts parts
|
106
|
+
end
|
108
107
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
108
|
+
parts.group_by(&:tracking_id).each do |tracking_id, same|
|
109
|
+
next if same.size == 1
|
110
|
+
raise <<~ERROR
|
111
|
+
#{tracking_id} is defined #{same.size} times
|
112
|
+
use a different `kennel_id` when defining multiple projects/monitors/dashboards to avoid this conflict
|
113
|
+
ERROR
|
114
|
+
end
|
116
115
|
|
116
|
+
Progress.progress "Building json" do
|
117
117
|
# trigger json caching here so it counts into generating
|
118
|
-
Utils.parallel(parts, &:
|
119
|
-
|
120
|
-
parts
|
118
|
+
Utils.parallel(parts, &:build)
|
121
119
|
end
|
120
|
+
|
121
|
+
OptionalValidations.valid?(parts) or raise GenerationAbortedError
|
122
|
+
|
123
|
+
parts
|
122
124
|
end
|
123
125
|
end
|
124
126
|
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.
|
4
|
+
version: 1.124.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
|
+
date: 2022-11-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: diff-lcs
|
@@ -89,7 +89,6 @@ files:
|
|
89
89
|
- Readme.md
|
90
90
|
- lib/kennel.rb
|
91
91
|
- lib/kennel/api.rb
|
92
|
-
- lib/kennel/compatibility.rb
|
93
92
|
- lib/kennel/file_cache.rb
|
94
93
|
- lib/kennel/filter.rb
|
95
94
|
- lib/kennel/github_reporter.rb
|
data/lib/kennel/compatibility.rb
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Kennel
|
4
|
-
module Compatibility
|
5
|
-
def self.included(into)
|
6
|
-
class << into
|
7
|
-
%I[out out= err err= strict_imports strict_imports= generate plan update].each do |sym|
|
8
|
-
define_method(sym) { |*args| instance.public_send(sym, *args) }
|
9
|
-
end
|
10
|
-
|
11
|
-
def build_default
|
12
|
-
Kennel::Engine.new
|
13
|
-
end
|
14
|
-
|
15
|
-
def instance
|
16
|
-
@instance ||= build_default
|
17
|
-
end
|
18
|
-
|
19
|
-
private
|
20
|
-
|
21
|
-
def api
|
22
|
-
instance.send(:api)
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|