krane 1.0.0 → 1.1.4

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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +1 -0
  3. data/{ISSUE_TEMPLATE.md → .github/ISSUE_TEMPLATE.md} +0 -0
  4. data/{pull_request_template.md → .github/pull_request_template.md} +0 -0
  5. data/.gitignore +0 -3
  6. data/.rubocop-http---shopify-github-io-ruby-style-guide-rubocop-yml +1020 -0
  7. data/.rubocop.yml +0 -12
  8. data/.shopify-build/{kubernetes-deploy.yml → krane.yml} +16 -1
  9. data/1.0-Upgrade.md +4 -5
  10. data/CHANGELOG.md +55 -1
  11. data/CONTRIBUTING.md +5 -5
  12. data/Gemfile +0 -11
  13. data/README.md +22 -24
  14. data/bin/ci +1 -1
  15. data/bin/test +2 -2
  16. data/dev.yml +4 -4
  17. data/krane.gemspec +22 -5
  18. data/lib/krane/cli/deploy_command.rb +3 -4
  19. data/lib/krane/cli/global_deploy_command.rb +3 -3
  20. data/lib/krane/cli/render_command.rb +3 -3
  21. data/lib/krane/cluster_resource_discovery.rb +9 -6
  22. data/lib/krane/concurrency.rb +2 -2
  23. data/lib/krane/container_logs.rb +1 -1
  24. data/lib/krane/container_overrides.rb +33 -0
  25. data/lib/krane/deploy_task.rb +5 -5
  26. data/lib/krane/ejson_secret_provisioner.rb +7 -4
  27. data/lib/krane/global_deploy_task.rb +3 -2
  28. data/lib/krane/kubectl.rb +11 -1
  29. data/lib/krane/kubernetes_resource.rb +7 -6
  30. data/lib/krane/kubernetes_resource/custom_resource.rb +1 -1
  31. data/lib/krane/kubernetes_resource/custom_resource_definition.rb +1 -1
  32. data/lib/krane/kubernetes_resource/daemon_set.rb +1 -0
  33. data/lib/krane/kubernetes_resource/deployment.rb +3 -2
  34. data/lib/krane/kubernetes_resource/pod.rb +12 -8
  35. data/lib/krane/kubernetes_resource/replica_set.rb +2 -16
  36. data/lib/krane/kubernetes_resource/service.rb +3 -7
  37. data/lib/krane/kubernetes_resource/stateful_set.rb +1 -0
  38. data/lib/krane/render_task.rb +2 -2
  39. data/lib/krane/resource_cache.rb +6 -0
  40. data/lib/krane/resource_watcher.rb +2 -1
  41. data/lib/krane/restart_task.rb +2 -2
  42. data/lib/krane/runner_task.rb +16 -17
  43. data/lib/krane/statsd.rb +2 -2
  44. data/lib/krane/template_sets.rb +1 -1
  45. data/lib/krane/version.rb +1 -1
  46. metadata +168 -20
  47. data/lib/krane/kubernetes_resource/cloudsql.rb +0 -43
  48. data/shipit.yml +0 -4
@@ -8,7 +8,8 @@ module Krane
8
8
  "filenames" => { type: :array, banner: 'config/deploy/production config/deploy/my-extra-resource.yml',
9
9
  aliases: :f, required: false, default: [],
10
10
  desc: "Directories and files that contains the configuration to apply" },
11
- "stdin" => { type: :boolean, default: false, desc: "Read resources from stdin" },
11
+ "stdin" => { type: :boolean, default: false,
12
+ desc: "[DEPRECATED] Read resources from stdin" },
12
13
  "global-timeout" => { type: :string, banner: "duration", default: DEFAULT_DEPLOY_TIMEOUT,
13
14
  desc: "Max duration to monitor workloads correctly deployed" },
14
15
  "verify-result" => { type: :boolean, default: true,
@@ -28,11 +29,10 @@ module Krane
28
29
 
29
30
  selector = ::Krane::LabelSelector.parse(options[:selector])
30
31
 
31
- # never mutate options directly
32
32
  filenames = options[:filenames].dup
33
33
  filenames << "-" if options[:stdin]
34
34
  if filenames.empty?
35
- raise Thor::RequiredArgumentMissingError, 'At least one of --filenames or --stdin must be set'
35
+ raise(Thor::RequiredArgumentMissingError, '--filenames must be set and not empty')
36
36
  end
37
37
 
38
38
  ::Krane::OptionsHelper.with_processed_template_paths(filenames) do |paths|
@@ -7,7 +7,8 @@ module Krane
7
7
  "bindings" => { type: :array, banner: "foo=bar abc=def", desc: 'Bindings for erb' },
8
8
  "filenames" => { type: :array, banner: 'config/deploy/production config/deploy/my-extra-resource.yml',
9
9
  required: false, default: [], aliases: 'f', desc: 'Directories and files to render' },
10
- "stdin" => { type: :boolean, desc: "Read resources from stdin", default: false },
10
+ "stdin" => { type: :boolean, default: false,
11
+ desc: "[DEPRECATED] Read resources from stdin" },
11
12
  "current-sha" => { type: :string, banner: "SHA", desc: "Expose SHA `current_sha` in ERB bindings",
12
13
  lazy_default: '' },
13
14
  }
@@ -20,11 +21,10 @@ module Krane
20
21
  bindings_parser = ::Krane::BindingsParser.new
21
22
  options[:bindings]&.each { |b| bindings_parser.add(b) }
22
23
 
23
- # never mutate options directly
24
24
  filenames = options[:filenames].dup
25
25
  filenames << "-" if options[:stdin]
26
26
  if filenames.empty?
27
- raise Thor::RequiredArgumentMissingError, 'At least one of --filenames or --stdin must be set'
27
+ raise(Thor::RequiredArgumentMissingError, '--filenames must be set and not empty')
28
28
  end
29
29
 
30
30
  ::Krane::OptionsHelper.with_processed_template_paths(filenames, render_erb: true) do |paths|
@@ -37,7 +37,7 @@ module Krane
37
37
  def fetch_resources(namespaced: false)
38
38
  command = %w(api-resources)
39
39
  command << "--namespaced=#{namespaced}"
40
- raw, _, st = kubectl.run(*command, output: "wide", attempts: 5,
40
+ raw, err, st = kubectl.run(*command, output: "wide", attempts: 5,
41
41
  use_namespace: false)
42
42
  if st.success?
43
43
  rows = raw.split("\n")
@@ -59,7 +59,7 @@ module Krane
59
59
  resource
60
60
  end
61
61
  else
62
- []
62
+ raise FatalKubeAPIError, "Error retrieving api-resources: #{err}"
63
63
  end
64
64
  end
65
65
 
@@ -68,7 +68,7 @@ module Krane
68
68
  # kubectl api-versions returns a list of group/version strings e.g. autoscaling/v2beta2
69
69
  # A kind may not exist in all versions of the group.
70
70
  def fetch_api_versions
71
- raw, _, st = kubectl.run("api-versions", attempts: 5, use_namespace: false)
71
+ raw, err, st = kubectl.run("api-versions", attempts: 5, use_namespace: false)
72
72
  # The "core" group is represented by an empty string
73
73
  versions = { "" => %w(v1) }
74
74
  if st.success?
@@ -78,6 +78,8 @@ module Krane
78
78
  versions[group] ||= []
79
79
  versions[group] << version
80
80
  end
81
+ else
82
+ raise FatalKubeAPIError, "Error retrieving api-versions: #{err}"
81
83
  end
82
84
  versions
83
85
  end
@@ -85,7 +87,8 @@ module Krane
85
87
  def version_for_kind(versions, kind)
86
88
  # Override list for kinds that don't appear in the lastest version of a group
87
89
  version_override = { "CronJob" => "v1beta1", "VolumeAttachment" => "v1beta1",
88
- "CSIDriver" => "v1beta1", "Ingress" => "v1beta1", "CSINode" => "v1beta1" }
90
+ "CSIDriver" => "v1beta1", "Ingress" => "v1beta1",
91
+ "CSINode" => "v1beta1", "Job" => "v1" }
89
92
 
90
93
  pattern = /v(?<major>\d+)(?<pre>alpha|beta)?(?<minor>\d+)?/
91
94
  latest = versions.sort_by do |version|
@@ -97,12 +100,12 @@ module Krane
97
100
  end
98
101
 
99
102
  def fetch_crds
100
- raw_json, _, st = kubectl.run("get", "CustomResourceDefinition", output: "json", attempts: 5,
103
+ raw_json, err, st = kubectl.run("get", "CustomResourceDefinition", output: "json", attempts: 5,
101
104
  use_namespace: false)
102
105
  if st.success?
103
106
  JSON.parse(raw_json)["items"]
104
107
  else
105
- []
108
+ raise FatalKubeAPIError, "Error retrieving CustomResourceDefinition: #{err}"
106
109
  end
107
110
  end
108
111
 
@@ -3,11 +3,11 @@ module Krane
3
3
  module Concurrency
4
4
  MAX_THREADS = 8
5
5
 
6
- def self.split_across_threads(all_work, &block)
6
+ def self.split_across_threads(all_work, max_threads: MAX_THREADS, &block)
7
7
  return if all_work.empty?
8
8
  raise ArgumentError, "Block of work is required" unless block_given?
9
9
 
10
- slice_size = ((all_work.length + MAX_THREADS - 1) / MAX_THREADS)
10
+ slice_size = ((all_work.length + max_threads - 1) / max_threads)
11
11
  threads = []
12
12
  all_work.each_slice(slice_size) do |work_group|
13
13
  threads << Thread.new { work_group.each(&block) }
@@ -3,7 +3,7 @@ module Krane
3
3
  class ContainerLogs
4
4
  attr_reader :lines, :container_name
5
5
 
6
- DEFAULT_LINE_LIMIT = 250
6
+ DEFAULT_LINE_LIMIT = 25
7
7
 
8
8
  def initialize(parent_id:, container_name:, namespace:, context:, logger:)
9
9
  @parent_id = parent_id
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+ module Krane
3
+ class ContainerOverrides
4
+ attr_reader :command, :arguments, :env_vars, :image_tag
5
+
6
+ def initialize(command: nil, arguments: nil, env_vars: [], image_tag: nil)
7
+ @command = command
8
+ @arguments = arguments
9
+ @env_vars = env_vars
10
+ @image_tag = image_tag
11
+ end
12
+
13
+ def apply!(container)
14
+ container.command = command if command
15
+ container.args = arguments if arguments
16
+
17
+ if image_tag
18
+ image = container.image
19
+ base_image, _old_tag = image.split(':')
20
+ new_image = "#{base_image}:#{image_tag}"
21
+
22
+ container.image = new_image
23
+ end
24
+
25
+ env_args = env_vars.map do |env|
26
+ key, value = env.split('=', 2)
27
+ { name: key, value: value }
28
+ end
29
+ container.env ||= []
30
+ container.env = container.env.map(&:to_h) + env_args
31
+ end
32
+ end
33
+ end
@@ -10,7 +10,6 @@ require 'krane/resource_cache'
10
10
  require 'krane/kubernetes_resource'
11
11
  %w(
12
12
  custom_resource
13
- cloudsql
14
13
  config_map
15
14
  deployment
16
15
  ingress
@@ -60,14 +59,14 @@ module Krane
60
59
  before_crs = %w(
61
60
  ResourceQuota
62
61
  NetworkPolicy
63
- )
64
- after_crs = %w(
65
62
  ConfigMap
66
63
  PersistentVolumeClaim
67
64
  ServiceAccount
68
65
  Role
69
66
  RoleBinding
70
67
  Secret
68
+ )
69
+ after_crs = %w(
71
70
  Pod
72
71
  )
73
72
 
@@ -117,8 +116,8 @@ module Krane
117
116
  # Runs the task, returning a boolean representing success or failure
118
117
  #
119
118
  # @return [Boolean]
120
- def run(*args)
121
- run!(*args)
119
+ def run(**args)
120
+ run!(**args)
122
121
  true
123
122
  rescue FatalDeploymentError
124
123
  false
@@ -216,6 +215,7 @@ module Krane
216
215
 
217
216
  def check_initial_status(resources)
218
217
  cache = ResourceCache.new(@task_config)
218
+ cache.prewarm(resources)
219
219
  Krane::Concurrency.split_across_threads(resources) { |r| r.sync(cache) }
220
220
  resources.each { |r| @logger.info(r.pretty_status) }
221
221
  end
@@ -134,10 +134,13 @@ module Krane
134
134
  end
135
135
 
136
136
  def decrypt_ejson(key_dir)
137
- # ejson seems to dump both errors and output to STDOUT
138
- out_err, st = Open3.capture2e("EJSON_KEYDIR=#{key_dir} ejson decrypt #{@ejson_file}")
139
- raise EjsonSecretError, out_err unless st.success?
140
- JSON.parse(out_err)
137
+ out, err, st = Open3.capture3("EJSON_KEYDIR=#{key_dir} ejson decrypt #{@ejson_file}")
138
+ unless st.success?
139
+ # older ejson versions dump some errors to STDOUT
140
+ msg = err.presence || out
141
+ raise EjsonSecretError, msg
142
+ end
143
+ JSON.parse(out)
141
144
  rescue JSON::ParserError
142
145
  raise EjsonSecretError, "Failed to parse decrypted ejson"
143
146
  end
@@ -46,8 +46,8 @@ module Krane
46
46
  # Runs the task, returning a boolean representing success or failure
47
47
  #
48
48
  # @return [Boolean]
49
- def run(*args)
50
- run!(*args)
49
+ def run(**args)
50
+ run!(**args)
51
51
  true
52
52
  rescue FatalDeploymentError
53
53
  false
@@ -202,6 +202,7 @@ module Krane
202
202
 
203
203
  def check_initial_status(resources)
204
204
  cache = ResourceCache.new(@task_config)
205
+ cache.prewarm(resources)
205
206
  Concurrency.split_across_threads(resources) { |r| r.sync(cache) }
206
207
  resources.each { |r| logger.info(r.pretty_status) }
207
208
  end
@@ -6,6 +6,7 @@ module Krane
6
6
  ERROR_MATCHERS = {
7
7
  not_found: /NotFound/,
8
8
  client_timeout: /Client\.Timeout exceeded while awaiting headers/,
9
+ empty: /\A\z/,
9
10
  }
10
11
  DEFAULT_TIMEOUT = 15
11
12
  MAX_RETRY_DELAY = 16
@@ -34,7 +35,16 @@ module Krane
34
35
  (1..attempts).to_a.each do |current_attempt|
35
36
  logger.debug("Running command (attempt #{current_attempt}): #{cmd.join(' ')}")
36
37
  out, err, st = Open3.capture3(*cmd)
37
- logger.debug("Kubectl out: " + out.gsub(/\s+/, ' ')) unless output_is_sensitive
38
+
39
+ # https://github.com/Shopify/krane/issues/395
40
+ unless out.valid_encoding?
41
+ out = out.dup.force_encoding(Encoding::UTF_8)
42
+ end
43
+
44
+ if logger.debug? && !output_is_sensitive
45
+ # don't do the gsub unless we're going to print this
46
+ logger.debug("Kubectl out: " + out.gsub(/\s+/, ' '))
47
+ end
38
48
 
39
49
  break if st.success?
40
50
  raise(ResourceNotFoundError, err) if err.match(ERROR_MATCHERS[:not_found]) && raise_if_not_found
@@ -16,7 +16,7 @@ module Krane
16
16
  TIMEOUT = 5.minutes
17
17
  LOG_LINE_COUNT = 250
18
18
  SERVER_DRY_RUN_DISABLED_ERROR =
19
- /(unknown flag: --server-dry-run)|(doesn't support dry-run)|(dryRun alpha feature is disabled)/
19
+ /(unknown flag: --server-dry-run)|(does[\s\']n[o|']t support dry[-\s]run)|(dryRun alpha feature is disabled)/
20
20
 
21
21
  DISABLE_FETCHING_LOG_INFO = 'DISABLE_FETCHING_LOG_INFO'
22
22
  DISABLE_FETCHING_EVENT_INFO = 'DISABLE_FETCHING_EVENT_INFO'
@@ -38,6 +38,7 @@ module Krane
38
38
  LAST_APPLIED_ANNOTATION = "kubectl.kubernetes.io/last-applied-configuration"
39
39
  SENSITIVE_TEMPLATE_CONTENT = false
40
40
  SERVER_DRY_RUNNABLE = false
41
+ SYNC_DEPENDENCIES = []
41
42
 
42
43
  class << self
43
44
  def build(namespace: nil, context:, definition:, logger:, statsd_tags:, crd: nil, global_names: [])
@@ -59,8 +60,8 @@ module Krane
59
60
  end
60
61
 
61
62
  def class_for_kind(kind)
62
- if Krane.const_defined?(kind)
63
- Krane.const_get(kind)
63
+ if Krane.const_defined?(kind, false)
64
+ Krane.const_get(kind, false)
64
65
  end
65
66
  rescue NameError
66
67
  nil
@@ -255,7 +256,7 @@ module Krane
255
256
  if cause == :gave_up
256
257
  debug_heading = ColorizedString.new("#{id}: GLOBAL WATCH TIMEOUT (#{info_hash[:timeout]} seconds)").yellow
257
258
  helpful_info << "If you expected it to take longer than #{info_hash[:timeout]} seconds for your deploy"\
258
- " to roll out, increase --max-watch-seconds."
259
+ " to roll out, increase --global-timeout."
259
260
  elsif deploy_failed?
260
261
  debug_heading = ColorizedString.new("#{id}: FAILED").red
261
262
  helpful_info << failure_message if failure_message.present?
@@ -571,7 +572,7 @@ module Krane
571
572
  def validate_with_server_side_dry_run(kubectl)
572
573
  command = ["apply", "-f", file_path, "--server-dry-run", "--output=name"]
573
574
  kubectl.run(*command, log_failure: false, output_is_sensitive: sensitive_template_content?,
574
- retry_whitelist: [:client_timeout], attempts: 3)
575
+ retry_whitelist: [:client_timeout, :empty], attempts: 3)
575
576
  end
576
577
 
577
578
  # Local dry run is supported on only create and apply
@@ -581,7 +582,7 @@ module Krane
581
582
  verb = deploy_method == :apply ? "apply" : "create"
582
583
  command = [verb, "-f", file_path, "--dry-run", "--output=name"]
583
584
  kubectl.run(*command, log_failure: false, output_is_sensitive: sensitive_template_content?,
584
- retry_whitelist: [:client_timeout], attempts: 3, use_namespace: !global?)
585
+ retry_whitelist: [:client_timeout, :empty], attempts: 3, use_namespace: !global?)
585
586
  end
586
587
 
587
588
  def labels
@@ -62,7 +62,7 @@ module Krane
62
62
  kind
63
63
  end
64
64
 
65
- def validate_definition(*)
65
+ def validate_definition(*, **)
66
66
  super
67
67
 
68
68
  @crd.validate_rollout_conditions
@@ -66,7 +66,7 @@ module Krane
66
66
  @rollout_conditions = nil
67
67
  end
68
68
 
69
- def validate_definition(*)
69
+ def validate_definition(*, **)
70
70
  super
71
71
 
72
72
  validate_rollout_conditions
@@ -3,6 +3,7 @@ require "krane/kubernetes_resource/pod_set_base"
3
3
  module Krane
4
4
  class DaemonSet < PodSetBase
5
5
  TIMEOUT = 5.minutes
6
+ SYNC_DEPENDENCIES = %w(Pod)
6
7
  attr_reader :pods
7
8
 
8
9
  def sync(cache)
@@ -4,6 +4,7 @@ require 'krane/kubernetes_resource/replica_set'
4
4
  module Krane
5
5
  class Deployment < KubernetesResource
6
6
  TIMEOUT = 7.minutes
7
+ SYNC_DEPENDENCIES = %w(Pod ReplicaSet)
7
8
  REQUIRED_ROLLOUT_ANNOTATION_SUFFIX = "required-rollout"
8
9
  REQUIRED_ROLLOUT_ANNOTATION_DEPRECATED = "kubernetes-deploy.shopify.io/#{REQUIRED_ROLLOUT_ANNOTATION_SUFFIX}"
9
10
  REQUIRED_ROLLOUT_ANNOTATION = "krane.shopify.io/#{REQUIRED_ROLLOUT_ANNOTATION_SUFFIX}"
@@ -96,7 +97,7 @@ module Krane
96
97
  progress_condition.present? ? deploy_failing_to_progress? : super
97
98
  end
98
99
 
99
- def validate_definition(*)
100
+ def validate_definition(*, **)
100
101
  super
101
102
 
102
103
  unless REQUIRED_ROLLOUT_TYPES.include?(required_rollout) || percent?(required_rollout)
@@ -190,7 +191,7 @@ module Krane
190
191
  def min_available_replicas
191
192
  if percent?(required_rollout)
192
193
  (desired_replicas * required_rollout.to_i / 100.0).ceil
193
- elsif max_unavailable =~ /%/
194
+ elsif max_unavailable.is_a?(String) && max_unavailable =~ /%/
194
195
  (desired_replicas * (100 - max_unavailable.to_i) / 100.0).ceil
195
196
  else
196
197
  desired_replicas - max_unavailable.to_i
@@ -219,14 +219,7 @@ module Krane
219
219
  limbo_reason = @status.dig("state", "waiting", "reason")
220
220
  limbo_message = @status.dig("state", "waiting", "message")
221
221
 
222
- if @status.dig("lastState", "terminated", "reason") == "ContainerCannotRun"
223
- # ref: https://github.com/kubernetes/kubernetes/blob/562e721ece8a16e05c7e7d6bdd6334c910733ab2/pkg/kubelet/dockershim/docker_container.go#L353
224
- exit_code = @status.dig('lastState', 'terminated', 'exitCode')
225
- "Failed to start (exit #{exit_code}): #{@status.dig('lastState', 'terminated', 'message')}"
226
- elsif @status.dig("state", "terminated", "reason") == "ContainerCannotRun"
227
- exit_code = @status.dig('state', 'terminated', 'exitCode')
228
- "Failed to start (exit #{exit_code}): #{@status.dig('state', 'terminated', 'message')}"
229
- elsif limbo_reason == "CrashLoopBackOff"
222
+ if limbo_reason == "CrashLoopBackOff"
230
223
  exit_code = @status.dig('lastState', 'terminated', 'exitCode')
231
224
  "Crashing repeatedly (exit #{exit_code}). See logs for more information."
232
225
  elsif limbo_reason == "ErrImagePull" && limbo_message.match(/not found/i)
@@ -234,6 +227,17 @@ module Krane
234
227
  "Did you wait for it to be built and pushed to the registry before deploying?"
235
228
  elsif limbo_reason == "CreateContainerConfigError"
236
229
  "Failed to generate container configuration: #{limbo_message}"
230
+ elsif @status.dig("lastState", "terminated", "reason") == "ContainerCannotRun"
231
+ # ref: https://github.com/kubernetes/kubernetes/blob/562e721ece8a16e05c7e7d6bdd6334c910733ab2/pkg/kubelet/dockershim/docker_container.go#L353
232
+ exit_code = @status.dig('lastState', 'terminated', 'exitCode')
233
+ # We've observed failures here that are actually issues with the node or kube infra, and not with the
234
+ # container. These issues have been transient and result in a 128 exit code, so do not treat these as fatal.
235
+ return if exit_code == 128
236
+ "Failed to start (exit #{exit_code}): #{@status.dig('lastState', 'terminated', 'message')}"
237
+ elsif @status.dig("state", "terminated", "reason") == "ContainerCannotRun"
238
+ exit_code = @status.dig('state', 'terminated', 'exitCode')
239
+ return if exit_code == 128
240
+ "Failed to start (exit #{exit_code}): #{@status.dig('state', 'terminated', 'message')}"
237
241
  end
238
242
  end
239
243
 
@@ -4,6 +4,7 @@ require 'krane/kubernetes_resource/pod_set_base'
4
4
  module Krane
5
5
  class ReplicaSet < PodSetBase
6
6
  TIMEOUT = 5.minutes
7
+ SYNC_DEPENDENCIES = %w(Pod)
7
8
  attr_reader :pods
8
9
 
9
10
  def initialize(namespace:, context:, definition:, logger:, statsd_tags: nil,
@@ -17,7 +18,7 @@ module Krane
17
18
 
18
19
  def sync(cache)
19
20
  super
20
- @pods = fetch_pods_if_needed(cache) || []
21
+ @pods = exists? ? find_pods(cache) : []
21
22
  end
22
23
 
23
24
  def status
@@ -58,21 +59,6 @@ module Krane
58
59
  observed_generation != current_generation
59
60
  end
60
61
 
61
- def fetch_pods_if_needed(cache)
62
- # If the ReplicaSet doesn't exist, its pods won't either
63
- return unless exists?
64
- # If the status hasn't been updated yet, we're not going to make a determination anyway
65
- return if stale_status?
66
- # If we don't want any pods at all, we don't need to look for them
67
- return if desired_replicas == 0
68
- # We only need to fetch pods so that deploy_failed? can check that they aren't ALL bad.
69
- # If we can already tell some pods are ok from the RS data, don't bother fetching them (which can be expensive)
70
- # Lower numbers here make us more susceptible to being fooled by replicas without probes briefly appearing ready
71
- return if ready_replicas > 1
72
-
73
- find_pods(cache)
74
- end
75
-
76
62
  def rollout_data
77
63
  return { "replicas" => 0 } unless exists?
78
64
  { "replicas" => 0 }.merge(