mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 03:01:21 -04:00
[Security Solution] create task for auto restarting failed OLM transforms (#113686)
This commit is contained in:
parent
8c89daedba
commit
69bee186c2
12 changed files with 526 additions and 35 deletions
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
import type { TransformConfigSchema } from './transforms/types';
|
import type { TransformConfigSchema } from './transforms/types';
|
||||||
import { ENABLE_CASE_CONNECTOR } from '../../cases/common';
|
import { ENABLE_CASE_CONNECTOR } from '../../cases/common';
|
||||||
import { metadataTransformPattern } from './endpoint/constants';
|
import { METADATA_TRANSFORMS_PATTERN } from './endpoint/constants';
|
||||||
|
|
||||||
export const APP_ID = 'securitySolution';
|
export const APP_ID = 'securitySolution';
|
||||||
export const CASES_FEATURE_ID = 'securitySolutionCases';
|
export const CASES_FEATURE_ID = 'securitySolutionCases';
|
||||||
|
@ -331,6 +331,23 @@ export const showAllOthersBucket: string[] = [
|
||||||
*/
|
*/
|
||||||
export const ELASTIC_NAME = 'estc';
|
export const ELASTIC_NAME = 'estc';
|
||||||
|
|
||||||
export const TRANSFORM_STATS_URL = `/api/transform/transforms/${metadataTransformPattern}-*/_stats`;
|
export const METADATA_TRANSFORM_STATS_URL = `/api/transform/transforms/${METADATA_TRANSFORMS_PATTERN}/_stats`;
|
||||||
|
|
||||||
export const RISKY_HOSTS_INDEX = 'ml_host_risk_score_latest';
|
export const RISKY_HOSTS_INDEX = 'ml_host_risk_score_latest';
|
||||||
|
|
||||||
|
export const TRANSFORM_STATES = {
|
||||||
|
ABORTING: 'aborting',
|
||||||
|
FAILED: 'failed',
|
||||||
|
INDEXING: 'indexing',
|
||||||
|
STARTED: 'started',
|
||||||
|
STOPPED: 'stopped',
|
||||||
|
STOPPING: 'stopping',
|
||||||
|
WAITING: 'waiting',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WARNING_TRANSFORM_STATES = new Set([
|
||||||
|
TRANSFORM_STATES.ABORTING,
|
||||||
|
TRANSFORM_STATES.FAILED,
|
||||||
|
TRANSFORM_STATES.STOPPED,
|
||||||
|
TRANSFORM_STATES.STOPPING,
|
||||||
|
]);
|
||||||
|
|
|
@ -20,10 +20,13 @@ export const metadataCurrentIndexPattern = 'metrics-endpoint.metadata_current_*'
|
||||||
/** The metadata Transform Name prefix with NO (package) version) */
|
/** The metadata Transform Name prefix with NO (package) version) */
|
||||||
export const metadataTransformPrefix = 'endpoint.metadata_current-default';
|
export const metadataTransformPrefix = 'endpoint.metadata_current-default';
|
||||||
|
|
||||||
/** The metadata Transform Name prefix with NO namespace and NO (package) version) */
|
// metadata transforms pattern for matching all metadata transform ids
|
||||||
export const metadataTransformPattern = 'endpoint.metadata_current-*';
|
export const METADATA_TRANSFORMS_PATTERN = 'endpoint.metadata_*';
|
||||||
|
|
||||||
|
// united metadata transform id
|
||||||
export const METADATA_UNITED_TRANSFORM = 'endpoint.metadata_united-default';
|
export const METADATA_UNITED_TRANSFORM = 'endpoint.metadata_united-default';
|
||||||
|
|
||||||
|
// united metadata transform destination index
|
||||||
export const METADATA_UNITED_INDEX = '.metrics-endpoint.metadata_united_default';
|
export const METADATA_UNITED_INDEX = '.metrics-endpoint.metadata_united_default';
|
||||||
|
|
||||||
export const policyIndexPattern = 'metrics-endpoint.policy-*';
|
export const policyIndexPattern = 'metrics-endpoint.policy-*';
|
||||||
|
|
|
@ -37,8 +37,8 @@ import {
|
||||||
PendingActionsHttpMockInterface,
|
PendingActionsHttpMockInterface,
|
||||||
pendingActionsHttpMock,
|
pendingActionsHttpMock,
|
||||||
} from '../../../common/lib/endpoint_pending_actions/mocks';
|
} from '../../../common/lib/endpoint_pending_actions/mocks';
|
||||||
import { TRANSFORM_STATS_URL } from '../../../../common/constants';
|
import { METADATA_TRANSFORM_STATS_URL, TRANSFORM_STATES } from '../../../../common/constants';
|
||||||
import { TransformStatsResponse, TRANSFORM_STATE } from './types';
|
import { TransformStatsResponse } from './types';
|
||||||
|
|
||||||
type EndpointMetadataHttpMocksInterface = ResponseProvidersInterface<{
|
type EndpointMetadataHttpMocksInterface = ResponseProvidersInterface<{
|
||||||
metadataList: () => HostResultList;
|
metadataList: () => HostResultList;
|
||||||
|
@ -238,14 +238,14 @@ export const failedTransformStateMock = {
|
||||||
count: 1,
|
count: 1,
|
||||||
transforms: [
|
transforms: [
|
||||||
{
|
{
|
||||||
state: TRANSFORM_STATE.FAILED,
|
state: TRANSFORM_STATES.FAILED,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
export const transformsHttpMocks = httpHandlerMockFactory<TransformHttpMocksInterface>([
|
export const transformsHttpMocks = httpHandlerMockFactory<TransformHttpMocksInterface>([
|
||||||
{
|
{
|
||||||
id: 'metadataTransformStats',
|
id: 'metadataTransformStats',
|
||||||
path: TRANSFORM_STATS_URL,
|
path: METADATA_TRANSFORM_STATS_URL,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
handler: () => failedTransformStateMock,
|
handler: () => failedTransformStateMock,
|
||||||
},
|
},
|
||||||
|
|
|
@ -78,7 +78,7 @@ import { resolvePathVariables } from '../../../../common/utils/resolve_path_vari
|
||||||
import { EndpointPackageInfoStateChanged } from './action';
|
import { EndpointPackageInfoStateChanged } from './action';
|
||||||
import { fetchPendingActionsByAgentId } from '../../../../common/lib/endpoint_pending_actions';
|
import { fetchPendingActionsByAgentId } from '../../../../common/lib/endpoint_pending_actions';
|
||||||
import { getIsInvalidDateRange } from '../utils';
|
import { getIsInvalidDateRange } from '../utils';
|
||||||
import { TRANSFORM_STATS_URL } from '../../../../../common/constants';
|
import { METADATA_TRANSFORM_STATS_URL } from '../../../../../common/constants';
|
||||||
|
|
||||||
type EndpointPageStore = ImmutableMiddlewareAPI<EndpointState, AppAction>;
|
type EndpointPageStore = ImmutableMiddlewareAPI<EndpointState, AppAction>;
|
||||||
|
|
||||||
|
@ -785,7 +785,9 @@ export async function handleLoadMetadataTransformStats(http: HttpStart, store: E
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const transformStatsResponse: TransformStatsResponse = await http.get(TRANSFORM_STATS_URL);
|
const transformStatsResponse: TransformStatsResponse = await http.get(
|
||||||
|
METADATA_TRANSFORM_STATS_URL
|
||||||
|
);
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'metadataTransformStatsChanged',
|
type: 'metadataTransformStatsChanged',
|
||||||
|
|
|
@ -30,7 +30,7 @@ import {
|
||||||
import { GetPolicyListResponse } from '../../policy/types';
|
import { GetPolicyListResponse } from '../../policy/types';
|
||||||
import { pendingActionsResponseMock } from '../../../../common/lib/endpoint_pending_actions/mocks';
|
import { pendingActionsResponseMock } from '../../../../common/lib/endpoint_pending_actions/mocks';
|
||||||
import { ACTION_STATUS_ROUTE } from '../../../../../common/endpoint/constants';
|
import { ACTION_STATUS_ROUTE } from '../../../../../common/endpoint/constants';
|
||||||
import { TRANSFORM_STATS_URL } from '../../../../../common/constants';
|
import { METADATA_TRANSFORM_STATS_URL } from '../../../../../common/constants';
|
||||||
import { TransformStats, TransformStatsResponse } from '../types';
|
import { TransformStats, TransformStatsResponse } from '../types';
|
||||||
|
|
||||||
const generator = new EndpointDocGenerator('seed');
|
const generator = new EndpointDocGenerator('seed');
|
||||||
|
@ -163,7 +163,7 @@ const endpointListApiPathHandlerMocks = ({
|
||||||
return pendingActionsResponseMock();
|
return pendingActionsResponseMock();
|
||||||
},
|
},
|
||||||
|
|
||||||
[TRANSFORM_STATS_URL]: (): TransformStatsResponse => ({
|
[METADATA_TRANSFORM_STATS_URL]: (): TransformStatsResponse => ({
|
||||||
count: transforms.length,
|
count: transforms.length,
|
||||||
transforms,
|
transforms,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { ServerApiError } from '../../../common/types';
|
||||||
import { GetPackagesResponse } from '../../../../../fleet/common';
|
import { GetPackagesResponse } from '../../../../../fleet/common';
|
||||||
import { IIndexPattern } from '../../../../../../../src/plugins/data/public';
|
import { IIndexPattern } from '../../../../../../../src/plugins/data/public';
|
||||||
import { AsyncResourceState } from '../../state';
|
import { AsyncResourceState } from '../../state';
|
||||||
|
import { TRANSFORM_STATES } from '../../../../common/constants';
|
||||||
|
|
||||||
export interface EndpointState {
|
export interface EndpointState {
|
||||||
/** list of host **/
|
/** list of host **/
|
||||||
|
@ -143,24 +144,7 @@ export interface EndpointIndexUIQueryParams {
|
||||||
admin_query?: string;
|
admin_query?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TRANSFORM_STATE = {
|
const transformStates = Object.values(TRANSFORM_STATES);
|
||||||
ABORTING: 'aborting',
|
|
||||||
FAILED: 'failed',
|
|
||||||
INDEXING: 'indexing',
|
|
||||||
STARTED: 'started',
|
|
||||||
STOPPED: 'stopped',
|
|
||||||
STOPPING: 'stopping',
|
|
||||||
WAITING: 'waiting',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WARNING_TRANSFORM_STATES = new Set([
|
|
||||||
TRANSFORM_STATE.ABORTING,
|
|
||||||
TRANSFORM_STATE.FAILED,
|
|
||||||
TRANSFORM_STATE.STOPPED,
|
|
||||||
TRANSFORM_STATE.STOPPING,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const transformStates = Object.values(TRANSFORM_STATE);
|
|
||||||
export type TransformState = typeof transformStates[number];
|
export type TransformState = typeof transformStates[number];
|
||||||
|
|
||||||
export interface TransformStats {
|
export interface TransformStats {
|
||||||
|
|
|
@ -46,8 +46,9 @@ import {
|
||||||
APP_PATH,
|
APP_PATH,
|
||||||
MANAGEMENT_PATH,
|
MANAGEMENT_PATH,
|
||||||
DEFAULT_TIMEPICKER_QUICK_RANGES,
|
DEFAULT_TIMEPICKER_QUICK_RANGES,
|
||||||
|
TRANSFORM_STATES,
|
||||||
} from '../../../../../common/constants';
|
} from '../../../../../common/constants';
|
||||||
import { TransformStats, TRANSFORM_STATE } from '../types';
|
import { TransformStats } from '../types';
|
||||||
import { metadataTransformPrefix } from '../../../../../common/endpoint/constants';
|
import { metadataTransformPrefix } from '../../../../../common/endpoint/constants';
|
||||||
|
|
||||||
// not sure why this can't be imported from '../../../../common/mock/formatted_relative';
|
// not sure why this can't be imported from '../../../../common/mock/formatted_relative';
|
||||||
|
@ -1403,7 +1404,7 @@ describe('when on the endpoint list page', () => {
|
||||||
const transforms: TransformStats[] = [
|
const transforms: TransformStats[] = [
|
||||||
{
|
{
|
||||||
id: `${metadataTransformPrefix}-0.20.0`,
|
id: `${metadataTransformPrefix}-0.20.0`,
|
||||||
state: TRANSFORM_STATE.STARTED,
|
state: TRANSFORM_STATES.STARTED,
|
||||||
} as TransformStats,
|
} as TransformStats,
|
||||||
];
|
];
|
||||||
setEndpointListApiMockImplementation(coreStart.http, { transforms });
|
setEndpointListApiMockImplementation(coreStart.http, { transforms });
|
||||||
|
@ -1414,7 +1415,7 @@ describe('when on the endpoint list page', () => {
|
||||||
|
|
||||||
it('is not displayed when non-relevant transform is failing', () => {
|
it('is not displayed when non-relevant transform is failing', () => {
|
||||||
const transforms: TransformStats[] = [
|
const transforms: TransformStats[] = [
|
||||||
{ id: 'not-metadata', state: TRANSFORM_STATE.FAILED } as TransformStats,
|
{ id: 'not-metadata', state: TRANSFORM_STATES.FAILED } as TransformStats,
|
||||||
];
|
];
|
||||||
setEndpointListApiMockImplementation(coreStart.http, { transforms });
|
setEndpointListApiMockImplementation(coreStart.http, { transforms });
|
||||||
render();
|
render();
|
||||||
|
@ -1426,7 +1427,7 @@ describe('when on the endpoint list page', () => {
|
||||||
const transforms: TransformStats[] = [
|
const transforms: TransformStats[] = [
|
||||||
{
|
{
|
||||||
id: `${metadataTransformPrefix}-0.20.0`,
|
id: `${metadataTransformPrefix}-0.20.0`,
|
||||||
state: TRANSFORM_STATE.FAILED,
|
state: TRANSFORM_STATES.FAILED,
|
||||||
} as TransformStats,
|
} as TransformStats,
|
||||||
];
|
];
|
||||||
setEndpointListApiMockImplementation(coreStart.http, { transforms });
|
setEndpointListApiMockImplementation(coreStart.http, { transforms });
|
||||||
|
|
|
@ -58,8 +58,8 @@ import { LinkToApp } from '../../../../common/components/endpoint/link_to_app';
|
||||||
import { TableRowActions } from './components/table_row_actions';
|
import { TableRowActions } from './components/table_row_actions';
|
||||||
import { EndpointAgentStatus } from './components/endpoint_agent_status';
|
import { EndpointAgentStatus } from './components/endpoint_agent_status';
|
||||||
import { CallOut } from '../../../../common/components/callouts';
|
import { CallOut } from '../../../../common/components/callouts';
|
||||||
import { WARNING_TRANSFORM_STATES } from '../types';
|
|
||||||
import { metadataTransformPrefix } from '../../../../../common/endpoint/constants';
|
import { metadataTransformPrefix } from '../../../../../common/endpoint/constants';
|
||||||
|
import { WARNING_TRANSFORM_STATES } from '../../../../../common/constants';
|
||||||
|
|
||||||
const MAX_PAGINATED_ITEM = 9999;
|
const MAX_PAGINATED_ITEM = 9999;
|
||||||
const TRANSFORM_URL = '/data/transform';
|
const TRANSFORM_URL = '/data/transform';
|
||||||
|
|
|
@ -0,0 +1,250 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
import { ApiResponse } from '@elastic/elasticsearch';
|
||||||
|
import { TransformGetTransformStatsResponse } from '@elastic/elasticsearch/api/types';
|
||||||
|
import {
|
||||||
|
CheckMetadataTransformsTask,
|
||||||
|
TYPE,
|
||||||
|
VERSION,
|
||||||
|
BASE_NEXT_ATTEMPT_DELAY,
|
||||||
|
} from './check_metadata_transforms_task';
|
||||||
|
import { createMockEndpointAppContext } from '../../mocks';
|
||||||
|
import { coreMock } from '../../../../../../../src/core/server/mocks';
|
||||||
|
import { taskManagerMock } from '../../../../../task_manager/server/mocks';
|
||||||
|
import { TaskManagerSetupContract, TaskStatus } from '../../../../../task_manager/server';
|
||||||
|
import { CoreSetup } from '../../../../../../../src/core/server';
|
||||||
|
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||||
|
import { ElasticsearchClientMock } from '../../../../../../../src/core/server/elasticsearch/client/mocks';
|
||||||
|
import { TRANSFORM_STATES } from '../../../../common/constants';
|
||||||
|
import { METADATA_TRANSFORMS_PATTERN } from '../../../../common/endpoint/constants';
|
||||||
|
import { RunResult } from '../../../../../task_manager/server/task';
|
||||||
|
|
||||||
|
const MOCK_TASK_INSTANCE = {
|
||||||
|
id: `${TYPE}:${VERSION}`,
|
||||||
|
runAt: new Date(),
|
||||||
|
attempts: 0,
|
||||||
|
ownerId: '',
|
||||||
|
status: TaskStatus.Running,
|
||||||
|
startedAt: new Date(),
|
||||||
|
scheduledAt: new Date(),
|
||||||
|
retryAt: new Date(),
|
||||||
|
params: {},
|
||||||
|
state: {},
|
||||||
|
taskType: TYPE,
|
||||||
|
};
|
||||||
|
const failedTransformId = 'failing-transform';
|
||||||
|
const goodTransformId = 'good-transform';
|
||||||
|
|
||||||
|
describe('check metadata transforms task', () => {
|
||||||
|
const { createSetup: coreSetupMock } = coreMock;
|
||||||
|
const { createSetup: tmSetupMock, createStart: tmStartMock } = taskManagerMock;
|
||||||
|
|
||||||
|
let mockTask: CheckMetadataTransformsTask;
|
||||||
|
let mockCore: CoreSetup;
|
||||||
|
let mockTaskManagerSetup: jest.Mocked<TaskManagerSetupContract>;
|
||||||
|
beforeAll(() => {
|
||||||
|
mockCore = coreSetupMock();
|
||||||
|
mockTaskManagerSetup = tmSetupMock();
|
||||||
|
mockTask = new CheckMetadataTransformsTask({
|
||||||
|
endpointAppContext: createMockEndpointAppContext(),
|
||||||
|
core: mockCore,
|
||||||
|
taskManager: mockTaskManagerSetup,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('task lifecycle', () => {
|
||||||
|
it('should create task', () => {
|
||||||
|
expect(mockTask).toBeInstanceOf(CheckMetadataTransformsTask);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should register task', () => {
|
||||||
|
expect(mockTaskManagerSetup.registerTaskDefinitions).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should schedule task', async () => {
|
||||||
|
const mockTaskManagerStart = tmStartMock();
|
||||||
|
await mockTask.start({ taskManager: mockTaskManagerStart });
|
||||||
|
expect(mockTaskManagerStart.ensureScheduled).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('task logic', () => {
|
||||||
|
let esClient: ElasticsearchClientMock;
|
||||||
|
beforeEach(async () => {
|
||||||
|
const [{ elasticsearch }] = await mockCore.getStartServices();
|
||||||
|
esClient = elasticsearch.client.asInternalUser as ElasticsearchClientMock;
|
||||||
|
});
|
||||||
|
|
||||||
|
const runTask = async (taskInstance = MOCK_TASK_INSTANCE) => {
|
||||||
|
const mockTaskManagerStart = tmStartMock();
|
||||||
|
await mockTask.start({ taskManager: mockTaskManagerStart });
|
||||||
|
const createTaskRunner =
|
||||||
|
mockTaskManagerSetup.registerTaskDefinitions.mock.calls[0][0][TYPE].createTaskRunner;
|
||||||
|
const taskRunner = createTaskRunner({ taskInstance });
|
||||||
|
return taskRunner.run();
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildFailedStatsResponse = () =>
|
||||||
|
({
|
||||||
|
body: {
|
||||||
|
transforms: [
|
||||||
|
{
|
||||||
|
id: goodTransformId,
|
||||||
|
state: TRANSFORM_STATES.STARTED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: failedTransformId,
|
||||||
|
state: TRANSFORM_STATES.FAILED,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
} as unknown as ApiResponse<TransformGetTransformStatsResponse>);
|
||||||
|
|
||||||
|
it('should stop task if transform stats response fails', async () => {
|
||||||
|
esClient.transform.getTransformStats.mockRejectedValue({});
|
||||||
|
await runTask();
|
||||||
|
expect(esClient.transform.getTransformStats).toHaveBeenCalledWith({
|
||||||
|
transform_id: METADATA_TRANSFORMS_PATTERN,
|
||||||
|
});
|
||||||
|
expect(esClient.transform.stopTransform).not.toHaveBeenCalled();
|
||||||
|
expect(esClient.transform.startTransform).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should attempt transform restart if failing state', async () => {
|
||||||
|
const transformStatsResponseMock = buildFailedStatsResponse();
|
||||||
|
esClient.transform.getTransformStats.mockResolvedValue(transformStatsResponseMock);
|
||||||
|
|
||||||
|
const taskResponse = (await runTask()) as RunResult;
|
||||||
|
|
||||||
|
expect(esClient.transform.getTransformStats).toHaveBeenCalledWith({
|
||||||
|
transform_id: METADATA_TRANSFORMS_PATTERN,
|
||||||
|
});
|
||||||
|
expect(esClient.transform.stopTransform).toHaveBeenCalledWith({
|
||||||
|
transform_id: failedTransformId,
|
||||||
|
allow_no_match: true,
|
||||||
|
wait_for_completion: true,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
expect(esClient.transform.startTransform).toHaveBeenCalledWith({
|
||||||
|
transform_id: failedTransformId,
|
||||||
|
});
|
||||||
|
expect(taskResponse?.state?.attempts).toEqual({
|
||||||
|
[goodTransformId]: 0,
|
||||||
|
[failedTransformId]: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly track transform restart attempts', async () => {
|
||||||
|
const transformStatsResponseMock = buildFailedStatsResponse();
|
||||||
|
esClient.transform.getTransformStats.mockResolvedValue(transformStatsResponseMock);
|
||||||
|
|
||||||
|
esClient.transform.stopTransform.mockRejectedValueOnce({});
|
||||||
|
let taskResponse = (await runTask()) as RunResult;
|
||||||
|
expect(taskResponse?.state?.attempts).toEqual({
|
||||||
|
[goodTransformId]: 0,
|
||||||
|
[failedTransformId]: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
esClient.transform.startTransform.mockRejectedValueOnce({});
|
||||||
|
taskResponse = (await runTask({
|
||||||
|
...MOCK_TASK_INSTANCE,
|
||||||
|
state: taskResponse.state,
|
||||||
|
})) as RunResult;
|
||||||
|
expect(taskResponse?.state?.attempts).toEqual({
|
||||||
|
[goodTransformId]: 0,
|
||||||
|
[failedTransformId]: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
taskResponse = (await runTask({
|
||||||
|
...MOCK_TASK_INSTANCE,
|
||||||
|
state: taskResponse.state,
|
||||||
|
})) as RunResult;
|
||||||
|
expect(taskResponse?.state?.attempts).toEqual({
|
||||||
|
[goodTransformId]: 0,
|
||||||
|
[failedTransformId]: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly back off subsequent restart attempts', async () => {
|
||||||
|
let transformStatsResponseMock = buildFailedStatsResponse();
|
||||||
|
esClient.transform.getTransformStats.mockResolvedValue(transformStatsResponseMock);
|
||||||
|
|
||||||
|
esClient.transform.stopTransform.mockRejectedValueOnce({});
|
||||||
|
let taskStartedAt = new Date();
|
||||||
|
let taskResponse = (await runTask()) as RunResult;
|
||||||
|
let delay = BASE_NEXT_ATTEMPT_DELAY * 60000;
|
||||||
|
let expectedRunAt = taskStartedAt.getTime() + delay;
|
||||||
|
expect(taskResponse?.runAt?.getTime()).toBeGreaterThanOrEqual(expectedRunAt);
|
||||||
|
// we don't have the exact timestamp it uses so give a buffer
|
||||||
|
let expectedRunAtUpperBound = expectedRunAt + 1000;
|
||||||
|
expect(taskResponse?.runAt?.getTime()).toBeLessThanOrEqual(expectedRunAtUpperBound);
|
||||||
|
|
||||||
|
esClient.transform.startTransform.mockRejectedValueOnce({});
|
||||||
|
taskStartedAt = new Date();
|
||||||
|
taskResponse = (await runTask({
|
||||||
|
...MOCK_TASK_INSTANCE,
|
||||||
|
state: taskResponse.state,
|
||||||
|
})) as RunResult;
|
||||||
|
// should be exponential on second+ attempt
|
||||||
|
delay = BASE_NEXT_ATTEMPT_DELAY ** 2 * 60000;
|
||||||
|
expectedRunAt = taskStartedAt.getTime() + delay;
|
||||||
|
expect(taskResponse?.runAt?.getTime()).toBeGreaterThanOrEqual(expectedRunAt);
|
||||||
|
// we don't have the exact timestamp it uses so give a buffer
|
||||||
|
expectedRunAtUpperBound = expectedRunAt + 1000;
|
||||||
|
expect(taskResponse?.runAt?.getTime()).toBeLessThanOrEqual(expectedRunAtUpperBound);
|
||||||
|
|
||||||
|
esClient.transform.stopTransform.mockRejectedValueOnce({});
|
||||||
|
taskStartedAt = new Date();
|
||||||
|
taskResponse = (await runTask({
|
||||||
|
...MOCK_TASK_INSTANCE,
|
||||||
|
state: taskResponse.state,
|
||||||
|
})) as RunResult;
|
||||||
|
// should be exponential on second+ attempt
|
||||||
|
delay = BASE_NEXT_ATTEMPT_DELAY ** 3 * 60000;
|
||||||
|
expectedRunAt = taskStartedAt.getTime() + delay;
|
||||||
|
expect(taskResponse?.runAt?.getTime()).toBeGreaterThanOrEqual(expectedRunAt);
|
||||||
|
// we don't have the exact timestamp it uses so give a buffer
|
||||||
|
expectedRunAtUpperBound = expectedRunAt + 1000;
|
||||||
|
expect(taskResponse?.runAt?.getTime()).toBeLessThanOrEqual(expectedRunAtUpperBound);
|
||||||
|
|
||||||
|
taskStartedAt = new Date();
|
||||||
|
taskResponse = (await runTask({
|
||||||
|
...MOCK_TASK_INSTANCE,
|
||||||
|
state: taskResponse.state,
|
||||||
|
})) as RunResult;
|
||||||
|
// back to base delay after success
|
||||||
|
delay = BASE_NEXT_ATTEMPT_DELAY * 60000;
|
||||||
|
expectedRunAt = taskStartedAt.getTime() + delay;
|
||||||
|
expect(taskResponse?.runAt?.getTime()).toBeGreaterThanOrEqual(expectedRunAt);
|
||||||
|
// we don't have the exact timestamp it uses so give a buffer
|
||||||
|
expectedRunAtUpperBound = expectedRunAt + 1000;
|
||||||
|
expect(taskResponse?.runAt?.getTime()).toBeLessThanOrEqual(expectedRunAtUpperBound);
|
||||||
|
|
||||||
|
transformStatsResponseMock = {
|
||||||
|
body: {
|
||||||
|
transforms: [
|
||||||
|
{
|
||||||
|
id: goodTransformId,
|
||||||
|
state: TRANSFORM_STATES.STARTED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: failedTransformId,
|
||||||
|
state: TRANSFORM_STATES.STARTED,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
} as unknown as ApiResponse<TransformGetTransformStatsResponse>;
|
||||||
|
esClient.transform.getTransformStats.mockResolvedValue(transformStatsResponseMock);
|
||||||
|
taskResponse = (await runTask({
|
||||||
|
...MOCK_TASK_INSTANCE,
|
||||||
|
state: taskResponse.state,
|
||||||
|
})) as RunResult;
|
||||||
|
// no more explicit runAt after subsequent success
|
||||||
|
expect(taskResponse?.runAt).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,214 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
import { ApiResponse } from '@elastic/elasticsearch';
|
||||||
|
import {
|
||||||
|
TransformGetTransformStatsResponse,
|
||||||
|
TransformGetTransformStatsTransformStats,
|
||||||
|
} from '@elastic/elasticsearch/api/types';
|
||||||
|
import { CoreSetup, ElasticsearchClient, Logger } from 'src/core/server';
|
||||||
|
import {
|
||||||
|
ConcreteTaskInstance,
|
||||||
|
TaskManagerSetupContract,
|
||||||
|
TaskManagerStartContract,
|
||||||
|
throwUnrecoverableError,
|
||||||
|
} from '../../../../../task_manager/server';
|
||||||
|
import { EndpointAppContext } from '../../types';
|
||||||
|
import { METADATA_TRANSFORMS_PATTERN } from '../../../../common/endpoint/constants';
|
||||||
|
import { WARNING_TRANSFORM_STATES } from '../../../../common/constants';
|
||||||
|
import { wrapErrorIfNeeded } from '../../utils';
|
||||||
|
|
||||||
|
const SCOPE = ['securitySolution'];
|
||||||
|
const INTERVAL = '2h';
|
||||||
|
const TIMEOUT = '4m';
|
||||||
|
export const TYPE = 'endpoint:metadata-check-transforms-task';
|
||||||
|
export const VERSION = '0.0.1';
|
||||||
|
const MAX_ATTEMPTS = 5;
|
||||||
|
export const BASE_NEXT_ATTEMPT_DELAY = 5; // minutes
|
||||||
|
|
||||||
|
export interface CheckMetadataTransformsTaskSetupContract {
|
||||||
|
endpointAppContext: EndpointAppContext;
|
||||||
|
core: CoreSetup;
|
||||||
|
taskManager: TaskManagerSetupContract;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CheckMetadataTransformsTaskStartContract {
|
||||||
|
taskManager: TaskManagerStartContract;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CheckMetadataTransformsTask {
|
||||||
|
private logger: Logger;
|
||||||
|
private wasStarted: boolean = false;
|
||||||
|
|
||||||
|
constructor(setupContract: CheckMetadataTransformsTaskSetupContract) {
|
||||||
|
const { endpointAppContext, core, taskManager } = setupContract;
|
||||||
|
this.logger = endpointAppContext.logFactory.get(this.getTaskId());
|
||||||
|
taskManager.registerTaskDefinitions({
|
||||||
|
[TYPE]: {
|
||||||
|
title: 'Security Solution Endpoint Metadata Periodic Tasks',
|
||||||
|
timeout: TIMEOUT,
|
||||||
|
createTaskRunner: ({ taskInstance }: { taskInstance: ConcreteTaskInstance }) => {
|
||||||
|
return {
|
||||||
|
run: async () => {
|
||||||
|
return this.runTask(taskInstance, core);
|
||||||
|
},
|
||||||
|
cancel: async () => {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public start = async ({ taskManager }: CheckMetadataTransformsTaskStartContract) => {
|
||||||
|
if (!taskManager) {
|
||||||
|
this.logger.error('missing required service during start');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.wasStarted = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await taskManager.ensureScheduled({
|
||||||
|
id: this.getTaskId(),
|
||||||
|
taskType: TYPE,
|
||||||
|
scope: SCOPE,
|
||||||
|
schedule: {
|
||||||
|
interval: INTERVAL,
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
attempts: {},
|
||||||
|
},
|
||||||
|
params: { version: VERSION },
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.debug(`Error scheduling task, received ${e.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private runTask = async (taskInstance: ConcreteTaskInstance, core: CoreSetup) => {
|
||||||
|
// if task was not `.start()`'d yet, then exit
|
||||||
|
if (!this.wasStarted) {
|
||||||
|
this.logger.debug('[runTask()] Aborted. MetadataTask not started yet');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that this task is current
|
||||||
|
if (taskInstance.id !== this.getTaskId()) {
|
||||||
|
// old task, die
|
||||||
|
throwUnrecoverableError(new Error('Outdated task version'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const [{ elasticsearch }] = await core.getStartServices();
|
||||||
|
const esClient = elasticsearch.client.asInternalUser;
|
||||||
|
|
||||||
|
let transformStatsResponse: ApiResponse<TransformGetTransformStatsResponse>;
|
||||||
|
try {
|
||||||
|
transformStatsResponse = await esClient?.transform.getTransformStats({
|
||||||
|
transform_id: METADATA_TRANSFORMS_PATTERN,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
const err = wrapErrorIfNeeded(e);
|
||||||
|
const errMessage = `failed to get transform stats with error: ${err}`;
|
||||||
|
this.logger.error(errMessage);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { transforms } = transformStatsResponse.body;
|
||||||
|
if (!transforms.length) {
|
||||||
|
this.logger.info('no OLM metadata transforms found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let didAttemptRestart: boolean = false;
|
||||||
|
let highestAttempt: number = 0;
|
||||||
|
const attempts = { ...taskInstance.state.attempts };
|
||||||
|
|
||||||
|
for (const transform of transforms) {
|
||||||
|
const restartedTransform = await this.restartTransform(
|
||||||
|
esClient,
|
||||||
|
transform,
|
||||||
|
attempts[transform.id]
|
||||||
|
);
|
||||||
|
if (restartedTransform.didAttemptRestart) {
|
||||||
|
didAttemptRestart = true;
|
||||||
|
}
|
||||||
|
attempts[transform.id] = restartedTransform.attempts;
|
||||||
|
highestAttempt = Math.max(attempts[transform.id], highestAttempt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// after a restart attempt run next check sooner with exponential backoff
|
||||||
|
let runAt: Date | undefined;
|
||||||
|
if (didAttemptRestart) {
|
||||||
|
const delay = BASE_NEXT_ATTEMPT_DELAY ** Math.max(highestAttempt, 1) * 60000;
|
||||||
|
runAt = new Date(new Date().getTime() + delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextState = { attempts };
|
||||||
|
const nextTask = runAt ? { state: nextState, runAt } : { state: nextState };
|
||||||
|
return nextTask;
|
||||||
|
};
|
||||||
|
|
||||||
|
private restartTransform = async (
|
||||||
|
esClient: ElasticsearchClient,
|
||||||
|
transform: TransformGetTransformStatsTransformStats,
|
||||||
|
currentAttempts: number = 0
|
||||||
|
) => {
|
||||||
|
let attempts = currentAttempts;
|
||||||
|
let didAttemptRestart = false;
|
||||||
|
|
||||||
|
if (!WARNING_TRANSFORM_STATES.has(transform.state)) {
|
||||||
|
return {
|
||||||
|
attempts,
|
||||||
|
didAttemptRestart,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempts > MAX_ATTEMPTS) {
|
||||||
|
this.logger.warn(
|
||||||
|
`transform ${transform.id} has failed to restart ${attempts} times. stopping auto restart attempts.`
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
attempts,
|
||||||
|
didAttemptRestart,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.logger.info(`failed transform detected with id: ${transform.id}. attempting restart.`);
|
||||||
|
await esClient.transform.stopTransform({
|
||||||
|
transform_id: transform.id,
|
||||||
|
allow_no_match: true,
|
||||||
|
wait_for_completion: true,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
await esClient.transform.startTransform({
|
||||||
|
transform_id: transform.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// restart succeeded, reset attempt count
|
||||||
|
attempts = 0;
|
||||||
|
} catch (e) {
|
||||||
|
const err = wrapErrorIfNeeded(e);
|
||||||
|
const errMessage = `failed to restart transform ${transform.id} with error: ${err}`;
|
||||||
|
this.logger.error(errMessage);
|
||||||
|
|
||||||
|
// restart failed, increment attempt count
|
||||||
|
attempts = attempts + 1;
|
||||||
|
} finally {
|
||||||
|
didAttemptRestart = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
attempts,
|
||||||
|
didAttemptRestart,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
private getTaskId = (): string => {
|
||||||
|
return `${TYPE}:${VERSION}`;
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './check_metadata_transforms_task';
|
|
@ -59,6 +59,7 @@ import { initRoutes } from './routes';
|
||||||
import { isAlertExecutor } from './lib/detection_engine/signals/types';
|
import { isAlertExecutor } from './lib/detection_engine/signals/types';
|
||||||
import { signalRulesAlertType } from './lib/detection_engine/signals/signal_rule_alert_type';
|
import { signalRulesAlertType } from './lib/detection_engine/signals/signal_rule_alert_type';
|
||||||
import { ManifestTask } from './endpoint/lib/artifacts';
|
import { ManifestTask } from './endpoint/lib/artifacts';
|
||||||
|
import { CheckMetadataTransformsTask } from './endpoint/lib/metadata';
|
||||||
import { initSavedObjects } from './saved_objects';
|
import { initSavedObjects } from './saved_objects';
|
||||||
import { AppClientFactory } from './client';
|
import { AppClientFactory } from './client';
|
||||||
import { createConfig, ConfigType } from './config';
|
import { createConfig, ConfigType } from './config';
|
||||||
|
@ -157,6 +158,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
||||||
private policyWatcher?: PolicyWatcher;
|
private policyWatcher?: PolicyWatcher;
|
||||||
|
|
||||||
private manifestTask: ManifestTask | undefined;
|
private manifestTask: ManifestTask | undefined;
|
||||||
|
private checkMetadataTransformsTask: CheckMetadataTransformsTask | undefined;
|
||||||
private artifactsCache: LRU<string, Buffer>;
|
private artifactsCache: LRU<string, Buffer>;
|
||||||
private telemetryUsageCounter?: UsageCounter;
|
private telemetryUsageCounter?: UsageCounter;
|
||||||
|
|
||||||
|
@ -363,6 +365,12 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
||||||
this.telemetryUsageCounter
|
this.telemetryUsageCounter
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.checkMetadataTransformsTask = new CheckMetadataTransformsTask({
|
||||||
|
endpointAppContext: endpointContext,
|
||||||
|
core,
|
||||||
|
taskManager: plugins.taskManager!,
|
||||||
|
});
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -452,6 +460,10 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
||||||
this.telemetryReceiver
|
this.telemetryReceiver
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.checkMetadataTransformsTask?.start({
|
||||||
|
taskManager: plugins.taskManager!,
|
||||||
|
});
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue