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

EMT-248: add post action request handler and resources #60581

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,98 @@
/*
* 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',
}),
getSavedObjectsClientContract: jest.fn().mockReturnValueOnce(mockSavedObjectsClient),
updateAgentActions: jest.fn().mockReturnValueOnce(agentAction),
} as jest.Mocked<ActionsService>;

const postNewAgentActionHandler = postNewAgentActionHandlerBuilder(actionsService);
await postNewAgentActionHandler(
({} 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 = actionsService.getSavedObjectsClientContract(request);

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 },
});
}
};
};
16 changes: 16 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,20 @@ 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,
getSavedObjectsClientContract: getInternalUserSOClient,
Copy link
Member

Choose a reason for hiding this comment

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

Actually we want to use the SOClient that comme from the request directly
like this here const soClient = context.core.savedObjects.client; and not the internal user one

Copy link
Contributor Author

Choose a reason for hiding this comment

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

okay will make the change.

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,63 @@
/*
* 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);
});
});
52 changes: 52 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,52 @@
/*
* 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 { KibanaRequest, 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);
Copy link
Contributor

Choose a reason for hiding this comment

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

Just realized that this order will have the passed values will overwrite the agentAction values

Is agentConfig the default values or final values?

Object.assign({}, newAgentConfig, agentAction) will use agentConfig as final values

}

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

getSavedObjectsClientContract: (kibanaRequest: KibanaRequest) => SavedObjectsClientContract;

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