Class: KubernetesDeploy::KubernetesResource

Inherits:
Object
  • Object
show all
Defined in:
lib/kubernetes-deploy/kubernetes_resource.rb

Defined Under Namespace

Classes: Event

Constant Summary collapse

TIMEOUT =
5.minutes
LOG_LINE_COUNT =
250
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
TIMEOUT_OVERRIDE_ANNOTATION =
"kubernetes-deploy.shopify.io/timeout-override"

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.



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 71

def initialize(namespace:, context:, definition:, logger:, statsd_tags: [])
  # subclasses must also set these if they define their own initializer
  @name = definition.dig("metadata", "name")
  unless @name.present?
    logger.summary.add_paragraph("Rendered template content:\n#{definition.to_yaml}")
    raise FatalDeploymentError, "Template is missing required field metadata.name"
  end

  @optional_statsd_tags = statsd_tags
  @namespace = namespace
  @context = context
  @logger = logger
  @definition = definition
  @statsd_report_done = false
  @validation_errors = []
  @instance_data = {}
end

Instance Attribute Details

#contextObject (readonly)

Returns the value of attribute context.



8
9
10
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 8

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.



9
10
11
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 9

def deploy_started_at=(value)
  @deploy_started_at = value
end

#nameObject (readonly)

Returns the value of attribute name.



8
9
10
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 8

def name
  @name
end

#namespaceObject (readonly)

Returns the value of attribute namespace.



8
9
10
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 8

def namespace
  @namespace
end

#typeObject



145
146
147
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 145

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

Class Method Details

.build(namespace:, context:, definition:, logger:, statsd_tags:) ⇒ Object



31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 31

def build(namespace:, context:, definition:, logger:, statsd_tags:)
  opts = { namespace: namespace, context: context, definition: definition, logger: logger,
           statsd_tags: statsd_tags }
  if definition["kind"].blank?
    raise InvalidTemplateError.new("Template missing 'Kind'", content: definition.to_yaml)
  elsif KubernetesDeploy.const_defined?(definition["kind"])
    klass = KubernetesDeploy.const_get(definition["kind"])
    klass.new(**opts)
  else
    inst = new(**opts)
    inst.type = definition["kind"]
    inst
  end
end

.kindObject



50
51
52
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 50

def kind
  name.demodulize
end

.timeoutObject



46
47
48
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 46

def timeout
  self::TIMEOUT
end

Instance Method Details

#debug_message(cause = nil, info_hash = {}) ⇒ Object



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 165

def debug_message(cause = nil, info_hash = {})
  helpful_info = []
  if cause == :gave_up
    helpful_info << 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?
    helpful_info << ColorizedString.new("#{id}: FAILED").red
    helpful_info << failure_message if failure_message.present?
  elsif deploy_timed_out?
    helpful_info << 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.
    helpful_info << 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
  helpful_info << "  - Final status: #{status}"

  if @events.present?
    helpful_info << "  - Events (common success events excluded):"
    @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 supports_logs?
    if ENV[DISABLE_FETCHING_LOG_INFO]
      helpful_info << "  - Logs: #{DISABLED_LOG_INFO_MESSAGE}"
    elsif @logs.blank? || @logs.values.all?(&:blank?)
      helpful_info << "  - Logs: #{DEBUG_RESOURCE_NOT_FOUND_MESSAGE}"
    else
      sorted_logs = @logs.sort_by { |_, log_lines| log_lines.length }
      sorted_logs.each do |identifier, log_lines|
        if log_lines.empty?
          helpful_info << "  - Logs from container '#{identifier}': #{DEBUG_RESOURCE_NOT_FOUND_MESSAGE}"
          next
        end

        helpful_info << "  - Logs from container '#{identifier}' (last #{LOG_LINE_COUNT} lines shown):"
        log_lines.each do |line|
          helpful_info << "      #{line}"
        end
      end
    end
  end

  helpful_info.join("\n")
end

#deploy_failed?Boolean

Returns:

  • (Boolean)


120
121
122
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 120

def deploy_failed?
  false
end

#deploy_methodObject

Expected values: :apply, :replace, :replace_force



155
156
157
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 155

def deploy_method
  :apply
end

#deploy_started?Boolean

Returns:

  • (Boolean)


124
125
126
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 124

def deploy_started?
  @deploy_started_at.present?
end

#deploy_succeeded?Boolean

Returns:

  • (Boolean)


128
129
130
131
132
133
134
135
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 128

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)


149
150
151
152
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 149

def deploy_timed_out?
  return false unless deploy_started?
  !deploy_succeeded? && !deploy_failed? && (Time.now.utc - @deploy_started_at > timeout)
end

#exists?Boolean

Returns:

  • (Boolean)


137
138
139
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 137

def exists?
  @instance_data.present?
end

#failure_messageObject



243
244
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 243

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



227
228
229
230
231
232
233
234
235
236
237
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 227

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



112
113
114
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 112

def file_path
  file.path
end

#idObject



108
109
110
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 108

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

#pretty_statusObject



246
247
248
249
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 246

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

#pretty_timeout_typeObject



67
68
69
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 67

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

#report_status_to_statsd(watch_time) ⇒ Object



251
252
253
254
255
256
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 251

def report_status_to_statsd(watch_time)
  unless @statsd_report_done
    ::StatsD.measure('resource.duration', watch_time, tags: statsd_tags)
    @statsd_report_done = true
  end
end

#statusObject



141
142
143
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 141

def status
  exists? ? "Exists" : "Unknown"
end

#sync(mediator) ⇒ Object



116
117
118
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 116

def sync(mediator)
  @instance_data = mediator.get_instance(type, name)
end

#sync_debug_info(kubectl) ⇒ Object



159
160
161
162
163
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 159

def sync_debug_info(kubectl)
  @events = fetch_events(kubectl) unless ENV[DISABLE_FETCHING_EVENT_INFO]
  @logs = fetch_logs(kubectl) if supports_logs? && !ENV[DISABLE_FETCHING_EVENT_INFO]
  @debug_info_synced = true
end

#timeoutObject



55
56
57
58
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 55

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

#timeout_messageObject



239
240
241
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 239

def timeout_message
  STANDARD_TIMEOUT_MESSAGE
end

#timeout_overrideObject



60
61
62
63
64
65
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 60

def timeout_override
  return @timeout_override if defined?(@timeout_override)
  @timeout_override = DurationParser.new(timeout_annotation).parse!.to_i
rescue DurationParser::ParsingError
  @timeout_override = nil
end

#validate_definition(kubectl) ⇒ Object



89
90
91
92
93
94
95
96
97
98
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 89

def validate_definition(kubectl)
  @validation_errors = []
  validate_timeout_annotation

  command = ["create", "-f", file_path, "--dry-run", "--output=name"]
  _, err, st = kubectl.run(*command, log_failure: false)
  return true if st.success?
  @validation_errors << err
  false
end

#validation_error_msgObject



100
101
102
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 100

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

#validation_failed?Boolean

Returns:

  • (Boolean)


104
105
106
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 104

def validation_failed?
  @validation_errors.present?
end