kubernetes-deploy 0.25.0 → 0.26.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.buildkite/pipeline.nightly.yml +0 -9
  3. data/.buildkite/pipeline.yml +0 -9
  4. data/CHANGELOG.md +23 -0
  5. data/CONTRIBUTING.md +164 -0
  6. data/README.md +29 -104
  7. data/dev.yml +3 -3
  8. data/exe/kubernetes-deploy +32 -21
  9. data/exe/kubernetes-render +20 -12
  10. data/exe/kubernetes-restart +5 -1
  11. data/lib/kubernetes-deploy.rb +1 -0
  12. data/lib/kubernetes-deploy/bindings_parser.rb +20 -8
  13. data/lib/kubernetes-deploy/cluster_resource_discovery.rb +1 -1
  14. data/lib/kubernetes-deploy/container_logs.rb +6 -0
  15. data/lib/kubernetes-deploy/deploy_task.rb +85 -44
  16. data/lib/kubernetes-deploy/ejson_secret_provisioner.rb +38 -84
  17. data/lib/kubernetes-deploy/errors.rb +8 -0
  18. data/lib/kubernetes-deploy/kubeclient_builder.rb +52 -20
  19. data/lib/kubernetes-deploy/kubeclient_builder/kube_config.rb +1 -1
  20. data/lib/kubernetes-deploy/kubectl.rb +11 -10
  21. data/lib/kubernetes-deploy/kubernetes_resource.rb +47 -9
  22. data/lib/kubernetes-deploy/kubernetes_resource/custom_resource.rb +1 -1
  23. data/lib/kubernetes-deploy/kubernetes_resource/custom_resource_definition.rb +1 -1
  24. data/lib/kubernetes-deploy/kubernetes_resource/deployment.rb +1 -1
  25. data/lib/kubernetes-deploy/kubernetes_resource/horizontal_pod_autoscaler.rb +12 -4
  26. data/lib/kubernetes-deploy/kubernetes_resource/network_policy.rb +22 -0
  27. data/lib/kubernetes-deploy/kubernetes_resource/pod.rb +2 -0
  28. data/lib/kubernetes-deploy/kubernetes_resource/secret.rb +23 -0
  29. data/lib/kubernetes-deploy/label_selector.rb +42 -0
  30. data/lib/kubernetes-deploy/options_helper.rb +31 -15
  31. data/lib/kubernetes-deploy/remote_logs.rb +6 -1
  32. data/lib/kubernetes-deploy/renderer.rb +5 -1
  33. data/lib/kubernetes-deploy/resource_cache.rb +4 -1
  34. data/lib/kubernetes-deploy/restart_task.rb +22 -10
  35. data/lib/kubernetes-deploy/runner_task.rb +5 -3
  36. data/lib/kubernetes-deploy/version.rb +1 -1
  37. metadata +6 -2
data/dev.yml CHANGED
@@ -1,14 +1,14 @@
1
1
  ---
2
2
  name: kubernetes-deploy
3
3
  up:
4
- - ruby: 2.3.3
4
+ - ruby: 2.3.3 # Matches gemspec
5
5
  - bundler
6
6
  - homebrew:
7
7
  - Caskroom/cask/minikube
8
8
  - custom:
9
9
  name: Minikube Cluster
10
- met?: test $(minikube status | grep Running | wc -l) -eq 2 && $(minikube status | grep -q 'Correctly Configured')
11
- meet: minikube start --kubernetes-version=v1.10.6 --vm-driver=hyperkit
10
+ met?: test $(minikube status | grep Running | wc -l) -ge 2 && $(minikube status | grep -q 'Correctly Configured')
11
+ meet: minikube start --kubernetes-version=v1.11.6 --vm-driver=hyperkit
12
12
  down: minikube stop
13
13
  commands:
14
14
  reset-minikube: minikube delete && rm -rf ~/.minikube
@@ -11,23 +11,29 @@ prune = true
11
11
  bindings = {}
12
12
  verbose_log_prefix = false
13
13
  max_watch_seconds = nil
14
+ selector = nil
14
15
 
15
16
  ARGV.options do |opts|
17
+ parser = KubernetesDeploy::BindingsParser.new
16
18
  opts.on("--bindings=BINDINGS", "Expose additional variables to ERB templates " \
17
- "(format: k1=v1,k2=v2, JSON string or file (JSON or YAML) path prefixed by '@')") do |binds|
18
- bindings.merge!(KubernetesDeploy::BindingsParser.parse(binds))
19
- end
19
+ "(format: k1=v1,k2=v2, JSON string or file (JSON or YAML) path prefixed by '@')") { |b| parser.add(b) }
20
20
 
21
21
  opts.on("--skip-wait", "Skip verification of non-priority-resource success (not recommended)") { skip_wait = true }
22
22
  prot_ns = KubernetesDeploy::DeployTask::PROTECTED_NAMESPACES.join(', ')
23
23
  opts.on("--allow-protected-ns", "Enable deploys to #{prot_ns}; requires --no-prune") { allow_protected_ns = true }
24
24
  opts.on("--no-prune", "Disable deletion of resources that do not appear in the template dir") { prune = false }
25
- opts.on("--template-dir=DIR", "Set the template dir (default: config/deploy/$ENVIRONMENT)") { |v| template_dir = v }
25
+ opts.on("--template-dir=DIR", "Set the template dir (default: config/deploy/$ENVIRONMENT).") do |d|
26
+ template_dir = d
27
+ end
26
28
  opts.on("--verbose-log-prefix", "Add [context][namespace] to the log prefix") { verbose_log_prefix = true }
27
29
  opts.on("--max-watch-seconds=seconds",
28
30
  "Timeout error is raised if it takes longer than the specified number of seconds") do |t|
29
31
  max_watch_seconds = t.to_i
30
32
  end
33
+ opts.on("--selector=SELECTOR", "Ensure that all resources in your template dir match the given selector, " \
34
+ "and restrict pruning to deployed resources it selects. (format: k1=v1,k2=v2)") do |s|
35
+ selector = KubernetesDeploy::LabelSelector.parse(s)
36
+ end
31
37
 
32
38
  opts.on_tail("-h", "--help", "Print this help") do
33
39
  puts opts
@@ -38,32 +44,37 @@ ARGV.options do |opts|
38
44
  exit
39
45
  end
40
46
  opts.parse!
47
+ bindings = parser.parse
41
48
  end
42
49
 
43
50
  namespace = ARGV[0]
44
51
  context = ARGV[1]
45
- template_dir = KubernetesDeploy::OptionsHelper.default_and_check_template_dir(template_dir)
46
- revision = KubernetesDeploy::OptionsHelper.revision_from_environment
47
52
  logger = KubernetesDeploy::FormattedLogger.build(namespace, context, verbose_prefix: verbose_log_prefix)
48
53
 
49
- runner = KubernetesDeploy::DeployTask.new(
50
- namespace: namespace,
51
- context: context,
52
- current_sha: revision,
53
- template_dir: template_dir,
54
- bindings: bindings,
55
- logger: logger,
56
- max_watch_seconds: max_watch_seconds
57
- )
58
-
59
54
  begin
60
- runner.run!(
61
- verify_result: !skip_wait,
62
- allow_protected_ns: allow_protected_ns,
63
- prune: prune
64
- )
55
+ KubernetesDeploy::OptionsHelper.with_validated_template_dir(template_dir) do |dir|
56
+ runner = KubernetesDeploy::DeployTask.new(
57
+ namespace: namespace,
58
+ context: context,
59
+ current_sha: ENV["REVISION"],
60
+ template_dir: dir,
61
+ bindings: bindings,
62
+ logger: logger,
63
+ max_watch_seconds: max_watch_seconds,
64
+ selector: selector,
65
+ )
66
+
67
+ runner.run!(
68
+ verify_result: !skip_wait,
69
+ allow_protected_ns: allow_protected_ns,
70
+ prune: prune
71
+ )
72
+ end
65
73
  rescue KubernetesDeploy::DeploymentTimeoutError
66
74
  exit(70)
67
75
  rescue KubernetesDeploy::FatalDeploymentError
68
76
  exit(1)
77
+ rescue KubernetesDeploy::OptionsHelper::OptionsError => e
78
+ logger.error(e.message)
79
+ exit(1)
69
80
  end
@@ -9,24 +9,32 @@ template_dir = nil
9
9
  bindings = {}
10
10
 
11
11
  ARGV.options do |opts|
12
+ parser = KubernetesDeploy::BindingsParser.new
12
13
  opts.on("--bindings=BINDINGS", "Expose additional variables to ERB templates " \
13
- "(format: k1=v1,k2=v2, JSON string or file (JSON or YAML) path prefixed by '@')") do |binds|
14
- bindings.merge!(KubernetesDeploy::BindingsParser.parse(binds))
14
+ "(format: k1=v1,k2=v2, JSON string or file (JSON or YAML) path prefixed by '@')") { |b| parser.add(b) }
15
+ opts.on("--template-dir=DIR", "Set the template dir (default: config/deploy/$ENVIRONMENT)." ) do |d|
16
+ template_dir = d
15
17
  end
16
- opts.on("--template-dir=DIR", "Set the template dir (default: config/deploy/$ENVIRONMENT)") { |v| template_dir = v }
17
18
  opts.parse!
19
+ bindings = parser.parse
18
20
  end
19
21
 
20
22
  templates = ARGV
21
23
  logger = KubernetesDeploy::FormattedLogger.build(verbose_prefix: false)
22
- revision = KubernetesDeploy::OptionsHelper.revision_from_environment
23
24
 
24
- runner = KubernetesDeploy::RenderTask.new(
25
- logger: logger,
26
- current_sha: revision,
27
- template_dir: template_dir,
28
- bindings: bindings,
29
- )
25
+ begin
26
+ KubernetesDeploy::OptionsHelper.with_validated_template_dir(template_dir) do |dir|
27
+ runner = KubernetesDeploy::RenderTask.new(
28
+ logger: logger,
29
+ current_sha: ENV["REVISION"],
30
+ template_dir: dir,
31
+ bindings: bindings,
32
+ )
30
33
 
31
- success = runner.run(STDOUT, templates)
32
- exit(1) unless success
34
+ success = runner.run(STDOUT, templates)
35
+ exit(1) unless success
36
+ end
37
+ rescue KubernetesDeploy::OptionsHelper::OptionsError => e
38
+ logger.error(e.message)
39
+ exit(1)
40
+ end
@@ -8,9 +8,13 @@ require 'kubernetes-deploy/restart_task'
8
8
 
9
9
  raw_deployments = nil
10
10
  max_watch_seconds = nil
11
+ selector = nil
11
12
  ARGV.options do |opts|
12
13
  opts.on("--deployments=LIST") { |v| raw_deployments = v.split(",") }
13
14
  opts.on("--max-watch-seconds=seconds") { |t| max_watch_seconds = t.to_i }
15
+ opts.on("--selector=SELECTOR", "Restarts deployments matching selector (format: k1=v1,k2=v2)") do |s|
16
+ selector = KubernetesDeploy::LabelSelector.parse(s)
17
+ end
14
18
  opts.parse!
15
19
  end
16
20
 
@@ -21,7 +25,7 @@ logger = KubernetesDeploy::FormattedLogger.build(namespace, context)
21
25
  restart = KubernetesDeploy::RestartTask.new(namespace: namespace, context: context, logger: logger,
22
26
  max_watch_seconds: max_watch_seconds)
23
27
  begin
24
- restart.perform!(raw_deployments)
28
+ restart.perform!(raw_deployments, selector: selector)
25
29
  rescue KubernetesDeploy::DeploymentTimeoutError
26
30
  exit(70)
27
31
  rescue KubernetesDeploy::FatalDeploymentError
@@ -21,6 +21,7 @@ require 'kubernetes-deploy/concurrency'
21
21
  require 'kubernetes-deploy/bindings_parser'
22
22
  require 'kubernetes-deploy/duration_parser'
23
23
  require 'kubernetes-deploy/resource_cache'
24
+ require 'kubernetes-deploy/label_selector'
24
25
 
25
26
  module KubernetesDeploy
26
27
  MIN_KUBE_VERSION = '1.10.0'
@@ -4,17 +4,29 @@ require 'yaml'
4
4
  require 'csv'
5
5
 
6
6
  module KubernetesDeploy
7
- module BindingsParser
8
- extend self
7
+ class BindingsParser
8
+ def self.parse(string)
9
+ new(string).parse
10
+ end
9
11
 
10
- def parse(string)
11
- bindings = parse_file(string) || parse_json(string) || parse_csv(string)
12
+ def initialize(initial_string = nil)
13
+ @raw_bindings = Array(initial_string)
14
+ end
12
15
 
13
- unless bindings
14
- raise ArgumentError, "Failed to parse bindings."
15
- end
16
+ def add(string)
17
+ @raw_bindings << string
18
+ end
16
19
 
17
- bindings
20
+ def parse
21
+ result = {}
22
+ @raw_bindings.each do |string|
23
+ bindings = parse_file(string) || parse_json(string) || parse_csv(string)
24
+ unless bindings
25
+ raise ArgumentError, "Failed to parse bindings."
26
+ end
27
+ result.deep_merge!(bindings)
28
+ end
29
+ result
18
30
  end
19
31
 
20
32
  private
@@ -19,7 +19,7 @@ module KubernetesDeploy
19
19
  private
20
20
 
21
21
  def fetch_crds
22
- raw_json, _, st = kubectl.run("get", "CustomResourceDefinition", "-a", "--output=json", attempts: 5)
22
+ raw_json, _, st = kubectl.run("get", "CustomResourceDefinition", "-a", output: "json", attempts: 5)
23
23
  if st.success?
24
24
  JSON.parse(raw_json)["items"]
25
25
  else
@@ -13,6 +13,7 @@ module KubernetesDeploy
13
13
  @logger = logger
14
14
  @lines = []
15
15
  @next_print_index = 0
16
+ @printed_latest = false
16
17
  end
17
18
 
18
19
  def sync
@@ -33,12 +34,17 @@ module KubernetesDeploy
33
34
  end
34
35
 
35
36
  @next_print_index = lines.length
37
+ @printed_latest = true
36
38
  end
37
39
 
38
40
  def print_all
39
41
  lines.each { |line| @logger.info("\t#{line}") }
40
42
  end
41
43
 
44
+ def printing_started?
45
+ @printed_latest
46
+ end
47
+
42
48
  private
43
49
 
44
50
  def fetch_latest
@@ -14,6 +14,7 @@ require 'kubernetes-deploy/kubernetes_resource'
14
14
  persistent_volume_claim
15
15
  pod
16
16
  redis
17
+ network_policy
17
18
  memcached
18
19
  service
19
20
  pod_template
@@ -30,6 +31,7 @@ require 'kubernetes-deploy/kubernetes_resource'
30
31
  job
31
32
  custom_resource_definition
32
33
  horizontal_pod_autoscaler
34
+ secret
33
35
  ).each do |subresource|
34
36
  require "kubernetes-deploy/kubernetes_resource/#{subresource}"
35
37
  end
@@ -43,7 +45,6 @@ require 'kubernetes-deploy/template_discovery'
43
45
 
44
46
  module KubernetesDeploy
45
47
  class DeployTask
46
- include KubeclientBuilder
47
48
  extend KubernetesDeploy::StatsD::MeasureMethods
48
49
 
49
50
  PROTECTED_NAMESPACES = %w(
@@ -58,11 +59,11 @@ module KubernetesDeploy
58
59
  # core/v1/PersistentVolumeClaim -- would delete data
59
60
  # core/v1/ReplicationController -- superseded by deployments/replicasets
60
61
  # extensions/v1beta1/ReplicaSet -- managed by deployments
61
- # core/v1/Secret -- should not committed / managed by shipit
62
62
 
63
63
  def predeploy_sequence
64
64
  before_crs = %w(
65
65
  ResourceQuota
66
+ NetworkPolicy
66
67
  )
67
68
  after_crs = %w(
68
69
  ConfigMap
@@ -70,6 +71,7 @@ module KubernetesDeploy
70
71
  ServiceAccount
71
72
  Role
72
73
  RoleBinding
74
+ Secret
73
75
  Pod
74
76
  )
75
77
 
@@ -82,10 +84,12 @@ module KubernetesDeploy
82
84
  core/v1/Pod
83
85
  core/v1/Service
84
86
  core/v1/ResourceQuota
87
+ core/v1/Secret
85
88
  batch/v1/Job
86
89
  extensions/v1beta1/DaemonSet
87
90
  extensions/v1beta1/Deployment
88
91
  extensions/v1beta1/Ingress
92
+ networking.k8s.io/v1/NetworkPolicy
89
93
  apps/v1beta1/StatefulSet
90
94
  autoscaling/v1/HorizontalPodAutoscaler
91
95
  policy/v1beta1/PodDisruptionBudget
@@ -99,7 +103,7 @@ module KubernetesDeploy
99
103
  end
100
104
 
101
105
  def initialize(namespace:, context:, current_sha:, template_dir:, logger:, kubectl_instance: nil, bindings: {},
102
- max_watch_seconds: nil)
106
+ max_watch_seconds: nil, selector: nil)
103
107
  @namespace = namespace
104
108
  @namespace_tags = []
105
109
  @context = context
@@ -114,6 +118,7 @@ module KubernetesDeploy
114
118
  logger: @logger,
115
119
  bindings: bindings,
116
120
  )
121
+ @selector = selector
117
122
  end
118
123
 
119
124
  def run(*args)
@@ -134,7 +139,6 @@ module KubernetesDeploy
134
139
 
135
140
  @logger.phase_heading("Checking initial resource statuses")
136
141
  check_initial_status(resources)
137
- create_ejson_secrets(prune)
138
142
 
139
143
  if deploy_has_priority_resources?(resources)
140
144
  @logger.phase_heading("Predeploying priority resources")
@@ -187,6 +191,10 @@ module KubernetesDeploy
187
191
 
188
192
  private
189
193
 
194
+ def kubeclient_builder
195
+ @kubeclient_builder ||= KubeclientBuilder.new
196
+ end
197
+
190
198
  def cluster_resource_discoverer
191
199
  @cluster_resource_discoverer ||= ClusterResourceDiscovery.new(
192
200
  namespace: @namespace,
@@ -196,11 +204,27 @@ module KubernetesDeploy
196
204
  )
197
205
  end
198
206
 
207
+ def ejson_provisioner
208
+ @ejson_provisioner ||= EjsonSecretProvisioner.new(
209
+ namespace: @namespace,
210
+ context: @context,
211
+ template_dir: @template_dir,
212
+ logger: @logger,
213
+ statsd_tags: @namespace_tags,
214
+ selector: @selector,
215
+ )
216
+ end
217
+
199
218
  def deploy_has_priority_resources?(resources)
200
219
  resources.any? { |r| predeploy_sequence.include?(r.type) }
201
220
  end
202
221
 
203
222
  def predeploy_priority_resources(resource_list)
223
+ bare_pods = resource_list.select { |resource| resource.is_a?(Pod) }
224
+ if bare_pods.count == 1
225
+ bare_pods.first.stream_logs = true
226
+ end
227
+
204
228
  predeploy_sequence.each do |resource_type|
205
229
  matching_resources = resource_list.select { |r| r.type == resource_type }
206
230
  next if matching_resources.empty?
@@ -221,7 +245,9 @@ module KubernetesDeploy
221
245
  measure_method(:predeploy_priority_resources, 'priority_resources.duration')
222
246
 
223
247
  def validate_resources(resources)
224
- KubernetesDeploy::Concurrency.split_across_threads(resources) { |r| r.validate_definition(kubectl) }
248
+ KubernetesDeploy::Concurrency.split_across_threads(resources) do |r|
249
+ r.validate_definition(kubectl, selector: @selector)
250
+ end
225
251
  failed_resources = resources.select(&:validation_failed?)
226
252
  return unless failed_resources.present?
227
253
 
@@ -240,20 +266,9 @@ module KubernetesDeploy
240
266
  end
241
267
  measure_method(:check_initial_status, "initial_status.duration")
242
268
 
243
- def create_ejson_secrets(prune)
244
- ejson = EjsonSecretProvisioner.new(
245
- namespace: @namespace,
246
- context: @context,
247
- template_dir: @template_dir,
248
- logger: @logger,
249
- prune: prune,
250
- )
251
- return unless ejson.secret_changes_required?
252
-
253
- @logger.phase_heading("Deploying kubernetes secrets from #{EjsonSecretProvisioner::EJSON_SECRETS_FILE}")
254
- ejson.run
269
+ def secrets_from_ejson
270
+ ejson_provisioner.resources
255
271
  end
256
- measure_method(:create_ejson_secrets)
257
272
 
258
273
  def discover_resources
259
274
  resources = []
@@ -269,11 +284,15 @@ module KubernetesDeploy
269
284
  @logger.info(" - #{r.id}")
270
285
  end
271
286
  end
287
+ secrets_from_ejson.each do |secret|
288
+ resources << secret
289
+ @logger.info(" - #{secret.id} (from ejson)")
290
+ end
272
291
  if (global = resources.select(&:global?).presence)
273
292
  @logger.warn("Detected non-namespaced #{'resource'.pluralize(global.count)} which will never be pruned:")
274
293
  global.each { |r| @logger.warn(" - #{r.id}") }
275
294
  end
276
- resources
295
+ resources.sort
277
296
  end
278
297
  measure_method(:discover_resources)
279
298
 
@@ -297,30 +316,16 @@ module KubernetesDeploy
297
316
  raise FatalDeploymentError, "Failed to render and parse template"
298
317
  end
299
318
 
300
- def record_invalid_template(err:, filename:, content:)
319
+ def record_invalid_template(err:, filename:, content: nil)
301
320
  debug_msg = ColorizedString.new("Invalid template: #{filename}\n").red
302
321
  debug_msg += "> Error message:\n#{FormattedLogger.indent_four(err)}"
303
- debug_msg += "\n> Template content:\n#{FormattedLogger.indent_four(content)}"
322
+ debug_msg += "\n> Template content:\n#{FormattedLogger.indent_four(content)}" if content
304
323
  @logger.summary.add_paragraph(debug_msg)
305
324
  end
306
325
 
307
326
  def validate_configuration(allow_protected_ns:, prune:)
308
327
  errors = []
309
- if ENV["KUBECONFIG"].blank?
310
- errors << "$KUBECONFIG not set"
311
- elsif config_files.empty?
312
- errors << "Kube config file name(s) not set in $KUBECONFIG"
313
- else
314
- config_files.each do |f|
315
- unless File.file?(f)
316
- errors << "Kube config not found at #{f}"
317
- end
318
- end
319
- end
320
-
321
- if @current_sha.blank?
322
- errors << "Current SHA must be specified"
323
- end
328
+ errors += kubeclient_builder.validate_config_files
324
329
 
325
330
  if !File.directory?(@template_dir)
326
331
  errors << "Template directory `#{@template_dir}` doesn't exist"
@@ -354,6 +359,8 @@ module KubernetesDeploy
354
359
 
355
360
  confirm_context_exists
356
361
  confirm_namespace_exists
362
+ confirm_ejson_keys_not_prunable if prune
363
+ @logger.info("Using resource selector #{@selector}") if @selector
357
364
  @namespace_tags |= tags_from_namespace_labels
358
365
  @logger.info("All required parameters and files are present")
359
366
  end
@@ -365,6 +372,9 @@ module KubernetesDeploy
365
372
 
366
373
  if resources.length > 1
367
374
  @logger.info("Deploying resources:")
375
+ resources.each do |r|
376
+ @logger.info("- #{r.id} (#{r.pretty_timeout_type})")
377
+ end
368
378
  else
369
379
  resource = resources.first
370
380
  @logger.info("Deploying #{resource.id} (#{resource.pretty_timeout_type})")
@@ -377,7 +387,6 @@ module KubernetesDeploy
377
387
  applyables += individuals.select { |r| pruneable_types.include?(r.type) }
378
388
 
379
389
  individuals.each do |r|
380
- @logger.info("- #{r.id} (#{r.pretty_timeout_type})") if resources.length > 1
381
390
  r.deploy_started_at = Time.now.utc
382
391
  case r.deploy_method
383
392
  when :replace
@@ -421,23 +430,28 @@ module KubernetesDeploy
421
430
 
422
431
  Dir.mktmpdir do |tmp_dir|
423
432
  resources.each do |r|
424
- @logger.info("- #{r.id} (#{r.pretty_timeout_type})") if resources.length > 1
425
433
  FileUtils.symlink(r.file_path, tmp_dir)
426
434
  r.deploy_started_at = Time.now.utc
427
435
  end
428
436
  command.push("-f", tmp_dir)
429
437
 
430
438
  if prune
431
- command.push("--prune", "--all")
439
+ command.push("--prune")
440
+ if @selector
441
+ command.push("--selector", @selector.to_s)
442
+ else
443
+ command.push("--all")
444
+ end
432
445
  prune_whitelist.each { |type| command.push("--prune-whitelist=#{type}") }
433
446
  end
434
447
 
435
- out, err, st = kubectl.run(*command, log_failure: false)
448
+ output_is_sensitive = resources.any?(&:kubectl_output_is_sensitive?)
449
+ out, err, st = kubectl.run(*command, log_failure: false, output_is_sensitive: output_is_sensitive)
436
450
 
437
451
  if st.success?
438
452
  log_pruning(out) if prune
439
453
  else
440
- record_apply_failure(err)
454
+ record_apply_failure(err, resources: resources)
441
455
  raise FatalDeploymentError, "Command failed: #{Shellwords.join(command)}"
442
456
  end
443
457
  end
@@ -452,22 +466,37 @@ module KubernetesDeploy
452
466
  @logger.summary.add_action("pruned #{pruned.length} #{'resource'.pluralize(pruned.length)}")
453
467
  end
454
468
 
455
- def record_apply_failure(err)
469
+ def record_apply_failure(err, resources: [])
456
470
  warn_msg = "WARNING: Any resources not mentioned in the error(s) below were likely created/updated. " \
457
471
  "You may wish to roll back this deploy."
458
472
  @logger.summary.add_paragraph(ColorizedString.new(warn_msg).yellow)
459
473
 
460
474
  unidentified_errors = []
475
+ filenames_with_sensitive_content = resources
476
+ .select(&:kubectl_output_is_sensitive?)
477
+ .map { |r| File.basename(r.file_path) }
478
+
461
479
  err.each_line do |line|
462
480
  bad_files = find_bad_files_from_kubectl_output(line)
463
481
  if bad_files.present?
464
- bad_files.each { |f| record_invalid_template(err: f[:err], filename: f[:filename], content: f[:content]) }
482
+ bad_files.each do |f|
483
+ if filenames_with_sensitive_content.include?(f[:filename])
484
+ # Hide the error and template contents in case it has senitive information
485
+ record_invalid_template(err: "SUPPRESSED FOR SECURITY", filename: f[:filename], content: nil)
486
+ else
487
+ record_invalid_template(err: f[:err], filename: f[:filename], content: f[:content])
488
+ end
489
+ end
465
490
  else
466
491
  unidentified_errors << line
467
492
  end
468
493
  end
469
494
 
470
- if unidentified_errors.present?
495
+ if unidentified_errors.present? && filenames_with_sensitive_content.any?
496
+ warn_msg = "WARNING: There was an error applying some or all resources. The raw output may be sensitive and " \
497
+ "so cannot be displayed."
498
+ @logger.summary.add_paragraph(ColorizedString.new(warn_msg).yellow)
499
+ elsif unidentified_errors.present?
471
500
  heading = ColorizedString.new('Unidentified error(s):').red
472
501
  msg = FormattedLogger.indent_four(unidentified_errors.join)
473
502
  @logger.summary.add_paragraph("#{heading}\n#{msg}")
@@ -526,6 +555,18 @@ module KubernetesDeploy
526
555
  @logger.info("Namespace #{@namespace} found")
527
556
  end
528
557
 
558
+ # make sure to never prune the ejson-keys secret
559
+ def confirm_ejson_keys_not_prunable
560
+ secret = ejson_provisioner.ejson_keys_secret
561
+ return unless secret.dig("metadata", "annotations", KubernetesResource::LAST_APPLIED_ANNOTATION)
562
+
563
+ @logger.error("Deploy cannot proceed because protected resource " \
564
+ "Secret/#{EjsonSecretProvisioner::EJSON_KEYS_SECRET} would be pruned.")
565
+ raise EjsonPrunableError
566
+ rescue Kubectl::ResourceNotFoundError => e
567
+ @logger.debug("Secret/#{EjsonSecretProvisioner::EJSON_KEYS_SECRET} does not exist: #{e}")
568
+ end
569
+
529
570
  def tags_from_namespace_labels
530
571
  namespace_info = nil
531
572
  with_retries(2) do