Commit 5c7a4307 authored by Reuben Pereira's avatar Reuben Pereira Committed by Thong Kuah

Add ETag caching for the pod logs json API

- ETag caching will help reduce the load from polling the API.
- Query parameters are not supported with ETag caching since the
middleware only looks at the request path.

Modify the frontend to work with path params

We've changed the pod logs API to use path params instead of
query params in order to support ETag caching.

Simpify api replacing patterns

- Use a single object as a parameter
- Remove some code
- Update specs

Separate out html and json logs endpoints

- Create separate APIs for the logs HTML page and the JSON endpoint
that returns k8s logs.
- Also correct the route for the JSON endpoint by adding the resource
names to the route.
parent 6e0ff297
...@@ -433,6 +433,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -433,6 +433,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
Gitlab.ee do Gitlab.ee do
get :logs get :logs
get '/pods/(:pod_name)/containers/(:container_name)/logs', to: 'environments#k8s_pod_logs', as: :k8s_pod_logs
end end
end end
......
...@@ -10,6 +10,11 @@ export default { ...@@ -10,6 +10,11 @@ export default {
groupEpicsPath: groupEpicsPath:
'/api/:version/groups/:id/epics?include_ancestor_groups=:includeAncestorGroups&include_descendant_groups=:includeDescendantGroups', '/api/:version/groups/:id/epics?include_ancestor_groups=:includeAncestorGroups&include_descendant_groups=:includeDescendantGroups',
epicIssuePath: '/api/:version/groups/:id/epics/:epic_iid/issues/:issue_id', epicIssuePath: '/api/:version/groups/:id/epics/:epic_iid/issues/:issue_id',
podLogsPath: '/:project_full_path/environments/:environment_id/pods/containers/logs.json',
podLogsPathWithPod:
'/:project_full_path/environments/:environment_id/pods/:pod_name/containers/logs.json',
podLogsPathWithPodContainer:
'/:project_full_path/environments/:environment_id/pods/:pod_name/containers/:container_name/logs.json',
userSubscription(namespaceId) { userSubscription(namespaceId) {
const url = Api.buildUrl(this.subscriptionPath).replace(':id', encodeURIComponent(namespaceId)); const url = Api.buildUrl(this.subscriptionPath).replace(':id', encodeURIComponent(namespaceId));
...@@ -70,4 +75,25 @@ export default { ...@@ -70,4 +75,25 @@ export default {
return axios.delete(url); return axios.delete(url);
}, },
getPodLogs({ projectFullPath, environmentId, podName, containerName }) {
let logPath = this.podLogsPath;
if (podName && containerName) {
logPath = this.podLogsPathWithPodContainer;
} else if (podName) {
logPath = this.podLogsPathWithPod;
}
let url = this.buildUrl(logPath)
.replace(':project_full_path', projectFullPath)
.replace(':environment_id', environmentId);
if (podName) {
url = url.replace(':pod_name', podName);
}
if (containerName) {
url = url.replace(':container_name', containerName);
}
return axios.get(url);
},
}; };
...@@ -8,13 +8,13 @@ import flash from '~/flash'; ...@@ -8,13 +8,13 @@ import flash from '~/flash';
import { __, s__, sprintf } from '~/locale'; import { __, s__, sprintf } from '~/locale';
import _ from 'underscore'; import _ from 'underscore';
import { backOff } from '~/lib/utils/common_utils'; import { backOff } from '~/lib/utils/common_utils';
import Api from 'ee/api';
const requestWithBackoff = (url, params) => const TWO_MINUTES = 120000;
const requestWithBackoff = (projectFullPath, environmentId, podName, containerName) =>
backOff((next, stop) => { backOff((next, stop) => {
axios Api.getPodLogs({ projectFullPath, environmentId, podName, containerName })
.get(url, {
params,
})
.then(res => { .then(res => {
if (!res.data) { if (!res.data) {
next(); next();
...@@ -25,17 +25,23 @@ const requestWithBackoff = (url, params) => ...@@ -25,17 +25,23 @@ const requestWithBackoff = (url, params) =>
.catch(err => { .catch(err => {
stop(err); stop(err);
}); });
}); }, TWO_MINUTES);
export default class KubernetesPodLogs extends LogOutputBehaviours { export default class KubernetesPodLogs extends LogOutputBehaviours {
constructor(container) { constructor(container) {
super(); super();
this.options = $(container).data(); this.options = $(container).data();
const { currentEnvironmentName, environmentsPath, logsEndpoint } = this.options; const {
currentEnvironmentName,
environmentsPath,
projectFullPath,
environmentId,
} = this.options;
this.environmentName = currentEnvironmentName; this.environmentName = currentEnvironmentName;
this.environmentsPath = environmentsPath; this.environmentsPath = environmentsPath;
this.logsEndpoint = logsEndpoint; this.projectFullPath = projectFullPath;
this.environmentId = environmentId;
[this.podName] = getParameterValues('pod_name'); [this.podName] = getParameterValues('pod_name');
if (this.podName) { if (this.podName) {
...@@ -94,7 +100,7 @@ export default class KubernetesPodLogs extends LogOutputBehaviours { ...@@ -94,7 +100,7 @@ export default class KubernetesPodLogs extends LogOutputBehaviours {
} }
getLogs() { getLogs() {
return requestWithBackoff(this.logsEndpoint, { pod_name: this.podName }) return requestWithBackoff(this.projectFullPath, this.environmentId, this.podName)
.then(res => { .then(res => {
const { logs, pods } = res.data; const { logs, pods } = res.data;
this.setupPodsDropdown(pods); this.setupPodsDropdown(pods);
......
...@@ -6,17 +6,16 @@ module EE ...@@ -6,17 +6,16 @@ module EE
extend ActiveSupport::Concern extend ActiveSupport::Concern
prepended do prepended do
before_action :authorize_read_pod_logs!, only: [:logs] before_action :authorize_read_pod_logs!, only: [:k8s_pod_logs, :logs]
before_action :environment_ee, only: [:logs] before_action :environment_ee, only: [:k8s_pod_logs, :logs]
before_action :authorize_create_environment_terminal!, only: [:terminal] before_action :authorize_create_environment_terminal!, only: [:terminal]
before_action do before_action do
push_frontend_feature_flag(:environment_logs_use_vue_ui) push_frontend_feature_flag(:environment_logs_use_vue_ui)
end end
end end
def logs def k8s_pod_logs
respond_to do |format| respond_to do |format|
format.html
format.json do format.json do
::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)
...@@ -34,6 +33,9 @@ module EE ...@@ -34,6 +33,9 @@ module EE
end end
end end
def logs
end
private private
def environment_ee def environment_ee
......
...@@ -34,7 +34,8 @@ module EE ...@@ -34,7 +34,8 @@ module EE
{ {
"current-environment-name": environment.name, "current-environment-name": environment.name,
"environments-path": project_environments_path(project, format: :json), "environments-path": project_environments_path(project, format: :json),
"logs-endpoint": logs_project_environment_path(project, environment, format: :json) "project-full-path": project.full_path,
"environment-id": environment.id
} }
end end
......
...@@ -6,6 +6,8 @@ module EE ...@@ -6,6 +6,8 @@ module EE
module Kubernetes module Kubernetes
extend ActiveSupport::Concern extend ActiveSupport::Concern
CACHE_KEY_GET_POD_LOG = 'get_pod_log'
LOGS_LIMIT = 500.freeze LOGS_LIMIT = 500.freeze
def calculate_reactive_cache_for(environment) def calculate_reactive_cache_for(environment)
...@@ -26,9 +28,12 @@ module EE ...@@ -26,9 +28,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(pod_name, namespace, container: nil) def read_pod_logs(environment_id, pod_name, namespace, container: nil)
# environment_id is required for use in reactive_cache_updated(),
# to invalidate the ETag cache.
with_reactive_cache( with_reactive_cache(
'get_pod_log', CACHE_KEY_GET_POD_LOG,
'environment_id' => environment_id,
'pod_name' => pod_name, 'pod_name' => pod_name,
'namespace' => namespace, 'namespace' => namespace,
'container' => container 'container' => container
...@@ -39,7 +44,7 @@ module EE ...@@ -39,7 +44,7 @@ module EE
def calculate_reactive_cache(request, opts) def calculate_reactive_cache(request, opts)
case request case request
when 'get_pod_log' when CACHE_KEY_GET_POD_LOG
container = opts['container'] container = opts['container']
pod_name = opts['pod_name'] pod_name = opts['pod_name']
namespace = opts['namespace'] namespace = opts['namespace']
...@@ -52,6 +57,28 @@ module EE ...@@ -52,6 +57,28 @@ module EE
end 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
::Gitlab::EtagCaching::Store.new.tap do |store|
store.touch(
::Gitlab::Routing.url_helpers.k8s_pod_logs_project_environment_path(
environment.project,
environment,
opts['pod_name'],
opts['container_name'],
format: :json
)
)
end
end
end
private private
def pod_logs(pod_name, namespace, container: nil) def pod_logs(pod_name, namespace, container: nil)
......
...@@ -75,6 +75,7 @@ class PodLogsService < ::BaseService ...@@ -75,6 +75,7 @@ class PodLogsService < ::BaseService
def pod_logs(result) def pod_logs(result)
response = environment.deployment_platform.read_pod_logs( response = environment.deployment_platform.read_pod_logs(
environment.id,
result[:pod_name], result[:pod_name],
namespace, namespace,
container: result[:container_name] container: result[:container_name]
......
...@@ -4,16 +4,20 @@ module EE ...@@ -4,16 +4,20 @@ module EE
module Gitlab module Gitlab
module EtagCaching module EtagCaching
module Router module Router
EE_ROUTES = [
::Gitlab::EtagCaching::Router::Route.new(
%r(^/groups/#{::Gitlab::PathRegex.full_namespace_route_regex}/-/epics/\d+/notes\z),
'epic_notes'
),
::Gitlab::EtagCaching::Router::Route.new(
%r(#{::Gitlab::EtagCaching::Router::RESERVED_WORDS_PREFIX}/environments/\d+/pods/(\S+/)?containers/(\S+/)?logs\.json\z),
'k8s_pod_logs'
)
].freeze
module ClassMethods module ClassMethods
def match(path) def match(path)
epic_route = ::Gitlab::EtagCaching::Router::Route.new( EE_ROUTES.find { |route| route.regexp.match(path) } || super
%r(^/groups/#{::Gitlab::PathRegex.full_namespace_route_regex}/-/epics/\d+/notes\z),
'epic_notes'
)
return epic_route if epic_route.regexp.match(path)
super
end end
end end
......
...@@ -92,7 +92,7 @@ describe Projects::EnvironmentsController do ...@@ -92,7 +92,7 @@ describe Projects::EnvironmentsController do
shared_examples 'resource not found' do |message| shared_examples 'resource not found' do |message|
it 'returns 400' do it 'returns 400' do
get :logs, params: environment_params(pod_name: pod_name, format: :json) get :k8s_pod_logs, params: environment_params(pod_name: pod_name, format: :json)
expect(response).to have_gitlab_http_status(:bad_request) expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to eq(message) expect(json_response['message']).to eq(message)
...@@ -114,7 +114,7 @@ describe Projects::EnvironmentsController do ...@@ -114,7 +114,7 @@ describe Projects::EnvironmentsController do
end end
end end
context 'when using HTML format' do context 'when licensed' do
it 'renders logs template' do it 'renders logs template' do
get :logs, params: environment_params(pod_name: pod_name) get :logs, params: environment_params(pod_name: pod_name)
...@@ -142,7 +142,7 @@ describe Projects::EnvironmentsController do ...@@ -142,7 +142,7 @@ describe Projects::EnvironmentsController do
end end
it 'returns the logs for a specific pod' do it 'returns the logs for a specific pod' do
get :logs, params: environment_params(pod_name: pod_name, format: :json) get :k8s_pod_logs, params: environment_params(pod_name: pod_name, format: :json)
expect(response).to have_gitlab_http_status(:success) expect(response).to have_gitlab_http_status(:success)
expect(json_response["logs"]).to match_array(["Log 1", "Log 2", "Log 3"]) expect(json_response["logs"]).to match_array(["Log 1", "Log 2", "Log 3"])
...@@ -155,7 +155,7 @@ describe Projects::EnvironmentsController do ...@@ -155,7 +155,7 @@ describe Projects::EnvironmentsController do
it 'registers a usage of the endpoint' do it 'registers a usage of the endpoint' do
expect(::Gitlab::UsageCounters::PodLogs).to receive(:increment).with(project.id) expect(::Gitlab::UsageCounters::PodLogs).to receive(:increment).with(project.id)
get :logs, params: environment_params(pod_name: pod_name, format: :json) get :k8s_pod_logs, params: environment_params(pod_name: pod_name, format: :json)
end end
context 'when kubernetes API returns error' do context 'when kubernetes API returns error' do
...@@ -170,7 +170,7 @@ describe Projects::EnvironmentsController do ...@@ -170,7 +170,7 @@ describe Projects::EnvironmentsController do
end end
it 'returns bad request' do it 'returns bad request' do
get :logs, params: environment_params(pod_name: pod_name, format: :json) get :k8s_pod_logs, params: environment_params(pod_name: pod_name, format: :json)
expect(response).to have_gitlab_http_status(:bad_request) expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response["logs"]).to eq(nil) expect(json_response["logs"]).to eq(nil)
...@@ -204,7 +204,7 @@ describe Projects::EnvironmentsController do ...@@ -204,7 +204,7 @@ describe Projects::EnvironmentsController do
end end
it 'returns the error without pods, pod_name and container_name' do it 'returns the error without pods, pod_name and container_name' do
get :logs, params: environment_params(pod_name: pod_name, format: :json) get :k8s_pod_logs, params: environment_params(pod_name: pod_name, format: :json)
expect(response).to have_gitlab_http_status(:bad_request) expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to eq('No deployment platform') expect(json_response['message']).to eq('No deployment platform')
...@@ -216,7 +216,7 @@ describe Projects::EnvironmentsController do ...@@ -216,7 +216,7 @@ describe Projects::EnvironmentsController do
let(:service_result) { { status: :processing } } let(:service_result) { { status: :processing } }
it 'renders accepted' do it 'renders accepted' do
get :logs, params: environment_params(pod_name: pod_name, format: :json) get :k8s_pod_logs, params: environment_params(pod_name: pod_name, format: :json)
expect(response).to have_gitlab_http_status(:accepted) expect(response).to have_gitlab_http_status(:accepted)
end end
......
...@@ -162,4 +162,54 @@ describe('Api', () => { ...@@ -162,4 +162,54 @@ describe('Api', () => {
.catch(done.fail); .catch(done.fail);
}); });
}); });
describe('getPodLogs', () => {
const projectFullPath = 'root/test-project';
const environmentId = 2;
const podName = 'pod';
const containerName = 'container';
const lastUrl = () => mock.history.get[0].url;
beforeEach(() => {
mock.onAny().reply(200);
});
afterEach(() => {
mock.reset();
});
it('calls `axios.get` with pod_name and container_name', done => {
const expectedUrl = `${dummyUrlRoot}/${projectFullPath}/environments/${environmentId}/pods/${podName}/containers/${containerName}/logs.json`;
Api.getPodLogs({ projectFullPath, environmentId, podName, containerName })
.then(() => {
expect(expectedUrl).toBe(lastUrl());
})
.then(done)
.catch(done.fail);
});
it('calls `axios.get` without pod_name and container_name', done => {
const expectedUrl = `${dummyUrlRoot}/${projectFullPath}/environments/${environmentId}/pods/containers/logs.json`;
Api.getPodLogs({ projectFullPath, environmentId })
.then(() => {
expect(expectedUrl).toBe(lastUrl());
})
.then(done)
.catch(done.fail);
});
it('calls `axios.get` with pod_name', done => {
const expectedUrl = `${dummyUrlRoot}/${projectFullPath}/environments/${environmentId}/pods/${podName}/containers/logs.json`;
Api.getPodLogs({ projectFullPath, environmentId, podName })
.then(() => {
expect(expectedUrl).toBe(lastUrl());
})
.then(done)
.catch(done.fail);
});
});
}); });
...@@ -41,9 +41,10 @@ describe EnvironmentsHelper do ...@@ -41,9 +41,10 @@ describe EnvironmentsHelper do
) )
end end
it 'returns logs parameters data' do it 'returns parameters for forming the pod logs API URL' do
expect(subject).to include( expect(subject).to include(
"logs-endpoint": logs_project_environment_path(project, environment, format: :json) "project-full-path": project.full_path,
"environment-id": environment.id
) )
end end
end end
......
...@@ -11,6 +11,7 @@ describe('Kubernetes Logs', () => { ...@@ -11,6 +11,7 @@ describe('Kubernetes Logs', () => {
let kubernetesLog; let kubernetesLog;
let mock; let mock;
let mockFlash; let mockFlash;
let podLogsAPIPath;
preloadFixtures(fixtureTemplate); preloadFixtures(fixtureTemplate);
...@@ -24,9 +25,11 @@ describe('Kubernetes Logs', () => { ...@@ -24,9 +25,11 @@ describe('Kubernetes Logs', () => {
mockDataset = kubernetesLogContainer.dataset; mockDataset = kubernetesLogContainer.dataset;
podLogsAPIPath = `/${mockDataset.projectFullPath}/environments/${mockDataset.environmentId}/pods/containers/logs.json`;
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mock.onGet(mockDataset.environmentsPath).reply(200, { environments: mockEnvironmentData }); mock.onGet(mockDataset.environmentsPath).reply(200, { environments: mockEnvironmentData });
mock.onGet(mockDataset.logsEndpoint).reply(200, { logs: logMockData, pods: podMockData }); mock.onGet(podLogsAPIPath).reply(200, { logs: logMockData, pods: podMockData });
}); });
afterEach(() => { afterEach(() => {
...@@ -158,7 +161,7 @@ describe('Kubernetes Logs', () => { ...@@ -158,7 +161,7 @@ describe('Kubernetes Logs', () => {
describe('shows an alert', () => { describe('shows an alert', () => {
it('with an error', done => { it('with an error', done => {
mock.onGet(mockDataset.logsEndpoint).reply(400); mock.onGet(podLogsAPIPath).reply(400);
kubernetesLog = new KubernetesLogs(kubernetesLogContainer); kubernetesLog = new KubernetesLogs(kubernetesLogContainer);
kubernetesLog kubernetesLog
...@@ -173,7 +176,7 @@ describe('Kubernetes Logs', () => { ...@@ -173,7 +176,7 @@ describe('Kubernetes Logs', () => {
it('with some explicit error', done => { it('with some explicit error', done => {
const errorMsg = 'Some k8s error'; const errorMsg = 'Some k8s error';
mock.onGet(mockDataset.logsEndpoint).reply(400, { mock.onGet(podLogsAPIPath).reply(400, {
message: errorMsg, message: errorMsg,
}); });
...@@ -198,7 +201,7 @@ describe('Kubernetes Logs', () => { ...@@ -198,7 +201,7 @@ describe('Kubernetes Logs', () => {
kubernetesLogContainer = document.querySelector('.js-kubernetes-logs'); kubernetesLogContainer = document.querySelector('.js-kubernetes-logs');
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mock.onGet(mockDataset.logsEndpoint).reply(200, { logs: logMockData, pods: [hackyPodName] }); mock.onGet(podLogsAPIPath).reply(200, { logs: logMockData, pods: [hackyPodName] });
}); });
afterEach(() => { afterEach(() => {
...@@ -226,13 +229,15 @@ describe('Kubernetes Logs', () => { ...@@ -226,13 +229,15 @@ describe('Kubernetes Logs', () => {
mockDataset = kubernetesLogContainer.dataset; mockDataset = kubernetesLogContainer.dataset;
podLogsAPIPath = `/${mockDataset.projectFullPath}/environments/${
mockDataset.environmentId
}/pods/${podMockData[1]}/containers/logs.json`;
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mock.onGet(mockDataset.environmentsPath).reply(200, { environments: mockEnvironmentData }); mock.onGet(mockDataset.environmentsPath).reply(200, { environments: mockEnvironmentData });
// Simulate reactive cache, 2 tries needed // Simulate reactive cache, 2 tries needed
mock.onGet(mockDataset.logsEndpoint, { pod_name: podMockData[1] }).replyOnce(202); mock.onGet(podLogsAPIPath).replyOnce(202);
mock mock.onGet(podLogsAPIPath).reply(200, { logs: logMockData, pods: podMockData });
.onGet(mockDataset.logsEndpoint, { pod_name: podMockData[1] })
.reply(200, { logs: logMockData, pods: podMockData });
}); });
it('queries the pod log data polling for reactive cache', done => { it('queries the pod log data polling for reactive cache', done => {
...@@ -243,12 +248,10 @@ describe('Kubernetes Logs', () => { ...@@ -243,12 +248,10 @@ describe('Kubernetes Logs', () => {
kubernetesLog kubernetesLog
.getData() .getData()
.then(() => { .then(() => {
const calls = mock.history.get.filter(r => r.url === mockDataset.logsEndpoint); const calls = mock.history.get.filter(r => r.url === podLogsAPIPath);
// expect 2 tries // expect 2 tries
expect(calls.length).toEqual(2); expect(calls.length).toEqual(2);
expect(calls[0].params).toEqual({ pod_name: podMockData[1] });
expect(calls[1].params).toEqual({ pod_name: podMockData[1] });
expect(document.querySelector('.js-build-output').textContent).toContain( expect(document.querySelector('.js-build-output').textContent).toContain(
logMockData[0].trim(), logMockData[0].trim(),
...@@ -270,6 +273,10 @@ describe('Kubernetes Logs', () => { ...@@ -270,6 +273,10 @@ describe('Kubernetes Logs', () => {
spyOnDependency(KubernetesLogs, 'getParameterValues').and.callFake(() => [podMockData[2]]); spyOnDependency(KubernetesLogs, 'getParameterValues').and.callFake(() => [podMockData[2]]);
kubernetesLogContainer = document.querySelector('.js-kubernetes-logs'); kubernetesLogContainer = document.querySelector('.js-kubernetes-logs');
podLogsAPIPath = `/${mockDataset.projectFullPath}/environments/${
mockDataset.environmentId
}/pods/${podMockData[2]}/containers/logs.json`;
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
}); });
...@@ -278,10 +285,9 @@ describe('Kubernetes Logs', () => { ...@@ -278,10 +285,9 @@ describe('Kubernetes Logs', () => {
kubernetesLog kubernetesLog
.getData() .getData()
.then(() => { .then(() => {
const logsCall = mock.history.get.filter(call => call.url === mockDataset.logsEndpoint); const logsCall = mock.history.get.filter(call => call.url === podLogsAPIPath);
expect(logsCall.length).toBe(1); expect(logsCall.length).toBe(1);
expect(logsCall[0].params.pod_name).toEqual(podMockData[2]);
done(); done();
}) })
.catch(done.fail); .catch(done.fail);
......
...@@ -19,4 +19,41 @@ describe Gitlab::EtagCaching::Router do ...@@ -19,4 +19,41 @@ describe Gitlab::EtagCaching::Router do
expect(result).to be_blank expect(result).to be_blank
end end
context 'k8s pod logs' do
it 'matches with pod_name and container_name' do
result = described_class.match(
'/environments/7/pods/pod_name/containers/container_name/logs.json'
)
expect(result).to be_present
expect(result.name).to eq 'k8s_pod_logs'
end
it 'matches with pod_name' do
result = described_class.match(
'/environments/7/pods/pod_name/containers/logs.json'
)
expect(result).to be_present
expect(result.name).to eq 'k8s_pod_logs'
end
it 'matches without pod_name and container_name' do
result = described_class.match(
'/environments/7/pods/containers/logs.json'
)
expect(result).to be_present
expect(result.name).to eq 'k8s_pod_logs'
end
it 'does not match non json format' do
result = described_class.match(
'/environments/7/logs'
)
expect(result).not_to be_present
end
end
end end
...@@ -135,13 +135,14 @@ describe Clusters::Platforms::Kubernetes do ...@@ -135,13 +135,14 @@ describe Clusters::Platforms::Kubernetes do
end end
describe '#read_pod_logs' do describe '#read_pod_logs' do
let(:environment) { create(:environment) }
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) }
let(:pod_name) { 'pod-1' } let(:pod_name) { 'pod-1' }
let(:namespace) { 'app' } let(:namespace) { 'app' }
let(:container) { 'some-container' } let(:container) { 'some-container' }
subject { service.read_pod_logs(pod_name, namespace, container: container) } subject { service.read_pod_logs(environment.id, pod_name, namespace, container: container) }
shared_examples 'successful log request' do shared_examples 'successful log request' do
it do it do
...@@ -224,7 +225,7 @@ describe Clusters::Platforms::Kubernetes do ...@@ -224,7 +225,7 @@ describe Clusters::Platforms::Kubernetes do
context 'when container name is not specified' do context 'when container name is not specified' do
let(:container) { 'container-0' } let(:container) { 'container-0' }
subject { service.read_pod_logs(pod_name, namespace) } subject { service.read_pod_logs(environment.id, pod_name, namespace) }
before do before do
stub_kubeclient_pod_details(pod_name, namespace) stub_kubeclient_pod_details(pod_name, namespace)
...@@ -237,7 +238,15 @@ describe Clusters::Platforms::Kubernetes do ...@@ -237,7 +238,15 @@ describe Clusters::Platforms::Kubernetes do
context 'with caching', :use_clean_rails_memory_store_caching do context 'with caching', :use_clean_rails_memory_store_caching do
let(:opts) do let(:opts) do
['get_pod_log', { 'pod_name' => pod_name, 'namespace' => namespace, 'container' => container }] [
'get_pod_log',
{
'environment_id' => environment.id,
'pod_name' => pod_name,
'namespace' => namespace,
'container' => container
}
]
end end
context 'result is cacheable' do context 'result is cacheable' do
...@@ -278,6 +287,37 @@ describe Clusters::Platforms::Kubernetes do ...@@ -278,6 +287,37 @@ describe Clusters::Platforms::Kubernetes do
end end
end end
end end
context '#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_pod_logs_project_environment_path(
environment.project,
environment,
opts['pod_name'],
opts['container_name'],
format: :json
)
)
.and_call_original
end
subject
end
end
end end
describe '#calculate_reactive_cache_for' do describe '#calculate_reactive_cache_for' do
......
...@@ -54,7 +54,7 @@ describe PodLogsService do ...@@ -54,7 +54,7 @@ describe PodLogsService do
shared_context 'return error' do |message| shared_context 'return error' do |message|
before do before do
allow_any_instance_of(EE::Clusters::Platforms::Kubernetes).to receive(:read_pod_logs) allow_any_instance_of(EE::Clusters::Platforms::Kubernetes).to receive(:read_pod_logs)
.with(pod_name, environment.deployment_namespace, container: container_name) .with(environment.id, pod_name, environment.deployment_namespace, container: container_name)
.and_return({ .and_return({
status: :error, status: :error,
error: message, error: message,
...@@ -67,7 +67,7 @@ describe PodLogsService do ...@@ -67,7 +67,7 @@ describe PodLogsService do
shared_context 'return success' do shared_context 'return success' do
before do before do
allow_any_instance_of(EE::Clusters::Platforms::Kubernetes).to receive(:read_pod_logs) allow_any_instance_of(EE::Clusters::Platforms::Kubernetes).to receive(:read_pod_logs)
.with(response_pod_name, environment.deployment_namespace, container: container_name) .with(environment.id, response_pod_name, environment.deployment_namespace, container: container_name)
.and_return({ .and_return({
status: :success, status: :success,
logs: "Log 1\nLog 2\nLog 3", logs: "Log 1\nLog 2\nLog 3",
...@@ -151,7 +151,7 @@ describe PodLogsService do ...@@ -151,7 +151,7 @@ describe PodLogsService do
it 'returns logs of first pod' do it 'returns logs of first pod' do
expect_any_instance_of(EE::Clusters::Platforms::Kubernetes).to receive(:read_pod_logs) expect_any_instance_of(EE::Clusters::Platforms::Kubernetes).to receive(:read_pod_logs)
.with(first_pod_name, environment.deployment_namespace, container: nil) .with(environment.id, first_pod_name, environment.deployment_namespace, container: nil)
subject.execute subject.execute
end end
...@@ -188,7 +188,7 @@ describe PodLogsService do ...@@ -188,7 +188,7 @@ describe PodLogsService do
context 'when nil is returned' do context 'when nil is returned' do
before do before do
allow_any_instance_of(EE::Clusters::Platforms::Kubernetes).to receive(:read_pod_logs) allow_any_instance_of(EE::Clusters::Platforms::Kubernetes).to receive(:read_pod_logs)
.with(pod_name, environment.deployment_namespace, container: container_name) .with(environment.id, pod_name, environment.deployment_namespace, container: container_name)
.and_return(nil) .and_return(nil)
end end
......
...@@ -3,8 +3,6 @@ ...@@ -3,8 +3,6 @@
module Gitlab module Gitlab
module EtagCaching module EtagCaching
class Router class Router
prepend_if_ee('EE::Gitlab::EtagCaching::Router') # rubocop: disable Cop/InjectEnterpriseEditionModule
Route = Struct.new(:regexp, :name) Route = Struct.new(:regexp, :name)
# We enable an ETag for every request matching the regex. # We enable an ETag for every request matching the regex.
# To match a regex the path needs to match the following: # To match a regex the path needs to match the following:
...@@ -80,3 +78,5 @@ module Gitlab ...@@ -80,3 +78,5 @@ module Gitlab
end end
end end
end end
Gitlab::EtagCaching::Router.prepend_if_ee('EE::Gitlab::EtagCaching::Router')
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
class="js-kubernetes-logs" class="js-kubernetes-logs"
data-current-environment-name="production" data-current-environment-name="production"
data-environments-path="/root/my-project/environments.json" data-environments-path="/root/my-project/environments.json"
data-logs-endpoint="/root/my-project/environments/1/logs.json" data-project-full-path="root/my-project"
data-environment-id=1
> >
<div class="build-page-pod-logs"> <div class="build-page-pod-logs">
<div class="build-trace-container prepend-top-default"> <div class="build-trace-container prepend-top-default">
......
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