Skip to content

Commit d22828d

Browse files
[Ingest]EMT-248: add post action request handler and resources (#60581) (#60705)
[Ingest]EMT-248: add resource to allow to post new agent action.
1 parent c7df9c8 commit d22828d

File tree

13 files changed

+423
-5
lines changed

13 files changed

+423
-5
lines changed

x-pack/plugins/ingest_manager/common/constants/routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export const AGENT_API_ROUTES = {
5050
EVENTS_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/events`,
5151
CHECKIN_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/checkin`,
5252
ACKS_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/acks`,
53+
ACTIONS_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/actions`,
5354
ENROLL_PATTERN: `${FLEET_API_ROOT}/agents/enroll`,
5455
UNENROLL_PATTERN: `${FLEET_API_ROOT}/agents/unenroll`,
5556
STATUS_PATTERN: `${FLEET_API_ROOT}/agent-status`,

x-pack/plugins/ingest_manager/common/types/models/agent.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,17 @@ export type AgentType =
1414

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

17-
export interface AgentAction extends SavedObjectAttributes {
17+
export interface NewAgentAction {
1818
type: 'CONFIG_CHANGE' | 'DATA_DUMP' | 'RESUME' | 'PAUSE';
19-
id: string;
20-
created_at: string;
2119
data?: string;
2220
sent_at?: string;
2321
}
2422

23+
export type AgentAction = NewAgentAction & {
24+
id: string;
25+
created_at: string;
26+
} & SavedObjectAttributes;
27+
2528
export interface AgentEvent {
2629
type: 'STATE' | 'ERROR' | 'ACTION_RESULT' | 'ACTION';
2730
subtype: // State

x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

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

99
export interface GetAgentsRequest {
1010
query: {
@@ -81,6 +81,20 @@ export interface PostAgentAcksResponse {
8181
success: boolean;
8282
}
8383

84+
export interface PostNewAgentActionRequest {
85+
body: {
86+
action: NewAgentAction;
87+
};
88+
params: {
89+
agentId: string;
90+
};
91+
}
92+
93+
export interface PostNewAgentActionResponse {
94+
success: boolean;
95+
item: AgentAction;
96+
}
97+
8498
export interface PostAgentUnenrollRequest {
8599
body: { kuery: string } | { ids: string[] };
86100
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { NewAgentActionSchema } from '../../types/models';
8+
import {
9+
KibanaResponseFactory,
10+
RequestHandlerContext,
11+
SavedObjectsClientContract,
12+
} from 'kibana/server';
13+
import { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock';
14+
import { httpServerMock } from '../../../../../../src/core/server/http/http_server.mocks';
15+
import { ActionsService } from '../../services/agents';
16+
import { AgentAction } from '../../../common/types/models';
17+
import { postNewAgentActionHandlerBuilder } from './actions_handlers';
18+
import {
19+
PostNewAgentActionRequest,
20+
PostNewAgentActionResponse,
21+
} from '../../../common/types/rest_spec';
22+
23+
describe('test actions handlers schema', () => {
24+
it('validate that new agent actions schema is valid', async () => {
25+
expect(
26+
NewAgentActionSchema.validate({
27+
type: 'CONFIG_CHANGE',
28+
data: 'data',
29+
sent_at: '2020-03-14T19:45:02.620Z',
30+
})
31+
).toBeTruthy();
32+
});
33+
34+
it('validate that new agent actions schema is invalid when required properties are not provided', async () => {
35+
expect(() => {
36+
NewAgentActionSchema.validate({
37+
data: 'data',
38+
sent_at: '2020-03-14T19:45:02.620Z',
39+
});
40+
}).toThrowError();
41+
});
42+
});
43+
44+
describe('test actions handlers', () => {
45+
let mockResponse: jest.Mocked<KibanaResponseFactory>;
46+
let mockSavedObjectsClient: jest.Mocked<SavedObjectsClientContract>;
47+
48+
beforeEach(() => {
49+
mockSavedObjectsClient = savedObjectsClientMock.create();
50+
mockResponse = httpServerMock.createResponseFactory();
51+
});
52+
53+
it('should succeed on valid new agent action', async () => {
54+
const postNewAgentActionRequest: PostNewAgentActionRequest = {
55+
body: {
56+
action: {
57+
type: 'CONFIG_CHANGE',
58+
data: 'data',
59+
sent_at: '2020-03-14T19:45:02.620Z',
60+
},
61+
},
62+
params: {
63+
agentId: 'id',
64+
},
65+
};
66+
67+
const mockRequest = httpServerMock.createKibanaRequest(postNewAgentActionRequest);
68+
69+
const agentAction = ({
70+
type: 'CONFIG_CHANGE',
71+
id: 'action1',
72+
sent_at: '2020-03-14T19:45:02.620Z',
73+
timestamp: '2019-01-04T14:32:03.36764-05:00',
74+
created_at: '2020-03-14T19:45:02.620Z',
75+
} as unknown) as AgentAction;
76+
77+
const actionsService: ActionsService = {
78+
getAgent: jest.fn().mockReturnValueOnce({
79+
id: 'agent',
80+
}),
81+
updateAgentActions: jest.fn().mockReturnValueOnce(agentAction),
82+
} as jest.Mocked<ActionsService>;
83+
84+
const postNewAgentActionHandler = postNewAgentActionHandlerBuilder(actionsService);
85+
await postNewAgentActionHandler(
86+
({
87+
core: {
88+
savedObjects: {
89+
client: mockSavedObjectsClient,
90+
},
91+
},
92+
} as unknown) as RequestHandlerContext,
93+
mockRequest,
94+
mockResponse
95+
);
96+
97+
const expectedAgentActionResponse = (mockResponse.ok.mock.calls[0][0]
98+
?.body as unknown) as PostNewAgentActionResponse;
99+
100+
expect(expectedAgentActionResponse.item).toEqual(agentAction);
101+
expect(expectedAgentActionResponse.success).toEqual(true);
102+
});
103+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
// handlers that handle agent actions request
8+
9+
import { RequestHandler } from 'kibana/server';
10+
import { TypeOf } from '@kbn/config-schema';
11+
import { PostNewAgentActionRequestSchema } from '../../types/rest_spec';
12+
import { ActionsService } from '../../services/agents';
13+
import { NewAgentAction } from '../../../common/types/models';
14+
import { PostNewAgentActionResponse } from '../../../common/types/rest_spec';
15+
16+
export const postNewAgentActionHandlerBuilder = function(
17+
actionsService: ActionsService
18+
): RequestHandler<
19+
TypeOf<typeof PostNewAgentActionRequestSchema.params>,
20+
undefined,
21+
TypeOf<typeof PostNewAgentActionRequestSchema.body>
22+
> {
23+
return async (context, request, response) => {
24+
try {
25+
const soClient = context.core.savedObjects.client;
26+
27+
const agent = await actionsService.getAgent(soClient, request.params.agentId);
28+
29+
const newAgentAction = request.body.action as NewAgentAction;
30+
31+
const savedAgentAction = await actionsService.updateAgentActions(
32+
soClient,
33+
agent,
34+
newAgentAction
35+
);
36+
37+
const body: PostNewAgentActionResponse = {
38+
success: true,
39+
item: savedAgentAction,
40+
};
41+
42+
return response.ok({ body });
43+
} catch (e) {
44+
if (e.isBoom) {
45+
return response.customError({
46+
statusCode: e.output.statusCode,
47+
body: { message: e.message },
48+
});
49+
}
50+
51+
return response.customError({
52+
statusCode: 500,
53+
body: { message: e.message },
54+
});
55+
}
56+
};
57+
};

x-pack/plugins/ingest_manager/server/routes/agent/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
PostAgentAcksRequestSchema,
2323
PostAgentUnenrollRequestSchema,
2424
GetAgentStatusRequestSchema,
25+
PostNewAgentActionRequestSchema,
2526
} from '../../types';
2627
import {
2728
getAgentsHandler,
@@ -37,6 +38,7 @@ import {
3738
} from './handlers';
3839
import { postAgentAcksHandlerBuilder } from './acks_handlers';
3940
import * as AgentService from '../../services/agents';
41+
import { postNewAgentActionHandlerBuilder } from './actions_handlers';
4042

4143
export const registerRoutes = (router: IRouter) => {
4244
// Get one
@@ -111,6 +113,19 @@ export const registerRoutes = (router: IRouter) => {
111113
})
112114
);
113115

116+
// Agent actions
117+
router.post(
118+
{
119+
path: AGENT_API_ROUTES.ACTIONS_PATTERN,
120+
validate: PostNewAgentActionRequestSchema,
121+
options: { tags: [`access:${PLUGIN_ID}-all`] },
122+
},
123+
postNewAgentActionHandlerBuilder({
124+
getAgent: AgentService.getAgent,
125+
updateAgentActions: AgentService.updateAgentActions,
126+
})
127+
);
128+
114129
router.post(
115130
{
116131
path: AGENT_API_ROUTES.UNENROLL_PATTERN,
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { createAgentAction, updateAgentActions } from './actions';
8+
import { Agent, AgentAction, NewAgentAction } from '../../../common/types/models';
9+
import { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock';
10+
import { AGENT_TYPE_PERMANENT } from '../../../common/constants';
11+
12+
interface UpdatedActions {
13+
actions: AgentAction[];
14+
}
15+
16+
describe('test agent actions services', () => {
17+
it('should update agent current actions with new action', async () => {
18+
const mockSavedObjectsClient = savedObjectsClientMock.create();
19+
20+
const newAgentAction: NewAgentAction = {
21+
type: 'CONFIG_CHANGE',
22+
data: 'data',
23+
sent_at: '2020-03-14T19:45:02.620Z',
24+
};
25+
26+
await updateAgentActions(
27+
mockSavedObjectsClient,
28+
({
29+
id: 'id',
30+
type: AGENT_TYPE_PERMANENT,
31+
actions: [
32+
{
33+
type: 'CONFIG_CHANGE',
34+
id: 'action1',
35+
sent_at: '2020-03-14T19:45:02.620Z',
36+
timestamp: '2019-01-04T14:32:03.36764-05:00',
37+
created_at: '2020-03-14T19:45:02.620Z',
38+
},
39+
],
40+
} as unknown) as Agent,
41+
newAgentAction
42+
);
43+
44+
const updatedAgentActions = (mockSavedObjectsClient.update.mock
45+
.calls[0][2] as unknown) as UpdatedActions;
46+
47+
expect(updatedAgentActions.actions.length).toEqual(2);
48+
const actualAgentAction = updatedAgentActions.actions.find(action => action?.data === 'data');
49+
expect(actualAgentAction?.type).toEqual(newAgentAction.type);
50+
expect(actualAgentAction?.data).toEqual(newAgentAction.data);
51+
expect(actualAgentAction?.sent_at).toEqual(newAgentAction.sent_at);
52+
});
53+
54+
it('should create agent action from new agent action model', async () => {
55+
const newAgentAction: NewAgentAction = {
56+
type: 'CONFIG_CHANGE',
57+
data: 'data',
58+
sent_at: '2020-03-14T19:45:02.620Z',
59+
};
60+
const now = new Date();
61+
const agentAction = createAgentAction(now, newAgentAction);
62+
63+
expect(agentAction.type).toEqual(newAgentAction.type);
64+
expect(agentAction.data).toEqual(newAgentAction.data);
65+
expect(agentAction.sent_at).toEqual(newAgentAction.sent_at);
66+
});
67+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { SavedObjectsClientContract } from 'kibana/server';
8+
import uuid from 'uuid';
9+
import {
10+
Agent,
11+
AgentAction,
12+
AgentSOAttributes,
13+
NewAgentAction,
14+
} from '../../../common/types/models';
15+
import { AGENT_SAVED_OBJECT_TYPE } from '../../../common/constants';
16+
17+
export async function updateAgentActions(
18+
soClient: SavedObjectsClientContract,
19+
agent: Agent,
20+
newAgentAction: NewAgentAction
21+
): Promise<AgentAction> {
22+
const agentAction = createAgentAction(new Date(), newAgentAction);
23+
24+
agent.actions.push(agentAction);
25+
26+
await soClient.update<AgentSOAttributes>(AGENT_SAVED_OBJECT_TYPE, agent.id, {
27+
actions: agent.actions,
28+
});
29+
30+
return agentAction;
31+
}
32+
33+
export function createAgentAction(createdAt: Date, newAgentAction: NewAgentAction): AgentAction {
34+
const agentAction = {
35+
id: uuid.v4(),
36+
created_at: createdAt.toISOString(),
37+
};
38+
39+
return Object.assign(agentAction, newAgentAction);
40+
}
41+
42+
export interface ActionsService {
43+
getAgent: (soClient: SavedObjectsClientContract, agentId: string) => Promise<Agent>;
44+
45+
updateAgentActions: (
46+
soClient: SavedObjectsClientContract,
47+
agent: Agent,
48+
newAgentAction: NewAgentAction
49+
) => Promise<AgentAction>;
50+
}

x-pack/plugins/ingest_manager/server/services/agents/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ export * from './unenroll';
1212
export * from './status';
1313
export * from './crud';
1414
export * from './update';
15+
export * from './actions';

x-pack/plugins/ingest_manager/server/types/models/agent.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,14 @@ export const AckEventSchema = schema.object({
5252
export const AgentEventSchema = schema.object({
5353
...AgentEventBase,
5454
});
55+
56+
export const NewAgentActionSchema = schema.object({
57+
type: schema.oneOf([
58+
schema.literal('CONFIG_CHANGE'),
59+
schema.literal('DATA_DUMP'),
60+
schema.literal('RESUME'),
61+
schema.literal('PAUSE'),
62+
]),
63+
data: schema.maybe(schema.string()),
64+
sent_at: schema.maybe(schema.string()),
65+
});

0 commit comments

Comments
 (0)