kubernetes-deploy 0.6.6 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/exe/kubernetes-deploy +21 -13
  3. data/exe/kubernetes-restart +7 -4
  4. data/exe/kubernetes-run +14 -10
  5. data/kubernetes-deploy.gemspec +1 -0
  6. data/lib/kubernetes-deploy.rb +3 -2
  7. data/lib/kubernetes-deploy/deferred_summary_logging.rb +87 -0
  8. data/lib/kubernetes-deploy/ejson_secret_provisioner.rb +18 -20
  9. data/lib/kubernetes-deploy/formatted_logger.rb +42 -0
  10. data/lib/kubernetes-deploy/kubectl.rb +21 -8
  11. data/lib/kubernetes-deploy/kubernetes_resource.rb +111 -52
  12. data/lib/kubernetes-deploy/kubernetes_resource/bugsnag.rb +3 -11
  13. data/lib/kubernetes-deploy/kubernetes_resource/cloudsql.rb +7 -14
  14. data/lib/kubernetes-deploy/kubernetes_resource/config_map.rb +5 -9
  15. data/lib/kubernetes-deploy/kubernetes_resource/deployment.rb +31 -14
  16. data/lib/kubernetes-deploy/kubernetes_resource/ingress.rb +1 -13
  17. data/lib/kubernetes-deploy/kubernetes_resource/persistent_volume_claim.rb +2 -9
  18. data/lib/kubernetes-deploy/kubernetes_resource/pod.rb +48 -22
  19. data/lib/kubernetes-deploy/kubernetes_resource/pod_disruption_budget.rb +5 -9
  20. data/lib/kubernetes-deploy/kubernetes_resource/pod_template.rb +5 -9
  21. data/lib/kubernetes-deploy/kubernetes_resource/redis.rb +9 -15
  22. data/lib/kubernetes-deploy/kubernetes_resource/service.rb +9 -10
  23. data/lib/kubernetes-deploy/resource_watcher.rb +22 -10
  24. data/lib/kubernetes-deploy/restart_task.rb +12 -7
  25. data/lib/kubernetes-deploy/runner.rb +163 -110
  26. data/lib/kubernetes-deploy/runner_task.rb +22 -19
  27. data/lib/kubernetes-deploy/version.rb +1 -1
  28. metadata +18 -4
  29. data/lib/kubernetes-deploy/logger.rb +0 -45
  30. data/lib/kubernetes-deploy/ui_helpers.rb +0 -19
@@ -3,24 +3,16 @@ module KubernetesDeploy
3
3
  class Service < KubernetesResource
4
4
  TIMEOUT = 5.minutes
5
5
 
6
- def initialize(name, namespace, context, file)
7
- @name = name
8
- @namespace = namespace
9
- @context = context
10
- @file = file
11
- end
12
-
13
6
  def sync
14
- _, _err, st = run_kubectl("get", type, @name)
7
+ _, _err, st = kubectl.run("get", type, @name)
15
8
  @found = st.success?
16
9
  if @found
17
- endpoints, _err, st = run_kubectl("get", "endpoints", @name, "--output=jsonpath={.subsets[*].addresses[*].ip}")
10
+ endpoints, _err, st = kubectl.run("get", "endpoints", @name, "--output=jsonpath={.subsets[*].addresses[*].ip}")
18
11
  @num_endpoints = (st.success? ? endpoints.split.length : 0)
19
12
  else
20
13
  @num_endpoints = 0
21
14
  end
22
15
  @status = "#{@num_endpoints} endpoints"
23
- log_status
24
16
  end
25
17
 
26
18
  def deploy_succeeded?
@@ -31,6 +23,13 @@ module KubernetesDeploy
31
23
  false
32
24
  end
33
25
 
26
+ def timeout_message
27
+ <<-MSG.strip_heredoc.strip
28
+ This service does not have any endpoints. If the related pods are failing, fixing them will solve this as well.
29
+ If the related pods are up, this service's selector is probably incorrect.
30
+ MSG
31
+ end
32
+
34
33
  def exists?
35
34
  @found
36
35
  end
@@ -1,37 +1,49 @@
1
1
  # frozen_string_literal: true
2
2
  module KubernetesDeploy
3
3
  class ResourceWatcher
4
- def initialize(resources)
4
+ def initialize(resources, logger:)
5
5
  unless resources.is_a?(Enumerable)
6
6
  raise ArgumentError, <<-MSG.strip
7
7
  ResourceWatcher expects Enumerable collection, got `#{resources.class}` instead
8
8
  MSG
9
9
  end
10
10
  @resources = resources
11
+ @logger = logger
11
12
  end
12
13
 
13
- def run(delay_sync: 3.seconds, logger: KubernetesDeploy.logger)
14
+ def run(delay_sync: 3.seconds)
14
15
  delay_sync_until = Time.now.utc
15
16
  started_at = delay_sync_until
16
- human_resources = @resources.map(&:id).join(", ")
17
- max_wait_time = @resources.map(&:timeout).max
18
- logger.info("Waiting for #{human_resources} with #{max_wait_time}s timeout")
19
17
 
20
18
  while @resources.present?
21
19
  if Time.now.utc < delay_sync_until
22
20
  sleep(delay_sync_until - Time.now.utc)
23
21
  end
22
+ watch_time = (Time.now.utc - started_at).round(1)
24
23
  delay_sync_until = Time.now.utc + delay_sync # don't pummel the API if the sync is fast
25
24
  @resources.each(&:sync)
26
25
  newly_finished_resources, @resources = @resources.partition(&:deploy_finished?)
26
+
27
+ new_success_list = []
27
28
  newly_finished_resources.each do |resource|
28
- next unless resource.deploy_failed? || resource.deploy_timed_out?
29
- logger.error("#{resource.id} failed to deploy with status '#{resource.status}'.")
29
+ if resource.deploy_failed?
30
+ @logger.error("#{resource.id} failed to deploy after #{watch_time}s")
31
+ elsif resource.deploy_timed_out?
32
+ @logger.error("#{resource.id} deployment timed out")
33
+ else
34
+ new_success_list << resource.id
35
+ end
36
+ end
37
+
38
+ unless new_success_list.empty?
39
+ success_string = ColorizedString.new("Successfully deployed in #{watch_time}s:").green
40
+ @logger.info("#{success_string} #{new_success_list.join(', ')}")
30
41
  end
31
- end
32
42
 
33
- watch_time = Time.now.utc - started_at
34
- logger.info("Spent #{watch_time.round(2)}s waiting for #{human_resources}")
43
+ if newly_finished_resources.present? && @resources.present? # something happened this cycle, more to go
44
+ @logger.info("Continuing to wait for: #{@resources.map(&:id).join(', ')}")
45
+ end
46
+ end
35
47
  end
36
48
  end
37
49
  end
@@ -1,11 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
  require 'kubernetes-deploy/kubeclient_builder'
3
- require 'kubernetes-deploy/ui_helpers'
4
3
  require 'kubernetes-deploy/resource_watcher'
5
4
 
6
5
  module KubernetesDeploy
7
6
  class RestartTask
8
- include UIHelpers
9
7
  include KubernetesDeploy::KubeclientBuilder
10
8
 
11
9
  class DeploymentNotFoundError < FatalDeploymentError
@@ -25,7 +23,7 @@ module KubernetesDeploy
25
23
  HTTP_OK_RANGE = 200..299
26
24
  ANNOTATION = "shipit.shopify.io/restart"
27
25
 
28
- def initialize(context:, namespace:, logger: KubernetesDeploy.logger)
26
+ def initialize(context:, namespace:, logger:)
29
27
  @context = context
30
28
  @namespace = namespace
31
29
  @logger = logger
@@ -35,6 +33,7 @@ module KubernetesDeploy
35
33
  end
36
34
 
37
35
  def perform(deployments_names = nil)
36
+ @logger.reset
38
37
  verify_namespace
39
38
 
40
39
  if deployments_names
@@ -53,21 +52,27 @@ module KubernetesDeploy
53
52
  end
54
53
  end
55
54
 
56
- phase_heading("Triggering restart by touching ENV[RESTARTED_AT]")
55
+ @logger.phase_heading("Triggering restart by touching ENV[RESTARTED_AT]")
57
56
  patch_kubeclient_deployments(deployments)
58
57
 
59
- phase_heading("Waiting for rollout")
58
+ @logger.phase_heading("Waiting for rollout")
60
59
  wait_for_rollout(deployments)
61
60
 
62
61
  names = deployments.map { |d| "`#{d.metadata.name}`" }
63
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
64
67
  end
65
68
 
66
69
  private
67
70
 
68
71
  def wait_for_rollout(kubeclient_resources)
69
- resources = kubeclient_resources.map { |d| Deployment.new(d.metadata.name, @namespace, @context, nil) }
70
- watcher = ResourceWatcher.new(resources)
72
+ resources = kubeclient_resources.map do |d|
73
+ Deployment.new(name: d.metadata.name, namespace: @namespace, context: @context, file: nil, logger: @logger)
74
+ end
75
+ watcher = ResourceWatcher.new(resources, logger: @logger)
71
76
  watcher.run
72
77
  end
73
78
 
@@ -22,14 +22,12 @@ require 'kubernetes-deploy/kubernetes_resource'
22
22
  require "kubernetes-deploy/kubernetes_resource/#{subresource}"
23
23
  end
24
24
  require 'kubernetes-deploy/resource_watcher'
25
- require "kubernetes-deploy/ui_helpers"
26
25
  require 'kubernetes-deploy/kubectl'
27
26
  require 'kubernetes-deploy/kubeclient_builder'
28
27
  require 'kubernetes-deploy/ejson_secret_provisioner'
29
28
 
30
29
  module KubernetesDeploy
31
30
  class Runner
32
- include UIHelpers
33
31
  include KubeclientBuilder
34
32
 
35
33
  PREDEPLOY_SEQUENCE = %w(
@@ -68,71 +66,63 @@ module KubernetesDeploy
68
66
  PRUNE_WHITELIST_V_1_5 = %w(extensions/v1beta1/HorizontalPodAutoscaler).freeze
69
67
  PRUNE_WHITELIST_V_1_6 = %w(autoscaling/v1/HorizontalPodAutoscaler).freeze
70
68
 
71
- def self.with_friendly_errors
72
- yield
73
- rescue FatalDeploymentError => error
74
- KubernetesDeploy.logger.fatal <<-MSG
75
- #{error.class}: #{error.message}
76
- #{error.backtrace && error.backtrace.join("\n ")}
77
- MSG
78
- exit 1
79
- end
80
-
81
- def initialize(namespace:, current_sha:, context:, template_dir:,
82
- wait_for_completion:, allow_protected_ns: false, prune: true, bindings: {})
69
+ def initialize(namespace:, context:, current_sha:, template_dir:, logger:, bindings: {})
83
70
  @namespace = namespace
84
71
  @context = context
85
72
  @current_sha = current_sha
86
73
  @template_dir = File.expand_path(template_dir)
74
+ @logger = logger
75
+ @bindings = bindings
87
76
  # Max length of podname is only 63chars so try to save some room by truncating sha to 8 chars
88
77
  @id = current_sha[0...8] + "-#{SecureRandom.hex(4)}" if current_sha
89
- @wait_for_completion = wait_for_completion
90
- @allow_protected_ns = allow_protected_ns
91
- @prune = prune
92
- @bindings = bindings
93
78
  end
94
79
 
95
- def wait_for_completion?
96
- @wait_for_completion
97
- end
80
+ def run(verify_result: true, allow_protected_ns: false, prune: true)
81
+ @logger.reset
98
82
 
99
- def allow_protected_ns?
100
- @allow_protected_ns
101
- end
102
-
103
- def run
104
- phase_heading("Validating configuration")
105
- validate_configuration
106
-
107
- phase_heading("Identifying deployment target")
83
+ @logger.phase_heading("Initializing deploy")
84
+ validate_configuration(allow_protected_ns: allow_protected_ns, prune: prune)
108
85
  confirm_context_exists
109
86
  confirm_namespace_exists
110
-
111
- phase_heading("Parsing deploy content")
112
87
  resources = discover_resources
113
88
 
114
- phase_heading("Checking initial resource statuses")
89
+ @logger.phase_heading("Checking initial resource statuses")
115
90
  resources.each(&:sync)
116
-
117
- ejson = EjsonSecretProvisioner.new(namespace: @namespace, context: @context, template_dir: @template_dir)
91
+ resources.each { |r| @logger.info(r.pretty_status) }
92
+
93
+ ejson = EjsonSecretProvisioner.new(
94
+ namespace: @namespace,
95
+ context: @context,
96
+ template_dir: @template_dir,
97
+ logger: @logger
98
+ )
118
99
  if ejson.secret_changes_required?
119
- phase_heading("Deploying kubernetes secrets from #{EjsonSecretProvisioner::EJSON_SECRETS_FILE}")
100
+ @logger.phase_heading("Deploying kubernetes secrets from #{EjsonSecretProvisioner::EJSON_SECRETS_FILE}")
120
101
  ejson.run
121
102
  end
122
103
 
123
- phase_heading("Predeploying priority resources")
124
- predeploy_priority_resources(resources)
104
+ if deploy_has_priority_resources?(resources)
105
+ @logger.phase_heading("Predeploying priority resources")
106
+ predeploy_priority_resources(resources)
107
+ end
125
108
 
126
- phase_heading("Deploying all resources")
127
- if PROTECTED_NAMESPACES.include?(@namespace) && @prune
109
+ @logger.phase_heading("Deploying all resources")
110
+ if PROTECTED_NAMESPACES.include?(@namespace) && prune
128
111
  raise FatalDeploymentError, "Refusing to deploy to protected namespace '#{@namespace}' with pruning enabled"
129
112
  end
130
113
 
131
- deploy_resources(resources, prune: @prune)
114
+ deploy_resources(resources, prune: prune)
132
115
 
133
- return unless wait_for_completion?
116
+ return true unless verify_result
134
117
  wait_for_completion(resources)
135
- report_final_status(resources)
118
+ record_statuses(resources)
119
+ success = resources.all?(&:deploy_succeeded?)
120
+ rescue FatalDeploymentError => error
121
+ @logger.summary.add_action(error.message)
122
+ success = false
123
+ ensure
124
+ @logger.print_summary(success)
125
+ success
136
126
  end
137
127
 
138
128
  def template_variables
@@ -144,6 +134,23 @@ MSG
144
134
 
145
135
  private
146
136
 
137
+ def record_statuses(resources)
138
+ successful_resources, failed_resources = resources.partition(&:deploy_succeeded?)
139
+ fail_count = failed_resources.length
140
+ success_count = successful_resources.length
141
+
142
+ if success_count > 0
143
+ @logger.summary.add_action("successfully deployed #{success_count} #{'resource'.pluralize(success_count)}")
144
+ final_statuses = successful_resources.map(&:pretty_status).join("\n")
145
+ @logger.summary.add_paragraph("#{ColorizedString.new('Successful resources').green}\n#{final_statuses}")
146
+ end
147
+
148
+ if fail_count > 0
149
+ @logger.summary.add_action("failed to deploy #{fail_count} #{'resource'.pluralize(fail_count)}")
150
+ failed_resources.each { |r| @logger.summary.add_paragraph(r.debug_message) }
151
+ end
152
+ end
153
+
147
154
  def versioned_prune_whitelist
148
155
  if server_major_version == "1.5"
149
156
  BASE_PRUNE_WHITELIST + PRUNE_WHITELIST_V_1_5
@@ -154,7 +161,7 @@ MSG
154
161
 
155
162
  def server_major_version
156
163
  @server_major_version ||= begin
157
- out, _, _ = run_kubectl('version', '--short')
164
+ out, _, _ = kubectl.run('version', '--short')
158
165
  matchdata = /Server Version: v(?<version>\d\.\d)/.match(out)
159
166
  raise "Could not determine server version" unless matchdata[:version]
160
167
  matchdata[:version]
@@ -163,7 +170,7 @@ MSG
163
170
 
164
171
  # Inspect the file referenced in the kubectl stderr
165
172
  # to make it easier for developer to understand what's going on
166
- def inspect_kubectl_out_for_files(stderr)
173
+ def find_bad_file_from_kubectl_output(stderr)
167
174
  # Output example:
168
175
  # Error from server (BadRequest): error when creating "/path/to/configmap-gqq5oh.yml20170411-33615-t0t3m":
169
176
  match = stderr.match(%r{BadRequest.*"(?<path>\/\S+\.yml\S+)"})
@@ -172,12 +179,12 @@ MSG
172
179
  path = match[:path]
173
180
  if path.present? && File.file?(path)
174
181
  suspicious_file = File.read(path)
175
- KubernetesDeploy.logger.warn("Inspecting the file mentioned in the error message (#{path})")
176
- KubernetesDeploy.logger.warn(suspicious_file)
177
- else
178
- KubernetesDeploy.logger.warn("Detected a file (#{path.inspect}) referenced in the kubectl stderr " \
179
- "but was unable to inspect it")
180
182
  end
183
+ [File.basename(path, ".*"), suspicious_file]
184
+ end
185
+
186
+ def deploy_has_priority_resources?(resources)
187
+ resources.any? { |r| PREDEPLOY_SEQUENCE.include?(r.type) }
181
188
  end
182
189
 
183
190
  def predeploy_priority_resources(resource_list)
@@ -186,31 +193,52 @@ MSG
186
193
  next if matching_resources.empty?
187
194
  deploy_resources(matching_resources)
188
195
  wait_for_completion(matching_resources)
189
- fail_list = matching_resources.select { |r| r.deploy_failed? || r.deploy_timed_out? }.map(&:id)
190
- unless fail_list.empty?
191
- raise FatalDeploymentError, "The following priority resources failed to deploy: #{fail_list.join(', ')}"
196
+
197
+ failed_resources = matching_resources.reject(&:deploy_succeeded?)
198
+ fail_count = failed_resources.length
199
+ if fail_count > 0
200
+ failed_resources.each { |r| @logger.summary.add_paragraph(r.debug_message) }
201
+ raise FatalDeploymentError, "Failed to deploy #{fail_count} priority #{'resource'.pluralize(fail_count)}"
192
202
  end
203
+ @logger.blank_line
193
204
  end
194
205
  end
195
206
 
196
207
  def discover_resources
197
208
  resources = []
209
+ @logger.info("Discovering templates:")
198
210
  Dir.foreach(@template_dir) do |filename|
199
211
  next unless filename.end_with?(".yml.erb", ".yml")
200
212
 
201
213
  split_templates(filename) do |tempfile|
202
214
  resource_id = discover_resource_via_dry_run(tempfile)
203
215
  type, name = resource_id.split("/", 2) # e.g. "pod/web-198612918-dzvfb"
204
- resources << KubernetesResource.for_type(type, name, @namespace, @context, tempfile)
205
- KubernetesDeploy.logger.info "Discovered template for #{resource_id}"
216
+ resources << KubernetesResource.for_type(type: type, name: name, namespace: @namespace, context: @context,
217
+ file: tempfile, logger: @logger)
218
+ @logger.info " - #{resource_id}"
206
219
  end
207
220
  end
208
221
  resources
209
222
  end
210
223
 
211
224
  def discover_resource_via_dry_run(tempfile)
212
- resource_id, _err, st = run_kubectl("create", "-f", tempfile.path, "--dry-run", "--output=name")
213
- raise FatalDeploymentError, "Dry run failed for template #{File.basename(tempfile.path)}." unless st.success?
225
+ command = ["create", "-f", tempfile.path, "--dry-run", "--output=name"]
226
+ resource_id, err, st = kubectl.run(*command, log_failure: false)
227
+
228
+ unless st.success?
229
+ debug_msg = <<-DEBUG_MSG.strip_heredoc
230
+ This usually means template '#{File.basename(tempfile.path, '.*')}' is not a valid Kubernetes template.
231
+
232
+ Error from kubectl:
233
+ #{err}
234
+
235
+ Rendered template content:
236
+ DEBUG_MSG
237
+ debug_msg += File.read(tempfile.path)
238
+ @logger.summary.add_paragraph(debug_msg)
239
+
240
+ raise FatalDeploymentError, "Kubectl dry run failed (command: #{Shellwords.join(command)})"
241
+ end
214
242
  resource_id
215
243
  end
216
244
 
@@ -226,21 +254,42 @@ MSG
226
254
  yield f
227
255
  end
228
256
  rescue Psych::SyntaxError => e
229
- KubernetesDeploy.logger.error(rendered_content)
230
- raise FatalDeploymentError, "Template #{filename} cannot be parsed: #{e.message}"
257
+ debug_msg = <<-INFO.strip_heredoc
258
+ Error message: #{e}
259
+
260
+ Template content:
261
+ ---
262
+ INFO
263
+ debug_msg += rendered_content
264
+ @logger.summary.add_paragraph(debug_msg)
265
+ raise FatalDeploymentError, "Template '#{filename}' cannot be parsed"
231
266
  end
232
267
 
233
- def report_final_status(resources)
234
- if resources.all?(&:deploy_succeeded?)
235
- log_green("Deploy succeeded!")
268
+ def record_apply_failure(err)
269
+ file_name, file_content = find_bad_file_from_kubectl_output(err)
270
+ if file_name
271
+ debug_msg = <<-HELPFUL_MESSAGE.strip_heredoc
272
+ This usually means your template named '#{file_name}' is invalid.
273
+
274
+ Error from kubectl:
275
+ #{err}
276
+
277
+ Rendered template content:
278
+ HELPFUL_MESSAGE
279
+ debug_msg += file_content || "Failed to read file"
236
280
  else
237
- fail_list = resources.select { |r| r.deploy_failed? || r.deploy_timed_out? }.map(&:id)
238
- raise FatalDeploymentError, "The following resources failed to deploy: #{fail_list.join(', ')}"
281
+ debug_msg = <<-FALLBACK_MSG
282
+ This usually means one of your templates is invalid, but we were unable to automatically identify which one.
283
+ Please inspect the error message from kubectl:
284
+ #{err}
285
+ FALLBACK_MSG
239
286
  end
287
+
288
+ @logger.summary.add_paragraph(debug_msg)
240
289
  end
241
290
 
242
291
  def wait_for_completion(watched_resources)
243
- watcher = ResourceWatcher.new(watched_resources)
292
+ watcher = ResourceWatcher.new(watched_resources, logger: @logger)
244
293
  watcher.run
245
294
  end
246
295
 
@@ -253,9 +302,12 @@ MSG
253
302
  erb_binding.local_variable_set(var_name, value)
254
303
  end
255
304
  erb_template.result(erb_binding)
305
+ rescue NameError => e
306
+ @logger.summary.add_paragraph("Error from renderer:\n #{e.message.tr("\n", ' ')}")
307
+ raise FatalDeploymentError, "Template '#{filename}' cannot be rendered"
256
308
  end
257
309
 
258
- def validate_configuration
310
+ def validate_configuration(allow_protected_ns:, prune:)
259
311
  errors = []
260
312
  if ENV["KUBECONFIG"].blank? || !File.file?(ENV["KUBECONFIG"])
261
313
  errors << "Kube config not found at #{ENV['KUBECONFIG']}"
@@ -274,15 +326,13 @@ MSG
274
326
  if @namespace.blank?
275
327
  errors << "Namespace must be specified"
276
328
  elsif PROTECTED_NAMESPACES.include?(@namespace)
277
- if allow_protected_ns? && @prune
329
+ if allow_protected_ns && prune
278
330
  errors << "Refusing to deploy to protected namespace '#{@namespace}' with pruning enabled"
279
- elsif allow_protected_ns?
280
- warning = <<-WARNING.strip_heredoc
281
- You're deploying to protected namespace #{@namespace}, which cannot be pruned.
282
- Existing resources can only be removed manually with kubectl. Removing templates from the set deployed will have no effect.
283
- ***Please do not deploy to #{@namespace} unless you really know what you are doing.***
284
- WARNING
285
- KubernetesDeploy.logger.warn(warning)
331
+ elsif allow_protected_ns
332
+ @logger.warn("You're deploying to protected namespace #{@namespace}, which cannot be pruned.")
333
+ @logger.warn("Existing resources can only be removed manually with kubectl. " \
334
+ "Removing templates from the set deployed will have no effect.")
335
+ @logger.warn("***Please do not deploy to #{@namespace} unless you really know what you are doing.***")
286
336
  else
287
337
  errors << "Refusing to deploy to protected namespace '#{@namespace}'"
288
338
  end
@@ -292,38 +342,41 @@ MSG
292
342
  errors << "Context must be specified"
293
343
  end
294
344
 
295
- raise FatalDeploymentError, "Configuration invalid: #{errors.join(', ')}" unless errors.empty?
296
- KubernetesDeploy.logger.info("All required parameters and files are present")
345
+ unless errors.empty?
346
+ @logger.summary.add_paragraph(errors.map { |err| "- #{err}" }.join("\n"))
347
+ raise FatalDeploymentError, "Configuration invalid"
348
+ end
349
+
350
+ @logger.info("All required parameters and files are present")
297
351
  end
298
352
 
299
353
  def deploy_resources(resources, prune: false)
300
- KubernetesDeploy.logger.info("Deploying resources:")
354
+ @logger.info("Deploying resources:")
301
355
 
302
356
  # Apply can be done in one large batch, the rest have to be done individually
303
357
  applyables, individuals = resources.partition { |r| r.deploy_method == :apply }
304
358
 
305
359
  individuals.each do |r|
306
- KubernetesDeploy.logger.info("- #{r.id}")
360
+ @logger.info("- #{r.id}")
307
361
  r.deploy_started = Time.now.utc
308
362
  case r.deploy_method
309
363
  when :replace
310
- _, _, st = run_kubectl("replace", "-f", r.file.path)
364
+ _, _, st = kubectl.run("replace", "-f", r.file.path, log_failure: false)
311
365
  when :replace_force
312
- _, _, st = run_kubectl("replace", "--force", "-f", r.file.path)
366
+ _, _, st = kubectl.run("replace", "--force", "-f", r.file.path, log_failure: false)
313
367
  else
314
368
  # Fail Fast! This is a programmer mistake.
315
369
  raise ArgumentError, "Unexpected deploy method! (#{r.deploy_method.inspect})"
316
370
  end
317
371
 
372
+ next if st.success?
373
+ # it doesn't exist so we can't replace it
374
+ _, err, st = kubectl.run("create", "-f", r.file.path, log_failure: false)
318
375
  unless st.success?
319
- # it doesn't exist so we can't replace it
320
- _, err, st = run_kubectl("create", "-f", r.file.path)
321
- unless st.success?
322
- raise FatalDeploymentError, <<-MSG.strip_heredoc
323
- Failed to replace or create resource: #{r.id}
324
- #{err}
325
- MSG
326
- end
376
+ raise FatalDeploymentError, <<-MSG.strip_heredoc
377
+ Failed to replace or create resource: #{r.id}
378
+ #{err}
379
+ MSG
327
380
  end
328
381
  end
329
382
 
@@ -335,7 +388,7 @@ MSG
335
388
 
336
389
  command = ["apply"]
337
390
  resources.each do |r|
338
- KubernetesDeploy.logger.info("- #{r.id}")
391
+ @logger.info("- #{r.id} (timeout: #{r.timeout}s)")
339
392
  command.push("-f", r.file.path)
340
393
  r.deploy_started = Time.now.utc
341
394
  end
@@ -345,43 +398,43 @@ MSG
345
398
  versioned_prune_whitelist.each { |type| command.push("--prune-whitelist=#{type}") }
346
399
  end
347
400
 
348
- _, err, st = run_kubectl(*command)
349
- unless st.success?
350
- inspect_kubectl_out_for_files(err)
351
- raise FatalDeploymentError, <<-MSG
352
- "The following command failed: #{Shellwords.join(command)}"
353
- #{err}
354
- MSG
401
+ out, err, st = kubectl.run(*command, log_failure: false)
402
+ if st.success?
403
+ log_pruning(out) if prune
404
+ else
405
+ record_apply_failure(err)
406
+ raise FatalDeploymentError, "Command failed: #{Shellwords.join(command)}"
355
407
  end
356
408
  end
357
409
 
410
+ def log_pruning(kubectl_output)
411
+ pruned = kubectl_output.scan(/^(.*) pruned$/)
412
+ return unless pruned.present?
413
+
414
+ @logger.info("The following resources were pruned: #{pruned.join(', ')}")
415
+ @logger.summary.add_action("pruned #{pruned.length} resources")
416
+ end
417
+
358
418
  def confirm_context_exists
359
- out, err, st = run_kubectl("config", "get-contexts", "-o", "name", namespaced: false, with_context: false)
419
+ out, err, st = kubectl.run("config", "get-contexts", "-o", "name",
420
+ use_namespace: false, use_context: false, log_failure: false)
360
421
  available_contexts = out.split("\n")
361
422
  if !st.success?
362
423
  raise FatalDeploymentError, err
363
424
  elsif !available_contexts.include?(@context)
364
425
  raise FatalDeploymentError, "Context #{@context} is not available. Valid contexts: #{available_contexts}"
365
426
  end
366
- KubernetesDeploy.logger.info("Context #{@context} found")
427
+ @logger.info("Context #{@context} found")
367
428
  end
368
429
 
369
430
  def confirm_namespace_exists
370
- _, _, st = run_kubectl("get", "namespace", @namespace, namespaced: false)
431
+ _, _, st = kubectl.run("get", "namespace", @namespace, use_namespace: false, log_failure: false)
371
432
  raise FatalDeploymentError, "Namespace #{@namespace} not found" unless st.success?
372
- KubernetesDeploy.logger.info("Namespace #{@namespace} found")
433
+ @logger.info("Namespace #{@namespace} found")
373
434
  end
374
435
 
375
- def run_kubectl(*args, namespaced: true, with_context: true)
376
- if namespaced
377
- raise KubectlError, "Namespace missing for namespaced command" if @namespace.blank?
378
- end
379
-
380
- if with_context
381
- raise KubectlError, "Explicit context is required to run this command" if @context.blank?
382
- end
383
-
384
- Kubectl.run_kubectl(*args, namespace: @namespace, context: @context)
436
+ def kubectl
437
+ @kubectl ||= Kubectl.new(namespace: @namespace, context: @context, logger: @logger, log_failure_by_default: true)
385
438
  end
386
439
  end
387
440
  end