kubernetes-deploy 0.12.2 → 0.12.3
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/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
|