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.
- checksums.yaml +4 -4
- data/exe/kubernetes-deploy +21 -13
- data/exe/kubernetes-restart +7 -4
- data/exe/kubernetes-run +14 -10
- data/kubernetes-deploy.gemspec +1 -0
- data/lib/kubernetes-deploy.rb +3 -2
- data/lib/kubernetes-deploy/deferred_summary_logging.rb +87 -0
- data/lib/kubernetes-deploy/ejson_secret_provisioner.rb +18 -20
- data/lib/kubernetes-deploy/formatted_logger.rb +42 -0
- data/lib/kubernetes-deploy/kubectl.rb +21 -8
- data/lib/kubernetes-deploy/kubernetes_resource.rb +111 -52
- data/lib/kubernetes-deploy/kubernetes_resource/bugsnag.rb +3 -11
- data/lib/kubernetes-deploy/kubernetes_resource/cloudsql.rb +7 -14
- data/lib/kubernetes-deploy/kubernetes_resource/config_map.rb +5 -9
- data/lib/kubernetes-deploy/kubernetes_resource/deployment.rb +31 -14
- data/lib/kubernetes-deploy/kubernetes_resource/ingress.rb +1 -13
- data/lib/kubernetes-deploy/kubernetes_resource/persistent_volume_claim.rb +2 -9
- data/lib/kubernetes-deploy/kubernetes_resource/pod.rb +48 -22
- data/lib/kubernetes-deploy/kubernetes_resource/pod_disruption_budget.rb +5 -9
- data/lib/kubernetes-deploy/kubernetes_resource/pod_template.rb +5 -9
- data/lib/kubernetes-deploy/kubernetes_resource/redis.rb +9 -15
- data/lib/kubernetes-deploy/kubernetes_resource/service.rb +9 -10
- data/lib/kubernetes-deploy/resource_watcher.rb +22 -10
- data/lib/kubernetes-deploy/restart_task.rb +12 -7
- data/lib/kubernetes-deploy/runner.rb +163 -110
- data/lib/kubernetes-deploy/runner_task.rb +22 -19
- data/lib/kubernetes-deploy/version.rb +1 -1
- metadata +18 -4
- data/lib/kubernetes-deploy/logger.rb +0 -45
- data/lib/kubernetes-deploy/ui_helpers.rb +0 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 60da18d26c2e8a36c8761924b17e1b263eacaf70
|
4
|
+
data.tar.gz: 6f05ac075997226ed8c526f6d02cbb4406617b55
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: da4f1f609882fb90af50e1505a9ca841eb8e6a95de220b3471686425964c953b025858bb86835b9d50f5f6053a77188affaf363d690b2211914a1c3bad4d17e3
|
7
|
+
data.tar.gz: 7fe5caa73866be72e7aa2351a4e0d562d4891eda7fc8261d4d9ceb5b5abc972bc09ca26a67459eec72d2b516bbb77fde10e4d63d78bbe791a93e2b810f5211d1
|
data/exe/kubernetes-deploy
CHANGED
@@ -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
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
data/exe/kubernetes-restart
CHANGED
@@ -12,7 +12,10 @@ ARGV.options do |opts|
|
|
12
12
|
opts.parse!
|
13
13
|
end
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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:
|
21
|
-
context:
|
24
|
+
namespace: namespace,
|
25
|
+
context: context,
|
26
|
+
logger: logger
|
22
27
|
)
|
23
28
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
data/kubernetes-deploy.gemspec
CHANGED
@@ -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"
|
data/lib/kubernetes-deploy.rb
CHANGED
@@ -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/
|
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
|
-
|
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
|
-
|
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
|
-
|
67
|
-
|
68
|
-
|
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
|
-
|
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 =
|
114
|
-
|
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 =
|
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
|
-
|
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
|
-
|
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 =
|
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
|
-
|
5
|
-
def
|
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
|
8
|
-
args.push("--context=#{context}") if
|
19
|
+
args.push("--namespace=#{@namespace}") if use_namespace
|
20
|
+
args.push("--context=#{@context}") if use_context
|
9
21
|
|
10
|
-
|
22
|
+
@logger.debug Shellwords.join(args)
|
11
23
|
out, err, st = Open3.capture3(*args)
|
12
|
-
|
24
|
+
@logger.debug(out.shellescape)
|
25
|
+
|
13
26
|
if !st.success? && log_failure
|
14
|
-
|
15
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
when '
|
40
|
-
|
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
|
53
|
-
# subclasses must also set these
|
54
|
-
@name
|
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
|
-
|
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
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
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
|
122
|
-
|
178
|
+
def timeout_message
|
179
|
+
STANDARD_TIMEOUT_MESSAGE
|
123
180
|
end
|
124
181
|
|
125
|
-
def
|
126
|
-
KubernetesResource.logger.info("[#{@context}][#{@namespace}] #{JSON.dump(status_data)}")
|
182
|
+
def failure_message
|
127
183
|
end
|
128
184
|
|
129
|
-
def
|
130
|
-
|
131
|
-
|
185
|
+
def pretty_status
|
186
|
+
padding = " " * (50 - id.length)
|
187
|
+
msg = exists? ? status : "not found"
|
188
|
+
"#{id}#{padding}#{msg}"
|
189
|
+
end
|
132
190
|
|
133
|
-
|
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
|