Class: Krane::KubernetesResource

Inherits:
Object
  • Object
show all
Defined in:
lib/krane/kubernetes_resource.rb

Defined Under Namespace

Classes: Event

Constant Summary collapse

GLOBAL =
false
TIMEOUT =
5.minutes
LOG_LINE_COUNT =
250
SERVER_DRY_RUN_DISABLED_ERROR =
/(unknown flag: --server-dry-run)|(does[\s\']n[o|']t support dry[-\s]run)|(dryRun alpha feature is disabled)/
DISABLE_FETCHING_LOG_INFO =
'DISABLE_FETCHING_LOG_INFO'
DISABLE_FETCHING_EVENT_INFO =
'DISABLE_FETCHING_EVENT_INFO'
DISABLED_LOG_INFO_MESSAGE =
"collection is disabled by the #{DISABLE_FETCHING_LOG_INFO} env var."
DISABLED_EVENT_INFO_MESSAGE =
"collection is disabled by the #{DISABLE_FETCHING_EVENT_INFO} env var."
DEBUG_RESOURCE_NOT_FOUND_MESSAGE =
"None found. Please check your usual logging service (e.g. Splunk)."
UNUSUAL_FAILURE_MESSAGE =
<<~MSG
It is very unusual for this resource type to fail to deploy. Please try the deploy again.
If that new deploy also fails, contact your cluster administrator.
MSG
STANDARD_TIMEOUT_MESSAGE =
<<~MSG
Kubernetes will continue to attempt to deploy this resource in the cluster, but at this point it is considered unlikely that it will succeed.
If you have reason to believe it will succeed, retry the deploy to continue to monitor the rollout.
MSG
ALLOWED_DEPLOY_METHOD_OVERRIDES =
%w(create replace replace-force)
DEPLOY_METHOD_OVERRIDE_ANNOTATION =
"deploy-method-override"
TIMEOUT_OVERRIDE_ANNOTATION =
"timeout-override"
LAST_APPLIED_ANNOTATION =
"kubectl.kubernetes.io/last-applied-configuration"
SENSITIVE_TEMPLATE_CONTENT =
false
SERVER_DRY_RUNNABLE =
false
SYNC_DEPENDENCIES =
[]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(namespace:, context:, definition:, logger:, statsd_tags: []) ⇒ KubernetesResource

Returns a new instance of KubernetesResource.



118
119
120
121
122
123
124
125
126
127
128
129
130
131
# 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

Instance Attribute Details

#contextObject (readonly)

Returns the value of attribute context.



15
16
17
# File 'lib/krane/kubernetes_resource.rb', line 15

def context
  @context
end

#deploy_started_at=(value) ⇒ Object (writeonly)

Sets the attribute deploy_started_at

Parameters:

  • value

    the value to set the attribute deploy_started_at to.



16
17
18
# File 'lib/krane/kubernetes_resource.rb', line 16

def deploy_started_at=(value)
  @deploy_started_at = value
end

#global=(value) ⇒ Object (writeonly)

Sets the attribute global

Parameters:

  • value

    the value to set the attribute global to.



16
17
18
# File 'lib/krane/kubernetes_resource.rb', line 16

def global=(value)
  @global = value
end

#nameObject (readonly)

Returns the value of attribute name.



15
16
17
# File 'lib/krane/kubernetes_resource.rb', line 15

def name
  @name
end

#namespaceObject (readonly)

Returns the value of attribute namespace.



15
16
17
# File 'lib/krane/kubernetes_resource.rb', line 15

def namespace
  @namespace
end

#typeObject



220
221
222
# File 'lib/krane/kubernetes_resource.rb', line 220

def type
  @type || self.class.kind
end

Class Method Details

.build(namespace: nil, context:, definition:, logger:, statsd_tags:, crd: nil, global_names: []) ⇒ Object



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# 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) ⇒ Object



65
66
67
68
69
70
71
# 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

.kindObject



77
78
79
# File 'lib/krane/kubernetes_resource.rb', line 77

def kind
  name.demodulize
end

.timeoutObject



73
74
75
# File 'lib/krane/kubernetes_resource.rb', line 73

def timeout
  self::TIMEOUT
end

Instance Method Details

#<=>(other) ⇒ Object



158
159
160
# File 'lib/krane/kubernetes_resource.rb', line 158

def <=>(other)
  id <=> other.id
end

#after_syncObject



173
174
# File 'lib/krane/kubernetes_resource.rb', line 173

def after_sync
end

#current_generationObject



205
206
207
208
# 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 = {}) ⇒ Object



261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
# 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?Boolean

Returns:

  • (Boolean)


184
185
186
# File 'lib/krane/kubernetes_resource.rb', line 184

def deploy_failed?
  false
end

#deploy_methodObject

Expected values: :apply, :create, :replace, :replace_force



244
245
246
247
248
249
250
# 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_overrideObject



252
253
254
# File 'lib/krane/kubernetes_resource.rb', line 252

def deploy_method_override
  krane_annotation_value(DEPLOY_METHOD_OVERRIDE_ANNOTATION)&.gsub("-", "_")&.to_sym
end

#deploy_started?Boolean

Returns:

  • (Boolean)


188
189
190
# File 'lib/krane/kubernetes_resource.rb', line 188

def deploy_started?
  @deploy_started_at.present?
end

#deploy_succeeded?Boolean

Returns:

  • (Boolean)


192
193
194
195
196
197
198
199
# 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?Boolean

Returns:

  • (Boolean)


238
239
240
241
# 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?Boolean

Returns:

  • (Boolean)


180
181
182
# File 'lib/krane/kubernetes_resource.rb', line 180

def disappeared?
  @disappeared
end

#exists?Boolean

Returns:

  • (Boolean)


201
202
203
# File 'lib/krane/kubernetes_resource.rb', line 201

def exists?
  @instance_data.present?
end

#failure_messageObject



346
347
# File 'lib/krane/kubernetes_resource.rb', line 346

def failure_message
end

#fetch_events(kubectl) ⇒ Object

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)"
]



330
331
332
333
334
335
336
337
338
339
340
# 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_pathObject



162
163
164
# File 'lib/krane/kubernetes_resource.rb', line 162

def file_path
  file.path
end

#global?Boolean

Returns:

  • (Boolean)


498
499
500
# File 'lib/krane/kubernetes_resource.rb', line 498

def global?
  @global || self.class::GLOBAL
end

#groupObject



224
225
226
227
# File 'lib/krane/kubernetes_resource.rb', line 224

def group
  grouping, version = @definition.dig("apiVersion").split("/")
  version ? grouping : "core"
end

#idObject



154
155
156
# File 'lib/krane/kubernetes_resource.rb', line 154

def id
  "#{type}/#{name}"
end

#kubectl_resource_typeObject



234
235
236
# File 'lib/krane/kubernetes_resource.rb', line 234

def kubectl_resource_type
  type
end

#observed_generationObject



210
211
212
213
214
# 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_statusObject



349
350
351
352
# File 'lib/krane/kubernetes_resource.rb', line 349

def pretty_status
  padding = " " * [50 - id.length, 1].max
  "#{id}#{padding}#{status}"
end

#pretty_timeout_typeObject



114
115
116
# File 'lib/krane/kubernetes_resource.rb', line 114

def pretty_timeout_type
  "timeout: #{timeout}s"
end

#report_status_to_statsd(watch_time) ⇒ Object



354
355
356
357
358
359
# 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) ⇒ Boolean

Returns:

  • (Boolean)


502
503
504
# File 'lib/krane/kubernetes_resource.rb', line 502

def selected?(selector)
  selector.nil? || selector.to_h <= labels
end

#sensitive_template_content?Boolean

Returns:

  • (Boolean)


361
362
363
# File 'lib/krane/kubernetes_resource.rb', line 361

def sensitive_template_content?
  self.class::SENSITIVE_TEMPLATE_CONTENT
end

#server_dry_run_validated?Boolean

Returns:

  • (Boolean)


375
376
377
# File 'lib/krane/kubernetes_resource.rb', line 375

def server_dry_run_validated?
  @server_dry_run_validated
end

#server_dry_runnable_resource?Boolean

Returns:

  • (Boolean)


365
366
367
368
369
# 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

#statusObject



216
217
218
# File 'lib/krane/kubernetes_resource.rb', line 216

def status
  exists? ? "Exists" : "Not Found"
end

#sync(cache) ⇒ Object



166
167
168
169
170
171
# 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) ⇒ Object



256
257
258
259
# 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?Boolean

Returns:

  • (Boolean)


176
177
178
# File 'lib/krane/kubernetes_resource.rb', line 176

def terminating?
  @instance_data.dig('metadata', 'deletionTimestamp').present?
end

#timeoutObject



101
102
103
104
# File 'lib/krane/kubernetes_resource.rb', line 101

def timeout
  return timeout_override if timeout_override.present?
  self.class.timeout
end

#timeout_messageObject



342
343
344
# File 'lib/krane/kubernetes_resource.rb', line 342

def timeout_message
  STANDARD_TIMEOUT_MESSAGE
end

#timeout_overrideObject



106
107
108
109
110
111
112
# 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_resourceObject



133
134
135
# File 'lib/krane/kubernetes_resource.rb', line 133

def to_kubeclient_resource
  Kubeclient::Resource.new(@definition)
end

#use_generated_name(instance_data) ⇒ Object

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



382
383
384
385
386
387
# 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?Boolean

Returns:

  • (Boolean)


371
372
373
# 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) ⇒ Object



137
138
139
140
141
142
143
144
# 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_msgObject



146
147
148
# File 'lib/krane/kubernetes_resource.rb', line 146

def validation_error_msg
  @validation_errors.join("\n")
end

#validation_failed?Boolean

Returns:

  • (Boolean)


150
151
152
# File 'lib/krane/kubernetes_resource.rb', line 150

def validation_failed?
  @validation_errors.present?
end

#versionObject



229
230
231
232
# File 'lib/krane/kubernetes_resource.rb', line 229

def version
  prefix, version = @definition.dig("apiVersion").split("/")
  version || prefix
end