kennel 1.87.1 → 1.90.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/Readme.md +6 -1
- data/lib/kennel.rb +19 -3
- data/lib/kennel/api.rb +67 -44
- data/lib/kennel/file_cache.rb +9 -5
- data/lib/kennel/importer.rb +23 -7
- data/lib/kennel/models/base.rb +1 -1
- data/lib/kennel/models/dashboard.rb +1 -0
- data/lib/kennel/models/monitor.rb +14 -2
- data/lib/kennel/models/record.rb +38 -3
- data/lib/kennel/models/slo.rb +2 -1
- data/lib/kennel/progress.rb +2 -0
- data/lib/kennel/syncer.rb +57 -84
- data/lib/kennel/tasks.rb +1 -1
- data/lib/kennel/template_variables.rb +2 -1
- data/lib/kennel/version.rb +1 -1
- data/template/Readme.md +1 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 861edba684922b252c0af4cbbdecbe258e28fc0dcba9a31f8c77e14ea3391110
|
|
4
|
+
data.tar.gz: fc11becb68eb696db030e517d99e12191eae10024c74ca852c7c308646517ef4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 660e66d7a1ae02a0f67d723ddd1beccddea63d9bf62658b4327ec2622ba32f161a6c9336b86a1295b8037195ed513c4514c0c1759e6eb71764665d11feeb19fe
|
|
7
|
+
data.tar.gz: a2f479c8579e94a7a796e59ea6be3433c795f1db35892a5ce57a345ac7449c87a344632ad8c35fd27256da70d5c49eb227dd70f89c7566ea4084085277f25e94
|
data/Readme.md
CHANGED
|
@@ -52,6 +52,7 @@ end
|
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
<!-- NOT IN template/Readme.md -->
|
|
55
|
+
|
|
55
56
|
## Installation
|
|
56
57
|
|
|
57
58
|
- create a new private `kennel` repo for your organization (do not fork this repo)
|
|
@@ -114,6 +115,7 @@ end
|
|
|
114
115
|
- use [datadog monitor UI](https://app.datadoghq.com/monitors/manage) to find a monitor
|
|
115
116
|
- get the `id` from the url
|
|
116
117
|
- run `URL='https://app.datadoghq.com/monitors/123' bundle exec rake kennel:import` and copy the output
|
|
118
|
+
- import task also works with SLO alerts, e.g. `URL='https://app.datadoghq.com/slo/edit/123abc456def123/alerts/789' bundle exec rake kennel:import`
|
|
117
119
|
- find or create a project in `projects/`
|
|
118
120
|
- add the monitor to `parts: [` list, for example:
|
|
119
121
|
```Ruby
|
|
@@ -292,9 +294,12 @@ https://foo.datadog.com/monitor/123
|
|
|
292
294
|
|
|
293
295
|
<!-- NOT IN template/Readme.md -->
|
|
294
296
|
|
|
295
|
-
|
|
296
297
|
## Development
|
|
297
298
|
|
|
299
|
+
### Benchmarking
|
|
300
|
+
|
|
301
|
+
Setting `FORCE_GET_CACHE=true` will cache all get requests, which makes benchmarking improvements more reliable.
|
|
302
|
+
|
|
298
303
|
### Integration testing
|
|
299
304
|
|
|
300
305
|
```Bash
|
data/lib/kennel.rb
CHANGED
|
@@ -55,7 +55,7 @@ module Kennel
|
|
|
55
55
|
|
|
56
56
|
def store(parts)
|
|
57
57
|
Progress.progress "Storing" do
|
|
58
|
-
old = Dir["generated
|
|
58
|
+
old = Dir["generated/#{project_filter || "**"}/*"]
|
|
59
59
|
used = []
|
|
60
60
|
|
|
61
61
|
Utils.parallel(parts, max: 2) do |part|
|
|
@@ -83,7 +83,7 @@ module Kennel
|
|
|
83
83
|
end
|
|
84
84
|
|
|
85
85
|
def syncer
|
|
86
|
-
@syncer ||= Syncer.new(api, generated, project:
|
|
86
|
+
@syncer ||= Syncer.new(api, generated, project: project_filter)
|
|
87
87
|
end
|
|
88
88
|
|
|
89
89
|
def api
|
|
@@ -94,9 +94,21 @@ module Kennel
|
|
|
94
94
|
@generated ||= begin
|
|
95
95
|
Progress.progress "Generating" do
|
|
96
96
|
load_all
|
|
97
|
+
known = []
|
|
97
98
|
parts = Models::Project.recursive_subclasses.flat_map do |project_class|
|
|
98
|
-
project_class.new
|
|
99
|
+
project = project_class.new
|
|
100
|
+
kennel_id = project.kennel_id
|
|
101
|
+
if project_filter
|
|
102
|
+
known << kennel_id
|
|
103
|
+
next [] if kennel_id != project_filter
|
|
104
|
+
end
|
|
105
|
+
project.validated_parts
|
|
99
106
|
end
|
|
107
|
+
|
|
108
|
+
if project_filter && parts.empty?
|
|
109
|
+
raise "#{project_filter} does not match any projects, try any of these:\n#{known.uniq.sort.join("\n")}"
|
|
110
|
+
end
|
|
111
|
+
|
|
100
112
|
parts.group_by(&:tracking_id).each do |tracking_id, same|
|
|
101
113
|
next if same.size == 1
|
|
102
114
|
raise <<~ERROR
|
|
@@ -109,6 +121,10 @@ module Kennel
|
|
|
109
121
|
end
|
|
110
122
|
end
|
|
111
123
|
|
|
124
|
+
def project_filter
|
|
125
|
+
ENV["PROJECT"]
|
|
126
|
+
end
|
|
127
|
+
|
|
112
128
|
def load_all
|
|
113
129
|
["teams", "parts", "projects"].each do |folder|
|
|
114
130
|
Dir["#{folder}/**/*.rb"].sort.each { |f| require "./#{f}" }
|
data/lib/kennel/api.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# encapsulates knowledge around how the api works
|
|
3
|
+
# especially 1-off weirdness that should not lak into other parts of the code
|
|
2
4
|
module Kennel
|
|
3
|
-
# encapsulates knowledge around how the api works
|
|
4
5
|
class Api
|
|
5
6
|
CACHE_FILE = "tmp/cache/details"
|
|
6
7
|
|
|
@@ -11,34 +12,24 @@ module Kennel
|
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def show(api_resource, id, params = {})
|
|
14
|
-
|
|
15
|
-
api_resource == "slo"
|
|
15
|
+
response = request :get, "/api/v1/#{api_resource}/#{id}", params: params
|
|
16
|
+
response = response.fetch(:data) if api_resource == "slo"
|
|
17
|
+
response
|
|
16
18
|
end
|
|
17
19
|
|
|
18
20
|
def list(api_resource, params = {})
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
loop do
|
|
26
|
-
result = request :get, "/api/v1/#{api_resource}", params: params.merge(limit: limit, offset: offset)
|
|
27
|
-
data = result.fetch(:data)
|
|
28
|
-
all.concat data
|
|
29
|
-
break all if data.size < limit
|
|
30
|
-
offset += limit
|
|
31
|
-
end
|
|
32
|
-
else
|
|
33
|
-
result = request :get, "/api/v1/#{api_resource}", params: params
|
|
34
|
-
result = result.fetch(:dashboards) if api_resource == "dashboard"
|
|
35
|
-
result
|
|
21
|
+
with_pagination api_resource == "slo", params do |paginated_params|
|
|
22
|
+
response = request :get, "/api/v1/#{api_resource}", params: paginated_params
|
|
23
|
+
response = response.fetch(:dashboards) if api_resource == "dashboard"
|
|
24
|
+
response = response.fetch(:data) if api_resource == "slo"
|
|
25
|
+
response
|
|
36
26
|
end
|
|
37
27
|
end
|
|
38
28
|
|
|
39
29
|
def create(api_resource, attributes)
|
|
40
|
-
|
|
41
|
-
|
|
30
|
+
response = request :post, "/api/v1/#{api_resource}", body: attributes
|
|
31
|
+
response = response.fetch(:data).first if api_resource == "slo"
|
|
32
|
+
response
|
|
42
33
|
end
|
|
43
34
|
|
|
44
35
|
def update(api_resource, id, attributes)
|
|
@@ -53,7 +44,6 @@ module Kennel
|
|
|
53
44
|
end
|
|
54
45
|
|
|
55
46
|
def fill_details!(api_resource, list)
|
|
56
|
-
return unless api_resource == "dashboard"
|
|
57
47
|
details_cache do |cache|
|
|
58
48
|
Utils.parallel(list) { |a| fill_detail!(api_resource, a, cache) }
|
|
59
49
|
end
|
|
@@ -61,6 +51,21 @@ module Kennel
|
|
|
61
51
|
|
|
62
52
|
private
|
|
63
53
|
|
|
54
|
+
def with_pagination(enabled, params)
|
|
55
|
+
return yield params unless enabled
|
|
56
|
+
raise ArgumentError if params[:limit] || params[:offset]
|
|
57
|
+
limit = 1000
|
|
58
|
+
offset = 0
|
|
59
|
+
all = []
|
|
60
|
+
|
|
61
|
+
loop do
|
|
62
|
+
response = yield params.merge(limit: limit, offset: offset)
|
|
63
|
+
all.concat response
|
|
64
|
+
return all if response.size < limit
|
|
65
|
+
offset += limit
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
64
69
|
# Make diff work even though we cannot mass-fetch definitions
|
|
65
70
|
def fill_detail!(api_resource, a, cache)
|
|
66
71
|
args = [api_resource, a.fetch(:id)]
|
|
@@ -74,34 +79,52 @@ module Kennel
|
|
|
74
79
|
end
|
|
75
80
|
|
|
76
81
|
def request(method, path, body: nil, params: {}, ignore_404: false)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
path = "#{path}?#{Faraday::FlatParamsEncoder.encode(params)}" if params.any?
|
|
83
|
+
with_cache ENV["FORCE_GET_CACHE"] && method == :get, path do
|
|
84
|
+
response = nil
|
|
85
|
+
tries = 2
|
|
86
|
+
|
|
87
|
+
tries.times do |i|
|
|
88
|
+
response = Utils.retry Faraday::ConnectionFailed, Faraday::TimeoutError, times: 2 do
|
|
89
|
+
@client.send(method, path) do |request|
|
|
90
|
+
request.body = JSON.generate(body) if body
|
|
91
|
+
request.headers["Content-type"] = "application/json"
|
|
92
|
+
request.headers["DD-API-KEY"] = @api_key
|
|
93
|
+
request.headers["DD-APPLICATION-KEY"] = @app_key
|
|
94
|
+
end
|
|
87
95
|
end
|
|
96
|
+
|
|
97
|
+
break if i == tries - 1 || method != :get || response.status < 500
|
|
98
|
+
Kennel.err.puts "Retrying on server error #{response.status} for #{path}"
|
|
88
99
|
end
|
|
89
100
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
101
|
+
if !response.success? && (response.status != 404 || !ignore_404)
|
|
102
|
+
message = +"Error #{response.status} during #{method.upcase} #{path}\n"
|
|
103
|
+
message << "request:\n#{JSON.pretty_generate(body)}\nresponse:\n" if body
|
|
104
|
+
message << response.body
|
|
105
|
+
raise message
|
|
106
|
+
end
|
|
93
107
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
108
|
+
if response.body.empty?
|
|
109
|
+
{}
|
|
110
|
+
else
|
|
111
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
112
|
+
end
|
|
99
113
|
end
|
|
114
|
+
end
|
|
100
115
|
|
|
101
|
-
|
|
102
|
-
|
|
116
|
+
# allow caching all requests to speedup/benchmark logic that includes repeated requests
|
|
117
|
+
def with_cache(enabled, key)
|
|
118
|
+
return yield unless enabled
|
|
119
|
+
dir = "tmp/cache"
|
|
120
|
+
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
|
121
|
+
file = "#{dir}/#{key.delete("/?=")}" # TODO: encode nicely
|
|
122
|
+
if File.exist?(file)
|
|
123
|
+
Marshal.load(File.read(file)) # rubocop:disable Security/MarshalLoad
|
|
103
124
|
else
|
|
104
|
-
|
|
125
|
+
result = yield
|
|
126
|
+
File.write(file, Marshal.dump(result))
|
|
127
|
+
result
|
|
105
128
|
end
|
|
106
129
|
end
|
|
107
130
|
end
|
data/lib/kennel/file_cache.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# cache that reads everything from a single file
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# - avoids doing multiple disk reads while iterating all definitions
|
|
5
|
+
# - has a global expiry to not keep deleted resources forever
|
|
6
6
|
module Kennel
|
|
7
7
|
class FileCache
|
|
8
8
|
def initialize(file, cache_version)
|
|
@@ -22,10 +22,11 @@ module Kennel
|
|
|
22
22
|
|
|
23
23
|
def fetch(key, key_version)
|
|
24
24
|
old_value, old_version = @data[key]
|
|
25
|
-
|
|
25
|
+
expected_version = [key_version, @cache_version]
|
|
26
|
+
return old_value if old_version == expected_version
|
|
26
27
|
|
|
27
28
|
new_value = yield
|
|
28
|
-
@data[key] = [new_value,
|
|
29
|
+
@data[key] = [new_value, expected_version, @expires]
|
|
29
30
|
new_value
|
|
30
31
|
end
|
|
31
32
|
|
|
@@ -46,8 +47,11 @@ module Kennel
|
|
|
46
47
|
File.write(@file, Marshal.dump(@data))
|
|
47
48
|
end
|
|
48
49
|
|
|
50
|
+
# keep the cache small to make loading it fast (5MB ~= 100ms)
|
|
51
|
+
# - delete expired keys
|
|
52
|
+
# - delete what would be deleted anyway when updating
|
|
49
53
|
def expire_old_data
|
|
50
|
-
@data.reject! { |_, (_, _,
|
|
54
|
+
@data.reject! { |_, (_, (_, cv), expires)| expires < @now || cv != @cache_version }
|
|
51
55
|
end
|
|
52
56
|
end
|
|
53
57
|
end
|
data/lib/kennel/importer.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Kennel
|
|
4
4
|
class Importer
|
|
5
5
|
TITLES = [:name, :title].freeze
|
|
6
|
-
SORT_ORDER = [*TITLES, :id, :kennel_id, :type, :tags, :query, *
|
|
6
|
+
SORT_ORDER = [*TITLES, :id, :kennel_id, :type, :tags, :query, *Models::Record.subclasses.map { |k| k::TRACKING_FIELDS }, :template_variables].freeze
|
|
7
7
|
|
|
8
8
|
def initialize(api)
|
|
9
9
|
@api = api
|
|
@@ -31,11 +31,10 @@ module Kennel
|
|
|
31
31
|
title.tr!(Kennel::Models::Record::LOCK, "") # avoid double lock icon
|
|
32
32
|
|
|
33
33
|
# calculate or reuse kennel_id
|
|
34
|
-
# TODO: this is copy-pasted from syncer, need to find a nice way to reuse it
|
|
35
|
-
tracking_field = Syncer::TRACKING_FIELDS.detect { |f| data[f] }
|
|
36
34
|
data[:kennel_id] =
|
|
37
|
-
if
|
|
38
|
-
|
|
35
|
+
if tracking_id = model.parse_tracking_id(data)
|
|
36
|
+
model.remove_tracking_id(data)
|
|
37
|
+
tracking_id.split(":").last
|
|
39
38
|
else
|
|
40
39
|
Kennel::Utils.parameterize(title)
|
|
41
40
|
end
|
|
@@ -67,9 +66,12 @@ module Kennel
|
|
|
67
66
|
when "dashboard"
|
|
68
67
|
widgets = data[:widgets]&.flat_map { |widget| widget.dig(:definition, :widgets) || [widget] }
|
|
69
68
|
widgets&.each do |widget|
|
|
70
|
-
|
|
69
|
+
convert_widget_to_compact_format!(widget)
|
|
70
|
+
dry_up_widget_metadata!(widget)
|
|
71
71
|
(widget.dig(:definition, :markers) || []).each { |m| m[:label]&.delete! " " }
|
|
72
72
|
end
|
|
73
|
+
else
|
|
74
|
+
# noop
|
|
73
75
|
end
|
|
74
76
|
|
|
75
77
|
data.delete(:tags) if data[:tags] == [] # do not create super + [] call
|
|
@@ -91,7 +93,7 @@ module Kennel
|
|
|
91
93
|
private
|
|
92
94
|
|
|
93
95
|
# reduce duplication in imports by using dry `q: :metadata` when possible
|
|
94
|
-
def
|
|
96
|
+
def dry_up_widget_metadata!(widget)
|
|
95
97
|
(widget.dig(:definition, :requests) || []).each do |request|
|
|
96
98
|
next unless request.is_a?(Hash)
|
|
97
99
|
next unless metadata = request[:metadata]
|
|
@@ -104,6 +106,20 @@ module Kennel
|
|
|
104
106
|
end
|
|
105
107
|
end
|
|
106
108
|
|
|
109
|
+
# new api format is very verbose, so use old dry format when possible
|
|
110
|
+
def convert_widget_to_compact_format!(widget)
|
|
111
|
+
(widget.dig(:definition, :requests) || []).each do |request|
|
|
112
|
+
next unless request.is_a?(Hash)
|
|
113
|
+
next if request[:formulas] && request[:formulas] != [{ formula: "query1" }]
|
|
114
|
+
next if request[:queries]&.size != 1
|
|
115
|
+
next if request[:queries].any? { |q| q[:data_source] != "metrics" }
|
|
116
|
+
next if widget.dig(:definition, :type) != request[:response_format]
|
|
117
|
+
request.delete(:formulas)
|
|
118
|
+
request.delete(:response_format)
|
|
119
|
+
request[:q] = request.delete(:queries).first.fetch(:query)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
107
123
|
def pretty_print(hash)
|
|
108
124
|
sort_widgets hash
|
|
109
125
|
|
data/lib/kennel/models/base.rb
CHANGED
|
@@ -11,7 +11,7 @@ module Kennel
|
|
|
11
11
|
|
|
12
12
|
def kennel_id
|
|
13
13
|
name = self.class.name
|
|
14
|
-
if name.start_with?("Kennel::")
|
|
14
|
+
if name.start_with?("Kennel::") # core objects would always generate the same id
|
|
15
15
|
raise_with_location ArgumentError, "Set :kennel_id"
|
|
16
16
|
end
|
|
17
17
|
@kennel_id ||= Utils.snake_case name
|
|
@@ -8,6 +8,7 @@ module Kennel
|
|
|
8
8
|
READONLY_ATTRIBUTES = superclass::READONLY_ATTRIBUTES + [
|
|
9
9
|
:author_handle, :author_name, :modified_at, :url, :is_read_only, :notify_list
|
|
10
10
|
]
|
|
11
|
+
TRACKING_FIELD = :description
|
|
11
12
|
REQUEST_DEFAULTS = {
|
|
12
13
|
style: { line_width: "normal", palette: "dog_classic", line_type: "solid" }
|
|
13
14
|
}.freeze
|
|
@@ -9,6 +9,7 @@ module Kennel
|
|
|
9
9
|
READONLY_ATTRIBUTES = superclass::READONLY_ATTRIBUTES + [
|
|
10
10
|
:multi, :matching_downtimes, :overall_state_modified, :overall_state, :restricted_roles
|
|
11
11
|
]
|
|
12
|
+
TRACKING_FIELD = :message
|
|
12
13
|
|
|
13
14
|
MONITOR_DEFAULTS = {
|
|
14
15
|
priority: nil
|
|
@@ -25,6 +26,7 @@ module Kennel
|
|
|
25
26
|
groupby_simple_monitor: false
|
|
26
27
|
}.freeze
|
|
27
28
|
DEFAULT_ESCALATION_MESSAGE = ["", nil].freeze
|
|
29
|
+
ALLOWED_PRIORITY_CLASSES = [NilClass, Integer].freeze
|
|
28
30
|
|
|
29
31
|
settings(
|
|
30
32
|
:query, :name, :message, :escalation_message, :critical, :type, :renotify_interval, :warning, :timeout_h, :evaluation_delay,
|
|
@@ -127,9 +129,15 @@ module Kennel
|
|
|
127
129
|
Utils.path_to_url "/monitors##{id}/edit"
|
|
128
130
|
end
|
|
129
131
|
|
|
130
|
-
# datadog uses / for show and # for edit as separator in it's links
|
|
131
132
|
def self.parse_url(url)
|
|
132
|
-
|
|
133
|
+
# datadog uses / for show and # for edit as separator in it's links
|
|
134
|
+
id = url[/\/monitors[\/#](\d+)/, 1]
|
|
135
|
+
|
|
136
|
+
# slo alert url
|
|
137
|
+
id ||= url[/\/slo\/edit\/[a-z\d]{10,}\/alerts\/(\d+)/, 1]
|
|
138
|
+
|
|
139
|
+
return unless id
|
|
140
|
+
|
|
133
141
|
Integer(id)
|
|
134
142
|
end
|
|
135
143
|
|
|
@@ -217,6 +225,10 @@ module Kennel
|
|
|
217
225
|
end
|
|
218
226
|
end
|
|
219
227
|
end
|
|
228
|
+
|
|
229
|
+
unless ALLOWED_PRIORITY_CLASSES.include?(priority.class)
|
|
230
|
+
invalid! "priority needs to be an Integer"
|
|
231
|
+
end
|
|
220
232
|
end
|
|
221
233
|
end
|
|
222
234
|
end
|
data/lib/kennel/models/record.rb
CHANGED
|
@@ -3,8 +3,10 @@ module Kennel
|
|
|
3
3
|
module Models
|
|
4
4
|
class Record < Base
|
|
5
5
|
LOCK = "\u{1F512}"
|
|
6
|
+
TRACKING_FIELDS = [:message, :description].freeze
|
|
6
7
|
READONLY_ATTRIBUTES = [
|
|
7
|
-
:deleted, :id, :created, :created_at, :creator, :org_id, :modified, :modified_at,
|
|
8
|
+
:deleted, :id, :created, :created_at, :creator, :org_id, :modified, :modified_at,
|
|
9
|
+
:klass, :tracking_id # added by syncer.rb
|
|
8
10
|
].freeze
|
|
9
11
|
|
|
10
12
|
settings :id, :kennel_id
|
|
@@ -22,6 +24,18 @@ module Kennel
|
|
|
22
24
|
subclasses.map { |s| [s.api_resource, s] }.to_h
|
|
23
25
|
end
|
|
24
26
|
|
|
27
|
+
def parse_tracking_id(a)
|
|
28
|
+
a[self::TRACKING_FIELD].to_s[/-- Managed by kennel (\S+:\S+)/, 1]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# TODO: combine with parse into a single method or a single regex
|
|
32
|
+
def remove_tracking_id(a)
|
|
33
|
+
value = a[self::TRACKING_FIELD]
|
|
34
|
+
a[self::TRACKING_FIELD] =
|
|
35
|
+
value.dup.sub!(/\n?-- Managed by kennel .*/, "") ||
|
|
36
|
+
raise("did not find tracking id in #{value}")
|
|
37
|
+
end
|
|
38
|
+
|
|
25
39
|
private
|
|
26
40
|
|
|
27
41
|
def normalize(_expected, actual)
|
|
@@ -60,19 +74,40 @@ module Kennel
|
|
|
60
74
|
end
|
|
61
75
|
|
|
62
76
|
def tracking_id
|
|
63
|
-
|
|
77
|
+
@tracking_id ||= begin
|
|
78
|
+
id = "#{project.kennel_id}:#{kennel_id}"
|
|
79
|
+
raise ValidationError, "#{id} kennel_id cannot include whitespace" if id.match?(/\s/) # <-> parse_tracking_id
|
|
80
|
+
id
|
|
81
|
+
end
|
|
64
82
|
end
|
|
65
83
|
|
|
66
84
|
def resolve_linked_tracking_ids!(*)
|
|
67
85
|
end
|
|
68
86
|
|
|
87
|
+
def add_tracking_id
|
|
88
|
+
json = as_json
|
|
89
|
+
if self.class.parse_tracking_id(json)
|
|
90
|
+
invalid! "remove \"-- Managed by kennel\" line it from #{self.class::TRACKING_FIELD} to copy a resource"
|
|
91
|
+
end
|
|
92
|
+
json[self.class::TRACKING_FIELD] =
|
|
93
|
+
"#{json[self.class::TRACKING_FIELD]}\n" \
|
|
94
|
+
"-- Managed by kennel #{tracking_id} in #{project.class.file_location}, do not modify manually".lstrip
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def remove_tracking_id
|
|
98
|
+
self.class.remove_tracking_id(as_json)
|
|
99
|
+
end
|
|
100
|
+
|
|
69
101
|
private
|
|
70
102
|
|
|
71
103
|
def resolve_link(tracking_id, type, id_map, force:)
|
|
72
104
|
id = id_map[tracking_id]
|
|
73
105
|
if id == :new
|
|
74
106
|
if force
|
|
75
|
-
invalid!
|
|
107
|
+
invalid!(
|
|
108
|
+
"#{type} #{tracking_id} was referenced but is also created by the current run.\n" \
|
|
109
|
+
"It could not be created because of a circular dependency, try creating only some of the resources"
|
|
110
|
+
)
|
|
76
111
|
else
|
|
77
112
|
nil # will be re-resolved after the linked object was created
|
|
78
113
|
end
|
data/lib/kennel/models/slo.rb
CHANGED
|
@@ -3,6 +3,7 @@ module Kennel
|
|
|
3
3
|
module Models
|
|
4
4
|
class Slo < Record
|
|
5
5
|
READONLY_ATTRIBUTES = superclass::READONLY_ATTRIBUTES + [:type_id, :monitor_tags]
|
|
6
|
+
TRACKING_FIELD = :description
|
|
6
7
|
DEFAULTS = {
|
|
7
8
|
description: nil,
|
|
8
9
|
query: nil,
|
|
@@ -63,7 +64,7 @@ module Kennel
|
|
|
63
64
|
end
|
|
64
65
|
|
|
65
66
|
def self.parse_url(url)
|
|
66
|
-
url[/\/slo(\?.*slo_id=|\/edit\/)([a-z\d]{10,})/, 2]
|
|
67
|
+
url[/\/slo(\?.*slo_id=|\/edit\/)([a-z\d]{10,})(&|$)/, 2]
|
|
67
68
|
end
|
|
68
69
|
|
|
69
70
|
def resolve_linked_tracking_ids!(id_map, **args)
|
data/lib/kennel/progress.rb
CHANGED
data/lib/kennel/syncer.rb
CHANGED
|
@@ -1,22 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
module Kennel
|
|
3
3
|
class Syncer
|
|
4
|
-
TRACKING_FIELDS = [:message, :description].freeze
|
|
5
4
|
DELETE_ORDER = ["dashboard", "slo", "monitor"].freeze # dashboards references monitors + slos, slos reference monitors
|
|
5
|
+
LINE_UP = "\e[1A\033[K" # go up and clear
|
|
6
6
|
|
|
7
7
|
def initialize(api, expected, project: nil)
|
|
8
8
|
@api = api
|
|
9
9
|
@project_filter = project
|
|
10
10
|
@expected = expected
|
|
11
|
-
if @project_filter
|
|
12
|
-
original = @expected
|
|
13
|
-
@expected = @expected.select { |e| e.project.kennel_id == @project_filter }
|
|
14
|
-
if @expected.empty?
|
|
15
|
-
possible = original.map { |e| e.project.kennel_id }.uniq.sort
|
|
16
|
-
raise "#{@project_filter} does not match any projects, try any of these:\n#{possible.join("\n")}"
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
@expected.each { |e| add_tracking_id e }
|
|
20
11
|
calculate_diff
|
|
21
12
|
prevent_irreversible_partial_updates
|
|
22
13
|
end
|
|
@@ -38,20 +29,28 @@ module Kennel
|
|
|
38
29
|
|
|
39
30
|
def update
|
|
40
31
|
each_resolved @create do |_, e|
|
|
32
|
+
message = "#{e.class.api_resource} #{e.tracking_id}"
|
|
33
|
+
Kennel.out.puts "Creating #{message}"
|
|
41
34
|
reply = @api.create e.class.api_resource, e.as_json
|
|
35
|
+
cache_metadata reply, e.class
|
|
42
36
|
id = reply.fetch(:id)
|
|
43
|
-
populate_id_map [reply] # allow resolving ids we could previously no resolve
|
|
44
|
-
Kennel.out.puts "
|
|
37
|
+
populate_id_map [], [reply] # allow resolving ids we could previously no resolve
|
|
38
|
+
Kennel.out.puts "#{LINE_UP}Created #{message} #{e.class.url(id)}"
|
|
45
39
|
end
|
|
46
40
|
|
|
47
41
|
each_resolved @update do |id, e|
|
|
42
|
+
message = "#{e.class.api_resource} #{e.tracking_id} #{e.class.url(id)}"
|
|
43
|
+
Kennel.out.puts "Updating #{message}"
|
|
48
44
|
@api.update e.class.api_resource, id, e.as_json
|
|
49
|
-
Kennel.out.puts "
|
|
45
|
+
Kennel.out.puts "#{LINE_UP}Updated #{message}"
|
|
50
46
|
end
|
|
51
47
|
|
|
52
48
|
@delete.each do |id, _, a|
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
klass = a.fetch(:klass)
|
|
50
|
+
message = "#{klass.api_resource} #{a.fetch(:tracking_id)} #{id}"
|
|
51
|
+
Kennel.out.puts "Deleting #{message}"
|
|
52
|
+
@api.delete klass.api_resource, id
|
|
53
|
+
Kennel.out.puts "#{LINE_UP}Deleted #{message}"
|
|
55
54
|
end
|
|
56
55
|
end
|
|
57
56
|
|
|
@@ -99,14 +98,13 @@ module Kennel
|
|
|
99
98
|
|
|
100
99
|
actual = Progress.progress("Downloading definitions") { download_definitions }
|
|
101
100
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
Progress.progress "Diffing" do
|
|
102
|
+
populate_id_map @expected, actual
|
|
103
|
+
filter_actual_by_project! actual
|
|
104
|
+
resolve_linked_tracking_ids! @expected # resolve dependencies to avoid diff
|
|
106
105
|
|
|
107
|
-
|
|
106
|
+
@expected.each(&:add_tracking_id) # avoid diff with actual
|
|
108
107
|
|
|
109
|
-
Progress.progress "Diffing" do
|
|
110
108
|
items = actual.map do |a|
|
|
111
109
|
e = matching_expected(a)
|
|
112
110
|
if e && @expected.delete(e)
|
|
@@ -117,9 +115,8 @@ module Kennel
|
|
|
117
115
|
end
|
|
118
116
|
|
|
119
117
|
# fill details of things we need to compare
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
detailed.each { |api_resource, actuals| @api.fill_details! api_resource, actuals }
|
|
118
|
+
details = items.map { |e, a| a if e && e.class.api_resource == "dashboard" }.compact
|
|
119
|
+
@api.fill_details! "dashboard", details
|
|
123
120
|
|
|
124
121
|
# pick out things to update or delete
|
|
125
122
|
items.each do |e, a|
|
|
@@ -127,26 +124,30 @@ module Kennel
|
|
|
127
124
|
if e
|
|
128
125
|
diff = e.diff(a)
|
|
129
126
|
@update << [id, e, a, diff] if diff.any?
|
|
130
|
-
elsif tracking_id
|
|
127
|
+
elsif a.fetch(:tracking_id) # was previously managed
|
|
131
128
|
@delete << [id, nil, a]
|
|
132
129
|
end
|
|
133
130
|
end
|
|
134
131
|
|
|
135
132
|
ensure_all_ids_found
|
|
136
133
|
@create = @expected.map { |e| [nil, e] }
|
|
134
|
+
@delete.sort_by! { |_, _, a| DELETE_ORDER.index a.fetch(:klass).api_resource }
|
|
137
135
|
end
|
|
138
|
-
|
|
139
|
-
@delete.sort_by! { |_, _, a| DELETE_ORDER.index a.fetch(:api_resource) }
|
|
140
136
|
end
|
|
141
137
|
|
|
142
138
|
def download_definitions
|
|
143
|
-
Utils.parallel(Models::Record.subclasses
|
|
144
|
-
results = @api.list(api_resource, with_downtimes: false) # lookup monitors without adding unnecessary downtime information
|
|
139
|
+
Utils.parallel(Models::Record.subclasses) do |klass|
|
|
140
|
+
results = @api.list(klass.api_resource, with_downtimes: false) # lookup monitors without adding unnecessary downtime information
|
|
145
141
|
results = results[results.keys.first] if results.is_a?(Hash) # dashboards are nested in {dashboards: []}
|
|
146
|
-
results.each { |
|
|
142
|
+
results.each { |a| cache_metadata(a, klass) }
|
|
147
143
|
end.flatten(1)
|
|
148
144
|
end
|
|
149
145
|
|
|
146
|
+
def cache_metadata(a, klass)
|
|
147
|
+
a[:klass] = klass
|
|
148
|
+
a[:tracking_id] = a.fetch(:klass).parse_tracking_id(a)
|
|
149
|
+
end
|
|
150
|
+
|
|
150
151
|
def ensure_all_ids_found
|
|
151
152
|
@expected.each do |e|
|
|
152
153
|
next unless id = e.id
|
|
@@ -158,7 +159,7 @@ module Kennel
|
|
|
158
159
|
def matching_expected(a)
|
|
159
160
|
# index list by all the thing we look up by: tracking id and actual id
|
|
160
161
|
@lookup_map ||= @expected.each_with_object({}) do |e, all|
|
|
161
|
-
keys = [
|
|
162
|
+
keys = [e.tracking_id]
|
|
162
163
|
keys << "#{e.class.api_resource}:#{e.id}" if e.id
|
|
163
164
|
keys.compact.each do |key|
|
|
164
165
|
raise "Lookup #{key} is duplicated" if all[key]
|
|
@@ -166,14 +167,15 @@ module Kennel
|
|
|
166
167
|
end
|
|
167
168
|
end
|
|
168
169
|
|
|
169
|
-
|
|
170
|
+
klass = a.fetch(:klass)
|
|
171
|
+
@lookup_map["#{klass.api_resource}:#{a.fetch(:id)}"] || @lookup_map[a.fetch(:tracking_id)]
|
|
170
172
|
end
|
|
171
173
|
|
|
172
174
|
def print_plan(step, list, color)
|
|
173
175
|
return if list.empty?
|
|
174
176
|
list.each do |_, e, a, diff|
|
|
175
|
-
|
|
176
|
-
Kennel.out.puts Utils.color(color, "#{step} #{api_resource} #{e&.tracking_id || tracking_id
|
|
177
|
+
klass = (e ? e.class : a.fetch(:klass))
|
|
178
|
+
Kennel.out.puts Utils.color(color, "#{step} #{klass.api_resource} #{e&.tracking_id || a.fetch(:tracking_id)}")
|
|
177
179
|
print_diff(diff) if diff # only for update
|
|
178
180
|
end
|
|
179
181
|
end
|
|
@@ -199,76 +201,47 @@ module Kennel
|
|
|
199
201
|
end
|
|
200
202
|
end
|
|
201
203
|
|
|
202
|
-
#
|
|
203
|
-
#
|
|
204
|
-
#
|
|
205
|
-
#
|
|
206
|
-
# Note: ideally we'd never add tracking in the first place, but at that point we do not know the diff yet
|
|
204
|
+
# - do not add tracking-id when working with existing ids on a branch,
|
|
205
|
+
# so resource do not get deleted when running an update on master (for example merge->CI)
|
|
206
|
+
# - make sure the diff is clean, by kicking out the now noop-update
|
|
207
|
+
# - ideally we'd never add tracking in the first place, but when adding tracking we do not know the diff yet
|
|
207
208
|
def prevent_irreversible_partial_updates
|
|
208
209
|
return unless @project_filter
|
|
209
210
|
@update.select! do |_, e, _, diff|
|
|
210
|
-
next true unless e.id #
|
|
211
|
+
next true unless e.id # safe to add tracking when not having id
|
|
211
212
|
|
|
212
213
|
diff.select! do |field_diff|
|
|
213
|
-
(_, field,
|
|
214
|
-
|
|
214
|
+
(_, field, actual) = field_diff
|
|
215
|
+
# TODO: refactor this so TRACKING_FIELD stays record-private
|
|
216
|
+
next true if e.class::TRACKING_FIELD != field.to_sym # need to sym here because Hashdiff produces strings
|
|
217
|
+
next true if e.class.parse_tracking_id(field.to_sym => actual) # already has tracking id
|
|
215
218
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
else
|
|
219
|
-
field_diff[3] = remove_tracking_id(e) # make plan output match update
|
|
220
|
-
old != field_diff[3]
|
|
221
|
-
end
|
|
219
|
+
field_diff[3] = e.remove_tracking_id # make `rake plan` output match what we are sending
|
|
220
|
+
actual != field_diff[3] # discard diff if now nothing changes
|
|
222
221
|
end
|
|
223
222
|
|
|
224
223
|
!diff.empty?
|
|
225
224
|
end
|
|
226
225
|
end
|
|
227
226
|
|
|
228
|
-
def populate_id_map(actual)
|
|
229
|
-
actual.each
|
|
227
|
+
def populate_id_map(expected, actual)
|
|
228
|
+
actual.each do |a|
|
|
229
|
+
next unless tracking_id = a.fetch(:tracking_id)
|
|
230
|
+
@id_map[tracking_id] = a.fetch(:id)
|
|
231
|
+
end
|
|
232
|
+
expected.each { |e| @id_map[e.tracking_id] ||= :new }
|
|
230
233
|
end
|
|
231
234
|
|
|
232
235
|
def resolve_linked_tracking_ids!(list, force: false)
|
|
233
236
|
list.each { |e| e.resolve_linked_tracking_ids!(@id_map, force: force) }
|
|
234
237
|
end
|
|
235
238
|
|
|
236
|
-
def
|
|
239
|
+
def filter_actual_by_project!(actual)
|
|
237
240
|
return unless @project_filter
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
!
|
|
241
|
+
actual.select! do |a|
|
|
242
|
+
tracking_id = a.fetch(:tracking_id)
|
|
243
|
+
!tracking_id || tracking_id.start_with?("#{@project_filter}:")
|
|
241
244
|
end
|
|
242
245
|
end
|
|
243
|
-
|
|
244
|
-
def add_tracking_id(e)
|
|
245
|
-
json = e.as_json
|
|
246
|
-
field = tracking_field(json)
|
|
247
|
-
raise "remove \"-- Managed by kennel\" line it from #{field} to copy a resource" if tracking_value(json[field])
|
|
248
|
-
json[field] = "#{json[field]}\n-- Managed by kennel #{e.tracking_id} in #{e.project.class.file_location}, do not modify manually".lstrip
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
def remove_tracking_id(e)
|
|
252
|
-
json = e.as_json
|
|
253
|
-
field = tracking_field(json)
|
|
254
|
-
value = json[field]
|
|
255
|
-
json[field] = value.dup.sub!(/\n?-- Managed by kennel .*/, "") || raise("did not find tracking id in #{value}")
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
def tracking_id(a)
|
|
259
|
-
tracking_value a[tracking_field(a)]
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
def tracking_value(content)
|
|
263
|
-
content.to_s[/-- Managed by kennel (\S+:\S+)/, 1]
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
def tracking_field(a)
|
|
267
|
-
TRACKING_FIELDS.detect { |f| a.key?(f) }
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
def tracking_field?(field)
|
|
271
|
-
TRACKING_FIELDS.include?(field.to_sym)
|
|
272
|
-
end
|
|
273
246
|
end
|
|
274
247
|
end
|
data/lib/kennel/tasks.rb
CHANGED
|
@@ -138,7 +138,7 @@ namespace :kennel do
|
|
|
138
138
|
resources.each do |resource|
|
|
139
139
|
Kennel::Progress.progress("Downloading #{resource}") do
|
|
140
140
|
list = api.list(resource)
|
|
141
|
-
api.fill_details!(resource, list)
|
|
141
|
+
api.fill_details!(resource, list) if resource == "dashboard"
|
|
142
142
|
end
|
|
143
143
|
list.each do |r|
|
|
144
144
|
r[:api_resource] = resource
|
|
@@ -36,7 +36,8 @@ module Kennel
|
|
|
36
36
|
|
|
37
37
|
def widget_queries(widget)
|
|
38
38
|
requests = widget.dig(:definition, :requests) || []
|
|
39
|
-
|
|
39
|
+
return requests.values.map { |r| r[:q] } if requests.is_a?(Hash) # hostmap widgets have hash requests
|
|
40
|
+
requests.flat_map { |r| r[:q] || r[:queries]&.map { |q| q[:query] } } # old format with q: or queries: [{query:}]
|
|
40
41
|
end
|
|
41
42
|
end
|
|
42
43
|
end
|
data/lib/kennel/version.rb
CHANGED
data/template/Readme.md
CHANGED
|
@@ -96,6 +96,7 @@ end
|
|
|
96
96
|
- use [datadog monitor UI](https://app.datadoghq.com/monitors/manage) to find a monitor
|
|
97
97
|
- get the `id` from the url
|
|
98
98
|
- run `URL='https://app.datadoghq.com/monitors/123' bundle exec rake kennel:import` and copy the output
|
|
99
|
+
- import task also works with SLO alerts, e.g. `URL='https://app.datadoghq.com/slo/edit/123abc456def123/alerts/789' bundle exec rake kennel:import`
|
|
99
100
|
- find or create a project in `projects/`
|
|
100
101
|
- add the monitor to `parts: [` list, for example:
|
|
101
102
|
```Ruby
|
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.90.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: 2021-
|
|
11
|
+
date: 2021-07-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|