kubernetes-deploy 0.12.2 → 0.12.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/kubernetes-deploy.gemspec +1 -1
- data/lib/kubernetes-deploy/errors.rb +1 -1
- data/lib/kubernetes-deploy/kubeclient_builder.rb +1 -2
- data/lib/kubernetes-deploy/resource_watcher.rb +34 -13
- data/lib/kubernetes-deploy/restart_task.rb +58 -41
- data/lib/kubernetes-deploy/runner.rb +10 -33
- data/lib/kubernetes-deploy/statsd.rb +4 -0
- data/lib/kubernetes-deploy/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d72e1288dc4248dfcb5a1843a63946c8b9e89c1d
|
4
|
+
data.tar.gz: cd7f50c0d7fc5524ce8c9b83113af0e1d8abb01b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d399ff5fd70beddf51c041ba7f39ba586501c7e9a0728e7fb820523d70b1ebb957cf5df14d3abeba78b197ed1fdcdbf3dfbbf7b489b6164be5725a32237c0ba
|
7
|
+
data.tar.gz: 381a1e2bc1828589974638e1facfb3090f31fc6d3e4be024fb3c85072e3ba6cf2108f72cc26071e6668f6b0952407d64eaf41bddeb2e1e668ffc9a83687ce09f
|
data/kubernetes-deploy.gemspec
CHANGED
@@ -24,7 +24,7 @@ Gem::Specification.new do |spec|
|
|
24
24
|
|
25
25
|
spec.required_ruby_version = '>= 2.3.0'
|
26
26
|
spec.add_dependency "activesupport", ">= 4.2"
|
27
|
-
spec.add_dependency "kubeclient", "~> 2.
|
27
|
+
spec.add_dependency "kubeclient", "~> 2.4"
|
28
28
|
spec.add_dependency "googleauth", ">= 0.5"
|
29
29
|
spec.add_dependency "ejson", "1.0.1"
|
30
30
|
spec.add_dependency "colorize", "~> 0.8"
|
@@ -5,7 +5,7 @@ module KubernetesDeploy
|
|
5
5
|
|
6
6
|
class NamespaceNotFoundError < FatalDeploymentError
|
7
7
|
def initialize(name, context)
|
8
|
-
super("Namespace `#{name}` not found in context `#{context}
|
8
|
+
super("Namespace `#{name}` not found in context `#{context}`")
|
9
9
|
end
|
10
10
|
end
|
11
11
|
end
|
@@ -6,8 +6,7 @@ module KubernetesDeploy
|
|
6
6
|
module KubeclientBuilder
|
7
7
|
class ContextMissingError < FatalDeploymentError
|
8
8
|
def initialize(context_name)
|
9
|
-
super("`#{context_name}` context must be configured in your KUBECONFIG (#{ENV['KUBECONFIG']}).
|
10
|
-
"Please see the README.")
|
9
|
+
super("`#{context_name}` context must be configured in your KUBECONFIG (#{ENV['KUBECONFIG']}).")
|
11
10
|
end
|
12
11
|
end
|
13
12
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module KubernetesDeploy
|
3
3
|
class ResourceWatcher
|
4
|
-
def initialize(resources, logger:, deploy_started_at: Time.now.utc)
|
4
|
+
def initialize(resources, logger:, deploy_started_at: Time.now.utc, operation_name: "deploy")
|
5
5
|
unless resources.is_a?(Enumerable)
|
6
6
|
raise ArgumentError, <<~MSG
|
7
7
|
ResourceWatcher expects Enumerable collection, got `#{resources.class}` instead
|
@@ -10,30 +10,33 @@ module KubernetesDeploy
|
|
10
10
|
@resources = resources
|
11
11
|
@logger = logger
|
12
12
|
@deploy_started_at = deploy_started_at
|
13
|
+
@operation_name = operation_name
|
13
14
|
end
|
14
15
|
|
15
|
-
def run(delay_sync: 3.seconds, reminder_interval: 30.seconds)
|
16
|
+
def run(delay_sync: 3.seconds, reminder_interval: 30.seconds, record_summary: true)
|
16
17
|
delay_sync_until = last_message_logged_at = Time.now.utc
|
18
|
+
remainder = @resources.dup
|
17
19
|
|
18
|
-
while
|
20
|
+
while remainder.present?
|
19
21
|
if Time.now.utc < delay_sync_until
|
20
22
|
sleep(delay_sync_until - Time.now.utc)
|
21
23
|
end
|
22
24
|
delay_sync_until = Time.now.utc + delay_sync # don't pummel the API if the sync is fast
|
23
25
|
|
24
|
-
KubernetesDeploy::Concurrency.split_across_threads(
|
25
|
-
newly_finished_resources,
|
26
|
+
KubernetesDeploy::Concurrency.split_across_threads(remainder, &:sync)
|
27
|
+
newly_finished_resources, remainder = remainder.partition(&:deploy_finished?)
|
26
28
|
|
27
29
|
if newly_finished_resources.present?
|
28
30
|
watch_time = (Time.now.utc - @deploy_started_at).round(1)
|
29
31
|
report_what_just_happened(newly_finished_resources, watch_time)
|
30
|
-
report_what_is_left(reminder: false)
|
32
|
+
report_what_is_left(remainder, reminder: false)
|
31
33
|
last_message_logged_at = Time.now.utc
|
32
34
|
elsif due_for_reminder?(last_message_logged_at, reminder_interval)
|
33
|
-
report_what_is_left(reminder: true)
|
35
|
+
report_what_is_left(remainder, reminder: true)
|
34
36
|
last_message_logged_at = Time.now.utc
|
35
37
|
end
|
36
38
|
end
|
39
|
+
record_statuses_for_summary(@resources) if record_summary
|
37
40
|
end
|
38
41
|
|
39
42
|
private
|
@@ -44,25 +47,43 @@ module KubernetesDeploy
|
|
44
47
|
new_successes, new_failures = resources.partition(&:deploy_succeeded?)
|
45
48
|
new_failures.each do |resource|
|
46
49
|
if resource.deploy_failed?
|
47
|
-
@logger.error("#{resource.id} failed to
|
50
|
+
@logger.error("#{resource.id} failed to #{@operation_name} after #{watch_time}s")
|
48
51
|
else
|
49
|
-
@logger.error("#{resource.id}
|
52
|
+
@logger.error("#{resource.id} rollout timed out")
|
50
53
|
end
|
51
54
|
end
|
52
55
|
|
53
56
|
if new_successes.present?
|
54
|
-
success_string = ColorizedString.new("Successfully
|
57
|
+
success_string = ColorizedString.new("Successfully #{@operation_name}ed in #{watch_time}s:").green
|
55
58
|
@logger.info("#{success_string} #{new_successes.map(&:id).join(', ')}")
|
56
59
|
end
|
57
60
|
end
|
58
61
|
|
59
|
-
def report_what_is_left(reminder:)
|
60
|
-
return unless
|
61
|
-
resource_list =
|
62
|
+
def report_what_is_left(resources, reminder:)
|
63
|
+
return unless resources.present?
|
64
|
+
resource_list = resources.map(&:id).join(', ')
|
62
65
|
msg = reminder ? "Still waiting for: #{resource_list}" : "Continuing to wait for: #{resource_list}"
|
63
66
|
@logger.info(msg)
|
64
67
|
end
|
65
68
|
|
69
|
+
def record_statuses_for_summary(resources)
|
70
|
+
successful_resources, failed_resources = resources.partition(&:deploy_succeeded?)
|
71
|
+
fail_count = failed_resources.length
|
72
|
+
success_count = successful_resources.length
|
73
|
+
|
74
|
+
if success_count > 0
|
75
|
+
@logger.summary.add_action("successfully #{@operation_name}ed #{success_count} "\
|
76
|
+
"#{'resource'.pluralize(success_count)}")
|
77
|
+
final_statuses = successful_resources.map(&:pretty_status).join("\n")
|
78
|
+
@logger.summary.add_paragraph("#{ColorizedString.new('Successful resources').green}\n#{final_statuses}")
|
79
|
+
end
|
80
|
+
|
81
|
+
if fail_count > 0
|
82
|
+
@logger.summary.add_action("failed to #{@operation_name} #{fail_count} #{'resource'.pluralize(fail_count)}")
|
83
|
+
failed_resources.each { |r| @logger.summary.add_paragraph(r.debug_message) }
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
66
87
|
def due_for_reminder?(last_message_logged_at, reminder_interval)
|
67
88
|
(last_message_logged_at.to_f + reminder_interval.to_f) <= Time.now.utc.to_f
|
68
89
|
end
|
@@ -6,13 +6,9 @@ module KubernetesDeploy
|
|
6
6
|
class RestartTask
|
7
7
|
include KubernetesDeploy::KubeclientBuilder
|
8
8
|
|
9
|
-
class
|
10
|
-
def initialize(name, namespace)
|
11
|
-
super("Deployment `#{name}` not found in namespace `#{namespace}`. Aborting the task.")
|
12
|
-
end
|
13
|
-
end
|
9
|
+
class FatalRestartError < FatalDeploymentError; end
|
14
10
|
|
15
|
-
class
|
11
|
+
class RestartAPIError < FatalRestartError
|
16
12
|
def initialize(deployment_name, response)
|
17
13
|
super("Failed to restart #{deployment_name}. " \
|
18
14
|
"API returned non-200 response code (#{response.code})\n" \
|
@@ -27,58 +23,71 @@ module KubernetesDeploy
|
|
27
23
|
@context = context
|
28
24
|
@namespace = namespace
|
29
25
|
@logger = logger
|
30
|
-
@kubeclient = build_v1_kubeclient(context)
|
31
|
-
@v1beta1_kubeclient = build_v1beta1_kubeclient(context)
|
32
|
-
@policy_v1beta1_kubeclient = build_policy_v1beta1_kubeclient(context)
|
33
26
|
end
|
34
27
|
|
35
28
|
def perform(deployments_names = nil)
|
29
|
+
start = Time.now.utc
|
36
30
|
@logger.reset
|
31
|
+
|
32
|
+
@logger.phase_heading("Initializing restart")
|
37
33
|
verify_namespace
|
34
|
+
deployments = identify_target_deployments(deployments_names)
|
35
|
+
|
36
|
+
@logger.phase_heading("Triggering restart by touching ENV[RESTARTED_AT]")
|
37
|
+
patch_kubeclient_deployments(deployments)
|
38
|
+
|
39
|
+
@logger.phase_heading("Waiting for rollout")
|
40
|
+
resources = build_watchables(deployments, start)
|
41
|
+
ResourceWatcher.new(resources, logger: @logger, operation_name: "restart").run
|
42
|
+
success = resources.all?(&:deploy_succeeded?)
|
43
|
+
rescue FatalDeploymentError => error
|
44
|
+
@logger.summary.add_action(error.message)
|
45
|
+
success = false
|
46
|
+
ensure
|
47
|
+
@logger.print_summary(success)
|
48
|
+
status = success ? "success" : "failed"
|
49
|
+
tags = %W(namespace:#{@namespace} context:#{@context} status:#{status} deployments:#{deployments.to_a.length}})
|
50
|
+
::StatsD.measure('restart.duration', StatsD.duration(start), tags: tags)
|
51
|
+
end
|
38
52
|
|
39
|
-
|
40
|
-
|
53
|
+
private
|
54
|
+
|
55
|
+
def identify_target_deployments(deployment_names)
|
56
|
+
if deployment_names.nil?
|
57
|
+
@logger.info("Configured to restart all deployments with the `#{ANNOTATION}` annotation")
|
58
|
+
deployments = v1beta1_kubeclient.get_deployments(namespace: @namespace)
|
59
|
+
.select { |d| d.metadata.annotations[ANNOTATION] }
|
41
60
|
|
42
61
|
if deployments.none?
|
43
|
-
raise
|
62
|
+
raise FatalRestartError, "no deployments with the `#{ANNOTATION}` annotation found in namespace #{@namespace}"
|
44
63
|
end
|
64
|
+
elsif deployment_names.empty?
|
65
|
+
raise FatalRestartError, "Configured to restart deployments by name, but list of names was blank"
|
45
66
|
else
|
46
|
-
|
47
|
-
|
48
|
-
|
67
|
+
deployment_names = deployment_names.uniq
|
68
|
+
list = deployment_names.join(', ')
|
69
|
+
@logger.info("Configured to restart deployments by name: #{list}")
|
49
70
|
|
71
|
+
deployments = fetch_deployments(deployment_names)
|
50
72
|
if deployments.none?
|
51
|
-
raise
|
73
|
+
raise FatalRestartError, "no deployments with names #{list} found in namespace #{@namespace}"
|
52
74
|
end
|
53
75
|
end
|
54
|
-
|
55
|
-
@logger.phase_heading("Triggering restart by touching ENV[RESTARTED_AT]")
|
56
|
-
patch_kubeclient_deployments(deployments)
|
57
|
-
|
58
|
-
@logger.phase_heading("Waiting for rollout")
|
59
|
-
wait_for_rollout(deployments)
|
60
|
-
|
61
|
-
names = deployments.map { |d| "`#{d.metadata.name}`" }
|
62
|
-
@logger.info "Restart of #{names.sort.join(', ')} deployments succeeded"
|
63
|
-
true
|
64
|
-
rescue FatalDeploymentError => error
|
65
|
-
@logger.fatal "#{error.class}: #{error.message}"
|
66
|
-
false
|
76
|
+
deployments
|
67
77
|
end
|
68
78
|
|
69
|
-
|
70
|
-
|
71
|
-
def wait_for_rollout(kubeclient_resources)
|
72
|
-
resources = kubeclient_resources.map do |d|
|
79
|
+
def build_watchables(kubeclient_resources, started)
|
80
|
+
kubeclient_resources.map do |d|
|
73
81
|
definition = d.to_h.deep_stringify_keys
|
74
|
-
Deployment.new(namespace: @namespace, context: @context, definition: definition, logger: @logger)
|
82
|
+
r = Deployment.new(namespace: @namespace, context: @context, definition: definition, logger: @logger)
|
83
|
+
r.deploy_started = started # we don't care what happened to the resource before the restart cmd ran
|
84
|
+
r
|
75
85
|
end
|
76
|
-
watcher = ResourceWatcher.new(resources, logger: @logger)
|
77
|
-
watcher.run
|
78
86
|
end
|
79
87
|
|
80
88
|
def verify_namespace
|
81
|
-
|
89
|
+
kubeclient.get_namespace(@namespace)
|
90
|
+
@logger.info("Namespace #{@namespace} found in context #{@context}")
|
82
91
|
rescue KubeException => error
|
83
92
|
if error.error_code == 404
|
84
93
|
raise NamespaceNotFoundError.new(@namespace, @context)
|
@@ -88,7 +97,7 @@ module KubernetesDeploy
|
|
88
97
|
end
|
89
98
|
|
90
99
|
def patch_deployment_with_restart(record)
|
91
|
-
|
100
|
+
v1beta1_kubeclient.patch_deployment(
|
92
101
|
record.metadata.name,
|
93
102
|
build_patch_payload(record),
|
94
103
|
@namespace
|
@@ -101,7 +110,7 @@ module KubernetesDeploy
|
|
101
110
|
if HTTP_OK_RANGE.cover?(response.code)
|
102
111
|
@logger.info "Triggered `#{record.metadata.name}` restart"
|
103
112
|
else
|
104
|
-
raise
|
113
|
+
raise RestartAPIError.new(record.metadata.name, response)
|
105
114
|
end
|
106
115
|
end
|
107
116
|
end
|
@@ -110,10 +119,10 @@ module KubernetesDeploy
|
|
110
119
|
list.map do |name|
|
111
120
|
record = nil
|
112
121
|
begin
|
113
|
-
record =
|
122
|
+
record = v1beta1_kubeclient.get_deployment(name, @namespace)
|
114
123
|
rescue KubeException => error
|
115
124
|
if error.error_code == 404
|
116
|
-
raise
|
125
|
+
raise FatalRestartError, "Deployment `#{name}` not found in namespace `#{@namespace}`"
|
117
126
|
else
|
118
127
|
raise
|
119
128
|
end
|
@@ -139,5 +148,13 @@ module KubernetesDeploy
|
|
139
148
|
}
|
140
149
|
}
|
141
150
|
end
|
151
|
+
|
152
|
+
def kubeclient
|
153
|
+
@kubeclient ||= build_v1_kubeclient(@context)
|
154
|
+
end
|
155
|
+
|
156
|
+
def v1beta1_kubeclient
|
157
|
+
@v1beta1_kubeclient ||= build_v1beta1_kubeclient(@context)
|
158
|
+
end
|
142
159
|
end
|
143
160
|
end
|
@@ -109,7 +109,7 @@ module KubernetesDeploy
|
|
109
109
|
@logger.phase_heading("Predeploying priority resources")
|
110
110
|
start_priority_resource = Time.now.utc
|
111
111
|
predeploy_priority_resources(resources)
|
112
|
-
::StatsD.measure('priority_resources.duration',
|
112
|
+
::StatsD.measure('priority_resources.duration', StatsD.duration(start_priority_resource), tags: statsd_tags)
|
113
113
|
end
|
114
114
|
|
115
115
|
@logger.phase_heading("Deploying all resources")
|
@@ -120,8 +120,7 @@ module KubernetesDeploy
|
|
120
120
|
if verify_result
|
121
121
|
start_normal_resource = Time.now.utc
|
122
122
|
deploy_resources(resources, prune: prune, verify: true)
|
123
|
-
::StatsD.measure('normal_resources.duration',
|
124
|
-
record_statuses(resources)
|
123
|
+
::StatsD.measure('normal_resources.duration', StatsD.duration(start_normal_resource), tags: statsd_tags)
|
125
124
|
success = resources.all?(&:deploy_succeeded?)
|
126
125
|
else
|
127
126
|
deploy_resources(resources, prune: prune, verify: false)
|
@@ -139,7 +138,7 @@ module KubernetesDeploy
|
|
139
138
|
ensure
|
140
139
|
@logger.print_summary(success)
|
141
140
|
status = success ? "success" : "failed"
|
142
|
-
::StatsD.measure('all_resources.duration',
|
141
|
+
::StatsD.measure('all_resources.duration', StatsD.duration(start), tags: statsd_tags << "status:#{status}")
|
143
142
|
success
|
144
143
|
end
|
145
144
|
|
@@ -152,23 +151,6 @@ module KubernetesDeploy
|
|
152
151
|
|
153
152
|
private
|
154
153
|
|
155
|
-
def record_statuses(resources)
|
156
|
-
successful_resources, failed_resources = resources.partition(&:deploy_succeeded?)
|
157
|
-
fail_count = failed_resources.length
|
158
|
-
success_count = successful_resources.length
|
159
|
-
|
160
|
-
if success_count > 0
|
161
|
-
@logger.summary.add_action("successfully deployed #{success_count} #{'resource'.pluralize(success_count)}")
|
162
|
-
final_statuses = successful_resources.map(&:pretty_status).join("\n")
|
163
|
-
@logger.summary.add_paragraph("#{ColorizedString.new('Successful resources').green}\n#{final_statuses}")
|
164
|
-
end
|
165
|
-
|
166
|
-
if fail_count > 0
|
167
|
-
@logger.summary.add_action("failed to deploy #{fail_count} #{'resource'.pluralize(fail_count)}")
|
168
|
-
failed_resources.each { |r| @logger.summary.add_paragraph(r.debug_message) }
|
169
|
-
end
|
170
|
-
end
|
171
|
-
|
172
154
|
# Inspect the file referenced in the kubectl stderr
|
173
155
|
# to make it easier for developer to understand what's going on
|
174
156
|
def find_bad_files_from_kubectl_output(stderr)
|
@@ -186,7 +168,7 @@ module KubernetesDeploy
|
|
186
168
|
PREDEPLOY_SEQUENCE.each do |resource_type|
|
187
169
|
matching_resources = resource_list.select { |r| r.type == resource_type }
|
188
170
|
next if matching_resources.empty?
|
189
|
-
deploy_resources(matching_resources, verify: true)
|
171
|
+
deploy_resources(matching_resources, verify: true, record_summary: false)
|
190
172
|
|
191
173
|
failed_resources = matching_resources.reject(&:deploy_succeeded?)
|
192
174
|
fail_count = failed_resources.length
|
@@ -264,11 +246,6 @@ module KubernetesDeploy
|
|
264
246
|
" " + str.gsub("\n", "\n ")
|
265
247
|
end
|
266
248
|
|
267
|
-
def wait_for_completion(watched_resources, started_at)
|
268
|
-
watcher = ResourceWatcher.new(watched_resources, logger: @logger, deploy_started_at: started_at)
|
269
|
-
watcher.run
|
270
|
-
end
|
271
|
-
|
272
249
|
def render_template(filename, raw_template)
|
273
250
|
return raw_template unless File.extname(filename) == ".erb"
|
274
251
|
|
@@ -326,7 +303,7 @@ module KubernetesDeploy
|
|
326
303
|
@logger.info("All required parameters and files are present")
|
327
304
|
end
|
328
305
|
|
329
|
-
def deploy_resources(resources, prune: false, verify:)
|
306
|
+
def deploy_resources(resources, prune: false, verify:, record_summary: true)
|
330
307
|
return if resources.empty?
|
331
308
|
deploy_started_at = Time.now.utc
|
332
309
|
|
@@ -365,7 +342,11 @@ module KubernetesDeploy
|
|
365
342
|
end
|
366
343
|
|
367
344
|
apply_all(applyables, prune)
|
368
|
-
|
345
|
+
|
346
|
+
if verify
|
347
|
+
watcher = ResourceWatcher.new(resources, logger: @logger, deploy_started_at: deploy_started_at)
|
348
|
+
watcher.run(record_summary: record_summary)
|
349
|
+
end
|
369
350
|
end
|
370
351
|
|
371
352
|
def apply_all(resources, prune)
|
@@ -429,9 +410,5 @@ module KubernetesDeploy
|
|
429
410
|
def statsd_tags
|
430
411
|
%W(namespace:#{@namespace} sha:#{@current_sha} context:#{@context})
|
431
412
|
end
|
432
|
-
|
433
|
-
def statsd_duration(start_time)
|
434
|
-
(Time.now.utc - start_time).round(1)
|
435
|
-
end
|
436
413
|
end
|
437
414
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kubernetes-deploy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.12.
|
4
|
+
version: 0.12.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Katrina Verey
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2017-09-
|
12
|
+
date: 2017-09-22 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activesupport
|
@@ -31,14 +31,14 @@ dependencies:
|
|
31
31
|
requirements:
|
32
32
|
- - "~>"
|
33
33
|
- !ruby/object:Gem::Version
|
34
|
-
version: '2.
|
34
|
+
version: '2.4'
|
35
35
|
type: :runtime
|
36
36
|
prerelease: false
|
37
37
|
version_requirements: !ruby/object:Gem::Requirement
|
38
38
|
requirements:
|
39
39
|
- - "~>"
|
40
40
|
- !ruby/object:Gem::Version
|
41
|
-
version: '2.
|
41
|
+
version: '2.4'
|
42
42
|
- !ruby/object:Gem::Dependency
|
43
43
|
name: googleauth
|
44
44
|
requirement: !ruby/object:Gem::Requirement
|