krane 1.1.1 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
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