tobsch-krane 1.0.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 +7 -0
- data/.buildkite/pipeline.nightly.yml +43 -0
- data/.github/probots.yml +2 -0
- data/.gitignore +20 -0
- data/.rubocop.yml +17 -0
- data/.shopify-build/VERSION +1 -0
- data/.shopify-build/kubernetes-deploy.yml +53 -0
- data/1.0-Upgrade.md +185 -0
- data/CHANGELOG.md +431 -0
- data/CODE_OF_CONDUCT.md +46 -0
- data/CONTRIBUTING.md +164 -0
- data/Gemfile +16 -0
- data/ISSUE_TEMPLATE.md +25 -0
- data/LICENSE.txt +21 -0
- data/README.md +655 -0
- data/Rakefile +36 -0
- data/bin/ci +21 -0
- data/bin/setup +16 -0
- data/bin/test +47 -0
- data/dev.yml +28 -0
- data/dev/flamegraph-from-tests +35 -0
- data/exe/krane +5 -0
- data/krane.gemspec +44 -0
- data/lib/krane.rb +7 -0
- data/lib/krane/bindings_parser.rb +88 -0
- data/lib/krane/cli/deploy_command.rb +75 -0
- data/lib/krane/cli/global_deploy_command.rb +54 -0
- data/lib/krane/cli/krane.rb +91 -0
- data/lib/krane/cli/render_command.rb +41 -0
- data/lib/krane/cli/restart_command.rb +34 -0
- data/lib/krane/cli/run_command.rb +54 -0
- data/lib/krane/cli/version_command.rb +13 -0
- data/lib/krane/cluster_resource_discovery.rb +113 -0
- data/lib/krane/common.rb +23 -0
- data/lib/krane/concerns/template_reporting.rb +29 -0
- data/lib/krane/concurrency.rb +18 -0
- data/lib/krane/container_logs.rb +106 -0
- data/lib/krane/deferred_summary_logging.rb +95 -0
- data/lib/krane/delayed_exceptions.rb +14 -0
- data/lib/krane/deploy_task.rb +363 -0
- data/lib/krane/deploy_task_config_validator.rb +29 -0
- data/lib/krane/duration_parser.rb +27 -0
- data/lib/krane/ejson_secret_provisioner.rb +154 -0
- data/lib/krane/errors.rb +28 -0
- data/lib/krane/formatted_logger.rb +57 -0
- data/lib/krane/global_deploy_task.rb +210 -0
- data/lib/krane/global_deploy_task_config_validator.rb +12 -0
- data/lib/krane/kubeclient_builder.rb +156 -0
- data/lib/krane/kubectl.rb +120 -0
- data/lib/krane/kubernetes_resource.rb +621 -0
- data/lib/krane/kubernetes_resource/cloudsql.rb +43 -0
- data/lib/krane/kubernetes_resource/config_map.rb +22 -0
- data/lib/krane/kubernetes_resource/cron_job.rb +18 -0
- data/lib/krane/kubernetes_resource/custom_resource.rb +87 -0
- data/lib/krane/kubernetes_resource/custom_resource_definition.rb +98 -0
- data/lib/krane/kubernetes_resource/daemon_set.rb +90 -0
- data/lib/krane/kubernetes_resource/deployment.rb +213 -0
- data/lib/krane/kubernetes_resource/horizontal_pod_autoscaler.rb +65 -0
- data/lib/krane/kubernetes_resource/ingress.rb +18 -0
- data/lib/krane/kubernetes_resource/job.rb +60 -0
- data/lib/krane/kubernetes_resource/network_policy.rb +22 -0
- data/lib/krane/kubernetes_resource/persistent_volume_claim.rb +80 -0
- data/lib/krane/kubernetes_resource/pod.rb +269 -0
- data/lib/krane/kubernetes_resource/pod_disruption_budget.rb +23 -0
- data/lib/krane/kubernetes_resource/pod_set_base.rb +71 -0
- data/lib/krane/kubernetes_resource/pod_template.rb +20 -0
- data/lib/krane/kubernetes_resource/replica_set.rb +92 -0
- data/lib/krane/kubernetes_resource/resource_quota.rb +22 -0
- data/lib/krane/kubernetes_resource/role.rb +22 -0
- data/lib/krane/kubernetes_resource/role_binding.rb +22 -0
- data/lib/krane/kubernetes_resource/secret.rb +24 -0
- data/lib/krane/kubernetes_resource/service.rb +104 -0
- data/lib/krane/kubernetes_resource/service_account.rb +22 -0
- data/lib/krane/kubernetes_resource/stateful_set.rb +70 -0
- data/lib/krane/label_selector.rb +42 -0
- data/lib/krane/oj.rb +4 -0
- data/lib/krane/options_helper.rb +39 -0
- data/lib/krane/remote_logs.rb +60 -0
- data/lib/krane/render_task.rb +118 -0
- data/lib/krane/renderer.rb +118 -0
- data/lib/krane/resource_cache.rb +68 -0
- data/lib/krane/resource_deployer.rb +265 -0
- data/lib/krane/resource_watcher.rb +171 -0
- data/lib/krane/restart_task.rb +228 -0
- data/lib/krane/rollout_conditions.rb +103 -0
- data/lib/krane/runner_task.rb +212 -0
- data/lib/krane/runner_task_config_validator.rb +18 -0
- data/lib/krane/statsd.rb +65 -0
- data/lib/krane/task_config.rb +22 -0
- data/lib/krane/task_config_validator.rb +96 -0
- data/lib/krane/template_sets.rb +173 -0
- data/lib/krane/version.rb +4 -0
- data/pull_request_template.md +8 -0
- data/screenshots/deploy-demo.gif +0 -0
- data/screenshots/migrate-logs.png +0 -0
- data/screenshots/missing-secret-fail.png +0 -0
- data/screenshots/success.png +0 -0
- data/screenshots/test-output.png +0 -0
- metadata +375 -0
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Krane
|
3
|
+
class DeployTaskConfigValidator < TaskConfigValidator
|
4
|
+
def initialize(protected_namespaces, prune, *arguments)
|
5
|
+
super(*arguments)
|
6
|
+
@protected_namespaces = protected_namespaces
|
7
|
+
@allow_protected_ns = !protected_namespaces.empty?
|
8
|
+
@prune = prune
|
9
|
+
@validations += %i(validate_protected_namespaces)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def validate_protected_namespaces
|
15
|
+
if @protected_namespaces.include?(namespace)
|
16
|
+
if @allow_protected_ns && @prune
|
17
|
+
@errors << "Refusing to deploy to protected namespace '#{namespace}' with pruning enabled"
|
18
|
+
elsif @allow_protected_ns
|
19
|
+
logger.warn("You're deploying to protected namespace #{namespace}, which cannot be pruned.")
|
20
|
+
logger.warn("Existing resources can only be removed manually with kubectl. " \
|
21
|
+
"Removing templates from the set deployed will have no effect.")
|
22
|
+
logger.warn("***Please do not deploy to #{namespace} unless you really know what you are doing.***")
|
23
|
+
else
|
24
|
+
@errors << "Refusing to deploy to protected namespace '#{namespace}'"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/duration'
|
4
|
+
|
5
|
+
module Krane
|
6
|
+
# This class is a less strict extension of ActiveSupport::Duration::ISO8601Parser.
|
7
|
+
# In addition to full ISO8601 durations, it can parse unprefixed ISO8601 time components (e.g. '1H').
|
8
|
+
# It is also case-insensitive.
|
9
|
+
# For example, this class considers the values "1H", "1h" and "PT1H" to be valid and equivalent.
|
10
|
+
class DurationParser
|
11
|
+
class ParsingError < ArgumentError; end
|
12
|
+
|
13
|
+
def initialize(value)
|
14
|
+
@iso8601_str = value.to_s.strip.upcase
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse!
|
18
|
+
ActiveSupport::Duration.parse("PT#{@iso8601_str}") # By default assume it is just a time component
|
19
|
+
rescue ActiveSupport::Duration::ISO8601Parser::ParsingError
|
20
|
+
begin
|
21
|
+
ActiveSupport::Duration.parse(@iso8601_str)
|
22
|
+
rescue ActiveSupport::Duration::ISO8601Parser::ParsingError => e
|
23
|
+
raise ParsingError, e.message
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'json'
|
3
|
+
require 'base64'
|
4
|
+
require 'open3'
|
5
|
+
require 'krane/kubectl'
|
6
|
+
|
7
|
+
module Krane
|
8
|
+
class EjsonSecretError < FatalDeploymentError
|
9
|
+
def initialize(msg)
|
10
|
+
super("Generation of Kubernetes secrets from ejson failed: #{msg}")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class EjsonSecretProvisioner
|
15
|
+
EJSON_SECRET_ANNOTATION = "kubernetes-deploy.shopify.io/ejson-secret"
|
16
|
+
EJSON_SECRET_KEY = "kubernetes_secrets"
|
17
|
+
EJSON_SECRETS_FILE = "secrets.ejson"
|
18
|
+
EJSON_KEYS_SECRET = "ejson-keys"
|
19
|
+
delegate :namespace, :context, :logger, to: :@task_config
|
20
|
+
|
21
|
+
def initialize(task_config:, ejson_keys_secret:, ejson_file:, statsd_tags:, selector: nil)
|
22
|
+
@ejson_keys_secret = ejson_keys_secret
|
23
|
+
@ejson_file = ejson_file
|
24
|
+
@statsd_tags = statsd_tags
|
25
|
+
@selector = selector
|
26
|
+
@task_config = task_config
|
27
|
+
@kubectl = Kubectl.new(
|
28
|
+
task_config: @task_config,
|
29
|
+
log_failure_by_default: false,
|
30
|
+
output_is_sensitive_default: true # output may contain ejson secrets
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
def resources
|
35
|
+
@resources ||= build_secrets
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def build_secrets
|
41
|
+
unless @ejson_keys_secret
|
42
|
+
raise EjsonSecretError, "Secret #{EJSON_KEYS_SECRET} not provided, cannot decrypt secrets"
|
43
|
+
end
|
44
|
+
return [] unless File.exist?(@ejson_file)
|
45
|
+
with_decrypted_ejson do |decrypted|
|
46
|
+
secrets = decrypted[EJSON_SECRET_KEY]
|
47
|
+
unless secrets.present?
|
48
|
+
logger.warn("#{EJSON_SECRETS_FILE} does not have key #{EJSON_SECRET_KEY}."\
|
49
|
+
"No secrets will be created.")
|
50
|
+
return []
|
51
|
+
end
|
52
|
+
|
53
|
+
secrets.map do |secret_name, secret_spec|
|
54
|
+
validate_secret_spec(secret_name, secret_spec)
|
55
|
+
resource = generate_secret_resource(secret_name, secret_spec["_type"], secret_spec["data"])
|
56
|
+
resource.validate_definition(@kubectl)
|
57
|
+
if resource.validation_failed?
|
58
|
+
raise EjsonSecretError, "Resulting resource Secret/#{secret_name} failed validation"
|
59
|
+
end
|
60
|
+
resource
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def encrypted_ejson
|
66
|
+
@encrypted_ejson ||= load_ejson_from_file
|
67
|
+
end
|
68
|
+
|
69
|
+
def public_key
|
70
|
+
encrypted_ejson["_public_key"]
|
71
|
+
end
|
72
|
+
|
73
|
+
def private_key
|
74
|
+
@private_key ||= fetch_private_key_from_secret
|
75
|
+
end
|
76
|
+
|
77
|
+
def validate_secret_spec(secret_name, spec)
|
78
|
+
errors = []
|
79
|
+
errors << "secret type unspecified" if spec["_type"].blank?
|
80
|
+
errors << "no data provided" if spec["data"].blank?
|
81
|
+
|
82
|
+
unless errors.empty?
|
83
|
+
raise EjsonSecretError, "Ejson incomplete for secret #{secret_name}: #{errors.join(', ')}"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def generate_secret_resource(secret_name, secret_type, data)
|
88
|
+
unless data.is_a?(Hash) && data.values.all? { |v| v.is_a?(String) } # Secret data is map[string]string
|
89
|
+
raise EjsonSecretError, "Data for secret #{secret_name} was invalid. Only key-value pairs are permitted."
|
90
|
+
end
|
91
|
+
encoded_data = data.each_with_object({}) do |(key, value), encoded|
|
92
|
+
# Leading underscores in ejson keys are used to skip encryption of the associated value
|
93
|
+
# To support this ejson feature, we need to exclude these leading underscores from the secret's keys
|
94
|
+
secret_key = key.sub(/\A_/, '')
|
95
|
+
encoded[secret_key] = Base64.strict_encode64(value)
|
96
|
+
end
|
97
|
+
|
98
|
+
labels = { "name" => secret_name }
|
99
|
+
labels.reverse_merge!(@selector.to_h) if @selector
|
100
|
+
|
101
|
+
secret = {
|
102
|
+
'kind' => 'Secret',
|
103
|
+
'apiVersion' => 'v1',
|
104
|
+
'type' => secret_type,
|
105
|
+
'metadata' => {
|
106
|
+
"name" => secret_name,
|
107
|
+
"labels" => labels,
|
108
|
+
"namespace" => namespace,
|
109
|
+
"annotations" => { EJSON_SECRET_ANNOTATION => "true" },
|
110
|
+
},
|
111
|
+
"data" => encoded_data,
|
112
|
+
}
|
113
|
+
|
114
|
+
Krane::Secret.build(
|
115
|
+
namespace: namespace, context: context, logger: logger, definition: secret, statsd_tags: @statsd_tags,
|
116
|
+
)
|
117
|
+
end
|
118
|
+
|
119
|
+
def load_ejson_from_file
|
120
|
+
return {} unless File.exist?(@ejson_file)
|
121
|
+
JSON.parse(File.read(@ejson_file))
|
122
|
+
end
|
123
|
+
|
124
|
+
def with_decrypted_ejson
|
125
|
+
return unless File.exist?(@ejson_file)
|
126
|
+
|
127
|
+
Dir.mktmpdir("ejson_keydir") do |key_dir|
|
128
|
+
File.write(File.join(key_dir, public_key), private_key)
|
129
|
+
decrypted = decrypt_ejson(key_dir)
|
130
|
+
yield decrypted
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def decrypt_ejson(key_dir)
|
135
|
+
# ejson seems to dump both errors and output to STDOUT
|
136
|
+
out_err, st = Open3.capture2e("EJSON_KEYDIR=#{key_dir} ejson decrypt #{@ejson_file}")
|
137
|
+
raise EjsonSecretError, out_err unless st.success?
|
138
|
+
JSON.parse(out_err)
|
139
|
+
rescue JSON::ParserError
|
140
|
+
raise EjsonSecretError, "Failed to parse decrypted ejson"
|
141
|
+
end
|
142
|
+
|
143
|
+
def fetch_private_key_from_secret
|
144
|
+
encoded_private_key = @ejson_keys_secret["data"][public_key]
|
145
|
+
unless encoded_private_key
|
146
|
+
raise EjsonSecretError, "Private key for #{public_key} not found in #{EJSON_KEYS_SECRET} secret"
|
147
|
+
end
|
148
|
+
|
149
|
+
Base64.decode64(encoded_private_key)
|
150
|
+
rescue Kubectl::ResourceNotFoundError
|
151
|
+
raise EjsonSecretError, "Secret/#{EJSON_KEYS_SECRET} is required to decrypt EJSON and could not be found"
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
data/lib/krane/errors.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Krane
|
4
|
+
class FatalDeploymentError < StandardError; end
|
5
|
+
class FatalKubeAPIError < FatalDeploymentError; end
|
6
|
+
class KubectlError < StandardError; end
|
7
|
+
class TaskConfigurationError < FatalDeploymentError; end
|
8
|
+
|
9
|
+
class InvalidTemplateError < FatalDeploymentError
|
10
|
+
attr_reader :content
|
11
|
+
attr_accessor :filename
|
12
|
+
def initialize(err, filename: nil, content: nil)
|
13
|
+
@filename = filename
|
14
|
+
@content = content
|
15
|
+
super(err)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class DeploymentTimeoutError < FatalDeploymentError; end
|
20
|
+
|
21
|
+
class EjsonPrunableError < FatalDeploymentError
|
22
|
+
def initialize
|
23
|
+
super("Found #{KubernetesResource::LAST_APPLIED_ANNOTATION} annotation on " \
|
24
|
+
"#{EjsonSecretProvisioner::EJSON_KEYS_SECRET} secret. " \
|
25
|
+
"krane will not continue since it is extremely unlikely that this secret should be pruned.")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'logger'
|
3
|
+
require 'colorized_string'
|
4
|
+
require 'krane/deferred_summary_logging'
|
5
|
+
|
6
|
+
module Krane
|
7
|
+
class FormattedLogger < Logger
|
8
|
+
include DeferredSummaryLogging
|
9
|
+
|
10
|
+
def self.indent_four(str)
|
11
|
+
" " + str.to_s.gsub("\n", "\n ")
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.build(namespace = nil, context = nil, stream = $stderr, verbose_prefix: false)
|
15
|
+
l = new(stream)
|
16
|
+
l.level = level_from_env
|
17
|
+
|
18
|
+
middle = if verbose_prefix
|
19
|
+
if namespace.blank?
|
20
|
+
raise ArgumentError, 'Must pass a namespace if logging verbosely'
|
21
|
+
end
|
22
|
+
if context.blank?
|
23
|
+
raise ArgumentError, 'Must pass a context if logging verbosely'
|
24
|
+
end
|
25
|
+
|
26
|
+
"[#{context}][#{namespace}]"
|
27
|
+
end
|
28
|
+
|
29
|
+
l.formatter = proc do |severity, datetime, _progname, msg|
|
30
|
+
colorized_line = ColorizedString.new("[#{severity}][#{datetime}]#{middle}\t#{msg}\n")
|
31
|
+
|
32
|
+
case severity
|
33
|
+
when "FATAL"
|
34
|
+
ColorizedString.new("[#{severity}][#{datetime}]#{middle}\t").red + "#{msg}\n"
|
35
|
+
when "ERROR"
|
36
|
+
colorized_line.red
|
37
|
+
when "WARN"
|
38
|
+
colorized_line.yellow
|
39
|
+
else
|
40
|
+
colorized_line
|
41
|
+
end
|
42
|
+
end
|
43
|
+
l
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.level_from_env
|
47
|
+
return ::Logger::DEBUG if ENV["DEBUG"]
|
48
|
+
|
49
|
+
if ENV["LEVEL"]
|
50
|
+
::Logger.const_get(ENV["LEVEL"].upcase)
|
51
|
+
else
|
52
|
+
::Logger::INFO
|
53
|
+
end
|
54
|
+
end
|
55
|
+
private_class_method :level_from_env
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
require 'krane/common'
|
5
|
+
require 'krane/concurrency'
|
6
|
+
require 'krane/resource_cache'
|
7
|
+
require 'krane/kubectl'
|
8
|
+
require 'krane/kubeclient_builder'
|
9
|
+
require 'krane/cluster_resource_discovery'
|
10
|
+
require 'krane/template_sets'
|
11
|
+
require 'krane/resource_deployer'
|
12
|
+
require 'krane/kubernetes_resource'
|
13
|
+
require 'krane/global_deploy_task_config_validator'
|
14
|
+
require 'krane/concerns/template_reporting'
|
15
|
+
|
16
|
+
%w(
|
17
|
+
custom_resource
|
18
|
+
custom_resource_definition
|
19
|
+
).each do |subresource|
|
20
|
+
require "krane/kubernetes_resource/#{subresource}"
|
21
|
+
end
|
22
|
+
|
23
|
+
module Krane
|
24
|
+
# Ship global resources to a context
|
25
|
+
class GlobalDeployTask
|
26
|
+
extend Krane::StatsD::MeasureMethods
|
27
|
+
include TemplateReporting
|
28
|
+
delegate :context, :logger, :global_kinds, to: :@task_config
|
29
|
+
|
30
|
+
# Initializes the deploy task
|
31
|
+
#
|
32
|
+
# @param context [String] Kubernetes context (*required*)
|
33
|
+
# @param global_timeout [Integer] Timeout in seconds
|
34
|
+
# @param selector [Hash] Selector(s) parsed by Krane::LabelSelector (*required*)
|
35
|
+
# @param filenames [Array<String>] An array of filenames and/or directories containing templates (*required*)
|
36
|
+
def initialize(context:, global_timeout: nil, selector: nil, filenames: [], logger: nil)
|
37
|
+
template_paths = filenames.map { |path| File.expand_path(path) }
|
38
|
+
|
39
|
+
@task_config = TaskConfig.new(context, nil, logger)
|
40
|
+
@template_sets = TemplateSets.from_dirs_and_files(paths: template_paths,
|
41
|
+
logger: @task_config.logger, render_erb: false)
|
42
|
+
@global_timeout = global_timeout
|
43
|
+
@selector = selector
|
44
|
+
end
|
45
|
+
|
46
|
+
# Runs the task, returning a boolean representing success or failure
|
47
|
+
#
|
48
|
+
# @return [Boolean]
|
49
|
+
def run(*args)
|
50
|
+
run!(*args)
|
51
|
+
true
|
52
|
+
rescue FatalDeploymentError
|
53
|
+
false
|
54
|
+
end
|
55
|
+
|
56
|
+
# Runs the task, raising exceptions in case of issues
|
57
|
+
#
|
58
|
+
# @param verify_result [Boolean] Wait for completion and verify success
|
59
|
+
# @param prune [Boolean] Enable deletion of resources that match the provided
|
60
|
+
# selector and do not appear in the template dir
|
61
|
+
#
|
62
|
+
# @return [nil]
|
63
|
+
def run!(verify_result: true, prune: true)
|
64
|
+
start = Time.now.utc
|
65
|
+
logger.reset
|
66
|
+
|
67
|
+
logger.phase_heading("Initializing deploy")
|
68
|
+
validate_configuration
|
69
|
+
resources = discover_resources
|
70
|
+
validate_resources(resources)
|
71
|
+
|
72
|
+
logger.phase_heading("Checking initial resource statuses")
|
73
|
+
check_initial_status(resources)
|
74
|
+
|
75
|
+
logger.phase_heading("Deploying all resources")
|
76
|
+
deploy!(resources, verify_result, prune)
|
77
|
+
|
78
|
+
StatsD.client.event("Deployment succeeded",
|
79
|
+
"Successfully deployed all resources to #{context}",
|
80
|
+
alert_type: "success", tags: statsd_tags + %w(status:success))
|
81
|
+
StatsD.client.distribution('all_resources.duration', StatsD.duration(start),
|
82
|
+
tags: statsd_tags << "status:success")
|
83
|
+
logger.print_summary(:success)
|
84
|
+
rescue Krane::DeploymentTimeoutError
|
85
|
+
logger.print_summary(:timed_out)
|
86
|
+
StatsD.client.event("Deployment timed out",
|
87
|
+
"One or more resources failed to deploy to #{context} in time",
|
88
|
+
alert_type: "error", tags: statsd_tags + %w(status:timeout))
|
89
|
+
StatsD.client.distribution('all_resources.duration', StatsD.duration(start),
|
90
|
+
tags: statsd_tags << "status:timeout")
|
91
|
+
raise
|
92
|
+
rescue Krane::FatalDeploymentError => error
|
93
|
+
logger.summary.add_action(error.message) if error.message != error.class.to_s
|
94
|
+
logger.print_summary(:failure)
|
95
|
+
StatsD.client.event("Deployment failed",
|
96
|
+
"One or more resources failed to deploy to #{context}",
|
97
|
+
alert_type: "error", tags: statsd_tags + %w(status:failed))
|
98
|
+
StatsD.client.distribution('all_resources.duration', StatsD.duration(start),
|
99
|
+
tags: statsd_tags << "status:failed")
|
100
|
+
raise
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def deploy!(resources, verify_result, prune)
|
106
|
+
resource_deployer = ResourceDeployer.new(task_config: @task_config,
|
107
|
+
prune_whitelist: prune_whitelist, global_timeout: @global_timeout,
|
108
|
+
selector: @selector, statsd_tags: statsd_tags)
|
109
|
+
resource_deployer.deploy!(resources, verify_result, prune)
|
110
|
+
end
|
111
|
+
|
112
|
+
def validate_configuration
|
113
|
+
task_config_validator = GlobalDeployTaskConfigValidator.new(@task_config,
|
114
|
+
kubectl, kubeclient_builder)
|
115
|
+
errors = []
|
116
|
+
errors += task_config_validator.errors
|
117
|
+
errors += @template_sets.validate
|
118
|
+
errors << "Selector is required" unless @selector.to_h.present?
|
119
|
+
unless errors.empty?
|
120
|
+
add_para_from_list(logger: logger, action: "Configuration invalid", enum: errors)
|
121
|
+
raise TaskConfigurationError
|
122
|
+
end
|
123
|
+
|
124
|
+
logger.info("Using resource selector #{@selector}")
|
125
|
+
logger.info("All required parameters and files are present")
|
126
|
+
end
|
127
|
+
measure_method(:validate_configuration)
|
128
|
+
|
129
|
+
def validate_resources(resources)
|
130
|
+
validate_globals(resources)
|
131
|
+
|
132
|
+
Concurrency.split_across_threads(resources) do |r|
|
133
|
+
r.validate_definition(@kubectl, selector: @selector)
|
134
|
+
end
|
135
|
+
|
136
|
+
resources.select(&:has_warnings?).each do |resource|
|
137
|
+
record_warnings(logger: logger, warning: resource.validation_warning_msg,
|
138
|
+
filename: File.basename(resource.file_path))
|
139
|
+
end
|
140
|
+
|
141
|
+
failed_resources = resources.select(&:validation_failed?)
|
142
|
+
if failed_resources.present?
|
143
|
+
failed_resources.each do |r|
|
144
|
+
content = File.read(r.file_path) if File.file?(r.file_path) && !r.sensitive_template_content?
|
145
|
+
record_invalid_template(logger: logger, err: r.validation_error_msg,
|
146
|
+
filename: File.basename(r.file_path), content: content)
|
147
|
+
end
|
148
|
+
raise FatalDeploymentError, "Template validation failed"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
measure_method(:validate_resources)
|
152
|
+
|
153
|
+
def validate_globals(resources)
|
154
|
+
return unless (namespaced = resources.reject(&:global?).presence)
|
155
|
+
namespaced_names = namespaced.map do |resource|
|
156
|
+
"#{resource.name} (#{resource.type}) in #{File.basename(resource.file_path)}"
|
157
|
+
end
|
158
|
+
namespaced_names = FormattedLogger.indent_four(namespaced_names.join("\n"))
|
159
|
+
|
160
|
+
logger.summary.add_paragraph(ColorizedString.new("Namespaced resources:\n#{namespaced_names}").yellow)
|
161
|
+
raise FatalDeploymentError, "This command cannot deploy namespaced resources. Use DeployTask instead."
|
162
|
+
end
|
163
|
+
|
164
|
+
def discover_resources
|
165
|
+
logger.info("Discovering resources:")
|
166
|
+
resources = []
|
167
|
+
crds_by_kind = cluster_resource_discoverer.crds.map { |crd| [crd.name, crd] }.to_h
|
168
|
+
@template_sets.with_resource_definitions do |r_def|
|
169
|
+
crd = crds_by_kind[r_def["kind"]]&.first
|
170
|
+
r = KubernetesResource.build(context: context, logger: logger, definition: r_def,
|
171
|
+
crd: crd, global_names: global_kinds, statsd_tags: statsd_tags)
|
172
|
+
resources << r
|
173
|
+
logger.info(" - #{r.id}")
|
174
|
+
end
|
175
|
+
|
176
|
+
resources.sort
|
177
|
+
rescue InvalidTemplateError => e
|
178
|
+
record_invalid_template(logger: logger, err: e.message, filename: e.filename, content: e.content)
|
179
|
+
raise FatalDeploymentError, "Failed to parse template"
|
180
|
+
end
|
181
|
+
measure_method(:discover_resources)
|
182
|
+
|
183
|
+
def cluster_resource_discoverer
|
184
|
+
@cluster_resource_discoverer ||= ClusterResourceDiscovery.new(task_config: @task_config)
|
185
|
+
end
|
186
|
+
|
187
|
+
def statsd_tags
|
188
|
+
%W(context:#{context})
|
189
|
+
end
|
190
|
+
|
191
|
+
def kubectl
|
192
|
+
@kubectl ||= Kubectl.new(task_config: @task_config, log_failure_by_default: true)
|
193
|
+
end
|
194
|
+
|
195
|
+
def kubeclient_builder
|
196
|
+
@kubeclient_builder ||= KubeclientBuilder.new
|
197
|
+
end
|
198
|
+
|
199
|
+
def prune_whitelist
|
200
|
+
cluster_resource_discoverer.prunable_resources(namespaced: false)
|
201
|
+
end
|
202
|
+
|
203
|
+
def check_initial_status(resources)
|
204
|
+
cache = ResourceCache.new(@task_config)
|
205
|
+
Concurrency.split_across_threads(resources) { |r| r.sync(cache) }
|
206
|
+
resources.each { |r| logger.info(r.pretty_status) }
|
207
|
+
end
|
208
|
+
measure_method(:check_initial_status, "initial_status.duration")
|
209
|
+
end
|
210
|
+
end
|