kennel 1.79.0 → 1.82.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: e2a8b643d03deb142461d127a97ae6ad45144cffffb3bae2925cc60cf6a1d683
4
- data.tar.gz: 1baaa314a376a99317163a871e5ec6df10dcd33a2cdd3cbc36d2bed9283fe288
3
+ metadata.gz: 97ae92e1ab137731096bbedde401059fc2b8d411ff667df489056f05a3cf42aa
4
+ data.tar.gz: c48683c4888e16da817885a44a876638086c32e8e370aaf16553c31b7fde0136
5
5
  SHA512:
6
- metadata.gz: e4a2266131f58c1d833172e95e3b87b80b49f120675b27baf5e70bd0c5cf191a1dafeb0374b407ff983b5d5e793479d79fdfd548921f161e2644df1dbb86abb8
7
- data.tar.gz: d791193329518eb1c277a24543abe1c3d4b62621af0046bd58a3347cdab3faa74da06dcc639dcd8c4d12ac0c76536652d91ada35db63b3d2fb71c7d618afa6b8
6
+ metadata.gz: 89f7a44c794130ecd4a4238ace5846d050412fd923cc5ae358cb469370c3dd380569bffe991b2251bc80baf4e183bd765591295b043ffc51fd88c5593b723f21
7
+ data.tar.gz: 4a9eece67d3f0e0cf8dd56a21440ce585397d531c4aa82d1d4c3d43db08169b57dd1e982ba74e68326ae61ed8278ac4d5fed7eefe8f6979dcf5a3c3a3e98ceb0
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
 
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
data/lib/kennel/api.rb CHANGED
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
  module Kennel
3
+ # encapsulates knowledge around how the api works
3
4
  class Api
5
+ CACHE_FILE = "tmp/cache/details"
6
+
4
7
  def initialize(app_key, api_key)
5
8
  @app_key = app_key
6
9
  @api_key = api_key
@@ -49,8 +52,27 @@ module Kennel
49
52
  request :delete, "/api/v1/#{api_resource}/#{id}", params: { force: "true" }, ignore_404: true
50
53
  end
51
54
 
55
+ def fill_details!(api_resource, list)
56
+ return unless api_resource == "dashboard"
57
+ details_cache do |cache|
58
+ Utils.parallel(list) { |a| fill_detail!(api_resource, a, cache) }
59
+ end
60
+ end
61
+
52
62
  private
53
63
 
64
+ # Make diff work even though we cannot mass-fetch definitions
65
+ def fill_detail!(api_resource, a, cache)
66
+ args = [api_resource, a.fetch(:id)]
67
+ full = cache.fetch(args, a.fetch(:modified_at)) { show(*args) }
68
+ a.merge!(full)
69
+ end
70
+
71
+ def details_cache(&block)
72
+ cache = FileCache.new CACHE_FILE, Kennel::VERSION
73
+ cache.open(&block)
74
+ end
75
+
54
76
  def request(method, path, body: nil, params: {}, ignore_404: false)
55
77
  params = params.merge(application_key: @app_key, api_key: @api_key)
56
78
  query = Faraday::FlatParamsEncoder.encode(params)
@@ -5,7 +5,6 @@ module Kennel
5
5
  include TemplateVariables
6
6
  include OptionalValidations
7
7
 
8
- API_LIST_INCOMPLETE = true
9
8
  DASHBOARD_DEFAULTS = { template_variables: [] }.freeze
10
9
  READONLY_ATTRIBUTES = superclass::READONLY_ATTRIBUTES + [
11
10
  :author_handle, :author_name, :modified_at, :url, :is_read_only, :notify_list
@@ -140,16 +139,16 @@ module Kennel
140
139
  when "uptime"
141
140
  if ids = definition[:monitor_ids]
142
141
  definition[:monitor_ids] = ids.map do |id|
143
- tracking_id?(id) ? resolve_link(id, :monitor, id_map, **args) : id
142
+ tracking_id?(id) ? (resolve_link(id, :monitor, id_map, **args) || id) : id
144
143
  end
145
144
  end
146
145
  when "alert_graph"
147
146
  if (id = definition[:alert_id]) && tracking_id?(id)
148
- definition[:alert_id] = resolve_link(id, :monitor, id_map, **args).to_s
147
+ definition[:alert_id] = (resolve_link(id, :monitor, id_map, **args) || id).to_s
149
148
  end
150
149
  when "slo"
151
150
  if (id = definition[:slo_id]) && tracking_id?(id)
152
- definition[:slo_id] = resolve_link(id, :slo, id_map, **args).to_s
151
+ definition[:slo_id] = (resolve_link(id, :slo, id_map, **args) || id).to_s
153
152
  end
154
153
  end
155
154
  end
@@ -186,22 +185,26 @@ module Kennel
186
185
  end
187
186
 
188
187
  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
188
+ definitions.map do |title, type, display_type, queries, options = {}, too_many_args = nil|
189
+ if title.is_a?(Hash) && !type
190
+ title # user gave a full widget, just use it
191
+ else
192
+ # validate inputs
193
+ if too_many_args || (!title || !type || !queries || !options.is_a?(Hash))
194
+ raise ArgumentError, "Expected exactly 5 arguments for each definition (title, type, display_type, queries, options)"
195
+ end
196
+ if (SUPPORTED_DEFINITION_OPTIONS | options.keys) != SUPPORTED_DEFINITION_OPTIONS
197
+ raise ArgumentError, "Supported options are: #{SUPPORTED_DEFINITION_OPTIONS.map(&:inspect).join(", ")}"
198
+ end
197
199
 
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
200
+ # build definition
201
+ requests = Array(queries).map do |q|
202
+ request = { q: q }
203
+ request[:display_type] = display_type if display_type
204
+ request
205
+ end
206
+ { definition: { title: title, type: type, requests: requests, **options } }
203
207
  end
204
- { definition: { title: title, type: type, requests: requests, **options } }
205
208
  end
206
209
  end
207
210
  end
@@ -112,9 +112,11 @@ module Kennel
112
112
  end
113
113
 
114
114
  def resolve_linked_tracking_ids!(id_map, **args)
115
- if as_json[:type] == "composite"
116
- as_json[:query] = as_json[:query].gsub(/%\{(.*?)\}/) do
117
- 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) || $&
118
120
  end
119
121
  end
120
122
  end
@@ -6,7 +6,6 @@ module Kennel
6
6
  READONLY_ATTRIBUTES = [
7
7
  :deleted, :id, :created, :created_at, :creator, :org_id, :modified, :modified_at, :api_resource
8
8
  ].freeze
9
- API_LIST_INCOMPLETE = false
10
9
 
11
10
  settings :id, :kennel_id
12
11
 
@@ -65,19 +64,18 @@ module Kennel
65
64
 
66
65
  private
67
66
 
68
- def resolve_link(id, type, id_map, force:)
69
- value = id_map[id]
70
- if value == :new
67
+ def resolve_link(tracking_id, type, id_map, force:)
68
+ id = id_map[tracking_id]
69
+ if id == :new
71
70
  if force
72
- # TODO: remove the need for this by sorting monitors by missing resolutions
73
- invalid! "#{id} needs to already exist, try again"
71
+ 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
72
  else
75
- id # will be re-resolved by syncer after the linked object was created
73
+ nil # will be re-resolved after the linked object was created
76
74
  end
77
- elsif value
78
- value
75
+ elsif id
76
+ id
79
77
  else
80
- invalid! "Unable to find #{type} #{id} (does not exist and is not being created by the current run)"
78
+ invalid! "Unable to find #{type} #{tracking_id} (does not exist and is not being created by the current run)"
81
79
  end
82
80
  end
83
81
 
@@ -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
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  module Kennel
3
3
  class Syncer
4
- CACHE_FILE = "tmp/cache/details" # keep in sync with .travis.yml caching
5
4
  TRACKING_FIELDS = [:message, :description].freeze
6
5
  DELETE_ORDER = ["dashboard", "slo", "monitor"].freeze # dashboards references monitors + slos, slos reference monitors
7
6
 
@@ -38,23 +37,14 @@ module Kennel
38
37
  end
39
38
 
40
39
  def update
41
- changed = (@create + @update).map { |_, e| e }
42
-
43
- @create.each do |_, e|
44
- e.resolve_linked_tracking_ids!({}, force: true)
45
-
40
+ each_resolved @create do |_, e|
46
41
  reply = @api.create e.class.api_resource, e.as_json
47
42
  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
-
43
+ populate_id_map [reply] # allow resolving ids we could previously no resolve
53
44
  Kennel.out.puts "Created #{e.class.api_resource} #{tracking_id(e.as_json)} #{e.url(id)}"
54
45
  end
55
46
 
56
- @update.each do |id, e|
57
- e.resolve_linked_tracking_ids!({}, force: true)
47
+ each_resolved @update do |id, e|
58
48
  @api.update e.class.api_resource, id, e.as_json
59
49
  Kennel.out.puts "Updated #{e.class.api_resource} #{tracking_id(e.as_json)} #{e.url(id)}"
60
50
  end
@@ -67,6 +57,37 @@ module Kennel
67
57
 
68
58
  private
69
59
 
60
+ # loop over items until everything is resolved or crash when we get stuck
61
+ # this solves cases like composite monitors depending on each other or monitor->monitor slo->slo monitor chains
62
+ def each_resolved(list)
63
+ list = list.dup
64
+ loop do
65
+ return if list.empty?
66
+ list.reject! do |id, e|
67
+ if resolved?(e)
68
+ yield id, e
69
+ true
70
+ else
71
+ false
72
+ end
73
+ end ||
74
+ assert_resolved(list[0][1]) # resolve something or show a circular dependency error
75
+ end
76
+ end
77
+
78
+ # TODO: optimize by storing an instance variable if already resolved
79
+ def resolved?(e)
80
+ assert_resolved e
81
+ true
82
+ rescue ValidationError
83
+ false
84
+ end
85
+
86
+ # raises ValidationError when not resolved
87
+ def assert_resolved(e)
88
+ resolve_linked_tracking_ids! [e], force: true
89
+ end
90
+
70
91
  def noop?
71
92
  @create.empty? && @update.empty? && @delete.empty?
72
93
  end
@@ -74,9 +95,15 @@ module Kennel
74
95
  def calculate_diff
75
96
  @update = []
76
97
  @delete = []
98
+ @id_map = {}
77
99
 
78
100
  actual = Progress.progress("Downloading definitions") { download_definitions }
79
- resolve_linked_tracking_ids! from: actual, to: @expected
101
+
102
+ # resolve dependencies to avoid diff
103
+ populate_id_map actual
104
+ @expected.each { |e| @id_map[e.tracking_id] ||= :new }
105
+ resolve_linked_tracking_ids! @expected
106
+
80
107
  filter_by_project! actual
81
108
 
82
109
  Progress.progress "Diffing" do
@@ -89,10 +116,10 @@ module Kennel
89
116
  end
90
117
  end
91
118
 
92
- details_cache do |cache|
93
- # fill details of things we need to compare (only do this part in parallel for safety & balancing)
94
- Utils.parallel(items.select { |e, _| e && e.class::API_LIST_INCOMPLETE }) { |_, a| fill_details(a, cache) }
95
- end
119
+ # fill details of things we need to compare
120
+ detailed = Hash.new { |h, k| h[k] = [] }
121
+ items.each { |e, a| detailed[a[:api_resource]] << a if e }
122
+ detailed.each { |api_resource, actuals| @api.fill_details! api_resource, actuals }
96
123
 
97
124
  # pick out things to update or delete
98
125
  items.each do |e, a|
@@ -107,27 +134,11 @@ module Kennel
107
134
 
108
135
  ensure_all_ids_found
109
136
  @create = @expected.map { |e| [nil, e] }
110
- @create.sort_by! { |_, e| -DELETE_ORDER.index(e.class.api_resource) }
111
137
  end
112
138
 
113
139
  @delete.sort_by! { |_, _, a| DELETE_ORDER.index a.fetch(:api_resource) }
114
140
  end
115
141
 
116
- # Make diff work even though we cannot mass-fetch definitions
117
- def fill_details(a, cache)
118
- resource = a.fetch(:api_resource)
119
- args = [resource, a.fetch(:id)]
120
- full = cache.fetch(args, a[:modified] || a.fetch(:modified_at)) do
121
- @api.show(*args)
122
- end
123
- a.merge!(full)
124
- end
125
-
126
- def details_cache(&block)
127
- cache = FileCache.new CACHE_FILE, Kennel::VERSION
128
- cache.open(&block)
129
- end
130
-
131
142
  def download_definitions
132
143
  Utils.parallel(Models::Record.subclasses.map(&:api_resource)) do |api_resource|
133
144
  results = @api.list(api_resource, with_downtimes: false) # lookup monitors without adding unnecessary downtime information
@@ -214,10 +225,12 @@ module Kennel
214
225
  end
215
226
  end
216
227
 
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) }
228
+ def populate_id_map(actual)
229
+ actual.each { |a| @id_map[tracking_id(a)] = a.fetch(:id) }
230
+ end
231
+
232
+ def resolve_linked_tracking_ids!(list, force: false)
233
+ list.each { |e| e.resolve_linked_tracking_ids!(@id_map, force: force) }
221
234
  end
222
235
 
223
236
  def filter_by_project!(definitions)
data/lib/kennel/tasks.rb CHANGED
@@ -132,8 +132,15 @@ namespace :kennel do
132
132
  else
133
133
  Kennel::Models::Record.subclasses.map(&:api_resource)
134
134
  end
135
+ api = Kennel.send(:api)
136
+ list = nil
137
+
135
138
  resources.each do |resource|
136
- Kennel.send(:api).list(resource).each do |r|
139
+ Kennel::Progress.progress("Downloading #{resource}") do
140
+ list = api.list(resource)
141
+ api.fill_details!(resource, list)
142
+ end
143
+ list.each do |r|
137
144
  Kennel.out.puts JSON.pretty_generate(r)
138
145
  end
139
146
  end
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.79.0"
3
+ VERSION = "1.82.0"
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.79.0
4
+ version: 1.82.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: 2020-12-30 00:00:00.000000000 Z
11
+ date: 2021-03-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday