mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Fleet] Create API that queries remote kibana sync status API by output ID (#217799)
Part II of https://github.com/elastic/kibana/issues/217025 ## Summary Create API that queries remote kibana sync status API by output ID. From the main cluster we call the remote kibana (simply using node-fetch) and query the endpoint added in https://github.com/elastic/kibana/pull/216178; this way the main cluster can have the status of the synced integrations on the remote cluster. ### Testing Note that dev_docs now have a guide to setup locally the remote clusters: https://github.com/elastic/kibana/blob/main/x-pack/platform/plugins/shared/fleet/dev_docs/local_setup/remote_clusters_ccr.md - Follow the testing steps from [this PR](https://github.com/elastic/kibana/pull/216178) - Install some integrations on cluster A (main) and wait 5 minutes to get `SyncIntegrationsTask` running - Verify that cluster B (remote) has the same integrations installed. From dev tools, run ``` GET kbn:/api/fleet/remote_synced_integrations/status ``` - Go on dev tools on cluster A and run the new endpoint - `remote_id` is the id of the remote output configured on cluster A: ``` GET kbn:/api/fleet/remote_synced_integrations/<remote_id>/remote_status ``` The response should be the same as above ### Screenshot On Remote cluster (Cluster B): <img width="1183" alt="Screenshot 2025-04-10 at 15 40 46" src="https://github.com/user-attachments/assets/60ea1c1e-9ccf-4bcf-8637-bc4079483e61" /> On main cluster (Cluster A): <img width="1690" alt="Screenshot 2025-04-11 at 11 10 30" src="https://github.com/user-attachments/assets/e72fd729-3486-41b0-9194-487233415a75" /> ### Checklist - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
076c378e30
commit
89d6dabfc2
13 changed files with 900 additions and 9 deletions
|
@ -43676,6 +43676,147 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/api/fleet/remote_synced_integrations/{outputId}/remote_status": {
|
||||
"get": {
|
||||
"description": "[Required authorization] Route required privileges: fleet-settings-read AND integrations-read.",
|
||||
"operationId": "get-fleet-remote-synced-integrations-outputid-remote-status",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "outputId",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"custom_assets": {
|
||||
"additionalProperties": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"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",
|
||||
"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 by outputId",
|
||||
"tags": [
|
||||
"CCR Remote synced integrations"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/fleet/service_tokens": {
|
||||
"post": {
|
||||
"description": "[Required authorization] Route required privileges: fleet-agents-all.",
|
||||
|
|
|
@ -43676,6 +43676,147 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/api/fleet/remote_synced_integrations/{outputId}/remote_status": {
|
||||
"get": {
|
||||
"description": "[Required authorization] Route required privileges: fleet-settings-read AND integrations-read.",
|
||||
"operationId": "get-fleet-remote-synced-integrations-outputid-remote-status",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "outputId",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"custom_assets": {
|
||||
"additionalProperties": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"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",
|
||||
"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 by outputId",
|
||||
"tags": [
|
||||
"CCR Remote synced integrations"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/fleet/service_tokens": {
|
||||
"post": {
|
||||
"description": "[Required authorization] Route required privileges: fleet-agents-all.",
|
||||
|
|
|
@ -38392,6 +38392,101 @@ paths:
|
|||
summary: Update a proxy
|
||||
tags:
|
||||
- Fleet proxies
|
||||
/api/fleet/remote_synced_integrations/{outputId}/remote_status:
|
||||
get:
|
||||
description: '[Required authorization] Route required privileges: fleet-settings-read AND integrations-read.'
|
||||
operationId: get-fleet-remote-synced-integrations-outputid-remote-status
|
||||
parameters:
|
||||
- in: path
|
||||
name: outputId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
custom_assets:
|
||||
additionalProperties:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
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
|
||||
- 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 by outputId
|
||||
tags:
|
||||
- CCR Remote synced integrations
|
||||
/api/fleet/remote_synced_integrations/status:
|
||||
get:
|
||||
description: '[Required authorization] Route required privileges: fleet-settings-read AND integrations-read.'
|
||||
|
|
|
@ -40634,6 +40634,101 @@ paths:
|
|||
summary: Update a proxy
|
||||
tags:
|
||||
- Fleet proxies
|
||||
/api/fleet/remote_synced_integrations/{outputId}/remote_status:
|
||||
get:
|
||||
description: '[Required authorization] Route required privileges: fleet-settings-read AND integrations-read.'
|
||||
operationId: get-fleet-remote-synced-integrations-outputid-remote-status
|
||||
parameters:
|
||||
- in: path
|
||||
name: outputId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
custom_assets:
|
||||
additionalProperties:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
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
|
||||
- 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 by outputId
|
||||
tags:
|
||||
- CCR Remote synced integrations
|
||||
/api/fleet/remote_synced_integrations/status:
|
||||
get:
|
||||
description: '[Required authorization] Route required privileges: fleet-settings-read AND integrations-read.'
|
||||
|
|
|
@ -213,6 +213,7 @@ export const DOWNLOAD_SOURCE_API_ROUTES = {
|
|||
|
||||
export const REMOTE_SYNCED_INTEGRATIONS_API_ROUTES = {
|
||||
STATUS_PATTERN: `${API_ROOT}/remote_synced_integrations/status`,
|
||||
INFO_PATTERN: `${API_ROOT}/remote_synced_integrations/{outputId}/remote_status`,
|
||||
};
|
||||
|
||||
export const CREATE_STANDALONE_AGENT_API_KEY_ROUTE = `${INTERNAL_ROOT}/create_standalone_agent_api_key`;
|
||||
|
|
|
@ -6,9 +6,12 @@
|
|||
*/
|
||||
|
||||
import type { RequestHandler } from '@kbn/core/server';
|
||||
import type { TypeOf } from '@kbn/config-schema';
|
||||
|
||||
import type { GetRemoteSyncedIntegrationsStatusResponse } from '../../../common/types';
|
||||
import { getRemoteSyncedIntegrationsStatus } from '../../tasks/sync_integrations/compare_synced_integrations';
|
||||
import type { GetRemoteSyncedIntegrationsInfoRequestSchema } from '../../types';
|
||||
import { getRemoteSyncedIntegrationsInfoByOutputId } from '../../tasks/sync_integrations/get_remote_status';
|
||||
|
||||
export const getRemoteSyncedIntegrationsStatusHandler: RequestHandler<undefined> = async (
|
||||
context,
|
||||
|
@ -30,3 +33,23 @@ export const getRemoteSyncedIntegrationsStatusHandler: RequestHandler<undefined>
|
|||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getRemoteSyncedIntegrationsInfoHandler: RequestHandler<
|
||||
TypeOf<typeof GetRemoteSyncedIntegrationsInfoRequestSchema.params>
|
||||
> = async (context, request, response) => {
|
||||
const coreContext = await context.core;
|
||||
const soClient = coreContext.savedObjects.client;
|
||||
try {
|
||||
const res: GetRemoteSyncedIntegrationsStatusResponse =
|
||||
await getRemoteSyncedIntegrationsInfoByOutputId(soClient, request.params.outputId);
|
||||
|
||||
return response.ok({ body: res });
|
||||
} catch (error) {
|
||||
if (error.isBoom && error.output.statusCode === 404) {
|
||||
return response.notFound({
|
||||
body: { message: `${request.params.outputId} not found` },
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -13,7 +13,12 @@ import { FLEET_API_PRIVILEGES } from '../../constants/api_privileges';
|
|||
import { genericErrorResponse } from '../schema/errors';
|
||||
import { GetRemoteSyncedIntegrationsStatusResponseSchema } from '../../types/models/synced_integrations';
|
||||
|
||||
import { getRemoteSyncedIntegrationsStatusHandler } from './handler';
|
||||
import { GetRemoteSyncedIntegrationsInfoRequestSchema } from '../../types';
|
||||
|
||||
import {
|
||||
getRemoteSyncedIntegrationsStatusHandler,
|
||||
getRemoteSyncedIntegrationsInfoHandler,
|
||||
} from './handler';
|
||||
|
||||
export const registerRoutes = (router: FleetAuthzRouter) => {
|
||||
router.versioned
|
||||
|
@ -49,4 +54,38 @@ export const registerRoutes = (router: FleetAuthzRouter) => {
|
|||
},
|
||||
getRemoteSyncedIntegrationsStatusHandler
|
||||
);
|
||||
|
||||
router.versioned
|
||||
.get({
|
||||
path: REMOTE_SYNCED_INTEGRATIONS_API_ROUTES.INFO_PATTERN,
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: [
|
||||
FLEET_API_PRIVILEGES.SETTINGS.READ,
|
||||
FLEET_API_PRIVILEGES.INTEGRATIONS.READ,
|
||||
],
|
||||
},
|
||||
},
|
||||
summary: `Get CCR Remote synced integrations status by outputId`,
|
||||
options: {
|
||||
tags: ['oas-tag:CCR Remote synced integrations'],
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: API_VERSIONS.public.v1,
|
||||
validate: {
|
||||
request: GetRemoteSyncedIntegrationsInfoRequestSchema,
|
||||
response: {
|
||||
200: {
|
||||
body: () => GetRemoteSyncedIntegrationsStatusResponseSchema,
|
||||
},
|
||||
400: {
|
||||
body: genericErrorResponse,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
getRemoteSyncedIntegrationsInfoHandler
|
||||
);
|
||||
};
|
||||
|
|
|
@ -10,9 +10,9 @@ 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/app_context';
|
||||
|
||||
import { appContextService } from '../../services';
|
||||
import { getPackageSavedObjects } from '../../services/epm/packages/get';
|
||||
|
||||
import { installCustomAsset, getPipeline, getComponentTemplate } from './custom_assets';
|
||||
import {
|
||||
|
@ -21,13 +21,12 @@ import {
|
|||
getRemoteSyncedIntegrationsStatus,
|
||||
} from './compare_synced_integrations';
|
||||
|
||||
jest.mock('../../services');
|
||||
jest.mock('../../services/app_context');
|
||||
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(),
|
||||
};
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import type {
|
||||
ElasticsearchClient,
|
||||
Logger,
|
||||
|
@ -131,13 +132,17 @@ const compareIntegrations = (
|
|||
const localIntegrationSO = installedIntegrationsByName[ccrIntegration.package_name];
|
||||
if (!localIntegrationSO) {
|
||||
return {
|
||||
...ccrIntegration,
|
||||
package_name: ccrIntegration.package_name,
|
||||
package_version: ccrIntegration.package_version,
|
||||
updated_at: ccrIntegration.updated_at,
|
||||
sync_status: 'synchronizing' as SyncStatus.SYNCHRONIZING,
|
||||
};
|
||||
}
|
||||
if (ccrIntegration.package_version !== localIntegrationSO?.attributes.version) {
|
||||
return {
|
||||
...ccrIntegration,
|
||||
package_name: ccrIntegration.package_name,
|
||||
package_version: ccrIntegration.package_version,
|
||||
updated_at: ccrIntegration.updated_at,
|
||||
sync_status: 'failed' as SyncStatus.FAILED,
|
||||
error: `Found incorrect installed version ${localIntegrationSO?.attributes.version}`,
|
||||
};
|
||||
|
@ -154,13 +159,16 @@ const compareIntegrations = (
|
|||
? `- reason: ${localIntegrationSO?.attributes?.latest_install_failed_attempts[0].error.message}`
|
||||
: '';
|
||||
return {
|
||||
...ccrIntegration,
|
||||
package_name: ccrIntegration.package_name,
|
||||
package_version: ccrIntegration.package_version,
|
||||
updated_at: ccrIntegration.updated_at,
|
||||
sync_status: 'failed' as SyncStatus.FAILED,
|
||||
error: `Installation status: ${localIntegrationSO?.attributes.install_status} ${latestFailedAttempt} ${latestFailedAttemptTime}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...ccrIntegration,
|
||||
package_name: ccrIntegration.package_name,
|
||||
package_version: ccrIntegration.package_version,
|
||||
sync_status: 'completed' as SyncStatus.COMPLETED,
|
||||
updated_at: localIntegrationSO?.updated_at,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,258 @@
|
|||
/*
|
||||
* 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 fetch from 'node-fetch';
|
||||
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import type { Logger } from '@kbn/core/server';
|
||||
|
||||
import { appContextService } from '../../services/app_context';
|
||||
|
||||
import { outputService } from '../../services/output';
|
||||
import type { Output } from '../../types';
|
||||
|
||||
import { FleetNotFoundError } from '../../errors';
|
||||
|
||||
import { getRemoteSyncedIntegrationsInfoByOutputId } from './get_remote_status';
|
||||
|
||||
jest.mock('../../services/app_context');
|
||||
|
||||
const mockedAppContextService = appContextService as jest.Mocked<typeof appContextService>;
|
||||
const mockedOutputService = outputService as jest.Mocked<typeof outputService>;
|
||||
|
||||
jest.mock('../../services/output');
|
||||
jest.mock('node-fetch');
|
||||
|
||||
let mockedLogger: jest.Mocked<Logger>;
|
||||
|
||||
describe('getRemoteSyncedIntegrationsInfoByOutputId', () => {
|
||||
let soClientMock: any;
|
||||
const output: Output = {
|
||||
id: 'remote1',
|
||||
type: 'remote_elasticsearch',
|
||||
hosts: ['http://elasticsearch:9200'],
|
||||
is_default: true,
|
||||
is_default_monitoring: true,
|
||||
name: 'Remote Output',
|
||||
};
|
||||
const mockedFetch = fetch as jest.MockedFunction<typeof fetch>;
|
||||
|
||||
const statusRes = {
|
||||
integrations: [
|
||||
{
|
||||
package_name: 'system',
|
||||
package_version: '1.67.3',
|
||||
updated_at: '2025-03-20T14:18:40.111Z',
|
||||
sync_status: 'synchronizing',
|
||||
},
|
||||
],
|
||||
custom_assets: {
|
||||
'ingest_pipeline:logs-system.auth@custom': {
|
||||
name: 'logs-system.auth@custom',
|
||||
type: 'ingest_pipeline',
|
||||
package_name: 'system',
|
||||
package_version: '1.67.3',
|
||||
sync_status: 'synchronizing',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
soClientMock = savedObjectsClientMock.create();
|
||||
mockedLogger = loggerMock.create();
|
||||
mockedAppContextService.getLogger.mockReturnValue(mockedLogger);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockedFetch.mockReset();
|
||||
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 getRemoteSyncedIntegrationsInfoByOutputId(soClientMock, 'remote1')).toEqual({
|
||||
integrations: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error if the passed outputId is not found', async () => {
|
||||
jest
|
||||
.spyOn(mockedAppContextService, 'getExperimentalFeatures')
|
||||
.mockReturnValue({ enableSyncIntegrationsOnRemote: true } as any);
|
||||
mockedOutputService.get.mockImplementation(() => {
|
||||
throw new FleetNotFoundError(
|
||||
'Saved object [ingest-outputs/remote-es-not-existent] not found'
|
||||
);
|
||||
});
|
||||
|
||||
await expect(
|
||||
getRemoteSyncedIntegrationsInfoByOutputId(soClientMock, 'remote-es-not-existent')
|
||||
).rejects.toThrowError();
|
||||
});
|
||||
|
||||
it('should throw error if the output returns undefined', async () => {
|
||||
jest
|
||||
.spyOn(mockedAppContextService, 'getExperimentalFeatures')
|
||||
.mockReturnValue({ enableSyncIntegrationsOnRemote: true } as any);
|
||||
mockedOutputService.get.mockResolvedValue(undefined as any);
|
||||
await expect(
|
||||
getRemoteSyncedIntegrationsInfoByOutputId(soClientMock, 'remote2')
|
||||
).rejects.toThrowError('No output found with id remote2');
|
||||
});
|
||||
|
||||
it('should throw error if the passed outputId is not of type remote_elasticsearch', async () => {
|
||||
const outputEs = {
|
||||
id: 'not_remote',
|
||||
type: 'elasticsearch',
|
||||
hosts: ['http://elasticsearch:9200'],
|
||||
is_default: true,
|
||||
is_default_monitoring: true,
|
||||
name: 'ES Output',
|
||||
} as any;
|
||||
jest
|
||||
.spyOn(mockedAppContextService, 'getExperimentalFeatures')
|
||||
.mockReturnValue({ enableSyncIntegrationsOnRemote: true } as any);
|
||||
mockedOutputService.get.mockResolvedValue(outputEs);
|
||||
|
||||
await expect(
|
||||
getRemoteSyncedIntegrationsInfoByOutputId(soClientMock, 'not_remote')
|
||||
).rejects.toThrowError('Output not_remote is not a remote elasticsearch output');
|
||||
});
|
||||
|
||||
it('should throw error if the output has sync_integrations = false', async () => {
|
||||
jest
|
||||
.spyOn(mockedAppContextService, 'getExperimentalFeatures')
|
||||
.mockReturnValue({ enableSyncIntegrationsOnRemote: true } as any);
|
||||
mockedOutputService.get.mockResolvedValue({ ...output, sync_integrations: false } as any);
|
||||
|
||||
await expect(
|
||||
getRemoteSyncedIntegrationsInfoByOutputId(soClientMock, 'remote1')
|
||||
).rejects.toThrowError('Synced integrations not enabled');
|
||||
});
|
||||
|
||||
it('should throw error if kibanaUrl is not present', async () => {
|
||||
jest
|
||||
.spyOn(mockedAppContextService, 'getExperimentalFeatures')
|
||||
.mockReturnValue({ enableSyncIntegrationsOnRemote: true } as any);
|
||||
mockedOutputService.get.mockResolvedValue({ ...output, sync_integrations: true } as any);
|
||||
|
||||
await expect(
|
||||
getRemoteSyncedIntegrationsInfoByOutputId(soClientMock, 'remote1')
|
||||
).rejects.toThrowError(new FleetNotFoundError('Remote Kibana URL not set on the output.'));
|
||||
});
|
||||
|
||||
it('should throw error if kibanaApiKey is not present', async () => {
|
||||
jest
|
||||
.spyOn(mockedAppContextService, 'getExperimentalFeatures')
|
||||
.mockReturnValue({ enableSyncIntegrationsOnRemote: true } as any);
|
||||
mockedOutputService.get.mockResolvedValue({
|
||||
...output,
|
||||
sync_integrations: true,
|
||||
kibana_url: 'http://remote-kibana-host',
|
||||
} as any);
|
||||
|
||||
await expect(
|
||||
getRemoteSyncedIntegrationsInfoByOutputId(soClientMock, 'remote1')
|
||||
).rejects.toThrowError(
|
||||
new FleetNotFoundError('Remote Kibana API key for http://remote-kibana-host not found')
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if it cannot establish a connection with remote kibana', async () => {
|
||||
jest
|
||||
.spyOn(mockedAppContextService, 'getExperimentalFeatures')
|
||||
.mockReturnValue({ enableSyncIntegrationsOnRemote: true } as any);
|
||||
mockedOutputService.get.mockResolvedValue({
|
||||
...output,
|
||||
sync_integrations: true,
|
||||
kibana_url: 'http://remote-kibana-host',
|
||||
kibana_api_key: 'APIKEY',
|
||||
} as any);
|
||||
|
||||
mockedFetch.mockImplementation(() => {
|
||||
throw new Error(
|
||||
`request to http://remote-kibana-host/api/fleet/remote_synced_integrations/status failed, reason: getaddrinfo ENOTFOUND remote-kibana-host`
|
||||
);
|
||||
});
|
||||
|
||||
await expect(
|
||||
getRemoteSyncedIntegrationsInfoByOutputId(soClientMock, 'remote1')
|
||||
).rejects.toThrowError(
|
||||
'request to http://remote-kibana-host/api/fleet/remote_synced_integrations/status failed, reason: getaddrinfo ENOTFOUND remote-kibana-host'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return the response from the remote status api', async () => {
|
||||
jest
|
||||
.spyOn(mockedAppContextService, 'getExperimentalFeatures')
|
||||
.mockReturnValue({ enableSyncIntegrationsOnRemote: true } as any);
|
||||
mockedOutputService.get.mockResolvedValue({
|
||||
...output,
|
||||
sync_integrations: true,
|
||||
kibana_url: 'http://remote-kibana-host',
|
||||
kibana_api_key: 'APIKEY',
|
||||
} as any);
|
||||
|
||||
mockedFetch.mockResolvedValueOnce({
|
||||
json: () => statusRes,
|
||||
status: 200,
|
||||
ok: true,
|
||||
} as any);
|
||||
|
||||
expect(await getRemoteSyncedIntegrationsInfoByOutputId(soClientMock, 'remote1')).toEqual(
|
||||
statusRes
|
||||
);
|
||||
});
|
||||
|
||||
it('should not throw if the remote status api has errors in the body', async () => {
|
||||
jest
|
||||
.spyOn(mockedAppContextService, 'getExperimentalFeatures')
|
||||
.mockReturnValue({ enableSyncIntegrationsOnRemote: true } as any);
|
||||
mockedOutputService.get.mockResolvedValue({
|
||||
...output,
|
||||
sync_integrations: true,
|
||||
kibana_url: 'http://remote-kibana-host',
|
||||
kibana_api_key: 'APIKEY',
|
||||
} as any);
|
||||
const statusWithErrorRes = {
|
||||
error: 'No integrations found on fleet-synced-integrations-ccr-*',
|
||||
integrations: [],
|
||||
};
|
||||
|
||||
mockedFetch.mockResolvedValueOnce({
|
||||
json: () => statusWithErrorRes,
|
||||
status: 200,
|
||||
ok: true,
|
||||
} as any);
|
||||
|
||||
expect(await getRemoteSyncedIntegrationsInfoByOutputId(soClientMock, 'remote1')).toEqual(
|
||||
statusWithErrorRes
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if the remote api returns error', async () => {
|
||||
jest
|
||||
.spyOn(mockedAppContextService, 'getExperimentalFeatures')
|
||||
.mockReturnValue({ enableSyncIntegrationsOnRemote: true } as any);
|
||||
mockedOutputService.get.mockResolvedValue({
|
||||
...output,
|
||||
sync_integrations: true,
|
||||
kibana_url: 'http://remote-kibana-host',
|
||||
kibana_api_key: 'APIKEY',
|
||||
} as any);
|
||||
|
||||
mockedFetch.mockImplementation(() => {
|
||||
throw new Error(`some error`);
|
||||
});
|
||||
await expect(
|
||||
getRemoteSyncedIntegrationsInfoByOutputId(soClientMock, 'remote1')
|
||||
).rejects.toThrowError('some error');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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 fetch from 'node-fetch';
|
||||
import type { RequestInit } from 'node-fetch';
|
||||
|
||||
import type { SavedObjectsClientContract } from '@kbn/core/server';
|
||||
|
||||
import { API_VERSIONS } from '../../../common';
|
||||
|
||||
import { appContextService } from '../../services';
|
||||
import { outputService } from '../../services';
|
||||
|
||||
import { FleetError, FleetNotFoundError } from '../../errors';
|
||||
|
||||
import type { GetRemoteSyncedIntegrationsStatusResponse } from '../../../common/types';
|
||||
|
||||
export const getRemoteSyncedIntegrationsInfoByOutputId = async (
|
||||
soClient: SavedObjectsClientContract,
|
||||
outputId: string
|
||||
): Promise<GetRemoteSyncedIntegrationsStatusResponse> => {
|
||||
const { enableSyncIntegrationsOnRemote } = appContextService.getExperimentalFeatures();
|
||||
const logger = appContextService.getLogger();
|
||||
|
||||
if (!enableSyncIntegrationsOnRemote) {
|
||||
return { integrations: [] };
|
||||
}
|
||||
try {
|
||||
const output = await outputService.get(soClient, outputId);
|
||||
if (!output) {
|
||||
throw new FleetNotFoundError(`No output found with id ${outputId}`);
|
||||
}
|
||||
if (output?.type !== 'remote_elasticsearch') {
|
||||
throw new FleetError(`Output ${outputId} is not a remote elasticsearch output`);
|
||||
}
|
||||
|
||||
const {
|
||||
kibana_api_key: kibanaApiKey,
|
||||
kibana_url: kibanaUrl,
|
||||
sync_integrations: syncIntegrations,
|
||||
} = output;
|
||||
|
||||
if (!syncIntegrations) {
|
||||
throw new FleetError(`Synced integrations not enabled`);
|
||||
}
|
||||
if (!kibanaUrl || kibanaUrl === '') {
|
||||
throw new FleetNotFoundError(`Remote Kibana URL not set on the output.`);
|
||||
}
|
||||
if (!kibanaApiKey) {
|
||||
throw new FleetNotFoundError(`Remote Kibana API key for ${kibanaUrl} not found`);
|
||||
}
|
||||
const options: RequestInit = {
|
||||
headers: {
|
||||
'kbn-xsrf': 'true',
|
||||
'User-Agent': `Kibana/${appContextService.getKibanaVersion()} node-fetch`,
|
||||
'Content-Type': 'application/json',
|
||||
'Elastic-Api-Version': API_VERSIONS.public.v1,
|
||||
Authorization: `ApiKey ${kibanaApiKey}`,
|
||||
},
|
||||
method: 'GET',
|
||||
};
|
||||
const url = `${kibanaUrl}/api/fleet/remote_synced_integrations/status`;
|
||||
logger.info(`Fetching ${kibanaUrl}/api/fleet/remote_synced_integrations/status`);
|
||||
|
||||
const res = await fetch(url, options);
|
||||
|
||||
const body = await res.json();
|
||||
|
||||
return body as GetRemoteSyncedIntegrationsStatusResponse;
|
||||
} catch (error) {
|
||||
logger.error(`${error}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
|
@ -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';
|
||||
|
|
|
@ -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 GetRemoteSyncedIntegrationsInfoRequestSchema = {
|
||||
params: schema.object({
|
||||
outputId: schema.string(),
|
||||
}),
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue