kubernetes-deploy 0.25.0 → 0.26.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.buildkite/pipeline.nightly.yml +0 -9
- data/.buildkite/pipeline.yml +0 -9
- data/CHANGELOG.md +23 -0
- data/CONTRIBUTING.md +164 -0
- data/README.md +29 -104
- data/dev.yml +3 -3
- data/exe/kubernetes-deploy +32 -21
- data/exe/kubernetes-render +20 -12
- data/exe/kubernetes-restart +5 -1
- data/lib/kubernetes-deploy.rb +1 -0
- data/lib/kubernetes-deploy/bindings_parser.rb +20 -8
- data/lib/kubernetes-deploy/cluster_resource_discovery.rb +1 -1
- data/lib/kubernetes-deploy/container_logs.rb +6 -0
- data/lib/kubernetes-deploy/deploy_task.rb +85 -44
- data/lib/kubernetes-deploy/ejson_secret_provisioner.rb +38 -84
- data/lib/kubernetes-deploy/errors.rb +8 -0
- data/lib/kubernetes-deploy/kubeclient_builder.rb +52 -20
- data/lib/kubernetes-deploy/kubeclient_builder/kube_config.rb +1 -1
- data/lib/kubernetes-deploy/kubectl.rb +11 -10
- data/lib/kubernetes-deploy/kubernetes_resource.rb +47 -9
- data/lib/kubernetes-deploy/kubernetes_resource/custom_resource.rb +1 -1
- data/lib/kubernetes-deploy/kubernetes_resource/custom_resource_definition.rb +1 -1
- data/lib/kubernetes-deploy/kubernetes_resource/deployment.rb +1 -1
- data/lib/kubernetes-deploy/kubernetes_resource/horizontal_pod_autoscaler.rb +12 -4
- data/lib/kubernetes-deploy/kubernetes_resource/network_policy.rb +22 -0
- data/lib/kubernetes-deploy/kubernetes_resource/pod.rb +2 -0
- data/lib/kubernetes-deploy/kubernetes_resource/secret.rb +23 -0
- data/lib/kubernetes-deploy/label_selector.rb +42 -0
- data/lib/kubernetes-deploy/options_helper.rb +31 -15
- data/lib/kubernetes-deploy/remote_logs.rb +6 -1
- data/lib/kubernetes-deploy/renderer.rb +5 -1
- data/lib/kubernetes-deploy/resource_cache.rb +4 -1
- data/lib/kubernetes-deploy/restart_task.rb +22 -10
- data/lib/kubernetes-deploy/runner_task.rb +5 -3
- data/lib/kubernetes-deploy/version.rb +1 -1
- metadata +6 -2
@@ -29,6 +29,8 @@ module KubernetesDeploy
|
|
29
29
|
MSG
|
30
30
|
|
31
31
|
TIMEOUT_OVERRIDE_ANNOTATION = "kubernetes-deploy.shopify.io/timeout-override"
|
32
|
+
LAST_APPLIED_ANNOTATION = "kubectl.kubernetes.io/last-applied-configuration"
|
33
|
+
KUBECTL_OUTPUT_IS_SENSITIVE = false
|
32
34
|
|
33
35
|
class << self
|
34
36
|
def build(namespace:, context:, definition:, logger:, statsd_tags:, crd: nil)
|
@@ -37,12 +39,8 @@ module KubernetesDeploy
|
|
37
39
|
if definition["kind"].blank?
|
38
40
|
raise InvalidTemplateError.new("Template missing 'Kind'", content: definition.to_yaml)
|
39
41
|
end
|
40
|
-
|
41
|
-
|
42
|
-
klass = KubernetesDeploy.const_get(definition["kind"])
|
43
|
-
return klass.new(**opts)
|
44
|
-
end
|
45
|
-
rescue NameError
|
42
|
+
if (klass = class_for_kind(definition["kind"]))
|
43
|
+
return klass.new(**opts)
|
46
44
|
end
|
47
45
|
if crd
|
48
46
|
CustomResource.new(crd: crd, **opts)
|
@@ -53,6 +51,14 @@ module KubernetesDeploy
|
|
53
51
|
end
|
54
52
|
end
|
55
53
|
|
54
|
+
def class_for_kind(kind)
|
55
|
+
if KubernetesDeploy.const_defined?(kind)
|
56
|
+
KubernetesDeploy.const_get(kind) # rubocop:disable Sorbet/ConstantsFromStrings
|
57
|
+
end
|
58
|
+
rescue NameError
|
59
|
+
nil
|
60
|
+
end
|
61
|
+
|
56
62
|
def timeout
|
57
63
|
self::TIMEOUT
|
58
64
|
end
|
@@ -101,14 +107,21 @@ module KubernetesDeploy
|
|
101
107
|
Kubeclient::Resource.new(@definition)
|
102
108
|
end
|
103
109
|
|
104
|
-
def validate_definition(kubectl)
|
110
|
+
def validate_definition(kubectl, selector: nil)
|
105
111
|
@validation_errors = []
|
112
|
+
validate_selector(selector) if selector
|
106
113
|
validate_timeout_annotation
|
107
114
|
|
108
115
|
command = ["create", "-f", file_path, "--dry-run", "--output=name"]
|
109
|
-
_, err, st = kubectl.run(*command, log_failure: false)
|
116
|
+
_, err, st = kubectl.run(*command, log_failure: false, output_is_sensitive: kubectl_output_is_sensitive?)
|
110
117
|
return true if st.success?
|
111
|
-
|
118
|
+
if kubectl_output_is_sensitive?
|
119
|
+
@validation_errors << <<-EOS
|
120
|
+
Validation for #{id} failed. Detailed information is unavailable as the raw error may contain sensitive data.
|
121
|
+
EOS
|
122
|
+
else
|
123
|
+
@validation_errors << err
|
124
|
+
end
|
112
125
|
false
|
113
126
|
end
|
114
127
|
|
@@ -124,6 +137,10 @@ module KubernetesDeploy
|
|
124
137
|
"#{type}/#{name}"
|
125
138
|
end
|
126
139
|
|
140
|
+
def <=>(other)
|
141
|
+
id <=> other.id
|
142
|
+
end
|
143
|
+
|
127
144
|
def file_path
|
128
145
|
file.path
|
129
146
|
end
|
@@ -305,6 +322,10 @@ module KubernetesDeploy
|
|
305
322
|
end
|
306
323
|
end
|
307
324
|
|
325
|
+
def kubectl_output_is_sensitive?
|
326
|
+
self.class::KUBECTL_OUTPUT_IS_SENSITIVE
|
327
|
+
end
|
328
|
+
|
308
329
|
class Event
|
309
330
|
EVENT_SEPARATOR = "ENDEVENT--BEGINEVENT"
|
310
331
|
FIELD_SEPARATOR = "ENDFIELD--BEGINFIELD"
|
@@ -388,6 +409,23 @@ module KubernetesDeploy
|
|
388
409
|
@definition.dig("metadata", "annotations", TIMEOUT_OVERRIDE_ANNOTATION)
|
389
410
|
end
|
390
411
|
|
412
|
+
def validate_selector(selector)
|
413
|
+
if labels.nil?
|
414
|
+
@validation_errors << "selector #{selector} passed in, but no labels were defined"
|
415
|
+
return
|
416
|
+
end
|
417
|
+
|
418
|
+
unless selector.to_h <= labels
|
419
|
+
label_name = 'label'.pluralize(labels.size)
|
420
|
+
label_string = LabelSelector.new(labels).to_s
|
421
|
+
@validation_errors << "selector #{selector} does not match #{label_name} #{label_string}"
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
def labels
|
426
|
+
@definition.dig("metadata", "labels")
|
427
|
+
end
|
428
|
+
|
391
429
|
def file
|
392
430
|
@file ||= create_definition_tempfile
|
393
431
|
end
|
@@ -92,7 +92,7 @@ module KubernetesDeploy
|
|
92
92
|
progress_condition.present? ? deploy_failing_to_progress? : super
|
93
93
|
end
|
94
94
|
|
95
|
-
def validate_definition(
|
95
|
+
def validate_definition(*)
|
96
96
|
super
|
97
97
|
|
98
98
|
unless REQUIRED_ROLLOUT_TYPES.include?(required_rollout) || percent?(required_rollout)
|
@@ -2,16 +2,17 @@
|
|
2
2
|
module KubernetesDeploy
|
3
3
|
class HorizontalPodAutoscaler < KubernetesResource
|
4
4
|
TIMEOUT = 3.minutes
|
5
|
-
|
5
|
+
RECOVERABLE_CONDITION_PREFIX = "FailedGet"
|
6
6
|
|
7
7
|
def deploy_succeeded?
|
8
|
-
scaling_active_condition["status"] == "True"
|
8
|
+
scaling_active_condition["status"] == "True" || scaling_disabled?
|
9
9
|
end
|
10
10
|
|
11
11
|
def deploy_failed?
|
12
12
|
return false unless exists?
|
13
|
-
|
14
|
-
scaling_active_condition["status"] == "False" &&
|
13
|
+
return false if scaling_disabled?
|
14
|
+
scaling_active_condition["status"] == "False" &&
|
15
|
+
!scaling_active_condition.fetch("reason", "").start_with?(RECOVERABLE_CONDITION_PREFIX)
|
15
16
|
end
|
16
17
|
|
17
18
|
def kubectl_resource_type
|
@@ -21,6 +22,8 @@ module KubernetesDeploy
|
|
21
22
|
def status
|
22
23
|
if !exists?
|
23
24
|
super
|
25
|
+
elsif scaling_disabled?
|
26
|
+
"ScalingDisabled"
|
24
27
|
elsif deploy_succeeded?
|
25
28
|
"Configured"
|
26
29
|
elsif scaling_active_condition.present? || able_to_scale_condition.present?
|
@@ -42,6 +45,11 @@ module KubernetesDeploy
|
|
42
45
|
|
43
46
|
private
|
44
47
|
|
48
|
+
def scaling_disabled?
|
49
|
+
scaling_active_condition["status"] == "False" &&
|
50
|
+
scaling_active_condition["reason"] == "ScalingDisabled"
|
51
|
+
end
|
52
|
+
|
45
53
|
def conditions
|
46
54
|
@instance_data.dig("status", "conditions") || []
|
47
55
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module KubernetesDeploy
|
3
|
+
class NetworkPolicy < KubernetesResource
|
4
|
+
TIMEOUT = 30.seconds
|
5
|
+
|
6
|
+
def status
|
7
|
+
exists? ? "Created" : "Not Found"
|
8
|
+
end
|
9
|
+
|
10
|
+
def deploy_succeeded?
|
11
|
+
exists?
|
12
|
+
end
|
13
|
+
|
14
|
+
def deploy_failed?
|
15
|
+
false
|
16
|
+
end
|
17
|
+
|
18
|
+
def timeout_message
|
19
|
+
UNUSUAL_FAILURE_MESSAGE
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module KubernetesDeploy
|
3
|
+
class Secret < KubernetesResource
|
4
|
+
TIMEOUT = 30.seconds
|
5
|
+
KUBECTL_OUTPUT_IS_SENSITIVE = true
|
6
|
+
|
7
|
+
def status
|
8
|
+
exists? ? "Available" : "Not Found"
|
9
|
+
end
|
10
|
+
|
11
|
+
def deploy_succeeded?
|
12
|
+
exists?
|
13
|
+
end
|
14
|
+
|
15
|
+
def deploy_failed?
|
16
|
+
false
|
17
|
+
end
|
18
|
+
|
19
|
+
def timeout_message
|
20
|
+
UNUSUAL_FAILURE_MESSAGE
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module KubernetesDeploy
|
4
|
+
class LabelSelector
|
5
|
+
def self.parse(string)
|
6
|
+
selector = {}
|
7
|
+
|
8
|
+
string.split(',').each do |kvp|
|
9
|
+
key, value = kvp.split('=', 2)
|
10
|
+
|
11
|
+
if key.blank?
|
12
|
+
raise ArgumentError, "key is blank"
|
13
|
+
end
|
14
|
+
|
15
|
+
if key.end_with?("!")
|
16
|
+
raise ArgumentError, "!= selectors are not supported"
|
17
|
+
end
|
18
|
+
|
19
|
+
if value&.start_with?("=")
|
20
|
+
raise ArgumentError, "== selectors are not supported"
|
21
|
+
end
|
22
|
+
|
23
|
+
selector[key] = value
|
24
|
+
end
|
25
|
+
|
26
|
+
new(selector)
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(hash)
|
30
|
+
@selector = hash
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_h
|
34
|
+
@selector
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_s
|
38
|
+
return "" if @selector.nil?
|
39
|
+
@selector.map { |k, v| "#{k}=#{v}" }.join(",")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -2,25 +2,41 @@
|
|
2
2
|
|
3
3
|
module KubernetesDeploy
|
4
4
|
module OptionsHelper
|
5
|
-
|
6
|
-
if !template_dir && ENV.key?("ENVIRONMENT")
|
7
|
-
template_dir = "config/deploy/#{ENV['ENVIRONMENT']}"
|
8
|
-
end
|
5
|
+
class OptionsError < StandardError; end
|
9
6
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
7
|
+
STDIN_TEMP_FILE = "from_stdin.yml.erb"
|
8
|
+
class << self
|
9
|
+
def with_validated_template_dir(template_dir)
|
10
|
+
if template_dir == '-'
|
11
|
+
Dir.mktmpdir("kubernetes-deploy") do |dir|
|
12
|
+
template_dir_from_stdin(temp_dir: dir)
|
13
|
+
yield dir
|
14
|
+
end
|
15
|
+
else
|
16
|
+
yield default_template_dir(template_dir)
|
17
|
+
end
|
15
18
|
end
|
16
19
|
|
17
|
-
|
18
|
-
|
20
|
+
private
|
21
|
+
|
22
|
+
def default_template_dir(template_dir)
|
23
|
+
if ENV.key?("ENVIRONMENT")
|
24
|
+
template_dir = File.join("config", "deploy", ENV['ENVIRONMENT'])
|
25
|
+
end
|
26
|
+
|
27
|
+
if !template_dir || template_dir.empty?
|
28
|
+
raise OptionsError, "Template directory is unknown. " \
|
29
|
+
"Either specify --template-dir argument or set $ENVIRONMENT to use config/deploy/$ENVIRONMENT " \
|
30
|
+
"as a default path."
|
31
|
+
end
|
32
|
+
|
33
|
+
template_dir
|
34
|
+
end
|
19
35
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
36
|
+
def template_dir_from_stdin(temp_dir:)
|
37
|
+
File.open(File.join(temp_dir, STDIN_TEMP_FILE), 'w+') { |f| f.print($stdin.read) }
|
38
|
+
rescue IOError, Errno::ENOENT => e
|
39
|
+
raise OptionsError, e.message
|
24
40
|
end
|
25
41
|
end
|
26
42
|
end
|
@@ -28,7 +28,12 @@ module KubernetesDeploy
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def print_latest
|
31
|
-
@container_logs.each
|
31
|
+
@container_logs.each do |cl|
|
32
|
+
unless cl.printing_started?
|
33
|
+
@logger.info("Streaming logs from #{@parent_id} container '#{cl.container_name}':")
|
34
|
+
end
|
35
|
+
cl.print_latest(prefix: @container_logs.length > 1)
|
36
|
+
end
|
32
37
|
end
|
33
38
|
|
34
39
|
def print_all(prevent_duplicate: true)
|
@@ -24,7 +24,11 @@ module KubernetesDeploy
|
|
24
24
|
@logger = logger
|
25
25
|
@bindings = bindings
|
26
26
|
# Max length of podname is only 63chars so try to save some room by truncating sha to 8 chars
|
27
|
-
@id =
|
27
|
+
@id = if ENV["TASK_ID"]
|
28
|
+
ENV["TASK_ID"]
|
29
|
+
elsif current_sha
|
30
|
+
current_sha[0...8] + "-#{SecureRandom.hex(4)}"
|
31
|
+
end
|
28
32
|
end
|
29
33
|
|
30
34
|
def render_template(filename, raw_template)
|
@@ -50,7 +50,10 @@ module KubernetesDeploy
|
|
50
50
|
end
|
51
51
|
|
52
52
|
def fetch_by_kind(kind)
|
53
|
-
|
53
|
+
resource_class = KubernetesResource.class_for_kind(kind)
|
54
|
+
output_is_sensitive = resource_class.nil? ? false : resource_class::KUBECTL_OUTPUT_IS_SENSITIVE
|
55
|
+
raw_json, _, st = @kubectl.run("get", kind, "--chunk-size=0", attempts: 5, output: "json",
|
56
|
+
output_is_sensitive: output_is_sensitive)
|
54
57
|
raise KubectlError unless st.success?
|
55
58
|
|
56
59
|
instances = {}
|
@@ -5,8 +5,6 @@ require 'kubernetes-deploy/kubectl'
|
|
5
5
|
|
6
6
|
module KubernetesDeploy
|
7
7
|
class RestartTask
|
8
|
-
include KubernetesDeploy::KubeclientBuilder
|
9
|
-
|
10
8
|
class FatalRestartError < FatalDeploymentError; end
|
11
9
|
|
12
10
|
class RestartAPIError < FatalRestartError
|
@@ -34,13 +32,13 @@ module KubernetesDeploy
|
|
34
32
|
false
|
35
33
|
end
|
36
34
|
|
37
|
-
def perform!(deployments_names = nil)
|
35
|
+
def perform!(deployments_names = nil, selector: nil)
|
38
36
|
start = Time.now.utc
|
39
37
|
@logger.reset
|
40
38
|
|
41
39
|
@logger.phase_heading("Initializing restart")
|
42
40
|
verify_namespace
|
43
|
-
deployments = identify_target_deployments(deployments_names)
|
41
|
+
deployments = identify_target_deployments(deployments_names, selector: selector)
|
44
42
|
if kubectl.server_version < Gem::Version.new(MIN_KUBE_VERSION)
|
45
43
|
@logger.warn(KubernetesDeploy::Errors.server_version_warning(kubectl.server_version))
|
46
44
|
end
|
@@ -76,17 +74,27 @@ module KubernetesDeploy
|
|
76
74
|
%W(namespace:#{@namespace} context:#{@context} status:#{status} deployments:#{deployments.to_a.length}})
|
77
75
|
end
|
78
76
|
|
79
|
-
def identify_target_deployments(deployment_names)
|
77
|
+
def identify_target_deployments(deployment_names, selector: nil)
|
80
78
|
if deployment_names.nil?
|
81
|
-
|
82
|
-
|
83
|
-
.
|
79
|
+
deployments = if selector.nil?
|
80
|
+
@logger.info("Configured to restart all deployments with the `#{ANNOTATION}` annotation")
|
81
|
+
v1beta1_kubeclient.get_deployments(namespace: @namespace)
|
82
|
+
else
|
83
|
+
selector_string = selector.to_s
|
84
|
+
@logger.info(
|
85
|
+
"Configured to restart all deployments with the `#{ANNOTATION}` annotation and #{selector_string} selector"
|
86
|
+
)
|
87
|
+
v1beta1_kubeclient.get_deployments(namespace: @namespace, label_selector: selector_string)
|
88
|
+
end
|
89
|
+
deployments.select! { |d| d.metadata.annotations[ANNOTATION] }
|
84
90
|
|
85
91
|
if deployments.none?
|
86
92
|
raise FatalRestartError, "no deployments with the `#{ANNOTATION}` annotation found in namespace #{@namespace}"
|
87
93
|
end
|
88
94
|
elsif deployment_names.empty?
|
89
95
|
raise FatalRestartError, "Configured to restart deployments by name, but list of names was blank"
|
96
|
+
elsif !selector.nil?
|
97
|
+
raise FatalRestartError, "Can't specify deployment names and selector at the same time"
|
90
98
|
else
|
91
99
|
deployment_names = deployment_names.uniq
|
92
100
|
list = deployment_names.join(', ')
|
@@ -166,7 +174,7 @@ module KubernetesDeploy
|
|
166
174
|
end
|
167
175
|
|
168
176
|
def kubeclient
|
169
|
-
@kubeclient ||= build_v1_kubeclient(@context)
|
177
|
+
@kubeclient ||= kubeclient_builder.build_v1_kubeclient(@context)
|
170
178
|
end
|
171
179
|
|
172
180
|
def kubectl
|
@@ -174,7 +182,11 @@ module KubernetesDeploy
|
|
174
182
|
end
|
175
183
|
|
176
184
|
def v1beta1_kubeclient
|
177
|
-
@v1beta1_kubeclient ||= build_v1beta1_kubeclient(@context)
|
185
|
+
@v1beta1_kubeclient ||= kubeclient_builder.build_v1beta1_kubeclient(@context)
|
186
|
+
end
|
187
|
+
|
188
|
+
def kubeclient_builder
|
189
|
+
@kubeclient_builder ||= KubeclientBuilder.new
|
178
190
|
end
|
179
191
|
end
|
180
192
|
end
|