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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 845bf8bd854e94df070d5637d37a8678f9a92411
4
- data.tar.gz: 32a57ce86b607e98133ec04875d88f4ffa37495c
3
+ metadata.gz: 60da18d26c2e8a36c8761924b17e1b263eacaf70
4
+ data.tar.gz: 6f05ac075997226ed8c526f6d02cbb4406617b55
5
5
  SHA512:
6
- metadata.gz: 8a6b635d9554e3737a5330fe4df509ec6ca5f5b1a0a3abac8016a199b084620d36ac47116e7863c9b8ebbda3e5a6a264b4b24da6262e772ee0250e2d98e48ea4
7
- data.tar.gz: c479251f382257c6be386422f13d87ab4252f4adaa91b5d1600aa9df30b0d8687b9bb0612642275d2dc4860fc15871bb3afdad8f59e6311e1e120b3d9da11c7a
6
+ metadata.gz: da4f1f609882fb90af50e1505a9ca841eb8e6a95de220b3471686425964c953b025858bb86835b9d50f5f6053a77188affaf363d690b2211914a1c3bad4d17e3
7
+ data.tar.gz: 7fe5caa73866be72e7aa2351a4e0d562d4891eda7fc8261d4d9ceb5b5abc972bc09ca26a67459eec72d2b516bbb77fde10e4d63d78bbe791a93e2b810f5211d1
@@ -10,6 +10,7 @@ template_dir = nil
10
10
  allow_protected_ns = false
11
11
  prune = true
12
12
  bindings = {}
13
+ verbose_log_prefix = false
13
14
 
14
15
  ARGV.options do |opts|
15
16
  opts.on("--bindings=BINDINGS", Array, "k1=v1,k2=v2") do |binds|
@@ -25,6 +26,7 @@ ARGV.options do |opts|
25
26
  opts.on("--allow-protected-ns") { allow_protected_ns = true }
26
27
  opts.on("--no-prune") { prune = false }
27
28
  opts.on("--template-dir=DIR") { |v| template_dir = v }
29
+ opts.on("--verbose-log-prefix") { verbose_log_prefix = true }
28
30
  opts.parse!
29
31
  end
30
32
 
@@ -43,16 +45,22 @@ revision = ENV.fetch('REVISION') do
43
45
  exit 1
44
46
  end
45
47
 
46
- KubernetesDeploy::Runner.with_friendly_errors do
47
- runner = KubernetesDeploy::Runner.new(
48
- namespace: ARGV[0],
49
- context: ARGV[1],
50
- current_sha: revision,
51
- template_dir: template_dir,
52
- wait_for_completion: !skip_wait,
53
- allow_protected_ns: allow_protected_ns,
54
- prune: prune,
55
- bindings: bindings
56
- )
57
- runner.run
58
- end
48
+ namespace = ARGV[0]
49
+ context = ARGV[1]
50
+ logger = KubernetesDeploy::FormattedLogger.build(namespace, context, verbose_prefix: verbose_log_prefix)
51
+
52
+ runner = KubernetesDeploy::Runner.new(
53
+ namespace: namespace,
54
+ context: context,
55
+ current_sha: revision,
56
+ template_dir: template_dir,
57
+ bindings: bindings,
58
+ logger: logger
59
+ )
60
+
61
+ success = runner.run(
62
+ verify_result: !skip_wait,
63
+ allow_protected_ns: allow_protected_ns,
64
+ prune: prune
65
+ )
66
+ exit 1 unless success
@@ -12,7 +12,10 @@ ARGV.options do |opts|
12
12
  opts.parse!
13
13
  end
14
14
 
15
- KubernetesDeploy::Runner.with_friendly_errors do
16
- restart = KubernetesDeploy::RestartTask.new(namespace: ARGV[0], context: ARGV[1])
17
- restart.perform(raw_deployments)
18
- end
15
+ namespace = ARGV[0]
16
+ context = ARGV[1]
17
+ logger = KubernetesDeploy::FormattedLogger.build(namespace, context)
18
+
19
+ restart = KubernetesDeploy::RestartTask.new(namespace: namespace, context: context, logger: logger)
20
+ success = restart.perform(raw_deployments)
21
+ exit 1 unless success
data/exe/kubernetes-run CHANGED
@@ -16,16 +16,20 @@ ARGV.options do |opts|
16
16
  opts.parse!
17
17
  end
18
18
 
19
+ namespace = ARGV[0]
20
+ context = ARGV[1]
21
+ logger = KubernetesDeploy::FormattedLogger.build(namespace, context)
22
+
19
23
  runner = KubernetesDeploy::RunnerTask.new(
20
- namespace: ARGV[0],
21
- context: ARGV[1],
24
+ namespace: namespace,
25
+ context: context,
26
+ logger: logger
22
27
  )
23
28
 
24
- KubernetesDeploy::Runner.with_friendly_errors do
25
- runner.run(
26
- task_template: template,
27
- entrypoint: entrypoint,
28
- args: ARGV[2..-1],
29
- env_vars: env_vars
30
- )
31
- end
29
+ success = runner.run(
30
+ task_template: template,
31
+ entrypoint: entrypoint,
32
+ args: ARGV[2..-1],
33
+ env_vars: env_vars
34
+ )
35
+ exit 1 unless success
@@ -24,6 +24,7 @@ Gem::Specification.new do |spec|
24
24
  spec.add_dependency "kubeclient", "~> 2.3"
25
25
  spec.add_dependency "googleauth", ">= 0.5"
26
26
  spec.add_dependency "ejson", "1.0.1"
27
+ spec.add_dependency "colorize", "~> 0.8"
27
28
 
28
29
  spec.add_development_dependency "bundler", "~> 1.13"
29
30
  spec.add_development_dependency "rake", "~> 10.0"
@@ -5,11 +5,12 @@ require 'active_support/core_ext/numeric/time'
5
5
  require 'active_support/core_ext/string/inflections'
6
6
  require 'active_support/core_ext/string/strip'
7
7
  require 'active_support/core_ext/hash/keys'
8
+ require 'active_support/core_ext/array/conversions'
9
+ require 'colorized_string'
8
10
 
9
11
  require 'kubernetes-deploy/errors'
10
- require 'kubernetes-deploy/logger'
12
+ require 'kubernetes-deploy/formatted_logger'
11
13
  require 'kubernetes-deploy/runner'
12
14
 
13
15
  module KubernetesDeploy
14
- include Logger
15
16
  end
@@ -0,0 +1,87 @@
1
+ module KubernetesDeploy
2
+ # Adds the methods kubernetes-deploy requires to your logger class.
3
+ # These methods include helpers for logging consistent headings, as well as facilities for
4
+ # displaying key information later, in a summary section, rather than when it occurred.
5
+ module DeferredSummaryLogging
6
+ attr_reader :summary
7
+ def initialize(*args)
8
+ reset
9
+ super
10
+ end
11
+
12
+ def reset
13
+ @summary = DeferredSummary.new
14
+ @current_phase = 0
15
+ end
16
+
17
+ def blank_line(level = :info)
18
+ public_send(level, "")
19
+ end
20
+
21
+ def phase_heading(phase_name)
22
+ @current_phase += 1
23
+ heading("Phase #{@current_phase}: #{phase_name}")
24
+ end
25
+
26
+ def heading(text, secondary_msg = '', secondary_msg_color = :blue)
27
+ padding = (100.0 - (text.length + secondary_msg.length)) / 2
28
+ blank_line
29
+ part1 = ColorizedString.new("#{'-' * padding.floor}#{text}").blue
30
+ part2 = ColorizedString.new(secondary_msg).colorize(secondary_msg_color)
31
+ part3 = ColorizedString.new('-' * padding.ceil).blue
32
+ info(part1 + part2 + part3)
33
+ end
34
+
35
+ # Outputs the deferred summary information saved via @logger.summary.add_action and @logger.summary.add_paragraph
36
+ def print_summary(success)
37
+ if success
38
+ heading("Result: ", "SUCCESS", :green)
39
+ level = :info
40
+ else
41
+ heading("Result: ", "FAILURE", :red)
42
+ level = :fatal
43
+ end
44
+
45
+ public_send(level, summary.actions_sentence)
46
+ summary.paragraphs.each do |para|
47
+ blank_line(level)
48
+ msg_lines = para.split("\n")
49
+ msg_lines.each { |line| public_send(level, line) }
50
+ end
51
+ end
52
+
53
+ class DeferredSummary
54
+ attr_reader :paragraphs
55
+
56
+ def initialize
57
+ @actions_taken = []
58
+ @paragraphs = []
59
+ end
60
+
61
+ def actions_sentence
62
+ case @actions_taken.length
63
+ when 0 then "No actions taken"
64
+ else
65
+ @actions_taken.to_sentence.capitalize
66
+ end
67
+ end
68
+
69
+ # Saves a sentence fragment to be displayed in the first sentence of the summary section
70
+ #
71
+ # Example:
72
+ # # The resulting summary will begin with "Created 3 secrets and failed to deploy 2 resources"
73
+ # @logger.summary.add_action("created 3 secrets")
74
+ # @logger.summary.add_cation("failed to deploy 2 resources")
75
+ def add_action(sentence_fragment)
76
+ @actions_taken << sentence_fragment
77
+ end
78
+
79
+ # Adds a paragraph to be displayed in the summary section
80
+ # Paragraphs will be printed in the order they were added, separated by a blank line
81
+ # This can be used to log a block of data on a particular topic, e.g. debug info for a particular failed resource
82
+ def add_paragraph(paragraph)
83
+ paragraphs << paragraph
84
+ end
85
+ end
86
+ end
87
+ end
@@ -2,7 +2,6 @@
2
2
  require 'json'
3
3
  require 'base64'
4
4
  require 'open3'
5
- require 'kubernetes-deploy/logger'
6
5
  require 'kubernetes-deploy/kubectl'
7
6
 
8
7
  module KubernetesDeploy
@@ -18,13 +17,12 @@ module KubernetesDeploy
18
17
  EJSON_SECRETS_FILE = "secrets.ejson"
19
18
  EJSON_KEYS_SECRET = "ejson-keys"
20
19
 
21
- def initialize(namespace:, context:, template_dir:)
20
+ def initialize(namespace:, context:, template_dir:, logger:)
22
21
  @namespace = namespace
23
22
  @context = context
24
23
  @ejson_file = "#{template_dir}/#{EJSON_SECRETS_FILE}"
25
-
26
- raise FatalDeploymentError, "Cannot create secrets without a namespace" if @namespace.blank?
27
- raise FatalDeploymentError, "Cannot create secrets without a context" if @context.blank?
24
+ @logger = logger
25
+ @kubectl = Kubectl.new(namespace: @namespace, context: @context, logger: @logger, log_failure_by_default: false)
28
26
  end
29
27
 
30
28
  def secret_changes_required?
@@ -42,7 +40,7 @@ module KubernetesDeploy
42
40
  with_decrypted_ejson do |decrypted|
43
41
  secrets = decrypted[MANAGED_SECRET_EJSON_KEY]
44
42
  unless secrets.present?
45
- KubernetesDeploy.logger.warn("#{EJSON_SECRETS_FILE} does not have key #{MANAGED_SECRET_EJSON_KEY}."\
43
+ @logger.warn("#{EJSON_SECRETS_FILE} does not have key #{MANAGED_SECRET_EJSON_KEY}."\
46
44
  "No secrets will be created.")
47
45
  return
48
46
  end
@@ -51,6 +49,7 @@ module KubernetesDeploy
51
49
  validate_secret_spec(secret_name, secret_spec)
52
50
  create_or_update_secret(secret_name, secret_spec["_type"], secret_spec["data"])
53
51
  end
52
+ @logger.summary.add_action("created/updated #{secrets.length} #{'secret'.pluralize(secrets.length)}")
54
53
  end
55
54
  end
56
55
 
@@ -58,16 +57,19 @@ module KubernetesDeploy
58
57
  ejson_secret_names = encrypted_ejson.fetch(MANAGED_SECRET_EJSON_KEY, {}).keys
59
58
  live_secrets = run_kubectl_json("get", "secrets")
60
59
 
60
+ prune_count = 0
61
61
  live_secrets.each do |secret|
62
62
  secret_name = secret["metadata"]["name"]
63
63
  next unless secret_managed?(secret)
64
64
  next if ejson_secret_names.include?(secret_name)
65
65
 
66
- KubernetesDeploy.logger.info("Pruning secret #{secret_name}")
67
- out, err, st = run_kubectl("delete", "secret", secret_name)
68
- KubernetesDeploy.logger.debug(out)
66
+ @logger.info("Pruning secret #{secret_name}")
67
+ prune_count += 1
68
+ out, err, st = @kubectl.run("delete", "secret", secret_name)
69
+ @logger.debug(out)
69
70
  raise EjsonSecretError, err unless st.success?
70
71
  end
72
+ @logger.summary.add_action("pruned #{prune_count} #{'secret'.pluralize(prune_count)}") if prune_count > 0
71
73
  end
72
74
 
73
75
  def managed_secrets_exist?
@@ -103,15 +105,15 @@ module KubernetesDeploy
103
105
 
104
106
  def create_or_update_secret(secret_name, secret_type, data)
105
107
  msg = secret_exists?(secret_name) ? "Updating secret #{secret_name}" : "Creating secret #{secret_name}"
106
- KubernetesDeploy.logger.info(msg)
108
+ @logger.info(msg)
107
109
 
108
110
  secret_yaml = generate_secret_yaml(secret_name, secret_type, data)
109
111
  file = Tempfile.new(secret_name)
110
112
  file.write(secret_yaml)
111
113
  file.close
112
114
 
113
- out, err, st = run_kubectl("apply", "--filename=#{file.path}")
114
- KubernetesDeploy.logger.debug(out)
115
+ out, err, st = @kubectl.run("apply", "--filename=#{file.path}")
116
+ @logger.debug(out)
115
117
  raise EjsonSecretError, err unless st.success?
116
118
  ensure
117
119
  file.unlink if file
@@ -144,7 +146,7 @@ module KubernetesDeploy
144
146
  end
145
147
 
146
148
  def secret_exists?(secret_name)
147
- _out, _err, st = run_kubectl("get", "secret", secret_name)
149
+ _out, _err, st = @kubectl.run("get", "secret", secret_name)
148
150
  st.success?
149
151
  end
150
152
 
@@ -166,7 +168,7 @@ module KubernetesDeploy
166
168
  end
167
169
 
168
170
  def decrypt_ejson(key_dir)
169
- KubernetesDeploy.logger.info("Decrypting #{EJSON_SECRETS_FILE}")
171
+ @logger.info("Decrypting #{EJSON_SECRETS_FILE}")
170
172
  # ejson seems to dump both errors and output to STDOUT
171
173
  out_err, st = Open3.capture2e("EJSON_KEYDIR=#{key_dir} ejson decrypt #{@ejson_file}")
172
174
  raise EjsonSecretError, out_err unless st.success?
@@ -176,7 +178,7 @@ module KubernetesDeploy
176
178
  end
177
179
 
178
180
  def fetch_private_key_from_secret
179
- KubernetesDeploy.logger.info("Fetching ejson private key from secret #{EJSON_KEYS_SECRET}")
181
+ @logger.info("Fetching ejson private key from secret #{EJSON_KEYS_SECRET}")
180
182
 
181
183
  secret = run_kubectl_json("get", "secret", EJSON_KEYS_SECRET)
182
184
  encoded_private_key = secret["data"][public_key]
@@ -189,14 +191,10 @@ module KubernetesDeploy
189
191
 
190
192
  def run_kubectl_json(*args)
191
193
  args += ["--output=json"]
192
- out, err, st = run_kubectl(*args)
194
+ out, err, st = @kubectl.run(*args)
193
195
  raise EjsonSecretError, err unless st.success?
194
196
  result = JSON.parse(out)
195
197
  result.fetch('items', result)
196
198
  end
197
-
198
- def run_kubectl(*args)
199
- Kubectl.run_kubectl(*args, namespace: @namespace, context: @context, log_failure: false)
200
- end
201
199
  end
202
200
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+ require 'logger'
3
+ require 'kubernetes-deploy/deferred_summary_logging'
4
+
5
+ module KubernetesDeploy
6
+ class FormattedLogger < Logger
7
+ include DeferredSummaryLogging
8
+
9
+ def self.build(namespace, context, stream = $stderr, verbose_prefix: false)
10
+ l = new(stream)
11
+ l.level = level_from_env
12
+
13
+ l.formatter = proc do |severity, datetime, _progname, msg|
14
+ middle = verbose_prefix ? "[#{context}][#{namespace}]" : ""
15
+ colorized_line = ColorizedString.new("[#{severity}][#{datetime}]#{middle}\t#{msg}\n")
16
+
17
+ case severity
18
+ when "FATAL"
19
+ ColorizedString.new("[#{severity}][#{datetime}]#{middle}\t").red + "#{msg}\n"
20
+ when "ERROR"
21
+ colorized_line.red
22
+ when "WARN"
23
+ colorized_line.yellow
24
+ else
25
+ colorized_line
26
+ end
27
+ end
28
+ l
29
+ end
30
+
31
+ def self.level_from_env
32
+ return ::Logger::DEBUG if ENV["DEBUG"]
33
+
34
+ if ENV["LEVEL"]
35
+ ::Logger.const_get(ENV["LEVEL"].upcase)
36
+ else
37
+ ::Logger::INFO
38
+ end
39
+ end
40
+ private_class_method :level_from_env
41
+ end
42
+ end
@@ -1,18 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module KubernetesDeploy
4
- module Kubectl
5
- def self.run_kubectl(*args, namespace:, context:, log_failure: true)
4
+ class Kubectl
5
+ def initialize(namespace:, context:, logger:, log_failure_by_default:)
6
+ @namespace = namespace
7
+ @context = context
8
+ @logger = logger
9
+ @log_failure_by_default = log_failure_by_default
10
+
11
+ raise ArgumentError, "namespace is required" if namespace.blank?
12
+ raise ArgumentError, "context is required" if context.blank?
13
+ end
14
+
15
+ def run(*args, log_failure: nil, use_context: true, use_namespace: true)
16
+ log_failure = @log_failure_by_default if log_failure.nil?
17
+
6
18
  args = args.unshift("kubectl")
7
- args.push("--namespace=#{namespace}") if namespace.present?
8
- args.push("--context=#{context}") if context.present?
19
+ args.push("--namespace=#{@namespace}") if use_namespace
20
+ args.push("--context=#{@context}") if use_context
9
21
 
10
- KubernetesDeploy.logger.debug Shellwords.join(args)
22
+ @logger.debug Shellwords.join(args)
11
23
  out, err, st = Open3.capture3(*args)
12
- KubernetesDeploy.logger.debug(out.shellescape)
24
+ @logger.debug(out.shellescape)
25
+
13
26
  if !st.success? && log_failure
14
- KubernetesDeploy.logger.warn("The following command failed: #{Shellwords.join(args)}")
15
- KubernetesDeploy.logger.warn(err)
27
+ @logger.warn("The following command failed: #{Shellwords.join(args)}")
28
+ @logger.warn(err)
16
29
  end
17
30
  [out.chomp, err.chomp, st]
18
31
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'json'
2
3
  require 'open3'
3
4
  require 'shellwords'
@@ -5,39 +6,42 @@ require 'kubernetes-deploy/kubectl'
5
6
 
6
7
  module KubernetesDeploy
7
8
  class KubernetesResource
8
- def self.logger=(value)
9
- @logger = value
10
- end
11
-
12
- def self.logger
13
- @logger ||= begin
14
- l = ::Logger.new($stderr)
15
- l.formatter = proc do |_severity, datetime, _progname, msg|
16
- "[KUBESTATUS][#{datetime}]\t#{msg}\n"
17
- end
18
- l
19
- end
20
- end
21
-
22
9
  attr_reader :name, :namespace, :file, :context
23
10
  attr_writer :type, :deploy_started
24
11
 
25
12
  TIMEOUT = 5.minutes
26
13
 
27
- def self.for_type(type, name, namespace, context, file)
28
- case type
29
- when 'cloudsql' then Cloudsql.new(name, namespace, context, file)
30
- when 'configmap' then ConfigMap.new(name, namespace, context, file)
31
- when 'deployment' then Deployment.new(name, namespace, context, file)
32
- when 'pod' then Pod.new(name, namespace, context, file)
33
- when 'redis' then Redis.new(name, namespace, context, file)
34
- when 'bugsnag' then Bugsnag.new(name, namespace, context, file)
35
- when 'ingress' then Ingress.new(name, namespace, context, file)
36
- when 'persistentvolumeclaim' then PersistentVolumeClaim.new(name, namespace, context, file)
37
- when 'service' then Service.new(name, namespace, context, file)
38
- when 'podtemplate' then PodTemplate.new(name, namespace, context, file)
39
- when 'poddisruptionbudget' then PodDisruptionBudget.new(name, namespace, context, file)
40
- else self.new(name, namespace, context, file).tap { |r| r.type = type }
14
+ DEBUG_RESOURCE_NOT_FOUND_MESSAGE = "None found. Please check your usual logging service (e.g. Splunk)."
15
+ UNUSUAL_FAILURE_MESSAGE = <<-MSG.strip_heredoc
16
+ It is very unusual for this resource type to fail to deploy. Please try the deploy again.
17
+ If that new deploy also fails, contact your cluster administrator.
18
+ MSG
19
+ STANDARD_TIMEOUT_MESSAGE = <<-MSG.strip_heredoc
20
+ Kubernetes will continue to attempt to deploy this resource in the cluster, but at this point it is considered unlikely that it will succeed.
21
+ If you have reason to believe it will succeed, retry the deploy to continue to monitor the rollout.
22
+ MSG
23
+
24
+ def self.for_type(type:, name:, namespace:, context:, file:, logger:)
25
+ subclass = case type
26
+ when 'cloudsql' then Cloudsql
27
+ when 'configmap' then ConfigMap
28
+ when 'deployment' then Deployment
29
+ when 'pod' then Pod
30
+ when 'redis' then Redis
31
+ when 'bugsnag' then Bugsnag
32
+ when 'ingress' then Ingress
33
+ when 'persistentvolumeclaim' then PersistentVolumeClaim
34
+ when 'service' then Service
35
+ when 'podtemplate' then PodTemplate
36
+ when 'poddisruptionbudget' then PodDisruptionBudget
37
+ end
38
+
39
+ opts = { name: name, namespace: namespace, context: context, file: file, logger: logger }
40
+ if subclass
41
+ subclass.new(**opts)
42
+ else
43
+ inst = new(**opts)
44
+ inst.tap { |r| r.type = type }
41
45
  end
42
46
  end
43
47
 
@@ -49,9 +53,13 @@ module KubernetesDeploy
49
53
  self.class.timeout
50
54
  end
51
55
 
52
- def initialize(name, namespace, context, file)
53
- # subclasses must also set these
54
- @name, @namespace, @context, @file = name, namespace, context, file
56
+ def initialize(name:, namespace:, context:, file:, logger:)
57
+ # subclasses must also set these if they define their own initializer
58
+ @name = name
59
+ @namespace = namespace
60
+ @context = context
61
+ @file = file
62
+ @logger = logger
55
63
  end
56
64
 
57
65
  def id
@@ -59,7 +67,6 @@ module KubernetesDeploy
59
67
  end
60
68
 
61
69
  def sync
62
- log_status
63
70
  end
64
71
 
65
72
  def deploy_failed?
@@ -68,7 +75,7 @@ module KubernetesDeploy
68
75
 
69
76
  def deploy_succeeded?
70
77
  if @deploy_started && !@success_assumption_warning_shown
71
- KubernetesDeploy.logger.warn("Don't know how to monitor resources of type #{type}. Assuming #{id} deployed successfully.")
78
+ @logger.warn("Don't know how to monitor resources of type #{type}. Assuming #{id} deployed successfully.")
72
79
  @success_assumption_warning_shown = true
73
80
  end
74
81
  true
@@ -80,7 +87,6 @@ module KubernetesDeploy
80
87
 
81
88
  def status
82
89
  @status ||= "Unknown"
83
- deploy_timed_out? ? "Timed out with status #{@status}" : @status
84
90
  end
85
91
 
86
92
  def type
@@ -106,31 +112,84 @@ module KubernetesDeploy
106
112
  tpr? ? :replace : :apply
107
113
  end
108
114
 
109
- def status_data
110
- {
111
- group: group_name,
112
- name: name,
113
- status_string: status,
114
- exists: exists?,
115
- succeeded: deploy_succeeded?,
116
- failed: deploy_failed?,
117
- timed_out: deploy_timed_out?
118
- }
115
+ def debug_message
116
+ helpful_info = []
117
+ if deploy_failed?
118
+ helpful_info << ColorizedString.new("#{id}: FAILED").red
119
+ helpful_info << failure_message if failure_message.present?
120
+ else
121
+ helpful_info << ColorizedString.new("#{id}: TIMED OUT").yellow + " (limit: #{timeout}s)"
122
+ helpful_info << timeout_message if timeout_message.present?
123
+ end
124
+ helpful_info << " - Final status: #{status}"
125
+
126
+ events = fetch_events
127
+ if events.present?
128
+ helpful_info << " - Events:"
129
+ events.each do |identifier, event_hashes|
130
+ event_hashes.each { |event| helpful_info << " [#{identifier}]\t#{event}" }
131
+ end
132
+ else
133
+ helpful_info << " - Events: #{DEBUG_RESOURCE_NOT_FOUND_MESSAGE}"
134
+ end
135
+
136
+ if respond_to?(:fetch_logs)
137
+ container_logs = fetch_logs
138
+ if container_logs.blank? || container_logs.values.all?(&:blank?)
139
+ helpful_info << " - Logs: #{DEBUG_RESOURCE_NOT_FOUND_MESSAGE}"
140
+ else
141
+ helpful_info << " - Logs:"
142
+ container_logs.each do |identifier, logs|
143
+ logs.split("\n").each do |line|
144
+ helpful_info << " [#{identifier}]\t#{line}"
145
+ end
146
+ end
147
+ end
148
+ end
149
+
150
+ helpful_info.join("\n")
151
+ end
152
+
153
+ # Returns a hash in the following format:
154
+ # {
155
+ # "pod/web-1" => [
156
+ # {"subject_kind" => "Pod", "some" => "stuff"}, # event 1
157
+ # {"subject_kind" => "Pod", "other" => "stuff"}, # event 2
158
+ # ]
159
+ # }
160
+ def fetch_events
161
+ return {} unless exists?
162
+ fields = "{.involvedObject.kind}\t{.count}\t{.message}\t{.reason}"
163
+ out, _err, st = kubectl.run("get", "events",
164
+ %(--output=jsonpath={range .items[?(@.involvedObject.name=="#{name}")]}#{fields}\n{end}))
165
+ return {} unless st.success?
166
+
167
+ event_collector = Hash.new { |hash, key| hash[key] = [] }
168
+ out.split("\n").each_with_object(event_collector) do |event_blob, events|
169
+ pieces = event_blob.split("\t")
170
+ subject_kind = pieces[0]
171
+ # jsonpath can only filter by one thing at a time, and we chose involvedObject.name
172
+ # This means we still need to filter by involvedObject.kind here to make sure we only retain relevant events
173
+ next unless subject_kind.downcase == type.downcase
174
+ events[id] << "#{pieces[3] || 'Unknown'}: #{pieces[2]} (#{pieces[1]} events)" # Reason: Message (X events)
175
+ end
119
176
  end
120
177
 
121
- def group_name
122
- type.downcase.pluralize
178
+ def timeout_message
179
+ STANDARD_TIMEOUT_MESSAGE
123
180
  end
124
181
 
125
- def log_status
126
- KubernetesResource.logger.info("[#{@context}][#{@namespace}] #{JSON.dump(status_data)}")
182
+ def failure_message
127
183
  end
128
184
 
129
- def run_kubectl(*args)
130
- raise FatalDeploymentError, "Namespace missing for namespaced command" if @namespace.blank?
131
- raise KubectlError, "Explicit context is required to run this command" if @context.blank?
185
+ def pretty_status
186
+ padding = " " * (50 - id.length)
187
+ msg = exists? ? status : "not found"
188
+ "#{id}#{padding}#{msg}"
189
+ end
132
190
 
133
- Kubectl.run_kubectl(*args, namespace: @namespace, context: @context, log_failure: false)
191
+ def kubectl
192
+ @kubectl ||= Kubectl.new(namespace: @namespace, context: @context, logger: @logger, log_failure_by_default: false)
134
193
  end
135
194
  end
136
195
  end