tobsch-krane 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Krane
|
3
|
+
class RolloutConditionsError < StandardError
|
4
|
+
end
|
5
|
+
|
6
|
+
class RolloutConditions
|
7
|
+
VALID_FAILURE_CONDITION_KEYS = [:path, :value, :error_msg_path, :custom_error_msg]
|
8
|
+
VALID_SUCCESS_CONDITION_KEYS = [:path, :value]
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def from_annotation(conditions_string)
|
12
|
+
return new(default_conditions) if conditions_string.downcase.strip == "true"
|
13
|
+
|
14
|
+
conditions = JSON.parse(conditions_string).slice('success_conditions', 'failure_conditions')
|
15
|
+
conditions.deep_symbolize_keys!
|
16
|
+
|
17
|
+
# Create JsonPath objects
|
18
|
+
conditions[:success_conditions]&.each do |query|
|
19
|
+
query.slice!(*VALID_SUCCESS_CONDITION_KEYS)
|
20
|
+
query[:path] = JsonPath.new(query[:path]) if query.key?(:path)
|
21
|
+
end
|
22
|
+
conditions[:failure_conditions]&.each do |query|
|
23
|
+
query.slice!(*VALID_FAILURE_CONDITION_KEYS)
|
24
|
+
query[:path] = JsonPath.new(query[:path]) if query.key?(:path)
|
25
|
+
query[:error_msg_path] = JsonPath.new(query[:error_msg_path]) if query.key?(:error_msg_path)
|
26
|
+
end
|
27
|
+
|
28
|
+
new(conditions)
|
29
|
+
rescue JSON::ParserError => e
|
30
|
+
raise RolloutConditionsError, "Rollout conditions are not valid JSON: #{e}"
|
31
|
+
rescue StandardError => e
|
32
|
+
raise RolloutConditionsError,
|
33
|
+
"Error parsing rollout conditions. " \
|
34
|
+
"This is most likely caused by an invalid JsonPath expression. Failed with: #{e}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def default_conditions
|
38
|
+
{
|
39
|
+
success_conditions: [
|
40
|
+
{
|
41
|
+
path: JsonPath.new('$.status.conditions[?(@.type == "Ready")].status'),
|
42
|
+
value: "True",
|
43
|
+
},
|
44
|
+
],
|
45
|
+
failure_conditions: [
|
46
|
+
{
|
47
|
+
path: JsonPath.new('$.status.conditions[?(@.type == "Failed")].status'),
|
48
|
+
value: "True",
|
49
|
+
error_msg_path: JsonPath.new('$.status.conditions[?(@.type == "Failed")].message'),
|
50
|
+
},
|
51
|
+
],
|
52
|
+
}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def initialize(conditions)
|
57
|
+
@success_conditions = conditions.fetch(:success_conditions, [])
|
58
|
+
@failure_conditions = conditions.fetch(:failure_conditions, [])
|
59
|
+
end
|
60
|
+
|
61
|
+
def rollout_successful?(instance_data)
|
62
|
+
@success_conditions.all? do |query|
|
63
|
+
query[:path].first(instance_data) == query[:value]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def rollout_failed?(instance_data)
|
68
|
+
@failure_conditions.any? do |query|
|
69
|
+
query[:path].first(instance_data) == query[:value]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def failure_messages(instance_data)
|
74
|
+
@failure_conditions.map do |query|
|
75
|
+
next unless query[:path].first(instance_data) == query[:value]
|
76
|
+
query[:custom_error_msg].presence || query[:error_msg_path]&.first(instance_data)
|
77
|
+
end.compact
|
78
|
+
end
|
79
|
+
|
80
|
+
def validate!
|
81
|
+
errors = validate_conditions(@success_conditions, 'success_conditions')
|
82
|
+
errors += validate_conditions(@failure_conditions, 'failure_conditions', required: false)
|
83
|
+
raise RolloutConditionsError, errors.join(", ") unless errors.empty?
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def validate_conditions(conditions, source_key, required: true)
|
89
|
+
return [] unless conditions.present? || required
|
90
|
+
errors = []
|
91
|
+
errors << "#{source_key} should be Array but found #{conditions.class}" unless conditions.is_a?(Array)
|
92
|
+
return errors if errors.present?
|
93
|
+
errors << "#{source_key} must contain at least one entry" if conditions.empty?
|
94
|
+
return errors if errors.present?
|
95
|
+
|
96
|
+
conditions.each do |query|
|
97
|
+
missing = [:path, :value].reject { |k| query.key?(k) }
|
98
|
+
errors << "Missing required key(s) for #{source_key.singularize}: #{missing}" if missing.present?
|
99
|
+
end
|
100
|
+
errors
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
require 'krane/common'
|
5
|
+
require 'krane/kubeclient_builder'
|
6
|
+
require 'krane/kubectl'
|
7
|
+
require 'krane/resource_cache'
|
8
|
+
require 'krane/resource_watcher'
|
9
|
+
require 'krane/kubernetes_resource'
|
10
|
+
require 'krane/kubernetes_resource/pod'
|
11
|
+
require 'krane/runner_task_config_validator'
|
12
|
+
|
13
|
+
module Krane
|
14
|
+
# Run a pod that exits upon completing a task
|
15
|
+
class RunnerTask
|
16
|
+
class TaskTemplateMissingError < TaskConfigurationError; end
|
17
|
+
|
18
|
+
attr_reader :pod_name
|
19
|
+
|
20
|
+
# Initializes the runner task
|
21
|
+
#
|
22
|
+
# @param namespace [String] Kubernetes namespace (*required*)
|
23
|
+
# @param context [String] Kubernetes context / cluster (*required*)
|
24
|
+
# @param logger [Object] Logger object (defaults to an instance of Krane::FormattedLogger)
|
25
|
+
# @param global_timeout [Integer] Timeout in seconds
|
26
|
+
def initialize(namespace:, context:, logger: nil, global_timeout: nil)
|
27
|
+
@logger = logger || Krane::FormattedLogger.build(namespace, context)
|
28
|
+
@task_config = Krane::TaskConfig.new(context, namespace, @logger)
|
29
|
+
@namespace = namespace
|
30
|
+
@context = context
|
31
|
+
@global_timeout = global_timeout
|
32
|
+
end
|
33
|
+
|
34
|
+
# Runs the task, returning a boolean representing success or failure
|
35
|
+
#
|
36
|
+
# @return [Boolean]
|
37
|
+
def run(*args)
|
38
|
+
run!(*args)
|
39
|
+
true
|
40
|
+
rescue DeploymentTimeoutError, FatalDeploymentError
|
41
|
+
false
|
42
|
+
end
|
43
|
+
|
44
|
+
# Runs the task, raising exceptions in case of issues
|
45
|
+
#
|
46
|
+
# @param template [String] The filename of the template you'll be rendering (*required*)
|
47
|
+
# @param command [Array<String>] Override the default command in the container image
|
48
|
+
# @param arguments [Array<String>] Override the default arguments for the command
|
49
|
+
# @param env_vars [Array<String>] List of env vars
|
50
|
+
# @param verify_result [Boolean] Wait for completion and verify pod success
|
51
|
+
#
|
52
|
+
# @return [nil]
|
53
|
+
def run!(template:, command:, arguments:, env_vars: [], verify_result: true)
|
54
|
+
start = Time.now.utc
|
55
|
+
@logger.reset
|
56
|
+
|
57
|
+
@logger.phase_heading("Initializing task")
|
58
|
+
|
59
|
+
@logger.info("Validating configuration")
|
60
|
+
verify_config!(template)
|
61
|
+
@logger.info("Using namespace '#{@namespace}' in context '#{@context}'")
|
62
|
+
|
63
|
+
pod = build_pod(template, command, arguments, env_vars, verify_result)
|
64
|
+
validate_pod(pod)
|
65
|
+
|
66
|
+
@logger.phase_heading("Running pod")
|
67
|
+
create_pod(pod)
|
68
|
+
|
69
|
+
if verify_result
|
70
|
+
@logger.phase_heading("Streaming logs")
|
71
|
+
watch_pod(pod)
|
72
|
+
else
|
73
|
+
record_status_once(pod)
|
74
|
+
end
|
75
|
+
StatsD.client.distribution('task_runner.duration', StatsD.duration(start), tags: statsd_tags('success'))
|
76
|
+
@logger.print_summary(:success)
|
77
|
+
rescue DeploymentTimeoutError
|
78
|
+
StatsD.client.distribution('task_runner.duration', StatsD.duration(start), tags: statsd_tags('timeout'))
|
79
|
+
@logger.print_summary(:timed_out)
|
80
|
+
raise
|
81
|
+
rescue FatalDeploymentError
|
82
|
+
StatsD.client.distribution('task_runner.duration', StatsD.duration(start), tags: statsd_tags('failure'))
|
83
|
+
@logger.print_summary(:failure)
|
84
|
+
raise
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def create_pod(pod)
|
90
|
+
@logger.info("Creating pod '#{pod.name}'")
|
91
|
+
pod.deploy_started_at = Time.now.utc
|
92
|
+
kubeclient.create_pod(pod.to_kubeclient_resource)
|
93
|
+
@pod_name = pod.name
|
94
|
+
@logger.info("Pod creation succeeded")
|
95
|
+
rescue Kubeclient::HttpError => e
|
96
|
+
msg = "Failed to create pod: #{e.class.name}: #{e.message}"
|
97
|
+
@logger.summary.add_paragraph(msg)
|
98
|
+
raise FatalDeploymentError, msg
|
99
|
+
end
|
100
|
+
|
101
|
+
def build_pod(template_name, command, args, env_vars, verify_result)
|
102
|
+
task_template = get_template(template_name)
|
103
|
+
@logger.info("Using template '#{template_name}'")
|
104
|
+
pod_template = build_pod_definition(task_template)
|
105
|
+
set_container_overrides!(pod_template, command, args, env_vars)
|
106
|
+
ensure_valid_restart_policy!(pod_template, verify_result)
|
107
|
+
Pod.new(namespace: @namespace, context: @context, logger: @logger, stream_logs: true,
|
108
|
+
definition: pod_template.to_hash.deep_stringify_keys, statsd_tags: [])
|
109
|
+
end
|
110
|
+
|
111
|
+
def validate_pod(pod)
|
112
|
+
pod.validate_definition(kubectl)
|
113
|
+
end
|
114
|
+
|
115
|
+
def watch_pod(pod)
|
116
|
+
rw = ResourceWatcher.new(resources: [pod], timeout: @global_timeout,
|
117
|
+
operation_name: "run", task_config: @task_config)
|
118
|
+
rw.run(delay_sync: 1, reminder_interval: 30.seconds)
|
119
|
+
raise DeploymentTimeoutError if pod.deploy_timed_out?
|
120
|
+
raise FatalDeploymentError if pod.deploy_failed?
|
121
|
+
end
|
122
|
+
|
123
|
+
def record_status_once(pod)
|
124
|
+
cache = ResourceCache.new(@task_config)
|
125
|
+
pod.sync(cache)
|
126
|
+
warning = <<~STRING
|
127
|
+
#{ColorizedString.new('Result verification is disabled for this task.').yellow}
|
128
|
+
The following status was observed immediately after pod creation:
|
129
|
+
#{pod.pretty_status}
|
130
|
+
STRING
|
131
|
+
@logger.summary.add_paragraph(warning)
|
132
|
+
end
|
133
|
+
|
134
|
+
def verify_config!(task_template)
|
135
|
+
task_config_validator = RunnerTaskConfigValidator.new(task_template, @task_config, kubectl,
|
136
|
+
kubeclient_builder)
|
137
|
+
unless task_config_validator.valid?
|
138
|
+
@logger.summary.add_action("Configuration invalid")
|
139
|
+
@logger.summary.add_paragraph([task_config_validator.errors].map { |err| "- #{err}" }.join("\n"))
|
140
|
+
raise Krane::TaskConfigurationError
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def get_template(template_name)
|
145
|
+
pod_template = kubeclient.get_pod_template(template_name, @namespace)
|
146
|
+
pod_template.template
|
147
|
+
rescue Kubeclient::ResourceNotFoundError
|
148
|
+
msg = "Pod template `#{template_name}` not found in namespace `#{@namespace}`, context `#{@context}`"
|
149
|
+
@logger.summary.add_paragraph(msg)
|
150
|
+
raise TaskTemplateMissingError, msg
|
151
|
+
rescue Kubeclient::HttpError => error
|
152
|
+
raise FatalKubeAPIError, "Error retrieving pod template: #{error.class.name}: #{error.message}"
|
153
|
+
end
|
154
|
+
|
155
|
+
def build_pod_definition(base_template)
|
156
|
+
pod_definition = base_template.dup
|
157
|
+
pod_definition.kind = 'Pod'
|
158
|
+
pod_definition.apiVersion = 'v1'
|
159
|
+
pod_definition.metadata.namespace = @namespace
|
160
|
+
|
161
|
+
unique_name = pod_definition.metadata.name + "-" + SecureRandom.hex(8)
|
162
|
+
@logger.warn("Name is too long, using '#{unique_name[0..62]}'") if unique_name.length > 63
|
163
|
+
pod_definition.metadata.name = unique_name[0..62]
|
164
|
+
|
165
|
+
pod_definition
|
166
|
+
end
|
167
|
+
|
168
|
+
def set_container_overrides!(pod_definition, command, args, env_vars)
|
169
|
+
container = pod_definition.spec.containers.find { |cont| cont.name == 'task-runner' }
|
170
|
+
if container.nil?
|
171
|
+
message = "Pod spec does not contain a template container called 'task-runner'"
|
172
|
+
@logger.summary.add_paragraph(message)
|
173
|
+
raise TaskConfigurationError, message
|
174
|
+
end
|
175
|
+
|
176
|
+
container.command = command if command
|
177
|
+
container.args = args if args
|
178
|
+
|
179
|
+
env_args = env_vars.map do |env|
|
180
|
+
key, value = env.split('=', 2)
|
181
|
+
{ name: key, value: value }
|
182
|
+
end
|
183
|
+
container.env ||= []
|
184
|
+
container.env = container.env.map(&:to_h) + env_args
|
185
|
+
end
|
186
|
+
|
187
|
+
def ensure_valid_restart_policy!(template, verify)
|
188
|
+
restart_policy = template.spec.restartPolicy
|
189
|
+
if verify && restart_policy != "Never"
|
190
|
+
@logger.warn("Changed Pod RestartPolicy from '#{restart_policy}' to 'Never'. Disable "\
|
191
|
+
"result verification to use '#{restart_policy}'.")
|
192
|
+
template.spec.restartPolicy = "Never"
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def kubectl
|
197
|
+
@kubectl ||= Kubectl.new(task_config: @task_config, log_failure_by_default: true)
|
198
|
+
end
|
199
|
+
|
200
|
+
def kubeclient
|
201
|
+
@kubeclient ||= kubeclient_builder.build_v1_kubeclient(@context)
|
202
|
+
end
|
203
|
+
|
204
|
+
def kubeclient_builder
|
205
|
+
@kubeclient_builder ||= KubeclientBuilder.new
|
206
|
+
end
|
207
|
+
|
208
|
+
def statsd_tags(status)
|
209
|
+
%W(namespace:#{@namespace} context:#{@context} status:#{status})
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Krane
|
3
|
+
class RunnerTaskConfigValidator < TaskConfigValidator
|
4
|
+
def initialize(template, *arguments)
|
5
|
+
super(*arguments)
|
6
|
+
@template = template
|
7
|
+
@validations += %i(validate_template)
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def validate_template
|
13
|
+
if @template.blank?
|
14
|
+
@errors << "Task template name can't be nil"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/krane/statsd.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'statsd-instrument'
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module Krane
|
6
|
+
class StatsD
|
7
|
+
PREFIX = "Krane"
|
8
|
+
|
9
|
+
def self.duration(start_time)
|
10
|
+
(Time.now.utc - start_time).round(1)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.client
|
14
|
+
@client ||= begin
|
15
|
+
sink = if ::StatsD::Instrument::Environment.current.env.fetch('STATSD_ENV', nil) == 'development'
|
16
|
+
::StatsD::Instrument::LogSink.new(Logger.new($stderr))
|
17
|
+
elsif (addr = ::StatsD::Instrument::Environment.current.env.fetch('STATSD_ADDR', nil))
|
18
|
+
::StatsD::Instrument::UDPSink.for_addr(addr)
|
19
|
+
else
|
20
|
+
::StatsD::Instrument::NullSink.new
|
21
|
+
end
|
22
|
+
::StatsD::Instrument::Client.new(prefix: PREFIX, sink: sink, default_sample_rate: 1.0)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module MeasureMethods
|
27
|
+
def measure_method(method_name, metric = nil)
|
28
|
+
unless method_defined?(method_name) || private_method_defined?(method_name)
|
29
|
+
raise NotImplementedError, "Cannot instrument undefined method #{method_name}"
|
30
|
+
end
|
31
|
+
|
32
|
+
unless const_defined?("InstrumentationProxy")
|
33
|
+
const_set("InstrumentationProxy", Module.new)
|
34
|
+
should_prepend = true
|
35
|
+
end
|
36
|
+
|
37
|
+
metric ||= "#{method_name}.duration"
|
38
|
+
self::InstrumentationProxy.send(:define_method, method_name) do |*args, &block|
|
39
|
+
begin
|
40
|
+
start_time = Time.now.utc
|
41
|
+
super(*args, &block)
|
42
|
+
rescue
|
43
|
+
error = true
|
44
|
+
raise
|
45
|
+
ensure
|
46
|
+
dynamic_tags = send(:statsd_tags) if respond_to?(:statsd_tags, true)
|
47
|
+
dynamic_tags ||= {}
|
48
|
+
if error
|
49
|
+
dynamic_tags[:error] = error if dynamic_tags.is_a?(Hash)
|
50
|
+
dynamic_tags << "error:#{error}" if dynamic_tags.is_a?(Array)
|
51
|
+
end
|
52
|
+
|
53
|
+
Krane::StatsD.client.distribution(
|
54
|
+
metric,
|
55
|
+
Krane::StatsD.duration(start_time),
|
56
|
+
tags: dynamic_tags
|
57
|
+
)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
prepend(self::InstrumentationProxy) if should_prepend
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'krane/cluster_resource_discovery'
|
4
|
+
|
5
|
+
module Krane
|
6
|
+
class TaskConfig
|
7
|
+
attr_reader :context, :namespace, :logger
|
8
|
+
|
9
|
+
def initialize(context, namespace, logger = nil)
|
10
|
+
@context = context
|
11
|
+
@namespace = namespace
|
12
|
+
@logger = logger || FormattedLogger.build(@namespace, @context)
|
13
|
+
end
|
14
|
+
|
15
|
+
def global_kinds
|
16
|
+
@global_kinds ||= begin
|
17
|
+
cluster_resource_discoverer = ClusterResourceDiscovery.new(task_config: self)
|
18
|
+
cluster_resource_discoverer.fetch_resources(namespaced: false).map { |g| g["kind"] }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Krane
|
3
|
+
class TaskConfigValidator
|
4
|
+
DEFAULT_VALIDATIONS = %i(
|
5
|
+
validate_kubeconfig
|
6
|
+
validate_context_exists_in_kubeconfig
|
7
|
+
validate_context_reachable
|
8
|
+
validate_server_version
|
9
|
+
validate_namespace_exists
|
10
|
+
).freeze
|
11
|
+
|
12
|
+
delegate :context, :namespace, :logger, to: :@task_config
|
13
|
+
|
14
|
+
def initialize(task_config, kubectl, kubeclient_builder, only: nil)
|
15
|
+
@task_config = task_config
|
16
|
+
@kubectl = kubectl
|
17
|
+
@kubeclient_builder = kubeclient_builder
|
18
|
+
@errors = nil
|
19
|
+
@validations = only || DEFAULT_VALIDATIONS
|
20
|
+
end
|
21
|
+
|
22
|
+
def valid?
|
23
|
+
@errors = []
|
24
|
+
@validations.each do |validator_name|
|
25
|
+
break if @errors.present?
|
26
|
+
send(validator_name)
|
27
|
+
end
|
28
|
+
@errors.empty?
|
29
|
+
end
|
30
|
+
|
31
|
+
def errors
|
32
|
+
valid?
|
33
|
+
@errors
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def validate_kubeconfig
|
39
|
+
@errors += @kubeclient_builder.validate_config_files
|
40
|
+
end
|
41
|
+
|
42
|
+
def validate_context_exists_in_kubeconfig
|
43
|
+
unless context.present?
|
44
|
+
return @errors << "Context can not be blank"
|
45
|
+
end
|
46
|
+
|
47
|
+
_, err, st = @kubectl.run("config", "get-contexts", context, "-o", "name",
|
48
|
+
use_namespace: false, use_context: false, log_failure: false)
|
49
|
+
|
50
|
+
unless st.success?
|
51
|
+
@errors << if err.match("error: context #{context} not found")
|
52
|
+
"Context #{context} missing from your kubeconfig file(s)"
|
53
|
+
else
|
54
|
+
"Something went wrong. #{err} "
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def validate_context_reachable
|
60
|
+
_, err, st = @kubectl.run("get", "namespaces", "-o", "name",
|
61
|
+
use_namespace: false, log_failure: false)
|
62
|
+
|
63
|
+
unless st.success?
|
64
|
+
@errors << "Something went wrong connecting to #{context}. #{err} "
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def validate_namespace_exists
|
69
|
+
unless namespace.present?
|
70
|
+
return @errors << "Namespace can not be blank"
|
71
|
+
end
|
72
|
+
|
73
|
+
_, err, st = @kubectl.run("get", "namespace", "-o", "name", namespace,
|
74
|
+
use_namespace: false, log_failure: false)
|
75
|
+
|
76
|
+
unless st.success?
|
77
|
+
@errors << if err.match("Error from server [(]NotFound[)]: namespace")
|
78
|
+
"Could not find Namespace: #{namespace} in Context: #{context}"
|
79
|
+
else
|
80
|
+
"Could not connect to kubernetes cluster. #{err}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def validate_server_version
|
86
|
+
if @kubectl.server_version < Gem::Version.new(MIN_KUBE_VERSION)
|
87
|
+
logger.warn(server_version_warning(@kubectl.server_version))
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def server_version_warning(server_version)
|
92
|
+
"Minimum cluster version requirement of #{MIN_KUBE_VERSION} not met. "\
|
93
|
+
"Using #{server_version} could result in unexpected behavior as it is no longer tested against"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|