Skip to content

Commit d2e01dc

Browse files
Add fleet policy revisions cleanup task (#242612)
Resolves: #119963 PR to add new Kibana config to allow list: elastic/cloud#149322 * Adds the background task `FleetPolicyRevisionsCleanupTask` which removes excess policy revisions from the `.fleet-policies` index * Enabled by default, disabled by new feature flag: ``` xpack.fleet.experimentalFeatures: enableFleetPolicyRevisionsCleanupTask: true ``` * Configurable with the following: ``` xpack.fleet.fleetPolicyRevisionsCleanup: interval: string // Time interval expression. Default: '1h' max_revisions: number // Maximum amount of policy revisions. Default: 10. max_policies_per_run: number // Maximum policies to delete revisions from per run. Default 100. ``` * Also adds an internal endpoint for the same operation: ``` POST kbn:/internal/fleet/agent_policies/_cleanup_revisions?apiVersion=1 { "maxRevisions": number, // max revisions a policy should have "maxPolicies": number, // max policies to process } ``` --------- Co-authored-by: kibanamachine <[email protected]>
1 parent b78cbc5 commit d2e01dc

File tree

24 files changed

+1798
-9
lines changed

24 files changed

+1798
-9
lines changed

x-pack/platform/plugins/shared/fleet/common/constants/routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export const AGENT_POLICY_API_ROUTES = {
9595
INFO_OUTPUTS_PATTERN: `${AGENT_POLICY_API_ROOT}/{agentPolicyId}/outputs`,
9696
AUTO_UPGRADE_AGENTS_STATUS_PATTERN: `${AGENT_POLICY_API_ROOT}/{agentPolicyId}/auto_upgrade_agents_status`,
9797
CREATE_WITH_PACKAGE_POLICIES: `${INTERNAL_ROOT}/agent_and_package_policies`,
98+
CLEANUP_REVISIONS_PATTERN: `${INTERNAL_ROOT}/agent_policies/_cleanup_revisions`,
9899
};
99100

100101
// Cloud Connector API routes

x-pack/platform/plugins/shared/fleet/common/experimental_features.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const _allowedExperimentalValues = {
1919
enableAgentStatusAlerting: true,
2020
enableAgentPrivilegeLevelChange: false,
2121
installIntegrationsKnowledge: false,
22+
enableFleetPolicyRevisionsCleanupTask: true,
2223
agentlessPoliciesAPI: true,
2324
useAgentlessAPIInUI: false,
2425
disabledAgentlessLegacyAPI: false,

x-pack/platform/plugins/shared/fleet/common/types/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ export interface FleetConfigType {
113113
hideDashboards?: boolean;
114114
integrationRollbackTTL?: string;
115115
installIntegrationsKnowledge?: boolean;
116+
fleetPolicyRevisionsCleanup?: {
117+
maxRevisions: number;
118+
interval: string;
119+
maxPoliciesPerRun: number;
120+
};
116121
}
117122

118123
// Calling Object.entries(PackagesGroupedByStatus) gave `status: string`

x-pack/platform/plugins/shared/fleet/moon.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ fileGroups:
128128
- common/**/*
129129
- public/**/*
130130
- server/**/*
131+
- tasks/**/*
131132
- server/**/*.json
132133
- scripts/**/*
133134
- package.json

x-pack/platform/plugins/shared/fleet/scripts/create_agents/create_agents.ts

Lines changed: 127 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { v4 as uuidv4 } from 'uuid';
1010
import yargs from 'yargs';
1111
import { omit } from 'lodash';
1212

13-
import type { AgentStatus } from '../../common';
13+
import { AGENT_POLICY_SAVED_OBJECT_TYPE, type AgentStatus } from '../../common';
1414
import type { Agent } from '../../common';
1515
const printUsage = () =>
1616
logger.info(`
@@ -29,6 +29,7 @@ const printUsage = () =>
2929
[--batches]: run the script in batches, defaults to 1 e.g if count is 50 and batches is 10, 500 agents will be created and 10 agent policies
3030
[--concurrentBatches]: how many batches to run concurrently, defaults to 10
3131
[--outdated]: agents will show as outdated (their revision is below the policies), defaults to false
32+
[--revisions]: includes the number of revisions to create per policy, defaults to 0
3233
`);
3334

3435
const DEFAULT_KIBANA_URL = 'http://localhost:5601';
@@ -57,6 +58,7 @@ const {
5758
batches: batchesArg = 1,
5859
outdated: outdatedArg = false,
5960
concurrentBatches: concurrentBatchesArg = 10,
61+
revisions: revisionsArg = 0,
6062
// ignore yargs positional args, we only care about named args
6163
_,
6264
$0,
@@ -67,9 +69,10 @@ const statusesArg = (statusArg as string).split(',') as AgentStatus[];
6769
const inactivityTimeout = inactivityTimeoutArg
6870
? Number(inactivityTimeoutArg).valueOf()
6971
: DEFAULT_UNENROLL_TIMEOUT;
70-
const batches = inactivityTimeoutArg ? Number(batchesArg).valueOf() : 1;
72+
const batches = batchesArg ? Number(batchesArg).valueOf() : 1;
7173
const concurrentBatches = concurrentBatchesArg ? Number(concurrentBatchesArg).valueOf() : 10;
7274
const count = countArg ? Number(countArg).valueOf() : DEFAULT_AGENT_COUNT;
75+
const revisionsCount = revisionsArg ? Number(revisionsArg).valueOf() : 0;
7376
const kbnAuth = 'Basic ' + Buffer.from(kbnUsername + ':' + kbnPassword).toString('base64');
7477

7578
const logger = new ToolingLog({
@@ -138,11 +141,13 @@ function createAgentWithStatus({
138141
status,
139142
version,
140143
hostname,
144+
revisionIdx = 1,
141145
}: {
142146
policyId: string;
143147
status: AgentStatus;
144148
version: string;
145149
hostname: string;
150+
revisionIdx?: number;
146151
}) {
147152
const baseAgent = {
148153
agent: {
@@ -153,8 +158,8 @@ function createAgentWithStatus({
153158
active: true,
154159
policy_id: policyId,
155160
type: 'PERMANENT',
156-
policy_revision_idx: 1,
157-
policy_revision: 1,
161+
policy_revision_idx: revisionIdx,
162+
policy_revision: revisionIdx,
158163
local_metadata: {
159164
elastic: {
160165
agent: {
@@ -178,7 +183,8 @@ function createAgentsWithStatuses(
178183
statusMap: Partial<{ [status in AgentStatus]: number }>,
179184
policyId: string,
180185
version: string,
181-
namePrefix?: string
186+
namePrefix?: string,
187+
latestRevision?: number
182188
) {
183189
// loop over statuses and create agents with that status
184190
const agents = [];
@@ -189,7 +195,13 @@ function createAgentsWithStatuses(
189195
for (let i = 0; i < statusCount; i++) {
190196
const hostname = `${namePrefix ? namePrefix + '-' : ''}${currentAgentStatus}-${i}`;
191197
agents.push(
192-
createAgentWithStatus({ policyId, status: currentAgentStatus, version, hostname })
198+
createAgentWithStatus({
199+
policyId,
200+
status: currentAgentStatus,
201+
version,
202+
hostname,
203+
revisionIdx: latestRevision,
204+
})
193205
);
194206
}
195207
}
@@ -268,7 +280,7 @@ async function createSuperUser() {
268280
body: JSON.stringify({
269281
indices: [
270282
{
271-
names: ['.fleet*'],
283+
names: ['.fleet*', '.kibana*'],
272284
privileges: ['all'],
273285
allow_restricted_indices: true,
274286
},
@@ -335,6 +347,104 @@ async function createAgentPolicy(id: string, name: string) {
335347
return data;
336348
}
337349

350+
async function createPolicyRevisions(policyId: string, latestRevisionIdx: number) {
351+
const nextLatestRevisionIdx = latestRevisionIdx + revisionsCount;
352+
await revisePolicySoRevisionIdx(policyId, nextLatestRevisionIdx);
353+
await backfillPolicyRevisions(policyId, nextLatestRevisionIdx);
354+
355+
return nextLatestRevisionIdx;
356+
}
357+
358+
async function getLatestFleetPolicyRevision(policyId: string) {
359+
const auth = 'Basic ' + Buffer.from(ES_SUPERUSER + ':' + ES_PASSWORD).toString('base64');
360+
const res = await fetch(`${ES_URL}/.fleet-policies/_search`, {
361+
method: 'post',
362+
body: JSON.stringify({
363+
size: 1,
364+
sort: { revision_idx: 'desc' },
365+
query: {
366+
match: {
367+
policy_id: policyId,
368+
},
369+
},
370+
}),
371+
headers: {
372+
Authorization: auth,
373+
'Content-Type': 'application/x-ndjson',
374+
},
375+
});
376+
377+
const data = await res.json();
378+
const latestPolicyRevision = data.hits.hits[0];
379+
380+
if (!latestPolicyRevision) {
381+
logger.error('Error retrieving latest fleet policy revision: ' + JSON.stringify(data));
382+
process.exit(1);
383+
}
384+
return latestPolicyRevision;
385+
}
386+
387+
async function backfillPolicyRevisions(policyId: string, maxRevisionIdx: number) {
388+
const latestPolicyRevision = await getLatestFleetPolicyRevision(policyId);
389+
390+
const auth = 'Basic ' + Buffer.from(ES_SUPERUSER + ':' + ES_PASSWORD).toString('base64');
391+
const body = Array(maxRevisionIdx)
392+
.fill(0)
393+
.flatMap((_rev, idx) => [
394+
INDEX_BULK_OP.replace(/{{id}}/, `${policyId}:${idx}`),
395+
JSON.stringify({
396+
...latestPolicyRevision._source,
397+
revision_idx: idx + 1,
398+
'@timestamp': new Date().toISOString(),
399+
}) + '\n',
400+
])
401+
.join('');
402+
403+
const res = await fetch(`${ES_URL}/.fleet-policies/_bulk`, {
404+
method: 'post',
405+
body,
406+
headers: {
407+
Authorization: auth,
408+
'Content-Type': 'application/x-ndjson',
409+
},
410+
});
411+
const data = await res.json();
412+
413+
if (!data.items) {
414+
logger.error('Error creating bulk policy revisions: ' + JSON.stringify(data));
415+
process.exit(1);
416+
}
417+
}
418+
419+
async function revisePolicySoRevisionIdx(policyId: string, revisionIdx: number) {
420+
const auth = 'Basic ' + Buffer.from(ES_SUPERUSER + ':' + ES_PASSWORD).toString('base64');
421+
const body = JSON.stringify({
422+
doc: {
423+
[AGENT_POLICY_SAVED_OBJECT_TYPE]: {
424+
revision: revisionIdx,
425+
},
426+
},
427+
});
428+
const res = await fetch(
429+
`${ES_URL}/.kibana_ingest/_update/${AGENT_POLICY_SAVED_OBJECT_TYPE}:${policyId}`,
430+
{
431+
method: 'post',
432+
body,
433+
headers: {
434+
Authorization: auth,
435+
'Content-Type': 'application/x-ndjson',
436+
},
437+
}
438+
);
439+
const data = await res.json();
440+
441+
if (!data.result) {
442+
logger.error('Error updating agent policy SO revision idx: ' + JSON.stringify(data));
443+
process.exit(1);
444+
}
445+
return data;
446+
}
447+
338448
async function bumpAgentPolicyRevision(id: string, policy: any) {
339449
const res = await fetch(`${kibanaUrl}/api/fleet/agent_policies/${id}`, {
340450
method: 'put',
@@ -348,6 +458,9 @@ async function bumpAgentPolicyRevision(id: string, policy: any) {
348458
'schema_version',
349459
'package_policies',
350460
'agents',
461+
'version',
462+
'unprivileged_agents',
463+
'fips_agents',
351464
]),
352465
monitoring_enabled: ['logs'], // change monitoring to add a revision
353466
}),
@@ -428,13 +541,19 @@ export async function run() {
428541
agentPolicyId = agentPolicy.item.id;
429542
logger.info(`Created agent policy ${agentPolicy.item.id}`);
430543

544+
let latestRevision: number = agentPolicy.item.revision ?? 1;
545+
if (revisionsCount > 0) {
546+
logger.info(`Creating ${revisionsCount} revisions for agent policy ${agentPolicyId}`);
547+
latestRevision = await createPolicyRevisions(agentPolicyId, latestRevision);
548+
}
431549
const statusMap = statusesArg.reduce((acc, status) => ({ ...acc, [status]: count }), {});
432550
logStatusMap(statusMap);
433551
const agents = createAgentsWithStatuses(
434552
statusMap,
435553
agentPolicyId,
436554
agentVersion,
437-
i > 0 ? `batch-${i}` : undefined
555+
i > 0 ? `batch-${i}` : undefined,
556+
latestRevision
438557
);
439558
const createRes = await createAgentDocsBulk(agents);
440559
if (outdatedArg) {

x-pack/platform/plugins/shared/fleet/server/config.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,18 @@ describe('Config schema', () => {
142142
}).not.toThrow();
143143
});
144144

145+
it('should allow to specify fleetPolicyRevisionsCleanup configuration', () => {
146+
expect(() => {
147+
config.schema.validate({
148+
fleetPolicyRevisionsCleanup: {
149+
maxRevisions: 20,
150+
interval: '2h',
151+
maxPoliciesPerRun: 50,
152+
},
153+
});
154+
}).not.toThrow();
155+
});
156+
145157
describe('deprecations', () => {
146158
it('should add two deprecations when trying to enable a non existing experimental feature with enableExperimental', () => {
147159
const res = applyConfigDeprecations({

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,13 @@ export const config: PluginConfigDescriptor = {
386386
taskInterval: schema.maybe(schema.string()),
387387
})
388388
),
389+
fleetPolicyRevisionsCleanup: schema.maybe(
390+
schema.object({
391+
maxRevisions: schema.number({ min: 1, defaultValue: 10 }),
392+
interval: schema.string({ defaultValue: '1h' }),
393+
maxPoliciesPerRun: schema.number({ min: 1, defaultValue: 100 }),
394+
})
395+
),
389396
integrationsHomeOverride: schema.maybe(schema.string()),
390397
prereleaseEnabledByDefault: schema.boolean({ defaultValue: false }),
391398
hideDashboards: schema.boolean({ defaultValue: false }),

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ import { AutomaticAgentUpgradeTask } from './tasks/automatic_agent_upgrade_task'
154154
import { registerPackagesBulkOperationTask } from './tasks/packages_bulk_operations';
155155
import { AutoInstallContentPackagesTask } from './tasks/auto_install_content_packages_task';
156156
import { AgentStatusChangeTask } from './tasks/agent_status_change_task';
157+
import { FleetPolicyRevisionsCleanupTask } from './tasks/fleet_policy_revisions_cleanup/fleet_policy_revisions_cleanup_task';
157158
import { registerSetupTasks } from './tasks/setup';
158159

159160
export interface FleetSetupDeps {
@@ -211,6 +212,7 @@ export interface FleetAppContext {
211212
automaticAgentUpgradeTask: AutomaticAgentUpgradeTask;
212213
autoInstallContentPackagesTask: AutoInstallContentPackagesTask;
213214
agentStatusChangeTask?: AgentStatusChangeTask;
215+
fleetPolicyRevisionsCleanupTask?: FleetPolicyRevisionsCleanupTask;
214216
taskManagerStart?: TaskManagerStartContract;
215217
fetchUsage?: (abortController: AbortController) => Promise<FleetUsage | undefined>;
216218
syncIntegrationsTask: SyncIntegrationsTask;
@@ -325,6 +327,7 @@ export class FleetPlugin
325327
private automaticAgentUpgradeTask?: AutomaticAgentUpgradeTask;
326328
private autoInstallContentPackagesTask?: AutoInstallContentPackagesTask;
327329
private agentStatusChangeTask?: AgentStatusChangeTask;
330+
private fleetPolicyRevisionsCleanupTask?: FleetPolicyRevisionsCleanupTask;
328331

329332
private agentService?: AgentService;
330333
private packageService?: PackageService;
@@ -711,6 +714,16 @@ export class FleetPlugin
711714
taskInterval: config.agentStatusChange?.taskInterval,
712715
},
713716
});
717+
this.fleetPolicyRevisionsCleanupTask = new FleetPolicyRevisionsCleanupTask({
718+
core,
719+
taskManager: deps.taskManager,
720+
logFactory: this.initializerContext.logger,
721+
config: {
722+
maxRevisions: config.fleetPolicyRevisionsCleanup?.maxRevisions,
723+
interval: config.fleetPolicyRevisionsCleanup?.interval,
724+
maxPoliciesPerRun: config.fleetPolicyRevisionsCleanup?.maxPoliciesPerRun,
725+
},
726+
});
714727
this.lockManagerService = new LockManagerService(core, this.initializerContext.logger.get());
715728

716729
// Register fields metadata extractors
@@ -770,6 +783,7 @@ export class FleetPlugin
770783
lockManagerService: this.lockManagerService,
771784
autoInstallContentPackagesTask: this.autoInstallContentPackagesTask!,
772785
agentStatusChangeTask: this.agentStatusChangeTask,
786+
fleetPolicyRevisionsCleanupTask: this.fleetPolicyRevisionsCleanupTask,
773787
alertingStart: plugins.alerting,
774788
});
775789
licenseService.start(plugins.licensing.license$);
@@ -793,6 +807,9 @@ export class FleetPlugin
793807
?.start({ taskManager: plugins.taskManager })
794808
.catch(() => {});
795809
this.agentStatusChangeTask?.start({ taskManager: plugins.taskManager }).catch(() => {});
810+
this.fleetPolicyRevisionsCleanupTask
811+
?.start({ taskManager: plugins.taskManager })
812+
.catch(() => {});
796813

797814
const logger = appContextService.getLogger();
798815

0 commit comments

Comments
 (0)