kubernetes-deploy 0.6.6 → 0.7.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 (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