kennel 1.79.0 → 1.82.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: 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