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:) ⇒ KubernetesResource

Returns a new instance of KubernetesResource.



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 63

def initialize(namespace:, context:, definition:, logger:)
  # 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

  @namespace = namespace
  @context = context
  @logger = logger
  @definition = definition
  @statsd_report_done = false
  @validation_errors = []
end

Instance Attribute Details

#contextObject (readonly)

Returns the value of attribute context.



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

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.



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

def deploy_started_at=(value)
  @deploy_started_at = value
end

#nameObject (readonly)

Returns the value of attribute name.



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

def name
  @name
end

#namespaceObject (readonly)

Returns the value of attribute namespace.



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

def namespace
  @namespace
end

#typeObject



133
134
135
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 133

def type
  @type || self.class.name.demodulize
end

Class Method Details

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



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

def self.build(namespace:, context:, definition:, logger:)
  opts = { namespace: namespace, context: context, definition: definition, logger: logger }
  if 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

.timeoutObject



43
44
45
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 43

def self.timeout
  self::TIMEOUT
end

Instance Method Details

#debug_messageObject



153
154
155
156
157
158
159
160
161
162
163
164
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
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 153

def debug_message
  sync_debug_info unless @debug_info_synced

  helpful_info = []
  if 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)


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

def deploy_failed?
  false
end

#deploy_methodObject

Expected values: :apply, :replace, :replace_force



143
144
145
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 143

def deploy_method
  :apply
end

#deploy_started?Boolean

Returns:

  • (Boolean)


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

def deploy_started?
  @deploy_started_at.present?
end

#deploy_succeeded?Boolean

Returns:

  • (Boolean)


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

def deploy_succeeded?
  if deploy_started? && !@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)


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

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)


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

def exists?
  nil
end

#failure_messageObject



228
229
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 228

def failure_message
end

#fetch_eventsObject

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



213
214
215
216
217
218
219
220
221
222
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 213

def fetch_events
  return {} unless exists?
  out, _err, st = kubectl.run("get", "events", "--output=go-template=#{Event.go_template_for(type, name)}")
  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



102
103
104
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 102

def file_path
  file.path
end

#idObject



98
99
100
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 98

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

#kubectlObject



237
238
239
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 237

def kubectl
  @kubectl ||= Kubectl.new(namespace: @namespace, context: @context, logger: @logger, log_failure_by_default: false)
end

#pretty_statusObject



231
232
233
234
235
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 231

def pretty_status
  padding = " " * [50 - id.length, 1].max
  msg = exists? ? status : "not found"
  "#{id}#{padding}#{msg}"
end

#pretty_timeout_typeObject



59
60
61
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 59

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

#report_status_to_statsd(watch_time) ⇒ Object



241
242
243
244
245
246
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 241

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



129
130
131
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 129

def status
  @status ||= "Unknown"
end

#syncObject



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

def sync
end

#sync_debug_infoObject



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

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

#timeoutObject



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

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

#timeout_messageObject



224
225
226
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 224

def timeout_message
  STANDARD_TIMEOUT_MESSAGE
end

#timeout_overrideObject



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

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_definitionObject



79
80
81
82
83
84
85
86
87
88
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 79

def validate_definition
  @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



90
91
92
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 90

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

#validation_failed?Boolean

Returns:

  • (Boolean)


94
95
96
# File 'lib/kubernetes-deploy/kubernetes_resource.rb', line 94

def validation_failed?
  @validation_errors.present?
end