From 585f4cdf4de9c9095c45c01be1b81de40f97fe44 Mon Sep 17 00:00:00 2001 From: "Joey F. Poon" Date: Wed, 2 Nov 2022 17:30:00 -0500 Subject: [PATCH] [Fleet] add DELETED file update task Adds Kibana task for updating file status to DELETED when there are no file chunks. --- .../plugins/fleet/common/constants/index.ts | 2 + .../fleet/server/constants/fleet_es_assets.ts | 4 + x-pack/plugins/fleet/server/plugin.ts | 8 + .../fleet/server/services/files/index.test.ts | 204 ++++++++++++++++++ .../fleet/server/services/files/index.ts | 149 +++++++++++++ .../tasks/check_deleted_files_task.test.ts | 176 +++++++++++++++ .../server/tasks/check_deleted_files_task.ts | 141 ++++++++++++ .../plugins/fleet/server/types/files/index.ts | 8 + .../check_registered_task_types.ts | 1 + .../test_suites/task_manager/health_route.ts | 3 +- 10 files changed, 695 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/fleet/server/services/files/index.test.ts create mode 100644 x-pack/plugins/fleet/server/services/files/index.ts create mode 100644 x-pack/plugins/fleet/server/tasks/check_deleted_files_task.test.ts create mode 100644 x-pack/plugins/fleet/server/tasks/check_deleted_files_task.ts create mode 100644 x-pack/plugins/fleet/server/types/files/index.ts diff --git a/x-pack/plugins/fleet/common/constants/index.ts b/x-pack/plugins/fleet/common/constants/index.ts index 01193687125c65..3aeed30c7e41be 100644 --- a/x-pack/plugins/fleet/common/constants/index.ts +++ b/x-pack/plugins/fleet/common/constants/index.ts @@ -25,6 +25,8 @@ export * from './authz'; // setting in the future? export const SO_SEARCH_LIMIT = 10000; +export const ES_SEARCH_LIMIT = 10000; + export const FLEET_SERVER_INDICES_VERSION = 1; export const FLEET_SERVER_ARTIFACTS_INDEX = '.fleet-artifacts'; diff --git a/x-pack/plugins/fleet/server/constants/fleet_es_assets.ts b/x-pack/plugins/fleet/server/constants/fleet_es_assets.ts index 001aa0caf6a47e..1b4d784fcdf9dc 100644 --- a/x-pack/plugins/fleet/server/constants/fleet_es_assets.ts +++ b/x-pack/plugins/fleet/server/constants/fleet_es_assets.ts @@ -194,3 +194,7 @@ on_failure: field: error.message value: - 'failed in Fleet agent final_pipeline: {{ _ingest.on_failure_message }}'`; + +// File storage indexes supporting endpoint Upload/download +export const FILE_STORAGE_METADATA_INDEX = '.fleet-*-files'; +export const FILE_STORAGE_DATA_INDEX = '.fleet-*-file-data'; diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index ee69ae9fc8e657..63ef9a304ade84 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -111,6 +111,7 @@ import { BulkActionsResolver } from './services/agents'; import type { PackagePolicyService } from './services/package_policy_service'; import { PackagePolicyServiceImpl } from './services/package_policy'; import { registerFleetUsageLogger, startFleetUsageLogger } from './services/fleet_usage_logger'; +import { CheckDeletedFilesTask } from './tasks/check_deleted_files_task'; export interface FleetSetupDeps { security: SecurityPluginSetup; @@ -220,6 +221,7 @@ export class FleetPlugin private readonly fleetStatus$: BehaviorSubject; private bulkActionsResolver?: BulkActionsResolver; private fleetUsageSender?: FleetUsageSender; + private checkDeletedFilesTask?: CheckDeletedFilesTask; private agentService?: AgentService; private packageService?: PackageService; @@ -426,6 +428,11 @@ export class FleetPlugin this.telemetryEventsSender.setup(deps.telemetry); this.bulkActionsResolver = new BulkActionsResolver(deps.taskManager, core); + this.checkDeletedFilesTask = new CheckDeletedFilesTask({ + core, + taskManager: deps.taskManager, + logFactory: this.initializerContext.logger, + }); } public start(core: CoreStart, plugins: FleetStartDeps): FleetStartContract { @@ -457,6 +464,7 @@ export class FleetPlugin this.telemetryEventsSender.start(plugins.telemetry, core); this.bulkActionsResolver?.start(plugins.taskManager); this.fleetUsageSender?.start(plugins.taskManager); + this.checkDeletedFilesTask?.start({ taskManager: plugins.taskManager }); startFleetUsageLogger(plugins.taskManager); const logger = appContextService.getLogger(); diff --git a/x-pack/plugins/fleet/server/services/files/index.test.ts b/x-pack/plugins/fleet/server/services/files/index.test.ts new file mode 100644 index 00000000000000..8d1baf22022357 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/files/index.test.ts @@ -0,0 +1,204 @@ +/* + * 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 type { ElasticsearchClientMock } from '@kbn/core/server/mocks'; +import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; + +import { ES_SEARCH_LIMIT } from '../../../common/constants'; +import { + FILE_STORAGE_DATA_INDEX, + FILE_STORAGE_METADATA_INDEX, +} from '../../constants/fleet_es_assets'; + +import { fileIdsWithoutChunksByIndex, getFilesByStatus, updateFilesStatus } from '.'; + +const ENDPOINT_FILE_METADATA_INDEX = '.fleet-endpoint-files'; +const ENDPOINT_FILE_INDEX = '.fleet-endpoint-file-data'; + +describe('files service', () => { + let esClientMock: ElasticsearchClientMock; + const abortController = new AbortController(); + + beforeEach(() => { + esClientMock = elasticsearchServiceMock.createElasticsearchClient(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('#getFilesByStatus()', () => { + it('should return expected values', async () => { + const status = 'READY'; + esClientMock.search.mockResolvedValueOnce({ + took: 5, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + hits: [ + { + _index: ENDPOINT_FILE_METADATA_INDEX, + _id: 'someid1', + }, + { + _index: ENDPOINT_FILE_METADATA_INDEX, + _id: 'someid2', + }, + ], + }, + }); + + const result = await getFilesByStatus(esClientMock, abortController, status); + + expect(esClientMock.search).toBeCalledWith( + { + index: FILE_STORAGE_METADATA_INDEX, + body: { + size: ES_SEARCH_LIMIT, + query: { + term: { + 'file.Status.keyword': status, + }, + }, + _source: false, + }, + ignore_unavailable: true, + }, + { signal: abortController.signal } + ); + expect(result).toEqual([ + { _index: ENDPOINT_FILE_METADATA_INDEX, _id: 'someid1' }, + { _index: ENDPOINT_FILE_METADATA_INDEX, _id: 'someid2' }, + ]); + }); + }); + + describe('#fileIdsWithoutChunks()', () => { + it('should return expected values', async () => { + esClientMock.search.mockResolvedValueOnce({ + took: 5, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + hits: [ + { + _index: ENDPOINT_FILE_INDEX, + _id: 'keep1', + _source: { + bid: 'keep1', + }, + }, + { + _index: ENDPOINT_FILE_INDEX, + _id: 'keep2', + _source: { + bid: 'keep2', + }, + }, + ], + }, + }); + + const files = [ + { _index: ENDPOINT_FILE_METADATA_INDEX, _id: 'keep1' }, + { _index: ENDPOINT_FILE_METADATA_INDEX, _id: 'keep2' }, + { _index: ENDPOINT_FILE_METADATA_INDEX, _id: 'delete1' }, + { _index: ENDPOINT_FILE_METADATA_INDEX, _id: 'delete2' }, + ]; + const { fileIdsByIndex: deletedFileIdsByIndex, allFileIds: allDeletedFileIds } = + await fileIdsWithoutChunksByIndex(esClientMock, abortController, files); + + expect(esClientMock.search).toBeCalledWith( + { + index: FILE_STORAGE_DATA_INDEX, + body: { + size: ES_SEARCH_LIMIT, + query: { + bool: { + must: [ + { + terms: { + 'bid.keyword': Array.from(files.map((file) => file._id)), + }, + }, + { + term: { + last: true, + }, + }, + ], + }, + }, + _source: ['bid'], + }, + }, + { signal: abortController.signal } + ); + expect(deletedFileIdsByIndex).toEqual({ + [ENDPOINT_FILE_METADATA_INDEX]: new Set(['delete1', 'delete2']), + }); + expect(allDeletedFileIds).toEqual(new Set(['delete1', 'delete2'])); + }); + }); + + describe('#updateFilesStatus()', () => { + it('calls esClient.updateByQuery with expected values', () => { + const FAKE_INTEGRATION_METADATA_INDEX = '.fleet-someintegration-files'; + const files = { + [ENDPOINT_FILE_METADATA_INDEX]: new Set(['delete1', 'delete2']), + [FAKE_INTEGRATION_METADATA_INDEX]: new Set(['delete2', 'delete3']), + }; + const status = 'DELETED'; + updateFilesStatus(esClientMock, abortController, files, status); + + expect(esClientMock.updateByQuery).toHaveBeenNthCalledWith( + 1, + { + index: ENDPOINT_FILE_METADATA_INDEX, + refresh: true, + query: { + ids: { + values: Array.from(files[ENDPOINT_FILE_METADATA_INDEX]), + }, + }, + script: { + source: `ctx._source.file.Status = '${status}'`, + lang: 'painless', + }, + }, + { signal: abortController.signal } + ); + expect(esClientMock.updateByQuery).toHaveBeenNthCalledWith( + 2, + { + index: FAKE_INTEGRATION_METADATA_INDEX, + refresh: true, + query: { + ids: { + values: Array.from(files[FAKE_INTEGRATION_METADATA_INDEX]), + }, + }, + script: { + source: `ctx._source.file.Status = '${status}'`, + lang: 'painless', + }, + }, + { signal: abortController.signal } + ); + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/files/index.ts b/x-pack/plugins/fleet/server/services/files/index.ts new file mode 100644 index 00000000000000..c5b472e917a537 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/files/index.ts @@ -0,0 +1,149 @@ +/* + * 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 type { ElasticsearchClient } from '@kbn/core/server'; +import type { UpdateByQueryResponse, SearchHit } from '@elastic/elasticsearch/lib/api/types'; + +import { + FILE_STORAGE_DATA_INDEX, + FILE_STORAGE_METADATA_INDEX, +} from '../../constants/fleet_es_assets'; +import { ES_SEARCH_LIMIT } from '../../../common/constants'; +import type { FILE_STATUS } from '../../types/files'; + +/** + * Gets files with given status + * + * @param esClient + * @param abortController + * @param status + */ +export async function getFilesByStatus( + esClient: ElasticsearchClient, + abortController: AbortController, + status: FILE_STATUS = 'READY' +): Promise { + const result = await esClient.search( + { + index: FILE_STORAGE_METADATA_INDEX, + body: { + size: ES_SEARCH_LIMIT, + query: { + term: { + 'file.Status.keyword': status, + }, + }, + _source: false, + }, + ignore_unavailable: true, + }, + { signal: abortController.signal } + ); + + return result.hits.hits; +} + +interface FileIdsByIndex { + [index: string]: Set; +} + +/** + * Returns subset of fileIds that don't have any file chunks + * + * @param esClient + * @param abortController + * @param files + */ +export async function fileIdsWithoutChunksByIndex( + esClient: ElasticsearchClient, + abortController: AbortController, + files: SearchHit[] +): Promise<{ fileIdsByIndex: FileIdsByIndex; allFileIds: Set }> { + const allFileIds: Set = new Set(); + const noChunkFileIdsByIndex = files.reduce((acc, file) => { + allFileIds.add(file._id); + + const fileIds = acc[file._index]; + acc[file._index] = fileIds ? fileIds.add(file._id) : new Set([file._id]); + return acc; + }, {} as FileIdsByIndex); + + const chunks = await esClient.search<{ bid: string }>( + { + index: FILE_STORAGE_DATA_INDEX, + body: { + size: ES_SEARCH_LIMIT, + query: { + bool: { + must: [ + { + terms: { + 'bid.keyword': Array.from(allFileIds), + }, + }, + { + term: { + last: true, + }, + }, + ], + }, + }, + _source: ['bid'], + }, + }, + { signal: abortController.signal } + ); + + chunks.hits.hits.forEach((hit) => { + const fileId = hit._source?.bid; + if (!fileId) return; + const integration = hit._index.split('-')[1]; + const metadataIndex = `.fleet-${integration}-files`; + if (noChunkFileIdsByIndex[metadataIndex]?.delete(fileId)) { + allFileIds.delete(fileId); + } + }); + + return { fileIdsByIndex: noChunkFileIdsByIndex, allFileIds }; +} + +/** + * Updates given files to provided status + * + * @param esClient + * @param abortController + * @param fileIdsByIndex + * @param status + */ +export function updateFilesStatus( + esClient: ElasticsearchClient, + abortController: AbortController, + fileIdsByIndex: FileIdsByIndex, + status: FILE_STATUS +): Promise { + return Promise.all( + Object.entries(fileIdsByIndex).map(([index, fileIds]) => { + return esClient.updateByQuery( + { + index, + refresh: true, + query: { + ids: { + values: Array.from(fileIds), + }, + }, + script: { + source: `ctx._source.file.Status = '${status}'`, + lang: 'painless', + }, + }, + { signal: abortController.signal } + ); + }) + ); +} diff --git a/x-pack/plugins/fleet/server/tasks/check_deleted_files_task.test.ts b/x-pack/plugins/fleet/server/tasks/check_deleted_files_task.test.ts new file mode 100644 index 00000000000000..899c2a85b8e6d7 --- /dev/null +++ b/x-pack/plugins/fleet/server/tasks/check_deleted_files_task.test.ts @@ -0,0 +1,176 @@ +/* + * 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 { coreMock } from '@kbn/core/server/mocks'; +import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; +import type { TaskManagerSetupContract } from '@kbn/task-manager-plugin/server'; +import { TaskStatus } from '@kbn/task-manager-plugin/server'; +import type { CoreSetup } from '@kbn/core/server'; +import type { ElasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; + +import { createAppContextStartContractMock } from '../mocks'; +import { FILE_STORAGE_DATA_INDEX, FILE_STORAGE_METADATA_INDEX } from '../constants/fleet_es_assets'; +import { appContextService } from '../services'; + +import { CheckDeletedFilesTask, TYPE, VERSION } from './check_deleted_files_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, +}; + +describe('check deleted files task', () => { + const { createSetup: coreSetupMock } = coreMock; + const { createSetup: tmSetupMock, createStart: tmStartMock } = taskManagerMock; + + let mockContract: ReturnType; + let mockTask: CheckDeletedFilesTask; + let mockCore: CoreSetup; + let mockTaskManagerSetup: jest.Mocked; + beforeEach(() => { + mockContract = createAppContextStartContractMock(); + appContextService.start(mockContract); + mockCore = coreSetupMock(); + mockTaskManagerSetup = tmSetupMock(); + mockTask = new CheckDeletedFilesTask({ + core: mockCore, + taskManager: mockTaskManagerSetup, + logFactory: loggingSystemMock.create(), + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('task lifecycle', () => { + it('should create task', () => { + expect(mockTask).toBeInstanceOf(CheckDeletedFilesTask); + }); + + 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; + const abortController = new AbortController(); + + beforeEach(async () => { + const [{ elasticsearch }] = await mockCore.getStartServices(); + esClient = elasticsearch.client.asInternalUser as ElasticsearchClientMock; + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + 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(); + }; + + it('should attempt to update deleted files', async () => { + // mock getReadyFiles search + esClient.search + .mockResolvedValueOnce({ + took: 5, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + total: { + value: 1, + relation: 'eq', + }, + hits: [ + { + _id: 'metadata-testid1', + _index: FILE_STORAGE_METADATA_INDEX, + _source: { file: { status: 'READY' } }, + }, + { + _id: 'metadata-testid2', + _index: FILE_STORAGE_METADATA_INDEX, + _source: { file: { status: 'READY' } }, + }, + ], + }, + }) + // mock doFilesHaveChunks search + .mockResolvedValueOnce({ + took: 5, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + total: { + value: 0, + relation: 'eq', + }, + hits: [ + { + _id: 'data-testid1', + _index: FILE_STORAGE_DATA_INDEX, + _source: { + bid: 'metadata-testid1', + }, + }, + ], + }, + }); + + await runTask(); + + expect(esClient.updateByQuery).toHaveBeenCalledWith( + { + index: FILE_STORAGE_METADATA_INDEX, + query: { + ids: { + values: ['metadata-testid2'], + }, + }, + refresh: true, + script: { + lang: 'painless', + source: "ctx._source.file.Status = 'DELETED'", + }, + }, + { signal: abortController.signal } + ); + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/tasks/check_deleted_files_task.ts b/x-pack/plugins/fleet/server/tasks/check_deleted_files_task.ts new file mode 100644 index 00000000000000..d7bafe07fb5317 --- /dev/null +++ b/x-pack/plugins/fleet/server/tasks/check_deleted_files_task.ts @@ -0,0 +1,141 @@ +/* + * 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 type { CoreSetup, Logger } from '@kbn/core/server'; +import type { + ConcreteTaskInstance, + TaskManagerSetupContract, + TaskManagerStartContract, +} from '@kbn/task-manager-plugin/server'; +import { throwUnrecoverableError } from '@kbn/task-manager-plugin/server'; +import type { LoggerFactory } from '@kbn/core/server'; +import { errors } from '@elastic/elasticsearch'; + +import { + fileIdsWithoutChunksByIndex, + getFilesByStatus, + updateFilesStatus, +} from '../services/files'; + +export const TYPE = 'fleet:check-deleted-files-task'; +export const VERSION = '1.0.0'; +const TITLE = 'Fleet Deleted Files Periodic Tasks'; +const TIMEOUT = '2m'; +const SCOPE = ['fleet']; +const INTERVAL = '1d'; + +interface CheckDeletedFilesTaskSetupContract { + core: CoreSetup; + taskManager: TaskManagerSetupContract; + logFactory: LoggerFactory; +} + +interface CheckDeletedFilesTaskStartContract { + taskManager: TaskManagerStartContract; +} + +export class CheckDeletedFilesTask { + private logger: Logger; + private wasStarted: boolean = false; + private abortController = new AbortController(); + + constructor(setupContract: CheckDeletedFilesTaskSetupContract) { + const { core, taskManager, logFactory } = setupContract; + this.logger = logFactory.get(this.taskId); + + taskManager.registerTaskDefinitions({ + [TYPE]: { + title: TITLE, + timeout: TIMEOUT, + createTaskRunner: ({ taskInstance }: { taskInstance: ConcreteTaskInstance }) => { + return { + run: async () => { + return this.runTask(taskInstance, core); + }, + cancel: async () => { + this.abortController.abort('task timed out'); + }, + }; + }, + }, + }); + } + + public start = async ({ taskManager }: CheckDeletedFilesTaskStartContract) => { + if (!taskManager) { + this.logger.error('missing required service during start'); + return; + } + + this.wasStarted = true; + + try { + await taskManager.ensureScheduled({ + id: this.taskId, + taskType: TYPE, + scope: SCOPE, + schedule: { + interval: INTERVAL, + }, + state: {}, + params: { version: VERSION }, + }); + } catch (e) { + this.logger.error(`Error scheduling task, received error: ${e}`); + } + }; + + private get taskId(): string { + return `${TYPE}:${VERSION}`; + } + + private runTask = async (taskInstance: ConcreteTaskInstance, core: CoreSetup) => { + if (!this.wasStarted) { + this.logger.debug('[runTask()] Aborted. Task not started yet'); + return; + } + + // Check that this task is current + if (taskInstance.id !== this.taskId) { + throwUnrecoverableError(new Error('Outdated task version')); + } + + const [{ elasticsearch }] = await core.getStartServices(); + const esClient = elasticsearch.client.asInternalUser; + + try { + const readyFiles = await getFilesByStatus(esClient, this.abortController); + if (!readyFiles.length) return; + + const { fileIdsByIndex: deletedFileIdsByIndex, allFileIds: allDeletedFileIds } = + await fileIdsWithoutChunksByIndex(esClient, this.abortController, readyFiles); + if (!allDeletedFileIds.size) return; + + this.logger.info(`Attempting to update ${allDeletedFileIds.size} files to DELETED status`); + this.logger.debug(`Attempting to file ids: ${deletedFileIdsByIndex}`); + const updatedFilesResponses = await updateFilesStatus( + esClient, + this.abortController, + deletedFileIdsByIndex, + 'DELETED' + ); + const failures = updatedFilesResponses.flatMap( + (updatedFilesResponse) => updatedFilesResponse.failures + ); + if (failures?.length) { + this.logger.warn(`Failed to update ${failures.length} files to DELETED status`); + this.logger.debug(`Failed to update files to DELETED status: ${failures}`); + } + } catch (err) { + if (err instanceof errors.RequestAbortedError) { + this.logger.warn(`request aborted due to timeout: ${err}`); + return; + } + this.logger.error(err); + } + }; +} diff --git a/x-pack/plugins/fleet/server/types/files/index.ts b/x-pack/plugins/fleet/server/types/files/index.ts new file mode 100644 index 00000000000000..aa3c7b7be18b25 --- /dev/null +++ b/x-pack/plugins/fleet/server/types/files/index.ts @@ -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 type FILE_STATUS = 'AWAITING_UPLOAD' | 'UPLOADING' | 'READY' | 'UPLOAD_ERROR' | 'DELETED'; diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts index 43319d48ec0bd9..518d36bbdc9a5e 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts @@ -112,6 +112,7 @@ export default function ({ getService }: FtrProviderContext) { 'dashboard_telemetry', 'endpoint:metadata-check-transforms-task', 'endpoint:user-artifact-packager', + 'fleet:check-deleted-files-task', 'fleet:reassign_action:retry', 'fleet:unenroll_action:retry', 'fleet:update_agent_tags:retry', diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/health_route.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/health_route.ts index 1bd158019c6f4f..6a2cfa6c71323a 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/health_route.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/health_route.ts @@ -145,7 +145,8 @@ export default function ({ getService }: FtrProviderContext) { }); }); - it('should return the task manager workload', async () => { + // https://github.com/elastic/kibana/issues/144558 + it.skip('should return the task manager workload', async () => { const health = await getHealth(); const { status,