forked from elastic/kibana
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Fleet] Create task that periodically unenrolls inactive agents (elas…
…tic#189861) Closes elastic#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 <[email protected]>
- Loading branch information
1 parent
0299a7a
commit 1565753
Showing
12 changed files
with
415 additions
and
33 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
181 changes: 181 additions & 0 deletions
181
x-pack/plugins/fleet/server/tasks/unenroll_inactive_agents_task.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<typeof agentPolicyService>; | ||
const mockedGetAgentsByKuery = getAgentsByKuery as jest.MockedFunction<typeof getAgentsByKuery>; | ||
|
||
describe('UnenrollInactiveAgentsTask', () => { | ||
const { createSetup: coreSetupMock } = coreMock; | ||
const { createSetup: tmSetupMock, createStart: tmStartMock } = taskManagerMock; | ||
|
||
let mockContract: ReturnType<typeof createAppContextStartContractMock>; | ||
let mockTask: UnenrollInactiveAgentsTask; | ||
let mockCore: CoreSetup; | ||
let mockTaskManagerSetup: jest.Mocked<TaskManagerSetupContract>; | ||
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(); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.