[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:
Cristina Amico 2025-04-15 11:48:36 +02:00 committed by GitHub
parent 076c378e30
commit 89d6dabfc2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 900 additions and 9 deletions

View file

@ -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.",

View file

@ -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.",

View file

@ -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.'

View file

@ -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.'

View file

@ -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`;

View file

@ -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;
}
};

View file

@ -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
);
};

View file

@ -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(),
};
});

View file

@ -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,
};

View file

@ -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');
});
});

View file

@ -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;
}
};

View file

@ -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';

View file

@ -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(),
}),
};