kennel 1.128.0 → 1.130.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 +12 -4
- data/lib/kennel/attribute_differ.rb +83 -0
- data/lib/kennel/console.rb +66 -0
- data/lib/kennel/github_reporter.rb +1 -1
- data/lib/kennel/importer.rb +2 -2
- data/lib/kennel/models/base.rb +1 -1
- data/lib/kennel/models/record.rb +5 -3
- data/lib/kennel/models/synthetic_test.rb +1 -2
- data/lib/kennel/parts_serializer.rb +18 -17
- data/lib/kennel/string_utils.rb +35 -0
- data/lib/kennel/syncer.rb +75 -127
- data/lib/kennel/unmuted_alerts.rb +1 -1
- data/lib/kennel/utils.rb +0 -104
- data/lib/kennel/version.rb +1 -1
- data/lib/kennel.rb +16 -17
- metadata +11 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d6ee3df09b69dd5a7af7285c219f4415c4e94ce2ab4f6533c5fff03be848e9c4
|
4
|
+
data.tar.gz: d7f923b69ad9b63774141ac0e9c6c04de7a26a67abd732c41c5f19d478704656
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7c967bf54743cc757a0c0c911e981701d4b5b393f6ebac66538432541c7c6dfecef2cd8c1c0e5389f90c7d0b91a54e79a29e0e1abf9d16fd67adad4bcd6209fe
|
7
|
+
data.tar.gz: 297239ea9305e4eadbe40a363160b0ca2aaf70dad237f95a448b00b30d3db9ed9f707b2736b2596ef73e8981d333b600af87da40978f5d1c9fc8e0dee3d7f9ea
|
data/lib/kennel/api.rb
CHANGED
@@ -5,6 +5,14 @@ module Kennel
|
|
5
5
|
class Api
|
6
6
|
CACHE_FILE = "tmp/cache/details"
|
7
7
|
|
8
|
+
def self.tag(api_resource, reply)
|
9
|
+
klass = Models::Record.api_resource_map[api_resource]
|
10
|
+
reply.merge(
|
11
|
+
klass: klass,
|
12
|
+
tracking_id: klass.parse_tracking_id(reply)
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
8
16
|
def initialize(app_key = nil, api_key = nil)
|
9
17
|
@app_key = app_key || ENV.fetch("DATADOG_APP_KEY")
|
10
18
|
@api_key = api_key || ENV.fetch("DATADOG_API_KEY")
|
@@ -16,7 +24,7 @@ module Kennel
|
|
16
24
|
response = request :get, "/api/v1/#{api_resource}/#{id}", params: params
|
17
25
|
response = response.fetch(:data) if api_resource == "slo"
|
18
26
|
response[:id] = response.delete(:public_id) if api_resource == "synthetics/tests"
|
19
|
-
response
|
27
|
+
self.class.tag(api_resource, response)
|
20
28
|
end
|
21
29
|
|
22
30
|
def list(api_resource, params = {})
|
@@ -32,7 +40,7 @@ module Kennel
|
|
32
40
|
# ignore monitor synthetics create and that inherit the kennel_id, we do not directly manage them
|
33
41
|
response.reject! { |m| m[:type] == "synthetics alert" } if api_resource == "monitor"
|
34
42
|
|
35
|
-
response
|
43
|
+
response.map { |r| self.class.tag(api_resource, r) }
|
36
44
|
end
|
37
45
|
end
|
38
46
|
|
@@ -40,13 +48,13 @@ module Kennel
|
|
40
48
|
response = request :post, "/api/v1/#{api_resource}", body: attributes
|
41
49
|
response = response.fetch(:data).first if api_resource == "slo"
|
42
50
|
response[:id] = response.delete(:public_id) if api_resource == "synthetics/tests"
|
43
|
-
response
|
51
|
+
self.class.tag(api_resource, response)
|
44
52
|
end
|
45
53
|
|
46
54
|
def update(api_resource, id, attributes)
|
47
55
|
response = request :put, "/api/v1/#{api_resource}/#{id}", body: attributes
|
48
56
|
response[:id] = response.delete(:public_id) if api_resource == "synthetics/tests"
|
49
|
-
response
|
57
|
+
self.class.tag(api_resource, response)
|
50
58
|
end
|
51
59
|
|
52
60
|
# - force=true to not dead-lock on dependent monitors+slos
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "diff/lcs"
|
4
|
+
|
5
|
+
module Kennel
|
6
|
+
class AttributeDiffer
|
7
|
+
def initialize
|
8
|
+
# min '2' because: -1 makes no sense, 0 does not work with * 2 math, 1 says '1 lines'
|
9
|
+
@max_diff_lines = [Integer(ENV.fetch("MAX_DIFF_LINES", "50")), 2].max
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def format(type, field, old, new = nil)
|
14
|
+
multiline = false
|
15
|
+
if type == "+"
|
16
|
+
temp = pretty_inspect(new)
|
17
|
+
new = pretty_inspect(old)
|
18
|
+
old = temp
|
19
|
+
elsif old.is_a?(String) && new.is_a?(String) && (old.include?("\n") || new.include?("\n"))
|
20
|
+
multiline = true
|
21
|
+
else # ~ and -
|
22
|
+
old = pretty_inspect(old)
|
23
|
+
new = pretty_inspect(new)
|
24
|
+
end
|
25
|
+
|
26
|
+
message =
|
27
|
+
if multiline
|
28
|
+
" #{type}#{field}\n" +
|
29
|
+
multiline_diff(old, new).map { |l| " #{l}" }.join("\n")
|
30
|
+
elsif (old + new).size > 100
|
31
|
+
" #{type}#{field}\n" \
|
32
|
+
" #{old} ->\n" \
|
33
|
+
" #{new}"
|
34
|
+
else
|
35
|
+
" #{type}#{field} #{old} -> #{new}"
|
36
|
+
end
|
37
|
+
|
38
|
+
truncate(message)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# display diff for multi-line strings
|
44
|
+
# must stay readable when color is off too
|
45
|
+
def multiline_diff(old, new)
|
46
|
+
Diff::LCS.sdiff(old.split("\n", -1), new.split("\n", -1)).flat_map do |diff|
|
47
|
+
case diff.action
|
48
|
+
when "-"
|
49
|
+
Console.color(:red, "- #{diff.old_element}")
|
50
|
+
when "+"
|
51
|
+
Console.color(:green, "+ #{diff.new_element}")
|
52
|
+
when "!"
|
53
|
+
[
|
54
|
+
Console.color(:red, "- #{diff.old_element}"),
|
55
|
+
Console.color(:green, "+ #{diff.new_element}")
|
56
|
+
]
|
57
|
+
else
|
58
|
+
" #{diff.old_element}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def truncate(message)
|
64
|
+
warning = Console.color(
|
65
|
+
:magenta,
|
66
|
+
" (Diff for this item truncated after #{@max_diff_lines} lines. " \
|
67
|
+
"Rerun with MAX_DIFF_LINES=#{@max_diff_lines * 2} to see more)"
|
68
|
+
)
|
69
|
+
StringUtils.truncate_lines(message, to: @max_diff_lines, warning: warning)
|
70
|
+
end
|
71
|
+
|
72
|
+
# TODO: use awesome-print or similar, but it has too many monkey-patches
|
73
|
+
# https://github.com/amazing-print/amazing_print/issues/36
|
74
|
+
def pretty_inspect(object)
|
75
|
+
string = object.inspect.dup
|
76
|
+
string.gsub!(/:([a-z_]+)=>/, "\\1: ")
|
77
|
+
10.times do
|
78
|
+
string.gsub!(/{(\S.*?\S)}/, "{ \\1 }") || break
|
79
|
+
end
|
80
|
+
string
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Kennel
|
3
|
+
module Console
|
4
|
+
COLORS = { red: 31, green: 32, yellow: 33, cyan: 36, magenta: 35, default: 0 }.freeze
|
5
|
+
|
6
|
+
class TeeIO < IO
|
7
|
+
def initialize(ios)
|
8
|
+
super(0) # called with fake file descriptor 0, so we can call super and get a proper class
|
9
|
+
@ios = ios
|
10
|
+
end
|
11
|
+
|
12
|
+
def write(string)
|
13
|
+
@ios.each { |io| io.write string }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class << self
|
18
|
+
def ask?(question)
|
19
|
+
Kennel.err.printf color(:red, "#{question} - press 'y' to continue: ", force: true)
|
20
|
+
begin
|
21
|
+
STDIN.gets.chomp == "y"
|
22
|
+
rescue Interrupt # do not show a backtrace if user decides to Ctrl+C here
|
23
|
+
Kennel.err.print "\n"
|
24
|
+
exit 1
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def color(color, text, force: false)
|
29
|
+
return text unless force || Kennel.out.tty?
|
30
|
+
|
31
|
+
"\e[#{COLORS.fetch(color)}m#{text}\e[0m"
|
32
|
+
end
|
33
|
+
|
34
|
+
def capture_stdout
|
35
|
+
old = Kennel.out
|
36
|
+
Kennel.out = StringIO.new
|
37
|
+
yield
|
38
|
+
Kennel.out.string
|
39
|
+
ensure
|
40
|
+
Kennel.out = old
|
41
|
+
end
|
42
|
+
|
43
|
+
def capture_stderr
|
44
|
+
old = Kennel.err
|
45
|
+
Kennel.err = StringIO.new
|
46
|
+
yield
|
47
|
+
Kennel.err.string
|
48
|
+
ensure
|
49
|
+
Kennel.err = old
|
50
|
+
end
|
51
|
+
|
52
|
+
def tee_output
|
53
|
+
old_stdout = Kennel.out
|
54
|
+
old_stderr = Kennel.err
|
55
|
+
capture = StringIO.new
|
56
|
+
Kennel.out = TeeIO.new([capture, Kennel.out])
|
57
|
+
Kennel.err = TeeIO.new([capture, Kennel.err])
|
58
|
+
yield
|
59
|
+
capture.string
|
60
|
+
ensure
|
61
|
+
Kennel.out = old_stdout
|
62
|
+
Kennel.err = old_stderr
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/kennel/importer.rb
CHANGED
@@ -33,7 +33,7 @@ module Kennel
|
|
33
33
|
model.remove_tracking_id(data)
|
34
34
|
tracking_id.split(":").last
|
35
35
|
else
|
36
|
-
Kennel::
|
36
|
+
Kennel::StringUtils.parameterize(title)
|
37
37
|
end
|
38
38
|
|
39
39
|
case resource
|
@@ -189,7 +189,7 @@ module Kennel
|
|
189
189
|
|
190
190
|
# important to the front and rest deterministic
|
191
191
|
def sort_hash(hash)
|
192
|
-
|
192
|
+
hash.sort_by { |k, _| [SORT_ORDER.index(k) || 999, k] }.to_h
|
193
193
|
end
|
194
194
|
end
|
195
195
|
end
|
data/lib/kennel/models/base.rb
CHANGED
data/lib/kennel/models/record.rb
CHANGED
@@ -35,8 +35,8 @@ module Kennel
|
|
35
35
|
:klass, :tracking_id # added by syncer.rb
|
36
36
|
].freeze
|
37
37
|
ALLOWED_KENNEL_ID_CHARS = "a-zA-Z_\\d.-"
|
38
|
-
ALLOWED_KENNEL_ID_FULL = "[#{ALLOWED_KENNEL_ID_CHARS}]+:[#{ALLOWED_KENNEL_ID_CHARS}]+"
|
39
|
-
ALLOWED_KENNEL_ID_REGEX = /\A#{ALLOWED_KENNEL_ID_FULL}\z
|
38
|
+
ALLOWED_KENNEL_ID_FULL = "[#{ALLOWED_KENNEL_ID_CHARS}]+:[#{ALLOWED_KENNEL_ID_CHARS}]+".freeze
|
39
|
+
ALLOWED_KENNEL_ID_REGEX = /\A#{ALLOWED_KENNEL_ID_FULL}\z/
|
40
40
|
|
41
41
|
settings :id, :kennel_id
|
42
42
|
|
@@ -52,7 +52,7 @@ module Kennel
|
|
52
52
|
end
|
53
53
|
|
54
54
|
def api_resource_map
|
55
|
-
subclasses.
|
55
|
+
subclasses.to_h { |s| [s.api_resource, s] }
|
56
56
|
end
|
57
57
|
|
58
58
|
def parse_tracking_id(a)
|
@@ -98,6 +98,8 @@ module Kennel
|
|
98
98
|
|
99
99
|
self.class.send(:normalize, expected, actual)
|
100
100
|
|
101
|
+
return [] if actual == expected # Hashdiff is slow, this is fast
|
102
|
+
|
101
103
|
# strict: ignore Integer vs Float
|
102
104
|
# similarity: show diff when not 100% similar
|
103
105
|
# use_lcs: saner output
|
@@ -3,8 +3,7 @@ module Kennel
|
|
3
3
|
module Models
|
4
4
|
class SyntheticTest < Record
|
5
5
|
TRACKING_FIELD = :message
|
6
|
-
DEFAULTS = {
|
7
|
-
}.freeze
|
6
|
+
DEFAULTS = {}.freeze
|
8
7
|
READONLY_ATTRIBUTES = superclass::READONLY_ATTRIBUTES + [:status, :monitor_id]
|
9
8
|
LOCATIONS = ["aws:ca-central-1", "aws:eu-north-1", "aws:eu-west-1", "aws:eu-west-3", "aws:eu-west-2", "aws:ap-south-1", "aws:us-west-2", "aws:us-west-1", "aws:sa-east-1", "aws:us-east-2", "aws:ap-northeast-1", "aws:ap-northeast-2", "aws:eu-central-1", "aws:ap-southeast-2", "aws:ap-southeast-1"].freeze
|
10
9
|
|
@@ -8,13 +8,9 @@ module Kennel
|
|
8
8
|
|
9
9
|
def write(parts)
|
10
10
|
Progress.progress "Storing" do
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
old = old_paths
|
15
|
-
used = write_changed(parts)
|
16
|
-
(old - used).uniq.each { |p| FileUtils.rm_rf(p) }
|
17
|
-
end
|
11
|
+
existing = existing_files_and_folders
|
12
|
+
used = write_changed(parts)
|
13
|
+
FileUtils.rm_rf(existing - used)
|
18
14
|
end
|
19
15
|
end
|
20
16
|
|
@@ -26,31 +22,36 @@ module Kennel
|
|
26
22
|
used = []
|
27
23
|
|
28
24
|
Utils.parallel(parts, max: 2) do |part|
|
29
|
-
path =
|
25
|
+
path = path_for_tracking_id(part.tracking_id)
|
30
26
|
|
31
|
-
used << File.dirname(path) #
|
27
|
+
used << File.dirname(path) # we have 1 level of sub folders, so this is enough
|
32
28
|
used << path
|
33
29
|
|
34
|
-
|
35
|
-
write_file_if_necessary(path,
|
30
|
+
content = part.as_json.merge(api_resource: part.class.api_resource)
|
31
|
+
write_file_if_necessary(path, content)
|
36
32
|
end
|
37
33
|
|
38
34
|
used
|
39
35
|
end
|
40
36
|
|
41
|
-
def
|
42
|
-
if filter.
|
43
|
-
filter.
|
37
|
+
def existing_files_and_folders
|
38
|
+
if filter.tracking_id_filter
|
39
|
+
filter.tracking_id_filter.map { |tracking_id| path_for_tracking_id(tracking_id) }
|
40
|
+
elsif filter.project_filter
|
41
|
+
filter.project_filter.flat_map { |project| Dir["generated/#{project}/*"] }
|
44
42
|
else
|
45
|
-
["generated"]
|
43
|
+
Dir["generated/**/*"] # also includes folders so we clean up empty directories
|
46
44
|
end
|
47
45
|
end
|
48
46
|
|
49
|
-
def
|
50
|
-
|
47
|
+
def path_for_tracking_id(tracking_id)
|
48
|
+
"generated/#{tracking_id.tr("/", ":").sub(":", "/")}.json"
|
51
49
|
end
|
52
50
|
|
53
51
|
def write_file_if_necessary(path, content)
|
52
|
+
# NOTE: always generating is faster than JSON.load-ing and comparing
|
53
|
+
content = JSON.pretty_generate(content) << "\n"
|
54
|
+
|
54
55
|
# 99% case
|
55
56
|
begin
|
56
57
|
return if File.read(path) == content
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Kennel
|
3
|
+
module StringUtils
|
4
|
+
class << self
|
5
|
+
def snake_case(string)
|
6
|
+
string
|
7
|
+
.gsub(/::/, "_") # Foo::Bar -> foo_bar
|
8
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2') # FOOBar -> foo_bar
|
9
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2') # fooBar -> foo_bar
|
10
|
+
.tr("-", "_") # foo-bar -> foo_bar
|
11
|
+
.downcase
|
12
|
+
end
|
13
|
+
|
14
|
+
# for child projects, not used internally
|
15
|
+
def title_case(string)
|
16
|
+
string.split(/[\s_]/).map(&:capitalize) * " "
|
17
|
+
end
|
18
|
+
|
19
|
+
# simplified version of https://apidock.com/rails/ActiveSupport/Inflector/parameterize
|
20
|
+
def parameterize(string)
|
21
|
+
string
|
22
|
+
.downcase
|
23
|
+
.gsub(/[^a-z0-9\-_]+/, "-") # remove unsupported
|
24
|
+
.gsub(/-{2,}/, "-") # remove duplicates
|
25
|
+
.gsub(/^-|-$/, "") # remove leading/trailing
|
26
|
+
end
|
27
|
+
|
28
|
+
def truncate_lines(text, to:, warning:)
|
29
|
+
lines = text.split(/\n/, to + 1)
|
30
|
+
lines[-1] = warning if lines.size > to
|
31
|
+
lines.join("\n")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/kennel/syncer.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "diff/lcs"
|
4
|
-
|
5
3
|
module Kennel
|
6
4
|
class Syncer
|
7
5
|
DELETE_ORDER = ["dashboard", "slo", "monitor", "synthetics/tests"].freeze # dashboards references monitors + slos, slos reference monitors
|
@@ -10,41 +8,47 @@ module Kennel
|
|
10
8
|
Plan = Struct.new(:changes, keyword_init: true)
|
11
9
|
Change = Struct.new(:type, :api_resource, :tracking_id, :id)
|
12
10
|
|
13
|
-
def initialize(api, expected, actual,
|
11
|
+
def initialize(api, expected, actual, strict_imports: true, project_filter: nil, tracking_id_filter: nil)
|
14
12
|
@api = api
|
15
|
-
@
|
13
|
+
@expected = Set.new expected # need Set to speed up deletion
|
14
|
+
@actual = actual
|
15
|
+
@strict_imports = strict_imports
|
16
16
|
@project_filter = project_filter
|
17
17
|
@tracking_id_filter = tracking_id_filter
|
18
|
-
|
19
|
-
@
|
20
|
-
|
21
|
-
|
18
|
+
|
19
|
+
@attribute_differ = AttributeDiffer.new
|
20
|
+
|
21
|
+
calculate_changes
|
22
|
+
validate_changes
|
22
23
|
prevent_irreversible_partial_updates
|
24
|
+
|
25
|
+
@warnings.each { |message| Kennel.out.puts Console.color(:yellow, "Warning: #{message}") }
|
23
26
|
end
|
24
27
|
|
25
28
|
def plan
|
26
|
-
Kennel.out.puts "Plan:"
|
27
|
-
if noop?
|
28
|
-
Kennel.out.puts Utils.color(:green, "Nothing to do")
|
29
|
-
else
|
30
|
-
@warnings.each { |message| Kennel.out.puts Utils.color(:yellow, "Warning: #{message}") }
|
31
|
-
print_plan "Create", @create, :green
|
32
|
-
print_plan "Update", @update, :yellow
|
33
|
-
print_plan "Delete", @delete, :red
|
34
|
-
end
|
35
|
-
|
36
29
|
Plan.new(
|
37
30
|
changes:
|
38
31
|
@create.map { |_id, e, _a| Change.new(:create, e.class.api_resource, e.tracking_id, nil) } +
|
39
|
-
|
40
|
-
|
32
|
+
@update.map { |id, e, _a| Change.new(:update, e.class.api_resource, e.tracking_id, id) } +
|
33
|
+
@delete.map { |id, _e, a| Change.new(:delete, a.fetch(:klass).api_resource, a.fetch(:tracking_id), id) }
|
41
34
|
)
|
42
35
|
end
|
43
36
|
|
37
|
+
def print_plan
|
38
|
+
Kennel.out.puts "Plan:"
|
39
|
+
if noop?
|
40
|
+
Kennel.out.puts Console.color(:green, "Nothing to do")
|
41
|
+
else
|
42
|
+
print_changes "Create", @create, :green
|
43
|
+
print_changes "Update", @update, :yellow
|
44
|
+
print_changes "Delete", @delete, :red
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
44
48
|
def confirm
|
45
49
|
return false if noop?
|
46
50
|
return true if ENV["CI"] || !STDIN.tty? || !Kennel.err.tty?
|
47
|
-
|
51
|
+
Console.ask?("Execute Plan ?")
|
48
52
|
end
|
49
53
|
|
50
54
|
def update
|
@@ -54,7 +58,6 @@ module Kennel
|
|
54
58
|
message = "#{e.class.api_resource} #{e.tracking_id}"
|
55
59
|
Kennel.out.puts "Creating #{message}"
|
56
60
|
reply = @api.create e.class.api_resource, e.as_json
|
57
|
-
Utils.inline_resource_metadata reply, e.class
|
58
61
|
id = reply.fetch(:id)
|
59
62
|
changes << Change.new(:create, e.class.api_resource, e.tracking_id, id)
|
60
63
|
populate_id_map [], [reply] # allow resolving ids we could previously no resolve
|
@@ -83,8 +86,6 @@ module Kennel
|
|
83
86
|
|
84
87
|
private
|
85
88
|
|
86
|
-
attr_reader :kennel
|
87
|
-
|
88
89
|
# loop over items until everything is resolved or crash when we get stuck
|
89
90
|
# this solves cases like composite monitors depending on each other or monitor->monitor slo->slo monitor chains
|
90
91
|
def each_resolved(list)
|
@@ -120,58 +121,67 @@ module Kennel
|
|
120
121
|
@create.empty? && @update.empty? && @delete.empty?
|
121
122
|
end
|
122
123
|
|
123
|
-
def
|
124
|
+
def calculate_changes
|
124
125
|
@warnings = []
|
125
|
-
@update = []
|
126
|
-
@delete = []
|
127
126
|
@id_map = IdMap.new
|
128
127
|
|
129
128
|
Progress.progress "Diffing" do
|
130
129
|
populate_id_map @expected, @actual
|
131
130
|
filter_actual! @actual
|
132
131
|
resolve_linked_tracking_ids! @expected # resolve dependencies to avoid diff
|
132
|
+
@expected.each(&:add_tracking_id) # avoid diff with actual, which has tracking_id
|
133
133
|
|
134
|
-
|
134
|
+
# see which expected match the actual
|
135
|
+
matching, unmatched_expected, unmatched_actual = partition_matched_expected
|
136
|
+
validate_expected_id_not_missing unmatched_expected
|
137
|
+
fill_details! matching # need details to diff later
|
135
138
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
[nil, a]
|
143
|
-
end
|
144
|
-
end
|
139
|
+
# update matching if needed
|
140
|
+
@update = matching.map do |e, a|
|
141
|
+
id = a.fetch(:id)
|
142
|
+
diff = e.diff(a)
|
143
|
+
[id, e, a, diff] if diff.any?
|
144
|
+
end.compact
|
145
145
|
|
146
|
-
#
|
147
|
-
|
148
|
-
@api.fill_details! "dashboard", details
|
146
|
+
# delete previously managed
|
147
|
+
@delete = unmatched_actual.map { |a| [a.fetch(:id), nil, a] if a.fetch(:tracking_id) }.compact
|
149
148
|
|
150
|
-
#
|
151
|
-
|
152
|
-
id = a.fetch(:id)
|
153
|
-
if e
|
154
|
-
diff = e.diff(a) # slow ...
|
155
|
-
if diff.any?
|
156
|
-
@update << [id, e, a, diff]
|
157
|
-
end
|
158
|
-
elsif a.fetch(:tracking_id) # was previously managed
|
159
|
-
@delete << [id, nil, a]
|
160
|
-
end
|
161
|
-
end
|
149
|
+
# unmatched expected need to be created
|
150
|
+
@create = unmatched_expected.map { |e| [nil, e] }
|
162
151
|
|
163
|
-
|
164
|
-
@create = @expected.map { |e| [nil, e] }
|
152
|
+
# order to avoid deadlocks
|
165
153
|
@delete.sort_by! { |_, _, a| DELETE_ORDER.index a.fetch(:klass).api_resource }
|
166
154
|
@update.sort_by! { |_, e, _| DELETE_ORDER.index e.class.api_resource } # slo needs to come before slo alert
|
167
155
|
end
|
168
156
|
end
|
169
157
|
|
170
|
-
def
|
171
|
-
|
158
|
+
def partition_matched_expected
|
159
|
+
lookup_map = matching_expected_lookup_map
|
160
|
+
unmatched_expected = @expected.dup
|
161
|
+
unmatched_actual = []
|
162
|
+
matched = []
|
163
|
+
@actual.each do |a|
|
164
|
+
e = matching_expected(a, lookup_map)
|
165
|
+
if e && unmatched_expected.delete?(e)
|
166
|
+
matched << [e, a]
|
167
|
+
else
|
168
|
+
unmatched_actual << a
|
169
|
+
end
|
170
|
+
end.compact
|
171
|
+
[matched, unmatched_expected, unmatched_actual]
|
172
|
+
end
|
173
|
+
|
174
|
+
# fill details of things we need to compare
|
175
|
+
def fill_details!(details_needed)
|
176
|
+
details_needed = details_needed.map { |e, a| a if e && e.class.api_resource == "dashboard" }.compact
|
177
|
+
@api.fill_details! "dashboard", details_needed
|
178
|
+
end
|
179
|
+
|
180
|
+
def validate_expected_id_not_missing(expected)
|
181
|
+
expected.each do |e|
|
172
182
|
next unless id = e.id
|
173
183
|
resource = e.class.api_resource
|
174
|
-
if
|
184
|
+
if @strict_imports
|
175
185
|
raise "Unable to find existing #{resource} with id #{id}\nIf the #{resource} was deleted, remove the `id: -> { #{id} }` line."
|
176
186
|
else
|
177
187
|
@warnings << "#{resource} #{e.tracking_id} specifies id #{id}, but no such #{resource} exists. 'id' will be ignored. Remove the `id: -> { #{id} }` line."
|
@@ -196,79 +206,18 @@ module Kennel
|
|
196
206
|
map["#{klass.api_resource}:#{a.fetch(:id)}"] || map[a.fetch(:tracking_id)]
|
197
207
|
end
|
198
208
|
|
199
|
-
def
|
209
|
+
def print_changes(step, list, color)
|
200
210
|
return if list.empty?
|
201
211
|
list.each do |_, e, a, diff|
|
202
212
|
klass = (e ? e.class : a.fetch(:klass))
|
203
|
-
Kennel.out.puts
|
204
|
-
|
205
|
-
end
|
206
|
-
end
|
207
|
-
|
208
|
-
def print_diff(diff)
|
209
|
-
diff.each do |type, field, old, new|
|
210
|
-
use_diff = false
|
211
|
-
if type == "+"
|
212
|
-
temp = Utils.pretty_inspect(new)
|
213
|
-
new = Utils.pretty_inspect(old)
|
214
|
-
old = temp
|
215
|
-
elsif old.is_a?(String) && new.is_a?(String) && (old.include?("\n") || new.include?("\n"))
|
216
|
-
use_diff = true
|
217
|
-
else # ~ and -
|
218
|
-
old = Utils.pretty_inspect(old)
|
219
|
-
new = Utils.pretty_inspect(new)
|
220
|
-
end
|
221
|
-
|
222
|
-
message =
|
223
|
-
if use_diff
|
224
|
-
" #{type}#{field}\n" +
|
225
|
-
diff(old, new).map { |l| " #{l}" }.join("\n")
|
226
|
-
elsif (old + new).size > 100
|
227
|
-
" #{type}#{field}\n" \
|
228
|
-
" #{old} ->\n" \
|
229
|
-
" #{new}"
|
230
|
-
else
|
231
|
-
" #{type}#{field} #{old} -> #{new}"
|
232
|
-
end
|
233
|
-
|
234
|
-
Kennel.out.puts truncate_diff(message)
|
235
|
-
end
|
236
|
-
end
|
237
|
-
|
238
|
-
# display diff for multi-line strings
|
239
|
-
# must stay readable when color is off too
|
240
|
-
def diff(old, new)
|
241
|
-
Diff::LCS.sdiff(old.split("\n", -1), new.split("\n", -1)).flat_map do |diff|
|
242
|
-
case diff.action
|
243
|
-
when "-"
|
244
|
-
Utils.color(:red, "- #{diff.old_element}")
|
245
|
-
when "+"
|
246
|
-
Utils.color(:green, "+ #{diff.new_element}")
|
247
|
-
when "!"
|
248
|
-
[
|
249
|
-
Utils.color(:red, "- #{diff.old_element}"),
|
250
|
-
Utils.color(:green, "+ #{diff.new_element}")
|
251
|
-
]
|
252
|
-
else
|
253
|
-
" #{diff.old_element}"
|
254
|
-
end
|
213
|
+
Kennel.out.puts Console.color(color, "#{step} #{klass.api_resource} #{e&.tracking_id || a.fetch(:tracking_id)}")
|
214
|
+
diff&.each { |args| Kennel.out.puts @attribute_differ.format(*args) } # only for update
|
255
215
|
end
|
256
216
|
end
|
257
217
|
|
258
|
-
def truncate_diff(message)
|
259
|
-
# min '2' because: -1 makes no sense, 0 does not work with * 2 math, 1 says '1 lines'
|
260
|
-
@max_diff_lines ||= [Integer(ENV.fetch("MAX_DIFF_LINES", "50")), 2].max
|
261
|
-
warning = Utils.color(
|
262
|
-
:magenta,
|
263
|
-
" (Diff for this item truncated after #{@max_diff_lines} lines. " \
|
264
|
-
"Rerun with MAX_DIFF_LINES=#{@max_diff_lines * 2} to see more)"
|
265
|
-
)
|
266
|
-
Utils.truncate_lines(message, to: @max_diff_lines, warning: warning)
|
267
|
-
end
|
268
|
-
|
269
218
|
# We've already validated the desired objects ('generated') in isolation.
|
270
219
|
# Now that we have made the plan, we can perform some more validation.
|
271
|
-
def
|
220
|
+
def validate_changes
|
272
221
|
@update.each do |_, expected, actuals, diffs|
|
273
222
|
expected.validate_update!(actuals, diffs)
|
274
223
|
end
|
@@ -276,11 +225,10 @@ module Kennel
|
|
276
225
|
|
277
226
|
# - do not add tracking-id when working with existing ids on a branch,
|
278
227
|
# so resource do not get deleted when running an update on master (for example merge->CI)
|
279
|
-
# - make sure the diff is clean, by kicking out the now noop-update
|
280
228
|
# - ideally we'd never add tracking in the first place, but when adding tracking we do not know the diff yet
|
281
229
|
def prevent_irreversible_partial_updates
|
282
|
-
return unless @project_filter
|
283
|
-
@update.select! do |_, e, _, diff|
|
230
|
+
return unless @project_filter # full update, so we are not on a branch
|
231
|
+
@update.select! do |_, e, _, diff| # ensure clean diff, by removing noop-update
|
284
232
|
next true unless e.id # safe to add tracking when not having id
|
285
233
|
|
286
234
|
diff.select! do |field_diff|
|
@@ -293,7 +241,7 @@ module Kennel
|
|
293
241
|
actual != field_diff[3] # discard diff if now nothing changes
|
294
242
|
end
|
295
243
|
|
296
|
-
|
244
|
+
diff.any?
|
297
245
|
end
|
298
246
|
end
|
299
247
|
|
@@ -321,7 +269,7 @@ module Kennel
|
|
321
269
|
(!@tracking_id_filter || @tracking_id_filter.include?(tracking_id))
|
322
270
|
|
323
271
|
@id_map.set(api_resource, tracking_id, a.fetch(:id))
|
324
|
-
if a
|
272
|
+
if a.fetch(:klass).api_resource == "synthetics/tests"
|
325
273
|
@id_map.set(Kennel::Models::Monitor.api_resource, tracking_id, a.fetch(:monitor_id))
|
326
274
|
end
|
327
275
|
end
|
@@ -22,7 +22,7 @@ module Kennel
|
|
22
22
|
m[:state][:groups].each do |g|
|
23
23
|
color = COLORS[g[:status]] || :default
|
24
24
|
since = "\t#{time_since(g[:last_triggered_ts])}"
|
25
|
-
Kennel.out.puts "#{Kennel::
|
25
|
+
Kennel.out.puts "#{Kennel::Console.color(color, g[:status])}\t#{g[:name]}#{since}"
|
26
26
|
end
|
27
27
|
Kennel.out.puts
|
28
28
|
end
|
data/lib/kennel/utils.rb
CHANGED
@@ -1,99 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module Kennel
|
3
3
|
module Utils
|
4
|
-
COLORS = { red: 31, green: 32, yellow: 33, cyan: 36, magenta: 35, default: 0 }.freeze
|
5
|
-
|
6
|
-
class TeeIO < IO
|
7
|
-
def initialize(ios)
|
8
|
-
@ios = ios
|
9
|
-
end
|
10
|
-
|
11
|
-
def write(string)
|
12
|
-
@ios.each { |io| io.write string }
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
4
|
class << self
|
17
|
-
def snake_case(string)
|
18
|
-
string
|
19
|
-
.gsub(/::/, "_") # Foo::Bar -> foo_bar
|
20
|
-
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2') # FOOBar -> foo_bar
|
21
|
-
.gsub(/([a-z\d])([A-Z])/, '\1_\2') # fooBar -> foo_bar
|
22
|
-
.tr("-", "_") # foo-bar -> foo_bar
|
23
|
-
.downcase
|
24
|
-
end
|
25
|
-
|
26
|
-
# for child projects, not used internally
|
27
|
-
def title_case(string)
|
28
|
-
string.split(/[\s_]/).map(&:capitalize) * " "
|
29
|
-
end
|
30
|
-
|
31
|
-
# simplified version of https://apidock.com/rails/ActiveSupport/Inflector/parameterize
|
32
|
-
def parameterize(string)
|
33
|
-
string
|
34
|
-
.downcase
|
35
|
-
.gsub(/[^a-z0-9\-_]+/, "-") # remove unsupported
|
36
|
-
.gsub(/-{2,}/, "-") # remove duplicates
|
37
|
-
.gsub(/^-|-$/, "") # remove leading/trailing
|
38
|
-
end
|
39
|
-
|
40
5
|
def presence(value)
|
41
6
|
value.nil? || value.empty? ? nil : value
|
42
7
|
end
|
43
8
|
|
44
|
-
def ask(question)
|
45
|
-
Kennel.err.printf color(:red, "#{question} - press 'y' to continue: ", force: true)
|
46
|
-
begin
|
47
|
-
STDIN.gets.chomp == "y"
|
48
|
-
rescue Interrupt # do not show a backtrace if user decides to Ctrl+C here
|
49
|
-
Kennel.err.print "\n"
|
50
|
-
exit 1
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def color(color, text, force: false)
|
55
|
-
return text unless force || Kennel.out.tty?
|
56
|
-
|
57
|
-
"\e[#{COLORS.fetch(color)}m#{text}\e[0m"
|
58
|
-
end
|
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
|
-
|
66
|
-
def capture_stdout
|
67
|
-
old = Kennel.out
|
68
|
-
Kennel.out = StringIO.new
|
69
|
-
yield
|
70
|
-
Kennel.out.string
|
71
|
-
ensure
|
72
|
-
Kennel.out = old
|
73
|
-
end
|
74
|
-
|
75
|
-
def capture_stderr
|
76
|
-
old = Kennel.err
|
77
|
-
Kennel.err = StringIO.new
|
78
|
-
yield
|
79
|
-
Kennel.err.string
|
80
|
-
ensure
|
81
|
-
Kennel.err = old
|
82
|
-
end
|
83
|
-
|
84
|
-
def tee_output
|
85
|
-
old_stdout = Kennel.out
|
86
|
-
old_stderr = Kennel.err
|
87
|
-
capture = StringIO.new
|
88
|
-
Kennel.out = TeeIO.new([capture, Kennel.out])
|
89
|
-
Kennel.err = TeeIO.new([capture, Kennel.err])
|
90
|
-
yield
|
91
|
-
capture.string
|
92
|
-
ensure
|
93
|
-
Kennel.out = old_stdout
|
94
|
-
Kennel.err = old_stderr
|
95
|
-
end
|
96
|
-
|
97
9
|
def capture_sh(command)
|
98
10
|
result = `#{command} 2>&1`
|
99
11
|
raise "Command failed:\n#{command}\n#{result}" unless $CHILD_STATUS.success?
|
@@ -149,22 +61,6 @@ module Kennel
|
|
149
61
|
else []
|
150
62
|
end
|
151
63
|
end
|
152
|
-
|
153
|
-
# TODO: use awesome-print or similar, but it has too many monkey-patches
|
154
|
-
# https://github.com/amazing-print/amazing_print/issues/36
|
155
|
-
def pretty_inspect(object)
|
156
|
-
string = object.inspect.dup
|
157
|
-
string.gsub!(/:([a-z_]+)=>/, "\\1: ")
|
158
|
-
10.times do
|
159
|
-
string.gsub!(/{(\S.*?\S)}/, "{ \\1 }") || break
|
160
|
-
end
|
161
|
-
string
|
162
|
-
end
|
163
|
-
|
164
|
-
def inline_resource_metadata(resource, klass)
|
165
|
-
resource[:klass] = klass
|
166
|
-
resource[:tracking_id] = klass.parse_tracking_id(resource)
|
167
|
-
end
|
168
64
|
end
|
169
65
|
end
|
170
66
|
end
|
data/lib/kennel/version.rb
CHANGED
data/lib/kennel.rb
CHANGED
@@ -5,11 +5,14 @@ require "zeitwerk"
|
|
5
5
|
require "English"
|
6
6
|
|
7
7
|
require "kennel/version"
|
8
|
+
require "kennel/console"
|
9
|
+
require "kennel/string_utils"
|
8
10
|
require "kennel/utils"
|
9
11
|
require "kennel/progress"
|
10
12
|
require "kennel/filter"
|
11
13
|
require "kennel/parts_serializer"
|
12
14
|
require "kennel/projects_provider"
|
15
|
+
require "kennel/attribute_differ"
|
13
16
|
require "kennel/syncer"
|
14
17
|
require "kennel/id_map"
|
15
18
|
require "kennel/api"
|
@@ -42,7 +45,6 @@ module Kennel
|
|
42
45
|
UnresolvableIdError = Class.new(StandardError)
|
43
46
|
DisallowedUpdateError = Class.new(StandardError)
|
44
47
|
GenerationAbortedError = Class.new(StandardError)
|
45
|
-
UpdateResult = Struct.new(:plan, :update, keyword_init: true)
|
46
48
|
|
47
49
|
class << self
|
48
50
|
attr_accessor :out, :err
|
@@ -52,12 +54,12 @@ module Kennel
|
|
52
54
|
self.err = $stderr
|
53
55
|
|
54
56
|
class Engine
|
57
|
+
attr_accessor :strict_imports
|
58
|
+
|
55
59
|
def initialize
|
56
60
|
@strict_imports = true
|
57
61
|
end
|
58
62
|
|
59
|
-
attr_accessor :strict_imports
|
60
|
-
|
61
63
|
# start generation and download in parallel to make planning faster
|
62
64
|
def preload
|
63
65
|
Utils.parallel([:generated, :definitions]) { |m| send m, plain: true }
|
@@ -65,16 +67,17 @@ module Kennel
|
|
65
67
|
|
66
68
|
def generate
|
67
69
|
parts = generated
|
68
|
-
|
70
|
+
PartsSerializer.new(filter: filter).write(parts) if ENV["STORE"] != "false" # quicker when debugging
|
69
71
|
parts
|
70
72
|
end
|
71
73
|
|
72
74
|
def plan
|
75
|
+
syncer.print_plan
|
73
76
|
syncer.plan
|
74
77
|
end
|
75
78
|
|
76
79
|
def update
|
77
|
-
syncer.
|
80
|
+
syncer.print_plan
|
78
81
|
syncer.update if syncer.confirm
|
79
82
|
end
|
80
83
|
|
@@ -87,7 +90,12 @@ module Kennel
|
|
87
90
|
def syncer
|
88
91
|
@syncer ||= begin
|
89
92
|
preload
|
90
|
-
Syncer.new(
|
93
|
+
Syncer.new(
|
94
|
+
api, generated, definitions,
|
95
|
+
strict_imports: strict_imports,
|
96
|
+
project_filter: filter.project_filter,
|
97
|
+
tracking_id_filter: filter.tracking_id_filter
|
98
|
+
)
|
91
99
|
end
|
92
100
|
end
|
93
101
|
|
@@ -95,18 +103,10 @@ module Kennel
|
|
95
103
|
@api ||= Api.new
|
96
104
|
end
|
97
105
|
|
98
|
-
def projects_provider
|
99
|
-
@projects_provider ||= ProjectsProvider.new
|
100
|
-
end
|
101
|
-
|
102
|
-
def parts_serializer
|
103
|
-
@parts_serializer ||= PartsSerializer.new(filter: filter)
|
104
|
-
end
|
105
|
-
|
106
106
|
def generated(**kwargs)
|
107
107
|
@generated ||= begin
|
108
108
|
parts = Progress.progress "Finding parts", **kwargs do
|
109
|
-
projects =
|
109
|
+
projects = ProjectsProvider.new.projects
|
110
110
|
projects = filter.filter_projects projects
|
111
111
|
|
112
112
|
parts = Utils.parallel(projects, &:validated_parts).flatten(1)
|
@@ -135,8 +135,7 @@ module Kennel
|
|
135
135
|
def definitions(**kwargs)
|
136
136
|
@definitions ||= Progress.progress("Downloading definitions", **kwargs) do
|
137
137
|
Utils.parallel(Models::Record.subclasses) do |klass|
|
138
|
-
|
139
|
-
results.each { |a| Utils.inline_resource_metadata(a, klass) }
|
138
|
+
api.list(klass.api_resource, with_downtimes: false) # lookup monitors without adding unnecessary downtime information
|
140
139
|
end.flatten(1)
|
141
140
|
end
|
142
141
|
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.130.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Grosser
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-12-
|
11
|
+
date: 2022-12-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: diff-lcs
|
@@ -80,7 +80,7 @@ dependencies:
|
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '2.4'
|
83
|
-
description:
|
83
|
+
description:
|
84
84
|
email: michael@grosser.it
|
85
85
|
executables: []
|
86
86
|
extensions: []
|
@@ -89,6 +89,8 @@ files:
|
|
89
89
|
- Readme.md
|
90
90
|
- lib/kennel.rb
|
91
91
|
- lib/kennel/api.rb
|
92
|
+
- lib/kennel/attribute_differ.rb
|
93
|
+
- lib/kennel/console.rb
|
92
94
|
- lib/kennel/file_cache.rb
|
93
95
|
- lib/kennel/filter.rb
|
94
96
|
- lib/kennel/github_reporter.rb
|
@@ -107,6 +109,7 @@ files:
|
|
107
109
|
- lib/kennel/progress.rb
|
108
110
|
- lib/kennel/projects_provider.rb
|
109
111
|
- lib/kennel/settings_as_methods.rb
|
112
|
+
- lib/kennel/string_utils.rb
|
110
113
|
- lib/kennel/subclass_tracking.rb
|
111
114
|
- lib/kennel/syncer.rb
|
112
115
|
- lib/kennel/tasks.rb
|
@@ -119,7 +122,7 @@ homepage: https://github.com/grosser/kennel
|
|
119
122
|
licenses:
|
120
123
|
- MIT
|
121
124
|
metadata: {}
|
122
|
-
post_install_message:
|
125
|
+
post_install_message:
|
123
126
|
rdoc_options: []
|
124
127
|
require_paths:
|
125
128
|
- lib
|
@@ -127,15 +130,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
127
130
|
requirements:
|
128
131
|
- - ">="
|
129
132
|
- !ruby/object:Gem::Version
|
130
|
-
version:
|
133
|
+
version: 3.1.0
|
131
134
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
132
135
|
requirements:
|
133
136
|
- - ">="
|
134
137
|
- !ruby/object:Gem::Version
|
135
138
|
version: '0'
|
136
139
|
requirements: []
|
137
|
-
rubygems_version: 3.
|
138
|
-
signing_key:
|
140
|
+
rubygems_version: 3.3.26
|
141
|
+
signing_key:
|
139
142
|
specification_version: 4
|
140
143
|
summary: Keep datadog monitors/dashboards/etc in version control, avoid chaotic management
|
141
144
|
via UI
|