Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 8 commits into from
Jul 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

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

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

Expand Down