krane 2.1.5 → 2.1.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/lib/krane/cluster_resource_discovery.rb +53 -81
- data/lib/krane/deploy_task.rb +17 -7
- data/lib/krane/kubernetes_resource.rb +5 -0
- data/lib/krane/kubernetes_resource/mutating_webhook_configuration.rb +86 -0
- data/lib/krane/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d496112ecd7ff05ab81ab6cad622632a7d169b38d1ad0340c5dc2088aa061677
|
4
|
+
data.tar.gz: 16c47fa820e244a7d42080e3fdf81065eda9167dda898e12412ab5c72d5bd4be
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 97eeceba4a6f132ec19bd40bfdcf9fb7ad94f0a791f784d6b3ec0e01eb78eddca2e6d5e6809fc2feb15d6f5a4aba9c7eee295e4660998d595d267700566d7ad5
|
7
|
+
data.tar.gz: 1c364bd503c83eb902e548d68d06aed5235e1ef33c8e72e3fe2fd90871da76d8580be288b822ac768fb75a1dd7ed0c1c801634a0d7068a18d74b8263670b0f7c
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,11 @@
|
|
1
1
|
## next
|
2
2
|
|
3
|
+
*Enhancements*
|
4
|
+
- Remove the need for a hard coded GVK overide list via improvements to cluster discovery [#778](https://github.com/Shopify/krane/pull/778)
|
5
|
+
|
6
|
+
*Bug Fixes*
|
7
|
+
- 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)
|
8
|
+
|
3
9
|
## 2.1.5
|
4
10
|
|
5
11
|
- 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).
|
@@ -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
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
65
|
-
|
66
|
-
|
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
|
-
|
74
|
-
|
75
|
-
|
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
|
49
|
+
raise FatalKubeAPIError, "Error retrieving mutatingwebhookconfigurations: #{err}"
|
81
50
|
end
|
82
|
-
versions
|
83
51
|
end
|
84
52
|
|
85
|
-
|
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
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
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
|
-
|
118
|
-
|
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
|
@@ -129,7 +101,7 @@ module Krane
|
|
129
101
|
end
|
130
102
|
|
131
103
|
def kubectl
|
132
|
-
@kubectl ||= Kubectl.new(task_config: @task_config, log_failure_by_default: true)
|
104
|
+
@kubectl ||= Kubectl.new(task_config: @task_config, log_failure_by_default: true, default_timeout: 1)
|
133
105
|
end
|
134
106
|
end
|
135
107
|
end
|
data/lib/krane/deploy_task.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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?
|
@@ -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
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
|
+
version: 2.1.6
|
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-
|
13
|
+
date: 2021-02-11 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activesupport
|
@@ -460,6 +460,7 @@ files:
|
|
460
460
|
- lib/krane/kubernetes_resource/horizontal_pod_autoscaler.rb
|
461
461
|
- lib/krane/kubernetes_resource/ingress.rb
|
462
462
|
- lib/krane/kubernetes_resource/job.rb
|
463
|
+
- lib/krane/kubernetes_resource/mutating_webhook_configuration.rb
|
463
464
|
- lib/krane/kubernetes_resource/network_policy.rb
|
464
465
|
- lib/krane/kubernetes_resource/persistent_volume_claim.rb
|
465
466
|
- lib/krane/kubernetes_resource/pod.rb
|