krane 2.2.0 → 2.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 46134385171fa351edf5611c3ecd248c299339d2134cf4ff64f6227d68470b96
4
- data.tar.gz: c30ad94176b02c7c402fa50ec5b73aa0c2c289a64401eefcc95494feb64354b3
3
+ metadata.gz: 676fdc875449178de6e0f4ff1f649271bd691637e4e56d540c5e5ea71dc8ded4
4
+ data.tar.gz: 6a16a1e9e32947b0bd6aff7f679ca7f4958a4ef4d3c27e8855fc2a7a6e39abfa
5
5
  SHA512:
6
- metadata.gz: '0792b68c262c3c310b7c51801c206f5422a2b65832eda020a2c0d58c804bb14adc755f9db5ff335b084d982303fbd376b846c5ff6af46d489530ffe6cfe2ae4b'
7
- data.tar.gz: 185a56c603ccbc5dfcdbd7d247d6b7bec512da59a69f2a3e873f41243e79a70f3281596d5346829a4cd26ac1453c8d458cf45f41f498d8e61f1c5cf4e77a13ad
6
+ metadata.gz: 20aad308cbb7c96ad518d0bb57026d6b95e6e8ced1f9eb968c2d144906920a45da272fdf20c94fd8d455a6265b266b6d10912573839e48b08c8162c382905774
7
+ data.tar.gz: 31711f6a966d5364d6e084eb67b8bdcff4e511e40967cc9fb0528a9e8dd1739ed3f59363ab1dd2ba122d40331019eaf2a9ef850830d6c92ddcdfc5a291bd5e89
data/.rubocop.yml CHANGED
@@ -1,5 +1,8 @@
1
1
  inherit_gem:
2
2
  rubocop-shopify: rubocop.yml
3
3
 
4
+ Style/DateTime:
5
+ Enabled: false
6
+
4
7
  AllCops:
5
8
  TargetRubyVersion: 2.4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## next
2
2
 
3
+ ## 2.3.0
4
+
5
+ - Restart tasks now support restarting StatefulSets and DaemonSets, in addition to Deployments [#836](https://github.com/Shopify/krane/pull/836)
6
+
3
7
  ## 2.2.0
4
8
 
5
9
  *Enhancements*
data/README.md CHANGED
@@ -451,7 +451,7 @@ resource to deploy.
451
451
 
452
452
  # krane restart
453
453
 
454
- `krane restart` is a tool for restarting all of the pods in one or more deployments. It triggers the restart by touching the `RESTARTED_AT` environment variable in the deployment's podSpec. The rollout strategy defined for each deployment will be respected by the restart.
454
+ `krane restart` is a tool for restarting all of the pods in one or more deployments, statefuls sets, and/or daemon sets. It triggers the restart by patching template metadata with the `kubectl.kubernetes.io/restartedAt` annotation (with the value being an RFC 3339 representation of the current time). Note this is the manner in which `kubectl rollout restart` itself triggers restarts.
455
455
 
456
456
  ## Usage
457
457
 
@@ -6,7 +6,11 @@ module Krane
6
6
  DEFAULT_RESTART_TIMEOUT = '300s'
7
7
  OPTIONS = {
8
8
  "deployments" => { type: :array, banner: "list of deployments",
9
- desc: "List of workload names to restart" },
9
+ desc: "List of deployment names to restart", default: [] },
10
+ "statefulsets" => { type: :array, banner: "list of statefulsets",
11
+ desc: "List of statefulset names to restart", default: [] },
12
+ "daemonsets" => { type: :array, banner: "list of daemonsets",
13
+ desc: "List of daemonset names to restart", default: [] },
10
14
  "global-timeout" => { type: :string, banner: "duration", default: DEFAULT_RESTART_TIMEOUT,
11
15
  desc: "Max duration to monitor workloads correctly restarted" },
12
16
  "selector" => { type: :string, banner: "'label=value'",
@@ -25,6 +29,8 @@ module Krane
25
29
  )
26
30
  restart.run!(
27
31
  deployments: options[:deployments],
32
+ statefulsets: options[:statefulsets],
33
+ daemonsets: options[:daemonsets],
28
34
  selector: selector,
29
35
  verify_result: options["verify-result"]
30
36
  )
@@ -22,6 +22,8 @@ module Krane
22
22
  HTTP_OK_RANGE = 200..299
23
23
  ANNOTATION = "shipit.shopify.io/restart"
24
24
 
25
+ RESTART_TRIGGER_ANNOTATION = "kubectl.kubernetes.io/restartedAt"
26
+
25
27
  attr_reader :task_config
26
28
 
27
29
  delegate :kubeclient_builder, to: :task_config
@@ -58,33 +60,41 @@ module Krane
58
60
  # @param verify_result [Boolean] Wait for completion and verify success
59
61
  #
60
62
  # @return [nil]
61
- def run!(deployments: nil, selector: nil, verify_result: true)
63
+ def run!(deployments: [], statefulsets: [], daemonsets: [], selector: nil, verify_result: true)
62
64
  start = Time.now.utc
63
65
  @logger.reset
64
66
 
65
67
  @logger.phase_heading("Initializing restart")
66
68
  verify_config!
67
- deployments = identify_target_deployments(deployments, selector: selector)
69
+ deployments, statefulsets, daemonsets = identify_target_workloads(deployments, statefulsets,
70
+ daemonsets, selector: selector)
68
71
 
69
- @logger.phase_heading("Triggering restart by touching ENV[RESTARTED_AT]")
72
+ @logger.phase_heading("Triggering restart by annotating pod template #{RESTART_TRIGGER_ANNOTATION} annotation")
70
73
  patch_kubeclient_deployments(deployments)
74
+ patch_kubeclient_statefulsets(statefulsets)
75
+ patch_kubeclient_daemonsets(daemonsets)
71
76
 
72
77
  if verify_result
73
78
  @logger.phase_heading("Waiting for rollout")
74
- resources = build_watchables(deployments, start)
79
+ resources = build_watchables(deployments, start, Deployment)
80
+ resources += build_watchables(statefulsets, start, StatefulSet)
81
+ resources += build_watchables(daemonsets, start, DaemonSet)
75
82
  verify_restart(resources)
76
83
  else
77
84
  warning = "Result verification is disabled for this task"
78
85
  @logger.summary.add_paragraph(ColorizedString.new(warning).yellow)
79
86
  end
80
- StatsD.client.distribution('restart.duration', StatsD.duration(start), tags: tags('success', deployments))
87
+ StatsD.client.distribution('restart.duration', StatsD.duration(start),
88
+ tags: tags('success', deployments, statefulsets, daemonsets))
81
89
  @logger.print_summary(:success)
82
90
  rescue DeploymentTimeoutError
83
- StatsD.client.distribution('restart.duration', StatsD.duration(start), tags: tags('timeout', deployments))
91
+ StatsD.client.distribution('restart.duration', StatsD.duration(start),
92
+ tags: tags('timeout', deployments, statefulsets, daemonsets))
84
93
  @logger.print_summary(:timed_out)
85
94
  raise
86
95
  rescue FatalDeploymentError => error
87
- StatsD.client.distribution('restart.duration', StatsD.duration(start), tags: tags('failure', deployments))
96
+ StatsD.client.distribution('restart.duration', StatsD.duration(start),
97
+ tags: tags('failure', deployments, statefulsets, daemonsets))
88
98
  @logger.summary.add_action(error.message) if error.message != error.class.to_s
89
99
  @logger.print_summary(:failure)
90
100
  raise
@@ -93,66 +103,140 @@ module Krane
93
103
 
94
104
  private
95
105
 
96
- def tags(status, deployments)
97
- %W(namespace:#{@namespace} context:#{@context} status:#{status} deployments:#{deployments.to_a.length}})
106
+ def tags(status, deployments, statefulsets, daemonsets)
107
+ %W(namespace:#{@namespace} context:#{@context} status:#{status} deployments:#{deployments.to_a.length}
108
+ statefulsets:#{statefulsets.to_a.length} daemonsets:#{daemonsets.to_a.length}})
98
109
  end
99
110
 
100
- def identify_target_deployments(deployment_names, selector: nil)
101
- if deployment_names.nil?
102
- deployments = if selector.nil?
103
- @logger.info("Configured to restart all deployments with the `#{ANNOTATION}` annotation")
104
- apps_v1_kubeclient.get_deployments(namespace: @namespace)
111
+ def identify_target_workloads(deployment_names, statefulset_names, daemonset_names, selector: nil)
112
+ if deployment_names.blank? && statefulset_names.blank? && daemonset_names.blank?
113
+ if selector.nil?
114
+ @logger.info("Configured to restart all workloads with the `#{ANNOTATION}` annotation")
105
115
  else
106
- selector_string = selector.to_s
107
116
  @logger.info(
108
- "Configured to restart all deployments with the `#{ANNOTATION}` annotation and #{selector_string} selector"
117
+ "Configured to restart all workloads with the `#{ANNOTATION}` annotation and #{selector} selector"
109
118
  )
110
- apps_v1_kubeclient.get_deployments(namespace: @namespace, label_selector: selector_string)
111
119
  end
112
- deployments.select! { |d| d.metadata.annotations[ANNOTATION] }
120
+ deployments = identify_target_deployments(selector: selector)
121
+ statefulsets = identify_target_statefulsets(selector: selector)
122
+ daemonsets = identify_target_daemonsets(selector: selector)
113
123
 
114
- if deployments.none?
115
- raise FatalRestartError, "no deployments with the `#{ANNOTATION}` annotation found in namespace #{@namespace}"
124
+ if deployments.none? && statefulsets.none? && daemonsets.none?
125
+ raise FatalRestartError, "no deployments, statefulsets, or daemonsets, with the `#{ANNOTATION}` " \
126
+ "annotation found in namespace #{@namespace}"
116
127
  end
117
- elsif deployment_names.empty?
118
- raise FatalRestartError, "Configured to restart deployments by name, but list of names was blank"
119
128
  elsif !selector.nil?
120
- raise FatalRestartError, "Can't specify deployment names and selector at the same time"
129
+ raise FatalRestartError, "Can't specify workload names and selector at the same time"
121
130
  else
122
- deployment_names = deployment_names.uniq
123
- list = deployment_names.join(', ')
124
- @logger.info("Configured to restart deployments by name: #{list}")
125
-
126
- deployments = fetch_deployments(deployment_names)
127
- if deployments.none?
128
- raise FatalRestartError, "no deployments with names #{list} found in namespace #{@namespace}"
131
+ deployments, statefulsets, daemonsets = identify_target_workloads_by_name(deployment_names,
132
+ statefulset_names, daemonset_names)
133
+ if deployments.none? && statefulsets.none? && daemonsets.none?
134
+ error_msgs = []
135
+ error_msgs << "no deployments with names #{list} found in namespace #{@namespace}" if deployment_names
136
+ error_msgs << "no statefulsets with names #{list} found in namespace #{@namespace}" if statefulset_names
137
+ error_msgs << "no daemonsets with names #{list} found in namespace #{@namespace}" if daemonset_names
138
+ raise FatalRestartError, error_msgs.join(', ')
129
139
  end
130
140
  end
131
- deployments
141
+ [deployments, statefulsets, daemonsets]
132
142
  end
133
143
 
134
- def build_watchables(kubeclient_resources, started)
144
+ def identify_target_workloads_by_name(deployment_names, statefulset_names, daemonset_names)
145
+ deployment_names = deployment_names.uniq
146
+ statefulset_names = statefulset_names.uniq
147
+ daemonset_names = daemonset_names.uniq
148
+
149
+ if deployment_names.present?
150
+ @logger.info("Configured to restart deployments by name: #{deployment_names.join(', ')}")
151
+ end
152
+ if statefulset_names.present?
153
+ @logger.info("Configured to restart statefulsets by name: #{statefulset_names.join(', ')}")
154
+ end
155
+ if daemonset_names.present?
156
+ @logger.info("Configured to restart daemonsets by name: #{daemonset_names.join(', ')}")
157
+ end
158
+
159
+ [fetch_deployments(deployment_names), fetch_statefulsets(statefulset_names), fetch_daemonsets(daemonset_names)]
160
+ end
161
+
162
+ def identify_target_deployments(selector: nil)
163
+ deployments = if selector.nil?
164
+ apps_v1_kubeclient.get_deployments(namespace: @namespace)
165
+ else
166
+ selector_string = selector.to_s
167
+ apps_v1_kubeclient.get_deployments(namespace: @namespace, label_selector: selector_string)
168
+ end
169
+ deployments.select { |d| d.metadata.annotations[ANNOTATION] }
170
+ end
171
+
172
+ def identify_target_statefulsets(selector: nil)
173
+ statefulsets = if selector.nil?
174
+ apps_v1_kubeclient.get_stateful_sets(namespace: @namespace)
175
+ else
176
+ selector_string = selector.to_s
177
+ apps_v1_kubeclient.get_stateful_sets(namespace: @namespace, label_selector: selector_string)
178
+ end
179
+ statefulsets.select { |d| d.metadata.annotations[ANNOTATION] }
180
+ end
181
+
182
+ def identify_target_daemonsets(selector: nil)
183
+ daemonsets = if selector.nil?
184
+ apps_v1_kubeclient.get_daemon_sets(namespace: @namespace)
185
+ else
186
+ selector_string = selector.to_s
187
+ apps_v1_kubeclient.get_daemon_sets(namespace: @namespace, label_selector: selector_string)
188
+ end
189
+ daemonsets.select { |d| d.metadata.annotations[ANNOTATION] }
190
+ end
191
+
192
+ def build_watchables(kubeclient_resources, started, klass)
135
193
  kubeclient_resources.map do |d|
136
194
  definition = d.to_h.deep_stringify_keys
137
- r = Deployment.new(namespace: @namespace, context: @context, definition: definition, logger: @logger)
195
+ r = klass.new(namespace: @namespace, context: @context, definition: definition, logger: @logger)
138
196
  r.deploy_started_at = started # we don't care what happened to the resource before the restart cmd ran
139
197
  r
140
198
  end
141
199
  end
142
200
 
143
201
  def patch_deployment_with_restart(record)
144
- apps_v1_kubeclient.patch_deployment(
145
- record.metadata.name,
146
- build_patch_payload(record),
147
- @namespace
148
- )
202
+ apps_v1_kubeclient.patch_deployment(record.metadata.name, build_patch_payload(record), @namespace)
203
+ end
204
+
205
+ def patch_statefulset_with_restart(record)
206
+ apps_v1_kubeclient.patch_stateful_set(record.metadata.name, build_patch_payload(record), @namespace)
207
+ end
208
+
209
+ def patch_daemonset_with_restart(record)
210
+ apps_v1_kubeclient.patch_daemon_set(record.metadata.name, build_patch_payload(record), @namespace)
149
211
  end
150
212
 
151
213
  def patch_kubeclient_deployments(deployments)
152
214
  deployments.each do |record|
153
215
  begin
154
216
  patch_deployment_with_restart(record)
155
- @logger.info("Triggered `#{record.metadata.name}` restart")
217
+ @logger.info("Triggered `Deployment/#{record.metadata.name}` restart")
218
+ rescue Kubeclient::HttpError => e
219
+ raise RestartAPIError.new(record.metadata.name, e.message)
220
+ end
221
+ end
222
+ end
223
+
224
+ def patch_kubeclient_statefulsets(statefulsets)
225
+ statefulsets.each do |record|
226
+ begin
227
+ patch_statefulset_with_restart(record)
228
+ @logger.info("Triggered `StatefulSet/#{record.metadata.name}` restart")
229
+ rescue Kubeclient::HttpError => e
230
+ raise RestartAPIError.new(record.metadata.name, e.message)
231
+ end
232
+ end
233
+ end
234
+
235
+ def patch_kubeclient_daemonsets(daemonsets)
236
+ daemonsets.each do |record|
237
+ begin
238
+ patch_daemonset_with_restart(record)
239
+ @logger.info("Triggered `DaemonSet/#{record.metadata.name}` restart")
156
240
  rescue Kubeclient::HttpError => e
157
241
  raise RestartAPIError.new(record.metadata.name, e.message)
158
242
  end
@@ -171,18 +255,38 @@ module Krane
171
255
  end
172
256
  end
173
257
 
174
- def build_patch_payload(deployment)
175
- containers = deployment.spec.template.spec.containers
258
+ def fetch_statefulsets(list)
259
+ list.map do |name|
260
+ record = nil
261
+ begin
262
+ record = apps_v1_kubeclient.get_stateful_set(name, @namespace)
263
+ rescue Kubeclient::ResourceNotFoundError
264
+ raise FatalRestartError, "StatefulSet `#{name}` not found in namespace `#{@namespace}`"
265
+ end
266
+ record
267
+ end
268
+ end
269
+
270
+ def fetch_daemonsets(list)
271
+ list.map do |name|
272
+ record = nil
273
+ begin
274
+ record = apps_v1_kubeclient.get_daemon_set(name, @namespace)
275
+ rescue Kubeclient::ResourceNotFoundError
276
+ raise FatalRestartError, "DaemonSet `#{name}` not found in namespace `#{@namespace}`"
277
+ end
278
+ record
279
+ end
280
+ end
281
+
282
+ def build_patch_payload(_deployment)
176
283
  {
177
284
  spec: {
178
285
  template: {
179
- spec: {
180
- containers: containers.map do |container|
181
- {
182
- name: container.name,
183
- env: [{ name: "RESTARTED_AT", value: Time.now.to_i.to_s }],
184
- }
185
- end,
286
+ metadata: {
287
+ annotations: {
288
+ RESTART_TRIGGER_ANNOTATION => Time.now.utc.to_datetime.rfc3339,
289
+ },
186
290
  },
187
291
  },
188
292
  },
data/lib/krane/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Krane
3
- VERSION = "2.2.0"
3
+ VERSION = "2.3.0"
4
4
  end
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.2.0
4
+ version: 2.3.0
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-06-09 00:00:00.000000000 Z
13
+ date: 2021-10-01 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activesupport
@@ -528,7 +528,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
528
528
  - !ruby/object:Gem::Version
529
529
  version: '0'
530
530
  requirements: []
531
- rubygems_version: 3.2.17
531
+ rubygems_version: 3.2.20
532
532
  signing_key:
533
533
  specification_version: 4
534
534
  summary: A command line tool that helps you ship changes to a Kubernetes namespace