2020-03-11 15:09:37 +00:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
module PodLogs
|
2020-03-12 03:10:15 +00:00
|
|
|
class KubernetesService < PodLogs::BaseService
|
2020-03-11 15:09:37 +00:00
|
|
|
LOGS_LIMIT = 500.freeze
|
|
|
|
REPLACEMENT_CHAR = "\u{FFFD}"
|
|
|
|
|
|
|
|
EncodingHelperError = Class.new(StandardError)
|
|
|
|
|
|
|
|
steps :check_arguments,
|
|
|
|
:get_raw_pods,
|
|
|
|
:get_pod_names,
|
|
|
|
:check_pod_name,
|
|
|
|
:check_container_name,
|
|
|
|
:pod_logs,
|
|
|
|
:encode_logs_to_utf8,
|
|
|
|
:split_logs,
|
|
|
|
:filter_return_keys
|
|
|
|
|
2020-04-24 21:09:48 +00:00
|
|
|
self.reactive_cache_work_type = :external_dependency
|
2020-03-11 15:09:37 +00:00
|
|
|
self.reactive_cache_worker_finder = ->(id, _cache_key, namespace, params) { new(::Clusters::Cluster.find(id), namespace, params: params) }
|
|
|
|
|
|
|
|
private
|
|
|
|
|
2020-04-15 18:09:36 +00:00
|
|
|
def get_raw_pods(result)
|
|
|
|
result[:raw_pods] = cluster.kubeclient.get_pods(namespace: namespace).map do |pod|
|
|
|
|
{
|
|
|
|
name: pod.metadata.name,
|
|
|
|
container_names: pod.spec.containers.map(&:name)
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
success(result)
|
|
|
|
end
|
|
|
|
|
2020-03-18 03:09:43 +00:00
|
|
|
def check_pod_name(result)
|
|
|
|
# If pod_name is not received as parameter, get the pod logs of the first
|
|
|
|
# pod of this namespace.
|
|
|
|
result[:pod_name] ||= result[:pods].first
|
|
|
|
|
|
|
|
unless result[:pod_name]
|
|
|
|
return error(_('No pods available'))
|
|
|
|
end
|
|
|
|
|
|
|
|
unless result[:pod_name].length.to_i <= K8S_NAME_MAX_LENGTH
|
|
|
|
return error(_('pod_name cannot be larger than %{max_length}'\
|
|
|
|
' chars' % { max_length: K8S_NAME_MAX_LENGTH }))
|
|
|
|
end
|
|
|
|
|
2020-05-07 09:09:51 +00:00
|
|
|
unless result[:pod_name] =~ Gitlab::Regex.kubernetes_dns_subdomain_regex
|
|
|
|
return error(_('pod_name can contain only lowercase letters, digits, \'-\', and \'.\' and must start and end with an alphanumeric character'))
|
|
|
|
end
|
|
|
|
|
2020-03-18 03:09:43 +00:00
|
|
|
unless result[:pods].include?(result[:pod_name])
|
|
|
|
return error(_('Pod does not exist'))
|
|
|
|
end
|
|
|
|
|
|
|
|
success(result)
|
|
|
|
end
|
|
|
|
|
|
|
|
def check_container_name(result)
|
2020-04-15 18:09:36 +00:00
|
|
|
pod_details = result[:raw_pods].find { |p| p[:name] == result[:pod_name] }
|
|
|
|
container_names = pod_details[:container_names]
|
2020-03-18 03:09:43 +00:00
|
|
|
|
|
|
|
# select first container if not specified
|
2020-04-15 18:09:36 +00:00
|
|
|
result[:container_name] ||= container_names.first
|
2020-03-18 03:09:43 +00:00
|
|
|
|
|
|
|
unless result[:container_name]
|
|
|
|
return error(_('No containers available'))
|
|
|
|
end
|
|
|
|
|
|
|
|
unless result[:container_name].length.to_i <= K8S_NAME_MAX_LENGTH
|
|
|
|
return error(_('container_name cannot be larger than'\
|
|
|
|
' %{max_length} chars' % { max_length: K8S_NAME_MAX_LENGTH }))
|
|
|
|
end
|
|
|
|
|
2020-05-07 09:09:51 +00:00
|
|
|
unless result[:container_name] =~ Gitlab::Regex.kubernetes_dns_subdomain_regex
|
|
|
|
return error(_('container_name can contain only lowercase letters, digits, \'-\', and \'.\' and must start and end with an alphanumeric character'))
|
|
|
|
end
|
|
|
|
|
2020-04-15 18:09:36 +00:00
|
|
|
unless container_names.include?(result[:container_name])
|
2020-03-18 03:09:43 +00:00
|
|
|
return error(_('Container does not exist'))
|
|
|
|
end
|
|
|
|
|
|
|
|
success(result)
|
|
|
|
end
|
|
|
|
|
2020-03-11 15:09:37 +00:00
|
|
|
def pod_logs(result)
|
|
|
|
result[:logs] = cluster.kubeclient.get_pod_log(
|
|
|
|
result[:pod_name],
|
|
|
|
namespace,
|
|
|
|
container: result[:container_name],
|
|
|
|
tail_lines: LOGS_LIMIT,
|
|
|
|
timestamps: true
|
|
|
|
).body
|
|
|
|
|
|
|
|
success(result)
|
|
|
|
rescue Kubeclient::ResourceNotFoundError
|
|
|
|
error(_('Pod not found'))
|
|
|
|
rescue Kubeclient::HttpError => e
|
|
|
|
::Gitlab::ErrorTracking.track_exception(e)
|
|
|
|
|
|
|
|
error(_('Kubernetes API returned status code: %{error_code}') % {
|
|
|
|
error_code: e.error_code
|
|
|
|
})
|
|
|
|
end
|
|
|
|
|
|
|
|
# Check https://gitlab.com/gitlab-org/gitlab/issues/34965#note_292261879
|
|
|
|
# for more details on why this is necessary.
|
|
|
|
def encode_logs_to_utf8(result)
|
|
|
|
return success(result) if result[:logs].nil?
|
|
|
|
return success(result) if result[:logs].encoding == Encoding::UTF_8
|
|
|
|
|
|
|
|
result[:logs] = encode_utf8(result[:logs])
|
|
|
|
|
|
|
|
success(result)
|
|
|
|
rescue EncodingHelperError
|
|
|
|
error(_('Unable to convert Kubernetes logs encoding to UTF-8'))
|
|
|
|
end
|
|
|
|
|
|
|
|
def split_logs(result)
|
|
|
|
result[:logs] = result[:logs].strip.lines(chomp: true).map do |line|
|
|
|
|
# message contains a RFC3339Nano timestamp, then a space, then the log line.
|
|
|
|
# resolution of the nanoseconds can vary, so we split on the first space
|
|
|
|
values = line.split(' ', 2)
|
|
|
|
{
|
|
|
|
timestamp: values[0],
|
2020-03-18 03:09:43 +00:00
|
|
|
message: values[1],
|
|
|
|
pod: result[:pod_name]
|
2020-03-11 15:09:37 +00:00
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
success(result)
|
|
|
|
end
|
|
|
|
|
|
|
|
def encode_utf8(logs)
|
|
|
|
utf8_logs = Gitlab::EncodingHelper.encode_utf8(logs.dup, replace: REPLACEMENT_CHAR)
|
|
|
|
|
|
|
|
# Gitlab::EncodingHelper.encode_utf8 can return '' or nil if an exception
|
|
|
|
# is raised while encoding. We prefer to return an error rather than wrongly
|
|
|
|
# display blank logs.
|
|
|
|
no_utf8_logs = logs.present? && utf8_logs.blank?
|
|
|
|
unexpected_encoding = utf8_logs&.encoding != Encoding::UTF_8
|
|
|
|
|
|
|
|
if no_utf8_logs || unexpected_encoding
|
|
|
|
raise EncodingHelperError, 'Could not convert Kubernetes logs to UTF-8'
|
|
|
|
end
|
|
|
|
|
|
|
|
utf8_logs
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|