kennel 1.122.0 → 1.124.0

Sign up to get free protection for your applications and to get access to all the features.
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