From c53b2a8bb056af90fb100c839d0e626bf4797760 Mon Sep 17 00:00:00 2001
From: Julia Bardi <90178898+juliaElastic@users.noreply.github.com>
Date: Mon, 14 Oct 2024 11:04:01 +0200
Subject: [PATCH] [Fleet] delete unenrolled agents task (#195544)
## Summary
Closes https://github.com/elastic/kibana/issues/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:
If the flag is preconfigured, disabled update on the UI with a tooltip:
The update is also prevented from the API:
Once the preconfiguration is removed, the UI update is allowed again.
### 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 <42973632+kibanamachine@users.noreply.github.com>
---
oas_docs/bundle.json | 52 ++++-
oas_docs/bundle.serverless.json | 52 ++++-
.../output/kibana.serverless.staging.yaml | 37 +++-
oas_docs/output/kibana.serverless.yaml | 37 +++-
oas_docs/output/kibana.staging.yaml | 37 +++-
oas_docs/output/kibana.yaml | 37 +++-
.../current_fields.json | 7 +-
.../current_mappings.json | 12 ++
.../check_incompatible_mappings.ts | 2 +
.../check_registered_types.test.ts | 2 +-
.../fleet/common/types/models/settings.ts | 6 +-
.../settings_page/advanced_section.tsx | 143 ++++++++++++++
.../components/settings_page/index.tsx | 3 +
x-pack/plugins/fleet/server/config.ts | 1 +
x-pack/plugins/fleet/server/errors/index.ts | 1 +
x-pack/plugins/fleet/server/mocks/index.ts | 1 +
x-pack/plugins/fleet/server/plugin.ts | 10 +
.../fleet/server/routes/output/index.ts | 2 +-
.../routes/settings/settings_handler.test.ts | 8 +
.../fleet/server/saved_objects/index.ts | 21 ++
.../epm/packages/get_prerelease_setting.ts | 4 +-
.../delete_unenrolled_agent_setting.test.ts | 71 +++++++
.../delete_unenrolled_agent_setting.ts | 39 ++++
.../fleet/server/services/settings.test.ts | 117 +++++++++++
.../plugins/fleet/server/services/settings.ts | 18 +-
x-pack/plugins/fleet/server/services/setup.ts | 10 +
.../delete_unenrolled_agents_task.test.ts | 138 +++++++++++++
.../tasks/delete_unenrolled_agents_task.ts | 181 ++++++++++++++++++
.../fleet/server/types/rest_spec/settings.ts | 14 +-
.../fleet/server/types/so_attributes.ts | 6 +-
.../check_registered_task_types.ts | 1 +
31 files changed, 1041 insertions(+), 29 deletions(-)
create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/advanced_section.tsx
create mode 100644 x-pack/plugins/fleet/server/services/preconfiguration/delete_unenrolled_agent_setting.test.ts
create mode 100644 x-pack/plugins/fleet/server/services/preconfiguration/delete_unenrolled_agent_setting.ts
create mode 100644 x-pack/plugins/fleet/server/tasks/delete_unenrolled_agents_task.test.ts
create mode 100644 x-pack/plugins/fleet/server/tasks/delete_unenrolled_agents_task.ts
diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json
index e52362ff13a6a..8fdffea9bac41 100644
--- a/oas_docs/bundle.json
+++ b/oas_docs/bundle.json
@@ -26150,7 +26150,7 @@
},
"/api/fleet/logstash_api_keys": {
"post": {
- "description": "Generate Logstash API keyy",
+ "description": "Generate Logstash API key",
"operationId": "%2Fapi%2Ffleet%2Flogstash_api_keys#0",
"parameters": [
{
@@ -40259,6 +40259,22 @@
"item": {
"additionalProperties": false,
"properties": {
+ "delete_unenrolled_agents": {
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "is_preconfigured": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "enabled",
+ "is_preconfigured"
+ ],
+ "type": "object"
+ },
"fleet_server_hosts": {
"items": {
"type": "string"
@@ -40305,7 +40321,6 @@
}
},
"required": [
- "prerelease_integrations_enabled",
"id"
],
"type": "object"
@@ -40404,6 +40419,22 @@
"additional_yaml_config": {
"type": "string"
},
+ "delete_unenrolled_agents": {
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "is_preconfigured": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "enabled",
+ "is_preconfigured"
+ ],
+ "type": "object"
+ },
"fleet_server_hosts": {
"items": {
"format": "uri",
@@ -40443,6 +40474,22 @@
"item": {
"additionalProperties": false,
"properties": {
+ "delete_unenrolled_agents": {
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "is_preconfigured": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "enabled",
+ "is_preconfigured"
+ ],
+ "type": "object"
+ },
"fleet_server_hosts": {
"items": {
"type": "string"
@@ -40489,7 +40536,6 @@
}
},
"required": [
- "prerelease_integrations_enabled",
"id"
],
"type": "object"
diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json
index 531ab412ce1bf..4719fcb479bb5 100644
--- a/oas_docs/bundle.serverless.json
+++ b/oas_docs/bundle.serverless.json
@@ -26150,7 +26150,7 @@
},
"/api/fleet/logstash_api_keys": {
"post": {
- "description": "Generate Logstash API keyy",
+ "description": "Generate Logstash API key",
"operationId": "%2Fapi%2Ffleet%2Flogstash_api_keys#0",
"parameters": [
{
@@ -40259,6 +40259,22 @@
"item": {
"additionalProperties": false,
"properties": {
+ "delete_unenrolled_agents": {
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "is_preconfigured": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "enabled",
+ "is_preconfigured"
+ ],
+ "type": "object"
+ },
"fleet_server_hosts": {
"items": {
"type": "string"
@@ -40305,7 +40321,6 @@
}
},
"required": [
- "prerelease_integrations_enabled",
"id"
],
"type": "object"
@@ -40404,6 +40419,22 @@
"additional_yaml_config": {
"type": "string"
},
+ "delete_unenrolled_agents": {
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "is_preconfigured": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "enabled",
+ "is_preconfigured"
+ ],
+ "type": "object"
+ },
"fleet_server_hosts": {
"items": {
"format": "uri",
@@ -40443,6 +40474,22 @@
"item": {
"additionalProperties": false,
"properties": {
+ "delete_unenrolled_agents": {
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "is_preconfigured": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "enabled",
+ "is_preconfigured"
+ ],
+ "type": "object"
+ },
"fleet_server_hosts": {
"items": {
"type": "string"
@@ -40489,7 +40536,6 @@
}
},
"required": [
- "prerelease_integrations_enabled",
"id"
],
"type": "object"
diff --git a/oas_docs/output/kibana.serverless.staging.yaml b/oas_docs/output/kibana.serverless.staging.yaml
index dc95e1014073a..aad5256524f4a 100644
--- a/oas_docs/output/kibana.serverless.staging.yaml
+++ b/oas_docs/output/kibana.serverless.staging.yaml
@@ -23652,7 +23652,7 @@ paths:
- Elastic Agent policies
/api/fleet/logstash_api_keys:
post:
- description: Generate Logstash API keyy
+ description: Generate Logstash API key
operationId: '%2Fapi%2Ffleet%2Flogstash_api_keys#0'
parameters:
- description: The version of the API to use
@@ -33283,6 +33283,17 @@ paths:
additionalProperties: false
type: object
properties:
+ delete_unenrolled_agents:
+ additionalProperties: false
+ type: object
+ properties:
+ enabled:
+ type: boolean
+ is_preconfigured:
+ type: boolean
+ required:
+ - enabled
+ - is_preconfigured
fleet_server_hosts:
items:
type: string
@@ -33314,7 +33325,6 @@ paths:
version:
type: string
required:
- - prerelease_integrations_enabled
- id
required:
- item
@@ -33376,6 +33386,17 @@ paths:
properties:
additional_yaml_config:
type: string
+ delete_unenrolled_agents:
+ additionalProperties: false
+ type: object
+ properties:
+ enabled:
+ type: boolean
+ is_preconfigured:
+ type: boolean
+ required:
+ - enabled
+ - is_preconfigured
fleet_server_hosts:
items:
format: uri
@@ -33404,6 +33425,17 @@ paths:
additionalProperties: false
type: object
properties:
+ delete_unenrolled_agents:
+ additionalProperties: false
+ type: object
+ properties:
+ enabled:
+ type: boolean
+ is_preconfigured:
+ type: boolean
+ required:
+ - enabled
+ - is_preconfigured
fleet_server_hosts:
items:
type: string
@@ -33435,7 +33467,6 @@ paths:
version:
type: string
required:
- - prerelease_integrations_enabled
- id
required:
- item
diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml
index dc95e1014073a..aad5256524f4a 100644
--- a/oas_docs/output/kibana.serverless.yaml
+++ b/oas_docs/output/kibana.serverless.yaml
@@ -23652,7 +23652,7 @@ paths:
- Elastic Agent policies
/api/fleet/logstash_api_keys:
post:
- description: Generate Logstash API keyy
+ description: Generate Logstash API key
operationId: '%2Fapi%2Ffleet%2Flogstash_api_keys#0'
parameters:
- description: The version of the API to use
@@ -33283,6 +33283,17 @@ paths:
additionalProperties: false
type: object
properties:
+ delete_unenrolled_agents:
+ additionalProperties: false
+ type: object
+ properties:
+ enabled:
+ type: boolean
+ is_preconfigured:
+ type: boolean
+ required:
+ - enabled
+ - is_preconfigured
fleet_server_hosts:
items:
type: string
@@ -33314,7 +33325,6 @@ paths:
version:
type: string
required:
- - prerelease_integrations_enabled
- id
required:
- item
@@ -33376,6 +33386,17 @@ paths:
properties:
additional_yaml_config:
type: string
+ delete_unenrolled_agents:
+ additionalProperties: false
+ type: object
+ properties:
+ enabled:
+ type: boolean
+ is_preconfigured:
+ type: boolean
+ required:
+ - enabled
+ - is_preconfigured
fleet_server_hosts:
items:
format: uri
@@ -33404,6 +33425,17 @@ paths:
additionalProperties: false
type: object
properties:
+ delete_unenrolled_agents:
+ additionalProperties: false
+ type: object
+ properties:
+ enabled:
+ type: boolean
+ is_preconfigured:
+ type: boolean
+ required:
+ - enabled
+ - is_preconfigured
fleet_server_hosts:
items:
type: string
@@ -33435,7 +33467,6 @@ paths:
version:
type: string
required:
- - prerelease_integrations_enabled
- id
required:
- item
diff --git a/oas_docs/output/kibana.staging.yaml b/oas_docs/output/kibana.staging.yaml
index 222a1c5d162b8..d8310ef797387 100644
--- a/oas_docs/output/kibana.staging.yaml
+++ b/oas_docs/output/kibana.staging.yaml
@@ -27081,7 +27081,7 @@ paths:
- Elastic Agent policies
/api/fleet/logstash_api_keys:
post:
- description: Generate Logstash API keyy
+ description: Generate Logstash API key
operationId: '%2Fapi%2Ffleet%2Flogstash_api_keys#0'
parameters:
- description: The version of the API to use
@@ -36712,6 +36712,17 @@ paths:
additionalProperties: false
type: object
properties:
+ delete_unenrolled_agents:
+ additionalProperties: false
+ type: object
+ properties:
+ enabled:
+ type: boolean
+ is_preconfigured:
+ type: boolean
+ required:
+ - enabled
+ - is_preconfigured
fleet_server_hosts:
items:
type: string
@@ -36743,7 +36754,6 @@ paths:
version:
type: string
required:
- - prerelease_integrations_enabled
- id
required:
- item
@@ -36805,6 +36815,17 @@ paths:
properties:
additional_yaml_config:
type: string
+ delete_unenrolled_agents:
+ additionalProperties: false
+ type: object
+ properties:
+ enabled:
+ type: boolean
+ is_preconfigured:
+ type: boolean
+ required:
+ - enabled
+ - is_preconfigured
fleet_server_hosts:
items:
format: uri
@@ -36833,6 +36854,17 @@ paths:
additionalProperties: false
type: object
properties:
+ delete_unenrolled_agents:
+ additionalProperties: false
+ type: object
+ properties:
+ enabled:
+ type: boolean
+ is_preconfigured:
+ type: boolean
+ required:
+ - enabled
+ - is_preconfigured
fleet_server_hosts:
items:
type: string
@@ -36864,7 +36896,6 @@ paths:
version:
type: string
required:
- - prerelease_integrations_enabled
- id
required:
- item
diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml
index 222a1c5d162b8..d8310ef797387 100644
--- a/oas_docs/output/kibana.yaml
+++ b/oas_docs/output/kibana.yaml
@@ -27081,7 +27081,7 @@ paths:
- Elastic Agent policies
/api/fleet/logstash_api_keys:
post:
- description: Generate Logstash API keyy
+ description: Generate Logstash API key
operationId: '%2Fapi%2Ffleet%2Flogstash_api_keys#0'
parameters:
- description: The version of the API to use
@@ -36712,6 +36712,17 @@ paths:
additionalProperties: false
type: object
properties:
+ delete_unenrolled_agents:
+ additionalProperties: false
+ type: object
+ properties:
+ enabled:
+ type: boolean
+ is_preconfigured:
+ type: boolean
+ required:
+ - enabled
+ - is_preconfigured
fleet_server_hosts:
items:
type: string
@@ -36743,7 +36754,6 @@ paths:
version:
type: string
required:
- - prerelease_integrations_enabled
- id
required:
- item
@@ -36805,6 +36815,17 @@ paths:
properties:
additional_yaml_config:
type: string
+ delete_unenrolled_agents:
+ additionalProperties: false
+ type: object
+ properties:
+ enabled:
+ type: boolean
+ is_preconfigured:
+ type: boolean
+ required:
+ - enabled
+ - is_preconfigured
fleet_server_hosts:
items:
format: uri
@@ -36833,6 +36854,17 @@ paths:
additionalProperties: false
type: object
properties:
+ delete_unenrolled_agents:
+ additionalProperties: false
+ type: object
+ properties:
+ enabled:
+ type: boolean
+ is_preconfigured:
+ type: boolean
+ required:
+ - enabled
+ - is_preconfigured
fleet_server_hosts:
items:
type: string
@@ -36864,7 +36896,6 @@ paths:
version:
type: string
required:
- - prerelease_integrations_enabled
- id
required:
- item
diff --git a/packages/kbn-check-mappings-update-cli/current_fields.json b/packages/kbn-check-mappings-update-cli/current_fields.json
index 3bd084fc677a6..fa55bd4800c8b 100644
--- a/packages/kbn-check-mappings-update-cli/current_fields.json
+++ b/packages/kbn-check-mappings-update-cli/current_fields.json
@@ -233,9 +233,7 @@
"payload.connector.type",
"type"
],
- "cloud-security-posture-settings": [
- "rules"
- ],
+ "cloud-security-posture-settings": [],
"config": [
"buildNum"
],
@@ -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",
diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json
index 9eb4375f082da..9f94d36af50f7 100644
--- a/packages/kbn-check-mappings-update-cli/current_mappings.json
+++ b/packages/kbn-check-mappings-update-cli/current_mappings.json
@@ -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"
},
diff --git a/packages/kbn-check-mappings-update-cli/src/compatibility/check_incompatible_mappings.ts b/packages/kbn-check-mappings-update-cli/src/compatibility/check_incompatible_mappings.ts
index 1355e8547250e..ce9e2acef8d4c 100644
--- a/packages/kbn-check-mappings-update-cli/src/compatibility/check_incompatible_mappings.ts
+++ b/packages/kbn-check-mappings-update-cli/src/compatibility/check_incompatible_mappings.ts
@@ -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(() => {});
}
}
diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts
index d6cfcae558b2c..c662bf9e5f753 100644
--- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts
+++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts
@@ -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",
diff --git a/x-pack/plugins/fleet/common/types/models/settings.ts b/x-pack/plugins/fleet/common/types/models/settings.ts
index 9a5166e41df96..a63211ef14c55 100644
--- a/x-pack/plugins/fleet/common/types/models/settings.ts
+++ b/x-pack/plugins/fleet/common/types/models/settings.ts
@@ -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 {
@@ -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;
+ };
}
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/advanced_section.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/advanced_section.tsx
new file mode 100644
index 0000000000000..c4e9478e0762e
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/advanced_section.tsx
@@ -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(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 (
+ <>
+
+
+
+
+
+
+
+
+
+
+ }
+ description={
+
+
+
+
+ ),
+ }}
+ />
+
+ }
+ >
+
+
+
+ }
+ checked={deleteUnenrolledAgentsChecked}
+ onChange={(e) => updateSettings(e.target.checked)}
+ disabled={!authz.fleet.allSettings || isPreconfigured}
+ />
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/index.tsx
index 809639ecae692..cfeb76ba0d240 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/index.tsx
@@ -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[];
@@ -52,6 +53,8 @@ export const SettingsPage: React.FunctionComponent = ({
/>
+
+
>
);
};
diff --git a/x-pack/plugins/fleet/server/config.ts b/x-pack/plugins/fleet/server/config.ts
index 6df693096f7c6..1797c30d15f4d 100644
--- a/x-pack/plugins/fleet/server/config.ts
+++ b/x-pack/plugins/fleet/server/config.ts
@@ -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({
diff --git a/x-pack/plugins/fleet/server/errors/index.ts b/x-pack/plugins/fleet/server/errors/index.ts
index 09b387e7a5cee..6782b8122a552 100644
--- a/x-pack/plugins/fleet/server/errors/index.ts
+++ b/x-pack/plugins/fleet/server/errors/index.ts
@@ -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 {}
diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts
index 17f85cd252c2b..43b113899072e 100644
--- a/x-pack/plugins/fleet/server/mocks/index.ts
+++ b/x-pack/plugins/fleet/server/mocks/index.ts
@@ -139,6 +139,7 @@ export const createAppContextStartContractMock = (
}
: {}),
unenrollInactiveAgentsTask: {} as any,
+ deleteUnenrolledAgentsTask: {} as any,
};
};
diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts
index c767424cef36a..ced8da63e9703 100644
--- a/x-pack/plugins/fleet/server/plugin.ts
+++ b/x-pack/plugins/fleet/server/plugin.ts
@@ -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;
@@ -192,6 +193,7 @@ export interface FleetAppContext {
auditLogger?: AuditLogger;
uninstallTokenService: UninstallTokenServiceInterface;
unenrollInactiveAgentsTask: UnenrollInactiveAgentsTask;
+ deleteUnenrolledAgentsTask: DeleteUnenrolledAgentsTask;
taskManagerStart?: TaskManagerStartContract;
}
@@ -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;
@@ -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 });
@@ -674,6 +682,7 @@ export class FleetPlugin
messageSigningService,
uninstallTokenService,
unenrollInactiveAgentsTask: this.unenrollInactiveAgentsTask!,
+ deleteUnenrolledAgentsTask: this.deleteUnenrolledAgentsTask!,
taskManagerStart: plugins.taskManager,
});
licenseService.start(plugins.licensing.license$);
@@ -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)
diff --git a/x-pack/plugins/fleet/server/routes/output/index.ts b/x-pack/plugins/fleet/server/routes/output/index.ts
index a90735f053208..c9d5b6acdd7d3 100644
--- a/x-pack/plugins/fleet/server/routes/output/index.ts
+++ b/x-pack/plugins/fleet/server/routes/output/index.ts
@@ -189,7 +189,7 @@ export const registerRoutes = (router: FleetAuthzRouter) => {
fleetAuthz: {
fleet: { allSettings: true },
},
- description: 'Generate Logstash API keyy',
+ description: 'Generate Logstash API key',
options: {
tags: ['oas-tag:Fleet outputs'],
},
diff --git a/x-pack/plugins/fleet/server/routes/settings/settings_handler.test.ts b/x-pack/plugins/fleet/server/routes/settings/settings_handler.test.ts
index 0b52c44cde269..73641bfaed71a 100644
--- a/x-pack/plugins/fleet/server/routes/settings/settings_handler.test.ts
+++ b/x-pack/plugins/fleet/server/routes/settings/settings_handler.test.ts
@@ -31,6 +31,10 @@ jest.mock('../../services', () => ({
has_seen_add_data_notice: true,
fleet_server_hosts: ['http://localhost:8220'],
prerelease_integrations_enabled: true,
+ delete_unenrolled_agents: {
+ enabled: true,
+ is_preconfigured: false,
+ },
}),
},
appContextService: {
@@ -74,6 +78,10 @@ describe('SettingsHandler', () => {
has_seen_add_data_notice: true,
fleet_server_hosts: ['http://localhost:8220'],
prerelease_integrations_enabled: true,
+ delete_unenrolled_agents: {
+ enabled: true,
+ is_preconfigured: false,
+ },
},
};
expect(response.ok).toHaveBeenCalledWith({
diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts
index 0eb6c86df01e2..ffb9381f8b30c 100644
--- a/x-pack/plugins/fleet/server/saved_objects/index.ts
+++ b/x-pack/plugins/fleet/server/saved_objects/index.ts
@@ -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: {
@@ -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]: {
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get_prerelease_setting.ts b/x-pack/plugins/fleet/server/services/epm/packages/get_prerelease_setting.ts
index df4b47d13ef2f..48783fa7a6b54 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/get_prerelease_setting.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/get_prerelease_setting.ts
@@ -13,7 +13,7 @@ import { getSettings } from '../../settings';
export async function getPrereleaseFromSettings(
savedObjectsClient: SavedObjectsClientContract
): Promise {
- let prerelease: boolean = false;
+ let prerelease: boolean | undefined = false;
try {
({ prerelease_integrations_enabled: prerelease } = await getSettings(savedObjectsClient));
} catch (err) {
@@ -21,5 +21,5 @@ export async function getPrereleaseFromSettings(
.getLogger()
.warn('Error while trying to load prerelease flag from settings, defaulting to false', err);
}
- return prerelease;
+ return prerelease ?? false;
}
diff --git a/x-pack/plugins/fleet/server/services/preconfiguration/delete_unenrolled_agent_setting.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration/delete_unenrolled_agent_setting.test.ts
new file mode 100644
index 0000000000000..aa1a7573f225b
--- /dev/null
+++ b/x-pack/plugins/fleet/server/services/preconfiguration/delete_unenrolled_agent_setting.test.ts
@@ -0,0 +1,71 @@
+/*
+ * 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 { settingsService } from '..';
+
+import { ensureDeleteUnenrolledAgentsSetting } from './delete_unenrolled_agent_setting';
+
+jest.mock('..', () => ({
+ settingsService: {
+ getSettingsOrUndefined: jest.fn(),
+ saveSettings: jest.fn(),
+ },
+}));
+
+describe('delete_unenrolled_agent_setting', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should update settings with delete_unenrolled_agents enabled', async () => {
+ await ensureDeleteUnenrolledAgentsSetting({} as any, true);
+
+ expect(settingsService.saveSettings).toHaveBeenCalledWith(
+ expect.anything(),
+ { delete_unenrolled_agents: { enabled: true, is_preconfigured: true } },
+ { fromSetup: true }
+ );
+ });
+
+ it('should update settings with delete_unenrolled_agents disabled', async () => {
+ await ensureDeleteUnenrolledAgentsSetting({} as any, false);
+
+ expect(settingsService.saveSettings).toHaveBeenCalledWith(
+ expect.anything(),
+ { delete_unenrolled_agents: { enabled: false, is_preconfigured: true } },
+ { fromSetup: true }
+ );
+ });
+
+ it('should update settings when previously preconfigured', async () => {
+ (settingsService.getSettingsOrUndefined as jest.Mock).mockResolvedValue({
+ delete_unenrolled_agents: {
+ enabled: false,
+ is_preconfigured: true,
+ },
+ });
+ await ensureDeleteUnenrolledAgentsSetting({} as any);
+
+ expect(settingsService.saveSettings).toHaveBeenCalledWith(
+ expect.anything(),
+ { delete_unenrolled_agents: { enabled: false, is_preconfigured: false } },
+ { fromSetup: true }
+ );
+ });
+
+ it('should not update settings when previously not preconfigured', async () => {
+ (settingsService.getSettingsOrUndefined as jest.Mock).mockResolvedValue({
+ delete_unenrolled_agents: {
+ enabled: false,
+ is_preconfigured: false,
+ },
+ });
+ await ensureDeleteUnenrolledAgentsSetting({} as any);
+
+ expect(settingsService.saveSettings).not.toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/plugins/fleet/server/services/preconfiguration/delete_unenrolled_agent_setting.ts b/x-pack/plugins/fleet/server/services/preconfiguration/delete_unenrolled_agent_setting.ts
new file mode 100644
index 0000000000000..ba54e1c5146b2
--- /dev/null
+++ b/x-pack/plugins/fleet/server/services/preconfiguration/delete_unenrolled_agent_setting.ts
@@ -0,0 +1,39 @@
+/*
+ * 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 type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
+
+import { settingsService } from '..';
+import type { FleetConfigType } from '../../config';
+
+export function getPreconfiguredDeleteUnenrolledAgentsSettingFromConfig(
+ config?: FleetConfigType
+): boolean | undefined {
+ return config.enableDeleteUnenrolledAgents;
+}
+
+export async function ensureDeleteUnenrolledAgentsSetting(
+ soClient: SavedObjectsClientContract,
+ enableDeleteUnenrolledAgents?: boolean
+) {
+ if (enableDeleteUnenrolledAgents === undefined) {
+ const settings = await settingsService.getSettingsOrUndefined(soClient);
+ if (!settings?.delete_unenrolled_agents?.is_preconfigured) {
+ return;
+ }
+ }
+ await settingsService.saveSettings(
+ soClient,
+ {
+ delete_unenrolled_agents: {
+ enabled: !!enableDeleteUnenrolledAgents,
+ is_preconfigured: enableDeleteUnenrolledAgents !== undefined,
+ },
+ },
+ { fromSetup: true }
+ );
+}
diff --git a/x-pack/plugins/fleet/server/services/settings.test.ts b/x-pack/plugins/fleet/server/services/settings.test.ts
index 33926b0ec12f7..92fb85a335775 100644
--- a/x-pack/plugins/fleet/server/services/settings.test.ts
+++ b/x-pack/plugins/fleet/server/services/settings.test.ts
@@ -14,6 +14,8 @@ import { GLOBAL_SETTINGS_ID, GLOBAL_SETTINGS_SAVED_OBJECT_TYPE } from '../../com
import type { Settings } from '../types';
+import { DeleteUnenrolledAgentsPreconfiguredError } from '../errors';
+
import { appContextService } from './app_context';
import { getSettings, saveSettings, settingsSetup } from './settings';
import { auditLoggingService } from './audit_logging';
@@ -225,4 +227,119 @@ describe('saveSettings', () => {
});
});
});
+
+ it('should allow updating preconfigured setting if called from setup', async () => {
+ const soClient = savedObjectsClientMock.create();
+
+ const newData: Partial> = {
+ delete_unenrolled_agents: {
+ enabled: true,
+ is_preconfigured: true,
+ },
+ };
+
+ soClient.find.mockResolvedValueOnce({
+ saved_objects: [
+ {
+ id: GLOBAL_SETTINGS_ID,
+ attributes: {
+ delete_unenrolled_agents: {
+ enabled: false,
+ is_preconfigured: true,
+ },
+ },
+ references: [],
+ type: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
+ score: 0,
+ },
+ ],
+ page: 1,
+ per_page: 10,
+ total: 1,
+ });
+ mockListFleetServerHosts.mockResolvedValueOnce({
+ items: [
+ {
+ id: 'fleet-server-host',
+ name: 'Fleet Server Host',
+ is_default: true,
+ is_preconfigured: false,
+ host_urls: ['http://localhost:8220'],
+ },
+ ],
+ page: 1,
+ perPage: 10,
+ total: 1,
+ });
+
+ soClient.update.mockResolvedValueOnce({
+ id: GLOBAL_SETTINGS_ID,
+ attributes: {},
+ references: [],
+ type: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
+ });
+
+ await saveSettings(soClient, newData, { fromSetup: true });
+
+ expect(soClient.update).toHaveBeenCalled();
+ });
+
+ it('should not allow updating preconfigured setting if not called from setup', async () => {
+ const soClient = savedObjectsClientMock.create();
+
+ const newData: Partial> = {
+ delete_unenrolled_agents: {
+ enabled: true,
+ is_preconfigured: true,
+ },
+ };
+
+ soClient.find.mockResolvedValueOnce({
+ saved_objects: [
+ {
+ id: GLOBAL_SETTINGS_ID,
+ attributes: {
+ delete_unenrolled_agents: {
+ enabled: false,
+ is_preconfigured: true,
+ },
+ },
+ references: [],
+ type: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
+ score: 0,
+ },
+ ],
+ page: 1,
+ per_page: 10,
+ total: 1,
+ });
+ mockListFleetServerHosts.mockResolvedValueOnce({
+ items: [
+ {
+ id: 'fleet-server-host',
+ name: 'Fleet Server Host',
+ is_default: true,
+ is_preconfigured: false,
+ host_urls: ['http://localhost:8220'],
+ },
+ ],
+ page: 1,
+ perPage: 10,
+ total: 1,
+ });
+
+ soClient.update.mockResolvedValueOnce({
+ id: GLOBAL_SETTINGS_ID,
+ attributes: {},
+ references: [],
+ type: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
+ });
+
+ try {
+ await saveSettings(soClient, newData);
+ fail('Expected to throw');
+ } catch (e) {
+ expect(e).toBeInstanceOf(DeleteUnenrolledAgentsPreconfiguredError);
+ }
+ });
});
diff --git a/x-pack/plugins/fleet/server/services/settings.ts b/x-pack/plugins/fleet/server/services/settings.ts
index 394a2365f3c03..5f7433403c4e6 100644
--- a/x-pack/plugins/fleet/server/services/settings.ts
+++ b/x-pack/plugins/fleet/server/services/settings.ts
@@ -13,6 +13,8 @@ import { GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, GLOBAL_SETTINGS_ID } from '../../com
import type { Settings, BaseSettings } from '../../common/types';
import type { SettingsSOAttributes } from '../types';
+import { DeleteUnenrolledAgentsPreconfiguredError } from '../errors';
+
import { appContextService } from './app_context';
import { listFleetServerHosts } from './fleet_server_host';
import { auditLoggingService } from './audit_logging';
@@ -47,6 +49,7 @@ export async function getSettings(soClient: SavedObjectsClientContract): Promise
settingsSo.attributes.use_space_awareness_migration_started_at,
fleet_server_hosts: fleetServerHosts.items.flatMap((item) => item.host_urls),
preconfigured_fields: getConfigFleetServerHosts() ? ['fleet_server_hosts'] : [],
+ delete_unenrolled_agents: settingsSo.attributes.delete_unenrolled_agents,
};
}
@@ -80,7 +83,10 @@ export async function settingsSetup(soClient: SavedObjectsClientContract) {
export async function saveSettings(
soClient: SavedObjectsClientContract,
newData: Partial>,
- options?: SavedObjectsUpdateOptions & { createWithOverwrite?: boolean }
+ options?: SavedObjectsUpdateOptions & {
+ createWithOverwrite?: boolean;
+ fromSetup?: boolean;
+ }
): Promise & Pick> {
const data = { ...newData };
if (data.fleet_server_hosts) {
@@ -91,6 +97,16 @@ export async function saveSettings(
try {
const settings = await getSettings(soClient);
+ if (
+ !options?.fromSetup &&
+ settings.delete_unenrolled_agents?.is_preconfigured &&
+ data.delete_unenrolled_agents
+ ) {
+ throw new DeleteUnenrolledAgentsPreconfiguredError(
+ `Setting delete_unenrolled_agents is preconfigured as 'enableDeleteUnenrolledAgents' and cannot be updated outside of kibana config file.`
+ );
+ }
+
auditLoggingService.writeCustomSoAuditLog({
action: 'update',
id: settings.id,
diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts
index c0b86d6394769..0d6ec183531a4 100644
--- a/x-pack/plugins/fleet/server/services/setup.ts
+++ b/x-pack/plugins/fleet/server/services/setup.ts
@@ -53,6 +53,10 @@ import { cleanUpOldFileIndices } from './setup/clean_old_fleet_indices';
import type { UninstallTokenInvalidError } from './security/uninstall_token_service';
import { ensureAgentPoliciesFleetServerKeysAndPolicies } from './setup/fleet_server_policies_enrollment_keys';
import { ensureSpaceSettings } from './preconfiguration/space_settings';
+import {
+ ensureDeleteUnenrolledAgentsSetting,
+ getPreconfiguredDeleteUnenrolledAgentsSettingFromConfig,
+} from './preconfiguration/delete_unenrolled_agent_setting';
export interface SetupStatus {
isInitialized: boolean;
@@ -195,6 +199,12 @@ async function createSetupSideEffects(
logger.debug('Setting up Space settings');
await ensureSpaceSettings(appContextService.getConfig()?.spaceSettings ?? []);
+ logger.debug('Setting up delete unenrolled agents setting');
+ await ensureDeleteUnenrolledAgentsSetting(
+ soClient,
+ getPreconfiguredDeleteUnenrolledAgentsSettingFromConfig(appContextService.getConfig())
+ );
+
logger.debug('Setting up Fleet outputs');
await Promise.all([
ensurePreconfiguredOutputs(
diff --git a/x-pack/plugins/fleet/server/tasks/delete_unenrolled_agents_task.test.ts b/x-pack/plugins/fleet/server/tasks/delete_unenrolled_agents_task.test.ts
new file mode 100644
index 0000000000000..54996c27975e3
--- /dev/null
+++ b/x-pack/plugins/fleet/server/tasks/delete_unenrolled_agents_task.test.ts
@@ -0,0 +1,138 @@
+/*
+ * 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 type { ElasticsearchClientMock } from '@kbn/core/server/mocks';
+import { coreMock } from '@kbn/core/server/mocks';
+import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
+import type { TaskManagerSetupContract } from '@kbn/task-manager-plugin/server';
+import { TaskStatus } from '@kbn/task-manager-plugin/server';
+import { getDeleteTaskRunResult } from '@kbn/task-manager-plugin/server/task';
+import type { CoreSetup } from '@kbn/core/server';
+import { loggingSystemMock } from '@kbn/core/server/mocks';
+
+import { settingsService } from '../services';
+import { createAppContextStartContractMock } from '../mocks';
+
+import { appContextService } from '../services';
+
+import { DeleteUnenrolledAgentsTask, TYPE, VERSION } from './delete_unenrolled_agents_task';
+
+jest.mock('../services');
+
+const MOCK_TASK_INSTANCE = {
+ id: `${TYPE}:${VERSION}`,
+ runAt: new Date(),
+ attempts: 0,
+ ownerId: '',
+ status: TaskStatus.Running,
+ startedAt: new Date(),
+ scheduledAt: new Date(),
+ retryAt: new Date(),
+ params: {},
+ state: {},
+ taskType: TYPE,
+};
+
+describe('DeleteUnenrolledAgentsTask', () => {
+ const { createSetup: coreSetupMock } = coreMock;
+ const { createSetup: tmSetupMock, createStart: tmStartMock } = taskManagerMock;
+
+ let mockContract: ReturnType;
+ let mockTask: DeleteUnenrolledAgentsTask;
+ let mockCore: CoreSetup;
+ let mockTaskManagerSetup: jest.Mocked;
+ const mockSettingsService = settingsService as jest.Mocked;
+
+ beforeEach(() => {
+ mockContract = createAppContextStartContractMock();
+ appContextService.start(mockContract);
+ mockCore = coreSetupMock();
+ mockTaskManagerSetup = tmSetupMock();
+ mockTask = new DeleteUnenrolledAgentsTask({
+ core: mockCore,
+ taskManager: mockTaskManagerSetup,
+ logFactory: loggingSystemMock.create(),
+ });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('Task lifecycle', () => {
+ it('Should create task', () => {
+ expect(mockTask).toBeInstanceOf(DeleteUnenrolledAgentsTask);
+ });
+
+ it('Should register task', () => {
+ expect(mockTaskManagerSetup.registerTaskDefinitions).toHaveBeenCalled();
+ });
+
+ it('Should schedule task', async () => {
+ const mockTaskManagerStart = tmStartMock();
+ await mockTask.start({ taskManager: mockTaskManagerStart });
+ expect(mockTaskManagerStart.ensureScheduled).toHaveBeenCalled();
+ });
+ });
+
+ describe('Task logic', () => {
+ let esClient: ElasticsearchClientMock;
+ const runTask = async (taskInstance = MOCK_TASK_INSTANCE) => {
+ const mockTaskManagerStart = tmStartMock();
+ await mockTask.start({ taskManager: mockTaskManagerStart });
+ const createTaskRunner =
+ mockTaskManagerSetup.registerTaskDefinitions.mock.calls[0][0][TYPE].createTaskRunner;
+ const taskRunner = createTaskRunner({ taskInstance });
+ return taskRunner.run();
+ };
+
+ beforeEach(async () => {
+ const [{ elasticsearch }] = await mockCore.getStartServices();
+ esClient = elasticsearch.client.asInternalUser as ElasticsearchClientMock;
+ esClient.deleteByQuery.mockResolvedValue({ deleted: 10 });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('Should delete unenrolled agents', async () => {
+ mockSettingsService.getSettingsOrUndefined.mockResolvedValue({
+ delete_unenrolled_agents: {
+ enabled: true,
+ is_preconfigured: false,
+ },
+ id: '1',
+ });
+
+ await runTask();
+
+ expect(esClient.deleteByQuery).toHaveBeenCalled();
+ });
+
+ it('Should not run if task is outdated', async () => {
+ const result = await runTask({ ...MOCK_TASK_INSTANCE, id: 'old-id' });
+
+ expect(esClient.deleteByQuery).not.toHaveBeenCalled();
+ expect(result).toEqual(getDeleteTaskRunResult());
+ });
+
+ it('Should exit if delete unenrolled agents flag is false', async () => {
+ mockSettingsService.getSettingsOrUndefined.mockResolvedValue({
+ delete_unenrolled_agents: {
+ enabled: false,
+ is_preconfigured: false,
+ },
+ id: '1',
+ });
+
+ await runTask();
+
+ expect(esClient.deleteByQuery).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/x-pack/plugins/fleet/server/tasks/delete_unenrolled_agents_task.ts b/x-pack/plugins/fleet/server/tasks/delete_unenrolled_agents_task.ts
new file mode 100644
index 0000000000000..440567effac7d
--- /dev/null
+++ b/x-pack/plugins/fleet/server/tasks/delete_unenrolled_agents_task.ts
@@ -0,0 +1,181 @@
+/*
+ * 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 { SavedObjectsClient } from '@kbn/core/server';
+import type {
+ CoreSetup,
+ ElasticsearchClient,
+ Logger,
+ SavedObjectsClientContract,
+} from '@kbn/core/server';
+import type {
+ ConcreteTaskInstance,
+ TaskManagerSetupContract,
+ TaskManagerStartContract,
+} from '@kbn/task-manager-plugin/server';
+import { getDeleteTaskRunResult } from '@kbn/task-manager-plugin/server/task';
+import type { LoggerFactory } from '@kbn/core/server';
+import { errors } from '@elastic/elasticsearch';
+
+import { AGENTS_INDEX } from '../../common/constants';
+
+import { settingsService } from '../services';
+
+export const TYPE = 'fleet:delete-unenrolled-agents-task';
+export const VERSION = '1.0.0';
+const TITLE = 'Fleet Delete Unenrolled Agents Task';
+const SCOPE = ['fleet'];
+const INTERVAL = '1h';
+const TIMEOUT = '1m';
+
+interface DeleteUnenrolledAgentsTaskSetupContract {
+ core: CoreSetup;
+ taskManager: TaskManagerSetupContract;
+ logFactory: LoggerFactory;
+}
+
+interface DeleteUnenrolledAgentsTaskStartContract {
+ taskManager: TaskManagerStartContract;
+}
+
+export class DeleteUnenrolledAgentsTask {
+ private logger: Logger;
+ private wasStarted: boolean = false;
+ private abortController = new AbortController();
+
+ constructor(setupContract: DeleteUnenrolledAgentsTaskSetupContract) {
+ const { core, taskManager, logFactory } = setupContract;
+ this.logger = logFactory.get(this.taskId);
+
+ taskManager.registerTaskDefinitions({
+ [TYPE]: {
+ title: TITLE,
+ timeout: TIMEOUT,
+ createTaskRunner: ({ taskInstance }: { taskInstance: ConcreteTaskInstance }) => {
+ return {
+ run: async () => {
+ return this.runTask(taskInstance, core);
+ },
+ cancel: async () => {
+ this.abortController.abort('Task timed out');
+ },
+ };
+ },
+ },
+ });
+ }
+
+ public start = async ({ taskManager }: DeleteUnenrolledAgentsTaskStartContract) => {
+ if (!taskManager) {
+ this.logger.error('[DeleteUnenrolledAgentsTask] Missing required service during start');
+ return;
+ }
+
+ this.wasStarted = true;
+ this.logger.info(`[DeleteUnenrolledAgentsTask] Started with interval of [${INTERVAL}]`);
+
+ try {
+ await taskManager.ensureScheduled({
+ id: this.taskId,
+ taskType: TYPE,
+ scope: SCOPE,
+ schedule: {
+ interval: INTERVAL,
+ },
+ state: {},
+ params: { version: VERSION },
+ });
+ } catch (e) {
+ this.logger.error(`Error scheduling task DeleteUnenrolledAgentsTask, error: ${e.message}`, e);
+ }
+ };
+
+ private get taskId(): string {
+ return `${TYPE}:${VERSION}`;
+ }
+
+ private endRun(msg: string = '') {
+ this.logger.info(`[DeleteUnenrolledAgentsTask] runTask ended${msg ? ': ' + msg : ''}`);
+ }
+
+ public async deleteUnenrolledAgents(esClient: ElasticsearchClient) {
+ this.logger.debug(`[DeleteUnenrolledAgentsTask] Fetching unenrolled agents`);
+
+ const response = await esClient.deleteByQuery(
+ {
+ index: AGENTS_INDEX,
+ body: {
+ query: {
+ bool: {
+ filter: [
+ {
+ term: {
+ active: false,
+ },
+ },
+ { exists: { field: 'unenrolled_at' } },
+ ],
+ },
+ },
+ },
+ },
+ { signal: this.abortController.signal }
+ );
+
+ this.logger.debug(
+ `[DeleteUnenrolledAgentsTask] Executed deletion of ${response.deleted} unenrolled agents`
+ );
+ }
+
+ public async isDeleteUnenrolledAgentsEnabled(
+ soClient: SavedObjectsClientContract
+ ): Promise {
+ const settings = await settingsService.getSettingsOrUndefined(soClient);
+ return settings?.delete_unenrolled_agents?.enabled ?? false;
+ }
+
+ public runTask = async (taskInstance: ConcreteTaskInstance, core: CoreSetup) => {
+ if (!this.wasStarted) {
+ this.logger.debug('[DeleteUnenrolledAgentsTask] runTask Aborted. Task not started yet');
+ return;
+ }
+ // Check that this task is current
+ if (taskInstance.id !== this.taskId) {
+ this.logger.debug(
+ `[DeleteUnenrolledAgentsTask] Outdated task version: Got [${taskInstance.id}] from task instance. Current version is [${this.taskId}]`
+ );
+ return getDeleteTaskRunResult();
+ }
+
+ this.logger.info(`[runTask()] started`);
+
+ const [coreStart] = await core.getStartServices();
+ const esClient = coreStart.elasticsearch.client.asInternalUser;
+ const soClient = new SavedObjectsClient(coreStart.savedObjects.createInternalRepository());
+
+ try {
+ if (!(await this.isDeleteUnenrolledAgentsEnabled(soClient))) {
+ this.logger.debug(
+ '[DeleteUnenrolledAgentsTask] Delete unenrolled agents flag is disabled, returning.'
+ );
+ this.endRun('Delete unenrolled agents is disabled');
+ return;
+ }
+ await this.deleteUnenrolledAgents(esClient);
+
+ this.endRun('success');
+ } catch (err) {
+ if (err instanceof errors.RequestAbortedError) {
+ this.logger.warn(`[DeleteUnenrolledAgentsTask] request aborted due to timeout: ${err}`);
+ this.endRun();
+ return;
+ }
+ this.logger.error(`[DeleteUnenrolledAgentsTask] error: ${err}`);
+ this.endRun('error');
+ }
+ };
+}
diff --git a/x-pack/plugins/fleet/server/types/rest_spec/settings.ts b/x-pack/plugins/fleet/server/types/rest_spec/settings.ts
index 0adaa69f1d30a..459070fa9591a 100644
--- a/x-pack/plugins/fleet/server/types/rest_spec/settings.ts
+++ b/x-pack/plugins/fleet/server/types/rest_spec/settings.ts
@@ -40,6 +40,12 @@ export const PutSettingsRequestSchema = {
),
kibana_ca_sha256: schema.maybe(schema.string()),
prerelease_integrations_enabled: schema.maybe(schema.boolean()),
+ delete_unenrolled_agents: schema.maybe(
+ schema.object({
+ enabled: schema.boolean(),
+ is_preconfigured: schema.boolean(),
+ })
+ ),
}),
};
@@ -56,7 +62,7 @@ export const SettingsResponseSchema = schema.object({
item: schema.object({
has_seen_add_data_notice: schema.maybe(schema.boolean()),
fleet_server_hosts: schema.maybe(schema.arrayOf(schema.string())),
- prerelease_integrations_enabled: schema.boolean(),
+ prerelease_integrations_enabled: schema.maybe(schema.boolean()),
id: schema.string(),
version: schema.maybe(schema.string()),
preconfigured_fields: schema.maybe(schema.arrayOf(schema.literal('fleet_server_hosts'))),
@@ -66,6 +72,12 @@ export const SettingsResponseSchema = schema.object({
schema.oneOf([schema.literal('pending'), schema.literal('success'), schema.literal('error')])
),
use_space_awareness_migration_started_at: schema.maybe(schema.string()),
+ delete_unenrolled_agents: schema.maybe(
+ schema.object({
+ enabled: schema.boolean(),
+ is_preconfigured: schema.boolean(),
+ })
+ ),
}),
});
diff --git a/x-pack/plugins/fleet/server/types/so_attributes.ts b/x-pack/plugins/fleet/server/types/so_attributes.ts
index 9be09fe4ee554..31207dda64bc8 100644
--- a/x-pack/plugins/fleet/server/types/so_attributes.ts
+++ b/x-pack/plugins/fleet/server/types/so_attributes.ts
@@ -234,13 +234,17 @@ export type OutputSOAttributes =
| OutputSoKafkaAttributes;
export interface SettingsSOAttributes {
- prerelease_integrations_enabled: boolean;
+ prerelease_integrations_enabled?: boolean;
has_seen_add_data_notice?: boolean;
fleet_server_hosts?: string[];
secret_storage_requirements_met?: boolean;
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;
+ };
}
export interface SpaceSettingsSOAttributes {
diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts
index f5ccfcc3ca56f..eeb8b6e3474c9 100644
--- a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts
+++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts
@@ -139,6 +139,7 @@ export default function ({ getService }: FtrProviderContext) {
'endpoint:metadata-check-transforms-task',
'endpoint:user-artifact-packager',
'fleet:check-deleted-files-task',
+ 'fleet:delete-unenrolled-agents-task',
'fleet:deploy_agent_policies',
'fleet:reassign_action:retry',
'fleet:request_diagnostics:retry',