Skip to content

Commit

Permalink
[Ingest]EMT-248: add post action request handler and resources (#60581)
Browse files Browse the repository at this point in the history
[Ingest]EMT-248: add resource to allow to post new agent action.
  • Loading branch information
nnamdifrankie authored Mar 19, 2020
1 parent 3acbbcd commit d1aaa44
Show file tree
Hide file tree
Showing 13 changed files with 423 additions and 5 deletions.
1 change: 1 addition & 0 deletions x-pack/plugins/ingest_manager/common/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const AGENT_API_ROUTES = {
EVENTS_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/events`,
CHECKIN_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/checkin`,
ACKS_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/acks`,
ACTIONS_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/actions`,
ENROLL_PATTERN: `${FLEET_API_ROOT}/agents/enroll`,
UNENROLL_PATTERN: `${FLEET_API_ROOT}/agents/unenroll`,
STATUS_PATTERN: `${FLEET_API_ROOT}/agent-status`,
Expand Down
9 changes: 6 additions & 3 deletions x-pack/plugins/ingest_manager/common/types/models/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@ export type AgentType =

export type AgentStatus = 'offline' | 'error' | 'online' | 'inactive' | 'warning';

export interface AgentAction extends SavedObjectAttributes {
export interface NewAgentAction {
type: 'CONFIG_CHANGE' | 'DATA_DUMP' | 'RESUME' | 'PAUSE';
id: string;
created_at: string;
data?: string;
sent_at?: string;
}

export type AgentAction = NewAgentAction & {
id: string;
created_at: string;
} & SavedObjectAttributes;

export interface AgentEvent {
type: 'STATE' | 'ERROR' | 'ACTION_RESULT' | 'ACTION';
subtype: // State
Expand Down
16 changes: 15 additions & 1 deletion x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { Agent, AgentAction, AgentEvent, AgentStatus, AgentType } from '../models';
import { Agent, AgentAction, AgentEvent, AgentStatus, AgentType, NewAgentAction } from '../models';

export interface GetAgentsRequest {
query: {
Expand Down Expand Up @@ -81,6 +81,20 @@ export interface PostAgentAcksResponse {
success: boolean;
}

export interface PostNewAgentActionRequest {
body: {
action: NewAgentAction;
};
params: {
agentId: string;
};
}

export interface PostNewAgentActionResponse {
success: boolean;
item: AgentAction;
}

export interface PostAgentUnenrollRequest {
body: { kuery: string } | { ids: string[] };
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* 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 { NewAgentActionSchema } from '../../types/models';
import {
KibanaResponseFactory,
RequestHandlerContext,
SavedObjectsClientContract,
} from 'kibana/server';
import { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock';
import { httpServerMock } from '../../../../../../src/core/server/http/http_server.mocks';
import { ActionsService } from '../../services/agents';
import { AgentAction } from '../../../common/types/models';
import { postNewAgentActionHandlerBuilder } from './actions_handlers';
import {
PostNewAgentActionRequest,
PostNewAgentActionResponse,
} from '../../../common/types/rest_spec';

describe('test actions handlers schema', () => {
it('validate that new agent actions schema is valid', async () => {
expect(
NewAgentActionSchema.validate({
type: 'CONFIG_CHANGE',
data: 'data',
sent_at: '2020-03-14T19:45:02.620Z',
})
).toBeTruthy();
});

it('validate that new agent actions schema is invalid when required properties are not provided', async () => {
expect(() => {
NewAgentActionSchema.validate({
data: 'data',
sent_at: '2020-03-14T19:45:02.620Z',
});
}).toThrowError();
});
});

describe('test actions handlers', () => {
let mockResponse: jest.Mocked<KibanaResponseFactory>;
let mockSavedObjectsClient: jest.Mocked<SavedObjectsClientContract>;

beforeEach(() => {
mockSavedObjectsClient = savedObjectsClientMock.create();
mockResponse = httpServerMock.createResponseFactory();
});

it('should succeed on valid new agent action', async () => {
const postNewAgentActionRequest: PostNewAgentActionRequest = {
body: {
action: {
type: 'CONFIG_CHANGE',
data: 'data',
sent_at: '2020-03-14T19:45:02.620Z',
},
},
params: {
agentId: 'id',
},
};

const mockRequest = httpServerMock.createKibanaRequest(postNewAgentActionRequest);

const agentAction = ({
type: 'CONFIG_CHANGE',
id: 'action1',
sent_at: '2020-03-14T19:45:02.620Z',
timestamp: '2019-01-04T14:32:03.36764-05:00',
created_at: '2020-03-14T19:45:02.620Z',
} as unknown) as AgentAction;

const actionsService: ActionsService = {
getAgent: jest.fn().mockReturnValueOnce({
id: 'agent',
}),
updateAgentActions: jest.fn().mockReturnValueOnce(agentAction),
} as jest.Mocked<ActionsService>;

const postNewAgentActionHandler = postNewAgentActionHandlerBuilder(actionsService);
await postNewAgentActionHandler(
({
core: {
savedObjects: {
client: mockSavedObjectsClient,
},
},
} as unknown) as RequestHandlerContext,
mockRequest,
mockResponse
);

const expectedAgentActionResponse = (mockResponse.ok.mock.calls[0][0]
?.body as unknown) as PostNewAgentActionResponse;

expect(expectedAgentActionResponse.item).toEqual(agentAction);
expect(expectedAgentActionResponse.success).toEqual(true);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* 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.
*/

// handlers that handle agent actions request

import { RequestHandler } from 'kibana/server';
import { TypeOf } from '@kbn/config-schema';
import { PostNewAgentActionRequestSchema } from '../../types/rest_spec';
import { ActionsService } from '../../services/agents';
import { NewAgentAction } from '../../../common/types/models';
import { PostNewAgentActionResponse } from '../../../common/types/rest_spec';

export const postNewAgentActionHandlerBuilder = function(
actionsService: ActionsService
): RequestHandler<
TypeOf<typeof PostNewAgentActionRequestSchema.params>,
undefined,
TypeOf<typeof PostNewAgentActionRequestSchema.body>
> {
return async (context, request, response) => {
try {
const soClient = context.core.savedObjects.client;

const agent = await actionsService.getAgent(soClient, request.params.agentId);

const newAgentAction = request.body.action as NewAgentAction;

const savedAgentAction = await actionsService.updateAgentActions(
soClient,
agent,
newAgentAction
);

const body: PostNewAgentActionResponse = {
success: true,
item: savedAgentAction,
};

return response.ok({ body });
} catch (e) {
if (e.isBoom) {
return response.customError({
statusCode: e.output.statusCode,
body: { message: e.message },
});
}

return response.customError({
statusCode: 500,
body: { message: e.message },
});
}
};
};
15 changes: 15 additions & 0 deletions x-pack/plugins/ingest_manager/server/routes/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
PostAgentAcksRequestSchema,
PostAgentUnenrollRequestSchema,
GetAgentStatusRequestSchema,
PostNewAgentActionRequestSchema,
} from '../../types';
import {
getAgentsHandler,
Expand All @@ -37,6 +38,7 @@ import {
} from './handlers';
import { postAgentAcksHandlerBuilder } from './acks_handlers';
import * as AgentService from '../../services/agents';
import { postNewAgentActionHandlerBuilder } from './actions_handlers';

export const registerRoutes = (router: IRouter) => {
// Get one
Expand Down Expand Up @@ -111,6 +113,19 @@ export const registerRoutes = (router: IRouter) => {
})
);

// Agent actions
router.post(
{
path: AGENT_API_ROUTES.ACTIONS_PATTERN,
validate: PostNewAgentActionRequestSchema,
options: { tags: [`access:${PLUGIN_ID}-all`] },
},
postNewAgentActionHandlerBuilder({
getAgent: AgentService.getAgent,
updateAgentActions: AgentService.updateAgentActions,
})
);

router.post(
{
path: AGENT_API_ROUTES.UNENROLL_PATTERN,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* 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 { createAgentAction, updateAgentActions } from './actions';
import { Agent, AgentAction, NewAgentAction } from '../../../common/types/models';
import { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock';
import { AGENT_TYPE_PERMANENT } from '../../../common/constants';

interface UpdatedActions {
actions: AgentAction[];
}

describe('test agent actions services', () => {
it('should update agent current actions with new action', async () => {
const mockSavedObjectsClient = savedObjectsClientMock.create();

const newAgentAction: NewAgentAction = {
type: 'CONFIG_CHANGE',
data: 'data',
sent_at: '2020-03-14T19:45:02.620Z',
};

await updateAgentActions(
mockSavedObjectsClient,
({
id: 'id',
type: AGENT_TYPE_PERMANENT,
actions: [
{
type: 'CONFIG_CHANGE',
id: 'action1',
sent_at: '2020-03-14T19:45:02.620Z',
timestamp: '2019-01-04T14:32:03.36764-05:00',
created_at: '2020-03-14T19:45:02.620Z',
},
],
} as unknown) as Agent,
newAgentAction
);

const updatedAgentActions = (mockSavedObjectsClient.update.mock
.calls[0][2] as unknown) as UpdatedActions;

expect(updatedAgentActions.actions.length).toEqual(2);
const actualAgentAction = updatedAgentActions.actions.find(action => action?.data === 'data');
expect(actualAgentAction?.type).toEqual(newAgentAction.type);
expect(actualAgentAction?.data).toEqual(newAgentAction.data);
expect(actualAgentAction?.sent_at).toEqual(newAgentAction.sent_at);
});

it('should create agent action from new agent action model', async () => {
const newAgentAction: NewAgentAction = {
type: 'CONFIG_CHANGE',
data: 'data',
sent_at: '2020-03-14T19:45:02.620Z',
};
const now = new Date();
const agentAction = createAgentAction(now, newAgentAction);

expect(agentAction.type).toEqual(newAgentAction.type);
expect(agentAction.data).toEqual(newAgentAction.data);
expect(agentAction.sent_at).toEqual(newAgentAction.sent_at);
});
});
50 changes: 50 additions & 0 deletions x-pack/plugins/ingest_manager/server/services/agents/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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 { SavedObjectsClientContract } from 'kibana/server';
import uuid from 'uuid';
import {
Agent,
AgentAction,
AgentSOAttributes,
NewAgentAction,
} from '../../../common/types/models';
import { AGENT_SAVED_OBJECT_TYPE } from '../../../common/constants';

export async function updateAgentActions(
soClient: SavedObjectsClientContract,
agent: Agent,
newAgentAction: NewAgentAction
): Promise<AgentAction> {
const agentAction = createAgentAction(new Date(), newAgentAction);

agent.actions.push(agentAction);

await soClient.update<AgentSOAttributes>(AGENT_SAVED_OBJECT_TYPE, agent.id, {
actions: agent.actions,
});

return agentAction;
}

export function createAgentAction(createdAt: Date, newAgentAction: NewAgentAction): AgentAction {
const agentAction = {
id: uuid.v4(),
created_at: createdAt.toISOString(),
};

return Object.assign(agentAction, newAgentAction);
}

export interface ActionsService {
getAgent: (soClient: SavedObjectsClientContract, agentId: string) => Promise<Agent>;

updateAgentActions: (
soClient: SavedObjectsClientContract,
agent: Agent,
newAgentAction: NewAgentAction
) => Promise<AgentAction>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export * from './unenroll';
export * from './status';
export * from './crud';
export * from './update';
export * from './actions';
11 changes: 11 additions & 0 deletions x-pack/plugins/ingest_manager/server/types/models/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,14 @@ export const AckEventSchema = schema.object({
export const AgentEventSchema = schema.object({
...AgentEventBase,
});

export const NewAgentActionSchema = schema.object({
type: schema.oneOf([
schema.literal('CONFIG_CHANGE'),
schema.literal('DATA_DUMP'),
schema.literal('RESUME'),
schema.literal('PAUSE'),
]),
data: schema.maybe(schema.string()),
sent_at: schema.maybe(schema.string()),
});
Loading

0 comments on commit d1aaa44

Please sign in to comment.