Commit 3ace1e66 authored by Nick Thomas's avatar Nick Thomas

Merge branch 'ak/refactor-service' into 'master'

Split PodLogsService per backend

See merge request gitlab-org/gitlab!23619
parents 35f3601b cd3c7e76
......@@ -9,6 +9,8 @@ module ReactiveCaching
ExceededReactiveCacheLimit = Class.new(StandardError)
included do
extend ActiveModel::Naming
class_attribute :reactive_cache_key
class_attribute :reactive_cache_lease_timeout
class_attribute :reactive_cache_refresh_interval
......
......@@ -14,22 +14,22 @@ module Projects
end
def k8s
render_logs
render_logs(::PodLogs::KubernetesService, k8s_params)
end
def elasticsearch
render_logs
render_logs(::PodLogs::ElasticsearchService, elasticsearch_params)
end
private
def render_logs
def render_logs(service, permitted_params)
::Gitlab::UsageCounters::PodLogs.increment(project.id)
::Gitlab::PollingInterval.set_header(response, interval: 3_000)
result = PodLogsService.new(environment, params: filter_params).execute
result = service.new(environment, params: permitted_params).execute
if result[:status] == :processing
if result.nil?
head :accepted
elsif result[:status] == :success
render json: result
......@@ -42,7 +42,11 @@ module Projects
params.permit(:environment_name)
end
def filter_params
def k8s_params
params.permit(:container_name, :pod_name)
end
def elasticsearch_params
params.permit(:container_name, :pod_name, :search, :start, :end)
end
......
......@@ -6,11 +6,6 @@ module EE
module Kubernetes
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
include ::Gitlab::Utils::StrongMemoize
CACHE_KEY_GET_POD_LOG = 'get_pod_log'
LOGS_LIMIT = 500.freeze
override :calculate_reactive_cache_for
def calculate_reactive_cache_for(environment)
......@@ -37,144 +32,12 @@ module EE
::Gitlab::Kubernetes::RolloutStatus.from_deployments(*deployments, pods: pods, legacy_deployments: legacy_deployments)
end
def read_pod_logs(environment_id, pod_name, namespace, container: nil, search: nil, start_time: nil, end_time: nil)
# environment_id is required for use in reactive_cache_updated(),
# to invalidate the ETag cache.
with_reactive_cache(
CACHE_KEY_GET_POD_LOG,
'environment_id' => environment_id,
'pod_name' => pod_name,
'namespace' => namespace,
'container' => container,
'search' => search,
"start_time" => start_time,
"end_time" => end_time
) do |result|
result
end
end
def calculate_reactive_cache(request, opts)
case request
when CACHE_KEY_GET_POD_LOG
container = opts['container']
pod_name = opts['pod_name']
namespace = opts['namespace']
search = opts['search']
start_time = opts['start_time']
end_time = opts['end_time']
handle_exceptions(_('Pod not found'), pod_name: pod_name, container_name: container, search: search, start_time: start_time, end_time: end_time) do
container ||= container_names_of(pod_name, namespace).first
pod_logs(pod_name, namespace, container: container, search: search, start_time: start_time, end_time: end_time)
end
end
end
def reactive_cache_updated(request, opts)
super
case request
when CACHE_KEY_GET_POD_LOG
environment = ::Environment.find_by(id: opts['environment_id'])
return unless environment
method = elastic_stack_available? ? :elasticsearch_project_logs_path : :k8s_project_logs_path
::Gitlab::EtagCaching::Store.new.tap do |store|
store.touch(
# not using send with untrusted input, this is better for readability
# rubocop:disable GitlabSecurity/PublicSend
::Gitlab::Routing.url_helpers.send(
method,
environment.project,
environment_name: environment.name,
pod_name: opts['pod_name'],
container_name: opts['container_name'],
format: :json
)
)
end
end
end
def elastic_stack_available?
!!cluster.application_elastic_stack&.installed?
end
private
def pod_logs(pod_name, namespace, container: nil, search: nil, start_time: nil, end_time: nil)
logs = if elastic_stack_available?
elastic_stack_pod_logs(namespace, pod_name, container, search, start_time, end_time)
else
platform_pod_logs(namespace, pod_name, container)
end
{
logs: logs,
status: :success,
pod_name: pod_name,
container_name: container
}
end
def platform_pod_logs(namespace, pod_name, container_name)
logs = kubeclient.get_pod_log(
pod_name, namespace, container: container_name, tail_lines: LOGS_LIMIT, timestamps: true
).body
logs.strip.split("\n").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]
}
end
end
def elastic_stack_pod_logs(namespace, pod_name, container_name, search, start_time, end_time)
client = elastic_stack_client
return [] if client.nil?
::Gitlab::Elasticsearch::Logs.new(client).pod_logs(namespace, pod_name, container_name, search, start_time, end_time)
end
def elastic_stack_client
strong_memoize(:elastic_stack_client) do
cluster.application_elastic_stack&.elasticsearch_client
end
end
def handle_exceptions(resource_not_found_error_message, opts, &block)
yield
rescue Kubeclient::ResourceNotFoundError
{
error: resource_not_found_error_message,
status: :error
}.merge(opts)
rescue Kubeclient::HttpError => e
::Gitlab::ErrorTracking.track_exception(e)
{
error: _('Kubernetes API returned status code: %{error_code}') % {
error_code: e.error_code
},
status: :error
}.merge(opts)
end
def container_names_of(pod_name, namespace)
return [] unless pod_name.present?
pod_details = kubeclient.get_pod(pod_name, namespace)
pod_details.spec.containers.collect(&:name)
end
def read_deployments(namespace)
kubeclient.get_deployments(namespace: namespace).as_json
rescue Kubeclient::ResourceNotFoundError
......
......@@ -61,20 +61,6 @@ module EE
end
end
def pod_names
return [] unless rollout_status_available?
rollout_status = rollout_status_with_reactive_cache
# If cache has not been populated yet, rollout_status will be nil and the
# caller should try again later.
return unless rollout_status
rollout_status.instances.map do |instance|
instance[:pod_name]
end
end
def protected?
project.protected_environment_by_name(name).present?
end
......
# frozen_string_literal: true
module PodLogs
class BaseService < ::BaseService
include ReactiveCaching
include Stepable
attr_reader :environment, :params
CACHE_KEY_GET_POD_LOG = 'get_pod_log'
K8S_NAME_MAX_LENGTH = 253
SUCCESS_RETURN_KEYS = %i(status logs pod_name container_name pods).freeze
def id
environment.id
end
def initialize(environment, params: {})
@environment = environment
@params = filter_params(params.dup.stringify_keys).to_hash
end
def execute
with_reactive_cache(
CACHE_KEY_GET_POD_LOG,
params
) do |result|
result
end
end
def calculate_reactive_cache(request, _opts)
case request
when CACHE_KEY_GET_POD_LOG
execute_steps
else
exception = StandardError.new('Unknown reactive cache request')
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(exception, request: request)
error(_('Unknown cache key'))
end
end
def reactive_cache_updated(request, _opts)
case request
when CACHE_KEY_GET_POD_LOG
::Gitlab::EtagCaching::Store.new.tap do |store|
store.touch(etag_path)
end
end
end
private
def valid_params
%w(pod_name container_name)
end
def check_param_lengths(_result)
pod_name = params['pod_name'].presence
container_name = params['container_name'].presence
if 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 }))
elsif 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
success(pod_name: pod_name, container_name: container_name)
end
def check_deployment_platform(result)
unless environment.deployment_platform
return error(_('No deployment platform available'))
end
success(result)
end
def get_raw_pods(result)
namespace = environment.deployment_namespace
result[:raw_pods] = environment.deployment_platform.kubeclient.get_pods(namespace: namespace)
success(result)
end
def get_pod_names(result)
result[:pods] = result[:raw_pods].map(&:metadata).map(&:name)
success(result)
end
def check_pod_name(result)
# If pod_name is not received as parameter, get the pod logs of the first
# pod of this environment.
result[:pod_name] ||= result[:pods].first
unless result[:pod_name]
return error(_('No pods available'))
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].first { |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 containers.include?(result[:container_name])
return error(_('Container does not exist'))
end
success(result)
end
def pod_logs(result)
raise NotImplementedError
end
def etag_path
raise NotImplementedError
end
def filter_return_keys(result)
result.slice(*SUCCESS_RETURN_KEYS)
end
def filter_params(params)
params.slice(*valid_params)
end
end
end
# frozen_string_literal: true
module PodLogs
class ElasticsearchService < BaseService
steps :check_param_lengths,
:check_deployment_platform,
:get_raw_pods,
:get_pod_names,
:check_pod_name,
:check_container_name,
:check_times,
:check_search,
:pod_logs,
:filter_return_keys
self.reactive_cache_worker_finder = ->(id, _cache_key, params) { new(Environment.find(id), params: params) }
private
def valid_params
%w(pod_name container_name search start end)
end
def check_times(result)
result[:start] = params['start'] if params.key?('start') && Time.iso8601(params['start'])
result[:end] = params['end'] if params.key?('end') && Time.iso8601(params['end'])
success(result)
rescue ArgumentError
error(_('Invalid start or end time format'))
end
def check_search(result)
result[:search] = params['search'] if params.key?('search')
success(result)
end
def pod_logs(result)
namespace = environment.deployment_namespace
client = environment.deployment_platform.cluster&.application_elastic_stack&.elasticsearch_client
return error(_('Unable to connect to Elasticsearch')) unless client
result[:logs] = ::Gitlab::Elasticsearch::Logs.new(client).pod_logs(
namespace,
result[:pod_name],
result[:container_name],
result[:search],
result[:start],
result[:end]
)
success(result)
end
def etag_path
::Gitlab::Routing.url_helpers.elasticsearch_project_logs_path(
environment.project,
params.merge({
environment_name: environment.name,
format: :json
})
)
end
end
end
# frozen_string_literal: true
module PodLogs
class KubernetesService < BaseService
LOGS_LIMIT = 500.freeze
steps :check_param_lengths,
:check_deployment_platform,
:get_raw_pods,
:get_pod_names,
:check_pod_name,
:check_container_name,
:pod_logs,
:filter_return_keys
self.reactive_cache_worker_finder = ->(id, _cache_key, params) { new(Environment.find(id), params: params) }
private
def pod_logs(result)
logs = environment.deployment_platform.kubeclient.get_pod_log(
result[:pod_name],
environment.deployment_namespace,
container: result[:container_name],
tail_lines: LOGS_LIMIT,
timestamps: true
).body
result[:logs] = 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]
}
end
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
def etag_path
::Gitlab::Routing.url_helpers.k8s_project_logs_path(
environment.project,
params.merge({
environment_name: environment.name,
format: :json
})
)
end
end
end
# frozen_string_literal: true
class PodLogsService < ::BaseService
include Stepable
attr_reader :environment
K8S_NAME_MAX_LENGTH = 253
PARAMS = %w(pod_name container_name search start end).freeze
SUCCESS_RETURN_KEYS = [:status, :logs, :pod_name, :container_name, :pods].freeze
steps :check_param_lengths,
:check_deployment_platform,
:check_pod_names,
:check_pod_name,
:check_times,
:pod_logs,
:filter_return_keys
def initialize(environment, params: {})
@environment = environment
@params = filter_params(params.dup).to_hash
end
def execute
execute_steps
end
private
def check_param_lengths(_result)
pod_name = params['pod_name'].presence
container_name = params['container_name'].presence
if 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 }))
elsif 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
success(pod_name: pod_name, container_name: container_name)
end
def check_deployment_platform(result)
unless environment.deployment_platform
return error(_('No deployment platform available'))
end
success(result)
end
def check_pod_names(result)
result[:pods] = environment.pod_names
return { status: :processing } unless result[:pods]
success(result)
end
def check_pod_name(result)
# If pod_name is not received as parameter, get the pod logs of the first
# pod of this environment.
result[:pod_name] ||= result[:pods].first
unless result[:pod_name]
return error(_('No pods available'))
end
success(result)
end
def check_times(result)
Time.iso8601(params['start']) if params['start']
Time.iso8601(params['end']) if params['end']
success(result)
rescue ArgumentError
error(_('Invalid start or end time format'))
end
def pod_logs(result)
response = environment.deployment_platform.read_pod_logs(
environment.id,
result[:pod_name],
namespace,
container: result[:container_name],
search: params['search'],
start_time: params['start'],
end_time: params['end']
)
return { status: :processing } unless response
result.merge!(response.slice(:pod_name, :container_name, :logs))
if response[:status] == :error
error(response[:error]).reverse_merge(result)
else
success(result)
end
end
def filter_return_keys(result)
result.slice(*SUCCESS_RETURN_KEYS)
end
def filter_params(params)
params.slice(*PARAMS)
end
def namespace
environment.deployment_namespace
end
end
......@@ -73,7 +73,7 @@ describe Projects::LogsController do
before do
stub_licensed_features(pod_logs: true)
allow_any_instance_of(PodLogsService).to receive(:execute).and_return(service_result)
allow_any_instance_of(::PodLogs::KubernetesService).to receive(:execute).and_return(service_result)
end
shared_examples 'resource not found' do |message|
......@@ -159,8 +159,8 @@ describe Projects::LogsController do
end
end
context 'when service returns status processing' do
let(:service_result) { { status: :processing } }
context 'when service is processing (returns nil)' do
let(:service_result) { nil }
it 'renders accepted' do
get :k8s, params: environment_params(pod_name: pod_name, format: :json)
......
......@@ -7,7 +7,7 @@ describe 'Environment > Pod Logs', :js do
SCROLL_DISTANCE = 400
let(:pod_names) { %w(foo bar) }
let(:pod_names) { %w(kube-pod) }
let(:pod_name) { pod_names.first }
let(:project) { create(:project, :repository) }
let(:environment) { create(:environment, project: project) }
......@@ -19,18 +19,9 @@ describe 'Environment > Pod Logs', :js do
create(:cluster, :provided_by_gcp, environment_scope: '*', projects: [project])
create(:deployment, :success, environment: environment)
stub_kubeclient_pod_details(pod_name, environment.deployment_namespace)
stub_kubeclient_pods(environment.deployment_namespace)
stub_kubeclient_logs(pod_name, environment.deployment_namespace, container: 'container-0')
# rollout_status_instances = [{ pod_name: foo }, {pod_name: bar}]
rollout_status_instances = pod_names.collect { |name| { pod_name: name } }
rollout_status = instance_double(
::Gitlab::Kubernetes::RolloutStatus, instances: rollout_status_instances
)
allow_any_instance_of(EE::Environment).to receive(:rollout_status_with_reactive_cache)
.and_return(rollout_status)
sign_in(project.owner)
end
......@@ -64,7 +55,7 @@ describe 'Environment > Pod Logs', :js do
find(".dropdown-menu-toggle:not([disabled])").click
dropdown_items = find(".dropdown-menu").all(".dropdown-item")
expect(dropdown_items.size).to eq(2)
expect(dropdown_items.size).to eq(1)
dropdown_items.each_with_index do |item, i|
expect(item.text).to eq(pod_names[i])
......
......@@ -134,214 +134,6 @@ describe Clusters::Platforms::Kubernetes do
end
end
describe '#read_pod_logs' do
let(:environment) { create(:environment) }
let(:cluster) { create(:cluster, :project, platform_kubernetes: service) }
let(:service) { create(:cluster_platform_kubernetes, :configured) }
let(:pod_name) { 'pod-1' }
let(:namespace) { 'app' }
let(:container) { 'some-container' }
let(:expected_logs) do
[
{ message: "Log 1", timestamp: "2019-12-13T14:04:22.123456Z" },
{ message: "Log 2", timestamp: "2019-12-13T14:04:23.123456Z" },
{ message: "Log 3", timestamp: "2019-12-13T14:04:24.123456Z" }
]
end
subject { service.read_pod_logs(environment.id, pod_name, namespace, container: container) }
shared_examples 'successful log request' do
it do
expect(subject[:logs]).to eq(expected_logs)
expect(subject[:status]).to eq(:success)
expect(subject[:pod_name]).to eq(pod_name)
expect(subject[:container_name]).to eq(container)
end
end
shared_examples 'returns pod_name and container_name' do
it do
expect(subject[:pod_name]).to eq(pod_name)
expect(subject[:container_name]).to eq(container)
end
end
context 'with reactive cache' do
before do
synchronous_reactive_cache(service)
end
context 'when ElasticSearch is enabled' do
let(:cluster) { create(:cluster, :project, platform_kubernetes: service) }
let!(:elastic_stack) { create(:clusters_applications_elastic_stack, :installed, cluster: cluster) }
before do
expect_any_instance_of(::Clusters::Applications::ElasticStack).to receive(:elasticsearch_client).at_least(:once).and_return(Elasticsearch::Transport::Client.new)
expect_any_instance_of(::Gitlab::Elasticsearch::Logs).to receive(:pod_logs).and_return(expected_logs)
end
include_examples 'successful log request'
end
context 'when kubernetes responds with valid logs' do
before do
stub_kubeclient_logs(pod_name, namespace, container: container)
end
context 'on a project level cluster' do
let(:cluster) { create(:cluster, :project, platform_kubernetes: service) }
include_examples 'successful log request'
end
context 'on a group level cluster' do
let(:cluster) { create(:cluster, :group, platform_kubernetes: service) }
include_examples 'successful log request'
end
context 'on an instance level cluster' do
let(:cluster) { create(:cluster, :instance, platform_kubernetes: service) }
include_examples 'successful log request'
end
end
context 'when kubernetes responds with 500s' do
before do
stub_kubeclient_logs(pod_name, namespace, container: 'some-container', status: 500)
end
it_behaves_like 'kubernetes API error', 500
it_behaves_like 'returns pod_name and container_name'
end
context 'when container does not exist' do
before do
container = 'some-container'
stub_kubeclient_logs(pod_name, namespace, container: container,
status: 400, message: "container #{container} is not valid for pod #{pod_name}")
end
it_behaves_like 'kubernetes API error', 400
it_behaves_like 'returns pod_name and container_name'
end
context 'when kubernetes responds with 404s' do
before do
stub_kubeclient_logs(pod_name, namespace, container: 'some-container', status: 404)
end
it_behaves_like 'resource not found error', 'Pod not found'
it_behaves_like 'returns pod_name and container_name'
end
context 'when container name is not specified' do
let(:container) { 'container-0' }
subject { service.read_pod_logs(environment.id, pod_name, namespace) }
before do
stub_kubeclient_pod_details(pod_name, namespace)
stub_kubeclient_logs(pod_name, namespace, container: container)
end
include_examples 'successful log request'
end
end
context 'with caching', :use_clean_rails_memory_store_caching do
let(:opts) do
[
'get_pod_log',
{
'environment_id' => environment.id,
'pod_name' => pod_name,
'namespace' => namespace,
'container' => container,
'search' => nil,
'start_time' => nil,
'end_time' => nil
}
]
end
context 'result is cacheable' do
before do
stub_kubeclient_logs(pod_name, namespace, container: container)
end
it do
result = subject
expect { stub_reactive_cache(service, result, opts) }.not_to raise_error
end
end
context 'when value present in cache' do
let(:return_value) { { 'status' => :success, 'logs' => 'logs' } }
before do
stub_reactive_cache(service, return_value, opts)
end
it 'returns cached value' do
result = subject
expect(result).to eq(return_value)
end
end
context 'when value not present in cache' do
it 'returns nil' do
expect(ReactiveCachingWorker)
.to receive(:perform_async)
.with(service.class, service.id, *opts)
result = subject
expect(result).to eq(nil)
end
end
end
describe '#reactive_cache_updated' do
let(:opts) do
{
'environment_id' => environment.id,
'pod_name' => pod_name,
'namespace' => namespace,
'container' => container
}
end
subject { service.reactive_cache_updated('get_pod_log', opts) }
it 'expires k8s_pod_logs etag cache' do
expect_next_instance_of(Gitlab::EtagCaching::Store) do |store|
expect(store).to receive(:touch)
.with(
::Gitlab::Routing.url_helpers.k8s_project_logs_path(
environment.project,
environment_name: environment.name,
pod_name: opts['pod_name'],
container_name: opts['container_name'],
format: :json
)
)
.and_call_original
end
subject
end
end
end
describe '#calculate_reactive_cache_for' do
let(:cluster) { create(:cluster, :project, platform_kubernetes: service) }
let(:service) { create(:cluster_platform_kubernetes, :configured) }
......
......@@ -57,31 +57,6 @@ describe Environment, :use_clean_rails_memory_store_caching do
end
end
describe '#pod_names' do
context 'when environment does not have a rollout status' do
it 'returns an empty array' do
expect(environment.pod_names).to eq([])
end
end
context 'when environment has a rollout status' do
let(:pod_name) { 'pod_1' }
let(:rollout_status) { instance_double(::Gitlab::Kubernetes::RolloutStatus, instances: [{ pod_name: pod_name }]) }
before do
create(:cluster, :provided_by_gcp, environment_scope: '*', projects: [project])
create(:deployment, :success, environment: environment)
end
it 'returns the pod_names' do
allow(environment).to receive(:rollout_status_with_reactive_cache)
.and_return(rollout_status)
expect(environment.pod_names).to eq([pod_name])
end
end
end
describe '#protected?' do
subject { environment.protected? }
......
# frozen_string_literal: true
require 'spec_helper'
describe ::PodLogs::BaseService do
include KubernetesHelpers
let_it_be(:environment, refind: true) { create(:environment) }
let(:pod_name) { 'pod-1' }
let(:container_name) { 'container-0' }
let(:params) { {} }
let(:raw_pods) do
JSON.parse([
kube_pod(name: pod_name)
].to_json, object_class: OpenStruct)
end
subject { described_class.new(environment, params: params) }
describe '#initialize' do
let(:params) do
{
'container_name' => container_name,
'another_param' => 'foo'
}
end
it 'filters the parameters' do
expect(subject.environment).to eq(environment)
expect(subject.params).to eq({
'container_name' => container_name
})
expect(subject.params.equal?(params)).to be(false)
end
end
describe '#check_param_lengths' do
context 'when pod_name and container_name are provided' do
let(:params) do
{
'pod_name' => pod_name,
'container_name' => container_name
}
end
it 'returns success' do
result = subject.send(:check_param_lengths, {})
expect(result[:status]).to eq(:success)
expect(result[:pod_name]).to eq(pod_name)
expect(result[:container_name]).to eq(container_name)
end
end
context 'when pod_name is too long' do
let(:params) do
{
'pod_name' => "a very long string." * 15
}
end
it 'returns an error' do
result = subject.send(:check_param_lengths, {})
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('pod_name cannot be larger than 253 chars')
end
end
context 'when container_name is too long' do
let(:params) do
{
'container_name' => "a very long string." * 15
}
end
it 'returns an error' do
result = subject.send(:check_param_lengths, {})
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('container_name cannot be larger than 253 chars')
end
end
end
describe '#check_deployment_platform' do
it 'returns success when deployment platform exist' do
create(:cluster, :provided_by_gcp, environment_scope: '*', projects: [environment.project])
create(:deployment, :success, environment: environment)
result = subject.send(:check_deployment_platform, {})
expect(result[:status]).to eq(:success)
end
it 'returns error when deployment platform does not exist' do
result = subject.send(:check_deployment_platform, {})
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('No deployment platform available')
end
end
describe '#get_raw_pods' do
let(:service) { create(:cluster_platform_kubernetes, :configured) }
it 'returns success with passthrough k8s response' do
create(:cluster, :provided_by_gcp, environment_scope: '*', projects: [environment.project])
create(:deployment, :success, environment: environment)
stub_kubeclient_pods(environment.deployment_namespace)
result = subject.send(:get_raw_pods, {})
expect(result[:status]).to eq(:success)
expect(result[:raw_pods].first).to be_a(Kubeclient::Resource)
end
end
describe '#get_pod_names' do
it 'returns success with a list of pods' do
result = subject.send(:get_pod_names, raw_pods: raw_pods)
expect(result[:status]).to eq(:success)
expect(result[:pods]).to eq([pod_name])
end
end
describe '#check_pod_name' do
it 'returns success if pod_name was specified' do
result = subject.send(:check_pod_name, pod_name: pod_name, pods: [pod_name])
expect(result[:status]).to eq(:success)
expect(result[:pod_name]).to eq(pod_name)
end
it 'returns success if pod_name was not specified but there are pods' do
result = subject.send(:check_pod_name, pod_name: nil, pods: [pod_name])
expect(result[:status]).to eq(:success)
expect(result[:pod_name]).to eq(pod_name)
end
it 'returns error if pod_name was not specified and there are no pods' do
result = subject.send(:check_pod_name, pod_name: nil, pods: [])
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('No pods available')
end
it 'returns error if pod_name was specified but does not exist' do
result = subject.send(:check_pod_name, pod_name: 'another_pod', pods: [pod_name])
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Pod does not exist')
end
end
describe '#check_container_name' do
it 'returns success if container_name was specified' do
result = subject.send(:check_container_name,
container_name: container_name,
pod_name: pod_name,
raw_pods: raw_pods
)
expect(result[:status]).to eq(:success)
expect(result[:container_name]).to eq(container_name)
end
it 'returns success if container_name was not specified and there are containers' do
result = subject.send(:check_container_name,
pod_name: pod_name,
raw_pods: raw_pods
)
expect(result[:status]).to eq(:success)
expect(result[:container_name]).to eq(container_name)
end
it 'returns error if container_name was not specified and there are no containers on the pod' do
raw_pods.first.spec.containers = []
result = subject.send(:check_container_name,
pod_name: pod_name,
raw_pods: raw_pods
)
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('No containers available')
end
it 'returns error if container_name was specified but does not exist' do
result = subject.send(:check_container_name,
container_name: 'foo',
pod_name: pod_name,
raw_pods: raw_pods
)
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Container does not exist')
end
end
describe '#reactive_cache_updated' do
context 'get_pod_log' do
let(:cache_key) { 'get_pod_log' }
it 'expires k8s_pod_logs etag cache' do
expected_path = "/root/autodevops-deploy/-/logs/k8s.json"
allow(subject).to receive(:etag_path).and_return(expected_path)
allow_next_instance_of(Gitlab::EtagCaching::Store) do |store|
allow(store).to receive(:touch).with(expected_path).and_call_original
end
subject.reactive_cache_updated(cache_key, {})
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe ::PodLogs::ElasticsearchService do
let_it_be(:environment, refind: true) { create(:environment) }
let(:pod_name) { 'pod-1' }
let(:container_name) { 'container-1' }
let(:search) { 'foo -bar' }
let(:start_time) { '2019-01-02T12:13:14+02:00' }
let(:end_time) { '2019-01-03T12:13:14+02:00' }
let(:params) { {} }
let(:expected_logs) do
[
{ message: "Log 1", timestamp: "2019-12-13T14:04:22.123456Z" },
{ message: "Log 2", timestamp: "2019-12-13T14:04:23.123456Z" },
{ message: "Log 3", timestamp: "2019-12-13T14:04:24.123456Z" }
]
end
subject { described_class.new(environment, params: params) }
describe '#check_times' do
context 'with start and end provided and valid' do
let(:params) do
{
'start' => start_time,
'end' => end_time
}
end
it 'returns success with times' do
result = subject.send(:check_times, {})
expect(result[:status]).to eq(:success)
expect(result[:start]).to eq(start_time)
expect(result[:end]).to eq(end_time)
end
end
context 'with start and end not provided' do
let(:params) do
{}
end
it 'returns success with nothing else' do
result = subject.send(:check_times, {})
expect(result.keys.length).to eq(1)
expect(result[:status]).to eq(:success)
end
end
context 'with start valid and end invalid' do
let(:params) do
{
'start' => start_time,
'end' => 'invalid date'
}
end
it 'returns error' do
result = subject.send(:check_times, {})
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Invalid start or end time format')
end
end
context 'with start invalid and end valid' do
let(:params) do
{
'start' => 'invalid date',
'end' => end_time
}
end
it 'returns error' do
result = subject.send(:check_times, {})
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Invalid start or end time format')
end
end
end
describe '#check_search' do
context 'with search provided and valid' do
let(:params) do
{
'search' => search
}
end
it 'returns success with search' do
result = subject.send(:check_search, {})
expect(result[:status]).to eq(:success)
expect(result[:search]).to eq(search)
end
end
context 'with search not provided' do
let(:params) do
{}
end
it 'returns success with nothing else' do
result = subject.send(:check_search, {})
expect(result.keys.length).to eq(1)
expect(result[:status]).to eq(:success)
end
end
end
describe '#pod_logs' do
let(:cluster) { create(:cluster, :provided_by_gcp, environment_scope: '*', projects: [environment.project]) }
let(:result_arg) do
{
pod_name: pod_name,
container_name: container_name,
search: search,
start: start_time,
end: end_time
}
end
before do
create(:deployment, :success, environment: environment)
create(:clusters_applications_elastic_stack, :installed, cluster: cluster)
end
it 'returns the logs' do
allow_any_instance_of(::Clusters::Applications::ElasticStack)
.to receive(:elasticsearch_client)
.and_return(Elasticsearch::Transport::Client.new)
allow_any_instance_of(::Gitlab::Elasticsearch::Logs)
.to receive(:pod_logs)
.with(environment.deployment_namespace, pod_name, container_name, search, start_time, end_time)
.and_return(expected_logs)
result = subject.send(:pod_logs, result_arg)
expect(result[:status]).to eq(:success)
expect(result[:logs]).to eq(expected_logs)
end
it 'returns an error when ES is unreachable' do
allow_any_instance_of(::Clusters::Applications::ElasticStack)
.to receive(:elasticsearch_client)
.and_return(nil)
result = subject.send(:pod_logs, result_arg)
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Unable to connect to Elasticsearch')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe ::PodLogs::KubernetesService do
include KubernetesHelpers
let_it_be(:environment, refind: true) { create(:environment) }
let(:pod_name) { 'pod-1' }
let(:container_name) { 'container-1' }
let(:params) { {} }
let(:expected_logs) do
[
{ message: "Log 1", timestamp: "2019-12-13T14:04:22.123456Z" },
{ message: "Log 2", timestamp: "2019-12-13T14:04:23.123456Z" },
{ message: "Log 3", timestamp: "2019-12-13T14:04:24.123456Z" }
]
end
subject { described_class.new(environment, params: params) }
describe '#pod_logs' do
let(:result_arg) do
{
pod_name: pod_name,
container_name: container_name
}
end
let(:service) { create(:cluster_platform_kubernetes, :configured) }
before do
create(:cluster, :provided_by_gcp, environment_scope: '*', projects: [environment.project])
create(:deployment, :success, environment: environment)
end
it 'returns the logs' do
stub_kubeclient_logs(pod_name, environment.deployment_namespace, container: container_name)
result = subject.send(:pod_logs, result_arg)
expect(result[:status]).to eq(:success)
expect(result[:logs]).to eq(expected_logs)
end
it 'handles Not Found errors from k8s' do
allow_any_instance_of(Gitlab::Kubernetes::KubeClient)
.to receive(:get_pod_log)
.with(any_args)
.and_raise(Kubeclient::ResourceNotFoundError.new(404, 'Not Found', {}))
result = subject.send(:pod_logs, result_arg)
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Pod not found')
end
it 'handles HTTP errors from k8s' do
allow_any_instance_of(Gitlab::Kubernetes::KubeClient)
.to receive(:get_pod_log)
.with(any_args)
.and_raise(Kubeclient::HttpError.new(500, 'Error', {}))
result = subject.send(:pod_logs, result_arg)
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Kubernetes API returned status code: 500')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe PodLogsService do
include KubernetesHelpers
include ReactiveCachingHelpers
describe '#execute' do
let(:environment) { create(:environment, name: 'production') }
let(:project) { environment.project }
let(:pod_name) { 'pod-1' }
let(:response_pod_name) { pod_name }
let(:pods) { [pod_name] }
let(:container_name) { 'container-1' }
let(:search) { nil }
let(:logs) { ['Log 1', 'Log 2', 'Log 3'] }
let(:result) { subject.execute }
let(:start_time) { nil }
let(:end_time) { nil }
let(:params) do
ActionController::Parameters.new(
{
'pod_name' => pod_name,
'container_name' => container_name,
'search' => search,
'start' => start_time,
'end' => end_time
}
).permit!
end
subject { described_class.new(environment, params: params) }
shared_examples 'success' do |message|
it do
expect(result[:status]).to eq(:success)
expect(result[:logs]).to eq(logs)
expect(result[:pods]).to eq(pods)
expect(result[:pod_name]).to eq(response_pod_name)
expect(result[:container_name]).to eq(container_name)
end
end
shared_examples 'error' do |message|
it do
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq(message)
end
end
shared_examples 'returns pod_name and container_name' do
it do
expect(result[:pod_name]).to eq(response_pod_name)
expect(result[:container_name]).to eq(container_name)
end
end
shared_context 'return error' do |message|
before do
allow_any_instance_of(EE::Clusters::Platforms::Kubernetes).to receive(:read_pod_logs)
.with(environment.id, pod_name, environment.deployment_namespace, container: container_name, search: search, start_time: start_time, end_time: end_time)
.and_return({
status: :error,
error: message,
pod_name: response_pod_name,
container_name: container_name
})
end
end
shared_context 'return success' do
before do
allow_any_instance_of(EE::Clusters::Platforms::Kubernetes).to receive(:read_pod_logs)
.with(environment.id, response_pod_name, environment.deployment_namespace, container: container_name, search: search, start_time: start_time, end_time: end_time)
.and_return({
status: :success,
logs: ["Log 1", "Log 2", "Log 3"],
pod_name: response_pod_name,
container_name: container_name
})
end
end
context 'when pod name is too large' do
let(:pod_name) { '1' * 254 }
it_behaves_like 'error', 'pod_name cannot be larger than 253 chars'
end
context 'when container name is too large' do
let(:container_name) { '1' * 254 }
it_behaves_like 'error', 'container_name cannot be larger than 253 chars'
end
context 'without deployment platform' do
it_behaves_like 'error', 'No deployment platform available'
end
context 'with deployment platform' do
let(:rollout_status) do
instance_double(::Gitlab::Kubernetes::RolloutStatus, instances: [{ pod_name: response_pod_name }])
end
before do
create(:cluster, :provided_by_gcp,
environment_scope: '*', projects: [project])
create(:deployment, :success, environment: environment)
allow(environment).to receive(:rollout_status_with_reactive_cache)
.and_return(rollout_status)
end
context 'when pod does not exist' do
include_context 'return error', 'Pod not found'
it_behaves_like 'error', 'Pod not found'
it_behaves_like 'returns pod_name and container_name'
end
context 'when container_name is specified' do
include_context 'return success'
it_behaves_like 'success'
end
context 'when container_name is not specified' do
let(:container_name) { nil }
let(:params) do
ActionController::Parameters.new(
{
'pod_name' => pod_name,
'container_name' => nil
}
).permit!
end
include_context 'return success'
it_behaves_like 'success'
end
context 'when pod_name is not specified' do
let(:pod_name) { '' }
let(:container_name) { nil }
let(:first_pod_name) { 'some-pod' }
let(:pods) { [first_pod_name] }
let(:response_pod_name) { first_pod_name }
include_context 'return success'
it_behaves_like 'success'
it 'returns logs of first pod' do
expect_any_instance_of(EE::Clusters::Platforms::Kubernetes).to receive(:read_pod_logs)
.with(environment.id, first_pod_name, environment.deployment_namespace, container: nil, search: search, start_time: start_time, end_time: end_time)
subject.execute
end
context 'when there are no pods' do
let(:rollout_status) { instance_double(::Gitlab::Kubernetes::RolloutStatus, instances: []) }
it 'returns error' do
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('No pods available')
end
end
context 'when rollout_status cache is empty' do
before do
allow(environment).to receive(:rollout_status_with_reactive_cache)
.and_return(nil)
end
it 'returns nil' do
expect(subject.execute).to eq(status: :processing, last_step: :check_pod_names)
end
end
end
context 'when search is specified' do
let(:pod_name) { 'some-pod' }
let(:container_name) { nil }
let(:search) { 'foo +bar' }
include_context 'return success'
it_behaves_like 'success'
end
context 'when start and end time is specified' do
let(:pod_name) { 'some-pod' }
let(:container_name) { nil }
let(:start_time) { '2019-12-13T14:35:34.034Z' }
let(:end_time) { '2019-12-13T14:35:34.034Z' }
include_context 'return success'
it_behaves_like 'success'
end
context 'when start and end time are invalid' do
let(:pod_name) { 'some-pod' }
let(:container_name) { nil }
let(:start_time) { '1' }
let(:end_time) { '2' }
it_behaves_like 'error', 'Invalid start or end time format'
end
context 'when error is returned' do
include_context 'return error', 'Kubernetes API returned status code: 400'
it_behaves_like 'error', 'Kubernetes API returned status code: 400'
it_behaves_like 'returns pod_name and container_name'
end
context 'when nil is returned' do
before do
allow_any_instance_of(EE::Clusters::Platforms::Kubernetes).to receive(:read_pod_logs)
.with(environment.id, pod_name, environment.deployment_namespace, container: container_name, search: search, start_time: start_time, end_time: end_time)
.and_return(nil)
end
it 'returns processing' do
expect(result).to eq(status: :processing, last_step: :pod_logs)
end
end
end
end
end
......@@ -59,7 +59,7 @@ module API
requires :token, type: String, desc: 'Token to authenticate against Kubernetes'
optional :ca_cert, type: String, desc: 'TLS certificate (needed if API is using a self-signed TLS certificate)'
optional :namespace, type: String, desc: 'Unique namespace related to Group'
optional :authorization_type, type: String, values: Clusters::Platforms::Kubernetes.authorization_types.keys, default: 'rbac', desc: 'Cluster authorization type, defaults to RBAC'
optional :authorization_type, type: String, values: ::Clusters::Platforms::Kubernetes.authorization_types.keys, default: 'rbac', desc: 'Cluster authorization type, defaults to RBAC'
end
use :create_params_ee
end
......@@ -96,7 +96,7 @@ module API
put ':id/clusters/:cluster_id' do
authorize! :update_cluster, cluster
update_service = Clusters::UpdateService.new(current_user, update_cluster_params)
update_service = ::Clusters::UpdateService.new(current_user, update_cluster_params)
if update_service.execute(cluster)
present cluster, with: Entities::ClusterGroup
......
......@@ -62,7 +62,7 @@ module API
requires :token, type: String, desc: 'Token to authenticate against Kubernetes'
optional :ca_cert, type: String, desc: 'TLS certificate (needed if API is using a self-signed TLS certificate)'
optional :namespace, type: String, desc: 'Unique namespace related to Project'
optional :authorization_type, type: String, values: Clusters::Platforms::Kubernetes.authorization_types.keys, default: 'rbac', desc: 'Cluster authorization type, defaults to RBAC'
optional :authorization_type, type: String, values: ::Clusters::Platforms::Kubernetes.authorization_types.keys, default: 'rbac', desc: 'Cluster authorization type, defaults to RBAC'
end
use :create_params_ee
end
......@@ -100,7 +100,7 @@ module API
put ':id/clusters/:cluster_id' do
authorize! :update_cluster, cluster
update_service = Clusters::UpdateService.new(current_user, update_cluster_params)
update_service = ::Clusters::UpdateService.new(current_user, update_cluster_params)
if update_service.execute(cluster)
present cluster, with: Entities::ClusterProject
......
......@@ -5062,6 +5062,9 @@ msgstr ""
msgid "Container Scanning"
msgstr ""
msgid "Container does not exist"
msgstr ""
msgid "Container registry images"
msgstr ""
......@@ -12680,6 +12683,9 @@ msgstr ""
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
msgid "No containers available"
msgstr ""
msgid "No contributions"
msgstr ""
......@@ -14026,6 +14032,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
msgid "Pod does not exist"
msgstr ""
msgid "Pod logs"
msgstr ""
......@@ -20431,6 +20440,9 @@ msgstr ""
msgid "Unable to collect memory info"
msgstr ""
msgid "Unable to connect to Elasticsearch"
msgstr ""
msgid "Unable to connect to Prometheus server"
msgstr ""
......@@ -20500,6 +20512,9 @@ msgstr ""
msgid "Unknown Error"
msgstr ""
msgid "Unknown cache key"
msgstr ""
msgid "Unknown encryption strategy: %{encrypted_strategy}!"
msgstr ""
......
......@@ -101,7 +101,7 @@ module KubernetesHelpers
end
logs_url = service.api_url + "/api/v1/namespaces/#{namespace}/pods/#{pod_name}" \
"/log?#{container_query_param}tailLines=#{Clusters::Platforms::Kubernetes::LOGS_LIMIT}&timestamps=true"
"/log?#{container_query_param}tailLines=#{::PodLogs::KubernetesService::LOGS_LIMIT}&timestamps=true"
if status
response = { status: status }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment