krane 2.0.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 57338bf07bebe1b05ce6701e0c89f2c91db5bd083818b35bd9f307aa669de5ba
4
- data.tar.gz: 94850854c314bb1f05bbc43baa2759dc10e53ee9845c8f3d9b0d33e1a2d7dfd6
3
+ metadata.gz: a3b3d47fb38961012857a5964baadcc19c6e5c147223383430f820af2029cf75
4
+ data.tar.gz: 94addb8d8743d249ec0437467c8eb5ccf075b41536a329e781ef80fad48dbad3
5
5
  SHA512:
6
- metadata.gz: a0414c56878acd433ef3fabf36dd71920ab887adef61ce6b88c3a9dd298fedfb98c8cea39f0a2dfee1d789144ee3b175274e5ecccfd9ff08aad97322cc709463
7
- data.tar.gz: 4f12d3be973fafb33dbbeb5f1f036afc6aa0cd5013b28a063c957befe34a0b65247e1a6b2390de49b52f5f6a9285de2206d5bbda819549a1a82e70e426c61a39
6
+ metadata.gz: 10038a1f57185f4d78fda538aadb71357f1c7d84b77b9a27873062b7bdd82e06e696666ab639384d84e25c010165bdfdd69533860fc50ce564fc419b09fc01a2
7
+ data.tar.gz: a12bf652e4503d37fd368f38d6219e48e1755ea688016094849d6558440b6db88aba22aaadfa807ac6e9f1ff1e3ceba171bfbf1cdcd07fe95bf7e2cc553a0e90
@@ -264,7 +264,7 @@ Style/MethodCallWithArgsParentheses:
264
264
  - raise
265
265
  - puts
266
266
  Exclude:
267
- - Gemfile
267
+ - '**/Gemfile'
268
268
 
269
269
  Style/MethodDefParentheses:
270
270
  EnforcedStyle: require_parentheses
@@ -675,7 +675,7 @@ Style/LineEndConcatenation:
675
675
  Style/MethodCallWithoutArgsParentheses:
676
676
  Enabled: true
677
677
 
678
- Style/MethodMissingSuper:
678
+ Lint/MissingSuper:
679
679
  Enabled: true
680
680
 
681
681
  Style/MissingRespondToMissing:
@@ -965,7 +965,7 @@ Lint/UselessAccessModifier:
965
965
  Lint/UselessAssignment:
966
966
  Enabled: true
967
967
 
968
- Lint/UselessComparison:
968
+ Lint/BinaryOperatorWithIdenticalOperands:
969
969
  Enabled: true
970
970
 
971
971
  Lint/UselessElseWithoutRescue:
@@ -1,5 +1,16 @@
1
1
  ## next
2
2
 
3
+ ## 2.1.0
4
+
5
+ *Features*
6
+ - _(experimental)_ Override deploy method via annotation. This feature is considered alpha and should not be considered stable [#753](https://github.com/Shopify/krane/pull/753)
7
+
8
+ *Enhancements*
9
+ - Increased the number of attempts on kubectl commands during Initializing deploy phase [#749](https://github.com/Shopify/krane/pull/749)
10
+ - Increased attempts on kubectl apply command during deploy [#751](https://github.com/Shopify/krane/pull/751)
11
+ - Whitelist context deadline error during kubctl dry run [#754](https://github.com/Shopify/krane/pull/754)
12
+ - Allow specifying a kubeconfig per task in the internal API [#746](https://github.com/Shopify/krane/pull/746)
13
+
3
14
  ## 2.0.0
4
15
 
5
16
  *Breaking Changes*
data/README.md CHANGED
@@ -159,6 +159,11 @@ before the deployment is considered successful.
159
159
  - _Default_: `true`
160
160
  - `true`: The custom resource will be deployed in the pre-deploy phase.
161
161
  - All other values: The custom resource will be deployed in the main deployment phase.
162
+ - `krane.shopify.io/deploy-method-override`: Cause a resource to be deployed by the specified `kubectl` command, instead of the default `apply`.
163
+ - _Compatibility_: Cannot be used for `PodDisruptionBudget`, since it always uses `create/replace-force`
164
+ - _Accepted values_: `create`, `replace`, and `replace-force`
165
+ - _Warning_: Resources whose deploy method is overridden are no longer subject to pruning on deploy.
166
+ - This feature is _experimental_ and may be removed at any time.
162
167
 
163
168
 
164
169
  ### Running tasks at the beginning of a deploy
data/dev.yml CHANGED
@@ -4,7 +4,8 @@ up:
4
4
  - ruby: 2.5.7 # Matches gemspec
5
5
  - bundler
6
6
  - homebrew:
7
- - Caskroom/cask/minikube
7
+ - homebrew/cask/minikube
8
+ - hyperkit
8
9
  - custom:
9
10
  name: Install the minikube fork of driver-hyperkit
10
11
  met?: command -v docker-machine-driver-hyperkit
@@ -56,6 +56,6 @@ Gem::Specification.new do |spec|
56
56
  spec.add_development_dependency("byebug")
57
57
  spec.add_development_dependency("ruby-prof")
58
58
  spec.add_development_dependency("ruby-prof-flamegraph")
59
- spec.add_development_dependency("rubocop", "~> 0.88.0")
59
+ spec.add_development_dependency("rubocop", "~> 0.89.1")
60
60
  spec.add_development_dependency("codecov")
61
61
  end
@@ -85,6 +85,10 @@ module Krane
85
85
  kubectl.server_version
86
86
  end
87
87
 
88
+ attr_reader :task_config
89
+
90
+ delegate :kubeclient_builder, to: :task_config
91
+
88
92
  # Initializes the deploy task
89
93
  #
90
94
  # @param namespace [String] Kubernetes namespace (*required*)
@@ -101,10 +105,10 @@ module Krane
101
105
  # @param render_erb [Boolean] Enable ERB rendering
102
106
  def initialize(namespace:, context:, current_sha: nil, logger: nil, kubectl_instance: nil, bindings: {},
103
107
  global_timeout: nil, selector: nil, filenames: [], protected_namespaces: nil,
104
- render_erb: false)
108
+ render_erb: false, kubeconfig: nil)
105
109
  @logger = logger || Krane::FormattedLogger.build(namespace, context)
106
110
  @template_sets = TemplateSets.from_dirs_and_files(paths: filenames, logger: @logger, render_erb: render_erb)
107
- @task_config = Krane::TaskConfig.new(context, namespace, @logger)
111
+ @task_config = Krane::TaskConfig.new(context, namespace, @logger, kubeconfig)
108
112
  @bindings = bindings
109
113
  @namespace = namespace
110
114
  @namespace_tags = []
@@ -190,10 +194,6 @@ module Krane
190
194
  selector: @selector, statsd_tags: statsd_tags, current_sha: @current_sha)
191
195
  end
192
196
 
193
- def kubeclient_builder
194
- @kubeclient_builder ||= KubeclientBuilder.new
195
- end
196
-
197
197
  def cluster_resource_discoverer
198
198
  @cluster_resource_discoverer ||= ClusterResourceDiscovery.new(
199
199
  task_config: @task_config,
@@ -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
@@ -189,10 +190,6 @@ module Krane
189
190
  @kubectl ||= Kubectl.new(task_config: @task_config, log_failure_by_default: true)
190
191
  end
191
192
 
192
- def kubeclient_builder
193
- @kubeclient_builder ||= KubeclientBuilder.new
194
- end
195
-
196
193
  def prune_whitelist
197
194
  cluster_resource_discoverer.prunable_resources(namespaced: false)
198
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
@@ -7,6 +7,7 @@ module Krane
7
7
  not_found: /NotFound/,
8
8
  client_timeout: /Client\.Timeout exceeded while awaiting headers/,
9
9
  empty: /\A\z/,
10
+ context_deadline: /context deadline exceeded/,
10
11
  }
11
12
  DEFAULT_TIMEOUT = 15
12
13
  MAX_RETRY_DELAY = 16
@@ -14,7 +15,7 @@ module Krane
14
15
 
15
16
  class ResourceNotFoundError < StandardError; end
16
17
 
17
- delegate :namespace, :context, :logger, to: :@task_config
18
+ delegate :namespace, :context, :logger, :kubeconfig, to: :@task_config
18
19
 
19
20
  def initialize(task_config:, log_failure_by_default:, default_timeout: DEFAULT_TIMEOUT,
20
21
  output_is_sensitive_default: false)
@@ -34,7 +35,8 @@ module Krane
34
35
 
35
36
  (1..attempts).to_a.each do |current_attempt|
36
37
  logger.debug("Running command (attempt #{current_attempt}): #{cmd.join(' ')}")
37
- out, err, st = Open3.capture3(*cmd)
38
+ env = { 'KUBECONFIG' => kubeconfig }
39
+ out, err, st = Open3.capture3(env, *cmd)
38
40
 
39
41
  # https://github.com/Shopify/krane/issues/395
40
42
  unless out.valid_encoding?
@@ -62,7 +64,8 @@ module Krane
62
64
  else
63
65
  logger.debug("Kubectl err: #{output_is_sensitive ? '<suppressed sensitive output>' : err}")
64
66
  end
65
- 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 })
66
69
 
67
70
  break unless retriable_err?(err, retry_whitelist) && current_attempt < attempts
68
71
  sleep(retry_delay(current_attempt))
@@ -79,7 +82,7 @@ module Krane
79
82
  def version_info
80
83
  @version_info ||=
81
84
  begin
82
- response, _, status = run("version", use_namespace: false, log_failure: true)
85
+ response, _, status = run("version", use_namespace: false, log_failure: true, attempts: 2)
83
86
  raise KubectlError, "Could not retrieve kubectl version info" unless status.success?
84
87
  extract_version_info_from_kubectl_response(response)
85
88
  end
@@ -35,6 +35,8 @@ module Krane
35
35
  If you have reason to believe it will succeed, retry the deploy to continue to monitor the rollout.
36
36
  MSG
37
37
 
38
+ ALLOWED_DEPLOY_METHOD_OVERRIDES = %w(create replace replace-force)
39
+ DEPLOY_METHOD_OVERRIDE_ANNOTATION = "deploy-method-override"
38
40
  TIMEOUT_OVERRIDE_ANNOTATION = "timeout-override"
39
41
  LAST_APPLIED_ANNOTATION = "kubectl.kubernetes.io/last-applied-configuration"
40
42
  SENSITIVE_TEMPLATE_CONTENT = false
@@ -136,6 +138,7 @@ module Krane
136
138
  @validation_errors = []
137
139
  validate_selector(selector) if selector
138
140
  validate_timeout_annotation
141
+ validate_deploy_method_override_annotation
139
142
  validate_spec_with_kubectl(kubectl)
140
143
  @validation_errors.present?
141
144
  end
@@ -237,10 +240,14 @@ module Krane
237
240
  if @definition.dig("metadata", "name").blank? && uses_generate_name?
238
241
  :create
239
242
  else
240
- :apply
243
+ deploy_method_override || :apply
241
244
  end
242
245
  end
243
246
 
247
+ def deploy_method_override
248
+ krane_annotation_value(DEPLOY_METHOD_OVERRIDE_ANNOTATION)&.to_sym
249
+ end
250
+
244
251
  def sync_debug_info(kubectl)
245
252
  @debug_events = fetch_events(kubectl) unless ENV[DISABLE_FETCHING_EVENT_INFO]
246
253
  @debug_logs = fetch_debug_logs if print_debug_logs? && !ENV[DISABLE_FETCHING_LOG_INFO]
@@ -504,6 +511,16 @@ module Krane
504
511
  @validation_errors << "#{timeout_annotation_key} annotation is invalid: #{e}"
505
512
  end
506
513
 
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}"
521
+ end
522
+ end
523
+
507
524
  def krane_annotation_value(suffix)
508
525
  @definition.dig("metadata", "annotations", Annotation.for(suffix))
509
526
  end
@@ -545,7 +562,7 @@ module Krane
545
562
  def validate_with_server_side_dry_run(kubectl)
546
563
  command = ["apply", "-f", file_path, "--server-dry-run", "--output=name"]
547
564
  kubectl.run(*command, log_failure: false, output_is_sensitive: sensitive_template_content?,
548
- retry_whitelist: [:client_timeout, :empty], attempts: 3)
565
+ retry_whitelist: [:client_timeout, :empty, :context_deadline], attempts: 3)
549
566
  end
550
567
 
551
568
  # Local dry run is supported on only create and apply
@@ -555,7 +572,7 @@ module Krane
555
572
  verb = deploy_method == :apply ? "apply" : "create"
556
573
  command = [verb, "-f", file_path, "--dry-run", "--output=name"]
557
574
  kubectl.run(*command, log_failure: false, output_is_sensitive: sensitive_template_content?,
558
- retry_whitelist: [:client_timeout, :empty], attempts: 3, use_namespace: !global?)
575
+ retry_whitelist: [:client_timeout, :empty, :context_deadline], attempts: 3, use_namespace: !global?)
559
576
  end
560
577
 
561
578
  def labels
@@ -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
@@ -95,11 +95,10 @@ module Krane
95
95
  applyables, individuals = resources.partition { |r| r.deploy_method == :apply }
96
96
  # Prunable resources should also applied so that they can be pruned
97
97
  pruneable_types = @prune_whitelist.map { |t| t.split("/").last }
98
- applyables += individuals.select { |r| pruneable_types.include?(r.type) }
98
+ applyables += individuals.select { |r| pruneable_types.include?(r.type) && !r.deploy_method_override }
99
99
 
100
100
  individuals.each do |individual_resource|
101
101
  individual_resource.deploy_started_at = Time.now.utc
102
-
103
102
  case individual_resource.deploy_method
104
103
  when :create
105
104
  err, status = create_resource(individual_resource)
@@ -153,7 +152,7 @@ module Krane
153
152
  output_is_sensitive = resources.any?(&:sensitive_template_content?)
154
153
  global_mode = resources.all?(&:global?)
155
154
  out, err, st = kubectl.run(*command, log_failure: false, output_is_sensitive: output_is_sensitive,
156
- use_namespace: !global_mode)
155
+ attempts: 2, use_namespace: !global_mode)
157
156
 
158
157
  if st.success?
159
158
  log_pruning(out) if prune
@@ -22,15 +22,19 @@ module Krane
22
22
  HTTP_OK_RANGE = 200..299
23
23
  ANNOTATION = "shipit.shopify.io/restart"
24
24
 
25
+ attr_reader :task_config
26
+
27
+ delegate :kubeclient_builder, to: :task_config
28
+
25
29
  # Initializes the restart task
26
30
  #
27
31
  # @param context [String] Kubernetes context / cluster (*required*)
28
32
  # @param namespace [String] Kubernetes namespace (*required*)
29
33
  # @param logger [Object] Logger object (defaults to an instance of Krane::FormattedLogger)
30
34
  # @param global_timeout [Integer] Timeout in seconds
31
- def initialize(context:, namespace:, logger: nil, global_timeout: nil)
35
+ def initialize(context:, namespace:, logger: nil, global_timeout: nil, kubeconfig: nil)
32
36
  @logger = logger || Krane::FormattedLogger.build(namespace, context)
33
- @task_config = Krane::TaskConfig.new(context, namespace, @logger)
37
+ @task_config = Krane::TaskConfig.new(context, namespace, @logger, kubeconfig)
34
38
  @context = context
35
39
  @namespace = namespace
36
40
  @global_timeout = global_timeout
@@ -220,9 +224,5 @@ module Krane
220
224
  def v1beta1_kubeclient
221
225
  @v1beta1_kubeclient ||= kubeclient_builder.build_v1beta1_kubeclient(@context)
222
226
  end
223
-
224
- def kubeclient_builder
225
- @kubeclient_builder ||= KubeclientBuilder.new
226
- end
227
227
  end
228
228
  end
@@ -16,7 +16,9 @@ module Krane
16
16
  class RunnerTask
17
17
  class TaskTemplateMissingError < TaskConfigurationError; end
18
18
 
19
- attr_reader :pod_name
19
+ attr_reader :pod_name, :task_config
20
+
21
+ delegate :kubeclient_builder, to: :task_config
20
22
 
21
23
  # Initializes the runner task
22
24
  #
@@ -24,9 +26,9 @@ module Krane
24
26
  # @param context [String] Kubernetes context / cluster (*required*)
25
27
  # @param logger [Object] Logger object (defaults to an instance of Krane::FormattedLogger)
26
28
  # @param global_timeout [Integer] Timeout in seconds
27
- def initialize(namespace:, context:, logger: nil, global_timeout: nil)
29
+ def initialize(namespace:, context:, logger: nil, global_timeout: nil, kubeconfig: nil)
28
30
  @logger = logger || Krane::FormattedLogger.build(namespace, context)
29
- @task_config = Krane::TaskConfig.new(context, namespace, @logger)
31
+ @task_config = Krane::TaskConfig.new(context, namespace, @logger, kubeconfig)
30
32
  @namespace = namespace
31
33
  @context = context
32
34
  @global_timeout = global_timeout
@@ -200,10 +202,6 @@ module Krane
200
202
  @kubeclient ||= kubeclient_builder.build_v1_kubeclient(@context)
201
203
  end
202
204
 
203
- def kubeclient_builder
204
- @kubeclient_builder ||= KubeclientBuilder.new
205
- end
206
-
207
205
  def statsd_tags(status)
208
206
  %W(namespace:#{@namespace} context:#{@context} status:#{status})
209
207
  end
@@ -4,12 +4,13 @@ require 'krane/cluster_resource_discovery'
4
4
 
5
5
  module Krane
6
6
  class TaskConfig
7
- attr_reader :context, :namespace, :logger
7
+ attr_reader :context, :namespace, :logger, :kubeconfig
8
8
 
9
- def initialize(context, namespace, logger = nil)
9
+ def initialize(context, namespace, logger = nil, kubeconfig = nil)
10
10
  @context = context
11
11
  @namespace = namespace
12
12
  @logger = logger || FormattedLogger.build(@namespace, @context)
13
+ @kubeconfig = kubeconfig || ENV['KUBECONFIG']
13
14
  end
14
15
 
15
16
  def global_kinds
@@ -18,5 +19,9 @@ module Krane
18
19
  cluster_resource_discoverer.fetch_resources(namespaced: false).map { |g| g["kind"] }
19
20
  end
20
21
  end
22
+
23
+ def kubeclient_builder
24
+ @kubeclient_builder ||= KubeclientBuilder.new(kubeconfig: kubeconfig)
25
+ end
21
26
  end
22
27
  end
@@ -45,7 +45,7 @@ module Krane
45
45
  end
46
46
 
47
47
  _, err, st = @kubectl.run("config", "get-contexts", context, "-o", "name",
48
- use_namespace: false, use_context: false, log_failure: false)
48
+ use_namespace: false, use_context: false, log_failure: false, attempts: 2)
49
49
 
50
50
  unless st.success?
51
51
  @errors << if err.match("error: context #{context} not found")
@@ -58,7 +58,7 @@ module Krane
58
58
 
59
59
  def validate_context_reachable
60
60
  _, err, st = @kubectl.run("get", "namespaces", "-o", "name",
61
- use_namespace: false, log_failure: false)
61
+ use_namespace: false, log_failure: false, attempts: 2)
62
62
 
63
63
  unless st.success?
64
64
  @errors << "Something went wrong connecting to #{context}. #{err} "
@@ -71,7 +71,7 @@ module Krane
71
71
  end
72
72
 
73
73
  _, err, st = @kubectl.run("get", "namespace", "-o", "name", namespace,
74
- use_namespace: false, log_failure: false)
74
+ use_namespace: false, log_failure: false, attempts: 3)
75
75
 
76
76
  unless st.success?
77
77
  @errors << if err.match("Error from server [(]NotFound[)]: namespace")
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Krane
3
- VERSION = "2.0.0"
3
+ VERSION = "2.1.0"
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: krane
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Katrina Verey
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: exe
12
12
  cert_chain: []
13
- date: 2020-08-26 00:00:00.000000000 Z
13
+ date: 2020-10-06 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activesupport
@@ -366,14 +366,14 @@ dependencies:
366
366
  requirements:
367
367
  - - "~>"
368
368
  - !ruby/object:Gem::Version
369
- version: 0.88.0
369
+ version: 0.89.1
370
370
  type: :development
371
371
  prerelease: false
372
372
  version_requirements: !ruby/object:Gem::Requirement
373
373
  requirements:
374
374
  - - "~>"
375
375
  - !ruby/object:Gem::Version
376
- version: 0.88.0
376
+ version: 0.89.1
377
377
  - !ruby/object:Gem::Dependency
378
378
  name: codecov
379
379
  requirement: !ruby/object:Gem::Requirement