[Ingest Manager] Improve agent unenrollment with unenroll action (#70031)

This commit is contained in:
Nicolas Chaulet 2020-07-03 08:23:12 -04:00 committed by GitHub
parent 571a610c7e
commit 72b300424b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 167 additions and 34 deletions

View file

@ -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"
},

View file

@ -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';
}

View file

@ -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;

View file

@ -236,7 +236,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
},
{
field: 'active',
width: '100px',
width: '120px',
name: i18n.translate('xpack.ingestManager.agentList.statusColumnTitle', {
defaultMessage: 'Status',
}),

View file

@ -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;
}

View file

@ -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 },
}
);

View file

@ -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,

View file

@ -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

View file

@ -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 },
});
}
};

View file

@ -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({

View file

@ -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>([

View file

@ -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(),
});
}

View file

@ -70,6 +70,11 @@ export const PostAgentUnenrollRequestSchema = {
params: schema.object({
agentId: schema.string(),
}),
body: schema.nullable(
schema.object({
force: schema.boolean(),
})
),
};
export const PutAgentReassignRequestSchema = {

View file

@ -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,
@ -132,7 +132,43 @@ export default function (providerContext: FtrProviderContext) {
.expect(200);
expect(unenrollResponse.success).to.eql(true);
// Checkin after unenrollment
// 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')

View file

@ -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);