Skip to content

Commit

Permalink
[Fleet] delete unenrolled agents task (elastic#195544)
Browse files Browse the repository at this point in the history
Closes elastic#189506

Testing steps:
- enable deleting unenrolled agents by adding
`xpack.fleet.enableDeleteUnenrolledAgents: true` to `kibana.dev.yml` or
turn it on on the UI
- add some unenroll agents with the helper script
```
cd x-pack/plugins/fleet
node scripts/create_agents/index.js --status unenrolled --count 10

 info Creating 10 agents with statuses:
 info    unenrolled: 10
 info Batch complete, created 10 agent docs, took 0, errors: false
 info All batches complete. Created 10 agents in total. Goodbye!
```
- restart kibana or wait for the task to run and verify that the
unenrolled agents were deleted
```
[2024-10-08T16:14:45.152+02:00][DEBUG][plugins.fleet.fleet:delete-unenrolled-agents-task:0.0.5] [DeleteUnenrolledAgentsTask] Executed deletion of 10 unenrolled agents
[2024-10-08T16:14:45.153+02:00][INFO ][plugins.fleet.fleet:delete-unenrolled-agents-task:0.0.5] [DeleteUnenrolledAgentsTask] runTask ended: success
```

Added to UI settings:
<img width="1057" alt="image"
src="https://github.com/user-attachments/assets/2c9279f9-86a8-4630-a6cd-5aaa42e05fe7">

If the flag is preconfigured, disabled update on the UI with a tooltip:
<img width="1009" alt="image"
src="https://github.com/user-attachments/assets/45041020-6447-4295-995e-6848f0238f88">

The update is also prevented from the API:
<img width="2522" alt="image"
src="https://github.com/user-attachments/assets/cfbc8e21-e062-4e7f-9d08-9767fa387752">

Once the preconfiguration is removed, the UI update is allowed again.

- [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]>
  • Loading branch information
juliaElastic and kibanamachine committed Oct 14, 2024
1 parent 29c591b commit 643bcad
Show file tree
Hide file tree
Showing 23 changed files with 791 additions and 9 deletions.
7 changes: 4 additions & 3 deletions packages/kbn-check-mappings-update-cli/current_fields.json
Original file line number Diff line number Diff line change
Expand Up @@ -233,9 +233,7 @@
"payload.connector.type",
"type"
],
"cloud-security-posture-settings": [
"rules"
],
"cloud-security-posture-settings": [],
"config": [
"buildNum"
],
Expand Down Expand Up @@ -718,6 +716,9 @@
"vars"
],
"ingest_manager_settings": [
"delete_unenrolled_agents",
"delete_unenrolled_agents.enabled",
"delete_unenrolled_agents.is_preconfigured",
"fleet_server_hosts",
"has_seen_add_data_notice",
"output_secret_storage_requirements_met",
Expand Down
12 changes: 12 additions & 0 deletions packages/kbn-check-mappings-update-cli/current_mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2373,6 +2373,18 @@
},
"ingest_manager_settings": {
"properties": {
"delete_unenrolled_agents": {
"properties": {
"enabled": {
"index": false,
"type": "boolean"
},
"is_preconfigured": {
"index": false,
"type": "boolean"
}
}
},
"fleet_server_hosts": {
"type": "keyword"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,7 @@ export async function checkIncompatibleMappings({
throw createFailError(
`Only mappings changes that are compatible with current mappings are allowed. Consider reaching out to the Kibana core team if you are stuck.`
);
} finally {
await esClient.indices.delete({ index: TEST_INDEX_NAME }).catch(() => {});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"ingest-download-sources": "279a68147e62e4d8858c09ad1cf03bd5551ce58d",
"ingest-outputs": "daafff49255ab700e07491376fe89f04fc998b91",
"ingest-package-policies": "53a94064674835fdb35e5186233bcd7052eabd22",
"ingest_manager_settings": "e794576a05d19dd5306a1e23cbb82c09bffabd65",
"ingest_manager_settings": "111a616eb72627c002029c19feb9e6c439a10505",
"inventory-view": "b8683c8e352a286b4aca1ab21003115a4800af83",
"kql-telemetry": "93c1d16c1a0dfca9c8842062cf5ef8f62ae401ad",
"legacy-url-alias": "9b8cca3fbb2da46fd12823d3cd38fdf1c9f24bc8",
Expand Down
6 changes: 5 additions & 1 deletion x-pack/plugins/fleet/common/types/models/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
export interface BaseSettings {
has_seen_add_data_notice?: boolean;
fleet_server_hosts?: string[];
prerelease_integrations_enabled: boolean;
prerelease_integrations_enabled?: boolean;
}

export interface Settings extends BaseSettings {
Expand All @@ -19,4 +19,8 @@ export interface Settings extends BaseSettings {
output_secret_storage_requirements_met?: boolean;
use_space_awareness_migration_status?: 'pending' | 'success' | 'error';
use_space_awareness_migration_started_at?: string | null;
delete_unenrolled_agents?: {
enabled: boolean;
is_preconfigured: boolean;
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* 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 React, { useCallback, useEffect } from 'react';

import {
EuiTitle,
EuiLink,
EuiSpacer,
EuiDescribedFormGroup,
EuiSwitch,
EuiForm,
EuiFormRow,
EuiToolTip,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';

import { i18n } from '@kbn/i18n';

import {
useAuthz,
useGetSettings,
usePutSettingsMutation,
useStartServices,
} from '../../../../hooks';

export const AdvancedSection: React.FunctionComponent<{}> = ({}) => {
const authz = useAuthz();
const { docLinks, notifications } = useStartServices();
const deleteUnenrolledAgents =
useGetSettings().data?.item?.delete_unenrolled_agents?.enabled ?? false;
const isPreconfigured =
useGetSettings().data?.item?.delete_unenrolled_agents?.is_preconfigured ?? false;
const [deleteUnenrolledAgentsChecked, setDeleteUnenrolledAgentsChecked] =
React.useState<boolean>(deleteUnenrolledAgents);
const { mutateAsync: mutateSettingsAsync } = usePutSettingsMutation();

useEffect(() => {
if (deleteUnenrolledAgents) {
setDeleteUnenrolledAgentsChecked(deleteUnenrolledAgents);
}
}, [deleteUnenrolledAgents]);

const updateSettings = useCallback(
async (deleteFlag: boolean) => {
try {
setDeleteUnenrolledAgentsChecked(deleteFlag);
const res = await mutateSettingsAsync({
delete_unenrolled_agents: {
enabled: deleteFlag,
is_preconfigured: false,
},
});

if (res.error) {
throw res.error;
}
} catch (error) {
setDeleteUnenrolledAgentsChecked(!deleteFlag);
notifications.toasts.addError(error, {
title: i18n.translate('xpack.fleet.errorUpdatingSettings', {
defaultMessage: 'Error updating settings',
}),
});
}
},
[mutateSettingsAsync, notifications.toasts]
);

return (
<>
<EuiTitle size="s">
<h4 data-test-subj="advancedHeader">
<FormattedMessage
id="xpack.fleet.settings.advancedSectionTitle"
defaultMessage="Advanced Settings"
/>
</h4>
</EuiTitle>
<EuiSpacer size="m" />
<EuiForm component="form">
<EuiDescribedFormGroup
title={
<h3>
<FormattedMessage
id="xpack.fleet.settings.deleteUnenrolledAgentsLabel"
defaultMessage="Delete unenrolled agents"
/>
</h3>
}
description={
<p>
<FormattedMessage
id="xpack.fleet.settings.advancedSection.switchLabel"
defaultMessage="Switching on this setting will enable auto deletion of unenrolled agents. For more information see our {docLink}."
values={{
docLink: (
<EuiLink target="_blank" external href={docLinks.links.fleet.settings}>
<FormattedMessage
id="xpack.fleet.settings.advancedSection.link"
defaultMessage="docs"
/>
</EuiLink>
),
}}
/>
</p>
}
>
<EuiFormRow label="">
<EuiToolTip
content={
isPreconfigured
? i18n.translate('xpack.fleet.settings.advancedSection.preconfiguredTitle', {
defaultMessage: 'This setting is preconfigured and cannot be updated.',
})
: undefined
}
>
<EuiSwitch
label={
<FormattedMessage
id="xpack.fleet.settings.deleteUnenrolledAgentsLabel"
defaultMessage="Delete unenrolled agents"
/>
}
checked={deleteUnenrolledAgentsChecked}
onChange={(e) => updateSettings(e.target.checked)}
disabled={!authz.fleet.allSettings || isPreconfigured}
/>
</EuiToolTip>
</EuiFormRow>
</EuiDescribedFormGroup>
</EuiForm>

<EuiSpacer size="m" />
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { FleetServerHostsSection } from './fleet_server_hosts_section';
import { OutputSection } from './output_section';
import { AgentBinarySection } from './agent_binary_section';
import { FleetProxiesSection } from './fleet_proxies_section';
import { AdvancedSection } from './advanced_section';

export interface SettingsPageProps {
outputs: Output[];
Expand Down Expand Up @@ -52,6 +53,8 @@ export const SettingsPage: React.FunctionComponent<SettingsPageProps> = ({
/>
<EuiSpacer size="m" />
<FleetProxiesSection proxies={proxies} deleteFleetProxy={deleteFleetProxy} />
<EuiSpacer size="m" />
<AdvancedSection />
</>
);
};
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export const config: PluginConfigDescriptor = {
schema: schema.object(
{
isAirGapped: schema.maybe(schema.boolean({ defaultValue: false })),
enableDeleteUnenrolledAgents: schema.maybe(schema.boolean({ defaultValue: false })),
registryUrl: schema.maybe(schema.uri({ scheme: ['http', 'https'] })),
registryProxyUrl: schema.maybe(schema.uri({ scheme: ['http', 'https'] })),
agents: schema.object({
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/server/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export class OutputUnauthorizedError extends FleetError {}
export class OutputInvalidError extends FleetError {}
export class OutputLicenceError extends FleetError {}
export class DownloadSourceError extends FleetError {}
export class DeleteUnenrolledAgentsPreconfiguredError extends FleetError {}

// Not found errors
export class AgentNotFoundError extends FleetNotFoundError {}
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/server/mocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export const createAppContextStartContractMock = (
}
: {}),
unenrollInactiveAgentsTask: {} as any,
deleteUnenrolledAgentsTask: {} as any,
};
};

Expand Down
10 changes: 10 additions & 0 deletions x-pack/plugins/fleet/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ import { fetchAgentMetrics } from './services/metrics/fetch_agent_metrics';
import { registerIntegrationFieldsExtractor } from './services/register_integration_fields_extractor';
import { registerUpgradeManagedPackagePoliciesTask } from './services/setup/managed_package_policies';
import { registerDeployAgentPoliciesTask } from './services/agent_policies/deploy_agent_policies_task';
import { DeleteUnenrolledAgentsTask } from './tasks/delete_unenrolled_agents_task';

export interface FleetSetupDeps {
security: SecurityPluginSetup;
Expand Down Expand Up @@ -192,6 +193,7 @@ export interface FleetAppContext {
auditLogger?: AuditLogger;
uninstallTokenService: UninstallTokenServiceInterface;
unenrollInactiveAgentsTask: UnenrollInactiveAgentsTask;
deleteUnenrolledAgentsTask: DeleteUnenrolledAgentsTask;
taskManagerStart?: TaskManagerStartContract;
}

Expand Down Expand Up @@ -284,6 +286,7 @@ export class FleetPlugin
private checkDeletedFilesTask?: CheckDeletedFilesTask;
private fleetMetricsTask?: FleetMetricsTask;
private unenrollInactiveAgentsTask?: UnenrollInactiveAgentsTask;
private deleteUnenrolledAgentsTask?: DeleteUnenrolledAgentsTask;

private agentService?: AgentService;
private packageService?: PackageService;
Expand Down Expand Up @@ -628,6 +631,11 @@ export class FleetPlugin
taskManager: deps.taskManager,
logFactory: this.initializerContext.logger,
});
this.deleteUnenrolledAgentsTask = new DeleteUnenrolledAgentsTask({
core,
taskManager: deps.taskManager,
logFactory: this.initializerContext.logger,
});

// Register fields metadata extractor
registerIntegrationFieldsExtractor({ core, fieldsMetadata: deps.fieldsMetadata });
Expand Down Expand Up @@ -674,6 +682,7 @@ export class FleetPlugin
messageSigningService,
uninstallTokenService,
unenrollInactiveAgentsTask: this.unenrollInactiveAgentsTask!,
deleteUnenrolledAgentsTask: this.deleteUnenrolledAgentsTask!,
taskManagerStart: plugins.taskManager,
});
licenseService.start(plugins.licensing.license$);
Expand All @@ -682,6 +691,7 @@ export class FleetPlugin
this.fleetUsageSender?.start(plugins.taskManager).catch(() => {});
this.checkDeletedFilesTask?.start({ taskManager: plugins.taskManager }).catch(() => {});
this.unenrollInactiveAgentsTask?.start({ taskManager: plugins.taskManager }).catch(() => {});
this.deleteUnenrolledAgentsTask?.start({ taskManager: plugins.taskManager }).catch(() => {});
startFleetUsageLogger(plugins.taskManager).catch(() => {});
this.fleetMetricsTask
?.start(plugins.taskManager, core.elasticsearch.client.asInternalUser)
Expand Down
21 changes: 21 additions & 0 deletions x-pack/plugins/fleet/server/saved_objects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,12 @@ export const getSavedObjectTypes = (
output_secret_storage_requirements_met: { type: 'boolean' },
use_space_awareness_migration_status: { type: 'keyword', index: false },
use_space_awareness_migration_started_at: { type: 'date', index: false },
delete_unenrolled_agents: {
properties: {
enabled: { type: 'boolean', index: false },
is_preconfigured: { type: 'boolean', index: false },
},
},
},
},
migrations: {
Expand All @@ -181,6 +187,21 @@ export const getSavedObjectTypes = (
},
],
},
3: {
changes: [
{
type: 'mappings_addition',
addedMappings: {
delete_unenrolled_agents: {
properties: {
enabled: { type: 'boolean', index: false },
is_preconfigured: { type: 'boolean', index: false },
},
},
},
},
],
},
},
},
[LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE]: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ import { getSettings } from '../../settings';
export async function getPrereleaseFromSettings(
savedObjectsClient: SavedObjectsClientContract
): Promise<boolean> {
let prerelease: boolean = false;
let prerelease: boolean | undefined = false;
try {
({ prerelease_integrations_enabled: prerelease } = await getSettings(savedObjectsClient));
} catch (err) {
appContextService
.getLogger()
.warn('Error while trying to load prerelease flag from settings, defaulting to false', err);
}
return prerelease;
return prerelease ?? false;
}
Loading

0 comments on commit 643bcad

Please sign in to comment.