diff options
Diffstat (limited to 'spec/services/pod_logs')
-rw-r--r-- | spec/services/pod_logs/base_service_spec.rb | 229 | ||||
-rw-r--r-- | spec/services/pod_logs/elasticsearch_service_spec.rb | 174 | ||||
-rw-r--r-- | spec/services/pod_logs/kubernetes_service_spec.rb | 166 |
3 files changed, 569 insertions, 0 deletions
diff --git a/spec/services/pod_logs/base_service_spec.rb b/spec/services/pod_logs/base_service_spec.rb new file mode 100644 index 00000000000..a18fda544df --- /dev/null +++ b/spec/services/pod_logs/base_service_spec.rb @@ -0,0 +1,229 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ::PodLogs::BaseService do + include KubernetesHelpers + + let_it_be(:cluster) { create(:cluster, :provided_by_gcp, environment_scope: '*') } + let(:namespace) { 'autodevops-deploy-9-production' } + + 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(cluster, namespace, params: params) } + + describe '#initialize' do + let(:params) do + { + 'container_name' => container_name, + 'another_param' => 'foo' + } + end + + it 'filters the parameters' do + expect(subject.cluster).to eq(cluster) + expect(subject.namespace).to eq(namespace) + expect(subject.params).to eq({ + 'container_name' => container_name + }) + expect(subject.params.equal?(params)).to be(false) + end + end + + describe '#check_arguments' do + context 'when cluster and namespace are provided' do + it 'returns success' do + result = subject.send(:check_arguments, {}) + + expect(result[:status]).to eq(:success) + end + end + + context 'when cluster is nil' do + let(:cluster) { nil } + + it 'returns an error' do + result = subject.send(:check_arguments, {}) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq('Cluster does not exist') + end + end + + context 'when namespace is nil' do + let(:namespace) { nil } + + it 'returns an error' do + result = subject.send(:check_arguments, {}) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq('Namespace is empty') + end + end + + context 'when namespace is empty' do + let(:namespace) { '' } + + it 'returns an error' do + result = subject.send(:check_arguments, {}) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq('Namespace is empty') + end + 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 '#get_raw_pods' do + let(:service) { create(:cluster_platform_kubernetes, :configured) } + + it 'returns success with passthrough k8s response' do + stub_kubeclient_pods(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 +end diff --git a/spec/services/pod_logs/elasticsearch_service_spec.rb b/spec/services/pod_logs/elasticsearch_service_spec.rb new file mode 100644 index 00000000000..0f0c36da56a --- /dev/null +++ b/spec/services/pod_logs/elasticsearch_service_spec.rb @@ -0,0 +1,174 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ::PodLogs::ElasticsearchService do + let_it_be(:cluster) { create(:cluster, :provided_by_gcp, environment_scope: '*') } + let(:namespace) { 'autodevops-deploy-9-production' } + + 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(cluster, namespace, 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(:result_arg) do + { + pod_name: pod_name, + container_name: container_name, + search: search, + start: start_time, + end: end_time + } + end + + before do + 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(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 + + it 'handles server errors from elasticsearch' 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) + .and_raise(Elasticsearch::Transport::Transport::Errors::ServiceUnavailable.new) + + result = subject.send(:pod_logs, result_arg) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq('Elasticsearch returned status code: ServiceUnavailable') + end + end +end diff --git a/spec/services/pod_logs/kubernetes_service_spec.rb b/spec/services/pod_logs/kubernetes_service_spec.rb new file mode 100644 index 00000000000..9fab88a14f6 --- /dev/null +++ b/spec/services/pod_logs/kubernetes_service_spec.rb @@ -0,0 +1,166 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ::PodLogs::KubernetesService do + include KubernetesHelpers + + let_it_be(:cluster) { create(:cluster, :provided_by_gcp, environment_scope: '*') } + let(:namespace) { 'autodevops-deploy-9-production' } + + let(:pod_name) { 'pod-1' } + let(:container_name) { 'container-1' } + let(:params) { {} } + + let(:raw_logs) do + "2019-12-13T14:04:22.123456Z Log 1\n2019-12-13T14:04:23.123456Z Log 2\n" \ + "2019-12-13T14:04:24.123456Z Log 3" + end + + subject { described_class.new(cluster, namespace, params: params) } + + describe '#pod_logs' do + let(:result_arg) do + { + pod_name: pod_name, + container_name: container_name + } + end + + let(:expected_logs) { raw_logs } + let(:service) { create(:cluster_platform_kubernetes, :configured) } + + it 'returns the logs' do + stub_kubeclient_logs(pod_name, 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 + + describe '#encode_logs_to_utf8', :aggregate_failures do + let(:service) { create(:cluster_platform_kubernetes, :configured) } + let(:expected_logs) { '2019-12-13T14:04:22.123456Z ✔ Started logging errors to Sentry' } + let(:raw_logs) { expected_logs.dup.force_encoding(Encoding::ASCII_8BIT) } + let(:result) { subject.send(:encode_logs_to_utf8, result_arg) } + + let(:result_arg) do + { + pod_name: pod_name, + container_name: container_name, + logs: raw_logs + } + end + + it 'converts logs to utf-8' do + expect(result[:status]).to eq(:success) + expect(result[:logs]).to eq(expected_logs) + end + + it 'returns error if output of encoding helper is blank' do + allow(Gitlab::EncodingHelper).to receive(:encode_utf8).and_return('') + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq('Unable to convert Kubernetes logs encoding to UTF-8') + end + + it 'returns error if output of encoding helper is nil' do + allow(Gitlab::EncodingHelper).to receive(:encode_utf8).and_return(nil) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq('Unable to convert Kubernetes logs encoding to UTF-8') + end + + it 'returns error if output of encoding helper is not UTF-8' do + allow(Gitlab::EncodingHelper).to receive(:encode_utf8) + .and_return(expected_logs.encode(Encoding::UTF_16BE)) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq('Unable to convert Kubernetes logs encoding to UTF-8') + end + + context 'when logs are nil' do + let(:raw_logs) { nil } + let(:expected_logs) { nil } + + it 'returns nil' do + expect(result[:status]).to eq(:success) + expect(result[:logs]).to eq(expected_logs) + end + end + + context 'when logs are blank' do + let(:raw_logs) { (+'').force_encoding(Encoding::ASCII_8BIT) } + let(:expected_logs) { '' } + + it 'returns blank string' do + expect(result[:status]).to eq(:success) + expect(result[:logs]).to eq(expected_logs) + end + end + + context 'when logs are already in utf-8' do + let(:raw_logs) { expected_logs } + + it 'does not fail' do + expect(result[:status]).to eq(:success) + expect(result[:logs]).to eq(expected_logs) + end + end + end + + describe '#split_logs' do + let(:service) { create(:cluster_platform_kubernetes, :configured) } + + 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 + + let(:result_arg) do + { + pod_name: pod_name, + container_name: container_name, + logs: raw_logs + } + end + + it 'returns the logs' do + result = subject.send(:split_logs, result_arg) + + aggregate_failures do + expect(result[:status]).to eq(:success) + expect(result[:logs]).to eq(expected_logs) + end + end + end +end |