Skip to content

Commit

Permalink
[Ingest Manager] Improve agent unenrollment with unenroll action (#70031
Browse files Browse the repository at this point in the history
)
  • Loading branch information
nchaulet committed Jul 3, 2020
1 parent 4548d3d commit 4a956f2
Show file tree
Hide file tree
Showing 15 changed files with 167 additions and 34 deletions.
39 changes: 37 additions & 2 deletions x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [],
Expand All @@ -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": {
Expand Down Expand Up @@ -4096,6 +4125,12 @@
"enrolled_at": {
"type": "string"
},
"unenrolled_at": {
"type": "string"
},
"unenrollment_started_at": {
"type": "string"
},
"shared_id": {
"type": "string"
},
Expand Down
3 changes: 3 additions & 0 deletions x-pack/plugins/ingest_manager/common/services/agent_status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
Expand Down
10 changes: 6 additions & 4 deletions x-pack/plugins/ingest_manager/common/types/models/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
},
{
field: 'active',
width: '100px',
width: '120px',
name: i18n.translate('xpack.ingestManager.agentList.statusColumnTitle', {
defaultMessage: 'Status',
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
}
);
Expand Down
21 changes: 0 additions & 21 deletions x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
GetOneAgentEventsResponse,
PostAgentCheckinResponse,
PostAgentEnrollResponse,
PostAgentUnenrollResponse,
GetAgentStatusResponse,
PutAgentReassignResponse,
} from '../../../common/types';
Expand All @@ -25,7 +24,6 @@ import {
GetOneAgentEventsRequestSchema,
PostAgentCheckinRequestSchema,
PostAgentEnrollRequestSchema,
PostAgentUnenrollRequestSchema,
GetAgentStatusRequestSchema,
PutAgentReassignRequestSchema,
} from '../../types';
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/ingest_manager/server/routes/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ import {
getAgentEventsHandler,
postAgentCheckinHandler,
postAgentEnrollHandler,
postAgentsUnenrollHandler,
getAgentStatusForConfigHandler,
putAgentsReassignHandler,
} from './handlers';
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 },
});
}
};
5 changes: 5 additions & 0 deletions x-pack/plugins/ingest_manager/server/saved_objects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down Expand Up @@ -314,6 +316,9 @@ export function registerEncryptedSavedObjects(
'config_newest_revision',
'updated_at',
'current_error_events',
'unenrolled_at',
'unenrollment_started_at',
'packages',
]),
});
encryptedSavedObjects.registerType({
Expand Down
7 changes: 7 additions & 0 deletions x-pack/plugins/ingest_manager/server/services/agents/acks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'];

Expand Down Expand Up @@ -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>([
Expand Down
15 changes: 15 additions & 0 deletions x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand 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(),
});
}
5 changes: 5 additions & 0 deletions x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ export const PostAgentUnenrollRequestSchema = {
params: schema.object({
agentId: schema.string(),
}),
body: schema.nullable(
schema.object({
force: schema.boolean(),
})
),
};

export const PutAgentReassignRequestSchema = {
Expand Down
40 changes: 38 additions & 2 deletions x-pack/test/api_integration/apis/fleet/agent_flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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')
Expand Down
4 changes: 2 additions & 2 deletions x-pack/test/api_integration/apis/fleet/unenroll_agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,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);

Expand All @@ -81,7 +81,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);

Expand Down

0 comments on commit 4a956f2

Please sign in to comment.