# frozen_string_literal: true module PodLogs class KubernetesService < PodLogs::BaseService 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 self.reactive_cache_worker_finder = ->(id, _cache_key, namespace, params) { new(::Clusters::Cluster.find(id), namespace, params: params) } private 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 unless result[:pods].include?(result[:pod_name]) return error(_('Pod does not exist')) end success(result) end def check_container_name(result) pod_details = result[:raw_pods].find { |p| p.metadata.name == result[:pod_name] } containers = pod_details.spec.containers.map(&:name) # select first container if not specified result[:container_name] ||= containers.first 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 unless containers.include?(result[:container_name]) return error(_('Container does not exist')) end success(result) end 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], message: values[1], pod: result[:pod_name] } 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