class Krane::KubernetesResource
Constants
- ALLOWED_DEPLOY_METHOD_OVERRIDES
- DEBUG_RESOURCE_NOT_FOUND_MESSAGE
- DEPLOY_METHOD_OVERRIDE_ANNOTATION
- 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
- SYNC_DEPENDENCIES
- TIMEOUT
- TIMEOUT_OVERRIDE_ANNOTATION
- 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 47 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 65 def class_for_kind(kind) if Krane.const_defined?(kind, false) Krane.const_get(kind, false) end rescue NameError nil end
kind()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 77 def kind name.demodulize end
new(namespace:, context:, definition:, logger:, statsd_tags: [])
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 118 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 = [] @instance_data = {} @server_dry_run_validated = false end
timeout()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 73 def timeout self::TIMEOUT end
Private Class Methods
validate_definition_essentials(definition)
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 83 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 158 def <=>(other) id <=> other.id end
after_sync()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 173 def after_sync end
current_generation()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 205 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 261 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 --global-timeout." 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 184 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 244 def deploy_method if @definition.dig("metadata", "name").blank? && uses_generate_name? :create else deploy_method_override || :apply end end
deploy_method_override()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 252 def deploy_method_override krane_annotation_value(DEPLOY_METHOD_OVERRIDE_ANNOTATION)&.to_sym end
deploy_started?()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 188 def deploy_started? @deploy_started_at.present? end
deploy_succeeded?()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 192 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 238 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 180 def disappeared? @disappeared end
exists?()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 201 def exists? @instance_data.present? end
failure_message()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 346 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 330 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 162 def file_path file.path end
global?()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 498 def global? @global || self.class::GLOBAL end
group()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 224 def group grouping, version = @definition.dig("apiVersion").split("/") version ? grouping : "core" end
id()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 154 def id "#{type}/#{name}" end
kubectl_resource_type()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 234 def kubectl_resource_type type end
observed_generation()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 210 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 349 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 114 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 354 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
selected?(selector)
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 502 def selected?(selector) selector.nil? || selector.to_h <= labels end
sensitive_template_content?()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 361 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 375 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 365 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 216 def status exists? ? "Exists" : "Not Found" end
sync(cache)
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 166 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 256 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 176 def terminating? @instance_data.dig('metadata', 'deletionTimestamp').present? end
timeout()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 101 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 342 def timeout_message STANDARD_TIMEOUT_MESSAGE end
timeout_override()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 106 def timeout_override return @timeout_override if defined?(@timeout_override) @timeout_override = DurationParser.new(krane_annotation_value(TIMEOUT_OVERRIDE_ANNOTATION)).parse!.to_i rescue DurationParser::ParsingError @timeout_override = nil end
to_kubeclient_resource()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 133 def to_kubeclient_resource Kubeclient::Resource.new(@definition) end
type()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 220 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 382 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 371 def uses_generate_name? @definition.dig('metadata', 'generateName').present? end
validate_definition(kubectl:, selector: nil, dry_run: true)
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 137 def validate_definition(kubectl:, selector: nil, dry_run: true) @validation_errors = [] validate_selector(selector) if selector validate_timeout_annotation validate_deploy_method_override_annotation validate_spec_with_kubectl(kubectl) if dry_run @validation_errors.present? end
validation_error_msg()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 146 def validation_error_msg @validation_errors.join("\n") end
validation_failed?()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 150 def validation_failed? @validation_errors.present? end
version()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 229 def version prefix, version = @definition.dig("apiVersion").split("/") version || prefix end
Private Instance Methods
create_definition_tempfile()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 600 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 596 def file @file ||= create_definition_tempfile end
krane_annotation_value(suffix)
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 533 def krane_annotation_value(suffix) @definition.dig("metadata", "annotations", Annotation.for(suffix)) end
labels()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 592 def labels @definition.dig("metadata", "labels") end
print_debug_logs?()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 608 def print_debug_logs? false end
validate_deploy_method_override_annotation()
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 523 def validate_deploy_method_override_annotation deploy_method_override_value = krane_annotation_value(DEPLOY_METHOD_OVERRIDE_ANNOTATION) deploy_method_override_annotation_key = Annotation.for(DEPLOY_METHOD_OVERRIDE_ANNOTATION) return unless deploy_method_override_value unless ALLOWED_DEPLOY_METHOD_OVERRIDES.include?(deploy_method_override_value) @validation_errors << "#{deploy_method_override_annotation_key} is invalid: Accepted values are: " \ "#{ALLOWED_DEPLOY_METHOD_OVERRIDES.join(', ')} but got #{deploy_method_override_value}" end end
validate_selector(selector)
click to toggle source
# File lib/krane/kubernetes_resource.rb, line 537 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 550 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 508 def validate_timeout_annotation timeout_override_value = krane_annotation_value(TIMEOUT_OVERRIDE_ANNOTATION) timeout_annotation_key = Annotation.for(TIMEOUT_OVERRIDE_ANNOTATION) 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 585 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, :empty, :context_deadline], 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 571 def validate_with_server_side_dry_run(kubectl) command = if kubectl.client_version >= Gem::Version.new('1.18') ["apply", "-f", file_path, "--dry-run=server", "--output=name"] else ["apply", "-f", file_path, "--server-dry-run", "--output=name"] end kubectl.run(*command, log_failure: false, output_is_sensitive: sensitive_template_content?, retry_whitelist: [:client_timeout, :empty, :context_deadline], attempts: 3) end