Skip to content

Commit 6c257ab

Browse files
[Fleet] add index and task for fleet-synced-integrations (#209762)
## Summary Closes #206237 Create `fleet-synced-integrations` index in Fleet setup, added async task that populates the index with a doc that includes remote ES output data and installed integrations data. ES change to add `kibana_system` privileges: elastic/elasticsearch#121753 To test locally: - run elasticsearch from source to apply the privilege changes, so that `kibana_system` can create the index. ``` yarn es source -E xpack.security.authc.api_key.enabled=true -E xpack.security.authc.token.enabled=true --source-path=/Users/juliabardi/elasticsearch -E path.data=/tmp/es-data -E xpack.ml.enabled=false ``` - enable the feature flag in `kibana.dev.yml`: `xpack.fleet.enableExperimental: ['enableSyncIntegrationsOnRemote']` - add a remote ES output with sync enabled - install some integrations - wait until Fleet setup and the task runs - verify that the index is created and contains a doc with the expected data ``` GET fleet-synced-integrations/_search "hits": { "total": { "value": 1, "relation": "eq" }, "max_score": 1, "hits": [ { "_index": "fleet-synced-integrations", "_id": "fleet-synced-integrations", "_score": 1, "_source": { "remote_es_hosts": [ { "hosts": [ "http://remote1:80" ], "name": "remote1", "sync_integrations": true } ], "integrations": [ { "package_version": "1.64.1", "updated_at": "2025-02-05T11:03:02.226Z", "package_name": "system" } ] } } ] ``` ### Checklist - [x] [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 <[email protected]>
1 parent 9fa8ec4 commit 6c257ab

File tree

10 files changed

+632
-1
lines changed

10 files changed

+632
-1
lines changed

x-pack/platform/plugins/shared/fleet/server/mocks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ export const createAppContextStartContractMock = (
140140
: {}),
141141
unenrollInactiveAgentsTask: {} as any,
142142
deleteUnenrolledAgentsTask: {} as any,
143+
syncIntegrationsTask: {} as any,
143144
};
144145
};
145146

x-pack/platform/plugins/shared/fleet/server/plugin.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ import { registerUpgradeManagedPackagePoliciesTask } from './services/setup/mana
147147
import { registerDeployAgentPoliciesTask } from './services/agent_policies/deploy_agent_policies_task';
148148
import { DeleteUnenrolledAgentsTask } from './tasks/delete_unenrolled_agents_task';
149149
import { registerBumpAgentPoliciesTask } from './services/agent_policies/bump_agent_policies_task';
150+
import { SyncIntegrationsTask } from './tasks/sync_integrations_task';
150151

151152
export interface FleetSetupDeps {
152153
security: SecurityPluginSetup;
@@ -200,6 +201,7 @@ export interface FleetAppContext {
200201
deleteUnenrolledAgentsTask: DeleteUnenrolledAgentsTask;
201202
taskManagerStart?: TaskManagerStartContract;
202203
fetchUsage?: (abortController: AbortController) => Promise<FleetUsage | undefined>;
204+
syncIntegrationsTask: SyncIntegrationsTask;
203205
}
204206

205207
export type FleetSetupContract = void;
@@ -301,6 +303,7 @@ export class FleetPlugin
301303
private fleetMetricsTask?: FleetMetricsTask;
302304
private unenrollInactiveAgentsTask?: UnenrollInactiveAgentsTask;
303305
private deleteUnenrolledAgentsTask?: DeleteUnenrolledAgentsTask;
306+
private syncIntegrationsTask?: SyncIntegrationsTask;
304307

305308
private agentService?: AgentService;
306309
private packageService?: PackageService;
@@ -647,6 +650,11 @@ export class FleetPlugin
647650
taskManager: deps.taskManager,
648651
logFactory: this.initializerContext.logger,
649652
});
653+
this.syncIntegrationsTask = new SyncIntegrationsTask({
654+
core,
655+
taskManager: deps.taskManager,
656+
logFactory: this.initializerContext.logger,
657+
});
650658

651659
// Register fields metadata extractors
652660
registerFieldsMetadataExtractors({ core, fieldsMetadata: deps.fieldsMetadata });
@@ -696,6 +704,7 @@ export class FleetPlugin
696704
deleteUnenrolledAgentsTask: this.deleteUnenrolledAgentsTask!,
697705
taskManagerStart: plugins.taskManager,
698706
fetchUsage: this.fetchUsage,
707+
syncIntegrationsTask: this.syncIntegrationsTask!,
699708
});
700709
licenseService.start(plugins.licensing.license$);
701710
this.telemetryEventsSender.start(plugins.telemetry, core).catch(() => {});
@@ -708,6 +717,7 @@ export class FleetPlugin
708717
this.fleetMetricsTask
709718
?.start(plugins.taskManager, core.elasticsearch.client.asInternalUser)
710719
.catch(() => {});
720+
this.syncIntegrationsTask?.start({ taskManager: plugins.taskManager }).catch(() => {});
711721

712722
const logger = appContextService.getLogger();
713723

x-pack/platform/plugins/shared/fleet/server/services/epm/packages/get.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ export async function getPackageSavedObjects(
311311
return result;
312312
}
313313

314-
async function getInstalledPackageSavedObjects(
314+
export async function getInstalledPackageSavedObjects(
315315
savedObjectsClient: SavedObjectsClientContract,
316316
options: Omit<GetInstalledPackagesOptions, 'savedObjectsClient' | 'esClient'>
317317
) {

x-pack/platform/plugins/shared/fleet/server/services/setup.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import {
6464
} from './preconfiguration/delete_unenrolled_agent_setting';
6565
import { backfillPackagePolicySupportsAgentless } from './backfill_agentless';
6666
import { updateDeprecatedComponentTemplates } from './setup/update_deprecated_component_templates';
67+
import { createOrUpdateFleetSyncedIntegrationsIndex } from './setup/fleet_synced_integrations';
6768

6869
export interface SetupStatus {
6970
isInitialized: boolean;
@@ -313,6 +314,9 @@ async function createSetupSideEffects(
313314
logger.debug('Update deprecated _source.mode in component templates');
314315
await updateDeprecatedComponentTemplates(esClient);
315316

317+
logger.debug('Create or update fleet-synced-integrations index');
318+
await createOrUpdateFleetSyncedIntegrationsIndex(esClient);
319+
316320
const nonFatalErrors = [
317321
...preconfiguredPackagesNonFatalErrors,
318322
...(messageSigningServiceNonFatalError ? [messageSigningServiceNonFatalError] : []),
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { createOrUpdateFleetSyncedIntegrationsIndex } from './fleet_synced_integrations';
9+
10+
jest.mock('../app_context', () => ({
11+
appContextService: {
12+
getExperimentalFeatures: jest.fn().mockReturnValue({ enableSyncIntegrationsOnRemote: true }),
13+
},
14+
}));
15+
16+
describe('fleet_synced_integrations', () => {
17+
let esClientMock: any;
18+
const mockExists = jest.fn();
19+
const mockGetMapping = jest.fn();
20+
21+
beforeEach(() => {
22+
esClientMock = {
23+
indices: {
24+
create: jest.fn(),
25+
exists: mockExists,
26+
getMapping: mockGetMapping,
27+
putMapping: jest.fn(),
28+
},
29+
};
30+
});
31+
32+
it('should create index if not exists', async () => {
33+
mockExists.mockResolvedValue(false);
34+
35+
await createOrUpdateFleetSyncedIntegrationsIndex(esClientMock);
36+
37+
expect(esClientMock.indices.create).toHaveBeenCalled();
38+
});
39+
40+
it('should update index if older version exists', async () => {
41+
mockExists.mockResolvedValue(true);
42+
mockGetMapping.mockResolvedValue({
43+
'fleet-synced-integrations': {
44+
mappings: {
45+
_meta: {
46+
version: '0.0',
47+
},
48+
},
49+
},
50+
});
51+
52+
await createOrUpdateFleetSyncedIntegrationsIndex(esClientMock);
53+
54+
expect(esClientMock.indices.putMapping).toHaveBeenCalled();
55+
});
56+
57+
it('should not update index if same version exists', async () => {
58+
mockExists.mockResolvedValue(true);
59+
mockGetMapping.mockResolvedValue({
60+
'fleet-synced-integrations': {
61+
mappings: {
62+
_meta: {
63+
version: '1.0',
64+
},
65+
},
66+
},
67+
});
68+
69+
await createOrUpdateFleetSyncedIntegrationsIndex(esClientMock);
70+
71+
expect(esClientMock.indices.putMapping).not.toHaveBeenCalled();
72+
});
73+
});
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import type { ElasticsearchClient } from '@kbn/core/server';
9+
10+
import { FleetSetupError } from '../../errors';
11+
import { appContextService } from '../app_context';
12+
13+
export const FLEET_SYNCED_INTEGRATIONS_INDEX_NAME = 'fleet-synced-integrations';
14+
15+
export const FLEET_SYNCED_INTEGRATIONS_INDEX_CONFIG = {
16+
settings: {
17+
auto_expand_replicas: '0-1',
18+
},
19+
mappings: {
20+
dynamic: false,
21+
_meta: {
22+
version: '1.0',
23+
},
24+
properties: {
25+
remote_es_hosts: {
26+
properties: {
27+
name: {
28+
type: 'keyword',
29+
},
30+
hosts: {
31+
type: 'keyword',
32+
},
33+
sync_integrations: {
34+
type: 'boolean',
35+
},
36+
},
37+
},
38+
integrations: {
39+
properties: {
40+
package_name: {
41+
type: 'keyword',
42+
},
43+
package_version: {
44+
type: 'keyword',
45+
},
46+
updated_at: {
47+
type: 'date',
48+
},
49+
},
50+
},
51+
},
52+
},
53+
};
54+
55+
export async function createOrUpdateFleetSyncedIntegrationsIndex(esClient: ElasticsearchClient) {
56+
const { enableSyncIntegrationsOnRemote } = appContextService.getExperimentalFeatures();
57+
58+
if (!enableSyncIntegrationsOnRemote) {
59+
return;
60+
}
61+
62+
await createOrUpdateIndex(
63+
esClient,
64+
FLEET_SYNCED_INTEGRATIONS_INDEX_NAME,
65+
FLEET_SYNCED_INTEGRATIONS_INDEX_CONFIG
66+
);
67+
}
68+
69+
async function createOrUpdateIndex(
70+
esClient: ElasticsearchClient,
71+
indexName: string,
72+
indexData: any
73+
) {
74+
const resExists = await esClient.indices.exists({
75+
index: indexName,
76+
});
77+
78+
if (resExists) {
79+
return updateIndex(esClient, indexName, indexData);
80+
}
81+
82+
return createIndex(esClient, indexName, indexData);
83+
}
84+
85+
async function updateIndex(esClient: ElasticsearchClient, indexName: string, indexData: any) {
86+
try {
87+
const res = await esClient.indices.getMapping({
88+
index: indexName,
89+
});
90+
91+
const versionChanged =
92+
res[indexName].mappings?._meta?.version !== indexData.mappings._meta.version;
93+
if (versionChanged) {
94+
await esClient.indices.putMapping({
95+
index: indexName,
96+
body: Object.assign({
97+
...indexData.mappings,
98+
}),
99+
});
100+
}
101+
} catch (err) {
102+
throw new FleetSetupError(`update of index [${indexName}] failed ${err}`);
103+
}
104+
}
105+
106+
async function createIndex(esClient: ElasticsearchClient, indexName: string, indexData: any) {
107+
try {
108+
await esClient.indices.create({
109+
index: indexName,
110+
body: indexData,
111+
});
112+
} catch (err) {
113+
if (err?.body?.error?.type !== 'resource_already_exists_exception') {
114+
throw new FleetSetupError(`create of index [${indexName}] failed ${err}`);
115+
}
116+
}
117+
}

x-pack/platform/plugins/shared/fleet/server/services/setup/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ export { upgradePackageInstallVersion } from './upgrade_package_install_version'
99
export { upgradeAgentPolicySchemaVersion } from './upgrade_agent_policy_schema_version';
1010
export { ensureAgentPoliciesFleetServerKeysAndPolicies } from './fleet_server_policies_enrollment_keys';
1111
export { updateDeprecatedComponentTemplates } from './update_deprecated_component_templates';
12+
export { createOrUpdateFleetSyncedIntegrationsIndex } from './fleet_synced_integrations';

0 commit comments

Comments
 (0)