kennel 1.78.4 → 1.81.2

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: 23d5816783075cabd0b6a51addfe89bd0d21f4935a547fd1bfbd0c754d6f3b18
4
- data.tar.gz: 73df009d112f1af7d1500428e700ac6cdea3d99cbb36a12dd3ceef926b2f5fd4
3
+ metadata.gz: 5f9d3050dbb7a4e94cd22e64618142bedffd6fdfc5b5ee8e20b621b90f1d8f27
4
+ data.tar.gz: 640a86b74e46e27ec7f1b09dd555f0ba8f99e4fc1f687e5230bb00d12636ae1d
5
5
  SHA512:
6
- metadata.gz: 04a237a134852bd15c4ce7eb6a309c0f8526c656f646269ccfeaf8f148013ea6c8328bcfbaffaee53c228af1e6b79c021ff9cade7afacb8a301f4e418302dbd0
7
- data.tar.gz: 3e51d0361af539cd3aca894f51d8e28cf5ef613722c1f3a7f7b5f3f88fb74e43ab535034144d246cc0f079d5d06e9e62f488bb76257390cae4a7f0f14636984e
6
+ metadata.gz: 80dbfff63550b6f5c3127cb8ecc97db6b3db38876469bcb9d50fce00a7e1b78e2f473dd3c3abaec081cb21037f53be72021d907c4285352b30dd3b6991f6c622
7
+ data.tar.gz: 02a3122ec70a951dc01f919c4d13877de88c6266048d006770a62adadd19587d74a603e8281699f878be0a20227a7086e37727c751cf5833e815fc795e3f9ee3
data/Readme.md CHANGED
@@ -84,7 +84,7 @@ end
84
84
  - `cp .env.example .env`
85
85
  - open [Datadog API Settings](https://app.datadoghq.com/account/settings#api)
86
86
  - create a `API Key` or get an existing one from an admin, then add it to `.env` as `DATADOG_API_KEY`
87
- - find or create (check last page) your personal "Application Key" and add it to `.env` as `DATADOG_APP_KEY=`
87
+ - open [Datadog API Settings](https://app.datadoghq.com/access/application-keys) and create a new key, then add it to `.env` as `DATADOG_APP_KEY=`
88
88
  - change the `DATADOG_SUBDOMAIN=app` in `.env` to your companies subdomain if you have one
89
89
  - verify it works by running `rake plan`, it might show some diff, but should not crash
90
90
  -->
@@ -212,14 +212,20 @@ removing the `id` will cause kennel to create a new resource in datadog.
212
212
  Some validations might be too strict for your usecase or just wrong, please [open an issue](https://github.com/grosser/kennel/issues) and
213
213
  to unblock use the `validate: -> { false }` option.
214
214
 
215
- ### Linking with kennel_ids
215
+ ### Linking resources with kennel_id
216
216
 
217
- To link to existing monitors via their kennel_id `projects kennel_id` + `:` + `monitors kennel id`
217
+ Link resources with their kennel_id in the format `project kennel_id` + `:` + `resource kennel_id`,
218
+ this should be used to create dependent resources like monitor + slos,
219
+ so they can be created in a single update and can be re-created if any of them is deleted.
218
220
 
219
- - Screens `uptime` widgets can use `monitor: {id: "foo:bar"}`
220
- - Screens `alert_graph` widgets can use `alert_id: "foo:bar"`
221
- - Monitors `composite` can use `query: -> { "%{foo:bar} || %{foo:baz}" }`
222
- - Slos can use `monitor_ids: -> ["foo:bar"]`
221
+ |Resource|Type|Syntax|
222
+ |---|---|---|
223
+ |Dashboard|uptime|`monitor: {id: "foo:bar"}`|
224
+ |Dashboard|alert_graph|`alert_id: "foo:bar"`|
225
+ |Dashboard|slo|`slo_id: "foo:bar"`|
226
+ |Monitor|composite|`query: -> { "%{foo:bar} && %{foo:baz}" }`|
227
+ |Monitor|slo alert|`query: -> { "error_budget(\"%{foo:bar}\").over(\"7d\") > 123.0" }`|
228
+ |Slo|monitor|`monitor_ids: -> ["foo:bar"]`|
223
229
 
224
230
  ### Debugging changes locally
225
231
 
@@ -296,5 +302,5 @@ Author
296
302
  [Michael Grosser](http://grosser.it)<br/>
297
303
  michael@grosser.it<br/>
298
304
  License: MIT<br/>
299
- [![Build Status](https://travis-ci.org/grosser/kennel.png)](https://travis-ci.org/grosser/kennel)
305
+ ![CI](https://github.com/grosser/kennel/workflows/CI/badge.svg)
300
306
  <!-- NOT IN -->
data/lib/kennel.rb CHANGED
@@ -39,12 +39,7 @@ module Kennel
39
39
  attr_accessor :out, :err
40
40
 
41
41
  def generate
42
- FileUtils.rm_rf("generated")
43
- generated.each do |part|
44
- path = "generated/#{part.tracking_id.sub(":", "/")}.json"
45
- FileUtils.mkdir_p(File.dirname(path))
46
- File.write(path, JSON.pretty_generate(part.as_json) << "\n")
47
- end
42
+ store generated
48
43
  end
49
44
 
50
45
  def plan
@@ -58,6 +53,35 @@ module Kennel
58
53
 
59
54
  private
60
55
 
56
+ def store(parts)
57
+ Progress.progress "Storing" do
58
+ old = Dir["generated/**/*"]
59
+ used = []
60
+
61
+ Utils.parallel(parts, max: 2) do |part|
62
+ path = "generated/#{part.tracking_id.tr("/", ":").sub(":", "/")}.json"
63
+ used << File.dirname(path) # only 1 level of sub folders, so this is safe
64
+ used << path
65
+ write_file_if_necessary(path, JSON.pretty_generate(part.as_json) << "\n")
66
+ end
67
+
68
+ # deleting all is slow, so only delete the extras
69
+ (old - used).each { |p| FileUtils.rm_rf(p) }
70
+ end
71
+ end
72
+
73
+ def write_file_if_necessary(path, content)
74
+ # 99% case
75
+ begin
76
+ return if File.read(path) == content
77
+ rescue Errno::ENOENT
78
+ FileUtils.mkdir_p(File.dirname(path))
79
+ end
80
+
81
+ # slow 1% case
82
+ File.write(path, content)
83
+ end
84
+
61
85
  def syncer
62
86
  @syncer ||= Syncer.new(api, generated, project: ENV["PROJECT"])
63
87
  end
@@ -73,8 +97,12 @@ module Kennel
73
97
  parts = Models::Project.recursive_subclasses.flat_map do |project_class|
74
98
  project_class.new.validated_parts
75
99
  end
76
- parts.map(&:tracking_id).group_by { |id| id }.select do |id, same|
77
- raise "#{id} is defined #{same.size} times" if same.size != 1
100
+ parts.group_by(&:tracking_id).each do |tracking_id, same|
101
+ next if same.size == 1
102
+ raise <<~ERROR
103
+ #{tracking_id} is defined #{same.size} times
104
+ use a different `kennel_id` when defining multiple projects/monitors/dashboards to avoid this conflict
105
+ ERROR
78
106
  end
79
107
  parts
80
108
  end
@@ -140,16 +140,16 @@ module Kennel
140
140
  when "uptime"
141
141
  if ids = definition[:monitor_ids]
142
142
  definition[:monitor_ids] = ids.map do |id|
143
- tracking_id?(id) ? resolve_link(id, :monitor, id_map, **args) : id
143
+ tracking_id?(id) ? (resolve_link(id, :monitor, id_map, **args) || id) : id
144
144
  end
145
145
  end
146
146
  when "alert_graph"
147
147
  if (id = definition[:alert_id]) && tracking_id?(id)
148
- definition[:alert_id] = resolve_link(id, :monitor, id_map, **args).to_s
148
+ definition[:alert_id] = (resolve_link(id, :monitor, id_map, **args) || id).to_s
149
149
  end
150
150
  when "slo"
151
151
  if (id = definition[:slo_id]) && tracking_id?(id)
152
- definition[:slo_id] = resolve_link(id, :slo, id_map, **args).to_s
152
+ definition[:slo_id] = (resolve_link(id, :slo, id_map, **args) || id).to_s
153
153
  end
154
154
  end
155
155
  end
@@ -186,22 +186,26 @@ module Kennel
186
186
  end
187
187
 
188
188
  def render_definitions(definitions)
189
- definitions.map do |title, type, display_type, queries, options = {}, ignored = nil|
190
- # validate inputs
191
- if ignored || (!title || !type || !queries || !options.is_a?(Hash))
192
- raise ArgumentError, "Expected exactly 5 arguments for each definition (title, type, display_type, queries, options)"
193
- end
194
- if (SUPPORTED_DEFINITION_OPTIONS | options.keys) != SUPPORTED_DEFINITION_OPTIONS
195
- raise ArgumentError, "Supported options are: #{SUPPORTED_DEFINITION_OPTIONS.map(&:inspect).join(", ")}"
196
- end
189
+ definitions.map do |title, type, display_type, queries, options = {}, too_many_args = nil|
190
+ if title.is_a?(Hash) && !type
191
+ title # user gave a full widget, just use it
192
+ else
193
+ # validate inputs
194
+ if too_many_args || (!title || !type || !queries || !options.is_a?(Hash))
195
+ raise ArgumentError, "Expected exactly 5 arguments for each definition (title, type, display_type, queries, options)"
196
+ end
197
+ if (SUPPORTED_DEFINITION_OPTIONS | options.keys) != SUPPORTED_DEFINITION_OPTIONS
198
+ raise ArgumentError, "Supported options are: #{SUPPORTED_DEFINITION_OPTIONS.map(&:inspect).join(", ")}"
199
+ end
197
200
 
198
- # build definition
199
- requests = Array(queries).map do |q|
200
- request = { q: q }
201
- request[:display_type] = display_type if display_type
202
- request
201
+ # build definition
202
+ requests = Array(queries).map do |q|
203
+ request = { q: q }
204
+ request[:display_type] = display_type if display_type
205
+ request
206
+ end
207
+ { definition: { title: title, type: type, requests: requests, **options } }
203
208
  end
204
- { definition: { title: title, type: type, requests: requests, **options } }
205
209
  end
206
210
  end
207
211
  end
@@ -12,6 +12,10 @@ module Kennel
12
12
  :multi, :matching_downtimes, :overall_state_modified, :overall_state, :restricted_roles
13
13
  ]
14
14
 
15
+ MONITOR_DEFAULTS = {
16
+ priority: nil
17
+ }.freeze
18
+
15
19
  # defaults that datadog uses when options are not sent, so safe to leave out if our values match their defaults
16
20
  MONITOR_OPTION_DEFAULTS = {
17
21
  evaluation_delay: nil,
@@ -27,7 +31,7 @@ module Kennel
27
31
  settings(
28
32
  :query, :name, :message, :escalation_message, :critical, :type, :renotify_interval, :warning, :timeout_h, :evaluation_delay,
29
33
  :ok, :no_data_timeframe, :notify_no_data, :notify_audit, :tags, :critical_recovery, :warning_recovery, :require_full_window,
30
- :threshold_windows, :new_host_delay
34
+ :threshold_windows, :new_host_delay, :priority
31
35
  )
32
36
 
33
37
  defaults(
@@ -46,7 +50,8 @@ module Kennel
46
50
  evaluation_delay: -> { MONITOR_OPTION_DEFAULTS.fetch(:evaluation_delay) },
47
51
  critical_recovery: -> { nil },
48
52
  warning_recovery: -> { nil },
49
- threshold_windows: -> { nil }
53
+ threshold_windows: -> { nil },
54
+ priority: -> { MONITOR_DEFAULTS.fetch(:priority) }
50
55
  )
51
56
 
52
57
  def as_json
@@ -57,6 +62,7 @@ module Kennel
57
62
  query: query.strip,
58
63
  message: message.strip,
59
64
  tags: tags.uniq,
65
+ priority: priority,
60
66
  options: {
61
67
  timeout_h: timeout_h,
62
68
  notify_no_data: notify_no_data,
@@ -106,9 +112,11 @@ module Kennel
106
112
  end
107
113
 
108
114
  def resolve_linked_tracking_ids!(id_map, **args)
109
- if as_json[:type] == "composite"
110
- as_json[:query] = as_json[:query].gsub(/%\{(.*?)\}/) do
111
- resolve_link($1, :monitor, id_map, **args)
115
+ case as_json[:type]
116
+ when "composite", "slo alert"
117
+ type = (as_json[:type] == "composite" ? :monitor : :slo)
118
+ as_json[:query] = as_json[:query].gsub(/%{(.*?)}/) do
119
+ resolve_link($1, type, id_map, **args) || $&
112
120
  end
113
121
  end
114
122
  end
@@ -129,6 +137,9 @@ module Kennel
129
137
 
130
138
  def self.normalize(expected, actual)
131
139
  super
140
+
141
+ ignore_default(expected, actual, MONITOR_DEFAULTS)
142
+
132
143
  options = actual.fetch(:options)
133
144
  options.delete(:silenced) # we do not manage silenced, so ignore it when diffing
134
145
 
@@ -65,19 +65,18 @@ module Kennel
65
65
 
66
66
  private
67
67
 
68
- def resolve_link(id, type, id_map, force:)
69
- value = id_map[id]
70
- if value == :new
68
+ def resolve_link(tracking_id, type, id_map, force:)
69
+ id = id_map[tracking_id]
70
+ if id == :new
71
71
  if force
72
- # TODO: remove the need for this by sorting monitors by missing resolutions
73
- invalid! "#{id} needs to already exist, try again"
72
+ invalid! "#{type} #{tracking_id} was referenced but is also created by the current run.\nIt could not be created because of a circular dependency, try creating only some of the resources"
74
73
  else
75
- id # will be re-resolved by syncer after the linked object was created
74
+ nil # will be re-resolved after the linked object was created
76
75
  end
77
- elsif value
78
- value
76
+ elsif id
77
+ id
79
78
  else
80
- invalid! "Unable to find #{type} #{id} (does not exist and is not being created by the current run)"
79
+ invalid! "Unable to find #{type} #{tracking_id} (does not exist and is not being created by the current run)"
81
80
  end
82
81
  end
83
82
 
@@ -69,7 +69,7 @@ module Kennel
69
69
  def resolve_linked_tracking_ids!(id_map, **args)
70
70
  return unless as_json[:monitor_ids] # ignore_default can remove it
71
71
  as_json[:monitor_ids] = as_json[:monitor_ids].map do |id|
72
- id.is_a?(String) ? resolve_link(id, :monitor, id_map, **args) : id
72
+ id.is_a?(String) ? (resolve_link(id, :monitor, id_map, **args) || id) : id
73
73
  end
74
74
  end
75
75
 
data/lib/kennel/syncer.rb CHANGED
@@ -38,23 +38,14 @@ module Kennel
38
38
  end
39
39
 
40
40
  def update
41
- changed = (@create + @update).map { |_, e| e }
42
-
43
- @create.each do |_, e|
44
- e.resolve_linked_tracking_ids!({}, force: true)
45
-
41
+ each_resolved @create do |_, e|
46
42
  reply = @api.create e.class.api_resource, e.as_json
47
43
  id = reply.fetch(:id)
48
-
49
- # resolve ids we could previously no resolve
50
- changed.delete e
51
- resolve_linked_tracking_ids! from: [reply], to: changed
52
-
44
+ populate_id_map [reply] # allow resolving ids we could previously no resolve
53
45
  Kennel.out.puts "Created #{e.class.api_resource} #{tracking_id(e.as_json)} #{e.url(id)}"
54
46
  end
55
47
 
56
- @update.each do |id, e|
57
- e.resolve_linked_tracking_ids!({}, force: true)
48
+ each_resolved @update do |id, e|
58
49
  @api.update e.class.api_resource, id, e.as_json
59
50
  Kennel.out.puts "Updated #{e.class.api_resource} #{tracking_id(e.as_json)} #{e.url(id)}"
60
51
  end
@@ -67,6 +58,37 @@ module Kennel
67
58
 
68
59
  private
69
60
 
61
+ # loop over items until everything is resolved or crash when we get stuck
62
+ # this solves cases like composite monitors depending on each other or monitor->monitor slo->slo monitor chains
63
+ def each_resolved(list)
64
+ list = list.dup
65
+ loop do
66
+ return if list.empty?
67
+ list.reject! do |id, e|
68
+ if resolved?(e)
69
+ yield id, e
70
+ true
71
+ else
72
+ false
73
+ end
74
+ end ||
75
+ assert_resolved(list[0][1]) # resolve something or show a circular dependency error
76
+ end
77
+ end
78
+
79
+ # TODO: optimize by storing an instance variable if already resolved
80
+ def resolved?(e)
81
+ assert_resolved e
82
+ true
83
+ rescue ValidationError
84
+ false
85
+ end
86
+
87
+ # raises ValidationError when not resolved
88
+ def assert_resolved(e)
89
+ resolve_linked_tracking_ids! [e], force: true
90
+ end
91
+
70
92
  def noop?
71
93
  @create.empty? && @update.empty? && @delete.empty?
72
94
  end
@@ -74,9 +96,15 @@ module Kennel
74
96
  def calculate_diff
75
97
  @update = []
76
98
  @delete = []
99
+ @id_map = {}
77
100
 
78
101
  actual = Progress.progress("Downloading definitions") { download_definitions }
79
- resolve_linked_tracking_ids! from: actual, to: @expected
102
+
103
+ # resolve dependencies to avoid diff
104
+ populate_id_map actual
105
+ @expected.each { |e| @id_map[e.tracking_id] ||= :new }
106
+ resolve_linked_tracking_ids! @expected
107
+
80
108
  filter_by_project! actual
81
109
 
82
110
  Progress.progress "Diffing" do
@@ -107,7 +135,6 @@ module Kennel
107
135
 
108
136
  ensure_all_ids_found
109
137
  @create = @expected.map { |e| [nil, e] }
110
- @create.sort_by! { |_, e| -DELETE_ORDER.index(e.class.api_resource) }
111
138
  end
112
139
 
113
140
  @delete.sort_by! { |_, _, a| DELETE_ORDER.index a.fetch(:api_resource) }
@@ -214,10 +241,12 @@ module Kennel
214
241
  end
215
242
  end
216
243
 
217
- def resolve_linked_tracking_ids!(from:, to:)
218
- map = from.each_with_object({}) { |a, lookup| lookup[tracking_id(a)] = a.fetch(:id) }
219
- to.each { |e| map[e.tracking_id] ||= :new }
220
- to.each { |e| e.resolve_linked_tracking_ids!(map, force: false) }
244
+ def populate_id_map(actual)
245
+ actual.each { |a| @id_map[tracking_id(a)] = a.fetch(:id) }
246
+ end
247
+
248
+ def resolve_linked_tracking_ids!(list, force: false)
249
+ list.each { |e| e.resolve_linked_tracking_ids!(@id_map, force: force) }
221
250
  end
222
251
 
223
252
  def filter_by_project!(definitions)
data/lib/kennel/utils.rb CHANGED
@@ -23,6 +23,11 @@ module Kennel
23
23
  .downcase
24
24
  end
25
25
 
26
+ # for child projects, not used internally
27
+ def title_case(string)
28
+ string.split(/[\s_]/).map(&:capitalize) * " "
29
+ end
30
+
26
31
  # simplified version of https://apidock.com/rails/ActiveSupport/Inflector/parameterize
27
32
  def parameterize(string)
28
33
  string
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Kennel
3
- VERSION = "1.78.4"
3
+ VERSION = "1.81.2"
4
4
  end
data/template/Readme.md CHANGED
@@ -67,7 +67,7 @@ end
67
67
  - `cp .env.example .env`
68
68
  - open [Datadog API Settings](https://app.datadoghq.com/account/settings#api)
69
69
  - create a `API Key` or get an existing one from an admin, then add it to `.env` as `DATADOG_API_KEY`
70
- - find or create (check last page) your personal "Application Key" and add it to `.env` as `DATADOG_APP_KEY=`
70
+ - open [Datadog API Settings](https://app.datadoghq.com/access/application-keys) and create a new key, then add it to `.env` as `DATADOG_APP_KEY=`
71
71
  - change the `DATADOG_SUBDOMAIN=app` in `.env` to your companies subdomain if you have one
72
72
  - verify it works by running `rake plan`, it might show some diff, but should not crash
73
73
 
@@ -194,14 +194,20 @@ removing the `id` will cause kennel to create a new resource in datadog.
194
194
  Some validations might be too strict for your usecase or just wrong, please [open an issue](https://github.com/grosser/kennel/issues) and
195
195
  to unblock use the `validate: -> { false }` option.
196
196
 
197
- ### Linking with kennel_ids
197
+ ### Linking resources with kennel_id
198
198
 
199
- To link to existing monitors via their kennel_id `projects kennel_id` + `:` + `monitors kennel id`
199
+ Link resources with their kennel_id in the format `project kennel_id` + `:` + `resource kennel_id`,
200
+ this should be used to create dependent resources like monitor + slos,
201
+ so they can be created in a single update and can be re-created if any of them is deleted.
200
202
 
201
- - Screens `uptime` widgets can use `monitor: {id: "foo:bar"}`
202
- - Screens `alert_graph` widgets can use `alert_id: "foo:bar"`
203
- - Monitors `composite` can use `query: -> { "%{foo:bar} || %{foo:baz}" }`
204
- - Slos can use `monitor_ids: -> ["foo:bar"]`
203
+ |Resource|Type|Syntax|
204
+ |---|---|---|
205
+ |Dashboard|uptime|`monitor: {id: "foo:bar"}`|
206
+ |Dashboard|alert_graph|`alert_id: "foo:bar"`|
207
+ |Dashboard|slo|`slo_id: "foo:bar"`|
208
+ |Monitor|composite|`query: -> { "%{foo:bar} && %{foo:baz}" }`|
209
+ |Monitor|slo alert|`query: -> { "error_budget(\"%{foo:bar}\").over(\"7d\") > 123.0" }`|
210
+ |Slo|monitor|`monitor_ids: -> ["foo:bar"]`|
205
211
 
206
212
  ### Debugging changes locally
207
213
 
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.78.4
4
+ version: 1.81.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Grosser
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-17 00:00:00.000000000 Z
11
+ date: 2021-02-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday