kubernetes-deploy 0.19.0 → 0.20.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
  SHA1:
3
- metadata.gz: 99f56eb5da1d7f17885cff3d19787b45a1484504
4
- data.tar.gz: e2919b899b313e2c4be0af2b7731816f9c8336de
3
+ metadata.gz: a581b73a9b59ce7dbc35a528acf269ba9ee12523
4
+ data.tar.gz: 4d3b6268317bf30b3011d1eb3e9f8bcc9c731c4f
5
5
  SHA512:
6
- metadata.gz: 103546e46a2e476e1cc1ae339fcce8362fd199d144d890d1b895ed46848181ba822dcb713eb672581f625b25e3992c4ea9439e59888c4b3f0b74b44d156bba50
7
- data.tar.gz: 7aeb474292e52f2b3e9ead4f038732804949c253da2119993c89d64e1357d7920c2031bce184821cb711c091ed54f8b6edf181c47d72b9242c3d2e0c972d7355
6
+ metadata.gz: 15197ca3f8d90b28927dc8a5ab365bb7f455434c49af708db82b2f8f5dd7430420a8e4819edbc1d4579cd2291df0a002f11924be3a865631a11e5a1fa3f3b344
7
+ data.tar.gz: bae4f408b4bfad19e61754991f24bf34d48dca2929f1d33dbd592b2fcc009b7135adc3dc92201c1e8d7633d80520a798e03aedec45778d59d81aa0a872ab7426
@@ -1,10 +1,11 @@
1
- - name: 'Run Test Suite (:kubernetes: 1.10-latest)'
2
- command: bin/ci
3
- agents:
4
- queue: minikube-ci
5
- env:
6
- LOGGING_LEVEL: 4
7
- KUBERNETES_VERSION: v1.10-latest
1
+ # Disableed until minikube can run 1.10
2
+ # - name: 'Run Test Suite (:kubernetes: 1.10-latest)'
3
+ # command: bin/ci
4
+ # agents:
5
+ # queue: minikube-ci
6
+ # env:
7
+ # LOGGING_LEVEL: 4
8
+ # KUBERNETES_VERSION: v1.10-latest
8
9
  - name: 'Run Test Suite (:kubernetes: 1.9-latest)'
9
10
  command: bin/ci
10
11
  agents:
data/CHANGELOG.md CHANGED
@@ -1,3 +1,23 @@
1
+ ### Master
2
+
3
+ *Features*
4
+
5
+ *Bug Fixes*
6
+
7
+ *Enhancements*
8
+
9
+ ### 0.20.0
10
+
11
+ *Features*
12
+ - Automatically add all Kubernetes namespace labels to StatsD tags ([#278](https://github.com/Shopify/kubernetes-deploy/pull/278))
13
+
14
+ *Bug Fixes*
15
+ - Prevent calling sleep with a negative value ([#273](https://github.com/Shopify/kubernetes-deploy/pull/273))
16
+ - Prevent no-op redeploys of bad code from hanging forever ([#262](https://github.com/Shopify/kubernetes-deploy/pull/262))
17
+
18
+ *Enhancements*
19
+ - Improve output for rendering errors ([#253](https://github.com/Shopify/kubernetes-deploy/pull/253))
20
+
1
21
  ### 0.19.0
2
22
  *Features*
3
23
  - Added `--max-watch-seconds=seconds` to kubernetes-restart and kubernetes-deploy. When set
data/dev.yml CHANGED
@@ -8,7 +8,7 @@ up:
8
8
  - custom:
9
9
  name: Minikube Cluster
10
10
  met?: test $(minikube status | grep Running | wc -l) -eq 2 && $(minikube status | grep -q 'Correctly Configured')
11
- meet: minikube start --vm-driver=xhyve --kubernetes-version=v1.7.5
11
+ meet: minikube start --kubernetes-version=v1.9.4 --vm-driver=hyperkit
12
12
  down: minikube stop
13
13
  commands:
14
14
  reset-minikube: minikube delete && rm -rf ~/.minikube
@@ -36,5 +36,5 @@ Gem::Specification.new do |spec|
36
36
  spec.add_development_dependency "minitest", "~> 5.0"
37
37
  spec.add_development_dependency "minitest-stub-const", "~> 0.6"
38
38
  spec.add_development_dependency "webmock", "~> 3.0"
39
- spec.add_development_dependency "mocha", "~> 1.1"
39
+ spec.add_development_dependency "mocha", "~> 1.5"
40
40
  end
@@ -92,6 +92,7 @@ module KubernetesDeploy
92
92
  def initialize(namespace:, context:, current_sha:, template_dir:, logger:, kubectl_instance: nil, bindings: {},
93
93
  max_watch_seconds: nil)
94
94
  @namespace = namespace
95
+ @namespace_tags = []
95
96
  @context = context
96
97
  @current_sha = current_sha
97
98
  @template_dir = File.expand_path(template_dir)
@@ -122,6 +123,7 @@ module KubernetesDeploy
122
123
  validate_configuration(allow_protected_ns: allow_protected_ns, prune: prune)
123
124
  confirm_context_exists
124
125
  confirm_namespace_exists
126
+ @namespace_tags |= tags_from_namespace_labels
125
127
  resources = discover_resources
126
128
  validate_definitions(resources)
127
129
 
@@ -187,15 +189,6 @@ module KubernetesDeploy
187
189
 
188
190
  private
189
191
 
190
- # Inspect the file referenced in the kubectl stderr
191
- # to make it easier for developer to understand what's going on
192
- def find_bad_files_from_kubectl_output(stderr)
193
- # stderr often contains one or more lines like the following, from which we can extract the file path(s):
194
- # Error from server (TypeOfError): error when creating "/path/to/service-gqq5oh.yml": Service "web" is invalid:
195
- matches = stderr.scan(%r{"(/\S+\.ya?ml\S*)"})
196
- matches&.flatten
197
- end
198
-
199
192
  def deploy_has_priority_resources?(resources)
200
193
  resources.any? { |r| PREDEPLOY_SEQUENCE.include?(r.type) }
201
194
  end
@@ -225,7 +218,8 @@ module KubernetesDeploy
225
218
  return unless failed_resources.present?
226
219
 
227
220
  failed_resources.each do |r|
228
- record_invalid_template(r.validation_error_msg, file_paths: [r.file_path])
221
+ content = File.read(r.file_path) if File.file?(r.file_path)
222
+ record_invalid_template(err: r.validation_error_msg, filename: File.basename(r.file_path), content: content)
229
223
  end
230
224
  raise FatalDeploymentError, "Template validation failed"
231
225
  end
@@ -238,7 +232,8 @@ module KubernetesDeploy
238
232
  next unless filename.end_with?(".yml.erb", ".yml", ".yaml", ".yaml.erb")
239
233
 
240
234
  split_templates(filename) do |r_def|
241
- r = KubernetesResource.build(namespace: @namespace, context: @context, logger: @logger, definition: r_def)
235
+ r = KubernetesResource.build(namespace: @namespace, context: @context, logger: @logger,
236
+ definition: r_def, statsd_tags: @namespace_tags)
242
237
  resources << r
243
238
  @logger.info " - #{r.id}"
244
239
  end
@@ -250,34 +245,25 @@ module KubernetesDeploy
250
245
  file_content = File.read(File.join(@template_dir, filename))
251
246
  rendered_content = @renderer.render_template(filename, file_content)
252
247
  YAML.load_stream(rendered_content) do |doc|
253
- yield doc unless doc.blank?
248
+ next if doc.blank?
249
+ unless doc.is_a?(Hash)
250
+ raise InvalidTemplateError.new("Template is not a valid Kubernetes manifest",
251
+ filename: filename, content: doc)
252
+ end
253
+ yield doc
254
254
  end
255
+ rescue InvalidTemplateError => e
256
+ record_invalid_template(err: e.message, filename: e.filename, content: e.content)
257
+ raise FatalDeploymentError, "Failed to render and parse template"
255
258
  rescue Psych::SyntaxError => e
256
- debug_msg = <<~INFO
257
- Error message: #{e}
258
-
259
- Template content:
260
- ---
261
- INFO
262
- debug_msg += rendered_content
263
- @logger.summary.add_paragraph(debug_msg)
264
- raise FatalDeploymentError, "Template '#{filename}' cannot be parsed"
259
+ record_invalid_template(err: e.message, filename: filename, content: rendered_content)
260
+ raise FatalDeploymentError, "Failed to render and parse template"
265
261
  end
266
262
 
267
- def record_invalid_template(err, file_paths:, original_filenames: nil)
268
- template_names = Array(original_filenames)
269
- file_content = Array(file_paths).each_with_object([]) do |file_path, contents|
270
- next unless File.file?(file_path)
271
- contents << File.read(file_path)
272
- template_names << File.basename(file_path) unless original_filenames
273
- end.join("\n")
274
- template_list = template_names.compact.join(", ").presence || "See error message"
275
-
276
- debug_msg = ColorizedString.new("Invalid #{'template'.pluralize(template_names.length)}: #{template_list}\n").red
277
- debug_msg += "> Error from kubectl:\n#{indent_four(err)}"
278
- if file_content.present?
279
- debug_msg += "\n> Rendered template content:\n#{indent_four(file_content)}"
280
- end
263
+ def record_invalid_template(err:, filename:, content:)
264
+ debug_msg = ColorizedString.new("Invalid template: #{filename}\n").red
265
+ debug_msg += "> Error message:\n#{indent_four(err)}"
266
+ debug_msg += "\n> Template content:\n#{indent_four(content)}"
281
267
  @logger.summary.add_paragraph(debug_msg)
282
268
  end
283
269
 
@@ -406,11 +392,7 @@ module KubernetesDeploy
406
392
  if st.success?
407
393
  log_pruning(out) if prune
408
394
  else
409
- file_paths = find_bad_files_from_kubectl_output(err)
410
- warn_msg = "WARNING: Any resources not mentioned in the error below were likely created/updated. " \
411
- "You may wish to roll back this deploy."
412
- @logger.summary.add_paragraph(ColorizedString.new(warn_msg).yellow)
413
- record_invalid_template(err, file_paths: file_paths)
395
+ record_apply_failure(err)
414
396
  raise FatalDeploymentError, "Command failed: #{Shellwords.join(command)}"
415
397
  end
416
398
  end
@@ -424,6 +406,41 @@ module KubernetesDeploy
424
406
  @logger.summary.add_action("pruned #{pruned.length} #{'resource'.pluralize(pruned.length)}")
425
407
  end
426
408
 
409
+ def record_apply_failure(err)
410
+ warn_msg = "WARNING: Any resources not mentioned in the error(s) below were likely created/updated. " \
411
+ "You may wish to roll back this deploy."
412
+ @logger.summary.add_paragraph(ColorizedString.new(warn_msg).yellow)
413
+
414
+ unidentified_errors = []
415
+ err.each_line do |line|
416
+ bad_files = find_bad_files_from_kubectl_output(line)
417
+ if bad_files.present?
418
+ bad_files.each { |f| record_invalid_template(err: f[:err], filename: f[:filename], content: f[:content]) }
419
+ else
420
+ unidentified_errors << line
421
+ end
422
+ end
423
+
424
+ if unidentified_errors.present?
425
+ msg = "#{ColorizedString.new('Unidentified error(s):').red}\n#{indent_four(unidentified_errors.join)}"
426
+ @logger.summary.add_paragraph(msg)
427
+ end
428
+ end
429
+
430
+ # Inspect the file referenced in the kubectl stderr
431
+ # to make it easier for developer to understand what's going on
432
+ def find_bad_files_from_kubectl_output(line)
433
+ # stderr often contains one or more lines like the following, from which we can extract the file path(s):
434
+ # Error from server (TypeOfError): error when creating "/path/to/service-gqq5oh.yml": Service "web" is invalid:
435
+
436
+ line.scan(%r{"(/\S+\.ya?ml\S*)"}).each_with_object([]) do |matches, bad_files|
437
+ matches.each do |path|
438
+ content = File.read(path) if File.file?(path)
439
+ bad_files << { filename: File.basename(path), err: line, content: content }
440
+ end
441
+ end
442
+ end
443
+
427
444
  def confirm_context_exists
428
445
  out, err, st = kubectl.run("config", "get-contexts", "-o", "name",
429
446
  use_namespace: false, use_context: false, log_failure: false)
@@ -462,12 +479,24 @@ module KubernetesDeploy
462
479
  @logger.info("Namespace #{@namespace} found")
463
480
  end
464
481
 
482
+ def tags_from_namespace_labels
483
+ namespace_info = nil
484
+ with_retries(2) do
485
+ namespace_info, _, st = kubectl.run("get", "namespace", @namespace, "-o", "json", use_namespace: false,
486
+ log_failure: true)
487
+ st.success?
488
+ end
489
+ return [] if namespace_info.blank?
490
+ namespace_labels = JSON.parse(namespace_info, symbolize_names: true).fetch(:metadata, {}).fetch(:labels, {})
491
+ namespace_labels.map { |key, value| "#{key}:#{value}" }
492
+ end
493
+
465
494
  def kubectl
466
495
  @kubectl ||= Kubectl.new(namespace: @namespace, context: @context, logger: @logger, log_failure_by_default: true)
467
496
  end
468
497
 
469
498
  def statsd_tags
470
- %W(namespace:#{@namespace} sha:#{@current_sha} context:#{@context})
499
+ %W(namespace:#{@namespace} sha:#{@current_sha} context:#{@context}) | @namespace_tags
471
500
  end
472
501
 
473
502
  def with_retries(limit)
@@ -3,6 +3,15 @@ module KubernetesDeploy
3
3
  class FatalDeploymentError < StandardError; end
4
4
  class KubectlError < StandardError; end
5
5
 
6
+ class InvalidTemplateError < FatalDeploymentError
7
+ attr_reader :filename, :content
8
+ def initialize(err, filename:, content: nil)
9
+ @filename = filename
10
+ @content = content
11
+ super(err)
12
+ end
13
+ end
14
+
6
15
  class NamespaceNotFoundError < FatalDeploymentError
7
16
  def initialize(name, context)
8
17
  super("Namespace `#{name}` not found in context `#{context}`")
@@ -28,8 +28,9 @@ module KubernetesDeploy
28
28
  TIMEOUT_OVERRIDE_ANNOTATION = "kubernetes-deploy.shopify.io/timeout-override"
29
29
 
30
30
  class << self
31
- def build(namespace:, context:, definition:, logger:)
32
- opts = { namespace: namespace, context: context, definition: definition, logger: logger }
31
+ def build(namespace:, context:, definition:, logger:, statsd_tags:)
32
+ opts = { namespace: namespace, context: context, definition: definition, logger: logger,
33
+ statsd_tags: statsd_tags }
33
34
  if KubernetesDeploy.const_defined?(definition["kind"])
34
35
  klass = KubernetesDeploy.const_get(definition["kind"])
35
36
  klass.new(**opts)
@@ -65,7 +66,7 @@ module KubernetesDeploy
65
66
  "timeout: #{timeout}s"
66
67
  end
67
68
 
68
- def initialize(namespace:, context:, definition:, logger:)
69
+ def initialize(namespace:, context:, definition:, logger:, statsd_tags: [])
69
70
  # subclasses must also set these if they define their own initializer
70
71
  @name = definition.dig("metadata", "name")
71
72
  unless @name.present?
@@ -73,6 +74,7 @@ module KubernetesDeploy
73
74
  raise FatalDeploymentError, "Template is missing required field metadata.name"
74
75
  end
75
76
 
77
+ @optional_statsd_tags = statsd_tags
76
78
  @namespace = namespace
77
79
  @context = context
78
80
  @logger = logger
@@ -356,7 +358,9 @@ module KubernetesDeploy
356
358
  else
357
359
  "unknown"
358
360
  end
359
- %W(context:#{context} namespace:#{namespace} resource:#{id} type:#{type} sha:#{ENV['REVISION']} status:#{status})
361
+ tags = %W(context:#{context} namespace:#{namespace} resource:#{id}
362
+ type:#{type} sha:#{ENV['REVISION']} status:#{status})
363
+ tags | @optional_statsd_tags
360
364
  end
361
365
  end
362
366
  end
@@ -96,6 +96,16 @@ module KubernetesDeploy
96
96
 
97
97
  private
98
98
 
99
+ def current_generation
100
+ return -2 unless exists? # different default than observed
101
+ @instance_data.dig('metadata', 'generation')
102
+ end
103
+
104
+ def observed_generation
105
+ return -1 unless exists? # different default than current
106
+ @instance_data.dig('status', 'observedGeneration')
107
+ end
108
+
99
109
  def desired_replicas
100
110
  return -1 unless exists?
101
111
  @instance_data["spec"]["replicas"].to_i
@@ -133,12 +143,13 @@ module KubernetesDeploy
133
143
  # Deployments were being updated prematurely with incorrect progress information
134
144
  # https://github.com/kubernetes/kubernetes/issues/49637
135
145
  return false unless Time.now.utc - @deploy_started_at >= progress_deadline.to_i
136
- else
137
- return false unless deploy_started?
138
146
  end
139
147
 
140
- progress_condition["status"] == 'False' &&
141
- Time.parse(progress_condition["lastUpdateTime"]).to_i >= (@deploy_started_at - 5.seconds).to_i
148
+ # This assumes that when the controller bumps the observed generation, it also updates/clears all the status
149
+ # conditions. Specifically, it assumes the progress condition is immediately set to True if a rollout is starting.
150
+ deploy_started? &&
151
+ current_generation == observed_generation &&
152
+ progress_condition["status"] == 'False'
142
153
  end
143
154
 
144
155
  def find_latest_rs(mediator)
@@ -5,7 +5,8 @@ module KubernetesDeploy
5
5
 
6
6
  FAILED_PHASE_NAME = "Failed"
7
7
 
8
- def initialize(namespace:, context:, definition:, logger:, parent: nil, deploy_started_at: nil)
8
+ def initialize(namespace:, context:, definition:, logger:,
9
+ statsd_tags: nil, parent: nil, deploy_started_at: nil)
9
10
  @parent = parent
10
11
  @deploy_started_at = deploy_started_at
11
12
  @containers = definition.fetch("spec", {}).fetch("containers", []).map { |c| Container.new(c) }
@@ -14,7 +15,8 @@ module KubernetesDeploy
14
15
  raise FatalDeploymentError, "Template is missing required field spec.containers"
15
16
  end
16
17
  @containers += definition["spec"].fetch("initContainers", []).map { |c| Container.new(c, init_container: true) }
17
- super(namespace: namespace, context: context, definition: definition, logger: logger)
18
+ super(namespace: namespace, context: context, definition: definition,
19
+ logger: logger, statsd_tags: statsd_tags)
18
20
  end
19
21
 
20
22
  def sync(mediator)
@@ -5,11 +5,13 @@ module KubernetesDeploy
5
5
  TIMEOUT = 5.minutes
6
6
  attr_reader :pods
7
7
 
8
- def initialize(namespace:, context:, definition:, logger:, parent: nil, deploy_started_at: nil)
8
+ def initialize(namespace:, context:, definition:, logger:, statsd_tags: nil,
9
+ parent: nil, deploy_started_at: nil)
9
10
  @parent = parent
10
11
  @deploy_started_at = deploy_started_at
11
12
  @pods = []
12
- super(namespace: namespace, context: context, definition: definition, logger: logger)
13
+ super(namespace: namespace, context: context, definition: definition,
14
+ logger: logger, statsd_tags: statsd_tags)
13
15
  end
14
16
 
15
17
  SYNC_DEPENDENCIES = %w(Pod)
@@ -7,14 +7,14 @@ require 'json'
7
7
 
8
8
  module KubernetesDeploy
9
9
  class Renderer
10
- class InvalidPartialError < FatalDeploymentError
11
- attr_reader :parents
12
- def initialize(msg, parents = [])
10
+ class InvalidPartialError < InvalidTemplateError
11
+ attr_accessor :parents, :content, :filename
12
+ def initialize(msg, parents: [], content: nil, filename:)
13
13
  @parents = parents
14
- super(msg)
14
+ super(msg, content: content, filename: filename)
15
15
  end
16
16
  end
17
- class PartialNotFound < InvalidPartialError; end
17
+ class PartialNotFound < InvalidTemplateError; end
18
18
 
19
19
  def initialize(current_sha:, template_dir:, logger:, bindings: {})
20
20
  @current_sha = current_sha
@@ -35,11 +35,11 @@ module KubernetesDeploy
35
35
 
36
36
  ERB.new(raw_template, nil, '-').result(erb_binding)
37
37
  rescue InvalidPartialError => err
38
- all_parents = err.parents.dup.unshift(filename)
39
- raise FatalDeploymentError, "#{err.message} (included from: #{all_parents.join(' -> ')})"
38
+ err.parents = err.parents.dup.unshift(filename)
39
+ err.filename = "#{err.filename} (partial included from: #{err.parents.join(' -> ')})"
40
+ raise err
40
41
  rescue StandardError => err
41
- report_template_invalid(err.message, raw_template)
42
- raise FatalDeploymentError, "Template '#{filename}' cannot be rendered"
42
+ raise InvalidTemplateError.new(err.message, filename: filename, content: raw_template)
43
43
  end
44
44
 
45
45
  def render_partial(partial, locals)
@@ -60,12 +60,14 @@ module KubernetesDeploy
60
60
  # Note that JSON is a subset of YAML.
61
61
  JSON.generate(docs.children.first.to_ruby)
62
62
  rescue PartialNotFound => err
63
- raise InvalidPartialError, err.message
63
+ # get the filename from the first parent, not the missing partial itself
64
+ raise err if err.filename == partial
65
+ raise InvalidPartialError.new(err.message, filename: partial, content: expanded_template || template)
64
66
  rescue InvalidPartialError => err
65
- raise InvalidPartialError.new(err.message, err.parents.dup.unshift(File.basename(partial_path)))
67
+ err.parents = err.parents.dup.unshift(File.basename(partial_path))
68
+ raise err
66
69
  rescue StandardError => err
67
- report_template_invalid(err.message, expanded_template)
68
- raise InvalidPartialError, "Template '#{partial_path}' cannot be rendered"
70
+ raise InvalidPartialError.new(err.message, filename: partial_path, content: expanded_template || template)
69
71
  end
70
72
 
71
73
  private
@@ -91,12 +93,8 @@ module KubernetesDeploy
91
93
  return partial_path if File.exist?(partial_path)
92
94
  end
93
95
  end
94
- raise PartialNotFound, "Could not find partial '#{name}' in any of #{@partials_dirs.join(':')}"
95
- end
96
-
97
- def report_template_invalid(message, content)
98
- @logger.summary.add_paragraph("Error from renderer:\n #{message.tr("\n", ' ')}")
99
- @logger.summary.add_paragraph("Rendered template content:\n#{content}")
96
+ raise PartialNotFound.new("Could not find partial '#{name}' in any of #{@partials_dirs.join(':')}",
97
+ filename: name)
100
98
  end
101
99
 
102
100
  class TemplateContext
@@ -24,8 +24,8 @@ module KubernetesDeploy
24
24
  if @timeout && (Time.now.utc - monitoring_started > @timeout)
25
25
  report_and_give_up(remainder)
26
26
  end
27
- if Time.now.utc < delay_sync_until
28
- sleep(delay_sync_until - Time.now.utc)
27
+ if (sleep_duration = delay_sync_until - Time.now.utc) > 0
28
+ sleep(sleep_duration)
29
29
  end
30
30
  delay_sync_until = Time.now.utc + delay_sync # don't pummel the API if the sync is fast
31
31
 
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module KubernetesDeploy
3
- VERSION = "0.19.0"
3
+ VERSION = "0.20.0"
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kubernetes-deploy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.19.0
4
+ version: 0.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Katrina Verey
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2018-04-09 00:00:00.000000000 Z
12
+ date: 2018-04-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -185,14 +185,14 @@ dependencies:
185
185
  requirements:
186
186
  - - "~>"
187
187
  - !ruby/object:Gem::Version
188
- version: '1.1'
188
+ version: '1.5'
189
189
  type: :development
190
190
  prerelease: false
191
191
  version_requirements: !ruby/object:Gem::Requirement
192
192
  requirements:
193
193
  - - "~>"
194
194
  - !ruby/object:Gem::Version
195
- version: '1.1'
195
+ version: '1.5'
196
196
  description: Kubernetes deploy scripts
197
197
  email:
198
198
  - ops-accounts+shipit@shopify.com