From 15657536ed82d945c2dfc1251b3558e547311c26 Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Mon, 19 Aug 2024 15:09:47 +0200 Subject: [PATCH] [Fleet] Create task that periodically unenrolls inactive agents (#189861) Closes https://github.com/elastic/kibana/issues/179399 ## Summary Create a new periodic task that unenrolls inactive agents based on `unenroll_timeout` set on agent policies In the agent policy settings there is now a new section: ![Screenshot 2024-08-06 at 12 31 37](https://github.com/user-attachments/assets/f66164c5-3eff-442d-91bc-367387cefe3d) ### Testing - Create a policy with `unenroll_timeout` set to any value - Enroll many agents to a policy and make them inactive - you can use Horde or the script in `fleet/scripts/create_agents' that can directly create inactive agents - Leave the local env running for at least 10 minutes - You should see logs that indicate that the task ran successfully and remove the inactive agents ![Screenshot 2024-08-06 at 12 14 13](https://github.com/user-attachments/assets/573f32fb-eedb-4bee-918c-f26fedec9e0b) Note that the executed unenroll action is also visible in the UI: ![Screenshot 2024-08-06 at 12 19 52](https://github.com/user-attachments/assets/942932ac-70dd-4d77-bf47-20007ac54748) - If there are no agent policies with `unenroll_timeout` set or there are no inactive agents on those policies, you should see logs like these: ![Screenshot 2024-08-06 at 12 13 49](https://github.com/user-attachments/assets/8868c228-fd09-4ecf-ad02-e07a94812638) ### Checklist - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Elastic Machine --- .../agent_policy_advanced_fields/index.tsx | 20 +- .../agent_activity_flyout/activity_item.tsx | 6 +- .../agent_activity_flyout/helpers.tsx | 15 +- x-pack/plugins/fleet/server/mocks/index.ts | 1 + x-pack/plugins/fleet/server/plugin.ts | 10 + .../fleet/server/services/agent_policy.ts | 4 - .../unenroll_inactive_agents_task.test.ts | 181 ++++++++++++++++ .../tasks/unenroll_inactive_agents_task.ts | 204 ++++++++++++++++++ .../translations/translations/fr-FR.json | 2 - .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - .../check_registered_task_types.ts | 1 + 12 files changed, 415 insertions(+), 33 deletions(-) create mode 100644 x-pack/plugins/fleet/server/tasks/unenroll_inactive_agents_task.test.ts create mode 100644 x-pack/plugins/fleet/server/tasks/unenroll_inactive_agents_task.ts diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx index 185c694a5c84e9..28d14b62a575e4 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx @@ -22,7 +22,6 @@ import { EuiText, EuiFlexGroup, EuiFlexItem, - EuiBetaBadge, EuiBadge, EuiSwitch, } from '@elastic/eui'; @@ -796,29 +795,14 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent =

-   - - -

} description={ } > diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_activity_flyout/activity_item.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_activity_flyout/activity_item.tsx index 96b48948320bde..ee83c49744d35e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_activity_flyout/activity_item.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_activity_flyout/activity_item.tsx @@ -55,7 +55,7 @@ export const ActivityItem: React.FunctionComponent<{ ? action.nbAgentsAck : action.nbAgentsAck + ' of ' + action.nbAgentsActioned, agents: action.nbAgentsActioned === 1 ? 'agent' : 'agents', - completedText: getAction(action.type).completedText, + completedText: getAction(action.type, action.actionId).completedText, offlineText: action.status === 'ROLLOUT_PASSED' && action.nbAgentsActioned - action.nbAgentsAck > 0 ? `, ${ @@ -175,7 +175,7 @@ export const ActivityItem: React.FunctionComponent<{ id="xpack.fleet.agentActivityFlyout.cancelledTitle" defaultMessage="Agent {cancelledText} cancelled" values={{ - cancelledText: getAction(action.type).cancelledText, + cancelledText: getAction(action.type, action.actionId).cancelledText, }} /> @@ -201,7 +201,7 @@ export const ActivityItem: React.FunctionComponent<{ id="xpack.fleet.agentActivityFlyout.expiredTitle" defaultMessage="Agent {expiredText} expired" values={{ - expiredText: getAction(action.type).cancelledText, + expiredText: getAction(action.type, action.actionId).cancelledText, }} /> diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_activity_flyout/helpers.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_activity_flyout/helpers.tsx index a3c9d5807fae87..82fc266a04bccf 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_activity_flyout/helpers.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_activity_flyout/helpers.tsx @@ -31,6 +31,11 @@ const actionNames: { completedText: 'force unenrolled', cancelledText: 'force unenrollment', }, + AUTOMATIC_FORCE_UNENROLL: { + inProgressText: 'Automatic unenrolling', + completedText: 'automatically unenrolled', + cancelledText: 'automatic unenrollment', + }, UPDATE_TAGS: { inProgressText: 'Updating tags of', completedText: 'updated tags', @@ -60,7 +65,13 @@ const actionNames: { ACTION: { inProgressText: 'Actioning', completedText: 'actioned', cancelledText: 'action' }, }; -export const getAction = (type?: string) => actionNames[type ?? 'ACTION'] ?? actionNames.ACTION; +export const getAction = (type?: string, actionId?: string) => { + // handling a special case of force unenrollment coming from an automatic task + // we know what kind of action is from the actionId prefix + if (actionId?.includes('UnenrollInactiveAgentsTask-')) + return actionNames.AUTOMATICAL_FORCE_UNENROLL; + return actionNames[type ?? 'ACTION'] ?? actionNames.ACTION; +}; export const inProgressTitle = (action: ActionStatus) => ( ( ? action.nbAgentsActioned : action.nbAgentsActioned - action.nbAgentsAck + ' of ' + action.nbAgentsActioned, agents: action.nbAgentsActioned === 1 ? 'agent' : 'agents', - inProgressText: getAction(action.type).inProgressText, + inProgressText: getAction(action.type, action.actionId).inProgressText, reassignText: action.type === 'POLICY_REASSIGN' && action.newPolicyId ? `to ${action.newPolicyId}` : '', upgradeText: action.type === 'UPGRADE' ? `to version ${action.version}` : '', diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index 6a91add910fb83..7b11654f8def40 100644 --- a/x-pack/plugins/fleet/server/mocks/index.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -128,6 +128,7 @@ export const createAppContextStartContractMock = ( }, } : {}), + unenrollInactiveAgentsTask: {} as any, }; }; diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index d05ef1de33641a..043b02ca93d7fd 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -118,6 +118,7 @@ import type { PackagePolicyService } from './services/package_policy_service'; import { PackagePolicyServiceImpl } from './services/package_policy'; import { registerFleetUsageLogger, startFleetUsageLogger } from './services/fleet_usage_logger'; import { CheckDeletedFilesTask } from './tasks/check_deleted_files_task'; +import { UnenrollInactiveAgentsTask } from './tasks/unenroll_inactive_agents_task'; import { UninstallTokenService, type UninstallTokenServiceInterface, @@ -178,6 +179,7 @@ export interface FleetAppContext { messageSigningService: MessageSigningServiceInterface; auditLogger?: AuditLogger; uninstallTokenService: UninstallTokenServiceInterface; + unenrollInactiveAgentsTask: UnenrollInactiveAgentsTask; } export type FleetSetupContract = void; @@ -266,6 +268,7 @@ export class FleetPlugin private fleetUsageSender?: FleetUsageSender; private checkDeletedFilesTask?: CheckDeletedFilesTask; private fleetMetricsTask?: FleetMetricsTask; + private unenrollInactiveAgentsTask?: UnenrollInactiveAgentsTask; private agentService?: AgentService; private packageService?: PackageService; @@ -599,6 +602,11 @@ export class FleetPlugin taskManager: deps.taskManager, logFactory: this.initializerContext.logger, }); + this.unenrollInactiveAgentsTask = new UnenrollInactiveAgentsTask({ + core, + taskManager: deps.taskManager, + logFactory: this.initializerContext.logger, + }); // Register fields metadata extractor registerIntegrationFieldsExtractor({ core, fieldsMetadata: deps.fieldsMetadata }); @@ -644,12 +652,14 @@ export class FleetPlugin bulkActionsResolver: this.bulkActionsResolver!, messageSigningService, uninstallTokenService, + unenrollInactiveAgentsTask: this.unenrollInactiveAgentsTask!, }); licenseService.start(plugins.licensing.license$); this.telemetryEventsSender.start(plugins.telemetry, core).catch(() => {}); this.bulkActionsResolver?.start(plugins.taskManager).catch(() => {}); this.fleetUsageSender?.start(plugins.taskManager).catch(() => {}); this.checkDeletedFilesTask?.start({ taskManager: plugins.taskManager }).catch(() => {}); + this.unenrollInactiveAgentsTask?.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/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index b2ba41b78586a3..250cd867ee875a 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -1245,10 +1245,6 @@ class AgentPolicyService { default_fleet_server: policy.is_default_fleet_server === true, }; - if (policy.unenroll_timeout) { - fleetServerPolicy.unenroll_timeout = policy.unenroll_timeout; - } - acc.push(fleetServerPolicy); return acc; }, [] as FleetServerPolicy[]); diff --git a/x-pack/plugins/fleet/server/tasks/unenroll_inactive_agents_task.test.ts b/x-pack/plugins/fleet/server/tasks/unenroll_inactive_agents_task.test.ts new file mode 100644 index 00000000000000..dd1121a9046275 --- /dev/null +++ b/x-pack/plugins/fleet/server/tasks/unenroll_inactive_agents_task.test.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 { 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 { agentPolicyService } from '../services'; +import { createAgentPolicyMock } from '../../common/mocks'; +import { createAppContextStartContractMock } from '../mocks'; +import { getAgentsByKuery } from '../services/agents'; + +import { appContextService } from '../services'; + +import { unenrollBatch } from '../services/agents/unenroll_action_runner'; + +import type { AgentPolicy } from '../types'; + +import { UnenrollInactiveAgentsTask, TYPE, VERSION } from './unenroll_inactive_agents_task'; + +jest.mock('../services'); +jest.mock('../services/agents'); +jest.mock('../services/agents/unenroll_action_runner'); + +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, +}; + +const mockAgentPolicyService = agentPolicyService as jest.Mocked; +const mockedGetAgentsByKuery = getAgentsByKuery as jest.MockedFunction; + +describe('UnenrollInactiveAgentsTask', () => { + const { createSetup: coreSetupMock } = coreMock; + const { createSetup: tmSetupMock, createStart: tmStartMock } = taskManagerMock; + + let mockContract: ReturnType; + let mockTask: UnenrollInactiveAgentsTask; + let mockCore: CoreSetup; + let mockTaskManagerSetup: jest.Mocked; + const mockedUnenrollBatch = jest.mocked(unenrollBatch); + + const agents = [ + { + id: 'agent-1', + policy_id: 'agent-policy-2', + status: 'inactive', + }, + { + id: 'agent-2', + policy_id: 'agent-policy-1', + status: 'inactive', + }, + { + id: 'agent-3', + policy_id: 'agent-policy-1', + status: 'active', + }, + ]; + + const getMockAgentPolicyFetchAllAgentPolicies = (items: AgentPolicy[]) => + jest.fn().mockResolvedValue( + jest.fn(async function* () { + yield items; + })() + ); + + beforeEach(() => { + mockContract = createAppContextStartContractMock(); + appContextService.start(mockContract); + mockCore = coreSetupMock(); + mockTaskManagerSetup = tmSetupMock(); + mockTask = new UnenrollInactiveAgentsTask({ + core: mockCore, + taskManager: mockTaskManagerSetup, + logFactory: loggingSystemMock.create(), + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('Task lifecycle', () => { + it('Should create task', () => { + expect(mockTask).toBeInstanceOf(UnenrollInactiveAgentsTask); + }); + + 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', () => { + 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(() => { + mockAgentPolicyService.fetchAllAgentPolicies = getMockAgentPolicyFetchAllAgentPolicies([ + createAgentPolicyMock({ unenroll_timeout: 3000 }), + createAgentPolicyMock({ id: 'agent-policy-2', unenroll_timeout: 1000 }), + ]); + + mockedGetAgentsByKuery.mockResolvedValue({ + agents, + } as any); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Should unenroll eligible agents', async () => { + mockedUnenrollBatch.mockResolvedValueOnce({ actionId: 'actionid-01' }); + await runTask(); + expect(mockedUnenrollBatch).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + agents, + { + force: true, + revoke: true, + actionId: expect.stringContaining('UnenrollInactiveAgentsTask-'), + } + ); + }); + + it('Should not run if task is outdated', async () => { + const result = await runTask({ ...MOCK_TASK_INSTANCE, id: 'old-id' }); + + expect(mockedUnenrollBatch).not.toHaveBeenCalled(); + expect(result).toEqual(getDeleteTaskRunResult()); + }); + + it('Should exit if there are no agents policies with unenroll_timeout set', async () => { + mockAgentPolicyService.list.mockResolvedValue({ + items: [], + total: 0, + page: 1, + perPage: 1, + }); + expect(mockedUnenrollBatch).not.toHaveBeenCalled(); + }); + + it('Should exit if there are no eligible agents to unenroll', async () => { + mockedGetAgentsByKuery.mockResolvedValue({ + agents: [], + } as any); + expect(mockedUnenrollBatch).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/tasks/unenroll_inactive_agents_task.ts b/x-pack/plugins/fleet/server/tasks/unenroll_inactive_agents_task.ts new file mode 100644 index 00000000000000..d56c10cc61a202 --- /dev/null +++ b/x-pack/plugins/fleet/server/tasks/unenroll_inactive_agents_task.ts @@ -0,0 +1,204 @@ +/* + * 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 { v4 as uuidv4 } from 'uuid'; +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_PREFIX, AGENT_POLICY_SAVED_OBJECT_TYPE } from '../constants'; +import { getAgentsByKuery } from '../services/agents'; +import { unenrollBatch } from '../services/agents/unenroll_action_runner'; +import { agentPolicyService, auditLoggingService } from '../services'; + +export const TYPE = 'fleet:unenroll-inactive-agents-task'; +export const VERSION = '1.0.0'; +const TITLE = 'Fleet Unenroll Inactive Agent Task'; +const SCOPE = ['fleet']; +const INTERVAL = '10m'; +const TIMEOUT = '1m'; +const UNENROLLMENT_BATCHSIZE = 1000; +const POLICIES_BATCHSIZE = 500; + +interface UnenrollInactiveAgentsTaskSetupContract { + core: CoreSetup; + taskManager: TaskManagerSetupContract; + logFactory: LoggerFactory; +} + +interface UnenrollInactiveAgentsTaskStartContract { + taskManager: TaskManagerStartContract; +} + +export class UnenrollInactiveAgentsTask { + private logger: Logger; + private wasStarted: boolean = false; + private abortController = new AbortController(); + + constructor(setupContract: UnenrollInactiveAgentsTaskSetupContract) { + 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 }: UnenrollInactiveAgentsTaskStartContract) => { + if (!taskManager) { + this.logger.error('[UnenrollInactiveAgentsTask] Missing required service during start'); + return; + } + + this.wasStarted = true; + this.logger.info(`[UnenrollInactiveAgentsTask] 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 UnenrollInactiveAgentsTask, error: ${e.message}`, e); + } + }; + + private get taskId(): string { + return `${TYPE}:${VERSION}`; + } + + private endRun(msg: string = '') { + this.logger.info(`[UnenrollInactiveAgentsTask] runTask ended${msg ? ': ' + msg : ''}`); + } + + public async unenrollInactiveAgents( + esClient: ElasticsearchClient, + soClient: SavedObjectsClientContract + ) { + this.logger.debug( + `[UnenrollInactiveAgentsTask] Fetching agent policies with unenroll_timeout > 0` + ); + // find all agent policies that are not managed and having unenroll_timeout > 0 + // limit the search to POLICIES_BATCHSIZE at a time and loop until there are no agent policies left + const policiesKuery = `${AGENT_POLICY_SAVED_OBJECT_TYPE}.is_managed: false AND ${AGENT_POLICY_SAVED_OBJECT_TYPE}.unenroll_timeout > 0`; + let agentCounter = 0; + + const agentPolicyFetcher = await agentPolicyService.fetchAllAgentPolicies(soClient, { + kuery: policiesKuery, + perPage: POLICIES_BATCHSIZE, + }); + for await (const agentPolicyPageResults of agentPolicyFetcher) { + this.logger.debug( + `[UnenrollInactiveAgentsTask] Found "${agentPolicyPageResults.length}" agent policies with unenroll_timeout > 0` + ); + if (!agentPolicyPageResults.length) { + this.endRun('Found no policies to process'); + return; + } + + // find inactive agents enrolled on above policies + // limit batch size to UNENROLLMENT_BATCHSIZE to avoid scale issues + const kuery = `(${AGENTS_PREFIX}.policy_id:${agentPolicyPageResults + .map((policy) => `"${policy.id}"`) + .join(' or ')}) and ${AGENTS_PREFIX}.status: inactive`; + const res = await getAgentsByKuery(esClient, soClient, { + kuery, + showInactive: true, + page: 1, + perPage: UNENROLLMENT_BATCHSIZE, + }); + if (!res.agents.length) { + this.endRun('No inactive agents to unenroll'); + return; + } + agentCounter += res.agents.length; + if (agentCounter >= UNENROLLMENT_BATCHSIZE) { + this.endRun('Reached the maximum amount of agents to unenroll, exiting.'); + return; + } + this.logger.debug( + `[UnenrollInactiveAgentsTask] Found "${res.agents.length}" inactive agents to unenroll. Attempting unenrollment` + ); + const unenrolledBatch = await unenrollBatch(soClient, esClient, res.agents, { + revoke: true, + force: true, + actionId: `UnenrollInactiveAgentsTask-${uuidv4()}`, + }); + auditLoggingService.writeCustomAuditLog({ + message: `Recurrent unenrollment of ${agentCounter} inactive agents due to unenroll_timeout option set on agent policy. Fleet action [id=${unenrolledBatch.actionId}]`, + }); + this.logger.debug( + `[UnenrollInactiveAgentsTask] Executed unenrollment of ${agentCounter} inactive agents with actionId: ${unenrolledBatch.actionId}` + ); + } + } + + public runTask = async (taskInstance: ConcreteTaskInstance, core: CoreSetup) => { + if (!this.wasStarted) { + this.logger.debug('[UnenrollInactiveAgentsTask] runTask Aborted. Task not started yet'); + return; + } + // Check that this task is current + if (taskInstance.id !== this.taskId) { + this.logger.debug( + `[UnenrollInactiveAgentsTask] 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 { + await this.unenrollInactiveAgents(esClient, soClient); + + this.endRun('success'); + } catch (err) { + if (err instanceof errors.RequestAbortedError) { + this.logger.warn(`[UnenrollInactiveAgentsTask] request aborted due to timeout: ${err}`); + this.endRun(); + return; + } + this.logger.error(`[UnenrollInactiveAgentsTask] error: ${err}`); + this.endRun('error'); + } + }; +} diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 421ccac4a3df13..af2ab4bc10df84 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -19627,10 +19627,8 @@ "xpack.fleet.agentPolicyForm.tamperingSwitchLabel": "Empêcher la falsification des agents", "xpack.fleet.agentPolicyForm.tamperingSwitchLabel.disabledWarning": "L'intégration Elastic Defend est nécessaire pour activer cette fonctionnalité", "xpack.fleet.agentPolicyForm.tamperingUninstallLink": "Obtenir la commande de désinstallation", - "xpack.fleet.agentPolicyForm.unenrollmentTimeoutDeprecatedLabel": "Déclassé", "xpack.fleet.agentPolicyForm.unenrollmentTimeoutDescription": "Délai d'expiration facultatif en secondes. Si une valeur est indiquée et que la version du serveur Fleet est inférieure à 8.7.0, un agent est automatiquement désenregistré après une période d'inactivité équivalente à ce délai.", "xpack.fleet.agentPolicyForm.unenrollmentTimeoutLabel": "Délai d'expiration pour le désenregistrement", - "xpack.fleet.agentPolicyForm.unenrollmentTimeoutTooltip": "Ce paramètre est déclassé et sera retiré dans une prochaine version. Envisagez d'utiliser le délai d'inactivité à la place", "xpack.fleet.agentPolicyForm.unenrollTimeoutMinValueErrorMessage": "Le délai de désenregistrement doit être supérieur à zéro.", "xpack.fleet.agentPolicyList.actionsColumnTitle": "Actions", "xpack.fleet.agentPolicyList.addButton": "Créer une stratégie d'agent", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3a26962f3fb3f2..602e8ea6b08ab4 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -19615,10 +19615,8 @@ "xpack.fleet.agentPolicyForm.tamperingSwitchLabel": "エージェントの改ざんを防止", "xpack.fleet.agentPolicyForm.tamperingSwitchLabel.disabledWarning": "この機能を有効にするには、Elastic Defend統合が必要です。", "xpack.fleet.agentPolicyForm.tamperingUninstallLink": "アンインストールコマンドを取得", - "xpack.fleet.agentPolicyForm.unenrollmentTimeoutDeprecatedLabel": "非推奨", "xpack.fleet.agentPolicyForm.unenrollmentTimeoutDescription": "任意のタイムアウト(秒)。指定され、Fleetサーバーのバージョンが8.7.0より前の場合、この期間が経過した後、エージェントは自動的に登録解除されます。", "xpack.fleet.agentPolicyForm.unenrollmentTimeoutLabel": "登録解除タイムアウト", - "xpack.fleet.agentPolicyForm.unenrollmentTimeoutTooltip": "この設定はサポートが終了し、今後のリリースでは削除されます。代わりに、非アクティブタイムアウトの使用を検討してください。", "xpack.fleet.agentPolicyForm.unenrollTimeoutMinValueErrorMessage": "登録解除タイムアウトは0よりも大きい値でなければなりません。", "xpack.fleet.agentPolicyList.actionsColumnTitle": "アクション", "xpack.fleet.agentPolicyList.addButton": "エージェントポリシーを作成", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d601c699a9319a..21e15ecd10f537 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -19641,10 +19641,8 @@ "xpack.fleet.agentPolicyForm.tamperingSwitchLabel": "防止篡改代理", "xpack.fleet.agentPolicyForm.tamperingSwitchLabel.disabledWarning": "需要 Elastic Defend 集成才能启用此功能", "xpack.fleet.agentPolicyForm.tamperingUninstallLink": "获取卸载命令", - "xpack.fleet.agentPolicyForm.unenrollmentTimeoutDeprecatedLabel": "(已过时)", "xpack.fleet.agentPolicyForm.unenrollmentTimeoutDescription": "可选超时(秒)。若提供,且 Fleet 服务器的版本低于 8.7.0,代理断开连接此段时间后,将自动注销。", "xpack.fleet.agentPolicyForm.unenrollmentTimeoutLabel": "注销超时", - "xpack.fleet.agentPolicyForm.unenrollmentTimeoutTooltip": "此设置已过时,将在未来版本中移除。考虑改用非活动超时", "xpack.fleet.agentPolicyForm.unenrollTimeoutMinValueErrorMessage": "取消注册超时必须大于零。", "xpack.fleet.agentPolicyList.actionsColumnTitle": "操作", "xpack.fleet.agentPolicyList.addButton": "创建代理策略", 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 576f08d890e5b5..810db7295a79fb 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 @@ -140,6 +140,7 @@ export default function ({ getService }: FtrProviderContext) { 'fleet:check-deleted-files-task', 'fleet:reassign_action:retry', 'fleet:request_diagnostics:retry', + 'fleet:unenroll-inactive-agents-task', 'fleet:unenroll_action:retry', 'fleet:update_agent_tags:retry', 'fleet:upgrade_action:retry',