[Ingest]EMT-248: add post action request handler and resources (#60581)
[Ingest]EMT-248: add resource to allow to post new agent action.
This commit is contained in:
parent
3acbbcd2b0
commit
d1aaa4430a
|
@ -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`,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: {
|
||||
|
@ -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[] };
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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 },
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
|
@ -22,6 +22,7 @@ import {
|
|||
PostAgentAcksRequestSchema,
|
||||
PostAgentUnenrollRequestSchema,
|
||||
GetAgentStatusRequestSchema,
|
||||
PostNewAgentActionRequestSchema,
|
||||
} from '../../types';
|
||||
import {
|
||||
getAgentsHandler,
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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>;
|
||||
}
|
|
@ -12,3 +12,4 @@ export * from './unenroll';
|
|||
export * from './status';
|
||||
export * from './crud';
|
||||
export * from './update';
|
||||
export * from './actions';
|
||||
|
|
|
@ -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()),
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { AckEventSchema, AgentEventSchema, AgentTypeSchema } from '../models';
|
||||
import { AckEventSchema, AgentEventSchema, AgentTypeSchema, NewAgentActionSchema } from '../models';
|
||||
|
||||
export const GetAgentsRequestSchema = {
|
||||
query: schema.object({
|
||||
|
@ -52,6 +52,15 @@ export const PostAgentAcksRequestSchema = {
|
|||
}),
|
||||
};
|
||||
|
||||
export const PostNewAgentActionRequestSchema = {
|
||||
body: schema.object({
|
||||
action: NewAgentActionSchema,
|
||||
}),
|
||||
params: schema.object({
|
||||
agentId: schema.string(),
|
||||
}),
|
||||
};
|
||||
|
||||
export const PostAgentUnenrollRequestSchema = {
|
||||
body: schema.oneOf([
|
||||
schema.object({
|
||||
|
|
86
x-pack/test/api_integration/apis/fleet/agents/actions.ts
Normal file
86
x-pack/test/api_integration/apis/fleet/agents/actions.ts
Normal file
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default function(providerContext: FtrProviderContext) {
|
||||
const { getService } = providerContext;
|
||||
const esArchiver = getService('esArchiver');
|
||||
const supertest = getService('supertest');
|
||||
|
||||
describe('fleet_agents_actions', () => {
|
||||
before(async () => {
|
||||
await esArchiver.loadIfNeeded('fleet/agents');
|
||||
});
|
||||
after(async () => {
|
||||
await esArchiver.unload('fleet/agents');
|
||||
});
|
||||
|
||||
it('should return a 200 if this a valid actions request', async () => {
|
||||
const { body: apiResponse } = await supertest
|
||||
.post(`/api/ingest_manager/fleet/agents/agent1/actions`)
|
||||
.set('kbn-xsrf', 'xx')
|
||||
.send({
|
||||
action: {
|
||||
type: 'CONFIG_CHANGE',
|
||||
data: 'action_data',
|
||||
sent_at: '2020-03-18T19:45:02.620Z',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(apiResponse.success).to.be(true);
|
||||
expect(apiResponse.item.data).to.be('action_data');
|
||||
expect(apiResponse.item.sent_at).to.be('2020-03-18T19:45:02.620Z');
|
||||
|
||||
const { body: agentResponse } = await supertest
|
||||
.get(`/api/ingest_manager/fleet/agents/agent1`)
|
||||
.set('kbn-xsrf', 'xx')
|
||||
.expect(200);
|
||||
|
||||
const updatedAction = agentResponse.item.actions.find(
|
||||
(itemAction: Record<string, string>) => itemAction?.data === 'action_data'
|
||||
);
|
||||
|
||||
expect(updatedAction.type).to.be('CONFIG_CHANGE');
|
||||
expect(updatedAction.data).to.be('action_data');
|
||||
expect(updatedAction.sent_at).to.be('2020-03-18T19:45:02.620Z');
|
||||
});
|
||||
|
||||
it('should return a 400 when request does not have type information', async () => {
|
||||
const { body: apiResponse } = await supertest
|
||||
.post(`/api/ingest_manager/fleet/agents/agent1/actions`)
|
||||
.set('kbn-xsrf', 'xx')
|
||||
.send({
|
||||
action: {
|
||||
data: 'action_data',
|
||||
sent_at: '2020-03-18T19:45:02.620Z',
|
||||
},
|
||||
})
|
||||
.expect(400);
|
||||
expect(apiResponse.message).to.eql(
|
||||
'[request body.action.type]: expected at least one defined value but got [undefined]'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return a 404 when agent does not exist', async () => {
|
||||
const { body: apiResponse } = await supertest
|
||||
.post(`/api/ingest_manager/fleet/agents/agent100/actions`)
|
||||
.set('kbn-xsrf', 'xx')
|
||||
.send({
|
||||
action: {
|
||||
type: 'CONFIG_CHANGE',
|
||||
data: 'action_data',
|
||||
sent_at: '2020-03-18T19:45:02.620Z',
|
||||
},
|
||||
})
|
||||
.expect(404);
|
||||
expect(apiResponse.message).to.eql('Saved object [agents/agent100] not found');
|
||||
});
|
||||
});
|
||||
}
|
|
@ -15,5 +15,6 @@ export default function loadTests({ loadTestFile }) {
|
|||
loadTestFile(require.resolve('./agents/acks'));
|
||||
loadTestFile(require.resolve('./enrollment_api_keys/crud'));
|
||||
loadTestFile(require.resolve('./install'));
|
||||
loadTestFile(require.resolve('./agents/actions'));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue