class Krane::KubernetesResource
Constants
- DEBUG_RESOURCE_NOT_FOUND_MESSAGE
- DISABLED_EVENT_INFO_MESSAGE
- DISABLED_LOG_INFO_MESSAGE
- DISABLE_FETCHING_EVENT_INFO
- DISABLE_FETCHING_LOG_INFO
- GLOBAL
- LAST_APPLIED_ANNOTATION
- LOG_LINE_COUNT
- SENSITIVE_TEMPLATE_CONTENT
- SERVER_DRY_RUNNABLE
- SERVER_DRY_RUN_DISABLED_ERROR
- STANDARD_TIMEOUT_MESSAGE
- TIMEOUT
- TIMEOUT_OVERRIDE_ANNOTATION
- TIMEOUT_OVERRIDE_ANNOTATION_DEPRECATED
- TIMEOUT_OVERRIDE_ANNOTATION_SUFFIX
- UNUSUAL_FAILURE_MESSAGE
Attributes
context[R]
deploy_started_at[W]
global[W]
name[R]
namespace[R]
type[W]
Public Class Methods
build(namespace: nil, context:, definition:, logger:, statsd_tags:, crd: nil, global_names: [])
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 43 def build(namespace: nil, context:, definition:, logger:, statsd_tags:, crd: nil, global_names: []) validate_definition_essentials(definition) opts = { namespace: namespace, context: context, definition: definition, logger: logger, statsd_tags: statsd_tags } if (klass = class_for_kind(definition["kind"])) return klass.new(**opts) end if crd CustomResource.new(crd: crd, **opts) else type = definition["kind"] inst = new(**opts) inst.type = type inst.global = global_names.map(&:downcase).include?(type.downcase) inst end end
class_for_kind(kind)
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 61 def class_for_kind(kind) if Krane.const_defined?(kind) Krane.const_get(kind) end rescue NameError nil end
kind()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 73 def kind name.demodulize end
new(namespace:, context:, definition:, logger:, statsd_tags: [])
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 114 def initialize(namespace:, context:, definition:, logger:, statsd_tags: []) # subclasses must also set these if they define their own initializer @name = (definition.dig("metadata", "name") || definition.dig("metadata", "generateName")).to_s @optional_statsd_tags = statsd_tags @namespace = namespace @context = context @logger = logger @definition = definition @statsd_report_done = false @disappeared = false @validation_errors = [] @validation_warnings = [] @instance_data = {} @server_dry_run_validated = false end
timeout()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 69 def timeout self::TIMEOUT end
Private Class Methods
validate_definition_essentials(definition)
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 79 def validate_definition_essentials(definition) debug_content = <<~STRING apiVersion: #{definition.fetch('apiVersion', '<missing>')} kind: #{definition.fetch('kind', '<missing>')} metadata: #{definition.fetch('metadata', {})} <Template body suppressed because content sensitivity could not be determined.> STRING if definition["kind"].blank? raise InvalidTemplateError.new("Template is missing required field 'kind'", content: debug_content) end if definition.dig('metadata', 'name').blank? && definition.dig('metadata', 'generateName').blank? raise InvalidTemplateError.new("Template must specify one of 'metadata.name' or 'metadata.generateName'", content: debug_content) end end
Public Instance Methods
<=>(other)
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 164 def <=>(other) id <=> other.id end
after_sync()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 179 def after_sync end
current_generation()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 211 def current_generation return -1 unless exists? # must be different default than observed_generation @instance_data.dig("metadata", "generation") end
debug_message(cause = nil, info_hash = {})
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 253 def debug_message(cause = nil, info_hash = {}) helpful_info = [] if cause == :gave_up debug_heading = ColorizedString.new("#{id}: GLOBAL WATCH TIMEOUT (#{info_hash[:timeout]} seconds)").yellow helpful_info << "If you expected it to take longer than #{info_hash[:timeout]} seconds for your deploy"\ " to roll out, increase --max-watch-seconds." elsif deploy_failed? debug_heading = ColorizedString.new("#{id}: FAILED").red helpful_info << failure_message if failure_message.present? elsif deploy_timed_out? debug_heading = ColorizedString.new("#{id}: TIMED OUT (#{pretty_timeout_type})").yellow helpful_info << timeout_message if timeout_message.present? else # Arriving in debug_message when we neither failed nor timed out is very unexpected. Dump all available info. debug_heading = ColorizedString.new("#{id}: MONITORING ERROR").red helpful_info << failure_message if failure_message.present? helpful_info << timeout_message if timeout_message.present? && timeout_message != STANDARD_TIMEOUT_MESSAGE end final_status = " - Final status: #{status}" final_status = "\n#{final_status}" if helpful_info.present? && !helpful_info.last.end_with?("\n") helpful_info.prepend(debug_heading) helpful_info << final_status if @debug_events.present? helpful_info << " - Events (common success events excluded):" @debug_events.each do |identifier, event_hashes| event_hashes.each { |event| helpful_info << " [#{identifier}]\t#{event}" } end elsif ENV[DISABLE_FETCHING_EVENT_INFO] helpful_info << " - Events: #{DISABLED_EVENT_INFO_MESSAGE}" else helpful_info << " - Events: #{DEBUG_RESOURCE_NOT_FOUND_MESSAGE}" end if print_debug_logs? if ENV[DISABLE_FETCHING_LOG_INFO] helpful_info << " - Logs: #{DISABLED_LOG_INFO_MESSAGE}" elsif @debug_logs.blank? helpful_info << " - Logs: #{DEBUG_RESOURCE_NOT_FOUND_MESSAGE}" else container_logs = @debug_logs.container_logs.sort_by { |c| c.lines.length } container_logs.each do |logs| if logs.empty? helpful_info << " - Logs from container '#{logs.container_name}': #{DEBUG_RESOURCE_NOT_FOUND_MESSAGE}" next end if logs.lines.length == ContainerLogs::DEFAULT_LINE_LIMIT truncated = " (last #{ContainerLogs::DEFAULT_LINE_LIMIT} lines shown)" end helpful_info << " - Logs from container '#{logs.container_name}'#{truncated}:" logs.lines.each do |line| helpful_info << " #{line}" end end end end helpful_info.join("\n") end
deploy_failed?()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 190 def deploy_failed? false end
deploy_method()
click to toggle source
Expected values: :apply, :create, :replace, :replace_force
# File lib/krane/kubernetes_resource.rb, line 240 def deploy_method if @definition.dig("metadata", "name").blank? && uses_generate_name? :create else :apply end end
deploy_started?()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 194 def deploy_started? @deploy_started_at.present? end
deploy_succeeded?()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 198 def deploy_succeeded? return false unless deploy_started? unless @success_assumption_warning_shown @logger.warn("Don't know how to monitor resources of type #{type}. Assuming #{id} deployed successfully.") @success_assumption_warning_shown = true end true end
deploy_timed_out?()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 234 def deploy_timed_out? return false unless deploy_started? !deploy_succeeded? && !deploy_failed? && (Time.now.utc - @deploy_started_at > timeout) end
disappeared?()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 186 def disappeared? @disappeared end
exists?()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 207 def exists? @instance_data.present? end
failure_message()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 338 def failure_message end
fetch_events(kubectl)
click to toggle source
Returns a hash in the following format: {
"pod/web-1" => [ "Pulling: pulling image "hello-world:latest" (1 events)", "Pulled: Successfully pulled image "hello-world:latest" (1 events)" ]
}
# File lib/krane/kubernetes_resource.rb, line 322 def fetch_events(kubectl) return {} unless exists? out, _err, st = kubectl.run("get", "events", "--output=go-template=#{Event.go_template_for(type, name)}", log_failure: false, use_namespace: !global?) return {} unless st.success? event_collector = Hash.new { |hash, key| hash[key] = [] } Event.extract_all_from_go_template_blob(out).each_with_object(event_collector) do |candidate, events| events[id] << candidate.to_s if candidate.seen_since?(@deploy_started_at - 5.seconds) end end
file_path()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 168 def file_path file.path end
global?()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 441 def global? @global || self.class::GLOBAL end
has_warnings?()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 148 def has_warnings? @validation_warnings.present? end
id()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 160 def id "#{type}/#{name}" end
kubectl_resource_type()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 230 def kubectl_resource_type type end
observed_generation()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 216 def observed_generation return -2 unless exists? # populating this is a best practice, but not all controllers actually do it @instance_data.dig('status', 'observedGeneration') end
pretty_status()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 341 def pretty_status padding = " " * [50 - id.length, 1].max "#{id}#{padding}#{status}" end
pretty_timeout_type()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 110 def pretty_timeout_type "timeout: #{timeout}s" end
report_status_to_statsd(watch_time)
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 346 def report_status_to_statsd(watch_time) unless @statsd_report_done StatsD.client.distribution('resource.duration', watch_time, tags: statsd_tags) @statsd_report_done = true end end
sensitive_template_content?()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 353 def sensitive_template_content? self.class::SENSITIVE_TEMPLATE_CONTENT end
server_dry_run_validated?()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 367 def server_dry_run_validated? @server_dry_run_validated end
server_dry_runnable_resource?()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 357 def server_dry_runnable_resource? # generateName and server-side dry run are incompatible because the former only works with `create` # and the latter only works with `apply` self.class::SERVER_DRY_RUNNABLE && !uses_generate_name? end
status()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 222 def status exists? ? "Exists" : "Not Found" end
sync(cache)
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 172 def sync(cache) @instance_data = cache.get_instance(kubectl_resource_type, name, raise_if_not_found: true) rescue Krane::Kubectl::ResourceNotFoundError @disappeared = true if deploy_started? @instance_data = {} end
sync_debug_info(kubectl)
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 248 def sync_debug_info(kubectl) @debug_events = fetch_events(kubectl) unless ENV[DISABLE_FETCHING_EVENT_INFO] @debug_logs = fetch_debug_logs if print_debug_logs? && !ENV[DISABLE_FETCHING_LOG_INFO] end
terminating?()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 182 def terminating? @instance_data.dig('metadata', 'deletionTimestamp').present? end
timeout()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 97 def timeout return timeout_override if timeout_override.present? self.class.timeout end
timeout_message()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 334 def timeout_message STANDARD_TIMEOUT_MESSAGE end
timeout_override()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 102 def timeout_override return @timeout_override if defined?(@timeout_override) @timeout_override = DurationParser.new(krane_annotation_value(TIMEOUT_OVERRIDE_ANNOTATION_SUFFIX)).parse!.to_i rescue DurationParser::ParsingError @timeout_override = nil end
to_kubeclient_resource()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 130 def to_kubeclient_resource Kubeclient::Resource.new(@definition) end
type()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 226 def type @type || self.class.kind end
use_generated_name(instance_data)
click to toggle source
If a resource uses generateName, we don't know the full name of the resource until it's deployed to the cluster. In this case, we need to update our local definition with the realized name in order to accurately track the resource during deploy
# File lib/krane/kubernetes_resource.rb, line 374 def use_generated_name(instance_data) @name = instance_data.dig('metadata', 'name') @definition['metadata']['name'] = @name @definition['metadata'].delete('generateName') @file = create_definition_tempfile end
uses_generate_name?()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 363 def uses_generate_name? @definition.dig('metadata', 'generateName').present? end
validate_definition(kubectl, selector: nil)
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 134 def validate_definition(kubectl, selector: nil) @validation_errors = [] @validation_warnings = [] validate_selector(selector) if selector validate_timeout_annotation validate_annotation_version validate_spec_with_kubectl(kubectl) @validation_errors.present? end
validation_error_msg()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 152 def validation_error_msg @validation_errors.join("\n") end
validation_failed?()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 156 def validation_failed? @validation_errors.present? end
validation_warning_msg()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 144 def validation_warning_msg @validation_warnings.join("\n") end
Private Instance Methods
create_definition_tempfile()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 546 def create_definition_tempfile file = Tempfile.new(["#{type}-#{name}", ".yml"]) file.write(YAML.dump(@definition)) file ensure file&.close end
file()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 542 def file @file ||= create_definition_tempfile end
krane_annotation_key(suffix)
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 480 def krane_annotation_key(suffix) if @definition.dig("metadata", "annotations", "kubernetes-deploy.shopify.io/#{suffix}") "kubernetes-deploy.shopify.io/#{suffix}" elsif @definition.dig("metadata", "annotations", "krane.shopify.io/#{suffix}") "krane.shopify.io/#{suffix}" end end
krane_annotation_value(suffix)
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 475 def krane_annotation_value(suffix) @definition.dig("metadata", "annotations", "kubernetes-deploy.shopify.io/#{suffix}") || @definition.dig("metadata", "annotations", "krane.shopify.io/#{suffix}") end
labels()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 538 def labels @definition.dig("metadata", "labels") end
print_debug_logs?()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 554 def print_debug_logs? false end
validate_annotation_version()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 462 def validate_annotation_version return if validation_warning_msg.include?("annotations is deprecated") annotation_keys = @definition.dig("metadata", "annotations")&.keys annotation_keys&.each do |annotation| if annotation.include?("kubernetes-deploy.shopify.io") annotation_prefix = annotation.split('/').first @validation_warnings << "#{annotation_prefix} as a prefix for annotations is deprecated: "\ "Use the 'krane.shopify.io' annotation prefix instead" return end end end
validate_selector(selector)
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 488 def validate_selector(selector) if labels.nil? @validation_errors << "selector #{selector} passed in, but no labels were defined" return end unless selector.to_h <= labels label_name = 'label'.pluralize(labels.size) label_string = LabelSelector.new(labels).to_s @validation_errors << "selector #{selector} does not match #{label_name} #{label_string}" end end
validate_spec_with_kubectl(kubectl)
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 501 def validate_spec_with_kubectl(kubectl) err = "" if kubectl.server_dry_run_enabled? && server_dry_runnable_resource? _, err, st = validate_with_server_side_dry_run(kubectl) @server_dry_run_validated = st.success? return true if st.success? end if err.empty? || err.match(SERVER_DRY_RUN_DISABLED_ERROR) _, err, st = validate_with_local_dry_run(kubectl) end return true if st.success? @validation_errors << if sensitive_template_content? "Validation for #{id} failed. Detailed information is unavailable as the raw error may contain sensitive data." else err end end
validate_timeout_annotation()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 447 def validate_timeout_annotation timeout_override_value = krane_annotation_value(TIMEOUT_OVERRIDE_ANNOTATION_SUFFIX) timeout_annotation_key = krane_annotation_key(TIMEOUT_OVERRIDE_ANNOTATION_SUFFIX) return if timeout_override_value.nil? override = DurationParser.new(timeout_override_value).parse! if override <= 0 @validation_errors << "#{timeout_annotation_key} annotation is invalid: Value must be greater than 0" elsif override > 24.hours @validation_errors << "#{timeout_annotation_key} annotation is invalid: Value must be less than 24h" end rescue DurationParser::ParsingError => e @validation_errors << "#{timeout_annotation_key} annotation is invalid: #{e}" end
validate_with_local_dry_run(kubectl)
click to toggle source
Local dry run is supported on only create and apply If the deploy method is create, validating with apply will fail If the resource template uses generateName, validating with apply will fail
# File lib/krane/kubernetes_resource.rb, line 531 def validate_with_local_dry_run(kubectl) verb = deploy_method == :apply ? "apply" : "create" command = [verb, "-f", file_path, "--dry-run", "--output=name"] kubectl.run(*command, log_failure: false, output_is_sensitive: sensitive_template_content?, retry_whitelist: [:client_timeout], attempts: 3, use_namespace: !global?) end
validate_with_server_side_dry_run(kubectl)
click to toggle source
Server side dry run is only supported on apply
# File lib/krane/kubernetes_resource.rb, line 522 def validate_with_server_side_dry_run(kubectl) command = ["apply", "-f", file_path, "--server-dry-run", "--output=name"] kubectl.run(*command, log_failure: false, output_is_sensitive: sensitive_template_content?, retry_whitelist: [:client_timeout], attempts: 3) end