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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 43b746ea91f86217a6fc43383a3368ccd654c89936c44003088054380be8c36d
4
- data.tar.gz: 5fcd75471c8d0152236d032b023b4769975251047bc276f00219df3b1e60964d
3
+ metadata.gz: 31edba322106d5b2f1942c57640f19eff6a265ea970b5bb2cc7f4ef02d1889fd
4
+ data.tar.gz: f47474f8b7d745714fb49231755ed2bf41fae62cf8b5ff3817017c8cd8277713
5
5
  SHA512:
6
- metadata.gz: e9ce639cdd5748f689eeb352da8ea9c14a2a4243350368574b8b3c1b52534dbf470ca3e969ad96ecd685d8df93d368e7e4d5f5c98da2af7e5cc843ccbc8053ce
7
- data.tar.gz: 7039072c9129311145694b53823cab5c11263e972ae8f6c586e0b86063a8ab9d375c2ecd1069bd2812f626ce631f2940fe55ca9a018033b435df63bcbef1d355
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 lak into other parts of the code
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
@@ -15,11 +15,13 @@ module Kennel
15
15
  end
16
16
 
17
17
  def open
18
- load_data
19
- expire_old_data
20
- yield self
21
- ensure
22
- persist
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
- @data =
39
- begin
40
- Marshal.load(File.read(@file)) # rubocop:disable Security/MarshalLoad
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
@@ -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 = Kennel.send(:api).show("monitor", id)
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 as_json
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
- @json = {
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
- @json[:id] = id if id
168
+ json[:reflow_type] = reflow_type if reflow_type # setting nil breaks create with "ordered"
174
169
 
175
- validate_json(@json) if validate
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
- if bad_diff = diffs.find { |diff| diff[1] == "layout_type" }
214
- invalid! "Datadog does not allow update of #{bad_diff[1]} (#{bad_diff[2].inspect} -> #{bad_diff[3].inspect})"
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
- id: -> { nil },
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 as_json
60
- return @as_json if @as_json
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
- validate_json(data) if validate
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
- invalid! "Datadog does not allow update of #{path} (#{from.inspect} -> #{to.inspect})"
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
- invalid! "#parts must return an array of Records"
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
@@ -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 ValidationError, "#{id} must match #{ALLOWED_KENNEL_ID_REGEX}"
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
- invalid! "remove \"-- #{MARKER_TEXT}\" line it from #{self.class::TRACKING_FIELD} to copy a resource"
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(tracking_id, type, id_map, force:)
137
- if id_map.new?(type.to_s, tracking_id)
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
- invalid!(
140
- "#{type} #{tracking_id} was referenced but is also created by the current run.\n" \
141
- "It could not be created because of a circular dependency, try creating only some of the resources"
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(type.to_s, tracking_id)
203
+ elsif id = id_map.get(sought_type.to_s, sought_tracking_id)
147
204
  id
148
205
  else
149
- invalid! "Unable to find #{type} #{tracking_id} (does not exist and is not being created by the current run)"
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
- raise ValidationError, "#{tracking_id} #{message}"
215
+ unfiltered_validation_errors << ValidationMessage.new(message)
156
216
  end
157
217
 
158
218
  def raise_with_location(error, message)
@@ -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 initialize(*)
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
- if v = id
49
- data[:id] = v
50
- end
39
+
51
40
  if v = groups
52
41
  data[:groups] = v
53
42
  end
54
43
 
55
- @as_json = data
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 as_json
20
- return @as_json if @as_json
18
+ def build_json
21
19
  locations = locations()
22
- data = {
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
- Plan = Struct.new(:noop?, :no_change, :create, :update, :delete, keyword_init: true)
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
- noop?: noop?,
36
- no_change: @no_change,
37
- create: @create,
38
- update: @update,
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
- update_log = []
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
- update_log << [:create, e.class.api_resource, id]
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
- update_log << [:update, e.class.api_resource, id]
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
- update_log << [:delete, klass.api_resource, id]
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
- Update.new(update_log: update_log)
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 ValidationError
107
+ rescue UnresolvableIdError
108
108
  false
109
109
  end
110
110
 
111
- # raises ValidationError when not resolved
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 Kennel.strict_imports
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
- if use_diff
238
- Kennel.out.puts " #{type}#{field}"
239
- Kennel.out.puts(diff(old, new).map { |l| " #{l}" })
240
- elsif (old + new).size > 100
241
- Kennel.out.puts " #{type}#{field}"
242
- Kennel.out.puts " #{old} ->"
243
- Kennel.out.puts " #{new}"
244
- else
245
- Kennel.out.puts " #{type}#{field} #{old} -> #{new}"
246
- end
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.send(:api)
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.send(:api), tag)
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.send(:api).list("monitor", monitor_tags: tag, group_states: "no data")
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.send(:api)).import(resource, id)
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.send(:api)
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.send(:api).show(resource, id)
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
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Kennel
3
- VERSION = "1.122.0"
3
+ VERSION = "1.124.0"
4
4
  end
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
- class ValidationError < RuntimeError
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
- include Kennel::Compatibility
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 :out, :err, :strict_imports
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
- the_plan = syncer.plan
71
- the_update = syncer.update if syncer.confirm
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(ENV.fetch("DATADOG_APP_KEY"), ENV.fetch("DATADOG_API_KEY"))
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 "Generating" do
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
- parts = filter.filter_parts parts
105
+ filter.filter_parts parts
106
+ end
108
107
 
109
- parts.group_by(&:tracking_id).each do |tracking_id, same|
110
- next if same.size == 1
111
- raise <<~ERROR
112
- #{tracking_id} is defined #{same.size} times
113
- use a different `kennel_id` when defining multiple projects/monitors/dashboards to avoid this conflict
114
- ERROR
115
- end
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, &:as_json)
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.122.0
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-10-13 00:00:00.000000000 Z
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
@@ -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