kennel 1.75.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Readme.md +289 -0
- data/lib/kennel.rb +90 -0
- data/lib/kennel/api.rb +83 -0
- data/lib/kennel/file_cache.rb +53 -0
- data/lib/kennel/github_reporter.rb +49 -0
- data/lib/kennel/importer.rb +135 -0
- data/lib/kennel/models/base.rb +29 -0
- data/lib/kennel/models/dashboard.rb +209 -0
- data/lib/kennel/models/monitor.rb +219 -0
- data/lib/kennel/models/project.rb +31 -0
- data/lib/kennel/models/record.rb +94 -0
- data/lib/kennel/models/slo.rb +92 -0
- data/lib/kennel/models/team.rb +12 -0
- data/lib/kennel/optional_validations.rb +21 -0
- data/lib/kennel/progress.rb +34 -0
- data/lib/kennel/settings_as_methods.rb +86 -0
- data/lib/kennel/subclass_tracking.rb +19 -0
- data/lib/kennel/syncer.rb +260 -0
- data/lib/kennel/tasks.rb +148 -0
- data/lib/kennel/template_variables.rb +38 -0
- data/lib/kennel/unmuted_alerts.rb +89 -0
- data/lib/kennel/utils.rb +159 -0
- data/lib/kennel/version.rb +4 -0
- data/template/Readme.md +247 -0
- metadata +109 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Kennel
|
3
|
+
module OptionalValidations
|
4
|
+
def self.included(base)
|
5
|
+
base.settings :validate
|
6
|
+
base.defaults(validate: -> { true })
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def validate_json(data)
|
12
|
+
bad = Kennel::Utils.all_keys(data).grep_v(Symbol)
|
13
|
+
return if bad.empty?
|
14
|
+
invalid!(
|
15
|
+
"Only use Symbols as hash keys to avoid permanent diffs when updating.\n" \
|
16
|
+
"Change these keys to be symbols (usually 'foo' => 1 --> 'foo': 1)\n" \
|
17
|
+
"#{bad.map(&:inspect).join("\n")}"
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "benchmark"
|
3
|
+
|
4
|
+
module Kennel
|
5
|
+
class Progress
|
6
|
+
# print what we are doing and a spinner until it is done ... then show how long it took
|
7
|
+
def self.progress(name)
|
8
|
+
Kennel.err.print "#{name} ... "
|
9
|
+
|
10
|
+
animation = "-\\|/"
|
11
|
+
count = 0
|
12
|
+
stop = false
|
13
|
+
result = nil
|
14
|
+
|
15
|
+
spinner = Thread.new do
|
16
|
+
loop do
|
17
|
+
break if stop
|
18
|
+
Kennel.err.print animation[count % animation.size]
|
19
|
+
sleep 0.2
|
20
|
+
Kennel.err.print "\b"
|
21
|
+
count += 1
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
time = Benchmark.realtime { result = yield }
|
26
|
+
|
27
|
+
stop = true
|
28
|
+
spinner.join
|
29
|
+
Kennel.err.print "#{time.round(2)}s\n"
|
30
|
+
|
31
|
+
result
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Kennel
|
3
|
+
module SettingsAsMethods
|
4
|
+
SETTING_OVERRIDABLE_METHODS = [].freeze
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
base.extend ClassMethods
|
8
|
+
base.instance_variable_set(:@settings, [])
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def settings(*names)
|
13
|
+
duplicates = (@settings & names)
|
14
|
+
if duplicates.any?
|
15
|
+
raise ArgumentError, "Settings #{duplicates.map(&:inspect).join(", ")} are already defined"
|
16
|
+
end
|
17
|
+
|
18
|
+
overrides = ((instance_methods - self::SETTING_OVERRIDABLE_METHODS) & names)
|
19
|
+
if overrides.any?
|
20
|
+
raise ArgumentError, "Settings #{overrides.map(&:inspect).join(", ")} are already used as methods"
|
21
|
+
end
|
22
|
+
|
23
|
+
@settings.concat names
|
24
|
+
|
25
|
+
names.each do |name|
|
26
|
+
next if method_defined?(name)
|
27
|
+
define_method name do
|
28
|
+
raise_with_location ArgumentError, "'#{name}' on #{self.class} was not set or passed as option"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def defaults(options)
|
34
|
+
options.each do |name, block|
|
35
|
+
validate_setting_exist name
|
36
|
+
define_method name, &block
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def validate_setting_exist(name)
|
43
|
+
return if !@settings || @settings.include?(name)
|
44
|
+
supported = @settings.map(&:inspect)
|
45
|
+
raise ArgumentError, "Unsupported setting #{name.inspect}, supported settings are #{supported.join(", ")}"
|
46
|
+
end
|
47
|
+
|
48
|
+
def inherited(child)
|
49
|
+
super
|
50
|
+
child.instance_variable_set(:@settings, (@settings || []).dup)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def initialize(options = {})
|
55
|
+
super()
|
56
|
+
|
57
|
+
unless options.is_a?(Hash)
|
58
|
+
raise ArgumentError, "Expected #{self.class.name}.new options to be a Hash, got a #{options.class}"
|
59
|
+
end
|
60
|
+
|
61
|
+
options.each do |k, v|
|
62
|
+
next if v.class == Proc
|
63
|
+
raise ArgumentError, "Expected #{self.class.name}.new option :#{k} to be Proc, for example `#{k}: -> { 12 }`"
|
64
|
+
end
|
65
|
+
|
66
|
+
options.each do |name, block|
|
67
|
+
self.class.send :validate_setting_exist, name
|
68
|
+
define_singleton_method name, &block
|
69
|
+
end
|
70
|
+
|
71
|
+
# need expand_path so it works wih rake and when run individually
|
72
|
+
pwd = /^#{Regexp.escape(Dir.pwd)}\//
|
73
|
+
@invocation_location = caller.detect do |l|
|
74
|
+
if found = File.expand_path(l).sub!(pwd, "")
|
75
|
+
break found
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def raise_with_location(error, message)
|
81
|
+
message = message.dup
|
82
|
+
message << " on #{@invocation_location}" if @invocation_location
|
83
|
+
raise error, message
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Kennel
|
3
|
+
module SubclassTracking
|
4
|
+
def recursive_subclasses
|
5
|
+
subclasses + subclasses.flat_map(&:recursive_subclasses)
|
6
|
+
end
|
7
|
+
|
8
|
+
def subclasses
|
9
|
+
@subclasses ||= []
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def inherited(child)
|
15
|
+
super
|
16
|
+
subclasses << child
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,260 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Kennel
|
3
|
+
class Syncer
|
4
|
+
CACHE_FILE = "tmp/cache/details" # keep in sync with .travis.yml caching
|
5
|
+
TRACKING_FIELDS = [:message, :description].freeze
|
6
|
+
DELETE_ORDER = ["dashboard", "slo", "monitor"].freeze # dashboards references monitors + slos, slos reference monitors
|
7
|
+
|
8
|
+
def initialize(api, expected, project: nil)
|
9
|
+
@api = api
|
10
|
+
@project_filter = project
|
11
|
+
@expected = expected
|
12
|
+
if @project_filter
|
13
|
+
original = @expected
|
14
|
+
@expected = @expected.select { |e| e.project.kennel_id == @project_filter }
|
15
|
+
if @expected.empty?
|
16
|
+
possible = original.map { |e| e.project.kennel_id }.uniq.sort
|
17
|
+
raise "#{@project_filter} does not match any projects, try any of these:\n#{possible.join("\n")}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
@expected.each { |e| add_tracking_id e }
|
21
|
+
calculate_diff
|
22
|
+
prevent_irreversible_partial_updates
|
23
|
+
end
|
24
|
+
|
25
|
+
def plan
|
26
|
+
Kennel.out.puts "Plan:"
|
27
|
+
if noop?
|
28
|
+
Kennel.out.puts Utils.color(:green, "Nothing to do")
|
29
|
+
else
|
30
|
+
print_plan "Create", @create, :green
|
31
|
+
print_plan "Update", @update, :yellow
|
32
|
+
print_plan "Delete", @delete, :red
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def confirm
|
37
|
+
ENV["CI"] || !STDIN.tty? || Utils.ask("Execute Plan ?") unless noop?
|
38
|
+
end
|
39
|
+
|
40
|
+
def update
|
41
|
+
changed = (@create + @update).map { |_, e| e } unless @create.empty?
|
42
|
+
|
43
|
+
@create.each do |_, e|
|
44
|
+
e.resolve_linked_tracking_ids!({}, force: true)
|
45
|
+
|
46
|
+
reply = @api.create e.class.api_resource, e.as_json
|
47
|
+
id = reply.fetch(:id)
|
48
|
+
|
49
|
+
# resolve ids we could previously no resolve
|
50
|
+
changed.delete e
|
51
|
+
resolve_linked_tracking_ids! from: [reply], to: changed
|
52
|
+
|
53
|
+
Kennel.out.puts "Created #{e.class.api_resource} #{tracking_id(e.as_json)} #{e.url(id)}"
|
54
|
+
end
|
55
|
+
|
56
|
+
@update.each do |id, e|
|
57
|
+
e.resolve_linked_tracking_ids!({}, force: true)
|
58
|
+
@api.update e.class.api_resource, id, e.as_json
|
59
|
+
Kennel.out.puts "Updated #{e.class.api_resource} #{tracking_id(e.as_json)} #{e.url(id)}"
|
60
|
+
end
|
61
|
+
|
62
|
+
@delete.each do |id, _, a|
|
63
|
+
@api.delete a.fetch(:api_resource), id
|
64
|
+
Kennel.out.puts "Deleted #{a.fetch(:api_resource)} #{tracking_id(a)} #{id}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def noop?
|
71
|
+
@create.empty? && @update.empty? && @delete.empty?
|
72
|
+
end
|
73
|
+
|
74
|
+
def calculate_diff
|
75
|
+
@update = []
|
76
|
+
@delete = []
|
77
|
+
|
78
|
+
actual = Progress.progress("Downloading definitions") { download_definitions }
|
79
|
+
resolve_linked_tracking_ids! from: actual, to: @expected
|
80
|
+
filter_by_project! actual
|
81
|
+
|
82
|
+
Progress.progress "Diffing" do
|
83
|
+
items = actual.map do |a|
|
84
|
+
e = matching_expected(a)
|
85
|
+
if e && @expected.delete(e)
|
86
|
+
[e, a]
|
87
|
+
else
|
88
|
+
[nil, a]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
details_cache do |cache|
|
93
|
+
# fill details of things we need to compare (only do this part in parallel for safety & balancing)
|
94
|
+
Utils.parallel(items.select { |e, _| e && e.class::API_LIST_INCOMPLETE }) { |_, a| fill_details(a, cache) }
|
95
|
+
end
|
96
|
+
|
97
|
+
# pick out things to update or delete
|
98
|
+
items.each do |e, a|
|
99
|
+
id = a.fetch(:id)
|
100
|
+
if e
|
101
|
+
diff = e.diff(a)
|
102
|
+
@update << [id, e, a, diff] if diff.any?
|
103
|
+
elsif tracking_id(a) # was previously managed
|
104
|
+
@delete << [id, nil, a]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
ensure_all_ids_found
|
109
|
+
@create = @expected.map { |e| [nil, e] }
|
110
|
+
@create.sort_by! { |_, e| -DELETE_ORDER.index(e.class.api_resource) }
|
111
|
+
end
|
112
|
+
|
113
|
+
@delete.sort_by! { |_, _, a| DELETE_ORDER.index a.fetch(:api_resource) }
|
114
|
+
end
|
115
|
+
|
116
|
+
# Make diff work even though we cannot mass-fetch definitions
|
117
|
+
def fill_details(a, cache)
|
118
|
+
resource = a.fetch(:api_resource)
|
119
|
+
args = [resource, a.fetch(:id)]
|
120
|
+
full = cache.fetch(args, a[:modified] || a.fetch(:modified_at)) do
|
121
|
+
@api.show(*args)
|
122
|
+
end
|
123
|
+
a.merge!(full)
|
124
|
+
end
|
125
|
+
|
126
|
+
def details_cache(&block)
|
127
|
+
cache = FileCache.new CACHE_FILE, Kennel::VERSION
|
128
|
+
cache.open(&block)
|
129
|
+
end
|
130
|
+
|
131
|
+
def download_definitions
|
132
|
+
Utils.parallel(Models::Record.subclasses.map(&:api_resource)) do |api_resource|
|
133
|
+
results = @api.list(api_resource, with_downtimes: false) # lookup monitors without adding unnecessary downtime information
|
134
|
+
results = results[results.keys.first] if results.is_a?(Hash) # dashboards are nested in {dashboards: []}
|
135
|
+
results.each { |c| c[:api_resource] = api_resource } # store api resource for later diffing
|
136
|
+
end.flatten(1)
|
137
|
+
end
|
138
|
+
|
139
|
+
def ensure_all_ids_found
|
140
|
+
@expected.each do |e|
|
141
|
+
next unless id = e.id
|
142
|
+
raise "Unable to find existing #{e.class.api_resource} with id #{id}"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def matching_expected(a)
|
147
|
+
# index list by all the thing we look up by: tracking id and actual id
|
148
|
+
@lookup_map ||= @expected.each_with_object({}) do |e, all|
|
149
|
+
keys = [tracking_id(e.as_json)]
|
150
|
+
keys << "#{e.class.api_resource}:#{e.id}" if e.id
|
151
|
+
keys.compact.each do |key|
|
152
|
+
raise "Lookup #{key} is duplicated" if all[key]
|
153
|
+
all[key] = e
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
@lookup_map["#{a.fetch(:api_resource)}:#{a.fetch(:id)}"] || @lookup_map[tracking_id(a)]
|
158
|
+
end
|
159
|
+
|
160
|
+
def print_plan(step, list, color)
|
161
|
+
return if list.empty?
|
162
|
+
list.each do |_, e, a, diff|
|
163
|
+
api_resource = (e ? e.class.api_resource : a.fetch(:api_resource))
|
164
|
+
Kennel.out.puts Utils.color(color, "#{step} #{api_resource} #{e&.tracking_id || tracking_id(a)}")
|
165
|
+
print_diff(diff) if diff # only for update
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def print_diff(diff)
|
170
|
+
diff.each do |type, field, old, new|
|
171
|
+
if type == "+"
|
172
|
+
temp = Utils.pretty_inspect(new)
|
173
|
+
new = Utils.pretty_inspect(old)
|
174
|
+
old = temp
|
175
|
+
else # ~ and -
|
176
|
+
old = Utils.pretty_inspect(old)
|
177
|
+
new = Utils.pretty_inspect(new)
|
178
|
+
end
|
179
|
+
|
180
|
+
if (old + new).size > 100
|
181
|
+
Kennel.out.puts " #{type}#{field}"
|
182
|
+
Kennel.out.puts " #{old} ->"
|
183
|
+
Kennel.out.puts " #{new}"
|
184
|
+
else
|
185
|
+
Kennel.out.puts " #{type}#{field} #{old} -> #{new}"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Do not add tracking-id when working with existing ids on a branch,
|
191
|
+
# so resource do not get deleted fr:om merges to master.
|
192
|
+
# Also make sure the diff still makes sense, by kicking out the now noop-update.
|
193
|
+
#
|
194
|
+
# Note: ideally we'd never add tracking in the first place, but at that point we do not know the diff yet
|
195
|
+
def prevent_irreversible_partial_updates
|
196
|
+
return unless @project_filter
|
197
|
+
@update.select! do |_, e, _, diff|
|
198
|
+
next true unless e.id # short circuit for performance
|
199
|
+
|
200
|
+
diff.select! do |field_diff|
|
201
|
+
(_, field, old, new) = field_diff
|
202
|
+
next true unless tracking_field?(field)
|
203
|
+
|
204
|
+
if (old_tracking = tracking_value(old))
|
205
|
+
old_tracking == tracking_value(new) || raise("do not update! (atm unreachable)")
|
206
|
+
else
|
207
|
+
field_diff[3] = remove_tracking_id(e) # make plan output match update
|
208
|
+
old != field_diff[3]
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
!diff.empty?
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def resolve_linked_tracking_ids!(from:, to:)
|
217
|
+
map = from.each_with_object({}) { |a, lookup| lookup[tracking_id(a)] = a.fetch(:id) }
|
218
|
+
to.each { |e| map[e.tracking_id] ||= :new }
|
219
|
+
to.each { |e| e.resolve_linked_tracking_ids!(map, force: false) }
|
220
|
+
end
|
221
|
+
|
222
|
+
def filter_by_project!(definitions)
|
223
|
+
return unless @project_filter
|
224
|
+
definitions.select! do |a|
|
225
|
+
id = tracking_id(a)
|
226
|
+
!id || id.start_with?("#{@project_filter}:")
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def add_tracking_id(e)
|
231
|
+
json = e.as_json
|
232
|
+
field = tracking_field(json)
|
233
|
+
raise "remove \"-- Managed by kennel\" line it from #{field} to copy a resource" if tracking_value(json[field])
|
234
|
+
json[field] = "#{json[field]}\n-- Managed by kennel #{e.tracking_id} in #{e.project.class.file_location}, do not modify manually".lstrip
|
235
|
+
end
|
236
|
+
|
237
|
+
def remove_tracking_id(e)
|
238
|
+
json = e.as_json
|
239
|
+
field = tracking_field(json)
|
240
|
+
value = json[field]
|
241
|
+
json[field] = value.dup.sub!(/\n?-- Managed by kennel .*/, "") || raise("did not find tracking id in #{value}")
|
242
|
+
end
|
243
|
+
|
244
|
+
def tracking_id(a)
|
245
|
+
tracking_value a[tracking_field(a)]
|
246
|
+
end
|
247
|
+
|
248
|
+
def tracking_value(content)
|
249
|
+
content.to_s[/-- Managed by kennel (\S+:\S+)/, 1]
|
250
|
+
end
|
251
|
+
|
252
|
+
def tracking_field(a)
|
253
|
+
TRACKING_FIELDS.detect { |f| a.key?(f) }
|
254
|
+
end
|
255
|
+
|
256
|
+
def tracking_field?(field)
|
257
|
+
TRACKING_FIELDS.include?(field.to_sym)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
data/lib/kennel/tasks.rb
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "English"
|
3
|
+
require "kennel"
|
4
|
+
require "kennel/unmuted_alerts"
|
5
|
+
require "kennel/importer"
|
6
|
+
|
7
|
+
module Kennel
|
8
|
+
module Tasks
|
9
|
+
class << self
|
10
|
+
def abort(message = nil)
|
11
|
+
Kennel.err.puts message if message
|
12
|
+
raise SystemExit.new(1), message
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
namespace :kennel do
|
19
|
+
desc "Ensure there are no uncommited changes that would be hidden from PR reviewers"
|
20
|
+
task no_diff: :generate do
|
21
|
+
result = `git status --porcelain generated/`.strip
|
22
|
+
Kennel::Tasks.abort "Diff found:\n#{result}\nrun `rake generate` and commit the diff to fix" unless result == ""
|
23
|
+
Kennel::Tasks.abort "Error during diffing" unless $CHILD_STATUS.success?
|
24
|
+
end
|
25
|
+
|
26
|
+
# ideally do this on every run, but it's slow (~1.5s) and brittle (might not find all + might find false-positives)
|
27
|
+
# https://help.datadoghq.com/hc/en-us/requests/254114 for automatic validation
|
28
|
+
desc "Verify that all used monitor mentions are valid"
|
29
|
+
task validate_mentions: :environment do
|
30
|
+
known = Kennel.send(:api)
|
31
|
+
.send(:request, :get, "/monitor/notifications")
|
32
|
+
.fetch(:handles)
|
33
|
+
.values
|
34
|
+
.flatten(1)
|
35
|
+
.map { |v| v.fetch(:value) }
|
36
|
+
|
37
|
+
known += ENV["KNOWN"].to_s.split(",")
|
38
|
+
|
39
|
+
bad = []
|
40
|
+
Dir["generated/**/*.json"].each do |f|
|
41
|
+
next unless message = JSON.parse(File.read(f))["message"]
|
42
|
+
used = message.scan(/\s(@[^\s{,'"]+)/).flatten(1)
|
43
|
+
.grep(/^@.*@|^@.*-/) # ignore @here etc handles ... datadog uses @foo@bar.com for emails and @foo-bar for integrations
|
44
|
+
(used - known).each { |v| bad << [f, v] }
|
45
|
+
end
|
46
|
+
|
47
|
+
if bad.any?
|
48
|
+
url = Kennel::Utils.path_to_url "/account/settings"
|
49
|
+
puts "Invalid mentions found, either ignore them by adding to `KNOWN` env var or add them via #{url}"
|
50
|
+
bad.each { |f, v| puts "Invalid mention #{v} in monitor message of #{f}" }
|
51
|
+
Kennel::Tasks.abort
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
desc "generate local definitions"
|
56
|
+
task generate: :environment do
|
57
|
+
Kennel.generate
|
58
|
+
end
|
59
|
+
|
60
|
+
# also generate parts so users see and commit updated generated automatically
|
61
|
+
desc "show planned datadog changes (scope with PROJECT=name)"
|
62
|
+
task plan: :generate do
|
63
|
+
Kennel.plan
|
64
|
+
end
|
65
|
+
|
66
|
+
desc "update datadog (scope with PROJECT=name)"
|
67
|
+
task update_datadog: :environment do
|
68
|
+
Kennel.update
|
69
|
+
end
|
70
|
+
|
71
|
+
desc "update on push to the default branch, otherwise show plan"
|
72
|
+
task :ci do
|
73
|
+
branch = (ENV["TRAVIS_BRANCH"] || ENV["GITHUB_REF"]).to_s.sub(/^refs\/heads\//, "")
|
74
|
+
on_default_branch = (branch == (ENV["DEFAULT_BRANCH"] || "master"))
|
75
|
+
is_push = (ENV["TRAVIS_PULL_REQUEST"] == "false" || ENV["GITHUB_EVENT_NAME"] == "push")
|
76
|
+
task_name =
|
77
|
+
if on_default_branch && is_push
|
78
|
+
"kennel:update_datadog"
|
79
|
+
else
|
80
|
+
"kennel:plan" # show plan in CI logs
|
81
|
+
end
|
82
|
+
|
83
|
+
Rake::Task[task_name].invoke
|
84
|
+
end
|
85
|
+
|
86
|
+
desc "show unmuted alerts filtered by TAG, for example TAG=team:foo"
|
87
|
+
task alerts: :environment do
|
88
|
+
tag = ENV["TAG"] || Kennel::Tasks.abort("Call with TAG=foo:bar")
|
89
|
+
Kennel::UnmutedAlerts.print(Kennel.send(:api), tag)
|
90
|
+
end
|
91
|
+
|
92
|
+
desc "show monitors with no data by TAG, for example TAG=team:foo"
|
93
|
+
task nodata: :environment do
|
94
|
+
tag = ENV["TAG"] || Kennel::Tasks.abort("Call with TAG=foo:bar")
|
95
|
+
monitors = Kennel.send(:api).list("monitor", monitor_tags: tag, group_states: "no data")
|
96
|
+
monitors.select! { |m| m[:overall_state] == "No Data" }
|
97
|
+
monitors.reject! { |m| m[:tags].include? "nodata:ignore" }
|
98
|
+
if monitors.any?
|
99
|
+
Kennel.err.puts <<~TEXT
|
100
|
+
This is a useful task to find monitors that have mis-spelled metrics or never received data at any time.
|
101
|
+
To ignore monitors with nodata, tag the monitor with "nodata:ignore"
|
102
|
+
|
103
|
+
TEXT
|
104
|
+
end
|
105
|
+
|
106
|
+
monitors.each do |m|
|
107
|
+
Kennel.out.puts m[:name]
|
108
|
+
Kennel.out.puts Kennel::Utils.path_to_url("/monitors/#{m[:id]}")
|
109
|
+
Kennel.out.puts
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
desc "Convert existing resources to copy-pasteable definitions to import existing resources (call with URL= or call with RESOURCE= and ID=)"
|
114
|
+
task import: :environment do
|
115
|
+
if (id = ENV["ID"]) && (resource = ENV["RESOURCE"])
|
116
|
+
id = Integer(id) if id =~ /^\d+$/ # dashboards can have alphanumeric ids
|
117
|
+
elsif (url = ENV["URL"])
|
118
|
+
resource, id = Kennel::Models::Record.parse_any_url(url) || Kennel::Tasks.abort("Unable to parse url")
|
119
|
+
else
|
120
|
+
possible_resources = Kennel::Models::Record.subclasses.map(&:api_resource)
|
121
|
+
Kennel::Tasks.abort("Call with URL= or call with RESOURCE=#{possible_resources.join(" or ")} and ID=")
|
122
|
+
end
|
123
|
+
|
124
|
+
Kennel.out.puts Kennel::Importer.new(Kennel.send(:api)).import(resource, id)
|
125
|
+
end
|
126
|
+
|
127
|
+
desc "Dump ALL of datadog config as raw json ... useful for grep/search TYPE=slo|monitor|dashboard"
|
128
|
+
task dump: :environment do
|
129
|
+
Kennel.send(:api).list(ENV.fetch("TYPE")).each do |r|
|
130
|
+
Kennel.out.puts JSON.pretty_generate(r)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
task :environment do
|
135
|
+
require "kennel"
|
136
|
+
gem "dotenv"
|
137
|
+
require "dotenv"
|
138
|
+
source = ".env"
|
139
|
+
|
140
|
+
# warn when users have things like DATADOG_TOKEN already set and it will not be loaded from .env
|
141
|
+
unless ENV["KENNEL_SILENCE_UPDATED_ENV"]
|
142
|
+
updated = Dotenv.parse(source).select { |k, v| ENV[k] && ENV[k] != v }
|
143
|
+
warn "Environment variables #{updated.keys.join(", ")} need to be unset to be sourced from #{source}" if updated.any?
|
144
|
+
end
|
145
|
+
|
146
|
+
Dotenv.load(source)
|
147
|
+
end
|
148
|
+
end
|