krane 1.1.1 → 2.1.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.
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/.rubocop-http---shopify-github-io-ruby-style-guide-rubocop-yml +27 -33
  6. data/.rubocop.yml +0 -12
  7. data/.shopify-build/krane.yml +20 -6
  8. data/1.0-Upgrade.md +1 -1
  9. data/CHANGELOG.md +57 -0
  10. data/CONTRIBUTING.md +2 -2
  11. data/README.md +17 -14
  12. data/bin/ci +1 -1
  13. data/bin/test +2 -2
  14. data/dev.yml +3 -2
  15. data/krane.gemspec +6 -4
  16. data/lib/krane/annotation.rb +11 -0
  17. data/lib/krane/cli/deploy_command.rb +2 -3
  18. data/lib/krane/cli/global_deploy_command.rb +3 -3
  19. data/lib/krane/cli/render_command.rb +3 -3
  20. data/lib/krane/cluster_resource_discovery.rb +10 -6
  21. data/lib/krane/concerns/template_reporting.rb +0 -6
  22. data/lib/krane/container_logs.rb +1 -1
  23. data/lib/krane/container_overrides.rb +33 -0
  24. data/lib/krane/deploy_task.rb +21 -18
  25. data/lib/krane/ejson_secret_provisioner.rb +1 -2
  26. data/lib/krane/global_deploy_task.rb +8 -14
  27. data/lib/krane/kubeclient_builder.rb +4 -2
  28. data/lib/krane/kubectl.rb +18 -5
  29. data/lib/krane/kubernetes_resource.rb +32 -42
  30. data/lib/krane/kubernetes_resource/custom_resource.rb +2 -2
  31. data/lib/krane/kubernetes_resource/custom_resource_definition.rb +13 -10
  32. data/lib/krane/kubernetes_resource/deployment.rb +5 -7
  33. data/lib/krane/kubernetes_resource/persistent_volume_claim.rb +1 -0
  34. data/lib/krane/kubernetes_resource/pod.rb +12 -8
  35. data/lib/krane/psych_k8s_compatibility.rb +36 -0
  36. data/lib/krane/render_task.rb +2 -2
  37. data/lib/krane/renderer.rb +2 -0
  38. data/lib/krane/resource_deployer.rb +9 -5
  39. data/lib/krane/resource_watcher.rb +1 -1
  40. data/lib/krane/restart_task.rb +8 -8
  41. data/lib/krane/runner_task.rb +21 -24
  42. data/lib/krane/statsd.rb +2 -2
  43. data/lib/krane/task_config.rb +7 -2
  44. data/lib/krane/task_config_validator.rb +3 -3
  45. data/lib/krane/template_sets.rb +1 -1
  46. data/lib/krane/version.rb +1 -1
  47. metadata +17 -13
  48. data/lib/krane/kubernetes_resource/cloudsql.rb +0 -44
@@ -10,8 +10,10 @@ module Krane
10
10
  end
11
11
  end
12
12
 
13
- def initialize(kubeconfig: ENV["KUBECONFIG"])
14
- files = kubeconfig || "#{Dir.home}/.kube/config"
13
+ attr_reader :kubeconfig_files
14
+
15
+ def initialize(kubeconfig: nil)
16
+ files = kubeconfig || ENV["KUBECONFIG"] || "#{Dir.home}/.kube/config"
15
17
  # Split the list by colon for Linux and Mac, and semicolon for Windows.
16
18
  @kubeconfig_files = files.split(/[:;]/).map!(&:strip).reject(&:empty?)
17
19
  end
@@ -6,6 +6,8 @@ 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/,
10
+ context_deadline: /context deadline exceeded/,
9
11
  }
10
12
  DEFAULT_TIMEOUT = 15
11
13
  MAX_RETRY_DELAY = 16
@@ -13,7 +15,7 @@ module Krane
13
15
 
14
16
  class ResourceNotFoundError < StandardError; end
15
17
 
16
- delegate :namespace, :context, :logger, to: :@task_config
18
+ delegate :namespace, :context, :logger, :kubeconfig, to: :@task_config
17
19
 
18
20
  def initialize(task_config:, log_failure_by_default:, default_timeout: DEFAULT_TIMEOUT,
19
21
  output_is_sensitive_default: false)
@@ -33,8 +35,18 @@ module Krane
33
35
 
34
36
  (1..attempts).to_a.each do |current_attempt|
35
37
  logger.debug("Running command (attempt #{current_attempt}): #{cmd.join(' ')}")
36
- out, err, st = Open3.capture3(*cmd)
37
- logger.debug("Kubectl out: " + out.gsub(/\s+/, ' ')) unless output_is_sensitive
38
+ env = { 'KUBECONFIG' => kubeconfig }
39
+ out, err, st = Open3.capture3(env, *cmd)
40
+
41
+ # https://github.com/Shopify/krane/issues/395
42
+ unless out.valid_encoding?
43
+ out = out.dup.force_encoding(Encoding::UTF_8)
44
+ end
45
+
46
+ if logger.debug? && !output_is_sensitive
47
+ # don't do the gsub unless we're going to print this
48
+ logger.debug("Kubectl out: " + out.gsub(/\s+/, ' '))
49
+ end
38
50
 
39
51
  break if st.success?
40
52
  raise(ResourceNotFoundError, err) if err.match(ERROR_MATCHERS[:not_found]) && raise_if_not_found
@@ -52,7 +64,8 @@ module Krane
52
64
  else
53
65
  logger.debug("Kubectl err: #{output_is_sensitive ? '<suppressed sensitive output>' : err}")
54
66
  end
55
- StatsD.client.increment('kubectl.error', 1, tags: { context: context, namespace: namespace, cmd: cmd[1] })
67
+ StatsD.client.increment('kubectl.error', 1, tags: { context: context, namespace: namespace, cmd: cmd[1],
68
+ max_attempt: attempts, current_attempt: current_attempt })
56
69
 
57
70
  break unless retriable_err?(err, retry_whitelist) && current_attempt < attempts
58
71
  sleep(retry_delay(current_attempt))
@@ -69,7 +82,7 @@ module Krane
69
82
  def version_info
70
83
  @version_info ||=
71
84
  begin
72
- response, _, status = run("version", use_namespace: false, log_failure: true)
85
+ response, _, status = run("version", use_namespace: false, log_failure: true, attempts: 2)
73
86
  raise KubectlError, "Could not retrieve kubectl version info" unless status.success?
74
87
  extract_version_info_from_kubectl_response(response)
75
88
  end
@@ -6,9 +6,12 @@ require 'krane/remote_logs'
6
6
  require 'krane/duration_parser'
7
7
  require 'krane/label_selector'
8
8
  require 'krane/rollout_conditions'
9
+ require 'krane/psych_k8s_compatibility'
9
10
 
10
11
  module Krane
11
12
  class KubernetesResource
13
+ using PsychK8sCompatibility
14
+
12
15
  attr_reader :name, :namespace, :context
13
16
  attr_writer :type, :deploy_started_at, :global
14
17
 
@@ -32,9 +35,9 @@ module Krane
32
35
  If you have reason to believe it will succeed, retry the deploy to continue to monitor the rollout.
33
36
  MSG
34
37
 
35
- TIMEOUT_OVERRIDE_ANNOTATION_SUFFIX = "timeout-override"
36
- TIMEOUT_OVERRIDE_ANNOTATION_DEPRECATED = "kubernetes-deploy.shopify.io/#{TIMEOUT_OVERRIDE_ANNOTATION_SUFFIX}"
37
- TIMEOUT_OVERRIDE_ANNOTATION = "krane.shopify.io/#{TIMEOUT_OVERRIDE_ANNOTATION_SUFFIX}"
38
+ ALLOWED_DEPLOY_METHOD_OVERRIDES = %w(create replace replace-force)
39
+ DEPLOY_METHOD_OVERRIDE_ANNOTATION = "deploy-method-override"
40
+ TIMEOUT_OVERRIDE_ANNOTATION = "timeout-override"
38
41
  LAST_APPLIED_ANNOTATION = "kubectl.kubernetes.io/last-applied-configuration"
39
42
  SENSITIVE_TEMPLATE_CONTENT = false
40
43
  SERVER_DRY_RUNNABLE = false
@@ -60,8 +63,8 @@ module Krane
60
63
  end
61
64
 
62
65
  def class_for_kind(kind)
63
- if Krane.const_defined?(kind)
64
- Krane.const_get(kind)
66
+ if Krane.const_defined?(kind, false)
67
+ Krane.const_get(kind, false)
65
68
  end
66
69
  rescue NameError
67
70
  nil
@@ -103,7 +106,7 @@ module Krane
103
106
  def timeout_override
104
107
  return @timeout_override if defined?(@timeout_override)
105
108
 
106
- @timeout_override = DurationParser.new(krane_annotation_value(TIMEOUT_OVERRIDE_ANNOTATION_SUFFIX)).parse!.to_i
109
+ @timeout_override = DurationParser.new(krane_annotation_value(TIMEOUT_OVERRIDE_ANNOTATION)).parse!.to_i
107
110
  rescue DurationParser::ParsingError
108
111
  @timeout_override = nil
109
112
  end
@@ -123,7 +126,6 @@ module Krane
123
126
  @statsd_report_done = false
124
127
  @disappeared = false
125
128
  @validation_errors = []
126
- @validation_warnings = []
127
129
  @instance_data = {}
128
130
  @server_dry_run_validated = false
129
131
  end
@@ -134,22 +136,13 @@ module Krane
134
136
 
135
137
  def validate_definition(kubectl, selector: nil)
136
138
  @validation_errors = []
137
- @validation_warnings = []
138
139
  validate_selector(selector) if selector
139
140
  validate_timeout_annotation
140
- validate_annotation_version
141
+ validate_deploy_method_override_annotation
141
142
  validate_spec_with_kubectl(kubectl)
142
143
  @validation_errors.present?
143
144
  end
144
145
 
145
- def validation_warning_msg
146
- @validation_warnings.join("\n")
147
- end
148
-
149
- def has_warnings?
150
- @validation_warnings.present?
151
- end
152
-
153
146
  def validation_error_msg
154
147
  @validation_errors.join("\n")
155
148
  end
@@ -228,6 +221,11 @@ module Krane
228
221
  @type || self.class.kind
229
222
  end
230
223
 
224
+ def group
225
+ grouping, version = @definition.dig("apiVersion").split("/")
226
+ version ? grouping : "core"
227
+ end
228
+
231
229
  def kubectl_resource_type
232
230
  type
233
231
  end
@@ -242,10 +240,14 @@ module Krane
242
240
  if @definition.dig("metadata", "name").blank? && uses_generate_name?
243
241
  :create
244
242
  else
245
- :apply
243
+ deploy_method_override || :apply
246
244
  end
247
245
  end
248
246
 
247
+ def deploy_method_override
248
+ krane_annotation_value(DEPLOY_METHOD_OVERRIDE_ANNOTATION)&.to_sym
249
+ end
250
+
249
251
  def sync_debug_info(kubectl)
250
252
  @debug_events = fetch_events(kubectl) unless ENV[DISABLE_FETCHING_EVENT_INFO]
251
253
  @debug_logs = fetch_debug_logs if print_debug_logs? && !ENV[DISABLE_FETCHING_LOG_INFO]
@@ -495,8 +497,8 @@ module Krane
495
497
  private
496
498
 
497
499
  def validate_timeout_annotation
498
- timeout_override_value = krane_annotation_value(TIMEOUT_OVERRIDE_ANNOTATION_SUFFIX)
499
- timeout_annotation_key = krane_annotation_key(TIMEOUT_OVERRIDE_ANNOTATION_SUFFIX)
500
+ timeout_override_value = krane_annotation_value(TIMEOUT_OVERRIDE_ANNOTATION)
501
+ timeout_annotation_key = Annotation.for(TIMEOUT_OVERRIDE_ANNOTATION)
500
502
  return if timeout_override_value.nil?
501
503
 
502
504
  override = DurationParser.new(timeout_override_value).parse!
@@ -509,30 +511,18 @@ module Krane
509
511
  @validation_errors << "#{timeout_annotation_key} annotation is invalid: #{e}"
510
512
  end
511
513
 
512
- def validate_annotation_version
513
- return if validation_warning_msg.include?("annotations is deprecated")
514
- annotation_keys = @definition.dig("metadata", "annotations")&.keys
515
- annotation_keys&.each do |annotation|
516
- if annotation.include?("kubernetes-deploy.shopify.io")
517
- annotation_prefix = annotation.split('/').first
518
- @validation_warnings << "#{annotation_prefix} as a prefix for annotations is deprecated: "\
519
- "Use the 'krane.shopify.io' annotation prefix instead"
520
- return
521
- end
514
+ def validate_deploy_method_override_annotation
515
+ deploy_method_override_value = krane_annotation_value(DEPLOY_METHOD_OVERRIDE_ANNOTATION)
516
+ deploy_method_override_annotation_key = Annotation.for(DEPLOY_METHOD_OVERRIDE_ANNOTATION)
517
+ return unless deploy_method_override_value
518
+ unless ALLOWED_DEPLOY_METHOD_OVERRIDES.include?(deploy_method_override_value)
519
+ @validation_errors << "#{deploy_method_override_annotation_key} is invalid: Accepted values are: " \
520
+ "#{ALLOWED_DEPLOY_METHOD_OVERRIDES.join(', ')} but got #{deploy_method_override_value}"
522
521
  end
523
522
  end
524
523
 
525
524
  def krane_annotation_value(suffix)
526
- @definition.dig("metadata", "annotations", "kubernetes-deploy.shopify.io/#{suffix}") ||
527
- @definition.dig("metadata", "annotations", "krane.shopify.io/#{suffix}")
528
- end
529
-
530
- def krane_annotation_key(suffix)
531
- if @definition.dig("metadata", "annotations", "kubernetes-deploy.shopify.io/#{suffix}")
532
- "kubernetes-deploy.shopify.io/#{suffix}"
533
- elsif @definition.dig("metadata", "annotations", "krane.shopify.io/#{suffix}")
534
- "krane.shopify.io/#{suffix}"
535
- end
525
+ @definition.dig("metadata", "annotations", Annotation.for(suffix))
536
526
  end
537
527
 
538
528
  def validate_selector(selector)
@@ -572,7 +562,7 @@ module Krane
572
562
  def validate_with_server_side_dry_run(kubectl)
573
563
  command = ["apply", "-f", file_path, "--server-dry-run", "--output=name"]
574
564
  kubectl.run(*command, log_failure: false, output_is_sensitive: sensitive_template_content?,
575
- retry_whitelist: [:client_timeout], attempts: 3)
565
+ retry_whitelist: [:client_timeout, :empty, :context_deadline], attempts: 3)
576
566
  end
577
567
 
578
568
  # Local dry run is supported on only create and apply
@@ -582,7 +572,7 @@ module Krane
582
572
  verb = deploy_method == :apply ? "apply" : "create"
583
573
  command = [verb, "-f", file_path, "--dry-run", "--output=name"]
584
574
  kubectl.run(*command, log_failure: false, output_is_sensitive: sensitive_template_content?,
585
- retry_whitelist: [:client_timeout], attempts: 3, use_namespace: !global?)
575
+ retry_whitelist: [:client_timeout, :empty, :context_deadline], attempts: 3, use_namespace: !global?)
586
576
  end
587
577
 
588
578
  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
@@ -70,7 +70,7 @@ module Krane
70
70
  @validation_errors << "The CRD that specifies this resource is using invalid rollout conditions. " \
71
71
  "Krane will not be able to continue until those rollout conditions are fixed.\n" \
72
72
  "Rollout conditions can be found on the CRD that defines this resource (#{@crd.name}), " \
73
- "under the annotation #{CustomResourceDefinition::ROLLOUT_CONDITIONS_ANNOTATION}.\n" \
73
+ "under the annotation #{Annotation.for(CustomResourceDefinition::ROLLOUT_CONDITIONS_ANNOTATION)}.\n" \
74
74
  "Validation failed with: #{e}"
75
75
  end
76
76
 
@@ -2,9 +2,8 @@
2
2
  module Krane
3
3
  class CustomResourceDefinition < KubernetesResource
4
4
  TIMEOUT = 2.minutes
5
- ROLLOUT_CONDITIONS_ANNOTATION_SUFFIX = "instance-rollout-conditions"
6
- ROLLOUT_CONDITIONS_ANNOTATION = "krane.shopify.io/#{ROLLOUT_CONDITIONS_ANNOTATION_SUFFIX}"
7
- TIMEOUT_FOR_INSTANCE_ANNOTATION = "krane.shopify.io/instance-timeout"
5
+ ROLLOUT_CONDITIONS_ANNOTATION = "instance-rollout-conditions"
6
+ TIMEOUT_FOR_INSTANCE_ANNOTATION = "instance-timeout"
8
7
  GLOBAL = true
9
8
 
10
9
  def deploy_succeeded?
@@ -20,7 +19,7 @@ module Krane
20
19
  end
21
20
 
22
21
  def timeout_for_instance
23
- timeout = krane_annotation_value("instance-timeout")
22
+ timeout = krane_annotation_value(TIMEOUT_FOR_INSTANCE_ANNOTATION)
24
23
  DurationParser.new(timeout).parse!.to_i
25
24
  rescue DurationParser::ParsingError
26
25
  nil
@@ -46,6 +45,10 @@ module Krane
46
45
  @definition.dig("spec", "names", "kind")
47
46
  end
48
47
 
48
+ def group
49
+ @definition.dig("spec", "group")
50
+ end
51
+
49
52
  def prunable?
50
53
  prunable = krane_annotation_value("prunable")
51
54
  prunable == "true"
@@ -59,25 +62,25 @@ module Krane
59
62
  def rollout_conditions
60
63
  return @rollout_conditions if defined?(@rollout_conditions)
61
64
 
62
- @rollout_conditions = if krane_annotation_value(ROLLOUT_CONDITIONS_ANNOTATION_SUFFIX)
63
- RolloutConditions.from_annotation(krane_annotation_value(ROLLOUT_CONDITIONS_ANNOTATION_SUFFIX))
65
+ @rollout_conditions = if krane_annotation_value(ROLLOUT_CONDITIONS_ANNOTATION)
66
+ RolloutConditions.from_annotation(krane_annotation_value(ROLLOUT_CONDITIONS_ANNOTATION))
64
67
  end
65
68
  rescue RolloutConditionsError
66
69
  @rollout_conditions = nil
67
70
  end
68
71
 
69
- def validate_definition(*)
72
+ def validate_definition(*, **)
70
73
  super
71
74
 
72
75
  validate_rollout_conditions
73
76
  rescue RolloutConditionsError => e
74
- @validation_errors << "Annotation #{krane_annotation_key(ROLLOUT_CONDITIONS_ANNOTATION_SUFFIX)} "\
77
+ @validation_errors << "Annotation #{Annotation.for(ROLLOUT_CONDITIONS_ANNOTATION)} " \
75
78
  "on #{name} is invalid: #{e}"
76
79
  end
77
80
 
78
81
  def validate_rollout_conditions
79
- if krane_annotation_value(ROLLOUT_CONDITIONS_ANNOTATION_SUFFIX) && @rollout_conditions_validated.nil?
80
- conditions = RolloutConditions.from_annotation(krane_annotation_value(ROLLOUT_CONDITIONS_ANNOTATION_SUFFIX))
82
+ if krane_annotation_value(ROLLOUT_CONDITIONS_ANNOTATION) && @rollout_conditions_validated.nil?
83
+ conditions = RolloutConditions.from_annotation(krane_annotation_value(ROLLOUT_CONDITIONS_ANNOTATION))
81
84
  conditions.validate!
82
85
  end
83
86
 
@@ -5,9 +5,7 @@ module Krane
5
5
  class Deployment < KubernetesResource
6
6
  TIMEOUT = 7.minutes
7
7
  SYNC_DEPENDENCIES = %w(Pod ReplicaSet)
8
- REQUIRED_ROLLOUT_ANNOTATION_SUFFIX = "required-rollout"
9
- REQUIRED_ROLLOUT_ANNOTATION_DEPRECATED = "kubernetes-deploy.shopify.io/#{REQUIRED_ROLLOUT_ANNOTATION_SUFFIX}"
10
- REQUIRED_ROLLOUT_ANNOTATION = "krane.shopify.io/#{REQUIRED_ROLLOUT_ANNOTATION_SUFFIX}"
8
+ REQUIRED_ROLLOUT_ANNOTATION = "required-rollout"
11
9
  REQUIRED_ROLLOUT_TYPES = %w(maxUnavailable full none).freeze
12
10
  DEFAULT_REQUIRED_ROLLOUT = 'full'
13
11
 
@@ -97,7 +95,7 @@ module Krane
97
95
  progress_condition.present? ? deploy_failing_to_progress? : super
98
96
  end
99
97
 
100
- def validate_definition(*)
98
+ def validate_definition(*, **)
101
99
  super
102
100
 
103
101
  unless REQUIRED_ROLLOUT_TYPES.include?(required_rollout) || percent?(required_rollout)
@@ -106,7 +104,7 @@ module Krane
106
104
 
107
105
  strategy = @definition.dig('spec', 'strategy', 'type').to_s
108
106
  if required_rollout.downcase == 'maxunavailable' && strategy.present? && strategy.downcase != 'rollingupdate'
109
- @validation_errors << "'#{krane_annotation_key(REQUIRED_ROLLOUT_ANNOTATION_SUFFIX)}: #{required_rollout}' "\
107
+ @validation_errors << "'#{Annotation.for(REQUIRED_ROLLOUT_ANNOTATION)}: #{required_rollout}' " \
110
108
  "is incompatible with strategy '#{strategy}'"
111
109
  end
112
110
 
@@ -151,7 +149,7 @@ module Krane
151
149
  end
152
150
 
153
151
  def rollout_annotation_err_msg
154
- "'#{krane_annotation_key(REQUIRED_ROLLOUT_ANNOTATION_SUFFIX)}: #{required_rollout}' is invalid. "\
152
+ "'#{Annotation.for(REQUIRED_ROLLOUT_ANNOTATION)}: #{required_rollout}' is invalid. " \
155
153
  "Acceptable values: #{REQUIRED_ROLLOUT_TYPES.join(', ')}"
156
154
  end
157
155
 
@@ -191,7 +189,7 @@ module Krane
191
189
  def min_available_replicas
192
190
  if percent?(required_rollout)
193
191
  (desired_replicas * required_rollout.to_i / 100.0).ceil
194
- elsif max_unavailable =~ /%/
192
+ elsif max_unavailable.is_a?(String) && max_unavailable =~ /%/
195
193
  (desired_replicas * (100 - max_unavailable.to_i) / 100.0).ceil
196
194
  else
197
195
  desired_replicas - max_unavailable.to_i
@@ -63,6 +63,7 @@ module Krane
63
63
  attr_reader :name
64
64
 
65
65
  def initialize(definition)
66
+ super(definition: definition, namespace: nil, context: nil, logger: nil)
66
67
  @definition = definition
67
68
  @name = definition.dig("metadata", "name").to_s
68
69
  end
@@ -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
 
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'psych'
4
+
5
+ module PsychK8sCompatibility
6
+ def self.massage_node(n)
7
+ if n.is_a?(Psych::Nodes::Scalar) &&
8
+ (n.style == Psych::Nodes::Scalar::PLAIN) &&
9
+ n.value.is_a?(String) &&
10
+ n.value =~ /\A[+-]?\d+(?:\.\d+)?[eE][+-]?\d+\z/
11
+ n.style = Psych::Nodes::Scalar::DOUBLE_QUOTED
12
+ end
13
+ end
14
+
15
+ refine Psych.singleton_class do
16
+ def dump(o, io = nil, options = {})
17
+ if io.is_a?(Hash)
18
+ options = io
19
+ io = nil
20
+ end
21
+ visitor = Psych::Visitors::YAMLTree.create(options)
22
+ visitor << o
23
+ visitor.tree.each { |n| PsychK8sCompatibility.massage_node(n) }
24
+ visitor.tree.yaml(io, options)
25
+ end
26
+
27
+ def dump_stream(*objects)
28
+ visitor = Psych::Visitors::YAMLTree.create({})
29
+ objects.each do |o|
30
+ visitor << o
31
+ end
32
+ visitor.tree.each { |n| PsychK8sCompatibility.massage_node(n) }
33
+ visitor.tree.yaml
34
+ end
35
+ end
36
+ end
@@ -24,8 +24,8 @@ module Krane
24
24
  # Runs the task, returning a boolean representing success or failure
25
25
  #
26
26
  # @return [Boolean]
27
- def run(*args)
28
- run!(*args)
27
+ def run(**args)
28
+ run!(**args)
29
29
  true
30
30
  rescue Krane::FatalDeploymentError
31
31
  false