kubernetes-deploy 0.25.0 → 0.26.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 +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
|