Skip to content
Snippets Groups Projects
Commit 18118042 authored by Anna Vovchenko's avatar Anna Vovchenko :flag_ua:
Browse files

Implement abort pod logs stream

The pod logs stream should be aborted
when the user navigates away from the pod logs page.

Changelog: fixed
parent 1921ddb6
No related branches found
No related tags found
1 merge request!171415Abort watch request for the pods logs when navigating away from the page
Showing
with 206 additions and 17 deletions
......@@ -3,10 +3,12 @@ import { GlLoadingIcon, GlAlert, GlEmptyState, GlSprintf, GlIcon } from '@gitlab
import EmptyStateSvg from '@gitlab/svgs/dist/illustrations/status/status-nothing-md.svg';
import k8sLogsQuery from '~/environments/graphql/queries/k8s_logs.query.graphql';
import environmentClusterAgentQuery from '~/environments/graphql/queries/environment_cluster_agent.query.graphql';
import abortK8sPodLogsStream from '~/environments/graphql/mutations/abort_pod_logs_stream.mutation.graphql';
import { createK8sAccessConfiguration } from '~/environments/helpers/k8s_integration_helper';
import LogsViewer from '~/vue_shared/components/logs_viewer/logs_viewer.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { s__, __ } from '~/locale';
import { fetchPolicies } from '~/lib/graphql';
export default {
components: {
......@@ -45,11 +47,14 @@ export default {
data() {
return {
environmentError: null,
k8sLogs: null,
environment: null,
};
},
apollo: {
// eslint-disable-next-line @gitlab/vue-no-undef-apollo-properties
k8sLogs: {
fetchPolicy: fetchPolicies.NETWORK_ONLY,
nextFetchPolicy: fetchPolicies.CACHE_FIRST,
query: k8sLogsQuery,
variables() {
return {
......@@ -63,7 +68,6 @@ export default {
return Boolean(!this.gitlabAgentId);
},
},
// eslint-disable-next-line @gitlab/vue-no-undef-apollo-properties
environment: {
query: environmentClusterAgentQuery,
variables() {
......@@ -131,6 +135,17 @@ export default {
return data;
},
},
beforeDestroy() {
this.$apollo.mutate({
mutation: abortK8sPodLogsStream,
variables: {
configuration: this.k8sAccessConfiguration,
namespace: this.namespace,
podName: this.podName,
containerName: this.containerName,
},
});
},
i18n: {
emptyStateTitleForPod: s__('KubernetesLogs|No logs available for pod %{podName}'),
emptyStateTitleForContainer: s__(
......
......@@ -14,6 +14,7 @@ import k8sNamespacesQuery from './queries/k8s_namespaces.query.graphql';
import fluxKustomizationQuery from './queries/flux_kustomization.query.graphql';
import fluxHelmReleaseQuery from './queries/flux_helm_release.query.graphql';
import k8sEventsQuery from './queries/k8s_events.query.graphql';
import k8sPodLogsWatcherQuery from './queries/k8s_pod_logs_watcher.query.graphql';
import { resolvers } from './resolvers';
import typeDefs from './typedefs.graphql';
import { connectionStatus } from './resolvers/kubernetes/constants';
......@@ -183,6 +184,15 @@ export const apolloProvider = (endpoint) => {
},
});
cache.writeQuery({
query: k8sPodLogsWatcherQuery,
data: {
k8sPodLogsWatcher: {
watcher: null,
},
},
});
return new VueApollo({
defaultClient,
});
......
mutation abortK8sPodLogsStream(
$configuration: LocalConfiguration
$namespace: String
$podName: String
$containerName: String
) {
abortK8sPodLogsStream(
configuration: $configuration
namespace: $namespace
podName: $podName
containerName: $containerName
) @client {
errors
}
}
query k8sPodLogsWatcherQuery(
$configuration: LocalConfiguration
$namespace: String
$podName: String
$containerName: String
) {
k8sPodLogsWatcher(
configuration: $configuration
namespace: $namespace
podName: $podName
containerName: $containerName
) @client {
watcher
}
}
......@@ -22,7 +22,7 @@ import k8sServicesQuery from '../../queries/k8s_services.query.graphql';
import k8sDeploymentsQuery from '../../queries/k8s_deployments.query.graphql';
import k8sEventsQuery from '../../queries/k8s_events.query.graphql';
import { k8sResourceType } from './constants';
import { k8sLogs } from './k8s_logs';
import { k8sLogs, k8sPodLogsWatcher, abortK8sPodLogsStream } from './k8s_logs';
const watchServices = ({ configuration, namespace, client }) => {
const query = k8sServicesQuery;
......@@ -185,6 +185,7 @@ export const kubernetesMutations = {
return buildKubernetesErrors([error]);
});
},
abortK8sPodLogsStream,
};
export const kubernetesQueries = {
......@@ -284,4 +285,5 @@ export const kubernetesQueries = {
});
},
k8sLogs,
k8sPodLogsWatcher,
};
......@@ -7,6 +7,7 @@ import {
} from '@gitlab/cluster-client';
import { throttle } from 'lodash';
import k8sLogsQuery from '~/environments/graphql/queries/k8s_logs.query.graphql';
import k8sPodLogsWatcherQuery from '~/environments/graphql/queries/k8s_pod_logs_watcher.query.graphql';
export const buildWatchPath = ({ resource, api = 'api/v1', namespace = '' }) => {
return `/${api}/namespaces/${namespace}/pods/${resource}/log`;
......@@ -62,6 +63,12 @@ export const k8sLogs = (_, { configuration, namespace, podName, containerName },
watchApi
.subscribeToStream(watchPath, watchQuery)
.then((watcher) => {
client.writeQuery({
query: k8sPodLogsWatcherQuery,
data: { k8sPodLogsWatcher: { watcher } },
variables,
});
let logsData = [];
const writeLogsThrottled = throttle(() => {
const currentLogsData = cacheWrapper.readLogsData();
......@@ -89,3 +96,18 @@ export const k8sLogs = (_, { configuration, namespace, podName, containerName },
cacheWrapper.writeErrorData(err);
});
};
export const abortK8sPodLogsStream = (
_,
{ configuration, namespace, podName, containerName },
{ client },
) => {
const podLogsWatcher = client.readQuery({
query: k8sPodLogsWatcherQuery,
variables: { configuration, namespace, podName, containerName },
})?.k8sPodLogsWatcher?.watcher;
podLogsWatcher?.abortStream();
};
export const k8sPodLogsWatcher = () => ({ watcher: null });
......@@ -127,6 +127,10 @@ type K8sEvent {
type: String
}
type k8sWatcher {
watcher: JSON
}
extend type Query {
environmentApp(page: Int, scope: String): LocalEnvironmentApp
folder(environment: NestedLocalEnvironmentInput): LocalEnvironmentFolder
......@@ -151,6 +155,12 @@ extend type Query {
namespace: String
involvedObjectName: String
): [K8sEvent]
k8sPodLogsWatcher(
configuration: LocalConfiguration
namespace: String
podName: String
containerName: String
): k8sWatcher
}
input ResourceTypeParam {
......@@ -178,4 +188,10 @@ extend type Mutation {
namespace: String
podName: String
): LocalErrors
abortPodLogsStream(
configuration: LocalConfiguration
namespace: String
podName: String
containerName: String
): LocalErrors
}
......@@ -32,6 +32,7 @@ describe('kubernetes_logs', () => {
gitlabAgentId,
});
let k8sLogsQueryMock;
let abortK8sPodLogsStreamMock;
let environmentDataMock;
const defaultEnvironmentData = {
......@@ -63,6 +64,7 @@ describe('kubernetes_logs', () => {
k8sLogsQueryMock = jest.fn().mockResolvedValue({
logs: logsMockData,
});
abortK8sPodLogsStreamMock = jest.fn().mockResolvedValue({ errors: [] });
environmentDataMock = jest.fn().mockResolvedValue(defaultEnvironmentData);
};
......@@ -71,6 +73,9 @@ describe('kubernetes_logs', () => {
Query: {
k8sLogs: k8sLogsQueryMock,
},
Mutation: {
abortK8sPodLogsStream: abortK8sPodLogsStreamMock,
},
};
return createMockApollo([[environmentClusterAgentQuery, environmentDataMock]], mockResolvers);
......@@ -275,4 +280,26 @@ describe('kubernetes_logs', () => {
});
});
});
describe('beforeDestroy', () => {
beforeEach(async () => {
mountComponent();
await waitForPromises();
wrapper.destroy();
});
it('triggers `abortPodLogsStream` mutation to unsubscribe from the stream', () => {
expect(abortK8sPodLogsStreamMock).toHaveBeenCalledWith(
{},
{
configuration,
namespace: defaultProps.namespace,
podName: defaultProps.podName,
containerName: '',
},
expect.anything(),
expect.anything(),
);
});
});
});
import { EVENT_TIMEOUT, EVENT_PLAIN_TEXT, EVENT_ERROR } from '@gitlab/cluster-client';
import throttle from 'lodash/throttle';
import k8sLogsQuery from '~/environments/graphql/queries/k8s_logs.query.graphql';
import { buildWatchPath, k8sLogs } from '~/environments/graphql/resolvers/kubernetes/k8s_logs';
import k8sPodLogsWatcherQuery from '~/environments/graphql/queries/k8s_pod_logs_watcher.query.graphql';
import {
buildWatchPath,
k8sLogs,
abortK8sPodLogsStream,
} from '~/environments/graphql/resolvers/kubernetes/k8s_logs';
import { bootstrapWatcherMock } from '../watcher_mock_helper';
jest.mock('lodash/throttle', () => jest.fn());
let watchStream;
const configuration = {
basePath: 'kas-proxy/',
baseOptions: {
headers: { 'GitLab-Agent-Id': '1' },
},
};
const podName = 'test-pod';
const namespace = 'default';
const client = { writeQuery: jest.fn(), readQuery: jest.fn() };
describe('buildWatchPath', () => {
it('should return the correct path with namespace', () => {
const resource = 'my-pod';
const api = 'api/v1';
const namespace = 'my-namespace';
const path = buildWatchPath({ resource, api, namespace });
expect(path).toBe(`/${api}/namespaces/${namespace}/pods/${resource}/log`);
});
});
describe('k8sLogs', () => {
let watchStream;
const configuration = {
basePath: 'kas-proxy/',
baseOptions: {
headers: { 'GitLab-Agent-Id': '1' },
},
};
const podName = 'test-pod';
const namespace = 'default';
const client = { writeQuery: jest.fn(), readQuery: jest.fn() };
beforeEach(() => {
watchStream = bootstrapWatcherMock();
});
......@@ -89,4 +93,64 @@ describe('k8sLogs', () => {
});
},
);
it('should update `k8sPodLogsWatcher` query with the watcher', async () => {
await k8sLogs(null, { configuration, namespace, podName }, { client });
watchStream.triggerEvent(EVENT_PLAIN_TEXT, 'Log data');
expect(client.writeQuery).toHaveBeenCalledWith({
query: k8sPodLogsWatcherQuery,
variables: {
namespace,
configuration,
podName,
},
data: {
k8sPodLogsWatcher: { watcher: {} },
},
});
});
});
describe('abortK8sPodLogsStream', () => {
beforeEach(() => {
watchStream = bootstrapWatcherMock();
});
it('should read `k8sPodLogsWatcher` query to get the watcher', async () => {
await abortK8sPodLogsStream(
null,
{
configuration,
namespace,
podName,
},
{ client },
);
expect(client.readQuery).toHaveBeenCalledWith({
query: k8sPodLogsWatcherQuery,
variables: {
namespace,
configuration,
podName,
},
});
});
it('should abort the stream when the watcher is available', async () => {
client.readQuery.mockReturnValue({ k8sPodLogsWatcher: { watcher: watchStream } });
await abortK8sPodLogsStream(
null,
{
configuration,
namespace,
podName,
},
{ client },
);
expect(watchStream.abortStream).toHaveBeenCalledTimes(1);
});
});
......@@ -4,6 +4,7 @@ const mockWatcher = WatchApi.prototype;
const mockSubscribeFn = jest.fn().mockImplementation(() => {
return Promise.resolve(mockWatcher);
});
const mockAbortStreamFn = jest.fn();
const MockWatchStream = () => {
const callbacks = {};
......@@ -31,10 +32,12 @@ const MockWatchStream = () => {
export const bootstrapWatcherMock = () => {
const watchStream = new MockWatchStream();
jest.spyOn(mockWatcher, 'subscribeToStream').mockImplementation(mockSubscribeFn);
jest.spyOn(mockWatcher, 'abortStream').mockImplementation(mockAbortStreamFn);
jest.spyOn(mockWatcher, 'on').mockImplementation(watchStream.registerCallback);
return {
triggerEvent: watchStream.triggerEvent,
subscribeToStreamMock: mockSubscribeFn,
abortStream: mockAbortStreamFn,
};
};
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment