[Reporting/NP Migration] Remove server.expose of ExportTypeRegistry (#50973)

* [Reporting/NPMigration] typescriptify ExportTypeRegistry, remove from server.expose

* Minor routes registration cleanup

* move the ETR test file

* Re-pack the route registration, reduce LOC changes

* add EnqueueJobFn type

* Fix usage collector test

* remove a throw error used for development/debugging

* fix imports error

* Fix execute job tests

* wip test fixes

* test fixes for real

* fix more tests

* fix diffs

* Add TODOs about the ExportTypesRegistry.register unwrap the factory functions.

* really make headlessbrowserdriver required as an execute job factory option

* fix tests

* Use constants for license type keywords
This commit is contained in:
Tim Sullivan 2019-12-11 12:11:07 -07:00 committed by GitHub
parent f53e1a9dbd
commit 711b44b7fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 636 additions and 401 deletions

View file

@ -50,3 +50,9 @@ export const PNG_JOB_TYPE = 'PNG';
export const CSV_JOB_TYPE = 'csv';
export const CSV_FROM_SAVEDOBJECT_JOB_TYPE = 'csv_from_savedobject';
export const USES_HEADLESS_JOB_TYPES = [PDF_JOB_TYPE, PNG_JOB_TYPE];
export const LICENSE_TYPE_TRIAL = 'trial';
export const LICENSE_TYPE_BASIC = 'basic';
export const LICENSE_TYPE_STANDARD = 'standard';
export const LICENSE_TYPE_GOLD = 'gold';
export const LICENSE_TYPE_PLATINUM = 'platinum';

View file

@ -1,64 +0,0 @@
/*
* 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 { isString } from 'lodash';
export class ExportTypesRegistry {
constructor() {
this._map = new Map();
}
register(item) {
if (!isString(item.id)) {
throw new Error(`'item' must have a String 'id' property `);
}
if (this._map.has(item.id)) {
throw new Error(`'item' with id ${item.id} has already been registered`);
}
this._map.set(item.id, item);
}
getAll() {
return this._map.values();
}
getSize() {
return this._map.size;
}
getById(id) {
if (!this._map.has(id)) {
throw new Error(`Unknown id ${id}`);
}
return this._map.get(id);
}
get(callback) {
let result;
for (const value of this._map.values()) {
if (!callback(value)) {
continue;
}
if (result) {
throw new Error('Found multiple items matching predicate.');
}
result = value;
}
if (!result) {
throw new Error('Found no items matching predicate');
}
return result;
}
}

View file

@ -6,8 +6,12 @@
import * as Rx from 'rxjs';
import { first, mergeMap } from 'rxjs/operators';
import { ServerFacade, CaptureConfig } from '../../../../types';
import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers/chromium/driver';
import {
ServerFacade,
CaptureConfig,
HeadlessChromiumDriverFactory,
HeadlessChromiumDriver as HeadlessBrowser,
} from '../../../../types';
import {
ElementsPositionAndAttribute,
ScreenshotResults,
@ -26,10 +30,12 @@ import { getElementPositionAndAttributes } from './get_element_position_data';
import { getScreenshots } from './get_screenshots';
import { skipTelemetry } from './skip_telemetry';
export function screenshotsObservableFactory(server: ServerFacade) {
export function screenshotsObservableFactory(
server: ServerFacade,
browserDriverFactory: HeadlessChromiumDriverFactory
) {
const config = server.config();
const captureConfig: CaptureConfig = config.get('xpack.reporting.capture');
const { browserDriverFactory } = server.plugins.reporting!;
return function screenshotsObservable({
logger,

View file

@ -0,0 +1,39 @@
/*
* 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 {
CSV_JOB_TYPE as jobType,
LICENSE_TYPE_TRIAL,
LICENSE_TYPE_BASIC,
LICENSE_TYPE_STANDARD,
LICENSE_TYPE_GOLD,
LICENSE_TYPE_PLATINUM,
} from '../../common/constants';
import { ExportTypeDefinition, ESQueueCreateJobFn, ESQueueWorkerExecuteFn } from '../../types';
import { metadata } from './metadata';
import { createJobFactory } from './server/create_job';
import { executeJobFactory } from './server/execute_job';
import { JobParamsDiscoverCsv, JobDocPayloadDiscoverCsv } from './types';
export const getExportType = (): ExportTypeDefinition<
JobParamsDiscoverCsv,
ESQueueCreateJobFn<JobParamsDiscoverCsv>,
JobDocPayloadDiscoverCsv,
ESQueueWorkerExecuteFn<JobDocPayloadDiscoverCsv>
> => ({
...metadata,
jobType,
jobContentExtension: 'csv',
createJobFactory,
executeJobFactory,
validLicenses: [
LICENSE_TYPE_TRIAL,
LICENSE_TYPE_BASIC,
LICENSE_TYPE_STANDARD,
LICENSE_TYPE_GOLD,
LICENSE_TYPE_PLATINUM,
],
});

View file

@ -1,22 +0,0 @@
/*
* 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 { ExportTypesRegistry } from '../../../types';
import { createJobFactory } from './create_job';
import { executeJobFactory } from './execute_job';
import { metadata } from '../metadata';
import { CSV_JOB_TYPE as jobType } from '../../../common/constants';
export function register(registry: ExportTypesRegistry) {
registry.register({
...metadata,
jobType,
jobContentExtension: 'csv',
createJobFactory,
executeJobFactory,
validLicenses: ['trial', 'basic', 'standard', 'gold', 'platinum'],
});
}

View file

@ -4,9 +4,43 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
CSV_FROM_SAVEDOBJECT_JOB_TYPE,
LICENSE_TYPE_TRIAL,
LICENSE_TYPE_BASIC,
LICENSE_TYPE_STANDARD,
LICENSE_TYPE_GOLD,
LICENSE_TYPE_PLATINUM,
} from '../../common/constants';
import { ExportTypeDefinition, ImmediateCreateJobFn, ImmediateExecuteFn } from '../../types';
import { createJobFactory } from './server/create_job';
import { executeJobFactory } from './server/execute_job';
import { metadata } from './metadata';
import { JobParamsPanelCsv } from './types';
/*
* These functions are exported to share with the API route handler that
* generates csv from saved object immediately on request.
*/
export { executeJobFactory } from './server/execute_job';
export { createJobFactory } from './server/create_job';
export const getExportType = (): ExportTypeDefinition<
JobParamsPanelCsv,
ImmediateCreateJobFn<JobParamsPanelCsv>,
JobParamsPanelCsv,
ImmediateExecuteFn<JobParamsPanelCsv>
> => ({
...metadata,
jobType: CSV_FROM_SAVEDOBJECT_JOB_TYPE,
jobContentExtension: 'csv',
createJobFactory,
executeJobFactory,
validLicenses: [
LICENSE_TYPE_TRIAL,
LICENSE_TYPE_BASIC,
LICENSE_TYPE_STANDARD,
LICENSE_TYPE_GOLD,
LICENSE_TYPE_PLATINUM,
],
});

View file

@ -1,22 +0,0 @@
/*
* 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 { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants';
import { ExportTypesRegistry } from '../../../types';
import { metadata } from '../metadata';
import { createJobFactory } from './create_job';
import { executeJobFactory } from './execute_job';
export function register(registry: ExportTypesRegistry) {
registry.register({
...metadata,
jobType: CSV_FROM_SAVEDOBJECT_JOB_TYPE,
jobContentExtension: 'csv',
createJobFactory,
executeJobFactory,
validLicenses: ['trial', 'basic', 'standard', 'gold', 'platinum'],
});
}

View file

@ -0,0 +1,38 @@
/*
* 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 {
PNG_JOB_TYPE as jobType,
LICENSE_TYPE_TRIAL,
LICENSE_TYPE_STANDARD,
LICENSE_TYPE_GOLD,
LICENSE_TYPE_PLATINUM,
} from '../../common/constants';
import { ExportTypeDefinition, ESQueueCreateJobFn, ESQueueWorkerExecuteFn } from '../../types';
import { createJobFactory } from './server/create_job';
import { executeJobFactory } from './server/execute_job';
import { metadata } from './metadata';
import { JobParamsPNG, JobDocPayloadPNG } from './types';
export const getExportType = (): ExportTypeDefinition<
JobParamsPNG,
ESQueueCreateJobFn<JobParamsPNG>,
JobDocPayloadPNG,
ESQueueWorkerExecuteFn<JobDocPayloadPNG>
> => ({
...metadata,
jobType,
jobContentEncoding: 'base64',
jobContentExtension: 'PNG',
createJobFactory,
executeJobFactory,
validLicenses: [
LICENSE_TYPE_TRIAL,
LICENSE_TYPE_STANDARD,
LICENSE_TYPE_GOLD,
LICENSE_TYPE_PLATINUM,
],
});

View file

@ -68,7 +68,7 @@ test(`passes browserTimezone to generatePng`, async () => {
const generatePngObservable = generatePngObservableFactory();
generatePngObservable.mockReturnValue(Rx.of(Buffer.from('')));
const executeJob = executeJobFactory(mockServer);
const executeJob = executeJobFactory(mockServer, { browserDriverFactory: {} });
const browserTimezone = 'UTC';
await executeJob('pngJobId', { relativeUrl: '/app/kibana#/something', browserTimezone, headers: encryptedHeaders }, cancellationToken);
@ -76,7 +76,7 @@ test(`passes browserTimezone to generatePng`, async () => {
});
test(`returns content_type of application/png`, async () => {
const executeJob = executeJobFactory(mockServer);
const executeJob = executeJobFactory(mockServer, { browserDriverFactory: {} });
const encryptedHeaders = await encryptHeaders({});
const generatePngObservable = generatePngObservableFactory();
@ -93,7 +93,7 @@ test(`returns content of generatePng getBuffer base64 encoded`, async () => {
const generatePngObservable = generatePngObservableFactory();
generatePngObservable.mockReturnValue(Rx.of(Buffer.from(testContent)));
const executeJob = executeJobFactory(mockServer);
const executeJob = executeJobFactory(mockServer, { browserDriverFactory: {} });
const encryptedHeaders = await encryptHeaders({});
const { content } = await executeJob('pngJobId', { relativeUrl: '/app/kibana#/something',
timeRange: {}, headers: encryptedHeaders }, cancellationToken);

View file

@ -7,7 +7,12 @@
import * as Rx from 'rxjs';
import { mergeMap, catchError, map, takeUntil } from 'rxjs/operators';
import { PLUGIN_ID, PNG_JOB_TYPE } from '../../../../common/constants';
import { ServerFacade, ExecuteJobFactory, ESQueueWorkerExecuteFn } from '../../../../types';
import {
ServerFacade,
ExecuteJobFactory,
ESQueueWorkerExecuteFn,
HeadlessChromiumDriverFactory,
} from '../../../../types';
import { LevelLogger } from '../../../../server/lib';
import {
decryptJobHeaders,
@ -18,10 +23,13 @@ import {
import { JobDocPayloadPNG } from '../../types';
import { generatePngObservableFactory } from '../lib/generate_png';
export const executeJobFactory: ExecuteJobFactory<ESQueueWorkerExecuteFn<
JobDocPayloadPNG
>> = function executeJobFactoryFn(server: ServerFacade) {
const generatePngObservable = generatePngObservableFactory(server);
type QueuedPngExecutorFactory = ExecuteJobFactory<ESQueueWorkerExecuteFn<JobDocPayloadPNG>>;
export const executeJobFactory: QueuedPngExecutorFactory = function executeJobFactoryFn(
server: ServerFacade,
{ browserDriverFactory }: { browserDriverFactory: HeadlessChromiumDriverFactory }
) {
const generatePngObservable = generatePngObservableFactory(server, browserDriverFactory);
const logger = LevelLogger.createForServer(server, [PLUGIN_ID, PNG_JOB_TYPE, 'execute']);
return function executeJob(

View file

@ -1,23 +0,0 @@
/*
* 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 { ExportTypesRegistry } from '../../../types';
import { createJobFactory } from './create_job';
import { executeJobFactory } from './execute_job';
import { metadata } from '../metadata';
import { PNG_JOB_TYPE as jobType } from '../../../common/constants';
export function register(registry: ExportTypesRegistry) {
registry.register({
...metadata,
jobType,
jobContentEncoding: 'base64',
jobContentExtension: 'PNG',
createJobFactory,
executeJobFactory,
validLicenses: ['trial', 'standard', 'gold', 'platinum'],
});
}

View file

@ -7,13 +7,16 @@
import * as Rx from 'rxjs';
import { map } from 'rxjs/operators';
import { LevelLogger } from '../../../../server/lib';
import { ServerFacade, ConditionalHeaders } from '../../../../types';
import { ServerFacade, HeadlessChromiumDriverFactory, ConditionalHeaders } from '../../../../types';
import { screenshotsObservableFactory } from '../../../common/lib/screenshots';
import { PreserveLayout } from '../../../common/layouts/preserve_layout';
import { LayoutParams } from '../../../common/layouts/layout';
export function generatePngObservableFactory(server: ServerFacade) {
const screenshotsObservable = screenshotsObservableFactory(server);
export function generatePngObservableFactory(
server: ServerFacade,
browserDriverFactory: HeadlessChromiumDriverFactory
) {
const screenshotsObservable = screenshotsObservableFactory(server, browserDriverFactory);
return function generatePngObservable(
logger: LevelLogger,

View file

@ -0,0 +1,38 @@
/*
* 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 {
PDF_JOB_TYPE as jobType,
LICENSE_TYPE_TRIAL,
LICENSE_TYPE_STANDARD,
LICENSE_TYPE_GOLD,
LICENSE_TYPE_PLATINUM,
} from '../../common/constants';
import { ExportTypeDefinition, ESQueueCreateJobFn, ESQueueWorkerExecuteFn } from '../../types';
import { createJobFactory } from './server/create_job';
import { executeJobFactory } from './server/execute_job';
import { metadata } from './metadata';
import { JobParamsPDF, JobDocPayloadPDF } from './types';
export const getExportType = (): ExportTypeDefinition<
JobParamsPDF,
ESQueueCreateJobFn<JobParamsPDF>,
JobDocPayloadPDF,
ESQueueWorkerExecuteFn<JobDocPayloadPDF>
> => ({
...metadata,
jobType,
jobContentEncoding: 'base64',
jobContentExtension: 'pdf',
createJobFactory,
executeJobFactory,
validLicenses: [
LICENSE_TYPE_TRIAL,
LICENSE_TYPE_STANDARD,
LICENSE_TYPE_GOLD,
LICENSE_TYPE_PLATINUM,
],
});

View file

@ -67,7 +67,7 @@ test(`passes browserTimezone to generatePdf`, async () => {
const generatePdfObservable = generatePdfObservableFactory();
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
const executeJob = executeJobFactory(mockServer);
const executeJob = executeJobFactory(mockServer, { browserDriverFactory: {} });
const browserTimezone = 'UTC';
await executeJob('pdfJobId', { objects: [], browserTimezone, headers: encryptedHeaders }, cancellationToken);
@ -84,7 +84,7 @@ test(`passes browserTimezone to generatePdf`, async () => {
});
test(`returns content_type of application/pdf`, async () => {
const executeJob = executeJobFactory(mockServer);
const executeJob = executeJobFactory(mockServer, { browserDriverFactory: {} });
const encryptedHeaders = await encryptHeaders({});
const generatePdfObservable = generatePdfObservableFactory();
@ -104,7 +104,7 @@ test(`returns content of generatePdf getBuffer base64 encoded`, async () => {
const generatePdfObservable = generatePdfObservableFactory();
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(testContent)));
const executeJob = executeJobFactory(mockServer);
const executeJob = executeJobFactory(mockServer, { browserDriverFactory: {} });
const encryptedHeaders = await encryptHeaders({});
const { content } = await executeJob('pdfJobId', { objects: [], timeRange: {}, headers: encryptedHeaders }, cancellationToken);

View file

@ -6,7 +6,12 @@
import * as Rx from 'rxjs';
import { mergeMap, catchError, map, takeUntil } from 'rxjs/operators';
import { ExecuteJobFactory, ESQueueWorkerExecuteFn, ServerFacade } from '../../../../types';
import {
ServerFacade,
ExecuteJobFactory,
ESQueueWorkerExecuteFn,
HeadlessChromiumDriverFactory,
} from '../../../../types';
import { JobDocPayloadPDF } from '../../types';
import { PLUGIN_ID, PDF_JOB_TYPE } from '../../../../common/constants';
import { LevelLogger } from '../../../../server/lib';
@ -19,10 +24,13 @@ import {
getCustomLogo,
} from '../../../common/execute_job/';
export const executeJobFactory: ExecuteJobFactory<ESQueueWorkerExecuteFn<
JobDocPayloadPDF
>> = function executeJobFactoryFn(server: ServerFacade) {
const generatePdfObservable = generatePdfObservableFactory(server);
type QueuedPdfExecutorFactory = ExecuteJobFactory<ESQueueWorkerExecuteFn<JobDocPayloadPDF>>;
export const executeJobFactory: QueuedPdfExecutorFactory = function executeJobFactoryFn(
server: ServerFacade,
{ browserDriverFactory }: { browserDriverFactory: HeadlessChromiumDriverFactory }
) {
const generatePdfObservable = generatePdfObservableFactory(server, browserDriverFactory);
const logger = LevelLogger.createForServer(server, [PLUGIN_ID, PDF_JOB_TYPE, 'execute']);
return function executeJob(

View file

@ -1,23 +0,0 @@
/*
* 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 { ExportTypesRegistry } from '../../../types';
import { createJobFactory } from './create_job';
import { executeJobFactory } from './execute_job';
import { metadata } from '../metadata';
import { PDF_JOB_TYPE as jobType } from '../../../common/constants';
export function register(registry: ExportTypesRegistry) {
registry.register({
...metadata,
jobType,
jobContentEncoding: 'base64',
jobContentExtension: 'pdf',
createJobFactory,
executeJobFactory,
validLicenses: ['trial', 'standard', 'gold', 'platinum'],
});
}

View file

@ -8,7 +8,7 @@ import * as Rx from 'rxjs';
import { toArray, mergeMap } from 'rxjs/operators';
import { groupBy } from 'lodash';
import { LevelLogger } from '../../../../server/lib';
import { ServerFacade, ConditionalHeaders } from '../../../../types';
import { ServerFacade, HeadlessChromiumDriverFactory, ConditionalHeaders } from '../../../../types';
// @ts-ignore untyped module
import { pdf } from './pdf';
import { screenshotsObservableFactory } from '../../../common/lib/screenshots';
@ -26,8 +26,11 @@ const getTimeRange = (urlScreenshots: ScreenshotResults[]) => {
return null;
};
export function generatePdfObservableFactory(server: ServerFacade) {
const screenshotsObservable = screenshotsObservableFactory(server);
export function generatePdfObservableFactory(
server: ServerFacade,
browserDriverFactory: HeadlessChromiumDriverFactory
) {
const screenshotsObservable = screenshotsObservableFactory(server, browserDriverFactory);
const captureConcurrency = 1;
return function generatePdfObservable(

View file

@ -13,8 +13,7 @@ import { registerRoutes } from './server/routes';
import {
LevelLogger,
checkLicenseFactory,
createQueueFactory,
exportTypesRegistryFactory,
getExportTypesRegistry,
runValidations,
} from './server/lib';
import { config as reportingConfig } from './config';
@ -74,20 +73,23 @@ export const reporting = (kibana: any) => {
// TODO: Decouple Hapi: Build a server facade object based on the server to
// pass through to the libs. Do not pass server directly
async init(server: ServerFacade) {
const exportTypesRegistry = getExportTypesRegistry();
let isCollectorReady = false;
// Register a function with server to manage the collection of usage stats
const { usageCollection } = server.newPlatform.setup.plugins;
registerReportingUsageCollector(usageCollection, server, () => isCollectorReady);
registerReportingUsageCollector(
usageCollection,
server,
() => isCollectorReady,
exportTypesRegistry
);
const logger = LevelLogger.createForServer(server, [PLUGIN_ID]);
const [exportTypesRegistry, browserFactory] = await Promise.all([
exportTypesRegistryFactory(server),
createBrowserDriverFactory(server),
]);
server.expose('exportTypesRegistry', exportTypesRegistry);
const browserDriverFactory = await createBrowserDriverFactory(server);
logConfiguration(server, logger);
runValidations(server, logger, browserFactory);
runValidations(server, logger, browserDriverFactory);
const { xpack_main: xpackMainPlugin } = server.plugins;
mirrorPluginStatus(xpackMainPlugin, this);
@ -101,11 +103,8 @@ export const reporting = (kibana: any) => {
// Post initialization of the above code, the collector is now ready to fetch its data
isCollectorReady = true;
server.expose('browserDriverFactory', browserFactory);
server.expose('queue', createQueueFactory(server));
// Reporting routes
registerRoutes(server, logger);
registerRoutes(server, exportTypesRegistry, browserDriverFactory, logger);
},
deprecations({ unused }: any) {

View file

@ -5,7 +5,12 @@
*/
import { PLUGIN_ID } from '../../common/constants';
import { ServerFacade, QueueConfig } from '../../types';
import {
ServerFacade,
ExportTypesRegistry,
HeadlessChromiumDriverFactory,
QueueConfig,
} from '../../types';
// @ts-ignore
import { Esqueue } from './esqueue';
import { createWorkerFactory } from './create_worker';
@ -13,7 +18,15 @@ import { LevelLogger } from './level_logger';
// @ts-ignore
import { createTaggedLogger } from './create_tagged_logger'; // TODO remove createTaggedLogger once esqueue is removed
export function createQueueFactory(server: ServerFacade): Esqueue {
interface CreateQueueFactoryOpts {
exportTypesRegistry: ExportTypesRegistry;
browserDriverFactory: HeadlessChromiumDriverFactory;
}
export function createQueueFactory(
server: ServerFacade,
{ exportTypesRegistry, browserDriverFactory }: CreateQueueFactoryOpts
): Esqueue {
const queueConfig: QueueConfig = server.config().get('xpack.reporting.queue');
const index = server.config().get('xpack.reporting.index');
@ -29,7 +42,7 @@ export function createQueueFactory(server: ServerFacade): Esqueue {
if (queueConfig.pollEnabled) {
// create workers to poll the index for idle jobs waiting to be claimed and executed
const createWorker = createWorkerFactory(server);
const createWorker = createWorkerFactory(server, { exportTypesRegistry, browserDriverFactory });
createWorker(queue);
} else {
const logger = LevelLogger.createForServer(server, [PLUGIN_ID, 'create_queue']);

View file

@ -5,7 +5,8 @@
*/
import * as sinon from 'sinon';
import { ServerFacade } from '../../types';
import { ServerFacade, HeadlessChromiumDriverFactory } from '../../types';
import { ExportTypesRegistry } from './export_types_registry';
import { createWorkerFactory } from './create_worker';
// @ts-ignore
import { Esqueue } from './esqueue';
@ -22,16 +23,17 @@ configGetStub.withArgs('server.uuid').returns('g9ymiujthvy6v8yrh7567g6fwzgzftzfr
const executeJobFactoryStub = sinon.stub();
const getMockServer = (
exportTypes: any[] = [{ executeJobFactory: executeJobFactoryStub }]
): ServerFacade => {
const getMockServer = (): ServerFacade => {
return ({
log: sinon.stub(),
expose: sinon.stub(),
config: () => ({ get: configGetStub }),
plugins: { reporting: { exportTypesRegistry: { getAll: () => exportTypes } } },
} as unknown) as ServerFacade;
};
const getMockExportTypesRegistry = (
exportTypes: any[] = [{ executeJobFactory: executeJobFactoryStub }]
) => ({
getAll: () => exportTypes,
});
describe('Create Worker', () => {
let queue: Esqueue;
@ -44,7 +46,11 @@ describe('Create Worker', () => {
});
test('Creates a single Esqueue worker for Reporting', async () => {
const createWorker = createWorkerFactory(getMockServer());
const exportTypesRegistry = getMockExportTypesRegistry();
const createWorker = createWorkerFactory(getMockServer(), {
exportTypesRegistry: exportTypesRegistry as ExportTypesRegistry,
browserDriverFactory: {} as HeadlessChromiumDriverFactory,
});
const registerWorkerSpy = sinon.spy(queue, 'registerWorker');
createWorker(queue);
@ -68,15 +74,17 @@ Object {
});
test('Creates a single Esqueue worker for Reporting, even if there are multiple export types', async () => {
const createWorker = createWorkerFactory(
getMockServer([
{ executeJobFactory: executeJobFactoryStub },
{ executeJobFactory: executeJobFactoryStub },
{ executeJobFactory: executeJobFactoryStub },
{ executeJobFactory: executeJobFactoryStub },
{ executeJobFactory: executeJobFactoryStub },
])
);
const exportTypesRegistry = getMockExportTypesRegistry([
{ executeJobFactory: executeJobFactoryStub },
{ executeJobFactory: executeJobFactoryStub },
{ executeJobFactory: executeJobFactoryStub },
{ executeJobFactory: executeJobFactoryStub },
{ executeJobFactory: executeJobFactoryStub },
]);
const createWorker = createWorkerFactory(getMockServer(), {
exportTypesRegistry: exportTypesRegistry as ExportTypesRegistry,
browserDriverFactory: {} as HeadlessChromiumDriverFactory,
});
const registerWorkerSpy = sinon.spy(queue, 'registerWorker');
createWorker(queue);

View file

@ -5,6 +5,7 @@
*/
import { PLUGIN_ID } from '../../common/constants';
import { ExportTypesRegistry, HeadlessChromiumDriverFactory } from '../../types';
import { CancellationToken } from '../../common/cancellation_token';
import {
ESQueueInstance,
@ -21,14 +22,21 @@ import {
import { events as esqueueEvents } from './esqueue';
import { LevelLogger } from './level_logger';
export function createWorkerFactory<JobParamsType>(server: ServerFacade) {
interface CreateWorkerFactoryOpts {
exportTypesRegistry: ExportTypesRegistry;
browserDriverFactory: HeadlessChromiumDriverFactory;
}
export function createWorkerFactory<JobParamsType>(
server: ServerFacade,
{ exportTypesRegistry, browserDriverFactory }: CreateWorkerFactoryOpts
) {
type JobDocPayloadType = JobDocPayload<JobParamsType>;
const config = server.config();
const logger = LevelLogger.createForServer(server, [PLUGIN_ID, 'queue-worker']);
const queueConfig: QueueConfig = config.get('xpack.reporting.queue');
const kibanaName: string = config.get('server.name');
const kibanaId: string = config.get('server.uuid');
const { exportTypesRegistry } = server.plugins.reporting!;
// Once more document types are added, this will need to be passed in
return function createWorker(queue: ESQueueInstance<JobParamsType, JobDocPayloadType>) {
@ -41,8 +49,9 @@ export function createWorkerFactory<JobParamsType>(server: ServerFacade) {
for (const exportType of exportTypesRegistry.getAll() as Array<
ExportTypeDefinition<JobParamsType, any, any, any>
>) {
const executeJobFactory = exportType.executeJobFactory(server);
jobExecutors.set(exportType.jobType, executeJobFactory);
// TODO: the executeJobFn should be unwrapped in the register method of the export types registry
const jobExecutor = exportType.executeJobFactory(server, { browserDriverFactory });
jobExecutors.set(exportType.jobType, jobExecutor);
}
const workerFn = (jobSource: JobSource<JobParamsType>, ...workerRestArgs: any[]) => {

View file

@ -8,12 +8,14 @@ import { get } from 'lodash';
// @ts-ignore
import { events as esqueueEvents } from './esqueue';
import {
EnqueueJobFn,
ESQueueCreateJobFn,
ImmediateCreateJobFn,
Job,
ServerFacade,
RequestFacade,
Logger,
ExportTypesRegistry,
CaptureConfig,
QueueConfig,
ConditionalHeaders,
@ -26,13 +28,20 @@ interface ConfirmedJob {
_primary_term: number;
}
export function enqueueJobFactory(server: ServerFacade) {
interface EnqueueJobFactoryOpts {
exportTypesRegistry: ExportTypesRegistry;
esqueue: any;
}
export function enqueueJobFactory(
server: ServerFacade,
{ exportTypesRegistry, esqueue }: EnqueueJobFactoryOpts
): EnqueueJobFn {
const config = server.config();
const captureConfig: CaptureConfig = config.get('xpack.reporting.capture');
const browserType = captureConfig.browser.type;
const maxAttempts = captureConfig.maxAttempts;
const queueConfig: QueueConfig = config.get('xpack.reporting.queue');
const { exportTypesRegistry, queue: jobQueue } = server.plugins.reporting!;
return async function enqueueJob<JobParamsType>(
parentLogger: Logger,
@ -46,6 +55,12 @@ export function enqueueJobFactory(server: ServerFacade) {
const logger = parentLogger.clone(['queue-job']);
const exportType = exportTypesRegistry.getById(exportTypeId);
if (exportType == null) {
throw new Error(`Export type ${exportTypeId} does not exist in the registry!`);
}
// TODO: the createJobFn should be unwrapped in the register method of the export types registry
const createJob = exportType.createJobFactory(server) as CreateJobFn;
const payload = await createJob(jobParams, headers, request);
@ -57,7 +72,7 @@ export function enqueueJobFactory(server: ServerFacade) {
};
return new Promise((resolve, reject) => {
const job = jobQueue.addJob(exportType.jobType, payload, options);
const job = esqueue.addJob(exportType.jobType, payload, options);
job.on(esqueueEvents.EVENT_JOB_CREATED, (createdJob: ConfirmedJob) => {
if (createdJob.id === job.id) {

View file

@ -4,40 +4,110 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { resolve as pathResolve } from 'path';
import glob from 'glob';
import { ServerFacade } from '../../types';
import { PLUGIN_ID } from '../../common/constants';
import { oncePerServer } from './once_per_server';
import { LevelLogger } from './level_logger';
// @ts-ignore untype module TODO
import { ExportTypesRegistry } from '../../common/export_types_registry';
import memoizeOne from 'memoize-one';
import { isString } from 'lodash';
import { getExportType as getTypeCsv } from '../../export_types/csv';
import { getExportType as getTypeCsvFromSavedObject } from '../../export_types/csv_from_savedobject';
import { getExportType as getTypePng } from '../../export_types/png';
import { getExportType as getTypePrintablePdf } from '../../export_types/printable_pdf';
import { ExportTypeDefinition } from '../../types';
function scan(pattern: string) {
return new Promise((resolve, reject) => {
glob(pattern, {}, (err, files) => {
if (err) {
return reject(err);
type GetCallbackFn<JobParamsType, CreateJobFnType, JobPayloadType, ExecuteJobFnType> = (
item: ExportTypeDefinition<JobParamsType, CreateJobFnType, JobPayloadType, ExecuteJobFnType>
) => boolean;
// => ExportTypeDefinition<T, U, V, W>
export class ExportTypesRegistry {
private _map: Map<string, ExportTypeDefinition<any, any, any, any>> = new Map();
constructor() {}
register<JobParamsType, CreateJobFnType, JobPayloadType, ExecuteJobFnType>(
item: ExportTypeDefinition<JobParamsType, CreateJobFnType, JobPayloadType, ExecuteJobFnType>
): void {
if (!isString(item.id)) {
throw new Error(`'item' must have a String 'id' property `);
}
if (this._map.has(item.id)) {
throw new Error(`'item' with id ${item.id} has already been registered`);
}
// TODO: Unwrap the execute function from the item's executeJobFactory
// Move that work out of server/lib/create_worker to reduce dependence on ESQueue
this._map.set(item.id, item);
}
getAll() {
return Array.from(this._map.values());
}
getSize() {
return this._map.size;
}
getById<JobParamsType, CreateJobFnType, JobPayloadType, ExecuteJobFnType>(
id: string
): ExportTypeDefinition<JobParamsType, CreateJobFnType, JobPayloadType, ExecuteJobFnType> {
if (!this._map.has(id)) {
throw new Error(`Unknown id ${id}`);
}
return this._map.get(id) as ExportTypeDefinition<
JobParamsType,
CreateJobFnType,
JobPayloadType,
ExecuteJobFnType
>;
}
get<JobParamsType, CreateJobFnType, JobPayloadType, ExecuteJobFnType>(
findType: GetCallbackFn<JobParamsType, CreateJobFnType, JobPayloadType, ExecuteJobFnType>
): ExportTypeDefinition<JobParamsType, CreateJobFnType, JobPayloadType, ExecuteJobFnType> {
let result;
for (const value of this._map.values()) {
if (!findType(value)) {
continue; // try next value
}
const foundResult: ExportTypeDefinition<
JobParamsType,
CreateJobFnType,
JobPayloadType,
ExecuteJobFnType
> = value;
if (result) {
throw new Error('Found multiple items matching predicate.');
}
resolve(files);
});
});
result = foundResult;
}
if (!result) {
throw new Error('Found no items matching predicate');
}
return result;
}
}
const pattern = pathResolve(__dirname, '../../export_types/*/server/index.[jt]s');
async function exportTypesRegistryFn(server: ServerFacade) {
const logger = LevelLogger.createForServer(server, [PLUGIN_ID, 'exportTypes']);
const exportTypesRegistry = new ExportTypesRegistry();
const files: string[] = (await scan(pattern)) as string[];
function getExportTypesRegistryFn(): ExportTypesRegistry {
const registry = new ExportTypesRegistry();
files.forEach(file => {
logger.debug(`Found exportType at ${file}`);
const { register } = require(file); // eslint-disable-line @typescript-eslint/no-var-requires
register(exportTypesRegistry);
/* this replaces the previously async method of registering export types,
* where this would run a directory scan and types would be registered via
* discovery */
const getTypeFns: Array<() => ExportTypeDefinition<any, any, any, any>> = [
getTypeCsv,
getTypeCsvFromSavedObject,
getTypePng,
getTypePrintablePdf,
];
getTypeFns.forEach(getType => {
registry.register(getType());
});
return exportTypesRegistry;
return registry;
}
export const exportTypesRegistryFactory = oncePerServer(exportTypesRegistryFn);
// FIXME: is this the best way to return a singleton?
export const getExportTypesRegistry = memoizeOne(getExportTypesRegistryFn);

View file

@ -4,11 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { exportTypesRegistryFactory } from './export_types_registry';
export { getExportTypesRegistry } from './export_types_registry';
// @ts-ignore untyped module
export { checkLicenseFactory } from './check_license';
export { LevelLogger } from './level_logger';
export { createQueueFactory } from './create_queue';
export { cryptoFactory } from './crypto';
export { oncePerServer } from './once_per_server';
export { runValidations } from './validate';
export { createQueueFactory } from './create_queue';
export { enqueueJobFactory } from './enqueue_job';

View file

@ -10,6 +10,7 @@ import {
ServerFacade,
RequestFacade,
ResponseFacade,
HeadlessChromiumDriverFactory,
ReportingResponseToolkit,
Logger,
JobDocOutputExecuted,
@ -45,8 +46,17 @@ export function registerGenerateCsvFromSavedObjectImmediate(
handler: async (request: RequestFacade, h: ReportingResponseToolkit) => {
const logger = parentLogger.clone(['savedobject-csv']);
const jobParams = getJobParamsFromRequest(request, { isImmediate: true });
/* TODO these functions should be made available in the export types registry:
*
* const { createJobFn, executeJobFn } = exportTypesRegistry.getById(CSV_FROM_SAVEDOBJECT_JOB_TYPE)
*
* Calling an execute job factory requires passing a browserDriverFactory option, so we should not call the factory from here
*/
const createJobFn = createJobFactory(server);
const executeJobFn = executeJobFactory(server);
const executeJobFn = executeJobFactory(server, {
browserDriverFactory: {} as HeadlessChromiumDriverFactory,
});
const jobDocPayload: JobDocPayloadPanelCsv = await createJobFn(
jobParams,
request.headers,

View file

@ -0,0 +1,83 @@
/*
* 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 boom from 'boom';
import { API_BASE_URL } from '../../common/constants';
import {
ServerFacade,
ExportTypesRegistry,
HeadlessChromiumDriverFactory,
RequestFacade,
ReportingResponseToolkit,
Logger,
} from '../../types';
import { registerGenerateFromJobParams } from './generate_from_jobparams';
import { registerGenerateCsvFromSavedObject } from './generate_from_savedobject';
import { registerGenerateCsvFromSavedObjectImmediate } from './generate_from_savedobject_immediate';
import { registerLegacy } from './legacy';
import { createQueueFactory, enqueueJobFactory } from '../lib';
export function registerJobGenerationRoutes(
server: ServerFacade,
exportTypesRegistry: ExportTypesRegistry,
browserDriverFactory: HeadlessChromiumDriverFactory,
logger: Logger
) {
const config = server.config();
const DOWNLOAD_BASE_URL = config.get('server.basePath') + `${API_BASE_URL}/jobs/download`;
// @ts-ignore TODO
const { errors: esErrors } = server.plugins.elasticsearch.getCluster('admin');
const esqueue = createQueueFactory(server, { exportTypesRegistry, browserDriverFactory });
const enqueueJob = enqueueJobFactory(server, { exportTypesRegistry, esqueue });
/*
* Generates enqueued job details to use in responses
*/
async function handler(
exportTypeId: string,
jobParams: object,
request: RequestFacade,
h: ReportingResponseToolkit
) {
const user = request.pre.user;
const headers = request.headers;
const job = await enqueueJob(logger, exportTypeId, jobParams, user, headers, request);
// return the queue's job information
const jobJson = job.toJSON();
return h
.response({
path: `${DOWNLOAD_BASE_URL}/${jobJson.id}`,
job: jobJson,
})
.type('application/json');
}
function handleError(exportTypeId: string, err: Error) {
if (err instanceof esErrors['401']) {
return boom.unauthorized(`Sorry, you aren't authenticated`);
}
if (err instanceof esErrors['403']) {
return boom.forbidden(`Sorry, you are not authorized to create ${exportTypeId} reports`);
}
if (err instanceof esErrors['404']) {
return boom.boomify(err, { statusCode: 404 });
}
return err;
}
registerGenerateFromJobParams(server, handler, handleError);
registerLegacy(server, handler, handleError);
// Register beta panel-action download-related API's
if (config.get('xpack.reporting.csv.enablePanelActionDownload')) {
registerGenerateCsvFromSavedObject(server, handler, handleError);
registerGenerateCsvFromSavedObjectImmediate(server, logger);
}
}

View file

@ -4,69 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
import boom from 'boom';
import { API_BASE_URL } from '../../common/constants';
import { ServerFacade, RequestFacade, ReportingResponseToolkit, Logger } from '../../types';
import { enqueueJobFactory } from '../lib/enqueue_job';
import { registerGenerateFromJobParams } from './generate_from_jobparams';
import { registerGenerateCsvFromSavedObject } from './generate_from_savedobject';
import { registerGenerateCsvFromSavedObjectImmediate } from './generate_from_savedobject_immediate';
import { registerJobs } from './jobs';
import { registerLegacy } from './legacy';
import {
ServerFacade,
ExportTypesRegistry,
HeadlessChromiumDriverFactory,
Logger,
} from '../../types';
import { registerJobGenerationRoutes } from './generation';
import { registerJobInfoRoutes } from './jobs';
export function registerRoutes(server: ServerFacade, logger: Logger) {
const config = server.config();
const DOWNLOAD_BASE_URL = config.get('server.basePath') + `${API_BASE_URL}/jobs/download`;
// @ts-ignore TODO
const { errors: esErrors } = server.plugins.elasticsearch.getCluster('admin');
const enqueueJob = enqueueJobFactory(server);
/*
* Generates enqueued job details to use in responses
*/
async function handler(
exportTypeId: string,
jobParams: object,
request: RequestFacade,
h: ReportingResponseToolkit
) {
const user = request.pre.user;
const headers = request.headers;
const job = await enqueueJob(logger, exportTypeId, jobParams, user, headers, request);
// return the queue's job information
const jobJson = job.toJSON();
return h
.response({
path: `${DOWNLOAD_BASE_URL}/${jobJson.id}`,
job: jobJson,
})
.type('application/json');
}
function handleError(exportTypeId: string, err: Error) {
if (err instanceof esErrors['401']) {
return boom.unauthorized(`Sorry, you aren't authenticated`);
}
if (err instanceof esErrors['403']) {
return boom.forbidden(`Sorry, you are not authorized to create ${exportTypeId} reports`);
}
if (err instanceof esErrors['404']) {
return boom.boomify(err, { statusCode: 404 });
}
return err;
}
registerGenerateFromJobParams(server, handler, handleError);
registerLegacy(server, handler, handleError);
// Register beta panel-action download-related API's
if (config.get('xpack.reporting.csv.enablePanelActionDownload')) {
registerGenerateCsvFromSavedObject(server, handler, handleError);
registerGenerateCsvFromSavedObjectImmediate(server, logger);
}
registerJobs(server);
export function registerRoutes(
server: ServerFacade,
exportTypesRegistry: ExportTypesRegistry,
browserDriverFactory: HeadlessChromiumDriverFactory,
logger: Logger
) {
registerJobGenerationRoutes(server, exportTypesRegistry, browserDriverFactory, logger);
registerJobInfoRoutes(server, exportTypesRegistry, logger);
}

View file

@ -6,8 +6,8 @@
import Hapi from 'hapi';
import { difference, memoize } from 'lodash';
import { registerJobs } from './jobs';
import { ExportTypesRegistry } from '../../common/export_types_registry';
import { registerJobInfoRoutes } from './jobs';
import { ExportTypesRegistry } from '../lib/export_types_registry';
jest.mock('./lib/authorized_user_pre_routing', () => {
return {
authorizedUserPreRoutingFactory: () => () => ({})
@ -19,13 +19,17 @@ jest.mock('./lib/reporting_feature_pre_routing', () => {
};
});
let mockServer;
let exportTypesRegistry;
const mockLogger = {
error: jest.fn(),
debug: jest.fn(),
};
beforeEach(() => {
mockServer = new Hapi.Server({ debug: false, port: 8080, routes: { log: { collect: true } } });
mockServer.config = memoize(() => ({ get: jest.fn() }));
const exportTypesRegistry = new ExportTypesRegistry();
exportTypesRegistry = new ExportTypesRegistry();
exportTypesRegistry.register({
id: 'unencoded',
jobType: 'unencodedJobType',
@ -44,9 +48,6 @@ beforeEach(() => {
callWithRequest: jest.fn(),
callWithInternalUser: jest.fn(),
}))
},
reporting: {
exportTypesRegistry
}
};
});
@ -63,7 +64,7 @@ test(`returns 404 if job not found`, async () => {
mockServer.plugins.elasticsearch.getCluster('admin')
.callWithInternalUser.mockReturnValue(Promise.resolve(getHits()));
registerJobs(mockServer);
registerJobInfoRoutes(mockServer, exportTypesRegistry, mockLogger);
const request = {
method: 'GET',
@ -79,7 +80,7 @@ test(`returns 401 if not valid job type`, async () => {
mockServer.plugins.elasticsearch.getCluster('admin')
.callWithInternalUser.mockReturnValue(Promise.resolve(getHits({ jobtype: 'invalidJobType' })));
registerJobs(mockServer);
registerJobInfoRoutes(mockServer, exportTypesRegistry, mockLogger);
const request = {
method: 'GET',
@ -91,12 +92,11 @@ test(`returns 401 if not valid job type`, async () => {
});
describe(`when job is incomplete`, () => {
const getIncompleteResponse = async () => {
mockServer.plugins.elasticsearch.getCluster('admin')
.callWithInternalUser.mockReturnValue(Promise.resolve(getHits({ jobtype: 'unencodedJobType', status: 'pending' })));
registerJobs(mockServer);
registerJobInfoRoutes(mockServer, exportTypesRegistry, mockLogger);
const request = {
method: 'GET',
@ -133,7 +133,7 @@ describe(`when job is failed`, () => {
mockServer.plugins.elasticsearch.getCluster('admin')
.callWithInternalUser.mockReturnValue(Promise.resolve(hits));
registerJobs(mockServer);
registerJobInfoRoutes(mockServer, exportTypesRegistry, mockLogger);
const request = {
method: 'GET',
@ -178,7 +178,7 @@ describe(`when job is completed`, () => {
});
mockServer.plugins.elasticsearch.getCluster('admin').callWithInternalUser.mockReturnValue(Promise.resolve(hits));
registerJobs(mockServer);
registerJobInfoRoutes(mockServer, exportTypesRegistry, mockLogger);
const request = {
method: 'GET',

View file

@ -8,6 +8,8 @@ import boom from 'boom';
import { API_BASE_URL } from '../../common/constants';
import {
ServerFacade,
ExportTypesRegistry,
Logger,
RequestFacade,
ReportingResponseToolkit,
JobDocOutput,
@ -24,7 +26,11 @@ import {
const MAIN_ENTRY = `${API_BASE_URL}/jobs`;
export function registerJobs(server: ServerFacade) {
export function registerJobInfoRoutes(
server: ServerFacade,
exportTypesRegistry: ExportTypesRegistry,
logger: Logger
) {
const jobsQuery = jobsQueryFactory(server);
const getRouteConfig = getRouteConfigFactoryManagementPre(server);
const getRouteConfigDownload = getRouteConfigFactoryDownloadPre(server);
@ -119,7 +125,7 @@ export function registerJobs(server: ServerFacade) {
});
// trigger a download of the output from a job
const jobResponseHandler = jobResponseHandlerFactory(server);
const jobResponseHandler = jobResponseHandlerFactory(server, exportTypesRegistry);
server.route({
path: `${MAIN_ENTRY}/download/{docId}`,
method: 'GET',
@ -136,13 +142,15 @@ export function registerJobs(server: ServerFacade) {
const { statusCode } = response;
if (statusCode !== 200) {
const logLevel = statusCode === 500 ? 'error' : 'debug';
server.log(
[logLevel, 'reporting', 'download'],
`Report ${docId} has non-OK status: [${statusCode}] Reason: [${JSON.stringify(
response.source
)}]`
);
if (statusCode === 500) {
logger.error(`Report ${docId} has failed: ${JSON.stringify(response.source)}`);
} else {
logger.debug(
`Report ${docId} has non-OK status: [${statusCode}] Reason: [${JSON.stringify(
response.source
)}]`
);
}
}
if (!response.isBoom) {

View file

@ -9,6 +9,7 @@ import * as _ from 'lodash';
import contentDisposition from 'content-disposition';
import {
ServerFacade,
ExportTypesRegistry,
ExportTypeDefinition,
JobDocExecuted,
JobDocOutputExecuted,
@ -40,9 +41,10 @@ const getReportingHeaders = (output: JobDocOutputExecuted, exportType: ExportTyp
return metaDataHeaders;
};
export function getDocumentPayloadFactory(server: ServerFacade) {
const exportTypesRegistry = server.plugins.reporting!.exportTypesRegistry;
export function getDocumentPayloadFactory(
server: ServerFacade,
exportTypesRegistry: ExportTypesRegistry
) {
function encodeContent(content: string | null, exportType: ExportTypeType) {
switch (exportType.jobContentEncoding) {
case 'base64':

View file

@ -9,9 +9,9 @@ import { jobsQueryFactory } from '../../lib/jobs_query';
import { WHITELISTED_JOB_CONTENT_TYPES } from '../../../common/constants';
import { getDocumentPayloadFactory } from './get_document_payload';
export function jobResponseHandlerFactory(server) {
export function jobResponseHandlerFactory(server, exportTypesRegistry) {
const jobsQuery = jobsQueryFactory(server);
const getDocumentPayload = getDocumentPayloadFactory(server);
const getDocumentPayload = getDocumentPayloadFactory(server, exportTypesRegistry);
return function jobResponseHandler(validJobTypes, user, h, params, opts = {}) {
const { docId } = params;

View file

@ -4,17 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { exportTypesRegistryFactory } from '../lib/export_types_registry';
import { XPackMainPlugin } from '../../../xpack_main/xpack_main';
import { ExportTypesRegistry } from '../lib/export_types_registry';
/*
* Gets a handle to the Reporting export types registry and returns a few
* functions for examining them
* @param {Object} server: Kibana server
* @return {Object} export type handler
*/
export async function getExportTypesHandler(server) {
const exportTypesRegistry = await exportTypesRegistryFactory(server);
export function getExportTypesHandler(exportTypesRegistry: ExportTypesRegistry) {
return {
/*
* Based on the X-Pack license and which export types are available,
@ -23,12 +21,17 @@ export async function getExportTypesHandler(server) {
* @param {Object} xpackInfo: xpack_main plugin info object
* @return {Object} availability of each export type
*/
getAvailability(xpackInfo) {
const exportTypesAvailability = {};
getAvailability(xpackInfo: XPackMainPlugin['info']) {
const exportTypesAvailability: { [exportType: string]: boolean } = {};
const xpackInfoAvailable = xpackInfo && xpackInfo.isAvailable();
const licenseType = xpackInfo.license.getType();
for(const exportType of exportTypesRegistry.getAll()) {
exportTypesAvailability[exportType.jobType] = xpackInfoAvailable ? exportType.validLicenses.includes(licenseType) : false;
const licenseType: string | undefined = xpackInfo.license.getType();
if (!licenseType) {
throw new Error('No license type returned from XPackMainPlugin#info!');
}
for (const exportType of exportTypesRegistry.getAll()) {
exportTypesAvailability[exportType.jobType] = xpackInfoAvailable
? exportType.validLicenses.includes(licenseType)
: false;
}
return exportTypesAvailability;
@ -39,6 +42,6 @@ export async function getExportTypesHandler(server) {
*/
getNumExportTypes() {
return exportTypesRegistry.getSize();
}
},
};
}

View file

@ -5,7 +5,7 @@
*/
import { get } from 'lodash';
import { ServerFacade, ESCallCluster } from '../../types';
import { ServerFacade, ExportTypesRegistry, ESCallCluster } from '../../types';
import {
AggregationBuckets,
AggregationResults,
@ -16,7 +16,6 @@ import {
RangeStats,
} from './types';
import { decorateRangeStats } from './decorate_range_stats';
// @ts-ignore untyped module
import { getExportTypesHandler } from './get_export_type_handler';
const JOB_TYPES_KEY = 'jobTypes';
@ -101,7 +100,11 @@ async function handleResponse(
};
}
export async function getReportingUsage(server: ServerFacade, callCluster: ESCallCluster) {
export async function getReportingUsage(
server: ServerFacade,
callCluster: ESCallCluster,
exportTypesRegistry: ExportTypesRegistry
) {
const config = server.config();
const reportingIndex = config.get('xpack.reporting.index');
@ -138,13 +141,13 @@ export async function getReportingUsage(server: ServerFacade, callCluster: ESCal
return callCluster('search', params)
.then((response: AggregationResults) => handleResponse(server, response))
.then(async (usage: RangeStatSets) => {
.then((usage: RangeStatSets) => {
// Allow this to explicitly throw an exception if/when this config is deprecated,
// because we shouldn't collect browserType in that case!
const browserType = config.get('xpack.reporting.capture.browser.type');
const xpackInfo = server.plugins.xpack_main.info;
const exportTypesHandler = await getExportTypesHandler(server);
const exportTypesHandler = getExportTypesHandler(exportTypesRegistry);
const availability = exportTypesHandler.getAvailability(xpackInfo) as FeatureAvailabilityMap;
const { lastDay, last7Days, ...all } = usage;

View file

@ -4,8 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import sinon from 'sinon';
import { getExportTypesRegistry } from '../lib/export_types_registry';
import { getReportingUsageCollector } from './reporting_usage_collector';
const exportTypesRegistry = getExportTypesRegistry();
function getMockUsageCollection() {
class MockUsageCollector {
constructor(_server, { fetch }) {
@ -40,7 +43,6 @@ function getServerMock(customization) {
},
},
},
expose: () => {},
log: () => {},
config: () => ({
get: key => {
@ -67,8 +69,13 @@ describe('license checks', () => {
.returns('basic');
const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock()));
const usageCollection = getMockUsageCollection();
const { fetch: getReportingUsage } = getReportingUsageCollector(usageCollection, serverWithBasicLicenseMock);
usageStats = await getReportingUsage(callClusterMock);
const { fetch: getReportingUsage } = getReportingUsageCollector(
usageCollection,
serverWithBasicLicenseMock,
() => {},
exportTypesRegistry
);
usageStats = await getReportingUsage(callClusterMock, exportTypesRegistry);
});
test('sets enables to true', async () => {
@ -93,8 +100,13 @@ describe('license checks', () => {
.returns('none');
const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock()));
const usageCollection = getMockUsageCollection();
const { fetch: getReportingUsage } = getReportingUsageCollector(usageCollection, serverWithNoLicenseMock);
usageStats = await getReportingUsage(callClusterMock);
const { fetch: getReportingUsage } = getReportingUsageCollector(
usageCollection,
serverWithNoLicenseMock,
() => {},
exportTypesRegistry
);
usageStats = await getReportingUsage(callClusterMock, exportTypesRegistry);
});
test('sets enables to true', async () => {
@ -121,9 +133,11 @@ describe('license checks', () => {
const usageCollection = getMockUsageCollection();
const { fetch: getReportingUsage } = getReportingUsageCollector(
usageCollection,
serverWithPlatinumLicenseMock
serverWithPlatinumLicenseMock,
() => {},
exportTypesRegistry
);
usageStats = await getReportingUsage(callClusterMock);
usageStats = await getReportingUsage(callClusterMock, exportTypesRegistry);
});
test('sets enables to true', async () => {
@ -148,8 +162,13 @@ describe('license checks', () => {
.returns('basic');
const callClusterMock = jest.fn(() => Promise.resolve({}));
const usageCollection = getMockUsageCollection();
const { fetch: getReportingUsage } = getReportingUsageCollector(usageCollection, serverWithBasicLicenseMock);
usageStats = await getReportingUsage(callClusterMock);
const { fetch: getReportingUsage } = getReportingUsageCollector(
usageCollection,
serverWithBasicLicenseMock,
() => {},
exportTypesRegistry
);
usageStats = await getReportingUsage(callClusterMock, exportTypesRegistry);
});
test('sets enables to true', async () => {
@ -170,7 +189,12 @@ describe('data modeling', () => {
serverWithPlatinumLicenseMock.plugins.xpack_main.info.license.getType = sinon
.stub()
.returns('platinum');
({ fetch: getReportingUsage } = getReportingUsageCollector(usageCollection, serverWithPlatinumLicenseMock));
({ fetch: getReportingUsage } = getReportingUsageCollector(
usageCollection,
serverWithPlatinumLicenseMock,
() => {},
exportTypesRegistry
));
});
test('with normal looking usage data', async () => {
@ -295,6 +319,7 @@ describe('data modeling', () => {
})
)
);
const usageStats = await getReportingUsage(callClusterMock);
expect(usageStats).toMatchInlineSnapshot(`
Object {

View file

@ -7,7 +7,7 @@
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
// @ts-ignore untyped module
import { KIBANA_STATS_TYPE_MONITORING } from '../../../monitoring/common/constants';
import { ServerFacade, ESCallCluster } from '../../types';
import { ServerFacade, ExportTypesRegistry, ESCallCluster } from '../../types';
import { KIBANA_REPORTING_TYPE } from '../../common/constants';
import { getReportingUsage } from './get_reporting_usage';
import { RangeStats } from './types';
@ -19,12 +19,14 @@ import { RangeStats } from './types';
export function getReportingUsageCollector(
usageCollection: UsageCollectionSetup,
server: ServerFacade,
isReady: () => boolean
isReady: () => boolean,
exportTypesRegistry: ExportTypesRegistry
) {
return usageCollection.makeUsageCollector({
type: KIBANA_REPORTING_TYPE,
isReady,
fetch: (callCluster: ESCallCluster) => getReportingUsage(server, callCluster),
fetch: (callCluster: ESCallCluster) =>
getReportingUsage(server, callCluster, exportTypesRegistry),
/*
* Format the response data into a model for internal upload
@ -49,8 +51,14 @@ export function getReportingUsageCollector(
export function registerReportingUsageCollector(
usageCollection: UsageCollectionSetup,
server: ServerFacade,
isReady: () => boolean
isReady: () => boolean,
exportTypesRegistry: ExportTypesRegistry
) {
const collector = getReportingUsageCollector(usageCollection, server, isReady);
const collector = getReportingUsageCollector(
usageCollection,
server,
isReady,
exportTypesRegistry
);
usageCollection.registerCollector(collector);
}

View file

@ -13,9 +13,12 @@ import {
CallCluster,
} from '../../../../src/legacy/core_plugins/elasticsearch';
import { CancellationToken } from './common/cancellation_token';
import { LevelLogger } from './server/lib/level_logger';
import { HeadlessChromiumDriverFactory } from './server/browsers/chromium/driver_factory';
import { BrowserType } from './server/browsers/types';
export type ReportingPlugin = object; // For Plugin contract
export type Job = EventEmitter & {
id: string;
toJSON: () => {
@ -23,21 +26,6 @@ export type Job = EventEmitter & {
};
};
export interface ReportingPlugin {
queue: {
addJob: <PayloadType>(type: string, payload: PayloadType, options: object) => Job;
};
// TODO: convert exportTypesRegistry to TS
exportTypesRegistry: {
getById: <T, U, V, W>(id: string) => ExportTypeDefinition<T, U, V, W>;
getAll: <T, U, V, W>() => Array<ExportTypeDefinition<T, U, V, W>>;
get: <T, U, V, W>(
callback: (item: ExportTypeDefinition<T, U, V, W>) => boolean
) => ExportTypeDefinition<T, U, V, W>;
};
browserDriverFactory: HeadlessChromiumDriverFactory;
}
export interface ReportingConfigOptions {
browser: BrowserConfig;
poll: {
@ -88,7 +76,6 @@ export type ReportingPluginSpecOptions = Legacy.PluginSpecOptions;
export type ServerFacade = Legacy.Server & {
plugins: {
reporting?: ReportingPlugin;
xpack_main?: XPackMainPlugin & {
status?: any;
};
@ -107,6 +94,15 @@ interface ReportingRequest {
};
}
export type EnqueueJobFn = <JobParamsType>(
parentLogger: LevelLogger,
exportTypeId: string,
jobParams: JobParamsType,
user: string,
headers: Record<string, string>,
request: RequestFacade
) => Promise<Job>;
export type RequestFacade = ReportingRequest & Legacy.Request;
export type ResponseFacade = ResponseObject & {
@ -246,6 +242,10 @@ export interface JobDocOutputExecuted {
size: number;
}
export interface ESQueue {
addJob: (type: string, payload: object, options: object) => Job;
}
export interface ESQueueWorker {
on: (event: string, handler: any) => void;
}
@ -304,7 +304,12 @@ export interface ESQueueInstance<JobParamsType, JobDocPayloadType> {
}
export type CreateJobFactory<CreateJobFnType> = (server: ServerFacade) => CreateJobFnType;
export type ExecuteJobFactory<ExecuteJobFnType> = (server: ServerFacade) => ExecuteJobFnType;
export type ExecuteJobFactory<ExecuteJobFnType> = (
server: ServerFacade,
opts: {
browserDriverFactory: HeadlessChromiumDriverFactory;
}
) => ExecuteJobFnType;
export interface ExportTypeDefinition<
JobParamsType,
@ -322,21 +327,13 @@ export interface ExportTypeDefinition<
validLicenses: string[];
}
export interface ExportTypesRegistry {
register: <JobParamsType, CreateJobFnType, JobPayloadType, ExecuteJobFnType>(
exportTypeDefinition: ExportTypeDefinition<
JobParamsType,
CreateJobFnType,
JobPayloadType,
ExecuteJobFnType
>
) => void;
}
export { ExportTypesRegistry } from './server/lib/export_types_registry';
export { HeadlessChromiumDriver } from './server/browsers/chromium/driver';
export { HeadlessChromiumDriverFactory } from './server/browsers/chromium/driver_factory';
export { CancellationToken } from './common/cancellation_token';
// Prefer to import this type using: `import { LevelLogger } from 'relative/path/server/lib';`
export { LevelLogger as Logger } from './server/lib/level_logger';
export { LevelLogger as Logger };
export interface AbsoluteURLFactoryOptions {
defaultBasePath: string;