Commit cd3c7e76 authored by Adrien Kohlbecker's avatar Adrien Kohlbecker Committed by Nick Thomas

Split PodLogsService per backend

parent 6b95c3f9
...@@ -9,6 +9,8 @@ module ReactiveCaching ...@@ -9,6 +9,8 @@ module ReactiveCaching
ExceededReactiveCacheLimit = Class.new(StandardError) ExceededReactiveCacheLimit = Class.new(StandardError)
included do included do
extend ActiveModel::Naming
class_attribute :reactive_cache_key class_attribute :reactive_cache_key
class_attribute :reactive_cache_lease_timeout class_attribute :reactive_cache_lease_timeout
class_attribute :reactive_cache_refresh_interval class_attribute :reactive_cache_refresh_interval
......
...@@ -14,22 +14,22 @@ module Projects ...@@ -14,22 +14,22 @@ module Projects
end end
def k8s def k8s
render_logs render_logs(::PodLogs::KubernetesService, k8s_params)
end end
def elasticsearch def elasticsearch
render_logs render_logs(::PodLogs::ElasticsearchService, elasticsearch_params)
end end
private private
def render_logs def render_logs(service, permitted_params)
::Gitlab::UsageCounters::PodLogs.increment(project.id) ::Gitlab::UsageCounters::PodLogs.increment(project.id)
::Gitlab::PollingInterval.set_header(response, interval: 3_000) ::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 head :accepted
elsif result[:status] == :success elsif result[:status] == :success
render json: result render json: result
...@@ -42,7 +42,11 @@ module Projects ...@@ -42,7 +42,11 @@ module Projects
params.permit(:environment_name) params.permit(:environment_name)
end 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) params.permit(:container_name, :pod_name, :search, :start, :end)
end end
......
...@@ -6,11 +6,6 @@ module EE ...@@ -6,11 +6,6 @@ module EE
module Kubernetes module Kubernetes
extend ActiveSupport::Concern extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override 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 override :calculate_reactive_cache_for
def calculate_reactive_cache_for(environment) def calculate_reactive_cache_for(environment)
...@@ -37,144 +32,12 @@ module EE ...@@ -37,144 +32,12 @@ module EE
::Gitlab::Kubernetes::RolloutStatus.from_deployments(*deployments, pods: pods, legacy_deployments: legacy_deployments) ::Gitlab::Kubernetes::RolloutStatus.from_deployments(*deployments, pods: pods, legacy_deployments: legacy_deployments)
end 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? def elastic_stack_available?
!!cluster.application_elastic_stack&.installed? !!cluster.application_elastic_stack&.installed?
end end
private 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) def read_deployments(namespace)
kubeclient.get_deployments(namespace: namespace).as_json kubeclient.get_deployments(namespace: namespace).as_json
rescue Kubeclient::ResourceNotFoundError rescue Kubeclient::ResourceNotFoundError
......
...@@ -61,20 +61,6 @@ module EE ...@@ -61,20 +61,6 @@ module EE
end end
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? def protected?
project.protected_environment_by_name(name).present? project.protected_environment_by_name(name).present?
end 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 ...@@ -73,7 +73,7 @@ describe Projects::LogsController do
before do before do
stub_licensed_features(pod_logs: true) 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 end
shared_examples 'resource not found' do |message| shared_examples 'resource not found' do |message|
...@@ -159,8 +159,8 @@ describe Projects::LogsController do ...@@ -159,8 +159,8 @@ describe Projects::LogsController do
end end
end end
context 'when service returns status processing' do context 'when service is processing (returns nil)' do
let(:service_result) { { status: :processing } } let(:service_result) { nil }
it 'renders accepted' do it 'renders accepted' do
get :k8s, params: environment_params(pod_name: pod_name, format: :json) get :k8s, params: environment_params(pod_name: pod_name, format: :json)
......
...@@ -7,7 +7,7 @@ describe 'Environment > Pod Logs', :js do ...@@ -7,7 +7,7 @@ describe 'Environment > Pod Logs', :js do
SCROLL_DISTANCE = 400 SCROLL_DISTANCE = 400
let(:pod_names) { %w(foo bar) } let(:pod_names) { %w(kube-pod) }
let(:pod_name) { pod_names.first } let(:pod_name) { pod_names.first }
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:environment) { create(:environment, project: project) } let(:environment) { create(:environment, project: project) }
...@@ -19,18 +19,9 @@ describe 'Environment > Pod Logs', :js do ...@@ -19,18 +19,9 @@ describe 'Environment > Pod Logs', :js do
create(:cluster, :provided_by_gcp, environment_scope: '*', projects: [project]) create(:cluster, :provided_by_gcp, environment_scope: '*', projects: [project])
create(:deployment, :success, environment: environment) 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') 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) sign_in(project.owner)
end end
...@@ -64,7 +55,7 @@ describe 'Environment > Pod Logs', :js do ...@@ -64,7 +55,7 @@ describe 'Environment > Pod Logs', :js do
find(".dropdown-menu-toggle:not([disabled])").click find(".dropdown-menu-toggle:not([disabled])").click
dropdown_items = find(".dropdown-menu").all(".dropdown-item") 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| dropdown_items.each_with_index do |item, i|
expect(item.text).to eq(pod_names[i]) expect(item.text).to eq(pod_names[i])
......
...@@ -134,214 +134,6 @@ describe Clusters::Platforms::Kubernetes do ...@@ -134,214 +134,6 @@ describe Clusters::Platforms::Kubernetes do
end end
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 describe '#calculate_reactive_cache_for' do
let(:cluster) { create(:cluster, :project, platform_kubernetes: service) } let(:cluster) { create(:cluster, :project, platform_kubernetes: service) }
let(:service) { create(:cluster_platform_kubernetes, :configured) } let(:service) { create(:cluster_platform_kubernetes, :configured) }
......
...@@ -57,31 +57,6 @@ describe Environment, :use_clean_rails_memory_store_caching do ...@@ -57,31 +57,6 @@ describe Environment, :use_clean_rails_memory_store_caching do
end end
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 describe '#protected?' do
subject { environment.protected? } 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 ...@@ -59,7 +59,7 @@ module API
requires :token, type: String, desc: 'Token to authenticate against Kubernetes' 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 :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 :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 end
use :create_params_ee use :create_params_ee
end end
...@@ -96,7 +96,7 @@ module API ...@@ -96,7 +96,7 @@ module API
put ':id/clusters/:cluster_id' do put ':id/clusters/:cluster_id' do
authorize! :update_cluster, cluster 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) if update_service.execute(cluster)
present cluster, with: Entities::ClusterGroup present cluster, with: Entities::ClusterGroup
......
...@@ -62,7 +62,7 @@ module API ...@@ -62,7 +62,7 @@ module API
requires :token, type: String, desc: 'Token to authenticate against Kubernetes' 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 :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 :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 end
use :create_params_ee use :create_params_ee
end end
...@@ -100,7 +100,7 @@ module API ...@@ -100,7 +100,7 @@ module API
put ':id/clusters/:cluster_id' do put ':id/clusters/:cluster_id' do
authorize! :update_cluster, cluster 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) if update_service.execute(cluster)
present cluster, with: Entities::ClusterProject present cluster, with: Entities::ClusterProject
......
...@@ -5068,6 +5068,9 @@ msgstr "" ...@@ -5068,6 +5068,9 @@ msgstr ""
msgid "Container Scanning" msgid "Container Scanning"
msgstr "" msgstr ""
msgid "Container does not exist"
msgstr ""
msgid "Container registry images" msgid "Container registry images"
msgstr "" msgstr ""
...@@ -12659,6 +12662,9 @@ msgstr "" ...@@ -12659,6 +12662,9 @@ msgstr ""
msgid "No connection could be made to a Gitaly Server, please check your logs!" msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr "" msgstr ""
msgid "No containers available"
msgstr ""
msgid "No contributions" msgid "No contributions"
msgstr "" msgstr ""
...@@ -13999,6 +14005,9 @@ msgstr "" ...@@ -13999,6 +14005,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will." msgid "Please wait while we import the repository for you. Refresh at will."
msgstr "" msgstr ""
msgid "Pod does not exist"
msgstr ""
msgid "Pod logs" msgid "Pod logs"
msgstr "" msgstr ""
...@@ -20386,6 +20395,9 @@ msgstr "" ...@@ -20386,6 +20395,9 @@ msgstr ""
msgid "Unable to collect memory info" msgid "Unable to collect memory info"
msgstr "" msgstr ""
msgid "Unable to connect to Elasticsearch"
msgstr ""
msgid "Unable to connect to Prometheus server" msgid "Unable to connect to Prometheus server"
msgstr "" msgstr ""
...@@ -20455,6 +20467,9 @@ msgstr "" ...@@ -20455,6 +20467,9 @@ msgstr ""
msgid "Unknown Error" msgid "Unknown Error"
msgstr "" msgstr ""
msgid "Unknown cache key"
msgstr ""
msgid "Unknown encryption strategy: %{encrypted_strategy}!" msgid "Unknown encryption strategy: %{encrypted_strategy}!"
msgstr "" msgstr ""
......
...@@ -101,7 +101,7 @@ module KubernetesHelpers ...@@ -101,7 +101,7 @@ module KubernetesHelpers
end end
logs_url = service.api_url + "/api/v1/namespaces/#{namespace}/pods/#{pod_name}" \ 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 if status
response = { status: 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