kubernetes-deploy 0.25.0 → 0.26.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.
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