krane 1.1.2 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -12,10 +12,10 @@ module Krane
12
12
  end
13
13
 
14
14
  class EjsonSecretProvisioner
15
- EJSON_SECRET_ANNOTATION = "kubernetes-deploy.shopify.io/ejson-secret"
16
15
  EJSON_SECRET_KEY = "kubernetes_secrets"
17
16
  EJSON_SECRETS_FILE = "secrets.ejson"
18
17
  EJSON_KEYS_SECRET = "ejson-keys"
18
+
19
19
  delegate :namespace, :context, :logger, to: :@task_config
20
20
 
21
21
  def initialize(task_config:, ejson_keys_secret:, ejson_file:, statsd_tags:, selector: nil)
@@ -106,7 +106,6 @@ module Krane
106
106
  "name" => secret_name,
107
107
  "labels" => labels,
108
108
  "namespace" => namespace,
109
- "annotations" => { EJSON_SECRET_ANNOTATION => "true" },
110
109
  },
111
110
  "data" => encoded_data,
112
111
  }
@@ -134,7 +133,7 @@ module Krane
134
133
  end
135
134
 
136
135
  def decrypt_ejson(key_dir)
137
- out, err, st = Open3.capture3("EJSON_KEYDIR=#{key_dir} ejson decrypt #{@ejson_file}")
136
+ out, err, st = Open3.capture3({ 'EJSON_KEYDIR' => key_dir.to_s }, 'ejson', 'decrypt', @ejson_file.to_s)
138
137
  unless st.success?
139
138
  # older ejson versions dump some errors to STDOUT
140
139
  msg = err.presence || out
@@ -25,7 +25,8 @@ module Krane
25
25
  class GlobalDeployTask
26
26
  extend Krane::StatsD::MeasureMethods
27
27
  include TemplateReporting
28
- delegate :context, :logger, :global_kinds, to: :@task_config
28
+ delegate :context, :logger, :global_kinds, :kubeclient_builder, to: :@task_config
29
+ attr_reader :task_config
29
30
 
30
31
  # Initializes the deploy task
31
32
  #
@@ -33,10 +34,10 @@ module Krane
33
34
  # @param global_timeout [Integer] Timeout in seconds
34
35
  # @param selector [Hash] Selector(s) parsed by Krane::LabelSelector (*required*)
35
36
  # @param filenames [Array<String>] An array of filenames and/or directories containing templates (*required*)
36
- def initialize(context:, global_timeout: nil, selector: nil, filenames: [], logger: nil)
37
+ def initialize(context:, global_timeout: nil, selector: nil, filenames: [], logger: nil, kubeconfig: nil)
37
38
  template_paths = filenames.map { |path| File.expand_path(path) }
38
39
 
39
- @task_config = TaskConfig.new(context, nil, logger)
40
+ @task_config = TaskConfig.new(context, nil, logger, kubeconfig)
40
41
  @template_sets = TemplateSets.from_dirs_and_files(paths: template_paths,
41
42
  logger: @task_config.logger, render_erb: false)
42
43
  @global_timeout = global_timeout
@@ -46,8 +47,8 @@ module Krane
46
47
  # Runs the task, returning a boolean representing success or failure
47
48
  #
48
49
  # @return [Boolean]
49
- def run(*args)
50
- run!(*args)
50
+ def run(**args)
51
+ run!(**args)
51
52
  true
52
53
  rescue FatalDeploymentError
53
54
  false
@@ -133,11 +134,6 @@ module Krane
133
134
  r.validate_definition(@kubectl, selector: @selector)
134
135
  end
135
136
 
136
- resources.select(&:has_warnings?).each do |resource|
137
- record_warnings(logger: logger, warning: resource.validation_warning_msg,
138
- filename: File.basename(resource.file_path))
139
- end
140
-
141
137
  failed_resources = resources.select(&:validation_failed?)
142
138
  if failed_resources.present?
143
139
  failed_resources.each do |r|
@@ -173,6 +169,8 @@ module Krane
173
169
  logger.info(" - #{r.id}")
174
170
  end
175
171
 
172
+ StatsD.client.gauge('discover_resources.count', resources.size, tags: statsd_tags)
173
+
176
174
  resources.sort
177
175
  rescue InvalidTemplateError => e
178
176
  record_invalid_template(logger: logger, err: e.message, filename: e.filename, content: e.content)
@@ -192,10 +190,6 @@ module Krane
192
190
  @kubectl ||= Kubectl.new(task_config: @task_config, log_failure_by_default: true)
193
191
  end
194
192
 
195
- def kubeclient_builder
196
- @kubeclient_builder ||= KubeclientBuilder.new
197
- end
198
-
199
193
  def prune_whitelist
200
194
  cluster_resource_discoverer.prunable_resources(namespaced: false)
201
195
  end
@@ -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,7 +35,8 @@ 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)
38
+ env = { 'KUBECONFIG' => kubeconfig }
39
+ out, err, st = Open3.capture3(env, *cmd)
37
40
 
38
41
  # https://github.com/Shopify/krane/issues/395
39
42
  unless out.valid_encoding?
@@ -61,7 +64,8 @@ module Krane
61
64
  else
62
65
  logger.debug("Kubectl err: #{output_is_sensitive ? '<suppressed sensitive output>' : err}")
63
66
  end
64
- 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 })
65
69
 
66
70
  break unless retriable_err?(err, retry_whitelist) && current_attempt < attempts
67
71
  sleep(retry_delay(current_attempt))
@@ -78,7 +82,7 @@ module Krane
78
82
  def version_info
79
83
  @version_info ||=
80
84
  begin
81
- response, _, status = run("version", use_namespace: false, log_failure: true)
85
+ response, _, status = run("version", use_namespace: false, log_failure: true, attempts: 2)
82
86
  raise KubectlError, "Could not retrieve kubectl version info" unless status.success?
83
87
  extract_version_info_from_kubectl_response(response)
84
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