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
statsd_tags() click to toggle source
# File lib/krane/kubernetes_resource.rb, line 612
def statsd_tags
  status = if deploy_failed?
    "failure"
  elsif deploy_timed_out?
    "timeout"
  elsif deploy_succeeded?
    "success"
  else
    "unknown"
  end
  tags = %W(context:#{context} namespace:#{namespace} type:#{type} status:#{status})
  tags | @optional_statsd_tags
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