[Ingest Manager] Improve agent unenrollment with unenroll action (#70031)
This commit is contained in:
parent
571a610c7e
commit
72b300424b
15 changed files with 167 additions and 34 deletions
|
@ -3520,7 +3520,17 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/fleet/agents/unenroll": {
|
||||
"/fleet/agents/{agentId}/unenroll": {
|
||||
"parameters": [
|
||||
{
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": "agentId",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"post": {
|
||||
"summary": "Fleet - Agent - Unenroll",
|
||||
"tags": [],
|
||||
|
@ -3530,7 +3540,26 @@
|
|||
{
|
||||
"$ref": "#/components/parameters/xsrfHeader"
|
||||
}
|
||||
]
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"force": { "type": "boolean" }
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"example-1": {
|
||||
"value": {
|
||||
"force": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/fleet/config/{configId}/agent-status": {
|
||||
|
@ -4096,6 +4125,12 @@
|
|||
"enrolled_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"unenrolled_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"unenrollment_started_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"shared_id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
|
@ -21,6 +21,9 @@ export function getAgentStatus(agent: Agent, now: number = Date.now()): AgentSta
|
|||
if (!agent.active) {
|
||||
return 'inactive';
|
||||
}
|
||||
if (agent.unenrollment_started_at && !agent.unenrolled_at) {
|
||||
return 'unenrolling';
|
||||
}
|
||||
if (agent.current_error_events.length > 0) {
|
||||
return 'error';
|
||||
}
|
||||
|
|
|
@ -11,10 +11,10 @@ export type AgentType =
|
|||
| typeof AGENT_TYPE_PERMANENT
|
||||
| typeof AGENT_TYPE_TEMPORARY;
|
||||
|
||||
export type AgentStatus = 'offline' | 'error' | 'online' | 'inactive' | 'warning';
|
||||
|
||||
export type AgentStatus = 'offline' | 'error' | 'online' | 'inactive' | 'warning' | 'unenrolling';
|
||||
export type AgentActionType = 'CONFIG_CHANGE' | 'DATA_DUMP' | 'RESUME' | 'PAUSE' | 'UNENROLL';
|
||||
export interface NewAgentAction {
|
||||
type: 'CONFIG_CHANGE' | 'DATA_DUMP' | 'RESUME' | 'PAUSE';
|
||||
type: AgentActionType;
|
||||
data?: any;
|
||||
sent_at?: string;
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ export interface AgentAction extends NewAgentAction {
|
|||
}
|
||||
|
||||
export interface AgentActionSOAttributes {
|
||||
type: 'CONFIG_CHANGE' | 'DATA_DUMP' | 'RESUME' | 'PAUSE';
|
||||
type: AgentActionType;
|
||||
sent_at?: string;
|
||||
timestamp?: string;
|
||||
created_at: string;
|
||||
|
@ -73,6 +73,8 @@ interface AgentBase {
|
|||
type: AgentType;
|
||||
active: boolean;
|
||||
enrolled_at: string;
|
||||
unenrolled_at?: string;
|
||||
unenrollment_started_at?: string;
|
||||
shared_id?: string;
|
||||
access_api_key_id?: string;
|
||||
default_api_key?: string;
|
||||
|
|
|
@ -236,7 +236,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
},
|
||||
{
|
||||
field: 'active',
|
||||
width: '100px',
|
||||
width: '120px',
|
||||
name: i18n.translate('xpack.ingestManager.agentList.statusColumnTitle', {
|
||||
defaultMessage: 'Status',
|
||||
}),
|
||||
|
|
|
@ -53,6 +53,14 @@ const Status = {
|
|||
/>
|
||||
</EuiHealth>
|
||||
),
|
||||
Unenrolling: (
|
||||
<EuiHealth color="warning">
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.agentHealth.unenrollingStatusText"
|
||||
defaultMessage="Unenrolling"
|
||||
/>
|
||||
</EuiHealth>
|
||||
),
|
||||
};
|
||||
|
||||
function getStatusComponent(agent: Agent): React.ReactElement {
|
||||
|
@ -65,6 +73,8 @@ function getStatusComponent(agent: Agent): React.ReactElement {
|
|||
return Status.Offline;
|
||||
case 'warning':
|
||||
return Status.Warning;
|
||||
case 'unenrolling':
|
||||
return Status.Unenrolling;
|
||||
default:
|
||||
return Status.Online;
|
||||
}
|
||||
|
|
|
@ -74,7 +74,7 @@ export const AgentUnenrollProvider: React.FunctionComponent<Props> = ({ children
|
|||
const successMessage = i18n.translate(
|
||||
'xpack.ingestManager.unenrollAgents.successSingleNotificationTitle',
|
||||
{
|
||||
defaultMessage: "Unenrolled agent '{id}'",
|
||||
defaultMessage: "Unenrolling agent '{id}'",
|
||||
values: { id: agentId },
|
||||
}
|
||||
);
|
||||
|
|
|
@ -13,7 +13,6 @@ import {
|
|||
GetOneAgentEventsResponse,
|
||||
PostAgentCheckinResponse,
|
||||
PostAgentEnrollResponse,
|
||||
PostAgentUnenrollResponse,
|
||||
GetAgentStatusResponse,
|
||||
PutAgentReassignResponse,
|
||||
} from '../../../common/types';
|
||||
|
@ -25,7 +24,6 @@ import {
|
|||
GetOneAgentEventsRequestSchema,
|
||||
PostAgentCheckinRequestSchema,
|
||||
PostAgentEnrollRequestSchema,
|
||||
PostAgentUnenrollRequestSchema,
|
||||
GetAgentStatusRequestSchema,
|
||||
PutAgentReassignRequestSchema,
|
||||
} from '../../types';
|
||||
|
@ -302,25 +300,6 @@ export const getAgentsHandler: RequestHandler<
|
|||
}
|
||||
};
|
||||
|
||||
export const postAgentsUnenrollHandler: RequestHandler<TypeOf<
|
||||
typeof PostAgentUnenrollRequestSchema.params
|
||||
>> = async (context, request, response) => {
|
||||
const soClient = context.core.savedObjects.client;
|
||||
try {
|
||||
await AgentService.unenrollAgent(soClient, request.params.agentId);
|
||||
|
||||
const body: PostAgentUnenrollResponse = {
|
||||
success: true,
|
||||
};
|
||||
return response.ok({ body });
|
||||
} catch (e) {
|
||||
return response.customError({
|
||||
statusCode: 500,
|
||||
body: { message: e.message },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const putAgentsReassignHandler: RequestHandler<
|
||||
TypeOf<typeof PutAgentReassignRequestSchema.params>,
|
||||
undefined,
|
||||
|
|
|
@ -33,7 +33,6 @@ import {
|
|||
getAgentEventsHandler,
|
||||
postAgentCheckinHandler,
|
||||
postAgentEnrollHandler,
|
||||
postAgentsUnenrollHandler,
|
||||
getAgentStatusForConfigHandler,
|
||||
putAgentsReassignHandler,
|
||||
} from './handlers';
|
||||
|
@ -41,6 +40,7 @@ import { postAgentAcksHandlerBuilder } from './acks_handlers';
|
|||
import * as AgentService from '../../services/agents';
|
||||
import { postNewAgentActionHandlerBuilder } from './actions_handlers';
|
||||
import { appContextService } from '../../services';
|
||||
import { postAgentsUnenrollHandler } from './unenroll_handler';
|
||||
|
||||
export const registerRoutes = (router: IRouter) => {
|
||||
// Get one
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { RequestHandler } from 'src/core/server';
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import { PostAgentUnenrollResponse } from '../../../common/types';
|
||||
import { PostAgentUnenrollRequestSchema } from '../../types';
|
||||
import * as AgentService from '../../services/agents';
|
||||
|
||||
export const postAgentsUnenrollHandler: RequestHandler<
|
||||
TypeOf<typeof PostAgentUnenrollRequestSchema.params>,
|
||||
undefined,
|
||||
TypeOf<typeof PostAgentUnenrollRequestSchema.body>
|
||||
> = async (context, request, response) => {
|
||||
const soClient = context.core.savedObjects.client;
|
||||
try {
|
||||
if (request.body?.force === true) {
|
||||
await AgentService.forceUnenrollAgent(soClient, request.params.agentId);
|
||||
} else {
|
||||
await AgentService.unenrollAgent(soClient, request.params.agentId);
|
||||
}
|
||||
|
||||
const body: PostAgentUnenrollResponse = {
|
||||
success: true,
|
||||
};
|
||||
return response.ok({ body });
|
||||
} catch (e) {
|
||||
return response.customError({
|
||||
statusCode: 500,
|
||||
body: { message: e.message },
|
||||
});
|
||||
}
|
||||
};
|
|
@ -54,6 +54,8 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = {
|
|||
type: { type: 'keyword' },
|
||||
active: { type: 'boolean' },
|
||||
enrolled_at: { type: 'date' },
|
||||
unenrolled_at: { type: 'date' },
|
||||
unenrollment_started_at: { type: 'date' },
|
||||
access_api_key_id: { type: 'keyword' },
|
||||
version: { type: 'keyword' },
|
||||
user_provided_metadata: { type: 'flattened' },
|
||||
|
@ -313,6 +315,9 @@ export function registerEncryptedSavedObjects(
|
|||
'config_newest_revision',
|
||||
'updated_at',
|
||||
'current_error_events',
|
||||
'unenrolled_at',
|
||||
'unenrollment_started_at',
|
||||
'packages',
|
||||
]),
|
||||
});
|
||||
encryptedSavedObjects.registerType({
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
AGENT_ACTION_SAVED_OBJECT_TYPE,
|
||||
} from '../../constants';
|
||||
import { getAgentActionByIds } from './actions';
|
||||
import { forceUnenrollAgent } from './unenroll';
|
||||
|
||||
const ALLOWED_ACKNOWLEDGEMENT_TYPE: string[] = ['ACTION_RESULT'];
|
||||
|
||||
|
@ -63,6 +64,12 @@ export async function acknowledgeAgentActions(
|
|||
if (actions.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const isAgentUnenrolled = actions.some((action) => action.type === 'UNENROLL');
|
||||
if (isAgentUnenrolled) {
|
||||
await forceUnenrollAgent(soClient, agent.id);
|
||||
}
|
||||
|
||||
const config = getLatestConfigIfUpdated(agent, actions);
|
||||
|
||||
await soClient.bulkUpdate<AgentSOAttributes | AgentActionSOAttributes>([
|
||||
|
|
|
@ -9,8 +9,21 @@ import { AgentSOAttributes } from '../../types';
|
|||
import { AGENT_SAVED_OBJECT_TYPE } from '../../constants';
|
||||
import { getAgent } from './crud';
|
||||
import * as APIKeyService from '../api_keys';
|
||||
import { createAgentAction } from './actions';
|
||||
|
||||
export async function unenrollAgent(soClient: SavedObjectsClientContract, agentId: string) {
|
||||
const now = new Date().toISOString();
|
||||
await createAgentAction(soClient, {
|
||||
agent_id: agentId,
|
||||
created_at: now,
|
||||
type: 'UNENROLL',
|
||||
});
|
||||
await soClient.update<AgentSOAttributes>(AGENT_SAVED_OBJECT_TYPE, agentId, {
|
||||
unenrollment_started_at: now,
|
||||
});
|
||||
}
|
||||
|
||||
export async function forceUnenrollAgent(soClient: SavedObjectsClientContract, agentId: string) {
|
||||
const agent = await getAgent(soClient, agentId);
|
||||
|
||||
await Promise.all([
|
||||
|
@ -21,7 +34,9 @@ export async function unenrollAgent(soClient: SavedObjectsClientContract, agentI
|
|||
? APIKeyService.invalidateAPIKey(soClient, agent.default_api_key_id)
|
||||
: undefined,
|
||||
]);
|
||||
|
||||
await soClient.update<AgentSOAttributes>(AGENT_SAVED_OBJECT_TYPE, agentId, {
|
||||
active: false,
|
||||
unenrolled_at: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -70,6 +70,11 @@ export const PostAgentUnenrollRequestSchema = {
|
|||
params: schema.object({
|
||||
agentId: schema.string(),
|
||||
}),
|
||||
body: schema.nullable(
|
||||
schema.object({
|
||||
force: schema.boolean(),
|
||||
})
|
||||
),
|
||||
};
|
||||
|
||||
export const PutAgentReassignRequestSchema = {
|
||||
|
|
|
@ -90,7 +90,7 @@ export default function (providerContext: FtrProviderContext) {
|
|||
events: [
|
||||
{
|
||||
type: 'ACTION_RESULT',
|
||||
subtype: 'CONFIG',
|
||||
subtype: 'ACKNOWLEDGED',
|
||||
timestamp: '2019-01-04T14:32:03.36764-05:00',
|
||||
action_id: configChangeAction.id,
|
||||
agent_id: enrollmentResponse.item.id,
|
||||
|
@ -133,6 +133,42 @@ export default function (providerContext: FtrProviderContext) {
|
|||
expect(unenrollResponse.success).to.eql(true);
|
||||
|
||||
// Checkin after unenrollment
|
||||
const { body: checkinAfterUnenrollResponse } = await supertestWithoutAuth
|
||||
.post(`/api/ingest_manager/fleet/agents/${enrollmentResponse.item.id}/checkin`)
|
||||
.set('kbn-xsrf', 'xx')
|
||||
.set('Authorization', `ApiKey ${agentAccessAPIKey}`)
|
||||
.send({
|
||||
events: [],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(checkinAfterUnenrollResponse.success).to.eql(true);
|
||||
expect(checkinAfterUnenrollResponse.actions).length(1);
|
||||
expect(checkinAfterUnenrollResponse.actions[0].type).be('UNENROLL');
|
||||
const unenrollAction = checkinAfterUnenrollResponse.actions[0];
|
||||
|
||||
// ack unenroll actions
|
||||
const { body: ackUnenrollApiResponse } = await supertestWithoutAuth
|
||||
.post(`/api/ingest_manager/fleet/agents/${enrollmentResponse.item.id}/acks`)
|
||||
.set('Authorization', `ApiKey ${agentAccessAPIKey}`)
|
||||
.set('kbn-xsrf', 'xx')
|
||||
.send({
|
||||
events: [
|
||||
{
|
||||
type: 'ACTION_RESULT',
|
||||
subtype: 'ACKNOWLEDGED',
|
||||
timestamp: '2019-01-04T14:32:03.36764-05:00',
|
||||
action_id: unenrollAction.id,
|
||||
agent_id: enrollmentResponse.item.id,
|
||||
message: 'hello',
|
||||
payload: 'payload',
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(200);
|
||||
expect(ackUnenrollApiResponse.success).to.eql(true);
|
||||
|
||||
// Checkin after unenrollment acknowledged
|
||||
await supertestWithoutAuth
|
||||
.post(`/api/ingest_manager/fleet/agents/${enrollmentResponse.item.id}/checkin`)
|
||||
.set('kbn-xsrf', 'xx')
|
||||
|
|
|
@ -67,7 +67,7 @@ export default function (providerContext: FtrProviderContext) {
|
|||
.post(`/api/ingest_manager/fleet/agents/agent1/unenroll`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.send({
|
||||
ids: ['agent1'],
|
||||
force: true,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
|
@ -80,7 +80,7 @@ export default function (providerContext: FtrProviderContext) {
|
|||
.post(`/api/ingest_manager/fleet/agents/agent1/unenroll`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.send({
|
||||
ids: ['agent1'],
|
||||
force: true,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
|
|
Loading…
Reference in a new issue