krane 2.1.4 → 2.1.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7ae8bb4b0c7f920136990847601e19c7767075a89ead99781213feaf309d623d
4
- data.tar.gz: 90ad58122ca788ebd4010e62e624c0d73a0c153e48c942673e244e7fbb4ccb32
3
+ metadata.gz: 6126a210b181939121f8639ea63915439fb4188f0a82220b8c6db53cf7aa4980
4
+ data.tar.gz: f1808e32d27cae24775e65fa6fbb0ddb47e99b6b0ca1a62456ec721d386b94d8
5
5
  SHA512:
6
- metadata.gz: 7e09bdb58b4bca4380b77bb10589a2e5b57aa31e9d8838c4b6efc68ccbf58b5af84c360a1a2710564164e4d6f19419cd38d7b3952559c56d5f7dc026d432a194
7
- data.tar.gz: 659649bf0d0cfbe04d3fad0f32f3a05cc6ed9e117ca0755bf808d898b60dbf0d5be88cad609de1589cd93eb2b808e6cc9994a73f7b21267920b679b71afd138e
6
+ metadata.gz: 3fe412af18f0473f2fbb25b03ec34beee430f2d9483884af66e24220b4823059caf0b9b9974ba3bbc62481fe42bea8b86eda4efb341f335e259c631f85dfdbab
7
+ data.tar.gz: 8cb300e21c0608ad62b3caa3f82fc818e6de7cb11a2c8c8c63607b1b92335f6d0c198916f2b2fabd271646958e64db78a67d3251d492607d2d4301b5b0fe3e35
data/.rubocop.yml CHANGED
@@ -1,5 +1,5 @@
1
- inherit_from:
2
- - http://shopify.github.io/ruby-style-guide/rubocop.yml
1
+ inherit_gem:
2
+ rubocop-shopify: rubocop.yml
3
3
 
4
4
  AllCops:
5
5
  TargetRubyVersion: 2.4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  ## next
2
2
 
3
+ ## 2.1.9
4
+
5
+ *Other*
6
+
7
+ - Don't package screenshots in the built gem to reduce size [#817](https://github.com/Shopify/krane/pull/817)
8
+
9
+ ## 2.1.8
10
+
11
+ *Other*
12
+
13
+ - Change `statsd-instrument` dependency constraint to `< 4` [#815](https://github.com/Shopify/krane/pull/815)
14
+
15
+ ## 2.1.7
16
+
17
+ *Enhancements*
18
+ - ENV["KRANE_LOG_LINE_LIMIT"] allows the number of container logs printed for failures to be configurable from the 25 line default [#803](https://github.com/Shopify/krane/pull/803).
19
+
20
+ *Other*
21
+ - Remove the overly tight timeout on cluster resource discovery, which was causing too many timeouts in high latency environments [#813](https://github.com/Shopify/krane/pull/813)
22
+
23
+ ## 2.1.6
24
+
25
+ *Enhancements*
26
+ - Remove the need for a hard coded GVK overide list via improvements to cluster discovery [#778](https://github.com/Shopify/krane/pull/778)
27
+
28
+ *Bug Fixes*
29
+ - Remove resources that are targeted by side-effect-inducing mutating admission webhooks from the serverside dry run batch [#798](https://github.com/Shopify/krane/pull/798)
30
+
31
+ ## 2.1.5
32
+
33
+ - Fix bug where the wrong dry-run flag is used for kubectl if client version is below 1.18 AND server version is 1.18+ [#793](https://github.com/Shopify/krane/pull/793).
34
+
3
35
  ## 2.1.4
4
36
 
5
37
  *Enhancements*
data/dev.yml CHANGED
@@ -4,7 +4,7 @@ up:
4
4
  - ruby: 2.6.6 # Matches gemspec
5
5
  - bundler
6
6
  - homebrew:
7
- - homebrew/cask/minikube
7
+ - minikube
8
8
  - hyperkit
9
9
  - custom:
10
10
  name: Install the minikube fork of driver-hyperkit
data/krane.gemspec CHANGED
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
17
17
  spec.license = "MIT"
18
18
 
19
19
  spec.files = %x(git ls-files -z).split("\x0").reject do |f|
20
- f.match(%r{^(test|spec|features)/})
20
+ f.match(%r{^(test|spec|features|screenshots)/})
21
21
  end
22
22
  spec.bindir = "exe"
23
23
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
@@ -31,7 +31,7 @@ Gem::Specification.new do |spec|
31
31
  spec.add_dependency("googleauth", "~> 0.8")
32
32
  spec.add_dependency("ejson", "~> 1.0")
33
33
  spec.add_dependency("colorize", "~> 0.8")
34
- spec.add_dependency("statsd-instrument", ['>= 2.8', "< 3.1"])
34
+ spec.add_dependency("statsd-instrument", ['>= 2.8', "< 4"])
35
35
  spec.add_dependency("oj", "~> 3.0")
36
36
  spec.add_dependency("concurrent-ruby", "~> 1.1")
37
37
  spec.add_dependency("jsonpath", "~> 0.9.6")
@@ -57,5 +57,6 @@ Gem::Specification.new do |spec|
57
57
  spec.add_development_dependency("ruby-prof")
58
58
  spec.add_development_dependency("ruby-prof-flamegraph")
59
59
  spec.add_development_dependency("rubocop", "~> 0.89.1")
60
+ spec.add_development_dependency("rubocop-shopify", "~> 1.0.5")
60
61
  spec.add_development_dependency("simplecov")
61
62
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require 'concurrent'
2
3
 
3
4
  module Krane
4
5
  class ClusterResourceDiscovery
@@ -7,6 +8,7 @@ module Krane
7
8
  def initialize(task_config:, namespace_tags: [])
8
9
  @task_config = task_config
9
10
  @namespace_tags = namespace_tags
11
+ @api_path_cache = {}
10
12
  end
11
13
 
12
14
  def crds
@@ -18,104 +20,74 @@ module Krane
18
20
 
19
21
  def prunable_resources(namespaced:)
20
22
  black_list = %w(Namespace Node ControllerRevision)
21
- api_versions = fetch_api_versions
22
-
23
- fetch_resources(namespaced: namespaced).uniq { |r| r['kind'] }.map do |resource|
24
- next unless resource['verbs'].one? { |v| v == "delete" }
25
- next if black_list.include?(resource['kind'])
26
- gvk_string(api_versions, resource)
23
+ fetch_resources(namespaced: namespaced).map do |resource|
24
+ next unless resource["verbs"].one? { |v| v == "delete" }
25
+ next if black_list.include?(resource["kind"])
26
+ [resource["apigroup"], resource["version"], resource["kind"]].compact.join("/")
27
27
  end.compact
28
28
  end
29
29
 
30
- # kubectl api-resources -o wide returns 5 columns
31
- # NAME SHORTNAMES APIGROUP NAMESPACED KIND VERBS
32
- # SHORTNAMES and APIGROUP may be blank
33
- # VERBS is an array
34
- # serviceaccounts sa <blank> true ServiceAccount [create delete deletecollection get list patch update watch]
35
30
  def fetch_resources(namespaced: false)
36
- command = %w(api-resources)
37
- command << "--namespaced=#{namespaced}"
38
- raw, err, st = kubectl.run(*command, output: "wide", attempts: 5,
39
- use_namespace: false)
40
- if st.success?
41
- rows = raw.split("\n")
42
- header = rows[0]
43
- resources = rows[1..-1]
44
- full_width_field_names = header.downcase.scan(/[a-z]+[\W]*/)
45
- cursor = 0
46
- fields = full_width_field_names.each_with_object({}) do |name, hash|
47
- start = cursor
48
- cursor = start + name.length
49
- # Last field should consume the remainder of the line
50
- cursor = 0 if full_width_field_names.last == name.strip
51
- hash[name.strip] = [start, cursor - 1]
52
- end
53
- resources.map do |resource|
54
- resource = fields.map { |k, (s, e)| [k.strip, resource[s..e].strip] }.to_h
55
- # Manually parse verbs: "[get list]" into %w(get list)
56
- resource["verbs"] = resource["verbs"][1..-2].split
57
- resource
58
- end
59
- else
60
- raise FatalKubeAPIError, "Error retrieving api-resources: #{err}"
31
+ responses = Concurrent::Hash.new
32
+ Krane::Concurrency.split_across_threads(api_paths) do |path|
33
+ responses[path] = fetch_api_path(path)["resources"] || []
61
34
  end
35
+ responses.flat_map do |path, resources|
36
+ resources.map { |r| resource_hash(path, namespaced, r) }
37
+ end.compact.uniq { |r| r["kind"] }
62
38
  end
63
39
 
64
- private
65
-
66
- # kubectl api-versions returns a list of group/version strings e.g. autoscaling/v2beta2
67
- # A kind may not exist in all versions of the group.
68
- def fetch_api_versions
69
- raw, err, st = kubectl.run("api-versions", attempts: 5, use_namespace: false)
70
- # The "core" group is represented by an empty string
71
- versions = { "" => %w(v1) }
40
+ def fetch_mutating_webhook_configurations
41
+ command = %w(get mutatingwebhookconfigurations)
42
+ raw_json, err, st = kubectl.run(*command, output: "json", attempts: 5, use_namespace: false)
72
43
  if st.success?
73
- rows = raw.split("\n")
74
- rows.each do |group_version|
75
- group, version = group_version.split("/")
76
- versions[group] ||= []
77
- versions[group] << version
44
+ JSON.parse(raw_json)["items"].map do |definition|
45
+ Krane::MutatingWebhookConfiguration.new(namespace: namespace, context: context, logger: logger,
46
+ definition: definition, statsd_tags: @namespace_tags)
78
47
  end
79
48
  else
80
- raise FatalKubeAPIError, "Error retrieving api-versions: #{err}"
49
+ raise FatalKubeAPIError, "Error retrieving mutatingwebhookconfigurations: #{err}"
81
50
  end
82
- versions
83
51
  end
84
52
 
85
- def version_for_kind(versions, kind)
86
- # Override list for kinds that don't appear in the lastest version of a group
87
- version_override = { "CronJob" => "v1beta1", "VolumeAttachment" => "v1beta1",
88
- "CSIDriver" => "v1beta1", "Ingress" => "v1beta1",
89
- "CSINode" => "v1beta1", "Job" => "v1",
90
- "IngressClass" => "v1beta1", "FrontendConfig" => "v1beta1",
91
- "ServiceNetworkEndpointGroup" => "v1beta1",
92
- "EnvoyFilter" => "v1alpha3",
93
- "TCPIngress" => "v1beta1" }
53
+ private
94
54
 
95
- pattern = /v(?<major>\d+)(?<pre>alpha|beta)?(?<minor>\d+)?/
96
- latest = versions.sort_by do |version|
97
- match = version.match(pattern)
98
- pre = { "alpha" => 0, "beta" => 1, nil => 2 }.fetch(match[:pre])
99
- [match[:major].to_i, pre, match[:minor].to_i]
100
- end.last
101
- version_override.fetch(kind, latest)
55
+ def api_paths
56
+ @api_path_cache["/"] ||= begin
57
+ raw_json, err, st = kubectl.run("get", "--raw", "/", attempts: 5, use_namespace: false)
58
+ paths = if st.success?
59
+ JSON.parse(raw_json)["paths"]
60
+ else
61
+ raise FatalKubeAPIError, "Error retrieving raw path /: #{err}"
62
+ end
63
+ paths.select { |path| %r{^\/api.*\/v.*$}.match(path) }
64
+ end
102
65
  end
103
66
 
104
- def gvk_string(api_versions, resource)
105
- apiversion = resource['apiversion'].to_s
106
-
107
- ## In kubectl 1.20 APIGroups was replaced by APIVersions
108
- if apiversion.empty?
109
- apigroup = resource['apigroup'].to_s
110
- group_versions = api_versions[apigroup]
111
-
112
- version = version_for_kind(group_versions, resource['kind'])
113
- apigroup = 'core' if apigroup.empty?
114
- apiversion = "#{apigroup}/#{version}"
67
+ def fetch_api_path(path)
68
+ @api_path_cache[path] ||= begin
69
+ raw_json, err, st = kubectl.run("get", "--raw", path, attempts: 2, use_namespace: false)
70
+ if st.success?
71
+ JSON.parse(raw_json)
72
+ else
73
+ logger.warn("Error retrieving api path: #{err}")
74
+ {}
75
+ end
115
76
  end
77
+ end
116
78
 
117
- apiversion = "core/#{apiversion}" unless apiversion.include?("/")
118
- [apiversion, resource['kind']].compact.join("/")
79
+ def resource_hash(path, namespaced, blob)
80
+ return unless blob["namespaced"] == namespaced
81
+ # skip sub-resources
82
+ return if blob["name"].include?("/")
83
+ path_regex = %r{(/apis?/)(?<group>[^/]*)/?(?<version>v.+)}
84
+ match = path.match(path_regex)
85
+ {
86
+ "verbs" => blob["verbs"],
87
+ "kind" => blob["kind"],
88
+ "apigroup" => match[:group],
89
+ "version" => match[:version],
90
+ }
119
91
  end
120
92
 
121
93
  def fetch_crds
@@ -3,7 +3,7 @@ module Krane
3
3
  class ContainerLogs
4
4
  attr_reader :lines, :container_name
5
5
 
6
- DEFAULT_LINE_LIMIT = 25
6
+ DEFAULT_LINE_LIMIT = Integer(ENV.fetch('KRANE_LOG_LINE_LIMIT', 25))
7
7
 
8
8
  def initialize(parent_id:, container_name:, namespace:, context:, logger:)
9
9
  @parent_id = parent_id
@@ -30,6 +30,7 @@ require 'krane/kubernetes_resource'
30
30
  custom_resource_definition
31
31
  horizontal_pod_autoscaler
32
32
  secret
33
+ mutating_webhook_configuration
33
34
  ).each do |subresource|
34
35
  require "krane/kubernetes_resource/#{subresource}"
35
36
  end
@@ -277,16 +278,25 @@ module Krane
277
278
  end
278
279
  measure_method(:validate_configuration)
279
280
 
281
+ def partition_dry_run_resources(resources)
282
+ individuals = []
283
+ mutating_webhook_configurations = cluster_resource_discoverer.fetch_mutating_webhook_configurations
284
+ mutating_webhook_configurations.each do |mutating_webhook_configuration|
285
+ mutating_webhook_configuration.webhooks.each do |webhook|
286
+ individuals = (individuals + resources.select { |resource| webhook.matches_resource?(resource) }).uniq
287
+ resources -= individuals
288
+ end
289
+ end
290
+ [resources, individuals]
291
+ end
292
+
280
293
  def validate_resources(resources)
281
294
  validate_globals(resources)
282
- batch_dry_run_success = validate_dry_run(resources)
295
+ batchable_resources, individuals = partition_dry_run_resources(resources.dup)
296
+ batch_dry_run_success = kubectl.server_dry_run_enabled? && validate_dry_run(batchable_resources)
297
+ individuals += batchable_resources unless batch_dry_run_success
283
298
  Krane::Concurrency.split_across_threads(resources) do |r|
284
- # No need to pass in kubectl (and do per-resource dry run apply) if batch dry run succeeded
285
- if batch_dry_run_success
286
- r.validate_definition(kubectl: nil, selector: @selector, dry_run: false)
287
- else
288
- r.validate_definition(kubectl: kubectl, selector: @selector, dry_run: true)
289
- end
299
+ r.validate_definition(kubectl: kubectl, selector: @selector, dry_run: individuals.include?(r))
290
300
  end
291
301
  failed_resources = resources.select(&:validation_failed?)
292
302
  if failed_resources.present?
data/lib/krane/kubectl.rb CHANGED
@@ -101,7 +101,7 @@ module Krane
101
101
  end
102
102
 
103
103
  def dry_run_flag
104
- if server_version >= Gem::Version.new("1.18")
104
+ if client_version >= Gem::Version.new("1.18")
105
105
  "--dry-run=server"
106
106
  else
107
107
  "--server-dry-run"
@@ -226,6 +226,11 @@ module Krane
226
226
  version ? grouping : "core"
227
227
  end
228
228
 
229
+ def version
230
+ prefix, version = @definition.dig("apiVersion").split("/")
231
+ version || prefix
232
+ end
233
+
229
234
  def kubectl_resource_type
230
235
  type
231
236
  end
@@ -560,7 +565,7 @@ module Krane
560
565
 
561
566
  # Server side dry run is only supported on apply
562
567
  def validate_with_server_side_dry_run(kubectl)
563
- command = if kubectl.server_version >= Gem::Version.new('1.18')
568
+ command = if kubectl.client_version >= Gem::Version.new('1.18')
564
569
  ["apply", "-f", file_path, "--dry-run=server", "--output=name"]
565
570
  else
566
571
  ["apply", "-f", file_path, "--server-dry-run", "--output=name"]
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Krane
4
+ class MutatingWebhookConfiguration < KubernetesResource
5
+ GLOBAL = true
6
+
7
+ class Webhook
8
+ EQUIVALENT = 'Equivalent'
9
+ EXACT = 'Exact'
10
+
11
+ class Rule
12
+ def initialize(definition)
13
+ @definition = definition
14
+ end
15
+
16
+ def matches_resource?(resource, accept_equivalent:)
17
+ groups.each do |group|
18
+ versions.each do |version|
19
+ resources.each do |kind|
20
+ return true if (resource.group == group || group == '*' || accept_equivalent) &&
21
+ (resource.version == version || version == '*' || accept_equivalent) &&
22
+ (resource.type.downcase == kind.downcase.singularize || kind == "*")
23
+ end
24
+ end
25
+ end
26
+ false
27
+ end
28
+
29
+ def groups
30
+ @definition.dig('apiGroups')
31
+ end
32
+
33
+ def versions
34
+ @definition.dig('apiVersions')
35
+ end
36
+
37
+ def resources
38
+ @definition.dig('resources')
39
+ end
40
+ end
41
+
42
+ def initialize(definition)
43
+ @definition = definition
44
+ end
45
+
46
+ def side_effects
47
+ @definition.dig('sideEffects')
48
+ end
49
+
50
+ def has_side_effects?
51
+ !%w(None NoneOnDryRun).include?(side_effects)
52
+ end
53
+
54
+ def match_policy
55
+ @definition.dig('matchPolicy')
56
+ end
57
+
58
+ def matches_resource?(resource, skip_rule_if_side_effect_none: true)
59
+ return false if skip_rule_if_side_effect_none && !has_side_effects?
60
+ rules.any? do |rule|
61
+ rule.matches_resource?(resource, accept_equivalent: match_policy == EQUIVALENT)
62
+ end
63
+ end
64
+
65
+ def rules
66
+ @definition.fetch('rules', []).map { |rule| Rule.new(rule) }
67
+ end
68
+ end
69
+
70
+ def initialize(namespace:, context:, definition:, logger:, statsd_tags:)
71
+ @webhooks = (definition.dig('webhooks') || []).map { |hook| Webhook.new(hook) }
72
+ super(namespace: namespace, context: context, definition: definition,
73
+ logger: logger, statsd_tags: statsd_tags)
74
+ end
75
+
76
+ TIMEOUT = 30.seconds
77
+
78
+ def deploy_succeeded?
79
+ exists?
80
+ end
81
+
82
+ def webhooks
83
+ @definition.fetch('webhooks', []).map { |webhook| Webhook.new(webhook) }
84
+ end
85
+ end
86
+ end
data/lib/krane/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Krane
3
- VERSION = "2.1.4"
3
+ VERSION = "2.1.9"
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.1.4
4
+ version: 2.1.9
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: 2021-01-20 00:00:00.000000000 Z
13
+ date: 2021-05-28 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activesupport
@@ -91,7 +91,7 @@ dependencies:
91
91
  version: '2.8'
92
92
  - - "<"
93
93
  - !ruby/object:Gem::Version
94
- version: '3.1'
94
+ version: '4'
95
95
  type: :runtime
96
96
  prerelease: false
97
97
  version_requirements: !ruby/object:Gem::Requirement
@@ -101,7 +101,7 @@ dependencies:
101
101
  version: '2.8'
102
102
  - - "<"
103
103
  - !ruby/object:Gem::Version
104
- version: '3.1'
104
+ version: '4'
105
105
  - !ruby/object:Gem::Dependency
106
106
  name: oj
107
107
  requirement: !ruby/object:Gem::Requirement
@@ -374,6 +374,20 @@ dependencies:
374
374
  - - "~>"
375
375
  - !ruby/object:Gem::Version
376
376
  version: 0.89.1
377
+ - !ruby/object:Gem::Dependency
378
+ name: rubocop-shopify
379
+ requirement: !ruby/object:Gem::Requirement
380
+ requirements:
381
+ - - "~>"
382
+ - !ruby/object:Gem::Version
383
+ version: 1.0.5
384
+ type: :development
385
+ prerelease: false
386
+ version_requirements: !ruby/object:Gem::Requirement
387
+ requirements:
388
+ - - "~>"
389
+ - !ruby/object:Gem::Version
390
+ version: 1.0.5
377
391
  - !ruby/object:Gem::Dependency
378
392
  name: simplecov
379
393
  requirement: !ruby/object:Gem::Requirement
@@ -460,6 +474,7 @@ files:
460
474
  - lib/krane/kubernetes_resource/horizontal_pod_autoscaler.rb
461
475
  - lib/krane/kubernetes_resource/ingress.rb
462
476
  - lib/krane/kubernetes_resource/job.rb
477
+ - lib/krane/kubernetes_resource/mutating_webhook_configuration.rb
463
478
  - lib/krane/kubernetes_resource/network_policy.rb
464
479
  - lib/krane/kubernetes_resource/persistent_volume_claim.rb
465
480
  - lib/krane/kubernetes_resource/pod.rb
@@ -493,11 +508,6 @@ files:
493
508
  - lib/krane/task_config_validator.rb
494
509
  - lib/krane/template_sets.rb
495
510
  - lib/krane/version.rb
496
- - screenshots/deploy-demo.gif
497
- - screenshots/migrate-logs.png
498
- - screenshots/missing-secret-fail.png
499
- - screenshots/success.png
500
- - screenshots/test-output.png
501
511
  homepage: https://github.com/Shopify/krane
502
512
  licenses:
503
513
  - MIT
@@ -518,7 +528,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
518
528
  - !ruby/object:Gem::Version
519
529
  version: '0'
520
530
  requirements: []
521
- rubygems_version: 3.0.3
531
+ rubygems_version: 3.2.17
522
532
  signing_key:
523
533
  specification_version: 4
524
534
  summary: A command line tool that helps you ship changes to a Kubernetes namespace
Binary file
Binary file
Binary file
Binary file
Binary file