Skip to content

Commit

Permalink
Return referenced by count with each action on the find API (elastic#…
Browse files Browse the repository at this point in the history
…49104)

* Initial work

* .kibana index configurable, NP ready implementation

* Fix broken jest tests

* Fix broken functional tests

* Add functional test

* Cleanup actions_client.test.ts
  • Loading branch information
mikecote committed Nov 1, 2019
1 parent dbc2785 commit 32307fa
Show file tree
Hide file tree
Showing 8 changed files with 220 additions and 61 deletions.
76 changes: 24 additions & 52 deletions x-pack/legacy/plugins/actions/server/actions_client.test.ts
Expand Up @@ -11,9 +11,14 @@ import { ActionsClient } from './actions_client';
import { ExecutorType } from './types';
import { ActionExecutor, TaskRunnerFactory } from './lib';
import { taskManagerMock } from '../../task_manager/task_manager.mock';
import { savedObjectsClientMock } from '../../../../../src/core/server/mocks';
import {
elasticsearchServiceMock,
savedObjectsClientMock,
} from '../../../../../src/core/server/mocks';

const defaultKibanaIndex = '.kibana';
const savedObjectsClient = savedObjectsClientMock.create();
const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();

const mockTaskManager = taskManagerMock.create();

Expand All @@ -22,11 +27,22 @@ const actionTypeRegistryParams = {
taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor()),
};

let actionsClient: ActionsClient;
let actionTypeRegistry: ActionTypeRegistry;
const executor: ExecutorType = async options => {
return { status: 'ok' };
};

beforeEach(() => jest.resetAllMocks());
beforeEach(() => {
jest.resetAllMocks();
actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
actionsClient = new ActionsClient({
actionTypeRegistry,
savedObjectsClient,
scopedClusterClient,
defaultKibanaIndex,
});
});

describe('create()', () => {
test('creates an action with all given properties', async () => {
Expand All @@ -40,16 +56,11 @@ describe('create()', () => {
},
references: [],
};
const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
actionTypeRegistry.register({
id: 'my-action-type',
name: 'My action type',
executor,
});
const actionsClient = new ActionsClient({
actionTypeRegistry,
savedObjectsClient,
});
savedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
const result = await actionsClient.create({
action: {
Expand Down Expand Up @@ -80,11 +91,6 @@ describe('create()', () => {
});

test('validates config', async () => {
const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
const actionsClient = new ActionsClient({
actionTypeRegistry,
savedObjectsClient,
});
actionTypeRegistry.register({
id: 'my-action-type',
name: 'My action type',
Expand All @@ -110,11 +116,6 @@ describe('create()', () => {
});

test(`throws an error when an action type doesn't exist`, async () => {
const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
const actionsClient = new ActionsClient({
actionTypeRegistry,
savedObjectsClient,
});
await expect(
actionsClient.create({
action: {
Expand All @@ -130,16 +131,11 @@ describe('create()', () => {
});

test('encrypts action type options unless specified not to', async () => {
const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
actionTypeRegistry.register({
id: 'my-action-type',
name: 'My action type',
executor,
});
const actionsClient = new ActionsClient({
actionTypeRegistry,
savedObjectsClient,
});
savedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'type',
Expand Down Expand Up @@ -198,11 +194,6 @@ describe('create()', () => {

describe('get()', () => {
test('calls savedObjectsClient with id', async () => {
const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
const actionsClient = new ActionsClient({
actionTypeRegistry,
savedObjectsClient,
});
savedObjectsClient.get.mockResolvedValueOnce({
id: '1',
type: 'type',
Expand Down Expand Up @@ -242,12 +233,12 @@ describe('find()', () => {
},
],
};
const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
const actionsClient = new ActionsClient({
actionTypeRegistry,
savedObjectsClient,
});
savedObjectsClient.find.mockResolvedValueOnce(expectedResult);
scopedClusterClient.callAsInternalUser.mockResolvedValueOnce({
aggregations: {
'1': { doc_count: 6 },
},
});
const result = await actionsClient.find({});
expect(result).toEqual({
total: 1,
Expand All @@ -259,6 +250,7 @@ describe('find()', () => {
config: {
foo: 'bar',
},
referencedByCount: 6,
},
],
});
Expand All @@ -276,11 +268,6 @@ describe('find()', () => {
describe('delete()', () => {
test('calls savedObjectsClient with id', async () => {
const expectedResult = Symbol();
const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
const actionsClient = new ActionsClient({
actionTypeRegistry,
savedObjectsClient,
});
savedObjectsClient.delete.mockResolvedValueOnce(expectedResult);
const result = await actionsClient.delete({ id: '1' });
expect(result).toEqual(expectedResult);
Expand All @@ -296,16 +283,11 @@ describe('delete()', () => {

describe('update()', () => {
test('updates an action with all given properties', async () => {
const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
actionTypeRegistry.register({
id: 'my-action-type',
name: 'My action type',
executor,
});
const actionsClient = new ActionsClient({
actionTypeRegistry,
savedObjectsClient,
});
savedObjectsClient.get.mockResolvedValueOnce({
id: '1',
type: 'action',
Expand Down Expand Up @@ -362,11 +344,6 @@ describe('update()', () => {
});

test('validates config', async () => {
const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
const actionsClient = new ActionsClient({
actionTypeRegistry,
savedObjectsClient,
});
actionTypeRegistry.register({
id: 'my-action-type',
name: 'My action type',
Expand Down Expand Up @@ -400,16 +377,11 @@ describe('update()', () => {
});

test('encrypts action type options unless specified not to', async () => {
const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
actionTypeRegistry.register({
id: 'my-action-type',
name: 'My action type',
executor,
});
const actionsClient = new ActionsClient({
actionTypeRegistry,
savedObjectsClient,
});
savedObjectsClient.get.mockResolvedValueOnce({
id: 'my-action',
type: 'action',
Expand Down
92 changes: 85 additions & 7 deletions x-pack/legacy/plugins/actions/server/actions_client.ts
Expand Up @@ -4,10 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { SavedObjectsClientContract, SavedObjectAttributes, SavedObject } from 'src/core/server';
import {
IScopedClusterClient,
SavedObjectsClientContract,
SavedObjectAttributes,
SavedObject,
} from 'src/core/server';

import { ActionTypeRegistry } from './action_type_registry';
import { validateConfig, validateSecrets } from './lib';
import { ActionResult } from './types';
import { ActionResult, FindActionResult, RawAction } from './types';

interface ActionUpdate extends SavedObjectAttributes {
description: string;
Expand Down Expand Up @@ -44,10 +50,12 @@ interface FindResult {
page: number;
perPage: number;
total: number;
data: ActionResult[];
data: FindActionResult[];
}

interface ConstructorOptions {
defaultKibanaIndex: string;
scopedClusterClient: IScopedClusterClient;
actionTypeRegistry: ActionTypeRegistry;
savedObjectsClient: SavedObjectsClientContract;
}
Expand All @@ -58,12 +66,21 @@ interface UpdateOptions {
}

export class ActionsClient {
private readonly defaultKibanaIndex: string;
private readonly scopedClusterClient: IScopedClusterClient;
private readonly savedObjectsClient: SavedObjectsClientContract;
private readonly actionTypeRegistry: ActionTypeRegistry;

constructor({ actionTypeRegistry, savedObjectsClient }: ConstructorOptions) {
constructor({
actionTypeRegistry,
defaultKibanaIndex,
scopedClusterClient,
savedObjectsClient,
}: ConstructorOptions) {
this.actionTypeRegistry = actionTypeRegistry;
this.savedObjectsClient = savedObjectsClient;
this.scopedClusterClient = scopedClusterClient;
this.defaultKibanaIndex = defaultKibanaIndex;
}

/**
Expand Down Expand Up @@ -134,16 +151,22 @@ export class ActionsClient {
* Find actions
*/
public async find({ options = {} }: FindOptions): Promise<FindResult> {
const findResult = await this.savedObjectsClient.find({
const findResult = await this.savedObjectsClient.find<RawAction>({
...options,
type: 'action',
});

const data = await injectExtraFindData(
this.defaultKibanaIndex,
this.scopedClusterClient,
findResult.saved_objects.map(actionFromSavedObject)
);

return {
page: findResult.page,
perPage: findResult.per_page,
total: findResult.total,
data: findResult.saved_objects.map(actionFromSavedObject),
data,
};
}

Expand All @@ -155,9 +178,64 @@ export class ActionsClient {
}
}

function actionFromSavedObject(savedObject: SavedObject) {
function actionFromSavedObject(savedObject: SavedObject<RawAction>): ActionResult {
return {
id: savedObject.id,
...savedObject.attributes,
};
}

async function injectExtraFindData(
defaultKibanaIndex: string,
scopedClusterClient: IScopedClusterClient,
actionResults: ActionResult[]
): Promise<FindActionResult[]> {
const aggs: Record<string, any> = {};
for (const actionResult of actionResults) {
aggs[actionResult.id] = {
filter: {
bool: {
must: {
nested: {
path: 'references',
query: {
bool: {
filter: {
bool: {
must: [
{
term: {
'references.id': actionResult.id,
},
},
{
term: {
'references.type': 'action',
},
},
],
},
},
},
},
},
},
},
},
};
}
const aggregationResult = await scopedClusterClient.callAsInternalUser('search', {
index: defaultKibanaIndex,
body: {
aggs,
size: 0,
query: {
match_all: {},
},
},
});
return actionResults.map(actionResult => ({
...actionResult,
referencedByCount: aggregationResult.aggregations[actionResult.id].doc_count,
}));
}
8 changes: 8 additions & 0 deletions x-pack/legacy/plugins/actions/server/plugin.ts
Expand Up @@ -22,6 +22,7 @@ import {
ActionsCoreStart,
ActionsPluginsSetup,
ActionsPluginsStart,
KibanaConfig,
} from './shim';
import {
createActionRoute,
Expand All @@ -44,17 +45,20 @@ export interface PluginStartContract {
}

export class Plugin {
private readonly kibana$: Observable<KibanaConfig>;
private readonly config$: Observable<ActionsConfigType>;
private readonly logger: Logger;
private serverBasePath?: string;
private adminClient?: IClusterClient;
private taskRunnerFactory?: TaskRunnerFactory;
private actionTypeRegistry?: ActionTypeRegistry;
private actionExecutor?: ActionExecutor;
private defaultKibanaIndex?: string;

constructor(initializerContext: ActionsPluginInitializerContext) {
this.logger = initializerContext.logger.get('plugins', 'alerting');
this.config$ = initializerContext.config.create();
this.kibana$ = initializerContext.config.kibana$;
}

public async setup(
Expand All @@ -63,6 +67,7 @@ export class Plugin {
): Promise<PluginSetupContract> {
const config = await this.config$.pipe(first()).toPromise();
this.adminClient = await core.elasticsearch.adminClient$.pipe(first()).toPromise();
this.defaultKibanaIndex = (await this.kibana$.pipe(first()).toPromise()).index;

plugins.xpack_main.registerFeature({
id: 'actions',
Expand Down Expand Up @@ -141,6 +146,7 @@ export class Plugin {
adminClient,
serverBasePath,
taskRunnerFactory,
defaultKibanaIndex,
} = this;

function getServices(request: any): Services {
Expand Down Expand Up @@ -186,6 +192,8 @@ export class Plugin {
return new ActionsClient({
savedObjectsClient,
actionTypeRegistry: actionTypeRegistry!,
defaultKibanaIndex: defaultKibanaIndex!,
scopedClusterClient: adminClient!.asScoped(request),
});
},
};
Expand Down

0 comments on commit 32307fa

Please sign in to comment.