From bdc0100c27b7c3b8d683cee2bab6f24f85711fa3 Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Wed, 19 Mar 2025 09:59:53 +0100 Subject: [PATCH 01/26] [Fleet] Create API to report status of integrations synchronization --- .../shared/fleet/common/constants/routes.ts | 4 ++ .../shared/fleet/common/types/models/index.ts | 1 + .../models/remote_synced_integrations.ts | 11 ++++ .../fleet/common/types/rest_spec/index.ts | 1 + .../rest_spec/remote_synced_integrations.ts | 11 ++++ .../shared/fleet/server/routes/index.ts | 2 + .../remote_synced_integrations/handler.ts | 44 ++++++++++++++ .../remote_synced_integrations/index.ts | 58 +++++++++++++++++++ .../tasks/sync_integrations_on_remote.ts | 39 +++++++++++-- .../shared/fleet/server/types/models/index.ts | 1 + .../types/models/synced_integrations.ts | 18 ++++++ .../fleet/server/types/rest_spec/index.ts | 1 + .../rest_spec/remote_synced_integrations.ts | 13 +++++ 13 files changed, 198 insertions(+), 6 deletions(-) create mode 100644 x-pack/platform/plugins/shared/fleet/common/types/models/remote_synced_integrations.ts create mode 100644 x-pack/platform/plugins/shared/fleet/common/types/rest_spec/remote_synced_integrations.ts create mode 100644 x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/handler.ts create mode 100644 x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/index.ts create mode 100644 x-pack/platform/plugins/shared/fleet/server/types/models/synced_integrations.ts create mode 100644 x-pack/platform/plugins/shared/fleet/server/types/rest_spec/remote_synced_integrations.ts diff --git a/x-pack/platform/plugins/shared/fleet/common/constants/routes.ts b/x-pack/platform/plugins/shared/fleet/common/constants/routes.ts index 9d9416944b808..1dc54d2d39825 100644 --- a/x-pack/platform/plugins/shared/fleet/common/constants/routes.ts +++ b/x-pack/platform/plugins/shared/fleet/common/constants/routes.ts @@ -208,6 +208,10 @@ export const DOWNLOAD_SOURCE_API_ROUTES = { DELETE_PATTERN: `${API_ROOT}/agent_download_sources/{sourceId}`, }; +export const REMOTE_SYNCED_INTEGRATIONS_API_ROUTES = { + INFO_PATTERN: `${API_ROOT}/remote_synced_integrations/{outputId}`, +}; + export const CREATE_STANDALONE_AGENT_API_KEY_ROUTE = `${INTERNAL_ROOT}/create_standalone_agent_api_key`; // Fleet debug routes diff --git a/x-pack/platform/plugins/shared/fleet/common/types/models/index.ts b/x-pack/platform/plugins/shared/fleet/common/types/models/index.ts index a873c2f4bedde..507ff094c01fb 100644 --- a/x-pack/platform/plugins/shared/fleet/common/types/models/index.ts +++ b/x-pack/platform/plugins/shared/fleet/common/types/models/index.ts @@ -21,3 +21,4 @@ export * from './fleet_proxy'; export * from './secret'; export * from './setup_technology'; export * from './fleet_setup_lock'; +export * from './remote_synced_integrations'; diff --git a/x-pack/platform/plugins/shared/fleet/common/types/models/remote_synced_integrations.ts b/x-pack/platform/plugins/shared/fleet/common/types/models/remote_synced_integrations.ts new file mode 100644 index 0000000000000..df57736ad4d7b --- /dev/null +++ b/x-pack/platform/plugins/shared/fleet/common/types/models/remote_synced_integrations.ts @@ -0,0 +1,11 @@ +/* + * 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 interface RemoteSyncedIntegrationsBase { + id?: string; + name?: string; + version?: string; +} diff --git a/x-pack/platform/plugins/shared/fleet/common/types/rest_spec/index.ts b/x-pack/platform/plugins/shared/fleet/common/types/rest_spec/index.ts index 7aeaad859803b..229ef7878cce6 100644 --- a/x-pack/platform/plugins/shared/fleet/common/types/rest_spec/index.ts +++ b/x-pack/platform/plugins/shared/fleet/common/types/rest_spec/index.ts @@ -21,3 +21,4 @@ export * from './settings'; export * from './health_check'; export * from './fleet_server_hosts'; export * from './standalone_agent_api_key'; +export * from './remote_synced_integrations'; diff --git a/x-pack/platform/plugins/shared/fleet/common/types/rest_spec/remote_synced_integrations.ts b/x-pack/platform/plugins/shared/fleet/common/types/rest_spec/remote_synced_integrations.ts new file mode 100644 index 0000000000000..7535a17bc312a --- /dev/null +++ b/x-pack/platform/plugins/shared/fleet/common/types/rest_spec/remote_synced_integrations.ts @@ -0,0 +1,11 @@ +/* + * 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 { RemoteSyncedIntegrationsBase } from '../models'; + +export interface GetOneRemoteSyncedIntegrationsResponse { + item: RemoteSyncedIntegrationsBase; +} diff --git a/x-pack/platform/plugins/shared/fleet/server/routes/index.ts b/x-pack/platform/plugins/shared/fleet/server/routes/index.ts index 6c464727bf21f..ef41a9e1ae8ee 100644 --- a/x-pack/platform/plugins/shared/fleet/server/routes/index.ts +++ b/x-pack/platform/plugins/shared/fleet/server/routes/index.ts @@ -28,6 +28,7 @@ import { registerRoutes as registerMessageSigningServiceRoutes } from './message import { registerRoutes as registerUninstallTokenRoutes } from './uninstall_token'; import { registerRoutes as registerStandaloneAgentApiKeyRoutes } from './standalone_agent_api_key'; import { registerRoutes as registerDebugRoutes } from './debug'; +import { registerRoutes as registerRemoteSyncedIntegrations } from './remote_synced_integrations'; export function registerRoutes(fleetAuthzRouter: FleetAuthzRouter, config: FleetConfigType) { // Always register app routes for permissions checking @@ -50,6 +51,7 @@ export function registerRoutes(fleetAuthzRouter: FleetAuthzRouter, config: Fleet registerMessageSigningServiceRoutes(fleetAuthzRouter); registerUninstallTokenRoutes(fleetAuthzRouter, config); registerStandaloneAgentApiKeyRoutes(fleetAuthzRouter); + registerRemoteSyncedIntegrations(fleetAuthzRouter); registerDebugRoutes(fleetAuthzRouter); // Conditional config routes diff --git a/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/handler.ts b/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/handler.ts new file mode 100644 index 0000000000000..7585f00df82f9 --- /dev/null +++ b/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/handler.ts @@ -0,0 +1,44 @@ +/* + * 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 { RequestHandler } from '@kbn/core/server'; +import type { TypeOf } from '@kbn/config-schema'; + +import type { getOneRemoteSyncedIntegrationsRequestSchema } from '../../types'; + +import type { GetOneRemoteSyncedIntegrationsResponse } from '../../../common/types'; +import { getFollowerIndexStats } from '../../tasks/sync_integrations_on_remote'; +import { appContextService } from '../../services'; + +export const getRemoteSyncedIntegrationsInfoHandler: RequestHandler< + TypeOf +> = async (context, request, response) => { + const esClient = (await context.core).elasticsearch.client.asInternalUser; + const { enableSyncIntegrationsOnRemote } = appContextService.getExperimentalFeatures(); + + if (!enableSyncIntegrationsOnRemote) { + return; + } + try { + const stats = await getFollowerIndexStats(esClient, request.params.outputId); + console.log('## stats', stats); + + const body: GetOneRemoteSyncedIntegrationsResponse = { + item: {}, + }; + + return response.ok({ body }); + } catch (error) { + // if (error.isBoom && error.output.statusCode === 404) { + // return response.notFound({ + // body: { message: `Synced integrations with outputId ${request.params.outputId} not found` }, + // }); + // } + + throw error; + } +}; diff --git a/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/index.ts b/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/index.ts new file mode 100644 index 0000000000000..14bdc6fe7e7d8 --- /dev/null +++ b/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/index.ts @@ -0,0 +1,58 @@ +/* + * 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 { FleetAuthzRouter } from '../../services/security'; + +import { API_VERSIONS } from '../../../common/constants'; +import { REMOTE_SYNCED_INTEGRATIONS_API_ROUTES } from '../../../common/constants'; +import { FLEET_API_PRIVILEGES } from '../../constants/api_privileges'; +import { genericErrorResponse } from '../schema/errors'; +import { ListResponseSchema } from '../schema/utils'; +import { getOneRemoteSyncedIntegrationsRequestSchema } from '../../types'; +import { getOneRemoteSyncedIntegrationsResponseSchema } from '../../types/models/synced_integrations'; + +import { getRemoteSyncedIntegrationsInfoHandler } from './handler'; + +export const registerRoutes = (router: FleetAuthzRouter) => { + router.versioned + .get({ + path: REMOTE_SYNCED_INTEGRATIONS_API_ROUTES.INFO_PATTERN, + security: { + authz: { + requiredPrivileges: [ + { + anyRequired: [ + FLEET_API_PRIVILEGES.SETTINGS.READ, + FLEET_API_PRIVILEGES.INTEGRATIONS.READ, + ], + }, + ], + }, + }, + summary: `Get CCR Remote synced integrations info`, + options: { + tags: ['oas-tag:CCR Remote synced integrations'], + }, + }) + .addVersion( + { + version: API_VERSIONS.public.v1, + validate: { + request: getOneRemoteSyncedIntegrationsRequestSchema, + response: { + 200: { + body: () => ListResponseSchema(getOneRemoteSyncedIntegrationsResponseSchema), + }, + 400: { + body: genericErrorResponse, + }, + }, + }, + }, + getRemoteSyncedIntegrationsInfoHandler + ); +}; diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations_on_remote.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations_on_remote.ts index d98d4f565cfd5..01031cb3a7817 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations_on_remote.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations_on_remote.ts @@ -10,9 +10,9 @@ import type { ElasticsearchClient, SavedObjectsClient, Logger } from '@kbn/core/ import semverGte from 'semver/functions/gte'; import type { PackageClient } from '../services'; -import { outputService } from '../services'; +import { appContextService, outputService } from '../services'; -import { PackageNotFoundError } from '../errors'; +import { PackageNotFoundError, FleetNotFoundError, FleetError } from '../errors'; import type { SyncIntegrationsData } from './sync_integrations_task'; @@ -21,10 +21,10 @@ const FLEET_SYNCED_INTEGRATIONS_CCR_INDEX_PREFIX = 'fleet-synced-integrations-cc const MAX_RETRY_ATTEMPTS = 5; const RETRY_BACKOFF_MINUTES = [5, 10, 20, 40, 60]; -const getSyncedIntegrationsCCRDoc = async ( +const getFollowerIndex = async ( esClient: ElasticsearchClient, abortController: AbortController -): Promise => { +): Promise => { const indices = await esClient.indices.get( { index: FLEET_SYNCED_INTEGRATIONS_CCR_INDEX_PREFIX, @@ -33,7 +33,8 @@ const getSyncedIntegrationsCCRDoc = async ( ); const indexNames = Object.keys(indices); - if (indexNames.length > 1) { + + if (indexNames?.length > 1) { throw new Error( `Not supported to sync multiple indices with prefix ${FLEET_SYNCED_INTEGRATIONS_CCR_INDEX_PREFIX}` ); @@ -42,10 +43,18 @@ const getSyncedIntegrationsCCRDoc = async ( if (indexNames.length === 0) { return undefined; } + return indexNames[0]; +}; + +const getSyncedIntegrationsCCRDoc = async ( + esClient: ElasticsearchClient, + abortController: AbortController +): Promise => { + const index = await getFollowerIndex(esClient, abortController); const response = await esClient.search( { - index: indexNames[0], + index, }, { signal: abortController.signal } ); @@ -168,3 +177,21 @@ export const syncIntegrationsOnRemote = async ( await installPackageIfNotInstalled(pkg, packageClient, logger, abortController); } }; + +export const getFollowerIndexStats = async (esClient: ElasticsearchClient, outputId: string) => { + const logger = appContextService.getLogger(); + const index = `${FLEET_SYNCED_INTEGRATIONS_CCR_INDEX_PREFIX}`.replace('*', outputId); + + try { + const res = await esClient.ccr.followStats({ + index, + }); + + return res.indices; + } catch (err) { + logger.error('error', err.message); + if (err?.body?.error?.type === 'resource_not_found_exception') + throw new FleetNotFoundError(`Index ${index} not found`); + throw new FleetError(`Error: ${err.message}`); + } +}; diff --git a/x-pack/platform/plugins/shared/fleet/server/types/models/index.ts b/x-pack/platform/plugins/shared/fleet/server/types/models/index.ts index 0a58538616f77..1123306671704 100644 --- a/x-pack/platform/plugins/shared/fleet/server/types/models/index.ts +++ b/x-pack/platform/plugins/shared/fleet/server/types/models/index.ts @@ -12,3 +12,4 @@ export * from './output'; export * from './enrollment_api_key'; export * from './preconfiguration'; export * from './download_sources'; +export * from './synced_integrations'; diff --git a/x-pack/platform/plugins/shared/fleet/server/types/models/synced_integrations.ts b/x-pack/platform/plugins/shared/fleet/server/types/models/synced_integrations.ts new file mode 100644 index 0000000000000..891ff22680bbe --- /dev/null +++ b/x-pack/platform/plugins/shared/fleet/server/types/models/synced_integrations.ts @@ -0,0 +1,18 @@ +/* + * 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 { schema } from '@kbn/config-schema'; + +const RemoteSyncedIntegrationsBaseSchema = schema.object({ + id: schema.maybe(schema.string()), + name: schema.maybe(schema.string()), + version: schema.maybe(schema.string()), +}); + +export const getOneRemoteSyncedIntegrationsResponseSchema = schema.object({ + item: RemoteSyncedIntegrationsBaseSchema, +}); diff --git a/x-pack/platform/plugins/shared/fleet/server/types/rest_spec/index.ts b/x-pack/platform/plugins/shared/fleet/server/types/rest_spec/index.ts index aad4982b8e499..82ca11b9a7338 100644 --- a/x-pack/platform/plugins/shared/fleet/server/types/rest_spec/index.ts +++ b/x-pack/platform/plugins/shared/fleet/server/types/rest_spec/index.ts @@ -23,3 +23,4 @@ export * from './tags'; export * from './health_check'; export * from './message_signing_service'; export * from './standalone_agent_api_key'; +export * from './remote_synced_integrations'; diff --git a/x-pack/platform/plugins/shared/fleet/server/types/rest_spec/remote_synced_integrations.ts b/x-pack/platform/plugins/shared/fleet/server/types/rest_spec/remote_synced_integrations.ts new file mode 100644 index 0000000000000..644751a55fa9b --- /dev/null +++ b/x-pack/platform/plugins/shared/fleet/server/types/rest_spec/remote_synced_integrations.ts @@ -0,0 +1,13 @@ +/* + * 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 { schema } from '@kbn/config-schema'; + +export const getOneRemoteSyncedIntegrationsRequestSchema = { + params: schema.object({ + outputId: schema.string(), + }), +}; From ce1de43e03b40635b5d0c28d03bb82bf4979d7a3 Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Wed, 19 Mar 2025 17:04:34 +0100 Subject: [PATCH 02/26] use follower info api --- .../shared/fleet/server/errors/index.ts | 1 + .../remote_synced_integrations/handler.ts | 11 ++-------- .../tasks/sync_integrations_on_remote.ts | 20 +++++++++++-------- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/server/errors/index.ts b/x-pack/platform/plugins/shared/fleet/server/errors/index.ts index 2db40f6d4d0f9..6400f63acbab9 100644 --- a/x-pack/platform/plugins/shared/fleet/server/errors/index.ts +++ b/x-pack/platform/plugins/shared/fleet/server/errors/index.ts @@ -134,6 +134,7 @@ export class InputNotFoundError extends FleetNotFoundError {} export class OutputNotFoundError extends FleetNotFoundError {} export class PackageNotFoundError extends FleetNotFoundError {} export class ArchiveNotFoundError extends FleetNotFoundError {} +export class IndexNotFoundError extends FleetNotFoundError {} export class PackagePolicyNotFoundError extends FleetNotFoundError<{ /** The package policy ID that was not found */ diff --git a/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/handler.ts b/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/handler.ts index 7585f00df82f9..74c421ac2ea38 100644 --- a/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/handler.ts +++ b/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/handler.ts @@ -11,7 +11,7 @@ import type { TypeOf } from '@kbn/config-schema'; import type { getOneRemoteSyncedIntegrationsRequestSchema } from '../../types'; import type { GetOneRemoteSyncedIntegrationsResponse } from '../../../common/types'; -import { getFollowerIndexStats } from '../../tasks/sync_integrations_on_remote'; +import { getFollowerIndexInfo } from '../../tasks/sync_integrations_on_remote'; import { appContextService } from '../../services'; export const getRemoteSyncedIntegrationsInfoHandler: RequestHandler< @@ -24,8 +24,7 @@ export const getRemoteSyncedIntegrationsInfoHandler: RequestHandler< return; } try { - const stats = await getFollowerIndexStats(esClient, request.params.outputId); - console.log('## stats', stats); + const info = await getFollowerIndexInfo(esClient, request.params.outputId); const body: GetOneRemoteSyncedIntegrationsResponse = { item: {}, @@ -33,12 +32,6 @@ export const getRemoteSyncedIntegrationsInfoHandler: RequestHandler< return response.ok({ body }); } catch (error) { - // if (error.isBoom && error.output.statusCode === 404) { - // return response.notFound({ - // body: { message: `Synced integrations with outputId ${request.params.outputId} not found` }, - // }); - // } - throw error; } }; diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations_on_remote.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations_on_remote.ts index 01031cb3a7817..03f9913bb0ce8 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations_on_remote.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations_on_remote.ts @@ -12,7 +12,7 @@ import semverGte from 'semver/functions/gte'; import type { PackageClient } from '../services'; import { appContextService, outputService } from '../services'; -import { PackageNotFoundError, FleetNotFoundError, FleetError } from '../errors'; +import { PackageNotFoundError, IndexNotFoundError } from '../errors'; import type { SyncIntegrationsData } from './sync_integrations_task'; @@ -178,20 +178,24 @@ export const syncIntegrationsOnRemote = async ( } }; -export const getFollowerIndexStats = async (esClient: ElasticsearchClient, outputId: string) => { +export const getFollowerIndexInfo = async (esClient: ElasticsearchClient, outputId: string) => { const logger = appContextService.getLogger(); const index = `${FLEET_SYNCED_INTEGRATIONS_CCR_INDEX_PREFIX}`.replace('*', outputId); try { - const res = await esClient.ccr.followStats({ + const res = await esClient.ccr.followInfo({ index, }); - - return res.indices; + if (!res?.follower_indices || res.follower_indices.length === 0) + throw new IndexNotFoundError(`Follower index ${index} not found`); // 404 + + // if (res.follower_indices[0]?.status === 'paused') { + // throw new IndexNotFoundError(`Follower index ${index} paused`); // error code ? + // } + // indices comparison + return res.follower_indices; } catch (err) { logger.error('error', err.message); - if (err?.body?.error?.type === 'resource_not_found_exception') - throw new FleetNotFoundError(`Index ${index} not found`); - throw new FleetError(`Error: ${err.message}`); + throw err; } }; From d7fbe560fa8b50fd8bb8904885ca21c080f6214e Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Thu, 27 Mar 2025 12:48:01 +0100 Subject: [PATCH 03/26] Get working endpoint --- .../shared/fleet/common/constants/routes.ts | 2 +- .../models/remote_synced_integrations.ts | 9 +- .../rest_spec/remote_synced_integrations.ts | 6 +- .../remote_synced_integrations/handler.ts | 34 ++--- .../remote_synced_integrations/index.ts | 15 +- .../setup/fleet_synced_integrations.ts | 1 + .../sync_integrations_on_remote.ts | 140 ++++++++++++++++-- .../types/models/synced_integrations.ts | 7 +- .../rest_spec/remote_synced_integrations.ts | 2 +- 9 files changed, 169 insertions(+), 47 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/common/constants/routes.ts b/x-pack/platform/plugins/shared/fleet/common/constants/routes.ts index 1dc54d2d39825..1898645d59ab9 100644 --- a/x-pack/platform/plugins/shared/fleet/common/constants/routes.ts +++ b/x-pack/platform/plugins/shared/fleet/common/constants/routes.ts @@ -209,7 +209,7 @@ export const DOWNLOAD_SOURCE_API_ROUTES = { }; export const REMOTE_SYNCED_INTEGRATIONS_API_ROUTES = { - INFO_PATTERN: `${API_ROOT}/remote_synced_integrations/{outputId}`, + INFO_PATTERN: `${API_ROOT}/remote_synced_integrations/{outputId}/status`, }; export const CREATE_STANDALONE_AGENT_API_KEY_ROUTE = `${INTERNAL_ROOT}/create_standalone_agent_api_key`; diff --git a/x-pack/platform/plugins/shared/fleet/common/types/models/remote_synced_integrations.ts b/x-pack/platform/plugins/shared/fleet/common/types/models/remote_synced_integrations.ts index df57736ad4d7b..3cb8368bc1ae4 100644 --- a/x-pack/platform/plugins/shared/fleet/common/types/models/remote_synced_integrations.ts +++ b/x-pack/platform/plugins/shared/fleet/common/types/models/remote_synced_integrations.ts @@ -6,6 +6,11 @@ */ export interface RemoteSyncedIntegrationsBase { id?: string; - name?: string; - version?: string; + package_name: string; + package_version: string; +} +export interface RemoteSyncedIntegrationsStatus extends RemoteSyncedIntegrationsBase { + sync_status: boolean; + error?: string; + updated_at?: string; } diff --git a/x-pack/platform/plugins/shared/fleet/common/types/rest_spec/remote_synced_integrations.ts b/x-pack/platform/plugins/shared/fleet/common/types/rest_spec/remote_synced_integrations.ts index 7535a17bc312a..6621b26968da9 100644 --- a/x-pack/platform/plugins/shared/fleet/common/types/rest_spec/remote_synced_integrations.ts +++ b/x-pack/platform/plugins/shared/fleet/common/types/rest_spec/remote_synced_integrations.ts @@ -4,8 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { RemoteSyncedIntegrationsBase } from '../models'; +import type { RemoteSyncedIntegrationsStatus } from '../models'; -export interface GetOneRemoteSyncedIntegrationsResponse { - item: RemoteSyncedIntegrationsBase; +export interface GetRemoteSyncedIntegrationsStatusResponse { + items: RemoteSyncedIntegrationsStatus[]; } diff --git a/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/handler.ts b/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/handler.ts index 74c421ac2ea38..677c7be288c87 100644 --- a/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/handler.ts +++ b/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/handler.ts @@ -5,31 +5,31 @@ * 2.0. */ -import type { RequestHandler } from '@kbn/core/server'; import type { TypeOf } from '@kbn/config-schema'; +import type { RequestHandler } from '@kbn/core/server'; + +import type { GetRemoteSyncedIntegrationsStatusRequestSchema } from '../../types'; -import type { getOneRemoteSyncedIntegrationsRequestSchema } from '../../types'; +import type { GetRemoteSyncedIntegrationsStatusResponse } from '../../../common/types'; -import type { GetOneRemoteSyncedIntegrationsResponse } from '../../../common/types'; -import { getFollowerIndexInfo } from '../../tasks/sync_integrations_on_remote'; -import { appContextService } from '../../services'; +import { getRemoteSyncedIntegrationsStatus } from '../../tasks/sync_integrations/sync_integrations_on_remote'; -export const getRemoteSyncedIntegrationsInfoHandler: RequestHandler< - TypeOf +export const getRemoteSyncedIntegrationsStatusHandler: RequestHandler< + TypeOf > = async (context, request, response) => { - const esClient = (await context.core).elasticsearch.client.asInternalUser; - const { enableSyncIntegrationsOnRemote } = appContextService.getExperimentalFeatures(); + const coreContext = await context.core; + const esClient = coreContext.elasticsearch.client.asInternalUser; + const soClient = coreContext.savedObjects.client; - if (!enableSyncIntegrationsOnRemote) { - return; - } try { - const info = await getFollowerIndexInfo(esClient, request.params.outputId); - - const body: GetOneRemoteSyncedIntegrationsResponse = { - item: {}, + const res = await getRemoteSyncedIntegrationsStatus( + esClient, + soClient, + request.params.outputId + ); + const body: GetRemoteSyncedIntegrationsStatusResponse = { + items: res, }; - return response.ok({ body }); } catch (error) { throw error; diff --git a/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/index.ts b/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/index.ts index 14bdc6fe7e7d8..c772ae1795989 100644 --- a/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/index.ts +++ b/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/index.ts @@ -11,11 +11,10 @@ import { API_VERSIONS } from '../../../common/constants'; import { REMOTE_SYNCED_INTEGRATIONS_API_ROUTES } from '../../../common/constants'; import { FLEET_API_PRIVILEGES } from '../../constants/api_privileges'; import { genericErrorResponse } from '../schema/errors'; -import { ListResponseSchema } from '../schema/utils'; -import { getOneRemoteSyncedIntegrationsRequestSchema } from '../../types'; -import { getOneRemoteSyncedIntegrationsResponseSchema } from '../../types/models/synced_integrations'; +import { GetRemoteSyncedIntegrationsStatusRequestSchema } from '../../types'; +import { GetRemoteSyncedIntegrationsStatusResponseSchema } from '../../types/models/synced_integrations'; -import { getRemoteSyncedIntegrationsInfoHandler } from './handler'; +import { getRemoteSyncedIntegrationsStatusHandler } from './handler'; export const registerRoutes = (router: FleetAuthzRouter) => { router.versioned @@ -33,7 +32,7 @@ export const registerRoutes = (router: FleetAuthzRouter) => { ], }, }, - summary: `Get CCR Remote synced integrations info`, + summary: `Get CCR Remote synced integrations status`, options: { tags: ['oas-tag:CCR Remote synced integrations'], }, @@ -42,10 +41,10 @@ export const registerRoutes = (router: FleetAuthzRouter) => { { version: API_VERSIONS.public.v1, validate: { - request: getOneRemoteSyncedIntegrationsRequestSchema, + request: GetRemoteSyncedIntegrationsStatusRequestSchema, response: { 200: { - body: () => ListResponseSchema(getOneRemoteSyncedIntegrationsResponseSchema), + body: () => GetRemoteSyncedIntegrationsStatusResponseSchema, }, 400: { body: genericErrorResponse, @@ -53,6 +52,6 @@ export const registerRoutes = (router: FleetAuthzRouter) => { }, }, }, - getRemoteSyncedIntegrationsInfoHandler + getRemoteSyncedIntegrationsStatusHandler ); }; diff --git a/x-pack/platform/plugins/shared/fleet/server/services/setup/fleet_synced_integrations.ts b/x-pack/platform/plugins/shared/fleet/server/services/setup/fleet_synced_integrations.ts index d1efc51cabef1..5d949372667da 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/setup/fleet_synced_integrations.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/setup/fleet_synced_integrations.ts @@ -11,6 +11,7 @@ import { FleetSetupError } from '../../errors'; import { appContextService } from '../app_context'; export const FLEET_SYNCED_INTEGRATIONS_INDEX_NAME = 'fleet-synced-integrations'; +export const FLEET_SYNCED_INTEGRATIONS_CCR_INDEX_PREFIX = 'fleet-synced-integrations-ccr-*'; export const FLEET_SYNCED_INTEGRATIONS_INDEX_CONFIG = { settings: { diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts index 63a7cee8d3ac0..81ace539c8351 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts @@ -5,18 +5,28 @@ * 2.0. */ -import type { ElasticsearchClient, SavedObjectsClient, Logger } from '@kbn/core/server'; +import type { + ElasticsearchClient, + SavedObjectsClient, + Logger, + SavedObjectsClientContract, + SavedObjectsFindResult, +} from '@kbn/core/server'; import semverGte from 'semver/functions/gte'; import type { PackageClient } from '../../services'; import { outputService, appContextService } from '../../services'; +import { getPackageSavedObjects } from '../../services/epm/packages/get'; import { PackageNotFoundError, IndexNotFoundError } from '../../errors'; +import { FLEET_SYNCED_INTEGRATIONS_CCR_INDEX_PREFIX } from '../../services/setup/fleet_synced_integrations'; -import type { SyncIntegrationsData } from './model'; +import type { Installation } from '../../types'; + +import type { RemoteSyncedIntegrationsStatus } from '../../../common/types'; -const FLEET_SYNCED_INTEGRATIONS_CCR_INDEX_PREFIX = 'fleet-synced-integrations-ccr-*'; +import type { SyncIntegrationsData } from './model'; const MAX_RETRY_ATTEMPTS = 5; const RETRY_BACKOFF_MINUTES = [5, 10, 20, 40, 60]; @@ -92,10 +102,12 @@ async function installPackageIfNotInstalled( installation?.install_status === 'installed' && semverGte(installation.version, pkg.package_version) ) { + logger.debug(`installPackageIfNotInstalled - ${pkg.package_name} already installed`); return; } if (installation?.install_status === 'installing') { + logger.debug(`installPackageIfNotInstalled - ${pkg.package_name} status installing`); return; } @@ -103,6 +115,9 @@ async function installPackageIfNotInstalled( const attempt = installation.latest_install_failed_attempts?.length ?? 0; if (attempt >= MAX_RETRY_ATTEMPTS) { + logger.debug( + `installPackageIfNotInstalled - too many retry attempts at installing ${pkg.package_name}` + ); return; } const lastRetryAttemptTime = installation.latest_install_failed_attempts?.[0].created_at; @@ -178,24 +193,123 @@ export const syncIntegrationsOnRemote = async ( } }; -export const getFollowerIndexInfo = async (esClient: ElasticsearchClient, outputId: string) => { - const logger = appContextService.getLogger(); - const index = `${FLEET_SYNCED_INTEGRATIONS_CCR_INDEX_PREFIX}`.replace('*', outputId); - +export const getFollowerIndexInfo = async ( + esClient: ElasticsearchClient, + index: string, + logger: Logger +) => { + if (!index) { + throw new IndexNotFoundError(`Follower index ${index} not found`); + } try { const res = await esClient.ccr.followInfo({ index, }); if (!res?.follower_indices || res.follower_indices.length === 0) - throw new IndexNotFoundError(`Follower index ${index} not found`); // 404 + throw new IndexNotFoundError(`Follower index ${index} not available`); - // if (res.follower_indices[0]?.status === 'paused') { - // throw new IndexNotFoundError(`Follower index ${index} paused`); // error code ? - // } - // indices comparison - return res.follower_indices; + if (res.follower_indices[0]?.status === 'paused') { + throw new IndexNotFoundError(`Follower index ${index} paused`); // error code ? + } + return res.follower_indices[0]; } catch (err) { + if (err?.body?.error?.type === 'index_not_found_exception') + throw new IndexNotFoundError(`Index ${index} not found`); + logger.error('error', err.message); throw err; } }; + +export const compareSyncedIntegrations = async ( + esClient: ElasticsearchClient, + savedObjectsClient: SavedObjectsClientContract, + index: string, + logger: Logger +) => { + try { + const searchRes = await esClient.search({ + index, + size: 50, // ? + sort: [ + { + 'integrations.updated_at': { + order: 'desc', + }, + }, + ], + }); + const ccrIntegrations = searchRes.hits.hits[0]?._source?.integrations; + const installedIntegrations = await getPackageSavedObjects(savedObjectsClient); + + const installedIntegrationsByName = (installedIntegrations?.saved_objects || []).reduce( + (acc, integration) => { + if (integration?.id) { + acc[integration.id] = integration; + } + return acc; + }, + {} as Record> + ); + + const integrationsStatus: RemoteSyncedIntegrationsStatus[] | undefined = ccrIntegrations?.map( + (ccrIntegration) => { + const localIntegrationSO = installedIntegrationsByName[ccrIntegration.package_name]; + if (!localIntegrationSO) { + return { + ...ccrIntegration, + sync_status: false, + error: `Installation not found`, + }; + } + if (ccrIntegration.package_version !== localIntegrationSO?.attributes.version) { + return { + ...ccrIntegration, + sync_status: false, + error: `Installed version: ${localIntegrationSO?.attributes.version}`, + }; + } + if (localIntegrationSO?.attributes.install_status !== 'installed') { + return { + ...ccrIntegration, + sync_status: false, + error: `Installation status: ${localIntegrationSO?.attributes.install_status}`, + }; + } + return { + ...ccrIntegration, + sync_status: true, + updated_at: localIntegrationSO?.updated_at, + }; + } + ); + return integrationsStatus; + } catch (err) { + logger.error('error', err.message); + throw err; + } +}; + +export const getRemoteSyncedIntegrationsStatus = async ( + esClient: ElasticsearchClient, + soClient: SavedObjectsClientContract, + outputId: string +) => { + const { enableSyncIntegrationsOnRemote } = appContextService.getExperimentalFeatures(); + const logger = appContextService.getLogger(); + + if (!enableSyncIntegrationsOnRemote) { + return []; + } + const index = `${FLEET_SYNCED_INTEGRATIONS_CCR_INDEX_PREFIX}`.replace('*', outputId); + try { + const info = await getFollowerIndexInfo(esClient, index, logger); + let res: RemoteSyncedIntegrationsStatus[] = []; + if (info?.status === 'active') { + res = (await compareSyncedIntegrations(esClient, soClient, index, logger)) ?? []; + } + return res; + } catch (err) { + throw err; + } +}; diff --git a/x-pack/platform/plugins/shared/fleet/server/types/models/synced_integrations.ts b/x-pack/platform/plugins/shared/fleet/server/types/models/synced_integrations.ts index 891ff22680bbe..fec22653ed683 100644 --- a/x-pack/platform/plugins/shared/fleet/server/types/models/synced_integrations.ts +++ b/x-pack/platform/plugins/shared/fleet/server/types/models/synced_integrations.ts @@ -13,6 +13,9 @@ const RemoteSyncedIntegrationsBaseSchema = schema.object({ version: schema.maybe(schema.string()), }); -export const getOneRemoteSyncedIntegrationsResponseSchema = schema.object({ - item: RemoteSyncedIntegrationsBaseSchema, +export const GetRemoteSyncedIntegrationsStatusResponseSchema = schema.object({ + items: RemoteSyncedIntegrationsBaseSchema.extends({ + sync_status: schema.boolean(), + error: schema.maybe(schema.string()), + }), }); diff --git a/x-pack/platform/plugins/shared/fleet/server/types/rest_spec/remote_synced_integrations.ts b/x-pack/platform/plugins/shared/fleet/server/types/rest_spec/remote_synced_integrations.ts index 644751a55fa9b..5b2e219475470 100644 --- a/x-pack/platform/plugins/shared/fleet/server/types/rest_spec/remote_synced_integrations.ts +++ b/x-pack/platform/plugins/shared/fleet/server/types/rest_spec/remote_synced_integrations.ts @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -export const getOneRemoteSyncedIntegrationsRequestSchema = { +export const GetRemoteSyncedIntegrationsStatusRequestSchema = { params: schema.object({ outputId: schema.string(), }), From 525d0d51df34481b465a654944cff03b886a76e0 Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Thu, 27 Mar 2025 16:57:49 +0100 Subject: [PATCH 04/26] Add top level error --- .../rest_spec/remote_synced_integrations.ts | 1 + .../remote_synced_integrations/handler.ts | 8 ++---- .../sync_integrations_on_remote.ts | 28 ++++++++++++++----- .../types/models/synced_integrations.ts | 16 +++++++---- 4 files changed, 35 insertions(+), 18 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/common/types/rest_spec/remote_synced_integrations.ts b/x-pack/platform/plugins/shared/fleet/common/types/rest_spec/remote_synced_integrations.ts index 6621b26968da9..e28a73c3bd041 100644 --- a/x-pack/platform/plugins/shared/fleet/common/types/rest_spec/remote_synced_integrations.ts +++ b/x-pack/platform/plugins/shared/fleet/common/types/rest_spec/remote_synced_integrations.ts @@ -8,4 +8,5 @@ import type { RemoteSyncedIntegrationsStatus } from '../models'; export interface GetRemoteSyncedIntegrationsStatusResponse { items: RemoteSyncedIntegrationsStatus[]; + error?: string; } diff --git a/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/handler.ts b/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/handler.ts index 677c7be288c87..59dd650e0b7f3 100644 --- a/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/handler.ts +++ b/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/handler.ts @@ -22,15 +22,13 @@ export const getRemoteSyncedIntegrationsStatusHandler: RequestHandler< const soClient = coreContext.savedObjects.client; try { - const res = await getRemoteSyncedIntegrationsStatus( + const res: GetRemoteSyncedIntegrationsStatusResponse = await getRemoteSyncedIntegrationsStatus( esClient, soClient, request.params.outputId ); - const body: GetRemoteSyncedIntegrationsStatusResponse = { - items: res, - }; - return response.ok({ body }); + + return response.ok({ body: res }); } catch (error) { throw error; } diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts index 8f9057a169d1f..ab0ec5325005b 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts @@ -24,7 +24,7 @@ import { FLEET_SYNCED_INTEGRATIONS_CCR_INDEX_PREFIX } from '../../services/setup import type { Installation } from '../../types'; -import type { RemoteSyncedIntegrationsStatus } from '../../../common/types'; +import type { RemoteSyncedIntegrationsStatus, GetRemoteSyncedIntegrationsStatusResponse } from '../../../common/types'; import type { SyncIntegrationsData } from './model'; import { installCustomAsset } from './custom_assets'; @@ -240,6 +240,7 @@ export const compareSyncedIntegrations = async ( logger: Logger ) => { try { + const searchRes = await esClient.search({ index, size: 50, // ? @@ -251,8 +252,20 @@ export const compareSyncedIntegrations = async ( }, ], }); + if (searchRes.hits.hits[0]?._source === undefined) { + return { + items: [], + error: `No integrations found on ${index}` + } + } const ccrIntegrations = searchRes.hits.hits[0]?._source?.integrations; const installedIntegrations = await getPackageSavedObjects(savedObjectsClient); + if (!installedIntegrations) { + return { + items: [], + error: `No integrations installed on remote` + } + } const installedIntegrationsByName = (installedIntegrations?.saved_objects || []).reduce( (acc, integration) => { @@ -295,7 +308,7 @@ export const compareSyncedIntegrations = async ( }; } ); - return integrationsStatus; + return {items: integrationsStatus}; } catch (err) { logger.error('error', err.message); throw err; @@ -306,20 +319,21 @@ export const getRemoteSyncedIntegrationsStatus = async ( esClient: ElasticsearchClient, soClient: SavedObjectsClientContract, outputId: string -) => { +): Promise => { const { enableSyncIntegrationsOnRemote } = appContextService.getExperimentalFeatures(); const logger = appContextService.getLogger(); if (!enableSyncIntegrationsOnRemote) { - return []; + return { items: [] }; } const index = `${FLEET_SYNCED_INTEGRATIONS_CCR_INDEX_PREFIX}`.replace('*', outputId); try { const info = await getFollowerIndexInfo(esClient, index, logger); - let res: RemoteSyncedIntegrationsStatus[] = []; - if (info?.status === 'active') { - res = (await compareSyncedIntegrations(esClient, soClient, index, logger)) ?? []; + + if (info?.status !== 'active') { + return { items: [] }; } + const res = await compareSyncedIntegrations(esClient, soClient, index, logger); return res; } catch (err) { throw err; diff --git a/x-pack/platform/plugins/shared/fleet/server/types/models/synced_integrations.ts b/x-pack/platform/plugins/shared/fleet/server/types/models/synced_integrations.ts index fec22653ed683..5589915be6663 100644 --- a/x-pack/platform/plugins/shared/fleet/server/types/models/synced_integrations.ts +++ b/x-pack/platform/plugins/shared/fleet/server/types/models/synced_integrations.ts @@ -9,13 +9,17 @@ import { schema } from '@kbn/config-schema'; const RemoteSyncedIntegrationsBaseSchema = schema.object({ id: schema.maybe(schema.string()), - name: schema.maybe(schema.string()), - version: schema.maybe(schema.string()), + package_name: schema.maybe(schema.string()), + package_version: schema.maybe(schema.string()), +}); + +export const RemoteSyncedIntegrationsStatusSchema = RemoteSyncedIntegrationsBaseSchema.extends({ + sync_status: schema.boolean(), + error: schema.maybe(schema.string()), + updated_at: schema.maybe(schema.string()), }); export const GetRemoteSyncedIntegrationsStatusResponseSchema = schema.object({ - items: RemoteSyncedIntegrationsBaseSchema.extends({ - sync_status: schema.boolean(), - error: schema.maybe(schema.string()), - }), + items: schema.arrayOf(RemoteSyncedIntegrationsStatusSchema), + error: schema.maybe(schema.string()), }); From 84e10d6c11f6968fa478989c8fe02ca8104a1730 Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Fri, 28 Mar 2025 10:57:02 +0100 Subject: [PATCH 05/26] Address code review comments --- .../shared/fleet/common/constants/routes.ts | 2 +- .../models/remote_synced_integrations.ts | 8 ++- .../remote_synced_integrations/handler.ts | 5 +- .../remote_synced_integrations/index.ts | 13 ++--- .../sync_integrations_on_remote.ts | 53 ++++++++++--------- .../types/models/synced_integrations.ts | 7 ++- .../fleet/server/types/rest_spec/index.ts | 1 - .../rest_spec/remote_synced_integrations.ts | 13 ----- 8 files changed, 46 insertions(+), 56 deletions(-) delete mode 100644 x-pack/platform/plugins/shared/fleet/server/types/rest_spec/remote_synced_integrations.ts diff --git a/x-pack/platform/plugins/shared/fleet/common/constants/routes.ts b/x-pack/platform/plugins/shared/fleet/common/constants/routes.ts index 1898645d59ab9..124cfe6231d41 100644 --- a/x-pack/platform/plugins/shared/fleet/common/constants/routes.ts +++ b/x-pack/platform/plugins/shared/fleet/common/constants/routes.ts @@ -209,7 +209,7 @@ export const DOWNLOAD_SOURCE_API_ROUTES = { }; export const REMOTE_SYNCED_INTEGRATIONS_API_ROUTES = { - INFO_PATTERN: `${API_ROOT}/remote_synced_integrations/{outputId}/status`, + STATUS_PATTERN: `${API_ROOT}/remote_synced_integrations/status`, }; export const CREATE_STANDALONE_AGENT_API_KEY_ROUTE = `${INTERNAL_ROOT}/create_standalone_agent_api_key`; diff --git a/x-pack/platform/plugins/shared/fleet/common/types/models/remote_synced_integrations.ts b/x-pack/platform/plugins/shared/fleet/common/types/models/remote_synced_integrations.ts index 3cb8368bc1ae4..ff55db153cdc8 100644 --- a/x-pack/platform/plugins/shared/fleet/common/types/models/remote_synced_integrations.ts +++ b/x-pack/platform/plugins/shared/fleet/common/types/models/remote_synced_integrations.ts @@ -4,13 +4,19 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +export enum SyncStatus { + SYNCHRONIZING = 'synchronizing', + COMPLETED = 'completed', + FAILED= 'failed' +} + export interface RemoteSyncedIntegrationsBase { id?: string; package_name: string; package_version: string; } export interface RemoteSyncedIntegrationsStatus extends RemoteSyncedIntegrationsBase { - sync_status: boolean; + sync_status: SyncStatus; error?: string; updated_at?: string; } diff --git a/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/handler.ts b/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/handler.ts index 59dd650e0b7f3..97707a5be2195 100644 --- a/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/handler.ts +++ b/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/handler.ts @@ -8,14 +8,12 @@ import type { TypeOf } from '@kbn/config-schema'; import type { RequestHandler } from '@kbn/core/server'; -import type { GetRemoteSyncedIntegrationsStatusRequestSchema } from '../../types'; - import type { GetRemoteSyncedIntegrationsStatusResponse } from '../../../common/types'; import { getRemoteSyncedIntegrationsStatus } from '../../tasks/sync_integrations/sync_integrations_on_remote'; export const getRemoteSyncedIntegrationsStatusHandler: RequestHandler< - TypeOf + undefined > = async (context, request, response) => { const coreContext = await context.core; const esClient = coreContext.elasticsearch.client.asInternalUser; @@ -25,7 +23,6 @@ export const getRemoteSyncedIntegrationsStatusHandler: RequestHandler< const res: GetRemoteSyncedIntegrationsStatusResponse = await getRemoteSyncedIntegrationsStatus( esClient, soClient, - request.params.outputId ); return response.ok({ body: res }); diff --git a/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/index.ts b/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/index.ts index c772ae1795989..ce55db69cd6ac 100644 --- a/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/index.ts +++ b/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/index.ts @@ -11,7 +11,6 @@ import { API_VERSIONS } from '../../../common/constants'; import { REMOTE_SYNCED_INTEGRATIONS_API_ROUTES } from '../../../common/constants'; import { FLEET_API_PRIVILEGES } from '../../constants/api_privileges'; import { genericErrorResponse } from '../schema/errors'; -import { GetRemoteSyncedIntegrationsStatusRequestSchema } from '../../types'; import { GetRemoteSyncedIntegrationsStatusResponseSchema } from '../../types/models/synced_integrations'; import { getRemoteSyncedIntegrationsStatusHandler } from './handler'; @@ -19,16 +18,12 @@ import { getRemoteSyncedIntegrationsStatusHandler } from './handler'; export const registerRoutes = (router: FleetAuthzRouter) => { router.versioned .get({ - path: REMOTE_SYNCED_INTEGRATIONS_API_ROUTES.INFO_PATTERN, + path: REMOTE_SYNCED_INTEGRATIONS_API_ROUTES.STATUS_PATTERN, security: { authz: { requiredPrivileges: [ - { - anyRequired: [ - FLEET_API_PRIVILEGES.SETTINGS.READ, - FLEET_API_PRIVILEGES.INTEGRATIONS.READ, - ], - }, + FLEET_API_PRIVILEGES.SETTINGS.READ, + FLEET_API_PRIVILEGES.INTEGRATIONS.READ, ], }, }, @@ -41,7 +36,7 @@ export const registerRoutes = (router: FleetAuthzRouter) => { { version: API_VERSIONS.public.v1, validate: { - request: GetRemoteSyncedIntegrationsStatusRequestSchema, + request: {}, response: { 200: { body: () => GetRemoteSyncedIntegrationsStatusResponseSchema, diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts index ab0ec5325005b..3c463e987b6af 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts @@ -24,10 +24,11 @@ import { FLEET_SYNCED_INTEGRATIONS_CCR_INDEX_PREFIX } from '../../services/setup import type { Installation } from '../../types'; -import type { RemoteSyncedIntegrationsStatus, GetRemoteSyncedIntegrationsStatusResponse } from '../../../common/types'; +import type { RemoteSyncedIntegrationsStatus, GetRemoteSyncedIntegrationsStatusResponse, SyncStatus } from '../../../common/types'; import type { SyncIntegrationsData } from './model'; import { installCustomAsset } from './custom_assets'; +import { CcrFollowInfoFollowerIndex } from '@elastic/elasticsearch/lib/api/types'; const MAX_RETRY_ATTEMPTS = 5; const RETRY_BACKOFF_MINUTES = [5, 10, 20, 40, 60]; @@ -45,7 +46,7 @@ const getFollowerIndex = async ( const indexNames = Object.keys(indices); - if (indexNames?.length > 1) { + if (indexNames.length > 1) { throw new Error( `Not supported to sync multiple indices with prefix ${FLEET_SYNCED_INTEGRATIONS_CCR_INDEX_PREFIX}` ); @@ -207,43 +208,42 @@ export const syncIntegrationsOnRemote = async ( export const getFollowerIndexInfo = async ( esClient: ElasticsearchClient, - index: string, logger: Logger -) => { - if (!index) { - throw new IndexNotFoundError(`Follower index ${index} not found`); - } +): Promise<{info?: CcrFollowInfoFollowerIndex, error?: string}> => { try { + const index = await getFollowerIndex(esClient, new AbortController()); + if (!index) { + return {error: `Follower index not found`}; + } const res = await esClient.ccr.followInfo({ index, }); if (!res?.follower_indices || res.follower_indices.length === 0) - throw new IndexNotFoundError(`Follower index ${index} not available`); + return {error: `Follower index ${index} not available`}; if (res.follower_indices[0]?.status === 'paused') { - throw new IndexNotFoundError(`Follower index ${index} paused`); // error code ? + return {error: `Follower index ${index} paused`}; } - return res.follower_indices[0]; + return {info: res.follower_indices[0]}; } catch (err) { if (err?.body?.error?.type === 'index_not_found_exception') - throw new IndexNotFoundError(`Index ${index} not found`); + throw new IndexNotFoundError(`Index not found`); logger.error('error', err.message); throw err; } }; -export const compareSyncedIntegrations = async ( +export const fetchAndCompareSyncedIntegrations = async ( esClient: ElasticsearchClient, savedObjectsClient: SavedObjectsClientContract, index: string, logger: Logger ) => { try { - + // find integrations on ccr index const searchRes = await esClient.search({ index, - size: 50, // ? sort: [ { 'integrations.updated_at': { @@ -259,8 +259,11 @@ export const compareSyncedIntegrations = async ( } } const ccrIntegrations = searchRes.hits.hits[0]?._source?.integrations; + + // find integrations installed on remote const installedIntegrations = await getPackageSavedObjects(savedObjectsClient); - if (!installedIntegrations) { + + if (!installedIntegrations && ccrIntegrations.length > 0) { return { items: [], error: `No integrations installed on remote` @@ -283,27 +286,27 @@ export const compareSyncedIntegrations = async ( if (!localIntegrationSO) { return { ...ccrIntegration, - sync_status: false, + sync_status: 'failed' as SyncStatus.FAILED, error: `Installation not found`, }; } if (ccrIntegration.package_version !== localIntegrationSO?.attributes.version) { return { ...ccrIntegration, - sync_status: false, + sync_status: 'failed' as SyncStatus.FAILED, error: `Installed version: ${localIntegrationSO?.attributes.version}`, }; } if (localIntegrationSO?.attributes.install_status !== 'installed') { return { ...ccrIntegration, - sync_status: false, + sync_status: 'failed' as SyncStatus.FAILED, error: `Installation status: ${localIntegrationSO?.attributes.install_status}`, }; } return { ...ccrIntegration, - sync_status: true, + sync_status: 'completed' as SyncStatus.COMPLETED, updated_at: localIntegrationSO?.updated_at, }; } @@ -318,7 +321,6 @@ export const compareSyncedIntegrations = async ( export const getRemoteSyncedIntegrationsStatus = async ( esClient: ElasticsearchClient, soClient: SavedObjectsClientContract, - outputId: string ): Promise => { const { enableSyncIntegrationsOnRemote } = appContextService.getExperimentalFeatures(); const logger = appContextService.getLogger(); @@ -326,14 +328,13 @@ export const getRemoteSyncedIntegrationsStatus = async ( if (!enableSyncIntegrationsOnRemote) { return { items: [] }; } - const index = `${FLEET_SYNCED_INTEGRATIONS_CCR_INDEX_PREFIX}`.replace('*', outputId); - try { - const info = await getFollowerIndexInfo(esClient, index, logger); - if (info?.status !== 'active') { - return { items: [] }; + try { + const followerIndexRes = await getFollowerIndexInfo(esClient, logger); + if (followerIndexRes?.error || !followerIndexRes?.info) { + return { error: followerIndexRes?.error, items: [] }; } - const res = await compareSyncedIntegrations(esClient, soClient, index, logger); + const res = await fetchAndCompareSyncedIntegrations(esClient, soClient, followerIndexRes.info.follower_index, logger); return res; } catch (err) { throw err; diff --git a/x-pack/platform/plugins/shared/fleet/server/types/models/synced_integrations.ts b/x-pack/platform/plugins/shared/fleet/server/types/models/synced_integrations.ts index 5589915be6663..4137dd9402365 100644 --- a/x-pack/platform/plugins/shared/fleet/server/types/models/synced_integrations.ts +++ b/x-pack/platform/plugins/shared/fleet/server/types/models/synced_integrations.ts @@ -6,6 +6,7 @@ */ import { schema } from '@kbn/config-schema'; +import { SyncStatus } from '../../../common/types'; const RemoteSyncedIntegrationsBaseSchema = schema.object({ id: schema.maybe(schema.string()), @@ -14,7 +15,11 @@ const RemoteSyncedIntegrationsBaseSchema = schema.object({ }); export const RemoteSyncedIntegrationsStatusSchema = RemoteSyncedIntegrationsBaseSchema.extends({ - sync_status: schema.boolean(), + sync_status: schema.oneOf([ + schema.literal(SyncStatus.COMPLETED), + schema.literal(SyncStatus.SYNCHRONIZING), + schema.literal(SyncStatus.FAILED), + ]), error: schema.maybe(schema.string()), updated_at: schema.maybe(schema.string()), }); diff --git a/x-pack/platform/plugins/shared/fleet/server/types/rest_spec/index.ts b/x-pack/platform/plugins/shared/fleet/server/types/rest_spec/index.ts index 82ca11b9a7338..aad4982b8e499 100644 --- a/x-pack/platform/plugins/shared/fleet/server/types/rest_spec/index.ts +++ b/x-pack/platform/plugins/shared/fleet/server/types/rest_spec/index.ts @@ -23,4 +23,3 @@ export * from './tags'; export * from './health_check'; export * from './message_signing_service'; export * from './standalone_agent_api_key'; -export * from './remote_synced_integrations'; diff --git a/x-pack/platform/plugins/shared/fleet/server/types/rest_spec/remote_synced_integrations.ts b/x-pack/platform/plugins/shared/fleet/server/types/rest_spec/remote_synced_integrations.ts deleted file mode 100644 index 5b2e219475470..0000000000000 --- a/x-pack/platform/plugins/shared/fleet/server/types/rest_spec/remote_synced_integrations.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * 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 { schema } from '@kbn/config-schema'; - -export const GetRemoteSyncedIntegrationsStatusRequestSchema = { - params: schema.object({ - outputId: schema.string(), - }), -}; From 796b1017095e9bc0f9efb9ab5d4cd694b44f8cba Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Mon, 31 Mar 2025 12:12:23 +0200 Subject: [PATCH 06/26] Update dev docs --- .../local_setup/remote_clusters_ccr.md | 95 ++++++++++++++----- 1 file changed, 72 insertions(+), 23 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/dev_docs/local_setup/remote_clusters_ccr.md b/x-pack/platform/plugins/shared/fleet/dev_docs/local_setup/remote_clusters_ccr.md index c115c7c878a86..491d3306468d6 100644 --- a/x-pack/platform/plugins/shared/fleet/dev_docs/local_setup/remote_clusters_ccr.md +++ b/x-pack/platform/plugins/shared/fleet/dev_docs/local_setup/remote_clusters_ccr.md @@ -1,7 +1,15 @@ -# Test sync integrations feature +# Test sync integrations feature -## Start Cluster 1 (remote) +## Changes to `kibana.dev.yml` + +- Remove `elasticsearch.hosts` +- Enable feature flag +``` + xpack.fleet.enableExperimental: ['enableSyncIntegrationsOnRemote'] +``` + +## Start Cluster 1 (remote) - Start ES 1 ``` @@ -13,6 +21,7 @@ yarn es snapshot -E http.port=9500 -E transport.port=9600 -E path.data=../remote ``` yarn start --server.port=5701 --elasticsearch.hosts=http://localhost:9500 --dev.basePathProxyTarget=5703 ``` +- Login into http://localhost/5601/ ## Start Cluster 2 (main) @@ -28,20 +37,71 @@ yarn es snapshot --license trial -E path.data=/tmp/es-data -E http.host=0.0.0.0 yarn start ``` -## Enable feature flag +- Login into http://localhost/5701/ -- Add to `kibana.dev.yml` +To avoid issues, it might be needed to login to one of the kibana instances with an incognito session. +## Setup on Cluster 1 + +- Navigate to dev tools and create service token and api key ``` -xpack.fleet.enableExperimental: ['enableSyncIntegrationsOnRemote'] +POST kbn:/api/fleet/service_tokens +{ + "remote": true +} + +POST /_security/api_key + { + "name": "integration_sync_api_key", + "role_descriptors": { + "integration_writer": { + "cluster": [], + "indices":[], + "applications": [{ + "application": "kibana-.kibana", + "privileges": ["feature_fleet.read", "feature_fleetv2.read"], + "resources": ["*"] + }] + } + } + } ``` +Save the responses as they will be required in Cluster 2 (see next section). -## Add remote ES output -- Add remote ES output to Cluster 2 +### Add Remote Cluster + +- Add Remote Cluster to Cluster 1. Navigate to *Stack Management > Remote Clusters* ([link](http://localhost:5601/app/management/data/remote_clusters)) and follow the steps to add cluster 'local': + +- Click "Add a remote cluster" +- Choose a name, put `localhost:9300` for "Seed nodes", and save (check "Yes, I have setup trust") +- Make sure the connection status is "Connected" + +### Set up CCR + +Please note that [CCR](https://www.elastic.co/guide/en/elasticsearch/reference/7.17/ccr-getting-started-tutorial.html) requires both clusters to have the same license. At the time of writing an `enterprise` license is needed. +On cluster 1, navigate to *Stack Management > Cross-Cluster Replication* and create a follower index using the cluster from Step 1. + + - Leader index `fleet-synced-integrations` + - Follower index `fleet-synced-integrations-ccr-remote1` + +### Set up local ES output +This configuration is required to kick off the integration sync. The local host needs to match the remote ES output configured on Cluster 2 (see next section) + +``` +xpack.fleet.outputs: + - name: 'Preconfigured ES output' + type: 'elasticsearch' + id: 'local-output' + hosts: ["http://:9500"] +``` + +## Setup on Cluster 2 +### Add remote ES output +- Add remote ES output to Cluster 2. Insert `token` and `key` obtained from the previous commands. This example is adding a preconfigured output but it can also be added from the UI. ``` xpack.fleet.outputs: - - name: 'Preconfiged remote output' + - name: 'Preconfigured remote output' type: 'remote_elasticsearch' id: 'remote-output' hosts: ["http://:9500"] @@ -52,22 +112,11 @@ xpack.fleet.outputs: kibana_api_key: key ``` -## Add Remote Cluster - -- Add Remote Cluster to Cluster 1 - -http://localhost:5701/app/management/data/remote_clusters - -Add Remote cluster with remote address `localhost:9300` - -## Set up CCR - -- Add follower index to Cluster 1 - - Leader index `fleet-synced-integrations` - - Follower index `fleet-synced-integrations-ccr-remote1` +## Verify sync +With these configuration, the sync task should run every 5 minutes and the integrations installed on Cluster 1 should be the same as the ones on Cluster 2 -## Check contents of sync integrations indices +### Check contents of sync integrations indices - In Cluster 1 ``` @@ -79,7 +128,7 @@ GET fleet-synced-integrations-ccr-remote1/_search GET fleet-synced-integrations/_search ``` -## Read data from remote with CCS +### Read data from remote with CCS - Enroll Fleet Server to Cluster 2 From b438a719ee34d0dfb19a7a6cbbafc616936581bb Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Tue, 1 Apr 2025 11:07:49 +0200 Subject: [PATCH 07/26] Add comparison for custom assets --- .../models/remote_synced_integrations.ts | 12 +- .../rest_spec/remote_synced_integrations.ts | 5 +- .../local_setup/remote_clusters_ccr.md | 76 ++++--- .../remote_synced_integrations/handler.ts | 11 +- .../tasks/sync_integrations/custom_assets.ts | 20 +- .../server/tasks/sync_integrations/model.ts | 1 - .../sync_integrations_on_remote.ts | 187 +++++++++++++----- .../types/models/synced_integrations.ts | 22 ++- 8 files changed, 239 insertions(+), 95 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/common/types/models/remote_synced_integrations.ts b/x-pack/platform/plugins/shared/fleet/common/types/models/remote_synced_integrations.ts index ff55db153cdc8..97eedef3e1aac 100644 --- a/x-pack/platform/plugins/shared/fleet/common/types/models/remote_synced_integrations.ts +++ b/x-pack/platform/plugins/shared/fleet/common/types/models/remote_synced_integrations.ts @@ -4,10 +4,12 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import type { CustomAssetsData } from '../../../server/tasks/sync_integrations/model'; + export enum SyncStatus { SYNCHRONIZING = 'synchronizing', COMPLETED = 'completed', - FAILED= 'failed' + FAILED = 'failed', } export interface RemoteSyncedIntegrationsBase { @@ -20,3 +22,11 @@ export interface RemoteSyncedIntegrationsStatus extends RemoteSyncedIntegrations error?: string; updated_at?: string; } +export interface RemoteSyncedCustomAssetsStatus extends CustomAssetsData { + sync_status: SyncStatus; + error?: string; +} + +export interface RemoteSyncedCustomAssetsRecord { + [key: string]: RemoteSyncedCustomAssetsStatus; +} diff --git a/x-pack/platform/plugins/shared/fleet/common/types/rest_spec/remote_synced_integrations.ts b/x-pack/platform/plugins/shared/fleet/common/types/rest_spec/remote_synced_integrations.ts index e28a73c3bd041..1024a9238a63d 100644 --- a/x-pack/platform/plugins/shared/fleet/common/types/rest_spec/remote_synced_integrations.ts +++ b/x-pack/platform/plugins/shared/fleet/common/types/rest_spec/remote_synced_integrations.ts @@ -4,9 +4,10 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { RemoteSyncedIntegrationsStatus } from '../models'; +import type { RemoteSyncedCustomAssetsRecord, RemoteSyncedIntegrationsStatus } from '../models'; export interface GetRemoteSyncedIntegrationsStatusResponse { - items: RemoteSyncedIntegrationsStatus[]; + integrations: RemoteSyncedIntegrationsStatus[]; + custom_assets?: RemoteSyncedCustomAssetsRecord; error?: string; } diff --git a/x-pack/platform/plugins/shared/fleet/dev_docs/local_setup/remote_clusters_ccr.md b/x-pack/platform/plugins/shared/fleet/dev_docs/local_setup/remote_clusters_ccr.md index 491d3306468d6..3e8d97b073537 100644 --- a/x-pack/platform/plugins/shared/fleet/dev_docs/local_setup/remote_clusters_ccr.md +++ b/x-pack/platform/plugins/shared/fleet/dev_docs/local_setup/remote_clusters_ccr.md @@ -1,21 +1,31 @@ # Test sync integrations feature -## Changes to `kibana.dev.yml` +## Run ES and Kibana + +### Changes to `kibana.dev.yml` - Remove `elasticsearch.hosts` - Enable feature flag + ``` xpack.fleet.enableExperimental: ['enableSyncIntegrationsOnRemote'] ``` +This configuration allows to run two local ES clusters in parallel, each one having its own instance of Kibana. -## Start Cluster 1 (remote) +### Start Cluster 1 (remote) - Start ES 1 ``` yarn es snapshot -E http.port=9500 -E transport.port=9600 -E path.data=../remote --license trial -E http.host=0.0.0.0 ``` +Verify that node is healthy + +``` + curl -k -u elastic:changeme http://localhost:9500 +``` + - Start Kibana 1 ``` @@ -23,13 +33,20 @@ yarn start --server.port=5701 --elasticsearch.hosts=http://localhost:9500 --dev. ``` - Login into http://localhost/5601/ -## Start Cluster 2 (main) +### Start Cluster 2 (main) - Start ES 2 ``` yarn es snapshot --license trial -E path.data=/tmp/es-data -E http.host=0.0.0.0 ``` +Note that `transport.port` defaults to `9300` + +Verify that node is healthy: + +``` +curl -k -u elastic:changeme http://localhost:9200 +``` - Start Kibana 2 @@ -78,38 +95,36 @@ Save the responses as they will be required in Cluster 2 (see next section). ### Set up CCR -Please note that [CCR](https://www.elastic.co/guide/en/elasticsearch/reference/7.17/ccr-getting-started-tutorial.html) requires both clusters to have the same license. At the time of writing an `enterprise` license is needed. +Please note that [CCR](https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-getting-started-tutorial.html) requires both clusters to have the same license. At the time of writing an `enterprise` license is needed. On cluster 1, navigate to *Stack Management > Cross-Cluster Replication* and create a follower index using the cluster from Step 1. - Leader index `fleet-synced-integrations` - Follower index `fleet-synced-integrations-ccr-remote1` ### Set up local ES output -This configuration is required to kick off the integration sync. The local host needs to match the remote ES output configured on Cluster 2 (see next section) +This configuration is required to kick off the integration sync. The local host needs to match the remote ES output configured on Cluster 2 (see next section). Note that `kibana.dev.yml` is read by both kibana instances so it's better to add it in the UI to avoid conflicts. ``` -xpack.fleet.outputs: - - name: 'Preconfigured ES output' - type: 'elasticsearch' - id: 'local-output' - hosts: ["http://:9500"] + name: 'ES output' + type: 'elasticsearch' + id: 'local-output' + hosts: ["http://:9500"] ``` ## Setup on Cluster 2 -### Add remote ES output -- Add remote ES output to Cluster 2. Insert `token` and `key` obtained from the previous commands. This example is adding a preconfigured output but it can also be added from the UI. +### Add remote ES output +- Add remote ES output to Cluster 2. Insert `token` and `key` obtained from the previous commands. ``` -xpack.fleet.outputs: - - name: 'Preconfigured remote output' - type: 'remote_elasticsearch' - id: 'remote-output' - hosts: ["http://:9500"] - sync_integrations: true - kibana_url: "http://localhost:5701" - secrets: - service_token: token - kibana_api_key: key + name: 'Remote output' + type: 'remote_elasticsearch' + id: 'remote-output' + hosts: ["http://:9500"] + sync_integrations: true + kibana_url: "http://localhost:5701" + secrets: + service_token: token + kibana_api_key: key ``` ## Verify sync @@ -118,12 +133,19 @@ With these configuration, the sync task should run every 5 minutes and the integ ### Check contents of sync integrations indices -- In Cluster 1 +- From Cluster 1, verify that the follower index is active using the [follower info API](https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-get-follow-info.html): + +``` +GET fleet-synced-integrations-ccr-remote1/_ccr/info +``` + +Check content of ccr index: + ``` GET fleet-synced-integrations-ccr-remote1/_search ``` -- In Cluster 2 +- From Cluster 2 ``` GET fleet-synced-integrations/_search ``` @@ -132,11 +154,11 @@ GET fleet-synced-integrations/_search - Enroll Fleet Server to Cluster 2 -- Create Agent Policy in Cluster 2 and use remote output for integrations and monitoring +- Create an Agent Policy in Cluster 2 and use remote output for integrations and monitoring -- Enroll Agent to Cluster 2 to the Agent policy +- Enroll an agent the above Agent policy in Cluster 2 -- Check data in Cluster 1 with CCS +- Check data in Cluster 1 with [CCS](https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-cross-cluster-search.html) ``` GET remote1:metrics-*/_search diff --git a/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/handler.ts b/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/handler.ts index 97707a5be2195..4249144d58316 100644 --- a/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/handler.ts +++ b/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/handler.ts @@ -5,16 +5,17 @@ * 2.0. */ -import type { TypeOf } from '@kbn/config-schema'; import type { RequestHandler } from '@kbn/core/server'; import type { GetRemoteSyncedIntegrationsStatusResponse } from '../../../common/types'; import { getRemoteSyncedIntegrationsStatus } from '../../tasks/sync_integrations/sync_integrations_on_remote'; -export const getRemoteSyncedIntegrationsStatusHandler: RequestHandler< - undefined -> = async (context, request, response) => { +export const getRemoteSyncedIntegrationsStatusHandler: RequestHandler = async ( + context, + request, + response +) => { const coreContext = await context.core; const esClient = coreContext.elasticsearch.client.asInternalUser; const soClient = coreContext.savedObjects.client; @@ -22,7 +23,7 @@ export const getRemoteSyncedIntegrationsStatusHandler: RequestHandler< try { const res: GetRemoteSyncedIntegrationsStatusResponse = await getRemoteSyncedIntegrationsStatus( esClient, - soClient, + soClient ); return response.ok({ body: res }); diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/custom_assets.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/custom_assets.ts index 8bfedf60a664c..9f17359803e29 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/custom_assets.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/custom_assets.ts @@ -33,10 +33,10 @@ export const findIntegration = (assetName: string, integrations: IntegrationsDat }); }; -function getComponentTemplate( +export function getComponentTemplate( esClient: ElasticsearchClient, - name: string, - abortController: AbortController + abortController: AbortController, + name?: string ): Promise { return esClient.cluster.getComponentTemplate( { @@ -49,10 +49,10 @@ function getComponentTemplate( ); } -function getPipeline( +export function getPipeline( esClient: ElasticsearchClient, - name: string, - abortController: AbortController + abortController: AbortController, + name?: string ): Promise { return esClient.ingest.getPipeline( { @@ -71,7 +71,7 @@ export const getCustomAssets = async ( abortController: AbortController, previousSyncIntegrationsData: SyncIntegrationsData | undefined ): Promise => { - const customTemplates = await getComponentTemplate(esClient, '*@custom', abortController); + const customTemplates = await getComponentTemplate(esClient, abortController, '*@custom'); const customAssetsComponentTemplates = customTemplates.component_templates.reduce( (acc: CustomAssetsData[], template) => { @@ -90,7 +90,7 @@ export const getCustomAssets = async ( [] ); - const ingestPipelines = await getPipeline(esClient, '*@custom', abortController); + const ingestPipelines = await getPipeline(esClient, abortController, '*@custom'); const customAssetsIngestPipelines = Object.keys(ingestPipelines).reduce( (acc: CustomAssetsData[], pipeline) => { @@ -155,7 +155,7 @@ async function updateComponentTemplate( abortController: AbortController, logger: Logger ) { - const customTemplates = await getComponentTemplate(esClient, customAsset.name, abortController); + const customTemplates = await getComponentTemplate(esClient, abortController, customAsset.name); const existingTemplate = customTemplates.component_templates?.find( (template) => template.name === customAsset.name ); @@ -212,7 +212,7 @@ async function updateIngestPipeline( abortController: AbortController, logger: Logger ) { - const ingestPipelines = await getPipeline(esClient, customAsset.name, abortController); + const ingestPipelines = await getPipeline(esClient, abortController, customAsset.name); const existingPipeline = ingestPipelines[customAsset.name]; if (customAsset.is_deleted) { diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/model.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/model.ts index 14716101533ec..5405071f3e19b 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/model.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/model.ts @@ -25,7 +25,6 @@ export interface CustomAssetsError { timestamp: string; error: string; } - export interface SyncIntegrationsData { remote_es_hosts: Array<{ name: string; diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts index 3c463e987b6af..0a288e4f9fd07 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { isEqual } from 'lodash'; import type { ElasticsearchClient, SavedObjectsClient, @@ -15,6 +15,11 @@ import type { import semverGte from 'semver/functions/gte'; +import type { + CcrFollowInfoFollowerIndex, + ClusterComponentTemplateSummary, +} from '@elastic/elasticsearch/lib/api/types'; + import type { PackageClient } from '../../services'; import { outputService, appContextService } from '../../services'; import { getPackageSavedObjects } from '../../services/epm/packages/get'; @@ -24,11 +29,16 @@ import { FLEET_SYNCED_INTEGRATIONS_CCR_INDEX_PREFIX } from '../../services/setup import type { Installation } from '../../types'; -import type { RemoteSyncedIntegrationsStatus, GetRemoteSyncedIntegrationsStatusResponse, SyncStatus } from '../../../common/types'; +import type { + RemoteSyncedIntegrationsStatus, + GetRemoteSyncedIntegrationsStatusResponse, + SyncStatus, + RemoteSyncedCustomAssetsStatus, + RemoteSyncedCustomAssetsRecord, +} from '../../../common/types'; -import type { SyncIntegrationsData } from './model'; -import { installCustomAsset } from './custom_assets'; -import { CcrFollowInfoFollowerIndex } from '@elastic/elasticsearch/lib/api/types'; +import type { IntegrationsData, SyncIntegrationsData, CustomAssetsData } from './model'; +import { getPipeline, installCustomAsset, getComponentTemplate } from './custom_assets'; const MAX_RETRY_ATTEMPTS = 5; const RETRY_BACKOFF_MINUTES = [5, 10, 20, 40, 60]; @@ -209,22 +219,22 @@ export const syncIntegrationsOnRemote = async ( export const getFollowerIndexInfo = async ( esClient: ElasticsearchClient, logger: Logger -): Promise<{info?: CcrFollowInfoFollowerIndex, error?: string}> => { +): Promise<{ info?: CcrFollowInfoFollowerIndex; error?: string }> => { try { const index = await getFollowerIndex(esClient, new AbortController()); if (!index) { - return {error: `Follower index not found`}; + return { error: `Follower index not found` }; } const res = await esClient.ccr.followInfo({ index, }); if (!res?.follower_indices || res.follower_indices.length === 0) - return {error: `Follower index ${index} not available`}; + return { error: `Follower index ${index} not available` }; if (res.follower_indices[0]?.status === 'paused') { - return {error: `Follower index ${index} paused`}; + return { error: `Follower index ${index} paused` }; } - return {info: res.follower_indices[0]}; + return { info: res.follower_indices[0] }; } catch (err) { if (err?.body?.error?.type === 'index_not_found_exception') throw new IndexNotFoundError(`Index not found`); @@ -254,20 +264,21 @@ export const fetchAndCompareSyncedIntegrations = async ( }); if (searchRes.hits.hits[0]?._source === undefined) { return { - items: [], - error: `No integrations found on ${index}` - } + integrations: [], + error: `No integrations found on ${index}`, + }; } - const ccrIntegrations = searchRes.hits.hits[0]?._source?.integrations; + const ccrIndex = searchRes.hits.hits[0]?._source; + const { integrations: ccrIntegrations, custom_assets: ccrCustomAssets } = ccrIndex; // find integrations installed on remote const installedIntegrations = await getPackageSavedObjects(savedObjectsClient); if (!installedIntegrations && ccrIntegrations.length > 0) { return { - items: [], - error: `No integrations installed on remote` - } + integrations: [], + error: `No integrations installed on remote`, + }; } const installedIntegrationsByName = (installedIntegrations?.saved_objects || []).reduce( @@ -279,39 +290,114 @@ export const fetchAndCompareSyncedIntegrations = async ( }, {} as Record> ); + const customAssetsStatus = await compareCustomAssets(esClient, logger, ccrCustomAssets); + const integrationsStatus = compareIntegrations(ccrIntegrations, installedIntegrationsByName); + const result = { + ...integrationsStatus, + ...(customAssetsStatus && { custom_assets: customAssetsStatus }), + }; + + return result; + } catch (err) { + logger.error('error', err.message); + throw err; + } +}; - const integrationsStatus: RemoteSyncedIntegrationsStatus[] | undefined = ccrIntegrations?.map( - (ccrIntegration) => { - const localIntegrationSO = installedIntegrationsByName[ccrIntegration.package_name]; - if (!localIntegrationSO) { - return { - ...ccrIntegration, - sync_status: 'failed' as SyncStatus.FAILED, - error: `Installation not found`, - }; - } - if (ccrIntegration.package_version !== localIntegrationSO?.attributes.version) { - return { - ...ccrIntegration, +const compareIntegrations = ( + ccrIntegrations: IntegrationsData[], + installedIntegrationsByName: Record> +) => { + const integrationsStatus: RemoteSyncedIntegrationsStatus[] | undefined = ccrIntegrations?.map( + (ccrIntegration) => { + const localIntegrationSO = installedIntegrationsByName[ccrIntegration.package_name]; + if (!localIntegrationSO) { + return { + ...ccrIntegration, + sync_status: 'failed' as SyncStatus.FAILED, + error: `Installation not found`, + }; + } + if (ccrIntegration.package_version !== localIntegrationSO?.attributes.version) { + return { + ...ccrIntegration, + sync_status: 'failed' as SyncStatus.FAILED, + error: `Installed version: ${localIntegrationSO?.attributes.version}`, + }; + } + if (localIntegrationSO?.attributes.install_status !== 'installed') { + return { + ...ccrIntegration, + sync_status: 'failed' as SyncStatus.FAILED, + error: `Installation status: ${localIntegrationSO?.attributes.install_status}`, + }; + } + return { + ...ccrIntegration, + sync_status: 'completed' as SyncStatus.COMPLETED, + updated_at: localIntegrationSO?.updated_at, + }; + } + ); + return { integrations: integrationsStatus }; +}; + +const compareCustomAssets = async ( + esClient: ElasticsearchClient, + logger: Logger, + ccrCustomAssets: { [key: string]: CustomAssetsData } +) => { + if (!ccrCustomAssets) return; + + const abortController = new AbortController(); + + try { + const installedPipelines = await getPipeline(esClient, abortController); + const installedComponentTemplates = await getComponentTemplate(esClient, abortController); + + const componentTemplatesByName = ( + installedComponentTemplates?.component_templates || [] + ).reduce((acc, componentTemplate) => { + if (componentTemplate?.name) { + acc[componentTemplate.name] = componentTemplate.component_template?.template; + } + return acc; + }, {} as Record); + const result: RemoteSyncedCustomAssetsRecord = {}; + + // compare custom pipelines and custom component templates + Object.entries(ccrCustomAssets).forEach(([ccrCustomName, ccrCustomAsset]) => { + if (ccrCustomAsset.type === 'ingest_pipeline') { + const installedAsset = installedPipelines[ccrCustomAsset?.name]; + if (isEqual(installedAsset?.processors, ccrCustomAsset?.pipeline?.processors)) { + result[ccrCustomName] = { + ...ccrCustomAsset, + sync_status: 'completed' as SyncStatus.COMPLETED, + } as RemoteSyncedCustomAssetsStatus; + } else { + result[ccrCustomName] = { + ...ccrCustomAsset, sync_status: 'failed' as SyncStatus.FAILED, - error: `Installed version: ${localIntegrationSO?.attributes.version}`, - }; + } as RemoteSyncedCustomAssetsStatus; } - if (localIntegrationSO?.attributes.install_status !== 'installed') { - return { - ...ccrIntegration, + } + if (ccrCustomAsset.type === 'component_template') { + const installedAsset = componentTemplatesByName[ccrCustomAsset?.name]; + + if (isEqual(installedAsset, ccrCustomAsset?.template)) { + result[ccrCustomName] = { + ...ccrCustomAsset, + sync_status: 'completed' as SyncStatus.COMPLETED, + } as RemoteSyncedCustomAssetsStatus; + } else { + result[ccrCustomName] = { + ...ccrCustomAsset, sync_status: 'failed' as SyncStatus.FAILED, - error: `Installation status: ${localIntegrationSO?.attributes.install_status}`, - }; + } as RemoteSyncedCustomAssetsStatus; } - return { - ...ccrIntegration, - sync_status: 'completed' as SyncStatus.COMPLETED, - updated_at: localIntegrationSO?.updated_at, - }; } - ); - return {items: integrationsStatus}; + }); + return result; } catch (err) { logger.error('error', err.message); throw err; @@ -320,21 +406,26 @@ export const fetchAndCompareSyncedIntegrations = async ( export const getRemoteSyncedIntegrationsStatus = async ( esClient: ElasticsearchClient, - soClient: SavedObjectsClientContract, + soClient: SavedObjectsClientContract ): Promise => { const { enableSyncIntegrationsOnRemote } = appContextService.getExperimentalFeatures(); const logger = appContextService.getLogger(); if (!enableSyncIntegrationsOnRemote) { - return { items: [] }; + return { integrations: [] }; } try { const followerIndexRes = await getFollowerIndexInfo(esClient, logger); if (followerIndexRes?.error || !followerIndexRes?.info) { - return { error: followerIndexRes?.error, items: [] }; + return { error: followerIndexRes?.error, integrations: [] }; } - const res = await fetchAndCompareSyncedIntegrations(esClient, soClient, followerIndexRes.info.follower_index, logger); + const res = await fetchAndCompareSyncedIntegrations( + esClient, + soClient, + followerIndexRes.info.follower_index, + logger + ); return res; } catch (err) { throw err; diff --git a/x-pack/platform/plugins/shared/fleet/server/types/models/synced_integrations.ts b/x-pack/platform/plugins/shared/fleet/server/types/models/synced_integrations.ts index 4137dd9402365..be4dbbfd4d7bd 100644 --- a/x-pack/platform/plugins/shared/fleet/server/types/models/synced_integrations.ts +++ b/x-pack/platform/plugins/shared/fleet/server/types/models/synced_integrations.ts @@ -6,6 +6,7 @@ */ import { schema } from '@kbn/config-schema'; + import { SyncStatus } from '../../../common/types'; const RemoteSyncedIntegrationsBaseSchema = schema.object({ @@ -24,7 +25,26 @@ export const RemoteSyncedIntegrationsStatusSchema = RemoteSyncedIntegrationsBase updated_at: schema.maybe(schema.string()), }); +export const CustomAssetsDataSchema = schema + .object({ + type: schema.string(), + name: schema.string(), + package_name: schema.string(), + package_version: schema.string(), + is_deleted: schema.boolean(), + deleted_at: schema.maybe(schema.string()), + sync_status: schema.oneOf([ + schema.literal(SyncStatus.COMPLETED), + schema.literal(SyncStatus.SYNCHRONIZING), + schema.literal(SyncStatus.FAILED), + ]), + }) + .extendsDeep({ + unknowns: 'allow', + }); + export const GetRemoteSyncedIntegrationsStatusResponseSchema = schema.object({ - items: schema.arrayOf(RemoteSyncedIntegrationsStatusSchema), + integrations: schema.arrayOf(RemoteSyncedIntegrationsStatusSchema), + custom_assets: schema.maybe(schema.recordOf(schema.string(), CustomAssetsDataSchema)), error: schema.maybe(schema.string()), }); From f022ee1f4b205eaa9c493d226620363a953c4aa3 Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Tue, 1 Apr 2025 17:11:57 +0200 Subject: [PATCH 08/26] Address comments and add unit tests --- .../tasks/sync_integrations/custom_assets.ts | 21 +- .../sync_integrations_on_remote.test.ts | 366 +++++++++++++++++- .../sync_integrations_on_remote.ts | 22 +- 3 files changed, 394 insertions(+), 15 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/custom_assets.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/custom_assets.ts index 9f17359803e29..a1de4721998c2 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/custom_assets.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/custom_assets.ts @@ -18,6 +18,7 @@ import { retryTransientEsErrors } from '../../services/epm/elasticsearch/retry'; import type { CustomAssetsData, IntegrationsData, SyncIntegrationsData } from './model'; const DELETED_ASSET_TTL = 7 * 24 * 60 * 60 * 1000; // 7 days +export const CUSTOM_ASSETS_PREFIX = '*@custom'; export const findIntegration = (assetName: string, integrations: IntegrationsData[]) => { const matches = assetName.match(/^(\w*)?(?:\-)?(\w*)(?:\-)?(?:\.)?(?:\w*)?@custom$/); @@ -35,8 +36,8 @@ export const findIntegration = (assetName: string, integrations: IntegrationsDat export function getComponentTemplate( esClient: ElasticsearchClient, - abortController: AbortController, - name?: string + name: string, + abortController: AbortController ): Promise { return esClient.cluster.getComponentTemplate( { @@ -51,8 +52,8 @@ export function getComponentTemplate( export function getPipeline( esClient: ElasticsearchClient, - abortController: AbortController, - name?: string + name: string, + abortController: AbortController ): Promise { return esClient.ingest.getPipeline( { @@ -71,7 +72,11 @@ export const getCustomAssets = async ( abortController: AbortController, previousSyncIntegrationsData: SyncIntegrationsData | undefined ): Promise => { - const customTemplates = await getComponentTemplate(esClient, abortController, '*@custom'); + const customTemplates = await getComponentTemplate( + esClient, + CUSTOM_ASSETS_PREFIX, + abortController + ); const customAssetsComponentTemplates = customTemplates.component_templates.reduce( (acc: CustomAssetsData[], template) => { @@ -90,7 +95,7 @@ export const getCustomAssets = async ( [] ); - const ingestPipelines = await getPipeline(esClient, abortController, '*@custom'); + const ingestPipelines = await getPipeline(esClient, CUSTOM_ASSETS_PREFIX, abortController); const customAssetsIngestPipelines = Object.keys(ingestPipelines).reduce( (acc: CustomAssetsData[], pipeline) => { @@ -155,7 +160,7 @@ async function updateComponentTemplate( abortController: AbortController, logger: Logger ) { - const customTemplates = await getComponentTemplate(esClient, abortController, customAsset.name); + const customTemplates = await getComponentTemplate(esClient, customAsset.name, abortController); const existingTemplate = customTemplates.component_templates?.find( (template) => template.name === customAsset.name ); @@ -212,7 +217,7 @@ async function updateIngestPipeline( abortController: AbortController, logger: Logger ) { - const ingestPipelines = await getPipeline(esClient, abortController, customAsset.name); + const ingestPipelines = await getPipeline(esClient, customAsset.name, abortController); const existingPipeline = ingestPipelines[customAsset.name]; if (customAsset.is_deleted) { diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.test.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.test.ts index ab2a75260e3cd..cf8ebf12e83a5 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.test.ts @@ -5,15 +5,29 @@ * 2.0. */ +import { savedObjectsClientMock } from '@kbn/core/server/mocks'; + import { PackageNotFoundError } from '../../errors'; import { outputService } from '../../services'; +import { getPackageSavedObjects } from '../../services/epm/packages/get'; + import { installCustomAsset } from './custom_assets'; -import { syncIntegrationsOnRemote } from './sync_integrations_on_remote'; +import { + syncIntegrationsOnRemote, + getFollowerIndexInfo, + fetchAndCompareSyncedIntegrations, +} from './sync_integrations_on_remote'; jest.mock('../../services'); jest.mock('./custom_assets'); +jest.mock('../../services/epm/packages/get', () => { + return { + ...jest.requireActual('../../services/epm/packages/get'), + getPackageSavedObjects: jest.fn(), + }; +}); const outputServiceMock = outputService as jest.Mocked; @@ -451,3 +465,353 @@ describe('syncIntegrationsOnRemote', () => { expect(installCustomAsset).toHaveBeenCalledTimes(2); }); }); + +describe('getFollowerIndexInfo', () => { + let esClientMock: any; + let getIndicesMock: jest.Mock; + let searchMock: jest.Mock; + let loggerMock: any; + + beforeEach(() => { + getIndicesMock = jest.fn(); + searchMock = jest.fn(); + esClientMock = { + indices: { + get: getIndicesMock, + }, + search: searchMock, + ccr: { followInfo: jest.fn() }, + }; + + loggerMock = { + debug: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + }; + (installCustomAsset as jest.Mock).mockClear(); + getIndicesMock.mockResolvedValue({ + 'fleet-synced-integrations-ccr-remote1': {}, + }); + }); + + it('should return error if follower index is not available', async () => { + expect(await getFollowerIndexInfo(esClientMock, loggerMock)).toEqual({ + error: 'Follower index fleet-synced-integrations-ccr-remote1 not available', + }); + }); + + it('should return error if follower index returns empty array', async () => { + esClientMock.ccr.followInfo.mockResolvedValue({ + follower_indices: [], + } as any); + + expect(await getFollowerIndexInfo(esClientMock, loggerMock)).toEqual({ + error: 'Follower index fleet-synced-integrations-ccr-remote1 not available', + }); + }); + + it('should return info about follower index', async () => { + esClientMock.ccr.followInfo.mockResolvedValue({ + follower_indices: [ + { + follower_index: 'fleet-synced-integrations-ccr-remote1', + remote_cluster: 'Main', + leader_index: 'fleet-synced-integrations', + status: 'active', + parameters: { + max_read_request_operation_count: 5120, + max_write_request_operation_count: 5120, + max_outstanding_read_requests: 12, + max_outstanding_write_requests: 9, + max_read_request_size: '32mb', + max_write_request_size: '9223372036854775807b', + max_write_buffer_count: 2147483647, + max_write_buffer_size: '512mb', + max_retry_delay: '500ms', + read_poll_timeout: '1m', + }, + }, + ], + } as any); + const info = await getFollowerIndexInfo(esClientMock, loggerMock); + expect(info).toEqual({ + info: { + follower_index: 'fleet-synced-integrations-ccr-remote1', + remote_cluster: 'Main', + leader_index: 'fleet-synced-integrations', + status: 'active', + parameters: { + max_read_request_operation_count: 5120, + max_write_request_operation_count: 5120, + max_outstanding_read_requests: 12, + max_outstanding_write_requests: 9, + max_read_request_size: '32mb', + max_write_request_size: '9223372036854775807b', + max_write_buffer_count: 2147483647, + max_write_buffer_size: '512mb', + max_retry_delay: '500ms', + read_poll_timeout: '1m', + }, + }, + }); + }); + + it('should return error if follower is paused', async () => { + esClientMock.ccr.followInfo.mockResolvedValue({ + follower_indices: [ + { + follower_index: 'fleet-synced-integrations-ccr-remote1', + remote_cluster: 'Main', + leader_index: 'fleet-synced-integrations', + status: 'paused', + parameters: {}, + }, + ], + } as any); + const info = await getFollowerIndexInfo(esClientMock, loggerMock); + expect(info).toEqual({ + error: 'Follower index fleet-synced-integrations-ccr-remote1 paused', + }); + }); +}); + +describe('fetchAndCompareSyncedIntegrations', () => { + let esClientMock: any; + let getIndicesMock: jest.Mock; + let searchMock: jest.Mock; + const soClientMock = savedObjectsClientMock.create(); + + let loggerMock: any; + + beforeEach(() => { + getIndicesMock = jest.fn(); + searchMock = jest.fn(); + esClientMock = { + indices: { + get: getIndicesMock, + }, + search: searchMock, + }; + + loggerMock = { + debug: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + }; + (installCustomAsset as jest.Mock).mockClear(); + }); + + it('should return error if no integrations are found', async () => { + esClientMock.search.mockResolvedValueOnce({ + hits: { + hits: [], + }, + } as any); + const res = await fetchAndCompareSyncedIntegrations( + esClientMock, + soClientMock, + 'fleet-synced-integrations-ccr-*', + loggerMock + ); + expect(res).toEqual({ + error: 'No integrations found on fleet-synced-integrations-ccr-*', + integrations: [], + }); + }); + + it('should return error if there are integrations on sync index but none are installed', async () => { + esClientMock.search.mockResolvedValueOnce({ + hits: { + hits: [ + { + _source: { + integrations: [ + { + package_name: 'elastic_agent', + package_version: '2.2.0', + updated_at: '2025-03-20T14:18:40.076Z', + }, + { + package_name: 'system', + package_version: '1.67.3', + updated_at: '2025-03-20T14:18:40.111Z', + }, + ], + }, + }, + ], + }, + } as any); + const res = await fetchAndCompareSyncedIntegrations( + esClientMock, + soClientMock, + 'fleet-synced-integrations-ccr-*', + loggerMock + ); + expect(res).toEqual({ + error: 'No integrations installed on remote', + integrations: [], + }); + }); + it('should compare integrations installed on remote with the ones on sync index', async () => { + esClientMock.search.mockResolvedValueOnce({ + hits: { + hits: [ + { + _source: { + integrations: [ + { + package_name: 'elastic_agent', + package_version: '2.2.0', + updated_at: '2025-03-20T14:18:40.076Z', + }, + { + package_name: 'system', + package_version: '1.67.3', + updated_at: '2025-03-20T14:18:40.111Z', + }, + ], + }, + }, + ], + }, + } as any); + (getPackageSavedObjects as jest.MockedFunction).mockReturnValue({ + page: 1, + per_page: 10000, + total: 1, + saved_objects: [ + { + type: 'epm-packages', + id: 'elastic_agent', + attributes: { + version: '2.2.0', + install_status: 'installed', + }, + + updated_at: '2025-03-26T14:06:27.611Z', + }, + { + type: 'epm-packages', + id: 'system', + attributes: { + version: '1.67.3', + install_status: 'installed', + }, + + updated_at: '2025-03-26T14:06:27.611Z', + }, + ], + }); + const res = await fetchAndCompareSyncedIntegrations( + esClientMock, + soClientMock, + 'fleet-synced-integrations-ccr-*', + loggerMock + ); + expect(res).toEqual({ + integrations: [ + { + package_name: 'elastic_agent', + package_version: '2.2.0', + sync_status: 'completed', + updated_at: expect.any(String), + }, + { + package_name: 'system', + package_version: '1.67.3', + sync_status: 'completed', + updated_at: expect.any(String), + }, + ], + }); + }); + + it('should return errors for single integrations to cover for different cases', async () => { + esClientMock.search.mockResolvedValueOnce({ + hits: { + hits: [ + { + _source: { + integrations: [ + { + package_name: 'elastic_agent', + package_version: '2.2.0', + updated_at: '2025-03-20T14:18:40.076Z', + }, + { + package_name: 'system', + package_version: '1.67.3', + updated_at: '2025-03-20T14:18:40.111Z', + }, + { + package_name: 'synthetics', + package_version: '1.4.1', + updated_at: '2025-03-17T15:21:14.092Z', + }, + ], + }, + }, + ], + }, + } as any); + (getPackageSavedObjects as jest.MockedFunction).mockReturnValue({ + page: 1, + per_page: 10000, + total: 1, + saved_objects: [ + { + type: 'epm-packages', + id: 'system', + attributes: { + version: '1.67.2', + install_status: 'installed', + }, + updated_at: '2025-03-26T14:06:27.611Z', + }, + { + type: 'epm-packages', + id: 'synthetics', + attributes: { + version: '1.4.1', + install_status: 'not_installed', + }, + updated_at: '2025-03-26T14:06:27.611Z', + }, + ], + }); + const res = await fetchAndCompareSyncedIntegrations( + esClientMock, + soClientMock, + 'fleet-synced-integrations-ccr-*', + loggerMock + ); + expect(res).toEqual({ + integrations: [ + { + error: 'Installation not found on remote', + package_name: 'elastic_agent', + package_version: '2.2.0', + sync_status: 'failed', + updated_at: expect.any(String), + }, + { + package_name: 'system', + package_version: '1.67.3', + sync_status: 'failed', + updated_at: expect.any(String), + error: 'Found incorrect installed version 1.67.2', + }, + { + error: 'Installation status: not_installed', + package_name: 'synthetics', + package_version: '1.4.1', + sync_status: 'failed', + updated_at: expect.any(String), + }, + ], + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts index 0a288e4f9fd07..a4933eeb6414c 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts @@ -38,7 +38,12 @@ import type { } from '../../../common/types'; import type { IntegrationsData, SyncIntegrationsData, CustomAssetsData } from './model'; -import { getPipeline, installCustomAsset, getComponentTemplate } from './custom_assets'; +import { + getPipeline, + installCustomAsset, + getComponentTemplate, + CUSTOM_ASSETS_PREFIX, +} from './custom_assets'; const MAX_RETRY_ATTEMPTS = 5; const RETRY_BACKOFF_MINUTES = [5, 10, 20, 40, 60]; @@ -315,14 +320,14 @@ const compareIntegrations = ( return { ...ccrIntegration, sync_status: 'failed' as SyncStatus.FAILED, - error: `Installation not found`, + error: `Installation not found on remote`, }; } if (ccrIntegration.package_version !== localIntegrationSO?.attributes.version) { return { ...ccrIntegration, sync_status: 'failed' as SyncStatus.FAILED, - error: `Installed version: ${localIntegrationSO?.attributes.version}`, + error: `Found incorrect installed version ${localIntegrationSO?.attributes.version}`, }; } if (localIntegrationSO?.attributes.install_status !== 'installed') { @@ -352,8 +357,12 @@ const compareCustomAssets = async ( const abortController = new AbortController(); try { - const installedPipelines = await getPipeline(esClient, abortController); - const installedComponentTemplates = await getComponentTemplate(esClient, abortController); + const installedPipelines = await getPipeline(esClient, CUSTOM_ASSETS_PREFIX, abortController); + const installedComponentTemplates = await getComponentTemplate( + esClient, + CUSTOM_ASSETS_PREFIX, + abortController + ); const componentTemplatesByName = ( installedComponentTemplates?.component_templates || [] @@ -369,7 +378,8 @@ const compareCustomAssets = async ( Object.entries(ccrCustomAssets).forEach(([ccrCustomName, ccrCustomAsset]) => { if (ccrCustomAsset.type === 'ingest_pipeline') { const installedAsset = installedPipelines[ccrCustomAsset?.name]; - if (isEqual(installedAsset?.processors, ccrCustomAsset?.pipeline?.processors)) { + + if (isEqual(installedAsset, ccrCustomAsset?.pipeline)) { result[ccrCustomName] = { ...ccrCustomAsset, sync_status: 'completed' as SyncStatus.COMPLETED, From 80093978e6578da4f9a854d4495c62501960f2fe Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 1 Apr 2025 16:21:22 +0000 Subject: [PATCH 09/26] [CI] Auto-commit changed files from 'node scripts/capture_oas_snapshot --include-path /api/status --include-path /api/alerting/rule/ --include-path /api/alerting/rules --include-path /api/actions --include-path /api/security/role --include-path /api/spaces --include-path /api/streams --include-path /api/fleet --include-path /api/dashboards --update' --- oas_docs/bundle.json | 142 ++++++++++++++++++++++++++++++++ oas_docs/bundle.serverless.json | 142 ++++++++++++++++++++++++++++++++ 2 files changed, 284 insertions(+) diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index 070d45787fff4..49a7731e3ce9f 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -43637,6 +43637,145 @@ ] } }, + "/api/fleet/remote_synced_integrations/status": { + "get": { + "description": "[Required authorization] Route required privileges: ALL of [fleet-settings-read, integrations-read].", + "operationId": "get-fleet-remote-synced-integrations-status", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": false, + "properties": { + "custom_assets": { + "additionalProperties": { + "additionalProperties": true, + "properties": { + "deleted_at": { + "type": "string" + }, + "is_deleted": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "package_name": { + "type": "string" + }, + "package_version": { + "type": "string" + }, + "sync_status": { + "enum": [ + "completed", + "synchronizing", + "failed" + ], + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "type", + "name", + "package_name", + "package_version", + "is_deleted", + "sync_status" + ], + "type": "object" + }, + "type": "object" + }, + "error": { + "type": "string" + }, + "integrations": { + "items": { + "additionalProperties": false, + "properties": { + "error": { + "type": "string" + }, + "id": { + "type": "string" + }, + "package_name": { + "type": "string" + }, + "package_version": { + "type": "string" + }, + "sync_status": { + "enum": [ + "completed", + "synchronizing", + "failed" + ], + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "sync_status" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "integrations" + ], + "type": "object" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "additionalProperties": false, + "description": "Generic Error", + "properties": { + "attributes": {}, + "error": { + "type": "string" + }, + "errorType": { + "type": "string" + }, + "message": { + "type": "string" + }, + "statusCode": { + "type": "number" + } + }, + "required": [ + "message", + "attributes" + ], + "type": "object" + } + } + } + } + }, + "summary": "Get CCR Remote synced integrations status", + "tags": [ + "CCR Remote synced integrations" + ] + } + }, "/api/fleet/service_tokens": { "post": { "description": "[Required authorization] Route required privileges: ALL of [fleet-agents-all].", @@ -50257,6 +50396,9 @@ { "name": "alerting" }, + { + "name": "CCR Remote synced integrations" + }, { "name": "connectors" }, diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index acd91ee213f9d..8ef10b9bda7a0 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -43637,6 +43637,145 @@ ] } }, + "/api/fleet/remote_synced_integrations/status": { + "get": { + "description": "[Required authorization] Route required privileges: ALL of [fleet-settings-read, integrations-read].", + "operationId": "get-fleet-remote-synced-integrations-status", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": false, + "properties": { + "custom_assets": { + "additionalProperties": { + "additionalProperties": true, + "properties": { + "deleted_at": { + "type": "string" + }, + "is_deleted": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "package_name": { + "type": "string" + }, + "package_version": { + "type": "string" + }, + "sync_status": { + "enum": [ + "completed", + "synchronizing", + "failed" + ], + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "type", + "name", + "package_name", + "package_version", + "is_deleted", + "sync_status" + ], + "type": "object" + }, + "type": "object" + }, + "error": { + "type": "string" + }, + "integrations": { + "items": { + "additionalProperties": false, + "properties": { + "error": { + "type": "string" + }, + "id": { + "type": "string" + }, + "package_name": { + "type": "string" + }, + "package_version": { + "type": "string" + }, + "sync_status": { + "enum": [ + "completed", + "synchronizing", + "failed" + ], + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "sync_status" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "integrations" + ], + "type": "object" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "additionalProperties": false, + "description": "Generic Error", + "properties": { + "attributes": {}, + "error": { + "type": "string" + }, + "errorType": { + "type": "string" + }, + "message": { + "type": "string" + }, + "statusCode": { + "type": "number" + } + }, + "required": [ + "message", + "attributes" + ], + "type": "object" + } + } + } + } + }, + "summary": "Get CCR Remote synced integrations status", + "tags": [ + "CCR Remote synced integrations" + ] + } + }, "/api/fleet/service_tokens": { "post": { "description": "[Required authorization] Route required privileges: ALL of [fleet-agents-all].", @@ -49848,6 +49987,9 @@ { "name": "alerting" }, + { + "name": "CCR Remote synced integrations" + }, { "name": "connectors" }, From 778cb1adc7241a593e0e133f2334120f5dbffc05 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 1 Apr 2025 16:41:35 +0000 Subject: [PATCH 10/26] [CI] Auto-commit changed files from 'make api-docs' --- oas_docs/output/kibana.serverless.yaml | 96 ++++++++++++++++++++++++++ oas_docs/output/kibana.yaml | 96 ++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 35493df93dd41..e2e423c6fe7a8 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -66,6 +66,7 @@ tags: Configure APM source maps. A source map allows minified files to be mapped back to original source code--allowing you to maintain the speed advantage of minified code, without losing the ability to quickly and easily debug your application. For best results, uploading source maps should become a part of your deployment procedure, and not something you only do when you see unhelpful errors. That's because uploading source maps after errors happen won't make old errors magically readable--errors must occur again for source mapping to occur. name: APM sourcemaps + - name: CCR Remote synced integrations - name: connectors description: | Connectors provide a central place to store connection information for services and integrations with Elastic or third party systems. Alerting rules can use connectors to run actions when rule conditions are met. @@ -37710,6 +37711,101 @@ paths: summary: Update a proxy tags: - Fleet proxies + /api/fleet/remote_synced_integrations/status: + get: + description: '[Required authorization] Route required privileges: ALL of [fleet-settings-read, integrations-read].' + operationId: get-fleet-remote-synced-integrations-status + parameters: [] + responses: + '200': + content: + application/json: + schema: + additionalProperties: false + type: object + properties: + custom_assets: + additionalProperties: + additionalProperties: true + type: object + properties: + deleted_at: + type: string + is_deleted: + type: boolean + name: + type: string + package_name: + type: string + package_version: + type: string + sync_status: + enum: + - completed + - synchronizing + - failed + type: string + type: + type: string + required: + - type + - name + - package_name + - package_version + - is_deleted + - sync_status + type: object + error: + type: string + integrations: + items: + additionalProperties: false + type: object + properties: + error: + type: string + id: + type: string + package_name: + type: string + package_version: + type: string + sync_status: + enum: + - completed + - synchronizing + - failed + type: string + updated_at: + type: string + required: + - sync_status + type: array + required: + - integrations + '400': + content: + application/json: + schema: + additionalProperties: false + description: Generic Error + type: object + properties: + attributes: {} + error: + type: string + errorType: + type: string + message: + type: string + statusCode: + type: number + required: + - message + - attributes + summary: Get CCR Remote synced integrations status + tags: + - CCR Remote synced integrations /api/fleet/service_tokens: post: description: '[Required authorization] Route required privileges: ALL of [fleet-agents-all].' diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 15d47b0df20b9..490187df4d3d1 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -80,6 +80,7 @@ tags: description: Cases documentation url: https://www.elastic.co/guide/en/kibana/master/cases.html x-displayName: Cases + - name: CCR Remote synced integrations - name: connectors description: | Connectors provide a central place to store connection information for services and integrations with Elastic or third party systems. Alerting rules can use connectors to run actions when rule conditions are met. @@ -39945,6 +39946,101 @@ paths: summary: Update a proxy tags: - Fleet proxies + /api/fleet/remote_synced_integrations/status: + get: + description: '[Required authorization] Route required privileges: ALL of [fleet-settings-read, integrations-read].' + operationId: get-fleet-remote-synced-integrations-status + parameters: [] + responses: + '200': + content: + application/json: + schema: + additionalProperties: false + type: object + properties: + custom_assets: + additionalProperties: + additionalProperties: true + type: object + properties: + deleted_at: + type: string + is_deleted: + type: boolean + name: + type: string + package_name: + type: string + package_version: + type: string + sync_status: + enum: + - completed + - synchronizing + - failed + type: string + type: + type: string + required: + - type + - name + - package_name + - package_version + - is_deleted + - sync_status + type: object + error: + type: string + integrations: + items: + additionalProperties: false + type: object + properties: + error: + type: string + id: + type: string + package_name: + type: string + package_version: + type: string + sync_status: + enum: + - completed + - synchronizing + - failed + type: string + updated_at: + type: string + required: + - sync_status + type: array + required: + - integrations + '400': + content: + application/json: + schema: + additionalProperties: false + description: Generic Error + type: object + properties: + attributes: {} + error: + type: string + errorType: + type: string + message: + type: string + statusCode: + type: number + required: + - message + - attributes + summary: Get CCR Remote synced integrations status + tags: + - CCR Remote synced integrations /api/fleet/service_tokens: post: description: '[Required authorization] Route required privileges: ALL of [fleet-agents-all].' From ef49a31c418a4901f0ba3698726cd5d33d68c3d0 Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Wed, 2 Apr 2025 18:20:33 +0200 Subject: [PATCH 11/26] Address code review comments and add tests --- .../sync_integrations_on_remote.test.ts | 371 +++++++++++++++++- .../sync_integrations_on_remote.ts | 115 ++++-- 2 files changed, 444 insertions(+), 42 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.test.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.test.ts index cf8ebf12e83a5..c6e4e71d95950 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.test.ts @@ -12,8 +12,7 @@ import { outputService } from '../../services'; import { getPackageSavedObjects } from '../../services/epm/packages/get'; -import { installCustomAsset } from './custom_assets'; - +import { installCustomAsset, getPipeline, getComponentTemplate } from './custom_assets'; import { syncIntegrationsOnRemote, getFollowerIndexInfo, @@ -21,7 +20,9 @@ import { } from './sync_integrations_on_remote'; jest.mock('../../services'); -jest.mock('./custom_assets'); +jest.mock('./custom_assets', () => { + return { getPipeline: jest.fn(), getComponentTemplate: jest.fn(), installCustomAsset: jest.fn() }; +}); jest.mock('../../services/epm/packages/get', () => { return { ...jest.requireActual('../../services/epm/packages/get'), @@ -30,6 +31,8 @@ jest.mock('../../services/epm/packages/get', () => { }); const outputServiceMock = outputService as jest.Mocked; +const getPipelineMock = getPipeline as jest.Mocked; +const getComponentTemplateMock = getComponentTemplate as jest.Mocked; describe('syncIntegrationsOnRemote', () => { const abortController = new AbortController(); @@ -760,7 +763,7 @@ describe('fetchAndCompareSyncedIntegrations', () => { (getPackageSavedObjects as jest.MockedFunction).mockReturnValue({ page: 1, per_page: 10000, - total: 1, + total: 2, saved_objects: [ { type: 'epm-packages', @@ -814,4 +817,364 @@ describe('fetchAndCompareSyncedIntegrations', () => { ], }); }); + + describe('compare custom integrations', () => { + let searchMockWithCustomAssets: any; + + afterEach(() => { + jest.resetAllMocks(); + }); + beforeEach(() => { + searchMockWithCustomAssets = jest.fn().mockResolvedValue({ + hits: { + hits: [ + { + _source: { + integrations: [ + { + package_name: 'system', + package_version: '1.67.3', + updated_at: '2025-03-20T14:18:40.076Z', + }, + ], + custom_assets: { + 'component_template:logs-system.auth@custom': { + type: 'component_template', + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '1.67.3', + is_deleted: false, + template: { + mappings: { + dynamic_templates: [], + properties: { + 'Test-mapping': { + type: 'text', + }, + }, + }, + }, + }, + 'ingest_pipeline:logs-system.auth@custom': { + type: 'ingest_pipeline', + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '1.67.3', + is_deleted: false, + pipeline: { + processors: [ + { + set: { + field: 'test', + value: 'test-value', + }, + }, + ], + }, + }, + }, + }, + }, + ], + }, + }); + }); + + it('should return synchronizing state if the assets are different', async () => { + (getPipelineMock as jest.MockedFunction).mockResolvedValue({ + 'logs-system.auth@custom': { + processors: [ + { + user_agent: { + field: 'user_agent', + }, + }, + ], + }, + }); + (getComponentTemplateMock as jest.MockedFunction).mockResolvedValue({ + component_templates: [ + { + name: 'logs-system.auth@custom', + component_template: { + template: { + mappings: { + properties: { + new_field: { + type: 'text', + }, + }, + }, + }, + }, + }, + ], + }); + + esClientMock = { + search: searchMockWithCustomAssets, + }; + (getPackageSavedObjects as jest.MockedFunction).mockReturnValue({ + page: 1, + per_page: 10000, + total: 1, + saved_objects: [ + { + type: 'epm-packages', + id: 'system', + attributes: { + version: '1.67.3', + install_status: 'installed', + }, + updated_at: '2025-03-26T14:06:27.611Z', + }, + ], + }); + + const res = await fetchAndCompareSyncedIntegrations( + esClientMock, + soClientMock, + 'fleet-synced-integrations-ccr-*', + loggerMock + ); + + expect(res).toEqual({ + integrations: [ + { + package_name: 'system', + package_version: '1.67.3', + sync_status: 'completed', + updated_at: expect.any(String), + }, + ], + custom_assets: { + 'component_template:logs-system.auth@custom': { + is_deleted: false, + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '1.67.3', + sync_status: 'synchronizing', + template: { + mappings: { + dynamic_templates: [], + properties: { + 'Test-mapping': { + type: 'text', + }, + }, + }, + }, + type: 'component_template', + }, + 'ingest_pipeline:logs-system.auth@custom': { + is_deleted: false, + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '1.67.3', + pipeline: { + processors: [ + { + set: { + field: 'test', + value: 'test-value', + }, + }, + ], + }, + sync_status: 'synchronizing', + type: 'ingest_pipeline', + }, + }, + }); + }); + + it('should return completed if custom assets are equal', async () => { + (getPipelineMock as jest.MockedFunction).mockResolvedValue({ + 'logs-system.auth@custom': { + processors: [ + { + set: { + field: 'test', + value: 'test-value', + }, + }, + ], + }, + }); + (getComponentTemplateMock as jest.MockedFunction).mockResolvedValue({ + component_templates: [ + { + name: 'logs-system.auth@custom', + component_template: { + template: { + mappings: { + dynamic_templates: [], + properties: { + 'Test-mapping': { + type: 'text', + }, + }, + }, + }, + }, + }, + ], + }); + + esClientMock = { + search: searchMockWithCustomAssets, + }; + (getPackageSavedObjects as jest.MockedFunction).mockReturnValue({ + page: 1, + per_page: 10000, + total: 1, + saved_objects: [ + { + type: 'epm-packages', + id: 'system', + attributes: { + version: '1.67.3', + install_status: 'installed', + }, + updated_at: '2025-03-26T14:06:27.611Z', + }, + ], + }); + + const res = await fetchAndCompareSyncedIntegrations( + esClientMock, + soClientMock, + 'fleet-synced-integrations-ccr-*', + loggerMock + ); + + expect(res).toEqual({ + integrations: [ + { + package_name: 'system', + package_version: '1.67.3', + sync_status: 'completed', + updated_at: expect.any(String), + }, + ], + custom_assets: { + 'component_template:logs-system.auth@custom': { + is_deleted: false, + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '1.67.3', + sync_status: 'completed', + template: { + mappings: { + dynamic_templates: [], + properties: { + 'Test-mapping': { + type: 'text', + }, + }, + }, + }, + type: 'component_template', + }, + 'ingest_pipeline:logs-system.auth@custom': { + is_deleted: false, + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '1.67.3', + pipeline: { + processors: [ + { + set: { + field: 'test', + value: 'test-value', + }, + }, + ], + }, + sync_status: 'completed', + type: 'ingest_pipeline', + }, + }, + }); + }); + + it('should return error if no custom assets are installed', async () => { + esClientMock = { + search: searchMockWithCustomAssets, + }; + (getPackageSavedObjects as jest.MockedFunction).mockReturnValue({ + page: 1, + per_page: 10000, + total: 1, + saved_objects: [ + { + type: 'epm-packages', + id: 'system', + attributes: { + version: '1.67.3', + install_status: 'installed', + }, + updated_at: '2025-03-26T14:06:27.611Z', + }, + ], + }); + + const res = await fetchAndCompareSyncedIntegrations( + esClientMock, + soClientMock, + 'fleet-synced-integrations-ccr-*', + loggerMock + ); + expect(res).toEqual({ + custom_assets: { + 'component_template:logs-system.auth@custom': { + is_deleted: false, + error: 'No component templates found on remote index', + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '1.67.3', + sync_status: 'failed', + template: { + mappings: { + dynamic_templates: [], + properties: { + 'Test-mapping': { + type: 'text', + }, + }, + }, + }, + type: 'component_template', + }, + 'ingest_pipeline:logs-system.auth@custom': { + error: 'No custom pipelines found on remote index', + is_deleted: false, + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '1.67.3', + pipeline: { + processors: [ + { + set: { + field: 'test', + value: 'test-value', + }, + }, + ], + }, + sync_status: 'failed', + type: 'ingest_pipeline', + }, + }, + integrations: [ + { + package_name: 'system', + package_version: '1.67.3', + sync_status: 'completed', + updated_at: expect.any(String), + }, + ], + }); + }); + }); }); diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts index a4933eeb6414c..59734adb6d206 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts @@ -18,6 +18,7 @@ import semverGte from 'semver/functions/gte'; import type { CcrFollowInfoFollowerIndex, ClusterComponentTemplateSummary, + IngestGetPipelineResponse, } from '@elastic/elasticsearch/lib/api/types'; import type { PackageClient } from '../../services'; @@ -295,7 +296,7 @@ export const fetchAndCompareSyncedIntegrations = async ( }, {} as Record> ); - const customAssetsStatus = await compareCustomAssets(esClient, logger, ccrCustomAssets); + const customAssetsStatus = await fetchAndCompareCustomAssets(esClient, logger, ccrCustomAssets); const integrationsStatus = compareIntegrations(ccrIntegrations, installedIntegrationsByName); const result = { ...integrationsStatus, @@ -303,9 +304,12 @@ export const fetchAndCompareSyncedIntegrations = async ( }; return result; - } catch (err) { - logger.error('error', err.message); - throw err; + } catch (error) { + logger.error('error', error.message); + return { + integrations: [], + error, + }; } }; @@ -347,7 +351,7 @@ const compareIntegrations = ( return { integrations: integrationsStatus }; }; -const compareCustomAssets = async ( +const fetchAndCompareCustomAssets = async ( esClient: ElasticsearchClient, logger: Logger, ccrCustomAssets: { [key: string]: CustomAssetsData } @@ -372,46 +376,81 @@ const compareCustomAssets = async ( } return acc; }, {} as Record); + const componentTemplates = installedComponentTemplates?.component_templates + ? componentTemplatesByName + : undefined; const result: RemoteSyncedCustomAssetsRecord = {}; // compare custom pipelines and custom component templates Object.entries(ccrCustomAssets).forEach(([ccrCustomName, ccrCustomAsset]) => { - if (ccrCustomAsset.type === 'ingest_pipeline') { - const installedAsset = installedPipelines[ccrCustomAsset?.name]; - - if (isEqual(installedAsset, ccrCustomAsset?.pipeline)) { - result[ccrCustomName] = { - ...ccrCustomAsset, - sync_status: 'completed' as SyncStatus.COMPLETED, - } as RemoteSyncedCustomAssetsStatus; - } else { - result[ccrCustomName] = { - ...ccrCustomAsset, - sync_status: 'failed' as SyncStatus.FAILED, - } as RemoteSyncedCustomAssetsStatus; - } - } - if (ccrCustomAsset.type === 'component_template') { - const installedAsset = componentTemplatesByName[ccrCustomAsset?.name]; - - if (isEqual(installedAsset, ccrCustomAsset?.template)) { - result[ccrCustomName] = { - ...ccrCustomAsset, - sync_status: 'completed' as SyncStatus.COMPLETED, - } as RemoteSyncedCustomAssetsStatus; - } else { - result[ccrCustomName] = { - ...ccrCustomAsset, - sync_status: 'failed' as SyncStatus.FAILED, - } as RemoteSyncedCustomAssetsStatus; - } - } + const res = compareCustomAssets({ + ccrCustomAsset, + ingestPipelines: installedPipelines, + componentTemplates, + }); + result[ccrCustomName] = res; }); return result; - } catch (err) { - logger.error('error', err.message); - throw err; + } catch (error) { + logger.error('error', error.message); + return { error }; + } +}; + +const compareCustomAssets = ({ + ccrCustomAsset, + ingestPipelines, + componentTemplates, +}: { + ccrCustomAsset: CustomAssetsData; + ingestPipelines?: IngestGetPipelineResponse; + componentTemplates?: Record; +}): RemoteSyncedCustomAssetsStatus => { + if (ccrCustomAsset.type === 'ingest_pipeline') { + if (!ingestPipelines) { + return { + ...ccrCustomAsset, + sync_status: 'failed' as SyncStatus.FAILED, + error: `No custom pipelines found on remote index`, + }; + } + + const installedAsset = ingestPipelines[ccrCustomAsset?.name]; + + if (isEqual(installedAsset, ccrCustomAsset?.pipeline)) { + return { + ...ccrCustomAsset, + sync_status: 'completed' as SyncStatus.COMPLETED, + }; + } else { + return { + ...ccrCustomAsset, + sync_status: 'synchronizing' as SyncStatus.SYNCHRONIZING, + }; + } + } else if (ccrCustomAsset.type === 'component_template') { + if (!componentTemplates) { + return { + ...ccrCustomAsset, + sync_status: 'failed' as SyncStatus.FAILED, + error: `No component templates found on remote index`, + }; + } + + const installedAsset = componentTemplates[ccrCustomAsset?.name]; + if (isEqual(installedAsset, ccrCustomAsset?.template)) { + return { + ...ccrCustomAsset, + sync_status: 'completed' as SyncStatus.COMPLETED, + }; + } else { + return { + ...ccrCustomAsset, + sync_status: 'synchronizing' as SyncStatus.SYNCHRONIZING, + }; + } } + return {} as RemoteSyncedCustomAssetsStatus; }; export const getRemoteSyncedIntegrationsStatus = async ( From 73d2a9e4f5345670933a9fb5a25f2357077651e1 Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Thu, 3 Apr 2025 12:01:35 +0200 Subject: [PATCH 12/26] Add tests and address code review comments --- .../models/remote_synced_integrations.ts | 2 +- .../sync_integrations_on_remote.test.ts | 90 +++++++++++++++++-- .../sync_integrations_on_remote.ts | 22 +++-- 3 files changed, 96 insertions(+), 18 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/common/types/models/remote_synced_integrations.ts b/x-pack/platform/plugins/shared/fleet/common/types/models/remote_synced_integrations.ts index 97eedef3e1aac..b498670dfac74 100644 --- a/x-pack/platform/plugins/shared/fleet/common/types/models/remote_synced_integrations.ts +++ b/x-pack/platform/plugins/shared/fleet/common/types/models/remote_synced_integrations.ts @@ -28,5 +28,5 @@ export interface RemoteSyncedCustomAssetsStatus extends CustomAssetsData { } export interface RemoteSyncedCustomAssetsRecord { - [key: string]: RemoteSyncedCustomAssetsStatus; + [key: string]: RemoteSyncedCustomAssetsStatus | string; } diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.test.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.test.ts index c6e4e71d95950..5866685115fce 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.test.ts @@ -1098,7 +1098,7 @@ describe('fetchAndCompareSyncedIntegrations', () => { }); }); - it('should return error if no custom assets are installed', async () => { + it('should return synchronizing status if no custom assets are found installed', async () => { esClientMock = { search: searchMockWithCustomAssets, }; @@ -1129,11 +1129,10 @@ describe('fetchAndCompareSyncedIntegrations', () => { custom_assets: { 'component_template:logs-system.auth@custom': { is_deleted: false, - error: 'No component templates found on remote index', name: 'logs-system.auth@custom', package_name: 'system', package_version: '1.67.3', - sync_status: 'failed', + sync_status: 'synchronizing', template: { mappings: { dynamic_templates: [], @@ -1147,7 +1146,6 @@ describe('fetchAndCompareSyncedIntegrations', () => { type: 'component_template', }, 'ingest_pipeline:logs-system.auth@custom': { - error: 'No custom pipelines found on remote index', is_deleted: false, name: 'logs-system.auth@custom', package_name: 'system', @@ -1162,7 +1160,7 @@ describe('fetchAndCompareSyncedIntegrations', () => { }, ], }, - sync_status: 'failed', + sync_status: 'synchronizing', type: 'ingest_pipeline', }, }, @@ -1176,5 +1174,87 @@ describe('fetchAndCompareSyncedIntegrations', () => { ], }); }); + it('should return error in the top level if there is any error in the function', async () => { + esClientMock = { + search: jest.fn().mockRejectedValueOnce(new Error('Some es error')), + }; + + const res = await fetchAndCompareSyncedIntegrations( + esClientMock, + soClientMock, + 'fleet-synced-integrations-ccr-*', + loggerMock + ); + expect(res).toEqual({ + error: 'Some es error', + integrations: [], + }); + }); + + it('should return error when custom assets throws error', async () => { + (getPipelineMock as jest.MockedFunction).mockRejectedValueOnce( + new Error('Error in getPipeline') + ); + (getComponentTemplateMock as jest.MockedFunction).mockResolvedValue({ + component_templates: [ + { + name: 'logs-system.auth@custom', + component_template: { + template: { + mappings: { + dynamic_templates: [], + properties: { + 'Test-mapping': { + type: 'text', + }, + }, + }, + }, + }, + }, + ], + }); + + esClientMock = { + search: searchMockWithCustomAssets, + }; + (getPackageSavedObjects as jest.MockedFunction).mockReturnValue({ + page: 1, + per_page: 10000, + total: 1, + saved_objects: [ + { + type: 'epm-packages', + id: 'system', + attributes: { + version: '1.67.3', + install_status: 'installed', + }, + updated_at: '2025-03-26T14:06:27.611Z', + }, + ], + }); + + const res = await fetchAndCompareSyncedIntegrations( + esClientMock, + soClientMock, + 'fleet-synced-integrations-ccr-*', + loggerMock + ); + + expect(res).toEqual({ + integrations: [ + { + package_name: 'system', + package_version: '1.67.3', + sync_status: 'completed', + updated_at: expect.any(String), + }, + ], + custom_assets: { + error: 'Error in getPipeline', + }, + }); + }); }); }); diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts index 59734adb6d206..acc41a11549d9 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts @@ -255,7 +255,7 @@ export const fetchAndCompareSyncedIntegrations = async ( savedObjectsClient: SavedObjectsClientContract, index: string, logger: Logger -) => { +): Promise => { try { // find integrations on ccr index const searchRes = await esClient.search({ @@ -305,10 +305,10 @@ export const fetchAndCompareSyncedIntegrations = async ( return result; } catch (error) { - logger.error('error', error.message); + logger.error('error', error?.message); return { integrations: [], - error, + error: error?.message, }; } }; @@ -316,7 +316,7 @@ export const fetchAndCompareSyncedIntegrations = async ( const compareIntegrations = ( ccrIntegrations: IntegrationsData[], installedIntegrationsByName: Record> -) => { +): { integrations: RemoteSyncedIntegrationsStatus[] } => { const integrationsStatus: RemoteSyncedIntegrationsStatus[] | undefined = ccrIntegrations?.map( (ccrIntegration) => { const localIntegrationSO = installedIntegrationsByName[ccrIntegration.package_name]; @@ -355,8 +355,8 @@ const fetchAndCompareCustomAssets = async ( esClient: ElasticsearchClient, logger: Logger, ccrCustomAssets: { [key: string]: CustomAssetsData } -) => { - if (!ccrCustomAssets) return; +): Promise => { + if (!ccrCustomAssets) return {}; const abortController = new AbortController(); @@ -392,8 +392,8 @@ const fetchAndCompareCustomAssets = async ( }); return result; } catch (error) { - logger.error('error', error.message); - return { error }; + logger.error('error', error?.message); + return { error: error?.message }; } }; @@ -410,8 +410,7 @@ const compareCustomAssets = ({ if (!ingestPipelines) { return { ...ccrCustomAsset, - sync_status: 'failed' as SyncStatus.FAILED, - error: `No custom pipelines found on remote index`, + sync_status: 'synchronizing' as SyncStatus.SYNCHRONIZING, }; } @@ -432,8 +431,7 @@ const compareCustomAssets = ({ if (!componentTemplates) { return { ...ccrCustomAsset, - sync_status: 'failed' as SyncStatus.FAILED, - error: `No component templates found on remote index`, + sync_status: 'synchronizing' as SyncStatus.SYNCHRONIZING, }; } From 17d615f92cc6e3584770c43c7bbd745d81fdc2b5 Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Thu, 3 Apr 2025 12:10:35 +0200 Subject: [PATCH 13/26] Move functions to its own file --- .../compare_synced_integrations.test.ts | 820 ++++++++++++++++++ .../compare_synced_integrations.ts | 295 +++++++ .../sync_integrations_on_remote.test.ts | 804 +---------------- .../sync_integrations_on_remote.ts | 298 +------ 4 files changed, 1123 insertions(+), 1094 deletions(-) create mode 100644 x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts create mode 100644 x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.ts diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts new file mode 100644 index 0000000000000..c93cac41b7a5b --- /dev/null +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts @@ -0,0 +1,820 @@ +/* + * 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 { savedObjectsClientMock } from '@kbn/core/server/mocks'; + +import { getPackageSavedObjects } from '../../services/epm/packages/get'; + +import { installCustomAsset, getPipeline, getComponentTemplate } from './custom_assets'; +import { + getFollowerIndexInfo, + fetchAndCompareSyncedIntegrations, +} from './compare_synced_integrations'; + +jest.mock('../../services'); +jest.mock('./custom_assets', () => { + return { getPipeline: jest.fn(), getComponentTemplate: jest.fn(), installCustomAsset: jest.fn() }; +}); +jest.mock('../../services/epm/packages/get', () => { + return { + ...jest.requireActual('../../services/epm/packages/get'), + getPackageSavedObjects: jest.fn(), + }; +}); + +const getPipelineMock = getPipeline as jest.Mocked; +const getComponentTemplateMock = getComponentTemplate as jest.Mocked; + +describe('getFollowerIndexInfo', () => { + let esClientMock: any; + let getIndicesMock: jest.Mock; + let searchMock: jest.Mock; + let loggerMock: any; + + beforeEach(() => { + getIndicesMock = jest.fn(); + searchMock = jest.fn(); + esClientMock = { + indices: { + get: getIndicesMock, + }, + search: searchMock, + ccr: { followInfo: jest.fn() }, + }; + + loggerMock = { + debug: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + }; + (installCustomAsset as jest.Mock).mockClear(); + getIndicesMock.mockResolvedValue({ + 'fleet-synced-integrations-ccr-remote1': {}, + }); + }); + + it('should return error if follower index is not available', async () => { + expect(await getFollowerIndexInfo(esClientMock, loggerMock)).toEqual({ + error: 'Follower index fleet-synced-integrations-ccr-remote1 not available', + }); + }); + + it('should return error if follower index returns empty array', async () => { + esClientMock.ccr.followInfo.mockResolvedValue({ + follower_indices: [], + } as any); + + expect(await getFollowerIndexInfo(esClientMock, loggerMock)).toEqual({ + error: 'Follower index fleet-synced-integrations-ccr-remote1 not available', + }); + }); + + it('should return info about follower index', async () => { + esClientMock.ccr.followInfo.mockResolvedValue({ + follower_indices: [ + { + follower_index: 'fleet-synced-integrations-ccr-remote1', + remote_cluster: 'Main', + leader_index: 'fleet-synced-integrations', + status: 'active', + parameters: { + max_read_request_operation_count: 5120, + max_write_request_operation_count: 5120, + max_outstanding_read_requests: 12, + max_outstanding_write_requests: 9, + max_read_request_size: '32mb', + max_write_request_size: '9223372036854775807b', + max_write_buffer_count: 2147483647, + max_write_buffer_size: '512mb', + max_retry_delay: '500ms', + read_poll_timeout: '1m', + }, + }, + ], + } as any); + const info = await getFollowerIndexInfo(esClientMock, loggerMock); + expect(info).toEqual({ + info: { + follower_index: 'fleet-synced-integrations-ccr-remote1', + remote_cluster: 'Main', + leader_index: 'fleet-synced-integrations', + status: 'active', + parameters: { + max_read_request_operation_count: 5120, + max_write_request_operation_count: 5120, + max_outstanding_read_requests: 12, + max_outstanding_write_requests: 9, + max_read_request_size: '32mb', + max_write_request_size: '9223372036854775807b', + max_write_buffer_count: 2147483647, + max_write_buffer_size: '512mb', + max_retry_delay: '500ms', + read_poll_timeout: '1m', + }, + }, + }); + }); + + it('should return error if follower is paused', async () => { + esClientMock.ccr.followInfo.mockResolvedValue({ + follower_indices: [ + { + follower_index: 'fleet-synced-integrations-ccr-remote1', + remote_cluster: 'Main', + leader_index: 'fleet-synced-integrations', + status: 'paused', + parameters: {}, + }, + ], + } as any); + const info = await getFollowerIndexInfo(esClientMock, loggerMock); + expect(info).toEqual({ + error: 'Follower index fleet-synced-integrations-ccr-remote1 paused', + }); + }); +}); + +describe('fetchAndCompareSyncedIntegrations', () => { + let esClientMock: any; + let getIndicesMock: jest.Mock; + let searchMock: jest.Mock; + const soClientMock = savedObjectsClientMock.create(); + + let loggerMock: any; + + beforeEach(() => { + getIndicesMock = jest.fn(); + searchMock = jest.fn(); + esClientMock = { + indices: { + get: getIndicesMock, + }, + search: searchMock, + }; + + loggerMock = { + debug: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + }; + (installCustomAsset as jest.Mock).mockClear(); + }); + + it('should return error if no integrations are found', async () => { + esClientMock.search.mockResolvedValueOnce({ + hits: { + hits: [], + }, + } as any); + const res = await fetchAndCompareSyncedIntegrations( + esClientMock, + soClientMock, + 'fleet-synced-integrations-ccr-*', + loggerMock + ); + expect(res).toEqual({ + error: 'No integrations found on fleet-synced-integrations-ccr-*', + integrations: [], + }); + }); + + it('should return error if there are integrations on sync index but none are installed', async () => { + esClientMock.search.mockResolvedValueOnce({ + hits: { + hits: [ + { + _source: { + integrations: [ + { + package_name: 'elastic_agent', + package_version: '2.2.0', + updated_at: '2025-03-20T14:18:40.076Z', + }, + { + package_name: 'system', + package_version: '1.67.3', + updated_at: '2025-03-20T14:18:40.111Z', + }, + ], + }, + }, + ], + }, + } as any); + const res = await fetchAndCompareSyncedIntegrations( + esClientMock, + soClientMock, + 'fleet-synced-integrations-ccr-*', + loggerMock + ); + expect(res).toEqual({ + error: 'No integrations installed on remote', + integrations: [], + }); + }); + it('should compare integrations installed on remote with the ones on sync index', async () => { + esClientMock.search.mockResolvedValueOnce({ + hits: { + hits: [ + { + _source: { + integrations: [ + { + package_name: 'elastic_agent', + package_version: '2.2.0', + updated_at: '2025-03-20T14:18:40.076Z', + }, + { + package_name: 'system', + package_version: '1.67.3', + updated_at: '2025-03-20T14:18:40.111Z', + }, + ], + }, + }, + ], + }, + } as any); + (getPackageSavedObjects as jest.MockedFunction).mockReturnValue({ + page: 1, + per_page: 10000, + total: 1, + saved_objects: [ + { + type: 'epm-packages', + id: 'elastic_agent', + attributes: { + version: '2.2.0', + install_status: 'installed', + }, + + updated_at: '2025-03-26T14:06:27.611Z', + }, + { + type: 'epm-packages', + id: 'system', + attributes: { + version: '1.67.3', + install_status: 'installed', + }, + + updated_at: '2025-03-26T14:06:27.611Z', + }, + ], + }); + const res = await fetchAndCompareSyncedIntegrations( + esClientMock, + soClientMock, + 'fleet-synced-integrations-ccr-*', + loggerMock + ); + expect(res).toEqual({ + integrations: [ + { + package_name: 'elastic_agent', + package_version: '2.2.0', + sync_status: 'completed', + updated_at: expect.any(String), + }, + { + package_name: 'system', + package_version: '1.67.3', + sync_status: 'completed', + updated_at: expect.any(String), + }, + ], + }); + }); + + it('should return errors for single integrations to cover for different cases', async () => { + esClientMock.search.mockResolvedValueOnce({ + hits: { + hits: [ + { + _source: { + integrations: [ + { + package_name: 'elastic_agent', + package_version: '2.2.0', + updated_at: '2025-03-20T14:18:40.076Z', + }, + { + package_name: 'system', + package_version: '1.67.3', + updated_at: '2025-03-20T14:18:40.111Z', + }, + { + package_name: 'synthetics', + package_version: '1.4.1', + updated_at: '2025-03-17T15:21:14.092Z', + }, + ], + }, + }, + ], + }, + } as any); + (getPackageSavedObjects as jest.MockedFunction).mockReturnValue({ + page: 1, + per_page: 10000, + total: 2, + saved_objects: [ + { + type: 'epm-packages', + id: 'system', + attributes: { + version: '1.67.2', + install_status: 'installed', + }, + updated_at: '2025-03-26T14:06:27.611Z', + }, + { + type: 'epm-packages', + id: 'synthetics', + attributes: { + version: '1.4.1', + install_status: 'not_installed', + }, + updated_at: '2025-03-26T14:06:27.611Z', + }, + ], + }); + const res = await fetchAndCompareSyncedIntegrations( + esClientMock, + soClientMock, + 'fleet-synced-integrations-ccr-*', + loggerMock + ); + expect(res).toEqual({ + integrations: [ + { + error: 'Installation not found on remote', + package_name: 'elastic_agent', + package_version: '2.2.0', + sync_status: 'failed', + updated_at: expect.any(String), + }, + { + package_name: 'system', + package_version: '1.67.3', + sync_status: 'failed', + updated_at: expect.any(String), + error: 'Found incorrect installed version 1.67.2', + }, + { + error: 'Installation status: not_installed', + package_name: 'synthetics', + package_version: '1.4.1', + sync_status: 'failed', + updated_at: expect.any(String), + }, + ], + }); + }); + + describe('compare custom integrations', () => { + let searchMockWithCustomAssets: any; + + afterEach(() => { + jest.resetAllMocks(); + }); + beforeEach(() => { + searchMockWithCustomAssets = jest.fn().mockResolvedValue({ + hits: { + hits: [ + { + _source: { + integrations: [ + { + package_name: 'system', + package_version: '1.67.3', + updated_at: '2025-03-20T14:18:40.076Z', + }, + ], + custom_assets: { + 'component_template:logs-system.auth@custom': { + type: 'component_template', + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '1.67.3', + is_deleted: false, + template: { + mappings: { + dynamic_templates: [], + properties: { + 'Test-mapping': { + type: 'text', + }, + }, + }, + }, + }, + 'ingest_pipeline:logs-system.auth@custom': { + type: 'ingest_pipeline', + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '1.67.3', + is_deleted: false, + pipeline: { + processors: [ + { + set: { + field: 'test', + value: 'test-value', + }, + }, + ], + }, + }, + }, + }, + }, + ], + }, + }); + }); + + it('should return synchronizing state if the assets are different', async () => { + (getPipelineMock as jest.MockedFunction).mockResolvedValue({ + 'logs-system.auth@custom': { + processors: [ + { + user_agent: { + field: 'user_agent', + }, + }, + ], + }, + }); + (getComponentTemplateMock as jest.MockedFunction).mockResolvedValue({ + component_templates: [ + { + name: 'logs-system.auth@custom', + component_template: { + template: { + mappings: { + properties: { + new_field: { + type: 'text', + }, + }, + }, + }, + }, + }, + ], + }); + + esClientMock = { + search: searchMockWithCustomAssets, + }; + (getPackageSavedObjects as jest.MockedFunction).mockReturnValue({ + page: 1, + per_page: 10000, + total: 1, + saved_objects: [ + { + type: 'epm-packages', + id: 'system', + attributes: { + version: '1.67.3', + install_status: 'installed', + }, + updated_at: '2025-03-26T14:06:27.611Z', + }, + ], + }); + + const res = await fetchAndCompareSyncedIntegrations( + esClientMock, + soClientMock, + 'fleet-synced-integrations-ccr-*', + loggerMock + ); + + expect(res).toEqual({ + integrations: [ + { + package_name: 'system', + package_version: '1.67.3', + sync_status: 'completed', + updated_at: expect.any(String), + }, + ], + custom_assets: { + 'component_template:logs-system.auth@custom': { + is_deleted: false, + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '1.67.3', + sync_status: 'synchronizing', + template: { + mappings: { + dynamic_templates: [], + properties: { + 'Test-mapping': { + type: 'text', + }, + }, + }, + }, + type: 'component_template', + }, + 'ingest_pipeline:logs-system.auth@custom': { + is_deleted: false, + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '1.67.3', + pipeline: { + processors: [ + { + set: { + field: 'test', + value: 'test-value', + }, + }, + ], + }, + sync_status: 'synchronizing', + type: 'ingest_pipeline', + }, + }, + }); + }); + + it('should return completed if custom assets are equal', async () => { + (getPipelineMock as jest.MockedFunction).mockResolvedValue({ + 'logs-system.auth@custom': { + processors: [ + { + set: { + field: 'test', + value: 'test-value', + }, + }, + ], + }, + }); + (getComponentTemplateMock as jest.MockedFunction).mockResolvedValue({ + component_templates: [ + { + name: 'logs-system.auth@custom', + component_template: { + template: { + mappings: { + dynamic_templates: [], + properties: { + 'Test-mapping': { + type: 'text', + }, + }, + }, + }, + }, + }, + ], + }); + + esClientMock = { + search: searchMockWithCustomAssets, + }; + (getPackageSavedObjects as jest.MockedFunction).mockReturnValue({ + page: 1, + per_page: 10000, + total: 1, + saved_objects: [ + { + type: 'epm-packages', + id: 'system', + attributes: { + version: '1.67.3', + install_status: 'installed', + }, + updated_at: '2025-03-26T14:06:27.611Z', + }, + ], + }); + + const res = await fetchAndCompareSyncedIntegrations( + esClientMock, + soClientMock, + 'fleet-synced-integrations-ccr-*', + loggerMock + ); + + expect(res).toEqual({ + integrations: [ + { + package_name: 'system', + package_version: '1.67.3', + sync_status: 'completed', + updated_at: expect.any(String), + }, + ], + custom_assets: { + 'component_template:logs-system.auth@custom': { + is_deleted: false, + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '1.67.3', + sync_status: 'completed', + template: { + mappings: { + dynamic_templates: [], + properties: { + 'Test-mapping': { + type: 'text', + }, + }, + }, + }, + type: 'component_template', + }, + 'ingest_pipeline:logs-system.auth@custom': { + is_deleted: false, + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '1.67.3', + pipeline: { + processors: [ + { + set: { + field: 'test', + value: 'test-value', + }, + }, + ], + }, + sync_status: 'completed', + type: 'ingest_pipeline', + }, + }, + }); + }); + + it('should return synchronizing status if no custom assets are found installed', async () => { + esClientMock = { + search: searchMockWithCustomAssets, + }; + (getPackageSavedObjects as jest.MockedFunction).mockReturnValue({ + page: 1, + per_page: 10000, + total: 1, + saved_objects: [ + { + type: 'epm-packages', + id: 'system', + attributes: { + version: '1.67.3', + install_status: 'installed', + }, + updated_at: '2025-03-26T14:06:27.611Z', + }, + ], + }); + + const res = await fetchAndCompareSyncedIntegrations( + esClientMock, + soClientMock, + 'fleet-synced-integrations-ccr-*', + loggerMock + ); + expect(res).toEqual({ + custom_assets: { + 'component_template:logs-system.auth@custom': { + is_deleted: false, + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '1.67.3', + sync_status: 'synchronizing', + template: { + mappings: { + dynamic_templates: [], + properties: { + 'Test-mapping': { + type: 'text', + }, + }, + }, + }, + type: 'component_template', + }, + 'ingest_pipeline:logs-system.auth@custom': { + is_deleted: false, + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '1.67.3', + pipeline: { + processors: [ + { + set: { + field: 'test', + value: 'test-value', + }, + }, + ], + }, + sync_status: 'synchronizing', + type: 'ingest_pipeline', + }, + }, + integrations: [ + { + package_name: 'system', + package_version: '1.67.3', + sync_status: 'completed', + updated_at: expect.any(String), + }, + ], + }); + }); + it('should return error in the top level if there is any error in the function', async () => { + esClientMock = { + search: jest.fn().mockRejectedValueOnce(new Error('Some es error')), + }; + + const res = await fetchAndCompareSyncedIntegrations( + esClientMock, + soClientMock, + 'fleet-synced-integrations-ccr-*', + loggerMock + ); + expect(res).toEqual({ + error: 'Some es error', + integrations: [], + }); + }); + + it('should return error when custom assets throws error', async () => { + (getPipelineMock as jest.MockedFunction).mockRejectedValueOnce( + new Error('Error in getPipeline') + ); + (getComponentTemplateMock as jest.MockedFunction).mockResolvedValue({ + component_templates: [ + { + name: 'logs-system.auth@custom', + component_template: { + template: { + mappings: { + dynamic_templates: [], + properties: { + 'Test-mapping': { + type: 'text', + }, + }, + }, + }, + }, + }, + ], + }); + + esClientMock = { + search: searchMockWithCustomAssets, + }; + (getPackageSavedObjects as jest.MockedFunction).mockReturnValue({ + page: 1, + per_page: 10000, + total: 1, + saved_objects: [ + { + type: 'epm-packages', + id: 'system', + attributes: { + version: '1.67.3', + install_status: 'installed', + }, + updated_at: '2025-03-26T14:06:27.611Z', + }, + ], + }); + + const res = await fetchAndCompareSyncedIntegrations( + esClientMock, + soClientMock, + 'fleet-synced-integrations-ccr-*', + loggerMock + ); + + expect(res).toEqual({ + integrations: [ + { + package_name: 'system', + package_version: '1.67.3', + sync_status: 'completed', + updated_at: expect.any(String), + }, + ], + custom_assets: { + error: 'Error in getPipeline', + }, + }); + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.ts new file mode 100644 index 0000000000000..9a5e0455e7297 --- /dev/null +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.ts @@ -0,0 +1,295 @@ +/* + * 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 { isEqual } from 'lodash'; +import type { + ElasticsearchClient, + Logger, + SavedObjectsClientContract, + SavedObjectsFindResult, +} from '@kbn/core/server'; + +import type { + CcrFollowInfoFollowerIndex, + ClusterComponentTemplateSummary, + IngestGetPipelineResponse, +} from '@elastic/elasticsearch/lib/api/types'; + +import { appContextService } from '../../services'; +import { getPackageSavedObjects } from '../../services/epm/packages/get'; + +import { IndexNotFoundError } from '../../errors'; + +import type { Installation } from '../../types'; + +import type { + RemoteSyncedIntegrationsStatus, + GetRemoteSyncedIntegrationsStatusResponse, + SyncStatus, + RemoteSyncedCustomAssetsStatus, + RemoteSyncedCustomAssetsRecord, +} from '../../../common/types'; + +import type { IntegrationsData, SyncIntegrationsData, CustomAssetsData } from './model'; +import { getPipeline, getComponentTemplate, CUSTOM_ASSETS_PREFIX } from './custom_assets'; +import { getFollowerIndex } from './sync_integrations_on_remote'; + +export const getFollowerIndexInfo = async ( + esClient: ElasticsearchClient, + logger: Logger +): Promise<{ info?: CcrFollowInfoFollowerIndex; error?: string }> => { + try { + const index = await getFollowerIndex(esClient, new AbortController()); + if (!index) { + return { error: `Follower index not found` }; + } + const res = await esClient.ccr.followInfo({ + index, + }); + if (!res?.follower_indices || res.follower_indices.length === 0) + return { error: `Follower index ${index} not available` }; + + if (res.follower_indices[0]?.status === 'paused') { + return { error: `Follower index ${index} paused` }; + } + return { info: res.follower_indices[0] }; + } catch (err) { + if (err?.body?.error?.type === 'index_not_found_exception') + throw new IndexNotFoundError(`Index not found`); + + logger.error('error', err.message); + throw err; + } +}; + +export const fetchAndCompareSyncedIntegrations = async ( + esClient: ElasticsearchClient, + savedObjectsClient: SavedObjectsClientContract, + index: string, + logger: Logger +): Promise => { + try { + // find integrations on ccr index + const searchRes = await esClient.search({ + index, + sort: [ + { + 'integrations.updated_at': { + order: 'desc', + }, + }, + ], + }); + if (searchRes.hits.hits[0]?._source === undefined) { + return { + integrations: [], + error: `No integrations found on ${index}`, + }; + } + const ccrIndex = searchRes.hits.hits[0]?._source; + const { integrations: ccrIntegrations, custom_assets: ccrCustomAssets } = ccrIndex; + + // find integrations installed on remote + const installedIntegrations = await getPackageSavedObjects(savedObjectsClient); + + if (!installedIntegrations && ccrIntegrations.length > 0) { + return { + integrations: [], + error: `No integrations installed on remote`, + }; + } + + const installedIntegrationsByName = (installedIntegrations?.saved_objects || []).reduce( + (acc, integration) => { + if (integration?.id) { + acc[integration.id] = integration; + } + return acc; + }, + {} as Record> + ); + const customAssetsStatus = await fetchAndCompareCustomAssets(esClient, logger, ccrCustomAssets); + const integrationsStatus = compareIntegrations(ccrIntegrations, installedIntegrationsByName); + const result = { + ...integrationsStatus, + ...(customAssetsStatus && { custom_assets: customAssetsStatus }), + }; + + return result; + } catch (error) { + logger.error('error', error?.message); + return { + integrations: [], + error: error?.message, + }; + } +}; + +const compareIntegrations = ( + ccrIntegrations: IntegrationsData[], + installedIntegrationsByName: Record> +): { integrations: RemoteSyncedIntegrationsStatus[] } => { + const integrationsStatus: RemoteSyncedIntegrationsStatus[] | undefined = ccrIntegrations?.map( + (ccrIntegration) => { + const localIntegrationSO = installedIntegrationsByName[ccrIntegration.package_name]; + if (!localIntegrationSO) { + return { + ...ccrIntegration, + sync_status: 'failed' as SyncStatus.FAILED, + error: `Installation not found on remote`, + }; + } + if (ccrIntegration.package_version !== localIntegrationSO?.attributes.version) { + return { + ...ccrIntegration, + sync_status: 'failed' as SyncStatus.FAILED, + error: `Found incorrect installed version ${localIntegrationSO?.attributes.version}`, + }; + } + if (localIntegrationSO?.attributes.install_status !== 'installed') { + return { + ...ccrIntegration, + sync_status: 'failed' as SyncStatus.FAILED, + error: `Installation status: ${localIntegrationSO?.attributes.install_status}`, + }; + } + return { + ...ccrIntegration, + sync_status: 'completed' as SyncStatus.COMPLETED, + updated_at: localIntegrationSO?.updated_at, + }; + } + ); + return { integrations: integrationsStatus }; +}; + +const fetchAndCompareCustomAssets = async ( + esClient: ElasticsearchClient, + logger: Logger, + ccrCustomAssets: { [key: string]: CustomAssetsData } +): Promise => { + if (!ccrCustomAssets) return; + + const abortController = new AbortController(); + + try { + const installedPipelines = await getPipeline(esClient, CUSTOM_ASSETS_PREFIX, abortController); + const installedComponentTemplates = await getComponentTemplate( + esClient, + CUSTOM_ASSETS_PREFIX, + abortController + ); + + const componentTemplatesByName = ( + installedComponentTemplates?.component_templates || [] + ).reduce((acc, componentTemplate) => { + if (componentTemplate?.name) { + acc[componentTemplate.name] = componentTemplate.component_template?.template; + } + return acc; + }, {} as Record); + const componentTemplates = installedComponentTemplates?.component_templates + ? componentTemplatesByName + : undefined; + const result: RemoteSyncedCustomAssetsRecord = {}; + + // compare custom pipelines and custom component templates + Object.entries(ccrCustomAssets).forEach(([ccrCustomName, ccrCustomAsset]) => { + const res = compareCustomAssets({ + ccrCustomAsset, + ingestPipelines: installedPipelines, + componentTemplates, + }); + result[ccrCustomName] = res; + }); + return result; + } catch (error) { + logger.error('error', error?.message); + return { error: error?.message }; + } +}; + +const compareCustomAssets = ({ + ccrCustomAsset, + ingestPipelines, + componentTemplates, +}: { + ccrCustomAsset: CustomAssetsData; + ingestPipelines?: IngestGetPipelineResponse; + componentTemplates?: Record; +}): RemoteSyncedCustomAssetsStatus => { + if (ccrCustomAsset.type === 'ingest_pipeline') { + if (!ingestPipelines) { + return { + ...ccrCustomAsset, + sync_status: 'synchronizing' as SyncStatus.SYNCHRONIZING, + }; + } + + const installedAsset = ingestPipelines[ccrCustomAsset?.name]; + + if (isEqual(installedAsset, ccrCustomAsset?.pipeline)) { + return { + ...ccrCustomAsset, + sync_status: 'completed' as SyncStatus.COMPLETED, + }; + } else { + return { + ...ccrCustomAsset, + sync_status: 'synchronizing' as SyncStatus.SYNCHRONIZING, + }; + } + } else if (ccrCustomAsset.type === 'component_template') { + if (!componentTemplates) { + return { + ...ccrCustomAsset, + sync_status: 'synchronizing' as SyncStatus.SYNCHRONIZING, + }; + } + + const installedAsset = componentTemplates[ccrCustomAsset?.name]; + if (isEqual(installedAsset, ccrCustomAsset?.template)) { + return { + ...ccrCustomAsset, + sync_status: 'completed' as SyncStatus.COMPLETED, + }; + } else { + return { + ...ccrCustomAsset, + sync_status: 'synchronizing' as SyncStatus.SYNCHRONIZING, + }; + } + } + return {} as RemoteSyncedCustomAssetsStatus; +}; + +export const getRemoteSyncedIntegrationsStatus = async ( + esClient: ElasticsearchClient, + soClient: SavedObjectsClientContract +): Promise => { + const { enableSyncIntegrationsOnRemote } = appContextService.getExperimentalFeatures(); + const logger = appContextService.getLogger(); + + if (!enableSyncIntegrationsOnRemote) { + return { integrations: [] }; + } + + try { + const followerIndexRes = await getFollowerIndexInfo(esClient, logger); + if (followerIndexRes?.error || !followerIndexRes?.info) { + return { error: followerIndexRes?.error, integrations: [] }; + } + const res = await fetchAndCompareSyncedIntegrations( + esClient, + soClient, + followerIndexRes.info.follower_index, + logger + ); + return res; + } catch (err) { + throw err; + } +}; diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.test.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.test.ts index 5866685115fce..e622df9281d0e 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.test.ts @@ -5,19 +5,11 @@ * 2.0. */ -import { savedObjectsClientMock } from '@kbn/core/server/mocks'; - import { PackageNotFoundError } from '../../errors'; import { outputService } from '../../services'; -import { getPackageSavedObjects } from '../../services/epm/packages/get'; - -import { installCustomAsset, getPipeline, getComponentTemplate } from './custom_assets'; -import { - syncIntegrationsOnRemote, - getFollowerIndexInfo, - fetchAndCompareSyncedIntegrations, -} from './sync_integrations_on_remote'; +import { installCustomAsset } from './custom_assets'; +import { syncIntegrationsOnRemote } from './sync_integrations_on_remote'; jest.mock('../../services'); jest.mock('./custom_assets', () => { @@ -31,8 +23,6 @@ jest.mock('../../services/epm/packages/get', () => { }); const outputServiceMock = outputService as jest.Mocked; -const getPipelineMock = getPipeline as jest.Mocked; -const getComponentTemplateMock = getComponentTemplate as jest.Mocked; describe('syncIntegrationsOnRemote', () => { const abortController = new AbortController(); @@ -468,793 +458,3 @@ describe('syncIntegrationsOnRemote', () => { expect(installCustomAsset).toHaveBeenCalledTimes(2); }); }); - -describe('getFollowerIndexInfo', () => { - let esClientMock: any; - let getIndicesMock: jest.Mock; - let searchMock: jest.Mock; - let loggerMock: any; - - beforeEach(() => { - getIndicesMock = jest.fn(); - searchMock = jest.fn(); - esClientMock = { - indices: { - get: getIndicesMock, - }, - search: searchMock, - ccr: { followInfo: jest.fn() }, - }; - - loggerMock = { - debug: jest.fn(), - error: jest.fn(), - warn: jest.fn(), - info: jest.fn(), - }; - (installCustomAsset as jest.Mock).mockClear(); - getIndicesMock.mockResolvedValue({ - 'fleet-synced-integrations-ccr-remote1': {}, - }); - }); - - it('should return error if follower index is not available', async () => { - expect(await getFollowerIndexInfo(esClientMock, loggerMock)).toEqual({ - error: 'Follower index fleet-synced-integrations-ccr-remote1 not available', - }); - }); - - it('should return error if follower index returns empty array', async () => { - esClientMock.ccr.followInfo.mockResolvedValue({ - follower_indices: [], - } as any); - - expect(await getFollowerIndexInfo(esClientMock, loggerMock)).toEqual({ - error: 'Follower index fleet-synced-integrations-ccr-remote1 not available', - }); - }); - - it('should return info about follower index', async () => { - esClientMock.ccr.followInfo.mockResolvedValue({ - follower_indices: [ - { - follower_index: 'fleet-synced-integrations-ccr-remote1', - remote_cluster: 'Main', - leader_index: 'fleet-synced-integrations', - status: 'active', - parameters: { - max_read_request_operation_count: 5120, - max_write_request_operation_count: 5120, - max_outstanding_read_requests: 12, - max_outstanding_write_requests: 9, - max_read_request_size: '32mb', - max_write_request_size: '9223372036854775807b', - max_write_buffer_count: 2147483647, - max_write_buffer_size: '512mb', - max_retry_delay: '500ms', - read_poll_timeout: '1m', - }, - }, - ], - } as any); - const info = await getFollowerIndexInfo(esClientMock, loggerMock); - expect(info).toEqual({ - info: { - follower_index: 'fleet-synced-integrations-ccr-remote1', - remote_cluster: 'Main', - leader_index: 'fleet-synced-integrations', - status: 'active', - parameters: { - max_read_request_operation_count: 5120, - max_write_request_operation_count: 5120, - max_outstanding_read_requests: 12, - max_outstanding_write_requests: 9, - max_read_request_size: '32mb', - max_write_request_size: '9223372036854775807b', - max_write_buffer_count: 2147483647, - max_write_buffer_size: '512mb', - max_retry_delay: '500ms', - read_poll_timeout: '1m', - }, - }, - }); - }); - - it('should return error if follower is paused', async () => { - esClientMock.ccr.followInfo.mockResolvedValue({ - follower_indices: [ - { - follower_index: 'fleet-synced-integrations-ccr-remote1', - remote_cluster: 'Main', - leader_index: 'fleet-synced-integrations', - status: 'paused', - parameters: {}, - }, - ], - } as any); - const info = await getFollowerIndexInfo(esClientMock, loggerMock); - expect(info).toEqual({ - error: 'Follower index fleet-synced-integrations-ccr-remote1 paused', - }); - }); -}); - -describe('fetchAndCompareSyncedIntegrations', () => { - let esClientMock: any; - let getIndicesMock: jest.Mock; - let searchMock: jest.Mock; - const soClientMock = savedObjectsClientMock.create(); - - let loggerMock: any; - - beforeEach(() => { - getIndicesMock = jest.fn(); - searchMock = jest.fn(); - esClientMock = { - indices: { - get: getIndicesMock, - }, - search: searchMock, - }; - - loggerMock = { - debug: jest.fn(), - error: jest.fn(), - warn: jest.fn(), - info: jest.fn(), - }; - (installCustomAsset as jest.Mock).mockClear(); - }); - - it('should return error if no integrations are found', async () => { - esClientMock.search.mockResolvedValueOnce({ - hits: { - hits: [], - }, - } as any); - const res = await fetchAndCompareSyncedIntegrations( - esClientMock, - soClientMock, - 'fleet-synced-integrations-ccr-*', - loggerMock - ); - expect(res).toEqual({ - error: 'No integrations found on fleet-synced-integrations-ccr-*', - integrations: [], - }); - }); - - it('should return error if there are integrations on sync index but none are installed', async () => { - esClientMock.search.mockResolvedValueOnce({ - hits: { - hits: [ - { - _source: { - integrations: [ - { - package_name: 'elastic_agent', - package_version: '2.2.0', - updated_at: '2025-03-20T14:18:40.076Z', - }, - { - package_name: 'system', - package_version: '1.67.3', - updated_at: '2025-03-20T14:18:40.111Z', - }, - ], - }, - }, - ], - }, - } as any); - const res = await fetchAndCompareSyncedIntegrations( - esClientMock, - soClientMock, - 'fleet-synced-integrations-ccr-*', - loggerMock - ); - expect(res).toEqual({ - error: 'No integrations installed on remote', - integrations: [], - }); - }); - it('should compare integrations installed on remote with the ones on sync index', async () => { - esClientMock.search.mockResolvedValueOnce({ - hits: { - hits: [ - { - _source: { - integrations: [ - { - package_name: 'elastic_agent', - package_version: '2.2.0', - updated_at: '2025-03-20T14:18:40.076Z', - }, - { - package_name: 'system', - package_version: '1.67.3', - updated_at: '2025-03-20T14:18:40.111Z', - }, - ], - }, - }, - ], - }, - } as any); - (getPackageSavedObjects as jest.MockedFunction).mockReturnValue({ - page: 1, - per_page: 10000, - total: 1, - saved_objects: [ - { - type: 'epm-packages', - id: 'elastic_agent', - attributes: { - version: '2.2.0', - install_status: 'installed', - }, - - updated_at: '2025-03-26T14:06:27.611Z', - }, - { - type: 'epm-packages', - id: 'system', - attributes: { - version: '1.67.3', - install_status: 'installed', - }, - - updated_at: '2025-03-26T14:06:27.611Z', - }, - ], - }); - const res = await fetchAndCompareSyncedIntegrations( - esClientMock, - soClientMock, - 'fleet-synced-integrations-ccr-*', - loggerMock - ); - expect(res).toEqual({ - integrations: [ - { - package_name: 'elastic_agent', - package_version: '2.2.0', - sync_status: 'completed', - updated_at: expect.any(String), - }, - { - package_name: 'system', - package_version: '1.67.3', - sync_status: 'completed', - updated_at: expect.any(String), - }, - ], - }); - }); - - it('should return errors for single integrations to cover for different cases', async () => { - esClientMock.search.mockResolvedValueOnce({ - hits: { - hits: [ - { - _source: { - integrations: [ - { - package_name: 'elastic_agent', - package_version: '2.2.0', - updated_at: '2025-03-20T14:18:40.076Z', - }, - { - package_name: 'system', - package_version: '1.67.3', - updated_at: '2025-03-20T14:18:40.111Z', - }, - { - package_name: 'synthetics', - package_version: '1.4.1', - updated_at: '2025-03-17T15:21:14.092Z', - }, - ], - }, - }, - ], - }, - } as any); - (getPackageSavedObjects as jest.MockedFunction).mockReturnValue({ - page: 1, - per_page: 10000, - total: 2, - saved_objects: [ - { - type: 'epm-packages', - id: 'system', - attributes: { - version: '1.67.2', - install_status: 'installed', - }, - updated_at: '2025-03-26T14:06:27.611Z', - }, - { - type: 'epm-packages', - id: 'synthetics', - attributes: { - version: '1.4.1', - install_status: 'not_installed', - }, - updated_at: '2025-03-26T14:06:27.611Z', - }, - ], - }); - const res = await fetchAndCompareSyncedIntegrations( - esClientMock, - soClientMock, - 'fleet-synced-integrations-ccr-*', - loggerMock - ); - expect(res).toEqual({ - integrations: [ - { - error: 'Installation not found on remote', - package_name: 'elastic_agent', - package_version: '2.2.0', - sync_status: 'failed', - updated_at: expect.any(String), - }, - { - package_name: 'system', - package_version: '1.67.3', - sync_status: 'failed', - updated_at: expect.any(String), - error: 'Found incorrect installed version 1.67.2', - }, - { - error: 'Installation status: not_installed', - package_name: 'synthetics', - package_version: '1.4.1', - sync_status: 'failed', - updated_at: expect.any(String), - }, - ], - }); - }); - - describe('compare custom integrations', () => { - let searchMockWithCustomAssets: any; - - afterEach(() => { - jest.resetAllMocks(); - }); - beforeEach(() => { - searchMockWithCustomAssets = jest.fn().mockResolvedValue({ - hits: { - hits: [ - { - _source: { - integrations: [ - { - package_name: 'system', - package_version: '1.67.3', - updated_at: '2025-03-20T14:18:40.076Z', - }, - ], - custom_assets: { - 'component_template:logs-system.auth@custom': { - type: 'component_template', - name: 'logs-system.auth@custom', - package_name: 'system', - package_version: '1.67.3', - is_deleted: false, - template: { - mappings: { - dynamic_templates: [], - properties: { - 'Test-mapping': { - type: 'text', - }, - }, - }, - }, - }, - 'ingest_pipeline:logs-system.auth@custom': { - type: 'ingest_pipeline', - name: 'logs-system.auth@custom', - package_name: 'system', - package_version: '1.67.3', - is_deleted: false, - pipeline: { - processors: [ - { - set: { - field: 'test', - value: 'test-value', - }, - }, - ], - }, - }, - }, - }, - }, - ], - }, - }); - }); - - it('should return synchronizing state if the assets are different', async () => { - (getPipelineMock as jest.MockedFunction).mockResolvedValue({ - 'logs-system.auth@custom': { - processors: [ - { - user_agent: { - field: 'user_agent', - }, - }, - ], - }, - }); - (getComponentTemplateMock as jest.MockedFunction).mockResolvedValue({ - component_templates: [ - { - name: 'logs-system.auth@custom', - component_template: { - template: { - mappings: { - properties: { - new_field: { - type: 'text', - }, - }, - }, - }, - }, - }, - ], - }); - - esClientMock = { - search: searchMockWithCustomAssets, - }; - (getPackageSavedObjects as jest.MockedFunction).mockReturnValue({ - page: 1, - per_page: 10000, - total: 1, - saved_objects: [ - { - type: 'epm-packages', - id: 'system', - attributes: { - version: '1.67.3', - install_status: 'installed', - }, - updated_at: '2025-03-26T14:06:27.611Z', - }, - ], - }); - - const res = await fetchAndCompareSyncedIntegrations( - esClientMock, - soClientMock, - 'fleet-synced-integrations-ccr-*', - loggerMock - ); - - expect(res).toEqual({ - integrations: [ - { - package_name: 'system', - package_version: '1.67.3', - sync_status: 'completed', - updated_at: expect.any(String), - }, - ], - custom_assets: { - 'component_template:logs-system.auth@custom': { - is_deleted: false, - name: 'logs-system.auth@custom', - package_name: 'system', - package_version: '1.67.3', - sync_status: 'synchronizing', - template: { - mappings: { - dynamic_templates: [], - properties: { - 'Test-mapping': { - type: 'text', - }, - }, - }, - }, - type: 'component_template', - }, - 'ingest_pipeline:logs-system.auth@custom': { - is_deleted: false, - name: 'logs-system.auth@custom', - package_name: 'system', - package_version: '1.67.3', - pipeline: { - processors: [ - { - set: { - field: 'test', - value: 'test-value', - }, - }, - ], - }, - sync_status: 'synchronizing', - type: 'ingest_pipeline', - }, - }, - }); - }); - - it('should return completed if custom assets are equal', async () => { - (getPipelineMock as jest.MockedFunction).mockResolvedValue({ - 'logs-system.auth@custom': { - processors: [ - { - set: { - field: 'test', - value: 'test-value', - }, - }, - ], - }, - }); - (getComponentTemplateMock as jest.MockedFunction).mockResolvedValue({ - component_templates: [ - { - name: 'logs-system.auth@custom', - component_template: { - template: { - mappings: { - dynamic_templates: [], - properties: { - 'Test-mapping': { - type: 'text', - }, - }, - }, - }, - }, - }, - ], - }); - - esClientMock = { - search: searchMockWithCustomAssets, - }; - (getPackageSavedObjects as jest.MockedFunction).mockReturnValue({ - page: 1, - per_page: 10000, - total: 1, - saved_objects: [ - { - type: 'epm-packages', - id: 'system', - attributes: { - version: '1.67.3', - install_status: 'installed', - }, - updated_at: '2025-03-26T14:06:27.611Z', - }, - ], - }); - - const res = await fetchAndCompareSyncedIntegrations( - esClientMock, - soClientMock, - 'fleet-synced-integrations-ccr-*', - loggerMock - ); - - expect(res).toEqual({ - integrations: [ - { - package_name: 'system', - package_version: '1.67.3', - sync_status: 'completed', - updated_at: expect.any(String), - }, - ], - custom_assets: { - 'component_template:logs-system.auth@custom': { - is_deleted: false, - name: 'logs-system.auth@custom', - package_name: 'system', - package_version: '1.67.3', - sync_status: 'completed', - template: { - mappings: { - dynamic_templates: [], - properties: { - 'Test-mapping': { - type: 'text', - }, - }, - }, - }, - type: 'component_template', - }, - 'ingest_pipeline:logs-system.auth@custom': { - is_deleted: false, - name: 'logs-system.auth@custom', - package_name: 'system', - package_version: '1.67.3', - pipeline: { - processors: [ - { - set: { - field: 'test', - value: 'test-value', - }, - }, - ], - }, - sync_status: 'completed', - type: 'ingest_pipeline', - }, - }, - }); - }); - - it('should return synchronizing status if no custom assets are found installed', async () => { - esClientMock = { - search: searchMockWithCustomAssets, - }; - (getPackageSavedObjects as jest.MockedFunction).mockReturnValue({ - page: 1, - per_page: 10000, - total: 1, - saved_objects: [ - { - type: 'epm-packages', - id: 'system', - attributes: { - version: '1.67.3', - install_status: 'installed', - }, - updated_at: '2025-03-26T14:06:27.611Z', - }, - ], - }); - - const res = await fetchAndCompareSyncedIntegrations( - esClientMock, - soClientMock, - 'fleet-synced-integrations-ccr-*', - loggerMock - ); - expect(res).toEqual({ - custom_assets: { - 'component_template:logs-system.auth@custom': { - is_deleted: false, - name: 'logs-system.auth@custom', - package_name: 'system', - package_version: '1.67.3', - sync_status: 'synchronizing', - template: { - mappings: { - dynamic_templates: [], - properties: { - 'Test-mapping': { - type: 'text', - }, - }, - }, - }, - type: 'component_template', - }, - 'ingest_pipeline:logs-system.auth@custom': { - is_deleted: false, - name: 'logs-system.auth@custom', - package_name: 'system', - package_version: '1.67.3', - pipeline: { - processors: [ - { - set: { - field: 'test', - value: 'test-value', - }, - }, - ], - }, - sync_status: 'synchronizing', - type: 'ingest_pipeline', - }, - }, - integrations: [ - { - package_name: 'system', - package_version: '1.67.3', - sync_status: 'completed', - updated_at: expect.any(String), - }, - ], - }); - }); - it('should return error in the top level if there is any error in the function', async () => { - esClientMock = { - search: jest.fn().mockRejectedValueOnce(new Error('Some es error')), - }; - - const res = await fetchAndCompareSyncedIntegrations( - esClientMock, - soClientMock, - 'fleet-synced-integrations-ccr-*', - loggerMock - ); - expect(res).toEqual({ - error: 'Some es error', - integrations: [], - }); - }); - - it('should return error when custom assets throws error', async () => { - (getPipelineMock as jest.MockedFunction).mockRejectedValueOnce( - new Error('Error in getPipeline') - ); - (getComponentTemplateMock as jest.MockedFunction).mockResolvedValue({ - component_templates: [ - { - name: 'logs-system.auth@custom', - component_template: { - template: { - mappings: { - dynamic_templates: [], - properties: { - 'Test-mapping': { - type: 'text', - }, - }, - }, - }, - }, - }, - ], - }); - - esClientMock = { - search: searchMockWithCustomAssets, - }; - (getPackageSavedObjects as jest.MockedFunction).mockReturnValue({ - page: 1, - per_page: 10000, - total: 1, - saved_objects: [ - { - type: 'epm-packages', - id: 'system', - attributes: { - version: '1.67.3', - install_status: 'installed', - }, - updated_at: '2025-03-26T14:06:27.611Z', - }, - ], - }); - - const res = await fetchAndCompareSyncedIntegrations( - esClientMock, - soClientMock, - 'fleet-synced-integrations-ccr-*', - loggerMock - ); - - expect(res).toEqual({ - integrations: [ - { - package_name: 'system', - package_version: '1.67.3', - sync_status: 'completed', - updated_at: expect.any(String), - }, - ], - custom_assets: { - error: 'Error in getPipeline', - }, - }); - }); - }); -}); diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts index acc41a11549d9..524891c27e801 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts @@ -4,52 +4,23 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { isEqual } from 'lodash'; -import type { - ElasticsearchClient, - SavedObjectsClient, - Logger, - SavedObjectsClientContract, - SavedObjectsFindResult, -} from '@kbn/core/server'; +import type { ElasticsearchClient, SavedObjectsClient, Logger } from '@kbn/core/server'; import semverGte from 'semver/functions/gte'; -import type { - CcrFollowInfoFollowerIndex, - ClusterComponentTemplateSummary, - IngestGetPipelineResponse, -} from '@elastic/elasticsearch/lib/api/types'; - import type { PackageClient } from '../../services'; -import { outputService, appContextService } from '../../services'; -import { getPackageSavedObjects } from '../../services/epm/packages/get'; +import { outputService } from '../../services'; -import { PackageNotFoundError, IndexNotFoundError } from '../../errors'; +import { PackageNotFoundError } from '../../errors'; import { FLEET_SYNCED_INTEGRATIONS_CCR_INDEX_PREFIX } from '../../services/setup/fleet_synced_integrations'; -import type { Installation } from '../../types'; - -import type { - RemoteSyncedIntegrationsStatus, - GetRemoteSyncedIntegrationsStatusResponse, - SyncStatus, - RemoteSyncedCustomAssetsStatus, - RemoteSyncedCustomAssetsRecord, -} from '../../../common/types'; - -import type { IntegrationsData, SyncIntegrationsData, CustomAssetsData } from './model'; -import { - getPipeline, - installCustomAsset, - getComponentTemplate, - CUSTOM_ASSETS_PREFIX, -} from './custom_assets'; +import type { SyncIntegrationsData } from './model'; +import { installCustomAsset } from './custom_assets'; const MAX_RETRY_ATTEMPTS = 5; const RETRY_BACKOFF_MINUTES = [5, 10, 20, 40, 60]; -const getFollowerIndex = async ( +export const getFollowerIndex = async ( esClient: ElasticsearchClient, abortController: AbortController ): Promise => { @@ -221,260 +192,3 @@ export const syncIntegrationsOnRemote = async ( } } }; - -export const getFollowerIndexInfo = async ( - esClient: ElasticsearchClient, - logger: Logger -): Promise<{ info?: CcrFollowInfoFollowerIndex; error?: string }> => { - try { - const index = await getFollowerIndex(esClient, new AbortController()); - if (!index) { - return { error: `Follower index not found` }; - } - const res = await esClient.ccr.followInfo({ - index, - }); - if (!res?.follower_indices || res.follower_indices.length === 0) - return { error: `Follower index ${index} not available` }; - - if (res.follower_indices[0]?.status === 'paused') { - return { error: `Follower index ${index} paused` }; - } - return { info: res.follower_indices[0] }; - } catch (err) { - if (err?.body?.error?.type === 'index_not_found_exception') - throw new IndexNotFoundError(`Index not found`); - - logger.error('error', err.message); - throw err; - } -}; - -export const fetchAndCompareSyncedIntegrations = async ( - esClient: ElasticsearchClient, - savedObjectsClient: SavedObjectsClientContract, - index: string, - logger: Logger -): Promise => { - try { - // find integrations on ccr index - const searchRes = await esClient.search({ - index, - sort: [ - { - 'integrations.updated_at': { - order: 'desc', - }, - }, - ], - }); - if (searchRes.hits.hits[0]?._source === undefined) { - return { - integrations: [], - error: `No integrations found on ${index}`, - }; - } - const ccrIndex = searchRes.hits.hits[0]?._source; - const { integrations: ccrIntegrations, custom_assets: ccrCustomAssets } = ccrIndex; - - // find integrations installed on remote - const installedIntegrations = await getPackageSavedObjects(savedObjectsClient); - - if (!installedIntegrations && ccrIntegrations.length > 0) { - return { - integrations: [], - error: `No integrations installed on remote`, - }; - } - - const installedIntegrationsByName = (installedIntegrations?.saved_objects || []).reduce( - (acc, integration) => { - if (integration?.id) { - acc[integration.id] = integration; - } - return acc; - }, - {} as Record> - ); - const customAssetsStatus = await fetchAndCompareCustomAssets(esClient, logger, ccrCustomAssets); - const integrationsStatus = compareIntegrations(ccrIntegrations, installedIntegrationsByName); - const result = { - ...integrationsStatus, - ...(customAssetsStatus && { custom_assets: customAssetsStatus }), - }; - - return result; - } catch (error) { - logger.error('error', error?.message); - return { - integrations: [], - error: error?.message, - }; - } -}; - -const compareIntegrations = ( - ccrIntegrations: IntegrationsData[], - installedIntegrationsByName: Record> -): { integrations: RemoteSyncedIntegrationsStatus[] } => { - const integrationsStatus: RemoteSyncedIntegrationsStatus[] | undefined = ccrIntegrations?.map( - (ccrIntegration) => { - const localIntegrationSO = installedIntegrationsByName[ccrIntegration.package_name]; - if (!localIntegrationSO) { - return { - ...ccrIntegration, - sync_status: 'failed' as SyncStatus.FAILED, - error: `Installation not found on remote`, - }; - } - if (ccrIntegration.package_version !== localIntegrationSO?.attributes.version) { - return { - ...ccrIntegration, - sync_status: 'failed' as SyncStatus.FAILED, - error: `Found incorrect installed version ${localIntegrationSO?.attributes.version}`, - }; - } - if (localIntegrationSO?.attributes.install_status !== 'installed') { - return { - ...ccrIntegration, - sync_status: 'failed' as SyncStatus.FAILED, - error: `Installation status: ${localIntegrationSO?.attributes.install_status}`, - }; - } - return { - ...ccrIntegration, - sync_status: 'completed' as SyncStatus.COMPLETED, - updated_at: localIntegrationSO?.updated_at, - }; - } - ); - return { integrations: integrationsStatus }; -}; - -const fetchAndCompareCustomAssets = async ( - esClient: ElasticsearchClient, - logger: Logger, - ccrCustomAssets: { [key: string]: CustomAssetsData } -): Promise => { - if (!ccrCustomAssets) return {}; - - const abortController = new AbortController(); - - try { - const installedPipelines = await getPipeline(esClient, CUSTOM_ASSETS_PREFIX, abortController); - const installedComponentTemplates = await getComponentTemplate( - esClient, - CUSTOM_ASSETS_PREFIX, - abortController - ); - - const componentTemplatesByName = ( - installedComponentTemplates?.component_templates || [] - ).reduce((acc, componentTemplate) => { - if (componentTemplate?.name) { - acc[componentTemplate.name] = componentTemplate.component_template?.template; - } - return acc; - }, {} as Record); - const componentTemplates = installedComponentTemplates?.component_templates - ? componentTemplatesByName - : undefined; - const result: RemoteSyncedCustomAssetsRecord = {}; - - // compare custom pipelines and custom component templates - Object.entries(ccrCustomAssets).forEach(([ccrCustomName, ccrCustomAsset]) => { - const res = compareCustomAssets({ - ccrCustomAsset, - ingestPipelines: installedPipelines, - componentTemplates, - }); - result[ccrCustomName] = res; - }); - return result; - } catch (error) { - logger.error('error', error?.message); - return { error: error?.message }; - } -}; - -const compareCustomAssets = ({ - ccrCustomAsset, - ingestPipelines, - componentTemplates, -}: { - ccrCustomAsset: CustomAssetsData; - ingestPipelines?: IngestGetPipelineResponse; - componentTemplates?: Record; -}): RemoteSyncedCustomAssetsStatus => { - if (ccrCustomAsset.type === 'ingest_pipeline') { - if (!ingestPipelines) { - return { - ...ccrCustomAsset, - sync_status: 'synchronizing' as SyncStatus.SYNCHRONIZING, - }; - } - - const installedAsset = ingestPipelines[ccrCustomAsset?.name]; - - if (isEqual(installedAsset, ccrCustomAsset?.pipeline)) { - return { - ...ccrCustomAsset, - sync_status: 'completed' as SyncStatus.COMPLETED, - }; - } else { - return { - ...ccrCustomAsset, - sync_status: 'synchronizing' as SyncStatus.SYNCHRONIZING, - }; - } - } else if (ccrCustomAsset.type === 'component_template') { - if (!componentTemplates) { - return { - ...ccrCustomAsset, - sync_status: 'synchronizing' as SyncStatus.SYNCHRONIZING, - }; - } - - const installedAsset = componentTemplates[ccrCustomAsset?.name]; - if (isEqual(installedAsset, ccrCustomAsset?.template)) { - return { - ...ccrCustomAsset, - sync_status: 'completed' as SyncStatus.COMPLETED, - }; - } else { - return { - ...ccrCustomAsset, - sync_status: 'synchronizing' as SyncStatus.SYNCHRONIZING, - }; - } - } - return {} as RemoteSyncedCustomAssetsStatus; -}; - -export const getRemoteSyncedIntegrationsStatus = async ( - esClient: ElasticsearchClient, - soClient: SavedObjectsClientContract -): Promise => { - const { enableSyncIntegrationsOnRemote } = appContextService.getExperimentalFeatures(); - const logger = appContextService.getLogger(); - - if (!enableSyncIntegrationsOnRemote) { - return { integrations: [] }; - } - - try { - const followerIndexRes = await getFollowerIndexInfo(esClient, logger); - if (followerIndexRes?.error || !followerIndexRes?.info) { - return { error: followerIndexRes?.error, integrations: [] }; - } - const res = await fetchAndCompareSyncedIntegrations( - esClient, - soClient, - followerIndexRes.info.follower_index, - logger - ); - return res; - } catch (err) { - throw err; - } -}; From aed15750a18e1115d73af9e0bd50d3fd2f0630df Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Thu, 3 Apr 2025 12:14:58 +0200 Subject: [PATCH 14/26] clean up --- .../routes/remote_synced_integrations/handler.ts | 3 +-- .../fleet/server/tasks/sync_integrations/model.ts | 1 + .../sync_integrations_on_remote.test.ts | 10 +--------- .../sync_integrations/sync_integrations_on_remote.ts | 1 - 4 files changed, 3 insertions(+), 12 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/handler.ts b/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/handler.ts index 4249144d58316..a3a6355740499 100644 --- a/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/handler.ts +++ b/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/handler.ts @@ -8,8 +8,7 @@ import type { RequestHandler } from '@kbn/core/server'; import type { GetRemoteSyncedIntegrationsStatusResponse } from '../../../common/types'; - -import { getRemoteSyncedIntegrationsStatus } from '../../tasks/sync_integrations/sync_integrations_on_remote'; +import { getRemoteSyncedIntegrationsStatus } from '../../tasks/sync_integrations/compare_synced_integrations'; export const getRemoteSyncedIntegrationsStatusHandler: RequestHandler = async ( context, diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/model.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/model.ts index 5405071f3e19b..14716101533ec 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/model.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/model.ts @@ -25,6 +25,7 @@ export interface CustomAssetsError { timestamp: string; error: string; } + export interface SyncIntegrationsData { remote_es_hosts: Array<{ name: string; diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.test.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.test.ts index e622df9281d0e..03211b1b18e74 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.test.ts @@ -9,18 +9,10 @@ import { PackageNotFoundError } from '../../errors'; import { outputService } from '../../services'; import { installCustomAsset } from './custom_assets'; + import { syncIntegrationsOnRemote } from './sync_integrations_on_remote'; jest.mock('../../services'); -jest.mock('./custom_assets', () => { - return { getPipeline: jest.fn(), getComponentTemplate: jest.fn(), installCustomAsset: jest.fn() }; -}); -jest.mock('../../services/epm/packages/get', () => { - return { - ...jest.requireActual('../../services/epm/packages/get'), - getPackageSavedObjects: jest.fn(), - }; -}); const outputServiceMock = outputService as jest.Mocked; diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts index 524891c27e801..bef795e3d6268 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.ts @@ -32,7 +32,6 @@ export const getFollowerIndex = async ( ); const indexNames = Object.keys(indices); - if (indexNames.length > 1) { throw new Error( `Not supported to sync multiple indices with prefix ${FLEET_SYNCED_INTEGRATIONS_CCR_INDEX_PREFIX}` From c9a14842e7105fd073b59119fd8acc170eb6ee9c Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Thu, 3 Apr 2025 16:13:11 +0200 Subject: [PATCH 15/26] Code review pt.1: architecture diagram, return data, version comparison --- .../models/remote_synced_integrations.ts | 4 +- .../remote_clusters_architecture.png | Bin 0 -> 114500 bytes .../local_setup/remote_clusters_ccr.md | 43 +++-- .../compare_synced_integrations.test.ts | 181 +++++++++++------- .../compare_synced_integrations.ts | 32 +++- .../server/tasks/sync_integrations/model.ts | 4 +- .../sync_integrations_on_remote.test.ts | 1 + .../types/models/synced_integrations.ts | 28 ++- 8 files changed, 186 insertions(+), 107 deletions(-) create mode 100644 x-pack/platform/plugins/shared/fleet/dev_docs/diagrams/remote_clusters/remote_clusters_architecture.png diff --git a/x-pack/platform/plugins/shared/fleet/common/types/models/remote_synced_integrations.ts b/x-pack/platform/plugins/shared/fleet/common/types/models/remote_synced_integrations.ts index b498670dfac74..a7209cd348c99 100644 --- a/x-pack/platform/plugins/shared/fleet/common/types/models/remote_synced_integrations.ts +++ b/x-pack/platform/plugins/shared/fleet/common/types/models/remote_synced_integrations.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { CustomAssetsData } from '../../../server/tasks/sync_integrations/model'; +import type { BaseCustomAssetsData } from '../../../server/tasks/sync_integrations/model'; export enum SyncStatus { SYNCHRONIZING = 'synchronizing', @@ -22,7 +22,7 @@ export interface RemoteSyncedIntegrationsStatus extends RemoteSyncedIntegrations error?: string; updated_at?: string; } -export interface RemoteSyncedCustomAssetsStatus extends CustomAssetsData { +export interface RemoteSyncedCustomAssetsStatus extends BaseCustomAssetsData { sync_status: SyncStatus; error?: string; } diff --git a/x-pack/platform/plugins/shared/fleet/dev_docs/diagrams/remote_clusters/remote_clusters_architecture.png b/x-pack/platform/plugins/shared/fleet/dev_docs/diagrams/remote_clusters/remote_clusters_architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..7f2145fbc49dc0494063a0d3ee79a591e07acbc7 GIT binary patch literal 114500 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 qZR2-2TuS6RE^XwyDk4eb$9txiUquA2IR` on Cluster B. The basic architecture is shown in the diagram below: + +![secret storage architecture](./diagrams/remote_clusters/remote_clusters_architecture.png) + +The communication between clusters happens as follows: +- Kibana on Cluster B exposes the content of the ccr index on endpoint `GET api/fleet/remote_synced_integrations/status` +- Fleet server periodically polls this endpoint and writes the response on datastream `fleet_server.output_integration_sync` - Kibana on Cluster A reads the content of the datastream and exposes the status of the integrations sync in the UI + ## Run ES and Kibana ### Changes to `kibana.dev.yml` @@ -13,7 +26,7 @@ This configuration allows to run two local ES clusters in parallel, each one having its own instance of Kibana. -### Start Cluster 1 (remote) +### Start Cluster B (remote) - Start ES 1 ``` @@ -33,7 +46,7 @@ yarn start --server.port=5701 --elasticsearch.hosts=http://localhost:9500 --dev. ``` - Login into http://localhost/5601/ -### Start Cluster 2 (main) +### Start Cluster A (main) - Start ES 2 @@ -58,7 +71,7 @@ yarn start To avoid issues, it might be needed to login to one of the kibana instances with an incognito session. -## Setup on Cluster 1 +## Setup on Cluster B - Navigate to dev tools and create service token and api key ``` @@ -83,11 +96,11 @@ POST /_security/api_key } } ``` -Save the responses as they will be required in Cluster 2 (see next section). +Save the responses as they will be required in Cluster A (see next section). ### Add Remote Cluster -- Add Remote Cluster to Cluster 1. Navigate to *Stack Management > Remote Clusters* ([link](http://localhost:5601/app/management/data/remote_clusters)) and follow the steps to add cluster 'local': +- Add a Remote Cluster to Cluster B. Navigate to *Stack Management > Remote Clusters* ([link](http://localhost:5601/app/management/data/remote_clusters)) and follow the steps to add cluster 'local': - Click "Add a remote cluster" - Choose a name, put `localhost:9300` for "Seed nodes", and save (check "Yes, I have setup trust") @@ -102,7 +115,7 @@ On cluster 1, navigate to *Stack Management > Cross-Cluster Replication* and c - Follower index `fleet-synced-integrations-ccr-remote1` ### Set up local ES output -This configuration is required to kick off the integration sync. The local host needs to match the remote ES output configured on Cluster 2 (see next section). Note that `kibana.dev.yml` is read by both kibana instances so it's better to add it in the UI to avoid conflicts. +This configuration is required to kick off the integration sync. The local host needs to match the remote ES output configured on A (see next section). Note that `kibana.dev.yml` is read by both kibana instances so it's better to add it in the UI to avoid conflicts. ``` name: 'ES output' @@ -111,10 +124,10 @@ This configuration is required to kick off the integration sync. The local host hosts: ["http://:9500"] ``` -## Setup on Cluster 2 +## Setup on Cluster A ### Add remote ES output -- Add remote ES output to Cluster 2. Insert `token` and `key` obtained from the previous commands. +- Add remote ES output to Cluster A. Insert `token` and `key` obtained from the previous commands. ``` name: 'Remote output' type: 'remote_elasticsearch' @@ -129,11 +142,11 @@ This configuration is required to kick off the integration sync. The local host ## Verify sync -With these configuration, the sync task should run every 5 minutes and the integrations installed on Cluster 1 should be the same as the ones on Cluster 2 +With these configuration, the sync task should run every 5 minutes and the integrations installed on Cluster B should be the same as the ones on Cluster A ### Check contents of sync integrations indices -- From Cluster 1, verify that the follower index is active using the [follower info API](https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-get-follow-info.html): +- From Cluster B, verify that the follower index is active using the [follower info API](https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-get-follow-info.html): ``` GET fleet-synced-integrations-ccr-remote1/_ccr/info @@ -145,20 +158,20 @@ Check content of ccr index: GET fleet-synced-integrations-ccr-remote1/_search ``` -- From Cluster 2 +- From Cluster A ``` GET fleet-synced-integrations/_search ``` ### Read data from remote with CCS -- Enroll Fleet Server to Cluster 2 +- Enroll Fleet Server to Cluster A -- Create an Agent Policy in Cluster 2 and use remote output for integrations and monitoring +- Create an Agent Policy in Cluster A and use remote output for integrations and monitoring -- Enroll an agent the above Agent policy in Cluster 2 +- Enroll an agent the above Agent policy in Cluster A -- Check data in Cluster 1 with [CCS](https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-cross-cluster-search.html) +- Check data in Cluster B with [CCS](https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-cross-cluster-search.html) ``` GET remote1:metrics-*/_search diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts index c93cac41b7a5b..c140b427993e1 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts @@ -509,38 +509,16 @@ describe('fetchAndCompareSyncedIntegrations', () => { ], custom_assets: { 'component_template:logs-system.auth@custom': { - is_deleted: false, name: 'logs-system.auth@custom', package_name: 'system', package_version: '1.67.3', sync_status: 'synchronizing', - template: { - mappings: { - dynamic_templates: [], - properties: { - 'Test-mapping': { - type: 'text', - }, - }, - }, - }, type: 'component_template', }, 'ingest_pipeline:logs-system.auth@custom': { - is_deleted: false, name: 'logs-system.auth@custom', package_name: 'system', package_version: '1.67.3', - pipeline: { - processors: [ - { - set: { - field: 'test', - value: 'test-value', - }, - }, - ], - }, sync_status: 'synchronizing', type: 'ingest_pipeline', }, @@ -548,7 +526,7 @@ describe('fetchAndCompareSyncedIntegrations', () => { }); }); - it('should return completed if custom assets are equal', async () => { + it('should return status = completed if custom assets are equal', async () => { (getPipelineMock as jest.MockedFunction).mockResolvedValue({ 'logs-system.auth@custom': { processors: [ @@ -619,40 +597,137 @@ describe('fetchAndCompareSyncedIntegrations', () => { ], custom_assets: { 'component_template:logs-system.auth@custom': { - is_deleted: false, name: 'logs-system.auth@custom', package_name: 'system', package_version: '1.67.3', sync_status: 'completed', - template: { - mappings: { - dynamic_templates: [], - properties: { - 'Test-mapping': { - type: 'text', - }, - }, - }, - }, type: 'component_template', }, 'ingest_pipeline:logs-system.auth@custom': { - is_deleted: false, name: 'logs-system.auth@custom', package_name: 'system', package_version: '1.67.3', - pipeline: { - processors: [ - { - set: { - field: 'test', - value: 'test-value', + sync_status: 'completed', + type: 'ingest_pipeline', + }, + }, + }); + }); + it('should return status = failed if versions do not match', async () => { + const searchMockWithVersionedPipeline = jest.fn().mockResolvedValue({ + hits: { + hits: [ + { + _source: { + integrations: [ + { + package_name: 'system', + package_version: '1.67.3', + updated_at: '2025-03-20T14:18:40.076Z', + }, + ], + custom_assets: { + 'ingest_pipeline:logs-system.auth@custom': { + type: 'ingest_pipeline', + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '1.67.3', + is_deleted: false, + pipeline: { + processors: [ + { + set: { + field: 'test', + value: 'test-value', + }, + }, + ], + version: 2, + }, }, }, - ], + }, }, + ], + }, + }); + (getPipelineMock as jest.MockedFunction).mockResolvedValue({ + 'logs-system.auth@custom': { + processors: [ + { + set: { + field: 'test', + value: 'test-value', + }, + }, + ], + version: 1, + }, + }); + (getComponentTemplateMock as jest.MockedFunction).mockResolvedValue({ + component_templates: [ + { + name: 'logs-system.auth@custom', + component_template: { + template: { + mappings: { + dynamic_templates: [], + properties: { + 'Test-mapping': { + type: 'text', + }, + }, + }, + }, + }, + }, + ], + }); + + esClientMock = { + search: searchMockWithVersionedPipeline, + }; + (getPackageSavedObjects as jest.MockedFunction).mockReturnValue({ + page: 1, + per_page: 10000, + total: 1, + saved_objects: [ + { + type: 'epm-packages', + id: 'system', + attributes: { + version: '1.67.3', + install_status: 'installed', + }, + updated_at: '2025-03-26T14:06:27.611Z', + }, + ], + }); + + const res = await fetchAndCompareSyncedIntegrations( + esClientMock, + soClientMock, + 'fleet-synced-integrations-ccr-*', + loggerMock + ); + + expect(res).toEqual({ + integrations: [ + { + package_name: 'system', + package_version: '1.67.3', sync_status: 'completed', + updated_at: expect.any(String), + }, + ], + custom_assets: { + 'ingest_pipeline:logs-system.auth@custom': { + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '1.67.3', + sync_status: 'failed', type: 'ingest_pipeline', + error: 'Found incorrect installed version 1', }, }, }); @@ -688,38 +763,16 @@ describe('fetchAndCompareSyncedIntegrations', () => { expect(res).toEqual({ custom_assets: { 'component_template:logs-system.auth@custom': { - is_deleted: false, name: 'logs-system.auth@custom', package_name: 'system', package_version: '1.67.3', sync_status: 'synchronizing', - template: { - mappings: { - dynamic_templates: [], - properties: { - 'Test-mapping': { - type: 'text', - }, - }, - }, - }, type: 'component_template', }, 'ingest_pipeline:logs-system.auth@custom': { - is_deleted: false, name: 'logs-system.auth@custom', package_name: 'system', package_version: '1.67.3', - pipeline: { - processors: [ - { - set: { - field: 'test', - value: 'test-value', - }, - }, - ], - }, sync_status: 'synchronizing', type: 'ingest_pipeline', }, diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.ts index 9a5e0455e7297..c0d4d3149c5bd 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.ts @@ -221,31 +221,47 @@ const compareCustomAssets = ({ ingestPipelines?: IngestGetPipelineResponse; componentTemplates?: Record; }): RemoteSyncedCustomAssetsStatus => { + const result = { + name: ccrCustomAsset.name, + type: ccrCustomAsset.type, + package_name: ccrCustomAsset.package_name, + package_version: ccrCustomAsset.package_version, + }; + if (ccrCustomAsset.type === 'ingest_pipeline') { if (!ingestPipelines) { return { - ...ccrCustomAsset, + ...result, sync_status: 'synchronizing' as SyncStatus.SYNCHRONIZING, }; } - const installedAsset = ingestPipelines[ccrCustomAsset?.name]; + const installedPipeline = ingestPipelines[ccrCustomAsset?.name]; - if (isEqual(installedAsset, ccrCustomAsset?.pipeline)) { + if (isEqual(installedPipeline, ccrCustomAsset?.pipeline)) { return { - ...ccrCustomAsset, + ...result, sync_status: 'completed' as SyncStatus.COMPLETED, }; + } else if ( + installedPipeline?.version && + installedPipeline.version < ccrCustomAsset.pipeline.version + ) { + return { + ...result, + sync_status: 'failed' as SyncStatus.FAILED, + error: `Found incorrect installed version ${installedPipeline.version}`, + }; } else { return { - ...ccrCustomAsset, + ...result, sync_status: 'synchronizing' as SyncStatus.SYNCHRONIZING, }; } } else if (ccrCustomAsset.type === 'component_template') { if (!componentTemplates) { return { - ...ccrCustomAsset, + ...result, sync_status: 'synchronizing' as SyncStatus.SYNCHRONIZING, }; } @@ -253,12 +269,12 @@ const compareCustomAssets = ({ const installedAsset = componentTemplates[ccrCustomAsset?.name]; if (isEqual(installedAsset, ccrCustomAsset?.template)) { return { - ...ccrCustomAsset, + ...result, sync_status: 'completed' as SyncStatus.COMPLETED, }; } else { return { - ...ccrCustomAsset, + ...result, sync_status: 'synchronizing' as SyncStatus.SYNCHRONIZING, }; } diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/model.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/model.ts index 14716101533ec..8eaef8dc4839b 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/model.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/model.ts @@ -11,11 +11,13 @@ export interface IntegrationsData { updated_at: string; } -export interface CustomAssetsData { +export interface BaseCustomAssetsData { type: string; name: string; package_name: string; package_version: string; +} +export interface CustomAssetsData extends BaseCustomAssetsData { is_deleted: boolean; deleted_at?: string; [key: string]: any; diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.test.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.test.ts index 03211b1b18e74..ab2a75260e3cd 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_on_remote.test.ts @@ -13,6 +13,7 @@ import { installCustomAsset } from './custom_assets'; import { syncIntegrationsOnRemote } from './sync_integrations_on_remote'; jest.mock('../../services'); +jest.mock('./custom_assets'); const outputServiceMock = outputService as jest.Mocked; diff --git a/x-pack/platform/plugins/shared/fleet/server/types/models/synced_integrations.ts b/x-pack/platform/plugins/shared/fleet/server/types/models/synced_integrations.ts index be4dbbfd4d7bd..52e227cd727b9 100644 --- a/x-pack/platform/plugins/shared/fleet/server/types/models/synced_integrations.ts +++ b/x-pack/platform/plugins/shared/fleet/server/types/models/synced_integrations.ts @@ -25,23 +25,17 @@ export const RemoteSyncedIntegrationsStatusSchema = RemoteSyncedIntegrationsBase updated_at: schema.maybe(schema.string()), }); -export const CustomAssetsDataSchema = schema - .object({ - type: schema.string(), - name: schema.string(), - package_name: schema.string(), - package_version: schema.string(), - is_deleted: schema.boolean(), - deleted_at: schema.maybe(schema.string()), - sync_status: schema.oneOf([ - schema.literal(SyncStatus.COMPLETED), - schema.literal(SyncStatus.SYNCHRONIZING), - schema.literal(SyncStatus.FAILED), - ]), - }) - .extendsDeep({ - unknowns: 'allow', - }); +export const CustomAssetsDataSchema = schema.object({ + type: schema.string(), + name: schema.string(), + package_name: schema.string(), + package_version: schema.string(), + sync_status: schema.oneOf([ + schema.literal(SyncStatus.COMPLETED), + schema.literal(SyncStatus.SYNCHRONIZING), + schema.literal(SyncStatus.FAILED), + ]), +}); export const GetRemoteSyncedIntegrationsStatusResponseSchema = schema.object({ integrations: schema.arrayOf(RemoteSyncedIntegrationsStatusSchema), From 65c6c751af83b213148f22708e10b65b57284d79 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 3 Apr 2025 14:30:26 +0000 Subject: [PATCH 16/26] [CI] Auto-commit changed files from 'node scripts/capture_oas_snapshot --include-path /api/status --include-path /api/alerting/rule/ --include-path /api/alerting/rules --include-path /api/actions --include-path /api/security/role --include-path /api/spaces --include-path /api/streams --include-path /api/fleet --include-path /api/dashboards --update' --- oas_docs/bundle.json | 9 +-------- oas_docs/bundle.serverless.json | 9 +-------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index cee87c46a2278..8b0de04581fb6 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -43651,14 +43651,8 @@ "properties": { "custom_assets": { "additionalProperties": { - "additionalProperties": true, + "additionalProperties": false, "properties": { - "deleted_at": { - "type": "string" - }, - "is_deleted": { - "type": "boolean" - }, "name": { "type": "string" }, @@ -43685,7 +43679,6 @@ "name", "package_name", "package_version", - "is_deleted", "sync_status" ], "type": "object" diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index a02bda70a90fc..30b47242519f8 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -43651,14 +43651,8 @@ "properties": { "custom_assets": { "additionalProperties": { - "additionalProperties": true, + "additionalProperties": false, "properties": { - "deleted_at": { - "type": "string" - }, - "is_deleted": { - "type": "boolean" - }, "name": { "type": "string" }, @@ -43685,7 +43679,6 @@ "name", "package_name", "package_version", - "is_deleted", "sync_status" ], "type": "object" From 8129bf4b1f7620e6c608165350815e4288ddea97 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 3 Apr 2025 14:47:30 +0000 Subject: [PATCH 17/26] [CI] Auto-commit changed files from 'make api-docs' --- oas_docs/output/kibana.serverless.yaml | 7 +------ oas_docs/output/kibana.yaml | 7 +------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index d506bd7cfca03..fcaf99bb877c5 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -37726,13 +37726,9 @@ paths: properties: custom_assets: additionalProperties: - additionalProperties: true + additionalProperties: false type: object properties: - deleted_at: - type: string - is_deleted: - type: boolean name: type: string package_name: @@ -37752,7 +37748,6 @@ paths: - name - package_name - package_version - - is_deleted - sync_status type: object error: diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 12ac7d8a4b303..7b39039298acd 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -39961,13 +39961,9 @@ paths: properties: custom_assets: additionalProperties: - additionalProperties: true + additionalProperties: false type: object properties: - deleted_at: - type: string - is_deleted: - type: boolean name: type: string package_name: @@ -39987,7 +39983,6 @@ paths: - name - package_name - package_version - - is_deleted - sync_status type: object error: From 090720453409b8dcb154f58dde7b104b6a9c4953 Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Thu, 3 Apr 2025 21:57:22 +0200 Subject: [PATCH 18/26] Code review pt.2: check is_deleted, remove wrong condition --- .../compare_synced_integrations.test.ts | 366 ++++++++++++++++-- .../compare_synced_integrations.ts | 41 +- 2 files changed, 357 insertions(+), 50 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts index c140b427993e1..b38cb9e105aba 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts @@ -184,7 +184,7 @@ describe('fetchAndCompareSyncedIntegrations', () => { }); }); - it('should return error if there are integrations on sync index but none are installed', async () => { + it('should return status = synchronizing if there are integrations on sync index but none are installed yet', async () => { esClientMock.search.mockResolvedValueOnce({ hits: { hits: [ @@ -214,8 +214,20 @@ describe('fetchAndCompareSyncedIntegrations', () => { loggerMock ); expect(res).toEqual({ - error: 'No integrations installed on remote', - integrations: [], + integrations: [ + { + sync_status: 'synchronizing', + package_name: 'elastic_agent', + package_version: '2.2.0', + updated_at: '2025-03-20T14:18:40.076Z', + }, + { + sync_status: 'synchronizing', + package_name: 'system', + package_version: '1.67.3', + updated_at: '2025-03-20T14:18:40.111Z', + }, + ], }); }); it('should compare integrations installed on remote with the ones on sync index', async () => { @@ -380,6 +392,40 @@ describe('fetchAndCompareSyncedIntegrations', () => { describe('compare custom integrations', () => { let searchMockWithCustomAssets: any; + const customComponentTemplate = { + type: 'component_template', + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '1.67.3', + is_deleted: false, + template: { + mappings: { + dynamic_templates: [], + properties: { + 'Test-mapping': { + type: 'text', + }, + }, + }, + }, + }; + const customPipeline = { + type: 'ingest_pipeline', + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '1.67.3', + is_deleted: false, + pipeline: { + processors: [ + { + set: { + field: 'test', + value: 'test-value', + }, + }, + ], + }, + }; afterEach(() => { jest.resetAllMocks(); @@ -398,40 +444,8 @@ describe('fetchAndCompareSyncedIntegrations', () => { }, ], custom_assets: { - 'component_template:logs-system.auth@custom': { - type: 'component_template', - name: 'logs-system.auth@custom', - package_name: 'system', - package_version: '1.67.3', - is_deleted: false, - template: { - mappings: { - dynamic_templates: [], - properties: { - 'Test-mapping': { - type: 'text', - }, - }, - }, - }, - }, - 'ingest_pipeline:logs-system.auth@custom': { - type: 'ingest_pipeline', - name: 'logs-system.auth@custom', - package_name: 'system', - package_version: '1.67.3', - is_deleted: false, - pipeline: { - processors: [ - { - set: { - field: 'test', - value: 'test-value', - }, - }, - ], - }, - }, + 'component_template:logs-system.auth@custom': customComponentTemplate, + 'ingest_pipeline:logs-system.auth@custom': customPipeline, }, }, }, @@ -787,6 +801,284 @@ describe('fetchAndCompareSyncedIntegrations', () => { ], }); }); + + it('should return completed status if is_deleted = true and the assets are not installed', async () => { + const searchMockWithDeletedAssets = jest.fn().mockResolvedValue({ + hits: { + hits: [ + { + _source: { + integrations: [ + { + package_name: 'system', + package_version: '1.67.3', + updated_at: '2025-03-20T14:18:40.076Z', + }, + ], + custom_assets: { + 'component_template:logs-system.auth@custom': { + ...customComponentTemplate, + is_deleted: true, + }, + 'ingest_pipeline:logs-system.auth@custom': { + ...customPipeline, + is_deleted: true, + }, + }, + }, + }, + ], + }, + }); + esClientMock = { + search: searchMockWithDeletedAssets, + }; + (getPackageSavedObjects as jest.MockedFunction).mockReturnValue({ + page: 1, + per_page: 10000, + total: 1, + saved_objects: [ + { + type: 'epm-packages', + id: 'system', + attributes: { + version: '1.67.3', + install_status: 'installed', + }, + updated_at: '2025-03-26T14:06:27.611Z', + }, + ], + }); + + const res = await fetchAndCompareSyncedIntegrations( + esClientMock, + soClientMock, + 'fleet-synced-integrations-ccr-*', + loggerMock + ); + expect(res).toEqual({ + custom_assets: { + 'component_template:logs-system.auth@custom': { + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '1.67.3', + sync_status: 'completed', + type: 'component_template', + }, + 'ingest_pipeline:logs-system.auth@custom': { + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '1.67.3', + sync_status: 'completed', + type: 'ingest_pipeline', + }, + }, + integrations: [ + { + package_name: 'system', + package_version: '1.67.3', + sync_status: 'completed', + updated_at: expect.any(String), + }, + ], + }); + }); + it('should return synchronizing status if assets are not installed and is_deleted === false', async () => { + const searchMockWithDeletedAssets = jest.fn().mockResolvedValue({ + hits: { + hits: [ + { + _source: { + integrations: [ + { + package_name: 'system', + package_version: '1.67.3', + updated_at: '2025-03-20T14:18:40.076Z', + }, + ], + custom_assets: { + 'component_template:logs-system.auth@custom': { + ...customComponentTemplate, + is_deleted: false, + }, + 'ingest_pipeline:logs-system.auth@custom': { + ...customPipeline, + is_deleted: false, + }, + }, + }, + }, + ], + }, + }); + esClientMock = { + search: searchMockWithDeletedAssets, + }; + (getPackageSavedObjects as jest.MockedFunction).mockReturnValue({ + page: 1, + per_page: 10000, + total: 1, + saved_objects: [ + { + type: 'epm-packages', + id: 'system', + attributes: { + version: '1.67.3', + install_status: 'installed', + }, + updated_at: '2025-03-26T14:06:27.611Z', + }, + ], + }); + + const res = await fetchAndCompareSyncedIntegrations( + esClientMock, + soClientMock, + 'fleet-synced-integrations-ccr-*', + loggerMock + ); + expect(res).toEqual({ + custom_assets: { + 'component_template:logs-system.auth@custom': { + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '1.67.3', + sync_status: 'synchronizing', + type: 'component_template', + }, + 'ingest_pipeline:logs-system.auth@custom': { + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '1.67.3', + sync_status: 'synchronizing', + type: 'ingest_pipeline', + }, + }, + integrations: [ + { + package_name: 'system', + package_version: '1.67.3', + sync_status: 'completed', + updated_at: expect.any(String), + }, + ], + }); + }); + + it('should return failed status if is_deleted = true but the assets are still installed', async () => { + const searchMockWithDeletedAssets = jest.fn().mockResolvedValue({ + hits: { + hits: [ + { + _source: { + integrations: [ + { + package_name: 'system', + package_version: '1.67.3', + updated_at: '2025-03-20T14:18:40.076Z', + }, + ], + custom_assets: { + 'component_template:logs-system.auth@custom': { + ...customComponentTemplate, + is_deleted: true, + }, + 'ingest_pipeline:logs-system.auth@custom': { + ...customPipeline, + is_deleted: true, + }, + }, + }, + }, + ], + }, + }); + esClientMock = { + search: searchMockWithDeletedAssets, + }; + (getPipelineMock as jest.MockedFunction).mockResolvedValue({ + 'logs-system.auth@custom': { + processors: [ + { + set: { + field: 'test', + value: 'test-value', + }, + }, + ], + }, + }); + (getComponentTemplateMock as jest.MockedFunction).mockResolvedValue({ + component_templates: [ + { + name: 'logs-system.auth@custom', + component_template: { + template: { + mappings: { + dynamic_templates: [], + properties: { + 'Test-mapping': { + type: 'text', + }, + }, + }, + }, + }, + }, + ], + }); + (getPackageSavedObjects as jest.MockedFunction).mockReturnValue({ + page: 1, + per_page: 10000, + total: 1, + saved_objects: [ + { + type: 'epm-packages', + id: 'system', + attributes: { + version: '1.67.3', + install_status: 'installed', + }, + updated_at: '2025-03-26T14:06:27.611Z', + }, + ], + }); + + const res = await fetchAndCompareSyncedIntegrations( + esClientMock, + soClientMock, + 'fleet-synced-integrations-ccr-*', + loggerMock + ); + expect(res).toEqual({ + custom_assets: { + 'component_template:logs-system.auth@custom': { + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '1.67.3', + sync_status: 'failed', + error: 'Asset logs-system.auth@custom marked for deletion found installed', + type: 'component_template', + }, + 'ingest_pipeline:logs-system.auth@custom': { + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '1.67.3', + sync_status: 'failed', + error: 'Asset logs-system.auth@custom marked for deletion found installed', + type: 'ingest_pipeline', + }, + }, + integrations: [ + { + package_name: 'system', + package_version: '1.67.3', + sync_status: 'completed', + updated_at: expect.any(String), + }, + ], + }); + }); it('should return error in the top level if there is any error in the function', async () => { esClientMock = { search: jest.fn().mockRejectedValueOnce(new Error('Some es error')), diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.ts index c0d4d3149c5bd..22f76cb1750e8 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.ts @@ -95,13 +95,6 @@ export const fetchAndCompareSyncedIntegrations = async ( // find integrations installed on remote const installedIntegrations = await getPackageSavedObjects(savedObjectsClient); - if (!installedIntegrations && ccrIntegrations.length > 0) { - return { - integrations: [], - error: `No integrations installed on remote`, - }; - } - const installedIntegrationsByName = (installedIntegrations?.saved_objects || []).reduce( (acc, integration) => { if (integration?.id) { @@ -138,8 +131,7 @@ const compareIntegrations = ( if (!localIntegrationSO) { return { ...ccrIntegration, - sync_status: 'failed' as SyncStatus.FAILED, - error: `Installation not found on remote`, + sync_status: 'synchronizing' as SyncStatus.SYNCHRONIZING, }; } if (ccrIntegration.package_version !== localIntegrationSO?.attributes.version) { @@ -230,6 +222,12 @@ const compareCustomAssets = ({ if (ccrCustomAsset.type === 'ingest_pipeline') { if (!ingestPipelines) { + if (ccrCustomAsset.is_deleted === true) { + return { + ...result, + sync_status: 'completed' as SyncStatus.COMPLETED, + }; + } return { ...result, sync_status: 'synchronizing' as SyncStatus.SYNCHRONIZING, @@ -237,8 +235,13 @@ const compareCustomAssets = ({ } const installedPipeline = ingestPipelines[ccrCustomAsset?.name]; - - if (isEqual(installedPipeline, ccrCustomAsset?.pipeline)) { + if (ccrCustomAsset.is_deleted === true && installedPipeline) { + return { + ...result, + sync_status: 'failed' as SyncStatus.FAILED, + error: `Asset ${ccrCustomAsset.name} marked for deletion found installed`, + }; + } else if (isEqual(installedPipeline, ccrCustomAsset?.pipeline)) { return { ...result, sync_status: 'completed' as SyncStatus.COMPLETED, @@ -260,14 +263,26 @@ const compareCustomAssets = ({ } } else if (ccrCustomAsset.type === 'component_template') { if (!componentTemplates) { + if (ccrCustomAsset.is_deleted === true) { + return { + ...result, + sync_status: 'completed' as SyncStatus.COMPLETED, + }; + } return { ...result, sync_status: 'synchronizing' as SyncStatus.SYNCHRONIZING, }; } - const installedAsset = componentTemplates[ccrCustomAsset?.name]; - if (isEqual(installedAsset, ccrCustomAsset?.template)) { + const installedCompTemplate = componentTemplates[ccrCustomAsset?.name]; + if (ccrCustomAsset.is_deleted === true && installedCompTemplate) { + return { + ...result, + sync_status: 'failed' as SyncStatus.FAILED, + error: `Asset ${ccrCustomAsset.name} marked for deletion found installed`, + }; + } else if (isEqual(installedCompTemplate, ccrCustomAsset?.template)) { return { ...result, sync_status: 'completed' as SyncStatus.COMPLETED, From 7d6e86595f5ec314e5836ee614c857fae4c92600 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 4 Apr 2025 06:41:22 +0000 Subject: [PATCH 19/26] [CI] Auto-commit changed files from 'node scripts/capture_oas_snapshot --include-path /api/status --include-path /api/alerting/rule/ --include-path /api/alerting/rules --include-path /api/actions --include-path /api/security/role --include-path /api/spaces --include-path /api/streams --include-path /api/fleet --include-path /api/dashboards --update' --- oas_docs/bundle.json | 2 +- oas_docs/bundle.serverless.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index d701aee2e051b..053e495632e32 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -43639,7 +43639,7 @@ }, "/api/fleet/remote_synced_integrations/status": { "get": { - "description": "[Required authorization] Route required privileges: ALL of [fleet-settings-read, integrations-read].", + "description": "[Required authorization] Route required privileges: fleet-settings-read AND integrations-read.", "operationId": "get-fleet-remote-synced-integrations-status", "parameters": [], "responses": { diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index 2f70f9bface77..9b6572ca6f6dd 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -43639,7 +43639,7 @@ }, "/api/fleet/remote_synced_integrations/status": { "get": { - "description": "[Required authorization] Route required privileges: ALL of [fleet-settings-read, integrations-read].", + "description": "[Required authorization] Route required privileges: fleet-settings-read AND integrations-read.", "operationId": "get-fleet-remote-synced-integrations-status", "parameters": [], "responses": { From 4519bd65d53eb7089295c24e6d568db0cdbd5e4b Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 4 Apr 2025 06:58:48 +0000 Subject: [PATCH 20/26] [CI] Auto-commit changed files from 'make api-docs' --- oas_docs/output/kibana.serverless.yaml | 2 +- oas_docs/output/kibana.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index f2f96992c6ba8..01aa32cbd71e2 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -37713,7 +37713,7 @@ paths: - Fleet proxies /api/fleet/remote_synced_integrations/status: get: - description: '[Required authorization] Route required privileges: ALL of [fleet-settings-read, integrations-read].' + description: '[Required authorization] Route required privileges: fleet-settings-read AND integrations-read.' operationId: get-fleet-remote-synced-integrations-status parameters: [] responses: diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index f76640c681203..dce423e8e1454 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -39948,7 +39948,7 @@ paths: - Fleet proxies /api/fleet/remote_synced_integrations/status: get: - description: '[Required authorization] Route required privileges: ALL of [fleet-settings-read, integrations-read].' + description: '[Required authorization] Route required privileges: fleet-settings-read AND integrations-read.' operationId: get-fleet-remote-synced-integrations-status parameters: [] responses: From c18648623349fc27ff892925b2174ec86269cfa9 Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Fri, 4 Apr 2025 11:02:53 +0200 Subject: [PATCH 21/26] Fix unit test --- .../compare_synced_integrations.test.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts index b38cb9e105aba..b41929cf72b1e 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts @@ -304,7 +304,7 @@ describe('fetchAndCompareSyncedIntegrations', () => { }); }); - it('should return errors for single integrations to cover for different cases', async () => { + it('should return errors for single integrations', async () => { esClientMock.search.mockResolvedValueOnce({ hits: { hits: [ @@ -337,12 +337,21 @@ describe('fetchAndCompareSyncedIntegrations', () => { per_page: 10000, total: 2, saved_objects: [ + { + package_name: 'elastic_agent', + package_version: '2.2.0', + updated_at: '2025-03-20T14:18:40.076Z', + attributes: { + version: '2.2.0', + install_status: 'installing', + }, + }, { type: 'epm-packages', id: 'system', attributes: { version: '1.67.2', - install_status: 'installed', + install_status: 'install', }, updated_at: '2025-03-26T14:06:27.611Z', }, @@ -366,10 +375,9 @@ describe('fetchAndCompareSyncedIntegrations', () => { expect(res).toEqual({ integrations: [ { - error: 'Installation not found on remote', package_name: 'elastic_agent', package_version: '2.2.0', - sync_status: 'failed', + sync_status: 'synchronizing', updated_at: expect.any(String), }, { From 2b4cb2e1b4f1bfa21bf50deb7b8b586e6beabbe5 Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Fri, 4 Apr 2025 12:10:13 +0200 Subject: [PATCH 22/26] Update docs and diagram --- .../remote_clusters_architecture.png | Bin 114500 -> 0 bytes .../remote_clusters_diagram.png | Bin 0 -> 55951 bytes .../local_setup/remote_clusters_ccr.md | 6 +++--- 3 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 x-pack/platform/plugins/shared/fleet/dev_docs/diagrams/remote_clusters/remote_clusters_architecture.png create mode 100644 x-pack/platform/plugins/shared/fleet/dev_docs/diagrams/remote_clusters/remote_clusters_diagram.png diff --git a/x-pack/platform/plugins/shared/fleet/dev_docs/diagrams/remote_clusters/remote_clusters_architecture.png b/x-pack/platform/plugins/shared/fleet/dev_docs/diagrams/remote_clusters/remote_clusters_architecture.png deleted file mode 100644 index 7f2145fbc49dc0494063a0d3ee79a591e07acbc7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 114500 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 Ive~Qu4` on Cluster B. The basic architecture is shown in the diagram below: -![secret storage architecture](./diagrams/remote_clusters/remote_clusters_architecture.png) +![secret storage architecture](./diagrams/remote_clusters/remote_clusters_diagram.png) The communication between clusters happens as follows: -- Kibana on Cluster B exposes the content of the ccr index on endpoint `GET api/fleet/remote_synced_integrations/status` -- Fleet server periodically polls this endpoint and writes the response on datastream `fleet_server.output_integration_sync` - Kibana on Cluster A reads the content of the datastream and exposes the status of the integrations sync in the UI +- Kibana on Cluster B exposes the content of the ccr index on endpoints `GET api/fleet/remote_synced_integrations/status` and `/api/fleet/remote_synced_integrations/{outputId}/status` +- Kibana on Cluster A reads polls these endpoints and exposes the status of the integrations sync in the UI ## Run ES and Kibana From fb1eb39db9bfc788d2217fae7689d911cd5fa6c5 Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Mon, 7 Apr 2025 12:16:25 +0200 Subject: [PATCH 23/26] Address code review comments pt.3, add more tests --- .../compare_synced_integrations.test.ts | 159 +++++++++++++----- .../compare_synced_integrations.ts | 25 ++- 2 files changed, 133 insertions(+), 51 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts index b41929cf72b1e..5512e9d3a4bdf 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts @@ -7,12 +7,18 @@ import { savedObjectsClientMock } from '@kbn/core/server/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; +import type { Logger } from '@kbn/core/server'; + import { getPackageSavedObjects } from '../../services/epm/packages/get'; +import { appContextService } from '../../services'; + import { installCustomAsset, getPipeline, getComponentTemplate } from './custom_assets'; import { getFollowerIndexInfo, fetchAndCompareSyncedIntegrations, + getRemoteSyncedIntegrationsStatus, } from './compare_synced_integrations'; jest.mock('../../services'); @@ -25,15 +31,16 @@ jest.mock('../../services/epm/packages/get', () => { getPackageSavedObjects: jest.fn(), }; }); +const mockedAppContextService = appContextService as jest.Mocked; const getPipelineMock = getPipeline as jest.Mocked; const getComponentTemplateMock = getComponentTemplate as jest.Mocked; +let mockedLogger: jest.Mocked; describe('getFollowerIndexInfo', () => { let esClientMock: any; let getIndicesMock: jest.Mock; let searchMock: jest.Mock; - let loggerMock: any; beforeEach(() => { getIndicesMock = jest.fn(); @@ -46,20 +53,19 @@ describe('getFollowerIndexInfo', () => { ccr: { followInfo: jest.fn() }, }; - loggerMock = { - debug: jest.fn(), - error: jest.fn(), - warn: jest.fn(), - info: jest.fn(), - }; + mockedLogger = loggerMock.create(); + mockedAppContextService.getLogger.mockReturnValue(mockedLogger); (installCustomAsset as jest.Mock).mockClear(); getIndicesMock.mockResolvedValue({ 'fleet-synced-integrations-ccr-remote1': {}, }); }); + afterEach(() => { + jest.resetAllMocks(); + }); it('should return error if follower index is not available', async () => { - expect(await getFollowerIndexInfo(esClientMock, loggerMock)).toEqual({ + expect(await getFollowerIndexInfo(esClientMock, mockedLogger)).toEqual({ error: 'Follower index fleet-synced-integrations-ccr-remote1 not available', }); }); @@ -69,7 +75,7 @@ describe('getFollowerIndexInfo', () => { follower_indices: [], } as any); - expect(await getFollowerIndexInfo(esClientMock, loggerMock)).toEqual({ + expect(await getFollowerIndexInfo(esClientMock, mockedLogger)).toEqual({ error: 'Follower index fleet-synced-integrations-ccr-remote1 not available', }); }); @@ -97,7 +103,7 @@ describe('getFollowerIndexInfo', () => { }, ], } as any); - const info = await getFollowerIndexInfo(esClientMock, loggerMock); + const info = await getFollowerIndexInfo(esClientMock, mockedLogger); expect(info).toEqual({ info: { follower_index: 'fleet-synced-integrations-ccr-remote1', @@ -132,7 +138,7 @@ describe('getFollowerIndexInfo', () => { }, ], } as any); - const info = await getFollowerIndexInfo(esClientMock, loggerMock); + const info = await getFollowerIndexInfo(esClientMock, mockedLogger); expect(info).toEqual({ error: 'Follower index fleet-synced-integrations-ccr-remote1 paused', }); @@ -145,8 +151,6 @@ describe('fetchAndCompareSyncedIntegrations', () => { let searchMock: jest.Mock; const soClientMock = savedObjectsClientMock.create(); - let loggerMock: any; - beforeEach(() => { getIndicesMock = jest.fn(); searchMock = jest.fn(); @@ -156,14 +160,12 @@ describe('fetchAndCompareSyncedIntegrations', () => { }, search: searchMock, }; - - loggerMock = { - debug: jest.fn(), - error: jest.fn(), - warn: jest.fn(), - info: jest.fn(), - }; (installCustomAsset as jest.Mock).mockClear(); + mockedLogger = loggerMock.create(); + mockedAppContextService.getLogger.mockReturnValue(mockedLogger); + }); + afterEach(() => { + jest.resetAllMocks(); }); it('should return error if no integrations are found', async () => { @@ -176,7 +178,7 @@ describe('fetchAndCompareSyncedIntegrations', () => { esClientMock, soClientMock, 'fleet-synced-integrations-ccr-*', - loggerMock + mockedLogger ); expect(res).toEqual({ error: 'No integrations found on fleet-synced-integrations-ccr-*', @@ -211,7 +213,7 @@ describe('fetchAndCompareSyncedIntegrations', () => { esClientMock, soClientMock, 'fleet-synced-integrations-ccr-*', - loggerMock + mockedLogger ); expect(res).toEqual({ integrations: [ @@ -284,7 +286,7 @@ describe('fetchAndCompareSyncedIntegrations', () => { esClientMock, soClientMock, 'fleet-synced-integrations-ccr-*', - loggerMock + mockedLogger ); expect(res).toEqual({ integrations: [ @@ -370,7 +372,7 @@ describe('fetchAndCompareSyncedIntegrations', () => { esClientMock, soClientMock, 'fleet-synced-integrations-ccr-*', - loggerMock + mockedLogger ); expect(res).toEqual({ integrations: [ @@ -517,7 +519,7 @@ describe('fetchAndCompareSyncedIntegrations', () => { esClientMock, soClientMock, 'fleet-synced-integrations-ccr-*', - loggerMock + mockedLogger ); expect(res).toEqual({ @@ -605,7 +607,7 @@ describe('fetchAndCompareSyncedIntegrations', () => { esClientMock, soClientMock, 'fleet-synced-integrations-ccr-*', - loggerMock + mockedLogger ); expect(res).toEqual({ @@ -730,7 +732,7 @@ describe('fetchAndCompareSyncedIntegrations', () => { esClientMock, soClientMock, 'fleet-synced-integrations-ccr-*', - loggerMock + mockedLogger ); expect(res).toEqual({ @@ -780,7 +782,7 @@ describe('fetchAndCompareSyncedIntegrations', () => { esClientMock, soClientMock, 'fleet-synced-integrations-ccr-*', - loggerMock + mockedLogger ); expect(res).toEqual({ custom_assets: { @@ -862,7 +864,7 @@ describe('fetchAndCompareSyncedIntegrations', () => { esClientMock, soClientMock, 'fleet-synced-integrations-ccr-*', - loggerMock + mockedLogger ); expect(res).toEqual({ custom_assets: { @@ -943,7 +945,7 @@ describe('fetchAndCompareSyncedIntegrations', () => { esClientMock, soClientMock, 'fleet-synced-integrations-ccr-*', - loggerMock + mockedLogger ); expect(res).toEqual({ custom_assets: { @@ -973,7 +975,7 @@ describe('fetchAndCompareSyncedIntegrations', () => { }); }); - it('should return failed status if is_deleted = true but the assets are still installed', async () => { + it('should return synchronizing status if is_deleted = true but the assets are still installed', async () => { const searchMockWithDeletedAssets = jest.fn().mockResolvedValue({ hits: { hits: [ @@ -1056,7 +1058,7 @@ describe('fetchAndCompareSyncedIntegrations', () => { esClientMock, soClientMock, 'fleet-synced-integrations-ccr-*', - loggerMock + mockedLogger ); expect(res).toEqual({ custom_assets: { @@ -1064,16 +1066,14 @@ describe('fetchAndCompareSyncedIntegrations', () => { name: 'logs-system.auth@custom', package_name: 'system', package_version: '1.67.3', - sync_status: 'failed', - error: 'Asset logs-system.auth@custom marked for deletion found installed', + sync_status: 'synchronizing', type: 'component_template', }, 'ingest_pipeline:logs-system.auth@custom': { name: 'logs-system.auth@custom', package_name: 'system', package_version: '1.67.3', - sync_status: 'failed', - error: 'Asset logs-system.auth@custom marked for deletion found installed', + sync_status: 'synchronizing', type: 'ingest_pipeline', }, }, @@ -1096,7 +1096,7 @@ describe('fetchAndCompareSyncedIntegrations', () => { esClientMock, soClientMock, 'fleet-synced-integrations-ccr-*', - loggerMock + mockedLogger ); expect(res).toEqual({ error: 'Some es error', @@ -1152,7 +1152,7 @@ describe('fetchAndCompareSyncedIntegrations', () => { esClientMock, soClientMock, 'fleet-synced-integrations-ccr-*', - loggerMock + mockedLogger ); expect(res).toEqual({ @@ -1171,3 +1171,86 @@ describe('fetchAndCompareSyncedIntegrations', () => { }); }); }); + +describe('getRemoteSyncedIntegrationsStatus', () => { + let esClientMock: any; + let getIndicesMock: jest.Mock; + let searchMock: jest.Mock; + let soClientMock: any; + + beforeEach(() => { + getIndicesMock = jest.fn(); + searchMock = jest.fn(); + esClientMock = { + indices: { + get: getIndicesMock, + }, + search: searchMock, + ccr: { followInfo: jest.fn() }, + }; + + soClientMock = savedObjectsClientMock.create(); + mockedLogger = loggerMock.create(); + mockedAppContextService.getLogger.mockReturnValue(mockedLogger); + + (installCustomAsset as jest.Mock).mockClear(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should return empty integrations array if feature flag is not available', async () => { + jest + .spyOn(mockedAppContextService, 'getExperimentalFeatures') + .mockReturnValue({ enableSyncIntegrationsOnRemote: false } as any); + expect(await getRemoteSyncedIntegrationsStatus(esClientMock, soClientMock)).toEqual({ + integrations: [], + }); + }); + + it('should return error if there is an error in getFollowerIndexInfo', async () => { + jest + .spyOn(mockedAppContextService, 'getExperimentalFeatures') + .mockReturnValue({ enableSyncIntegrationsOnRemote: true } as any); + getIndicesMock.mockResolvedValue({ + 'fleet-synced-integrations-ccr-remote1': {}, + }); + expect(await getRemoteSyncedIntegrationsStatus(esClientMock, soClientMock)).toEqual({ + error: 'Follower index fleet-synced-integrations-ccr-remote1 not available', + integrations: [], + }); + }); + + it('should return error if there is an error inside fetchAndCompareSyncedIntegrations', async () => { + jest + .spyOn(appContextService, 'getExperimentalFeatures') + .mockReturnValue({ enableSyncIntegrationsOnRemote: true } as any); + + esClientMock = { + search: jest.fn().mockRejectedValueOnce(new Error('Some ES error')), + indices: { + get: jest.fn().mockResolvedValue({ + 'fleet-synced-integrations-ccr-remote1': {}, + }), + }, + ccr: { + followInfo: jest.fn().mockResolvedValue({ + follower_indices: [ + { + follower_index: 'fleet-synced-integrations-ccr-remote1', + remote_cluster: 'Main', + leader_index: 'fleet-synced-integrations', + status: 'active', + parameters: {}, + }, + ], + }), + }, + }; + expect(await getRemoteSyncedIntegrationsStatus(esClientMock, soClientMock)).toEqual({ + integrations: [], + error: 'Some ES error', + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.ts index 22f76cb1750e8..9bc476ec84933 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.ts @@ -57,10 +57,11 @@ export const getFollowerIndexInfo = async ( } return { info: res.follower_indices[0] }; } catch (err) { - if (err?.body?.error?.type === 'index_not_found_exception') + if (err?.body?.error?.type === 'index_not_found_exception') { throw new IndexNotFoundError(`Index not found`); + } - logger.error('error', err.message); + logger.error('error', err?.message); throw err; } }; @@ -238,13 +239,7 @@ const compareCustomAssets = ({ if (ccrCustomAsset.is_deleted === true && installedPipeline) { return { ...result, - sync_status: 'failed' as SyncStatus.FAILED, - error: `Asset ${ccrCustomAsset.name} marked for deletion found installed`, - }; - } else if (isEqual(installedPipeline, ccrCustomAsset?.pipeline)) { - return { - ...result, - sync_status: 'completed' as SyncStatus.COMPLETED, + sync_status: 'synchronizing' as SyncStatus.SYNCHRONIZING, }; } else if ( installedPipeline?.version && @@ -255,6 +250,11 @@ const compareCustomAssets = ({ sync_status: 'failed' as SyncStatus.FAILED, error: `Found incorrect installed version ${installedPipeline.version}`, }; + } else if (isEqual(installedPipeline, ccrCustomAsset?.pipeline)) { + return { + ...result, + sync_status: 'completed' as SyncStatus.COMPLETED, + }; } else { return { ...result, @@ -279,8 +279,7 @@ const compareCustomAssets = ({ if (ccrCustomAsset.is_deleted === true && installedCompTemplate) { return { ...result, - sync_status: 'failed' as SyncStatus.FAILED, - error: `Asset ${ccrCustomAsset.name} marked for deletion found installed`, + sync_status: 'synchronizing' as SyncStatus.SYNCHRONIZING, }; } else if (isEqual(installedCompTemplate, ccrCustomAsset?.template)) { return { @@ -320,7 +319,7 @@ export const getRemoteSyncedIntegrationsStatus = async ( logger ); return res; - } catch (err) { - throw err; + } catch (error) { + return { error, integrations: [] }; } }; From fb345359e660fa2a327203e6afbdc51ab996bfd4 Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Mon, 7 Apr 2025 14:46:43 +0200 Subject: [PATCH 24/26] Replace failed state with synchronizing when versions do not match --- .../sync_integrations/compare_synced_integrations.test.ts | 5 ++--- .../tasks/sync_integrations/compare_synced_integrations.ts | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts index 5512e9d3a4bdf..1cd627c24372d 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts @@ -637,7 +637,7 @@ describe('fetchAndCompareSyncedIntegrations', () => { }, }); }); - it('should return status = failed if versions do not match', async () => { + it('should return status = synchronizing if versions do not match', async () => { const searchMockWithVersionedPipeline = jest.fn().mockResolvedValue({ hits: { hits: [ @@ -749,9 +749,8 @@ describe('fetchAndCompareSyncedIntegrations', () => { name: 'logs-system.auth@custom', package_name: 'system', package_version: '1.67.3', - sync_status: 'failed', + sync_status: 'synchronizing', type: 'ingest_pipeline', - error: 'Found incorrect installed version 1', }, }, }); diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.ts index 9bc476ec84933..cd1c36c01c684 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.ts @@ -247,8 +247,7 @@ const compareCustomAssets = ({ ) { return { ...result, - sync_status: 'failed' as SyncStatus.FAILED, - error: `Found incorrect installed version ${installedPipeline.version}`, + sync_status: 'synchronizing' as SyncStatus.SYNCHRONIZING, }; } else if (isEqual(installedPipeline, ccrCustomAsset?.pipeline)) { return { From fc6a8c8dca23e068538cc46071a99da88063940b Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Mon, 7 Apr 2025 16:20:10 +0200 Subject: [PATCH 25/26] Add failure reason from latest_install_failed_attempts --- .../compare_synced_integrations.test.ts | 98 ++++++++++--------- .../compare_synced_integrations.ts | 12 ++- 2 files changed, 65 insertions(+), 45 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts index 1cd627c24372d..c1d1750a81c2a 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts @@ -337,7 +337,7 @@ describe('fetchAndCompareSyncedIntegrations', () => { (getPackageSavedObjects as jest.MockedFunction).mockReturnValue({ page: 1, per_page: 10000, - total: 2, + total: 4, saved_objects: [ { package_name: 'elastic_agent', @@ -363,6 +363,14 @@ describe('fetchAndCompareSyncedIntegrations', () => { attributes: { version: '1.4.1', install_status: 'not_installed', + latest_install_failed_attempts: [ + { + created_at: '2023-06-20T08:47:31.457Z', + error: { + message: 'installation failure', + }, + }, + ], }, updated_at: '2025-03-26T14:06:27.611Z', }, @@ -390,7 +398,7 @@ describe('fetchAndCompareSyncedIntegrations', () => { error: 'Found incorrect installed version 1.67.2', }, { - error: 'Installation status: not_installed', + error: `Installation status: not_installed - reason: installation failure at Tue, 20 Jun 2023 08:47:31 GMT`, package_name: 'synthetics', package_version: '1.4.1', sync_status: 'failed', @@ -811,7 +819,7 @@ describe('fetchAndCompareSyncedIntegrations', () => { }); }); - it('should return completed status if is_deleted = true and the assets are not installed', async () => { + it('should return synchronizing status if assets are not installed and is_deleted === false', async () => { const searchMockWithDeletedAssets = jest.fn().mockResolvedValue({ hits: { hits: [ @@ -827,11 +835,11 @@ describe('fetchAndCompareSyncedIntegrations', () => { custom_assets: { 'component_template:logs-system.auth@custom': { ...customComponentTemplate, - is_deleted: true, + is_deleted: false, }, 'ingest_pipeline:logs-system.auth@custom': { ...customPipeline, - is_deleted: true, + is_deleted: false, }, }, }, @@ -871,14 +879,14 @@ describe('fetchAndCompareSyncedIntegrations', () => { name: 'logs-system.auth@custom', package_name: 'system', package_version: '1.67.3', - sync_status: 'completed', + sync_status: 'synchronizing', type: 'component_template', }, 'ingest_pipeline:logs-system.auth@custom': { name: 'logs-system.auth@custom', package_name: 'system', package_version: '1.67.3', - sync_status: 'completed', + sync_status: 'synchronizing', type: 'ingest_pipeline', }, }, @@ -892,7 +900,8 @@ describe('fetchAndCompareSyncedIntegrations', () => { ], }); }); - it('should return synchronizing status if assets are not installed and is_deleted === false', async () => { + + it('should return synchronizing status if is_deleted = true but the assets are still installed', async () => { const searchMockWithDeletedAssets = jest.fn().mockResolvedValue({ hits: { hits: [ @@ -908,11 +917,11 @@ describe('fetchAndCompareSyncedIntegrations', () => { custom_assets: { 'component_template:logs-system.auth@custom': { ...customComponentTemplate, - is_deleted: false, + is_deleted: true, }, 'ingest_pipeline:logs-system.auth@custom': { ...customPipeline, - is_deleted: false, + is_deleted: true, }, }, }, @@ -923,6 +932,37 @@ describe('fetchAndCompareSyncedIntegrations', () => { esClientMock = { search: searchMockWithDeletedAssets, }; + (getPipelineMock as jest.MockedFunction).mockResolvedValue({ + 'logs-system.auth@custom': { + processors: [ + { + set: { + field: 'test', + value: 'test-value', + }, + }, + ], + }, + }); + (getComponentTemplateMock as jest.MockedFunction).mockResolvedValue({ + component_templates: [ + { + name: 'logs-system.auth@custom', + component_template: { + template: { + mappings: { + dynamic_templates: [], + properties: { + 'Test-mapping': { + type: 'text', + }, + }, + }, + }, + }, + }, + ], + }); (getPackageSavedObjects as jest.MockedFunction).mockReturnValue({ page: 1, per_page: 10000, @@ -974,7 +1014,7 @@ describe('fetchAndCompareSyncedIntegrations', () => { }); }); - it('should return synchronizing status if is_deleted = true but the assets are still installed', async () => { + it('should return completed status if is_deleted = true and the assets are not installed', async () => { const searchMockWithDeletedAssets = jest.fn().mockResolvedValue({ hits: { hits: [ @@ -1005,37 +1045,6 @@ describe('fetchAndCompareSyncedIntegrations', () => { esClientMock = { search: searchMockWithDeletedAssets, }; - (getPipelineMock as jest.MockedFunction).mockResolvedValue({ - 'logs-system.auth@custom': { - processors: [ - { - set: { - field: 'test', - value: 'test-value', - }, - }, - ], - }, - }); - (getComponentTemplateMock as jest.MockedFunction).mockResolvedValue({ - component_templates: [ - { - name: 'logs-system.auth@custom', - component_template: { - template: { - mappings: { - dynamic_templates: [], - properties: { - 'Test-mapping': { - type: 'text', - }, - }, - }, - }, - }, - }, - ], - }); (getPackageSavedObjects as jest.MockedFunction).mockReturnValue({ page: 1, per_page: 10000, @@ -1065,14 +1074,14 @@ describe('fetchAndCompareSyncedIntegrations', () => { name: 'logs-system.auth@custom', package_name: 'system', package_version: '1.67.3', - sync_status: 'synchronizing', + sync_status: 'completed', type: 'component_template', }, 'ingest_pipeline:logs-system.auth@custom': { name: 'logs-system.auth@custom', package_name: 'system', package_version: '1.67.3', - sync_status: 'synchronizing', + sync_status: 'completed', type: 'ingest_pipeline', }, }, @@ -1086,6 +1095,7 @@ describe('fetchAndCompareSyncedIntegrations', () => { ], }); }); + it('should return error in the top level if there is any error in the function', async () => { esClientMock = { search: jest.fn().mockRejectedValueOnce(new Error('Some es error')), diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.ts index cd1c36c01c684..6f39049f71582 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.ts @@ -143,10 +143,20 @@ const compareIntegrations = ( }; } if (localIntegrationSO?.attributes.install_status !== 'installed') { + const latestFailedAttemptTime = localIntegrationSO?.attributes + ?.latest_install_failed_attempts?.[0].created_at + ? `at ${new Date( + localIntegrationSO?.attributes?.latest_install_failed_attempts?.[0].created_at + ).toUTCString()}` + : ''; + const latestFailedAttempt = localIntegrationSO?.attributes + ?.latest_install_failed_attempts?.[0]?.error?.message + ? `- reason: ${localIntegrationSO?.attributes?.latest_install_failed_attempts[0].error.message}` + : ''; return { ...ccrIntegration, sync_status: 'failed' as SyncStatus.FAILED, - error: `Installation status: ${localIntegrationSO?.attributes.install_status}`, + error: `Installation status: ${localIntegrationSO?.attributes.install_status} ${latestFailedAttempt} ${latestFailedAttemptTime}`, }; } return { From e51b00c2b480e68c108a0ceb164f0f5a8acbd359 Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Mon, 7 Apr 2025 17:09:36 +0200 Subject: [PATCH 26/26] Update diagram --- .../remote_clusters/remote_clusters.png | Bin 0 -> 56188 bytes .../remote_clusters/remote_clusters_diagram.png | Bin 55951 -> 0 bytes .../dev_docs/local_setup/remote_clusters_ccr.md | 6 +++--- 3 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 x-pack/platform/plugins/shared/fleet/dev_docs/diagrams/remote_clusters/remote_clusters.png delete mode 100644 x-pack/platform/plugins/shared/fleet/dev_docs/diagrams/remote_clusters/remote_clusters_diagram.png diff --git a/x-pack/platform/plugins/shared/fleet/dev_docs/diagrams/remote_clusters/remote_clusters.png b/x-pack/platform/plugins/shared/fleet/dev_docs/diagrams/remote_clusters/remote_clusters.png new file mode 100644 index 0000000000000000000000000000000000000000..fb0cd1770fce911e9e474b6f7d8f8d5061242212 GIT binary patch literal 56188 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
    NFCYh9W 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 aG@(28yHUoG7qs_=*SV^HrTCKFqyGm~mX>?~ literal 0 HcmV?d00001 diff --git a/x-pack/platform/plugins/shared/fleet/dev_docs/diagrams/remote_clusters/remote_clusters_diagram.png b/x-pack/platform/plugins/shared/fleet/dev_docs/diagrams/remote_clusters/remote_clusters_diagram.png deleted file mode 100644 index 8681a9a9ba6fdcbb74d9cc9d0e4ce4b0cb96446a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 55951 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 Ive~Qu4` on Cluster B. The basic architecture is shown in the diagram below: -![secret storage architecture](./diagrams/remote_clusters/remote_clusters_diagram.png) +![secret storage architecture](./diagrams/remote_clusters/remote_clusters.png) The communication between clusters happens as follows: -- Kibana on Cluster B exposes the content of the ccr index on endpoints `GET api/fleet/remote_synced_integrations/status` and `/api/fleet/remote_synced_integrations/{outputId}/status` -- Kibana on Cluster A reads polls these endpoints and exposes the status of the integrations sync in the UI +- Kibana on Cluster B exposes the content of the ccr index on endpoints `GET api/fleet/remote_synced_integrations/status` +- Kibana on Cluster A exposes endpoint `/api/fleet/remote_synced_integrations/{outputId}/status` and exposes the status of the integrations sync in the UI ## Run ES and Kibana