[Rollups] Server NP migration (#55606)

This commit is contained in:
Alison Goryachev 2020-02-07 10:00:58 -05:00 committed by GitHub
parent 6d03ad440f
commit 86e186c63b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 1075 additions and 1178 deletions

View file

@ -4,12 +4,22 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { LICENSE_TYPE_BASIC, LicenseType } from '../../../common/constants';
export const PLUGIN = {
ID: 'rollup',
MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_BASIC as LicenseType,
getI18nName: (i18n: any): string => {
return i18n.translate('xpack.rollupJobs.appName', {
defaultMessage: 'Rollup jobs',
});
},
};
export const CONFIG_ROLLUPS = 'rollups:enableIndexPatterns';
export const API_BASE_PATH = '/api/rollup';
export {
UIM_APP_NAME,
UIM_APP_LOAD,

View file

@ -5,20 +5,13 @@
*/
import { resolve } from 'path';
import { PLUGIN, CONFIG_ROLLUPS } from './common';
import { registerLicenseChecker } from './server/lib/register_license_checker';
import { rollupDataEnricher } from './rollup_data_enricher';
import { registerRollupSearchStrategy } from './server/lib/search_strategies';
import {
registerIndicesRoute,
registerFieldsForWildcardRoute,
registerSearchRoute,
registerJobsRoute,
} from './server/routes/api';
import { registerRollupUsageCollector } from './server/usage';
import { i18n } from '@kbn/i18n';
import { PluginInitializerContext } from 'src/core/server';
import { RollupSetup } from '../../../plugins/rollup/server';
import { PLUGIN, CONFIG_ROLLUPS } from './common';
import { plugin } from './server';
export function rollup(kibana) {
export function rollup(kibana: any) {
return new kibana.Plugin({
id: PLUGIN.ID,
configPrefix: 'xpack.rollup',
@ -45,22 +38,30 @@ export function rollup(kibana) {
visualize: ['plugins/rollup/legacy'],
search: ['plugins/rollup/legacy'],
},
init: function(server) {
const { usageCollection } = server.newPlatform.setup.plugins;
registerLicenseChecker(server);
registerIndicesRoute(server);
registerFieldsForWildcardRoute(server);
registerSearchRoute(server);
registerJobsRoute(server);
registerRollupUsageCollector(usageCollection, server);
if (
server.plugins.index_management &&
server.plugins.index_management.addIndexManagementDataEnricher
) {
server.plugins.index_management.addIndexManagementDataEnricher(rollupDataEnricher);
}
init(server: any) {
const { core: coreSetup, plugins } = server.newPlatform.setup;
const { usageCollection, metrics } = plugins;
registerRollupSearchStrategy(this.kbnServer);
const rollupSetup = (plugins.rollup as unknown) as RollupSetup;
const initContext = ({
config: rollupSetup.__legacy.config,
logger: rollupSetup.__legacy.logger,
} as unknown) as PluginInitializerContext;
const rollupPluginInstance = plugin(initContext);
rollupPluginInstance.setup(coreSetup, {
usageCollection,
metrics,
__LEGACY: {
plugins: {
xpack_main: server.plugins.xpack_main,
rollup: server.plugins[PLUGIN.ID],
index_management: server.plugins.index_management,
},
},
});
},
});
}

View file

@ -0,0 +1,14 @@
{
"id": "rollup",
"version": "kibana",
"requiredPlugins": [
"home",
"index_management",
"metrics"
],
"optionalPlugins": [
"usageCollection"
],
"server": true,
"ui": false
}

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const elasticsearchJsPlugin = (Client, config, components) => {
export const elasticsearchJsPlugin = (Client: any, config: any, components: any) => {
const ca = components.clientAction.factory;
Client.prototype.rollup = components.clientAction.namespaceFactory();

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { isEsErrorFactory } from './is_es_error_factory';
export { registerRollupUsageCollector } from './register';

View file

@ -5,25 +5,31 @@
*/
import { get } from 'lodash';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { CallCluster } from 'src/legacy/core_plugins/elasticsearch';
interface IdToFlagMap {
[key: string]: boolean;
}
const ROLLUP_USAGE_TYPE = 'rollups';
// elasticsearch index.max_result_window default value
const ES_MAX_RESULT_WINDOW_DEFAULT_VALUE = 1000;
function getIdFromSavedObjectId(savedObjectId) {
function getIdFromSavedObjectId(savedObjectId: string) {
// The saved object ID is formatted `{TYPE}:{ID}`.
return savedObjectId.split(':')[1];
}
function createIdToFlagMap(ids) {
function createIdToFlagMap(ids: string[]) {
return ids.reduce((map, id) => {
map[id] = true;
return map;
}, {});
}, {} as any);
}
async function fetchRollupIndexPatterns(kibanaIndex, callCluster) {
async function fetchRollupIndexPatterns(kibanaIndex: string, callCluster: CallCluster) {
const searchParams = {
size: ES_MAX_RESULT_WINDOW_DEFAULT_VALUE,
index: kibanaIndex,
@ -50,7 +56,11 @@ async function fetchRollupIndexPatterns(kibanaIndex, callCluster) {
});
}
async function fetchRollupSavedSearches(kibanaIndex, callCluster, rollupIndexPatternToFlagMap) {
async function fetchRollupSavedSearches(
kibanaIndex: string,
callCluster: CallCluster,
rollupIndexPatternToFlagMap: IdToFlagMap
) {
const searchParams = {
size: ES_MAX_RESULT_WINDOW_DEFAULT_VALUE,
index: kibanaIndex,
@ -86,19 +96,19 @@ async function fetchRollupSavedSearches(kibanaIndex, callCluster, rollupIndexPat
const searchSource = JSON.parse(searchSourceJSON);
if (rollupIndexPatternToFlagMap[searchSource.index]) {
const id = getIdFromSavedObjectId(savedObjectId);
const id = getIdFromSavedObjectId(savedObjectId) as string;
rollupSavedSearches.push(id);
}
return rollupSavedSearches;
}, []);
}, [] as string[]);
}
async function fetchRollupVisualizations(
kibanaIndex,
callCluster,
rollupIndexPatternToFlagMap,
rollupSavedSearchesToFlagMap
kibanaIndex: string,
callCluster: CallCluster,
rollupIndexPatternToFlagMap: IdToFlagMap,
rollupSavedSearchesToFlagMap: IdToFlagMap
) {
const searchParams = {
size: ES_MAX_RESULT_WINDOW_DEFAULT_VALUE,
@ -135,7 +145,7 @@ async function fetchRollupVisualizations(
savedSearchRefName,
kibanaSavedObjectMeta: { searchSourceJSON },
},
references = [],
references = [] as any[],
},
} = visualization;
@ -164,13 +174,14 @@ async function fetchRollupVisualizations(
};
}
export function registerRollupUsageCollector(usageCollection, server) {
const kibanaIndex = server.config().get('kibana.index');
export function registerRollupUsageCollector(
usageCollection: UsageCollectionSetup,
kibanaIndex: string
): void {
const collector = usageCollection.makeUsageCollector({
type: ROLLUP_USAGE_TYPE,
isReady: () => true,
fetch: async callCluster => {
fetch: async (callCluster: CallCluster) => {
const rollupIndexPatterns = await fetchRollupIndexPatterns(kibanaIndex, callCluster);
const rollupIndexPatternToFlagMap = createIdToFlagMap(rollupIndexPatterns);

View file

@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { PluginInitializerContext } from 'src/core/server';
import { RollupsServerPlugin } from './plugin';
export { wrapCustomError } from './wrap_custom_error';
export { wrapEsError } from './wrap_es_error';
export { wrapUnknownError } from './wrap_unknown_error';
export const plugin = (ctx: PluginInitializerContext) => new RollupsServerPlugin(ctx);

View file

@ -1,21 +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 { once } from 'lodash';
import { elasticsearchJsPlugin } from '../../client/elasticsearch_rollup';
const callWithRequest = once(server => {
const client = server.newPlatform.setup.core.elasticsearch.createClient('rollup', {
plugins: [elasticsearchJsPlugin],
});
return (request, ...args) => client.asScoped(request).callAsCurrentUser(...args);
});
export const callWithRequestFactory = (server, request) => {
return (...args) => {
return callWithRequest(server)(request, ...args);
};
};

View file

@ -0,0 +1,28 @@
/*
* 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 { ElasticsearchServiceSetup } from 'kibana/server';
import { once } from 'lodash';
import { elasticsearchJsPlugin } from '../../client/elasticsearch_rollup';
const callWithRequest = once((elasticsearchService: ElasticsearchServiceSetup) => {
const config = { plugins: [elasticsearchJsPlugin] };
return elasticsearchService.createClient('rollup', config);
});
export const callWithRequestFactory = (
elasticsearchService: ElasticsearchServiceSetup,
request: any
) => {
return (...args: any[]) => {
return (
callWithRequest(elasticsearchService)
.asScoped(request)
// @ts-ignore
.callAsCurrentUser(...args)
);
};
};

View file

@ -1,145 +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 expect from '@kbn/expect';
import { set } from 'lodash';
import { checkLicense } from '../check_license';
describe('check_license', function() {
let mockLicenseInfo;
beforeEach(() => (mockLicenseInfo = {}));
describe('license information is undefined', () => {
beforeEach(() => (mockLicenseInfo = undefined));
it('should set isAvailable to false', () => {
expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false);
});
it('should set showLinks to true', () => {
expect(checkLicense(mockLicenseInfo).showLinks).to.be(true);
});
it('should set enableLinks to false', () => {
expect(checkLicense(mockLicenseInfo).enableLinks).to.be(false);
});
it('should set a message', () => {
expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined);
});
});
describe('license information is not available', () => {
beforeEach(() => (mockLicenseInfo.isAvailable = () => false));
it('should set isAvailable to false', () => {
expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false);
});
it('should set showLinks to true', () => {
expect(checkLicense(mockLicenseInfo).showLinks).to.be(true);
});
it('should set enableLinks to false', () => {
expect(checkLicense(mockLicenseInfo).enableLinks).to.be(false);
});
it('should set a message', () => {
expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined);
});
});
describe('license information is available', () => {
beforeEach(() => {
mockLicenseInfo.isAvailable = () => true;
set(mockLicenseInfo, 'license.getType', () => 'basic');
});
describe('& license is > basic', () => {
beforeEach(() => set(mockLicenseInfo, 'license.isOneOf', () => true));
describe('& license is active', () => {
beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true));
it('should set isAvailable to true', () => {
expect(checkLicense(mockLicenseInfo).isAvailable).to.be(true);
});
it('should set showLinks to true', () => {
expect(checkLicense(mockLicenseInfo).showLinks).to.be(true);
});
it('should set enableLinks to true', () => {
expect(checkLicense(mockLicenseInfo).enableLinks).to.be(true);
});
it('should not set a message', () => {
expect(checkLicense(mockLicenseInfo).message).to.be(undefined);
});
});
describe('& license is expired', () => {
beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false));
it('should set isAvailable to false', () => {
expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false);
});
it('should set showLinks to true', () => {
expect(checkLicense(mockLicenseInfo).showLinks).to.be(true);
});
it('should set enableLinks to false', () => {
expect(checkLicense(mockLicenseInfo).enableLinks).to.be(false);
});
it('should set a message', () => {
expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined);
});
});
});
describe('& license is basic', () => {
beforeEach(() => set(mockLicenseInfo, 'license.isOneOf', () => true));
describe('& license is active', () => {
beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true));
it('should set isAvailable to true', () => {
expect(checkLicense(mockLicenseInfo).isAvailable).to.be(true);
});
it('should set showLinks to true', () => {
expect(checkLicense(mockLicenseInfo).showLinks).to.be(true);
});
it('should set enableLinks to true', () => {
expect(checkLicense(mockLicenseInfo).enableLinks).to.be(true);
});
it('should not set a message', () => {
expect(checkLicense(mockLicenseInfo).message).to.be(undefined);
});
});
describe('& license is expired', () => {
beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false));
it('should set isAvailable to false', () => {
expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false);
});
it('should set showLinks to true', () => {
expect(checkLicense(mockLicenseInfo).showLinks).to.be(true);
});
it('should set a message', () => {
expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined);
});
});
});
});
});

View file

@ -1,66 +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 { i18n } from '@kbn/i18n';
export function checkLicense(xpackLicenseInfo) {
const pluginName = 'Rollups';
// If, for some reason, we cannot get the license information
// from Elasticsearch, assume worst case and disable
if (!xpackLicenseInfo || !xpackLicenseInfo.isAvailable()) {
return {
isAvailable: false,
showLinks: true,
enableLinks: false,
message: i18n.translate('xpack.rollupJobs.checkLicense.errorUnavailableMessage', {
defaultMessage:
'You cannot use {pluginName} because license information is not available at this time.',
values: { pluginName },
}),
};
}
const VALID_LICENSE_MODES = ['trial', 'basic', 'standard', 'gold', 'platinum', 'enterprise'];
const isLicenseModeValid = xpackLicenseInfo.license.isOneOf(VALID_LICENSE_MODES);
const isLicenseActive = xpackLicenseInfo.license.isActive();
const licenseType = xpackLicenseInfo.license.getType();
// License is not valid
if (!isLicenseModeValid) {
return {
isAvailable: false,
showLinks: false,
message: i18n.translate('xpack.rollupJobs.checkLicense.errorUnsupportedMessage', {
defaultMessage:
'Your {licenseType} license does not support {pluginName}. Please upgrade your license.',
values: { licenseType, pluginName },
}),
};
}
// License is valid but not active
if (!isLicenseActive) {
return {
isAvailable: false,
showLinks: true,
enableLinks: false,
message: i18n.translate('xpack.rollupJobs.checkLicense.errorExpiredMessage', {
defaultMessage:
'You cannot use {pluginName} because your {licenseType} license has expired',
values: { licenseType, pluginName },
}),
};
}
// License is valid and active
return {
isAvailable: true,
showLinks: true,
enableLinks: true,
};
}

View file

@ -1,21 +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 expect from '@kbn/expect';
import { wrapCustomError } from '../wrap_custom_error';
describe('wrap_custom_error', () => {
describe('#wrapCustomError', () => {
it('should return a Boom object', () => {
const originalError = new Error('I am an error');
const statusCode = 404;
const wrappedError = wrapCustomError(originalError, statusCode);
expect(wrappedError.isBoom).to.be(true);
expect(wrappedError.output.statusCode).to.equal(statusCode);
});
});
});

View file

@ -1,39 +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 expect from '@kbn/expect';
import { wrapEsError } from '../wrap_es_error';
describe('wrap_es_error', () => {
describe('#wrapEsError', () => {
let originalError;
beforeEach(() => {
originalError = new Error('I am an error');
originalError.statusCode = 404;
originalError.response = '{}';
});
it('should return a Boom object', () => {
const wrappedError = wrapEsError(originalError);
expect(wrappedError.isBoom).to.be(true);
});
it('should return the correct Boom object', () => {
const wrappedError = wrapEsError(originalError);
expect(wrappedError.output.statusCode).to.be(originalError.statusCode);
expect(wrappedError.output.payload.message).to.be(originalError.message);
});
it('should return the correct Boom object with custom message', () => {
const wrappedError = wrapEsError(originalError, { 404: 'No encontrado!' });
expect(wrappedError.output.statusCode).to.be(originalError.statusCode);
expect(wrappedError.output.payload.message).to.be('No encontrado!');
});
});
});

View file

@ -1,19 +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 expect from '@kbn/expect';
import { wrapUnknownError } from '../wrap_unknown_error';
describe('wrap_unknown_error', () => {
describe('#wrapUnknownError', () => {
it('should return a Boom object', () => {
const originalError = new Error('I am an error');
const wrappedError = wrapUnknownError(originalError);
expect(wrappedError.isBoom).to.be(true);
});
});
});

View file

@ -1,18 +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 Boom from 'boom';
/**
* Wraps a custom error into a Boom error response and returns it
*
* @param err Object error
* @param statusCode Error status code
* @return Object Boom error response
*/
export function wrapCustomError(err, statusCode) {
return Boom.boomify(err, { statusCode });
}

View file

@ -1,59 +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 Boom from 'boom';
function extractCausedByChain(causedBy = {}, accumulator = []) {
const { reason, caused_by } = causedBy; // eslint-disable-line camelcase
if (reason) {
accumulator.push(reason);
}
// eslint-disable-next-line camelcase
if (caused_by) {
return extractCausedByChain(caused_by, accumulator);
}
return accumulator;
}
/**
* Wraps an error thrown by the ES JS client into a Boom error response and returns it
*
* @param err Object Error thrown by ES JS client
* @param statusCodeToMessageMap Object Optional map of HTTP status codes => error messages
* @return Object Boom error response
*/
export function wrapEsError(err, statusCodeToMessageMap = {}) {
const { statusCode, response } = err;
const {
error: {
root_cause = [], // eslint-disable-line camelcase
caused_by, // eslint-disable-line camelcase
} = {},
} = JSON.parse(response);
// If no custom message if specified for the error's status code, just
// wrap the error as a Boom error response and return it
if (!statusCodeToMessageMap[statusCode]) {
const boomError = Boom.boomify(err, { statusCode });
// The caused_by chain has the most information so use that if it's available. If not then
// settle for the root_cause.
const causedByChain = extractCausedByChain(caused_by);
const defaultCause = root_cause.length ? extractCausedByChain(root_cause[0]) : undefined;
boomError.output.payload.cause = causedByChain.length ? causedByChain : defaultCause;
return boomError;
}
// Otherwise, use the custom message to create a Boom error response and
// return it
const message = statusCodeToMessageMap[statusCode];
return new Boom(message, { statusCode });
}

View file

@ -1,17 +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 Boom from 'boom';
/**
* Wraps an unknown error into a Boom error response and returns it
*
* @param err Object Unknown error
* @return Object Boom error response
*/
export function wrapUnknownError(err) {
return Boom.boomify(err);
}

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { checkLicense } from './check_license';
export { isEsError } from './is_es_error';

View file

@ -0,0 +1,13 @@
/*
* 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 * as legacyElasticsearch from 'elasticsearch';
const esErrorsParent = legacyElasticsearch.errors._Abstract;
export function isEsError(err: Error) {
return err instanceof esErrorsParent;
}

View file

@ -1,44 +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 expect from '@kbn/expect';
import { isEsErrorFactory } from '../is_es_error_factory';
import { set } from 'lodash';
class MockAbstractEsError {}
describe('is_es_error_factory', () => {
let mockServer;
let isEsError;
beforeEach(() => {
const mockEsErrors = {
_Abstract: MockAbstractEsError,
};
mockServer = {};
set(mockServer, 'plugins.elasticsearch.getCluster', () => ({ errors: mockEsErrors }));
isEsError = isEsErrorFactory(mockServer);
});
describe('#isEsErrorFactory', () => {
it('should return a function', () => {
expect(isEsError).to.be.a(Function);
});
describe('returned function', () => {
it('should return true if passed-in err is a known esError', () => {
const knownEsError = new MockAbstractEsError();
expect(isEsError(knownEsError)).to.be(true);
});
it('should return false if passed-in err is not a known esError', () => {
const unknownEsError = {};
expect(isEsError(unknownEsError)).to.be(false);
});
});
});
});

View file

@ -1,18 +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 { memoize } from 'lodash';
const esErrorsFactory = memoize(server => {
return server.plugins.elasticsearch.getCluster('admin').errors;
});
export function isEsErrorFactory(server) {
const esErrors = esErrorsFactory(server);
return function isEsError(err) {
return err instanceof esErrors._Abstract;
};
}

View file

@ -37,10 +37,10 @@ export function mergeJobConfigurations(jobs = []) {
throw new Error('No capabilities available');
}
const allAggs = {};
const allAggs: { [key: string]: any } = {};
// For each job, look through all of its fields
jobs.forEach(job => {
jobs.forEach((job: { fields: { [key: string]: any } }) => {
const fields = job.fields;
const fieldNames = Object.keys(fields);
@ -49,7 +49,7 @@ export function mergeJobConfigurations(jobs = []) {
const fieldAggs = fields[fieldName];
// Look through each field's capabilities (aggregations)
fieldAggs.forEach(agg => {
fieldAggs.forEach((agg: { agg: string; interval: string }) => {
const aggName = agg.agg;
const aggDoesntExist = !allAggs[aggName];
const fieldDoesntExist = allAggs[aggName] && !allAggs[aggName][fieldName];

View file

@ -1,66 +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 expect from '@kbn/expect';
import { licensePreRoutingFactory } from '../license_pre_routing_factory';
describe('license_pre_routing_factory', () => {
describe('#reportingFeaturePreRoutingFactory', () => {
let mockServer;
let mockLicenseCheckResults;
beforeEach(() => {
mockServer = {
plugins: {
xpack_main: {
info: {
feature: () => ({
getLicenseCheckResults: () => mockLicenseCheckResults,
}),
},
},
},
};
});
it('only instantiates one instance per server', () => {
const firstInstance = licensePreRoutingFactory(mockServer);
const secondInstance = licensePreRoutingFactory(mockServer);
expect(firstInstance).to.be(secondInstance);
});
describe('isAvailable is false', () => {
beforeEach(() => {
mockLicenseCheckResults = {
isAvailable: false,
};
});
it('replies with 403', () => {
const licensePreRouting = licensePreRoutingFactory(mockServer);
const response = licensePreRouting();
expect(response).to.be.an(Error);
expect(response.isBoom).to.be(true);
expect(response.output.statusCode).to.be(403);
});
});
describe('isAvailable is true', () => {
beforeEach(() => {
mockLicenseCheckResults = {
isAvailable: true,
};
});
it('replies with nothing', () => {
const licensePreRouting = licensePreRoutingFactory(mockServer);
const response = licensePreRouting();
expect(response).to.be(null);
});
});
});
});

View file

@ -1,28 +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 { once } from 'lodash';
import { wrapCustomError } from '../error_wrappers';
import { PLUGIN } from '../../../common';
export const licensePreRoutingFactory = once(server => {
const xpackMainPlugin = server.plugins.xpack_main;
// License checking and enable/disable logic
function licensePreRouting() {
const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN.ID).getLicenseCheckResults();
if (!licenseCheckResults.isAvailable) {
const error = new Error(licenseCheckResults.message);
const statusCode = 403;
const wrappedError = wrapCustomError(error, statusCode);
return wrappedError;
} else {
return null;
}
}
return licensePreRouting;
});

View file

@ -0,0 +1,62 @@
/*
* 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 { licensePreRoutingFactory } from '.';
import {
LICENSE_STATUS_VALID,
LICENSE_STATUS_INVALID,
} from '../../../../../common/constants/license_status';
import { kibanaResponseFactory } from '../../../../../../../src/core/server';
describe('licensePreRoutingFactory()', () => {
let mockServer;
let mockLicenseCheckResults;
beforeEach(() => {
mockServer = {
plugins: {
xpack_main: {
info: {
feature: () => ({
getLicenseCheckResults: () => mockLicenseCheckResults,
}),
},
},
},
};
});
describe('status is invalid', () => {
beforeEach(() => {
mockLicenseCheckResults = {
status: LICENSE_STATUS_INVALID,
};
});
it('replies with 403', () => {
const routeWithLicenseCheck = licensePreRoutingFactory(mockServer, () => {});
const stubRequest = {};
const response = routeWithLicenseCheck({}, stubRequest, kibanaResponseFactory);
expect(response.status).to.be(403);
});
});
describe('status is valid', () => {
beforeEach(() => {
mockLicenseCheckResults = {
status: LICENSE_STATUS_VALID,
};
});
it('replies with nothing', () => {
const routeWithLicenseCheck = licensePreRoutingFactory(mockServer, () => null);
const stubRequest = {};
const response = routeWithLicenseCheck({}, stubRequest, kibanaResponseFactory);
expect(response).to.be(null);
});
});
});

View file

@ -0,0 +1,43 @@
/*
* 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,
KibanaResponseFactory,
RequestHandler,
RequestHandlerContext,
} from 'src/core/server';
import { PLUGIN } from '../../../common';
import { LICENSE_STATUS_VALID } from '../../../../../common/constants/license_status';
import { ServerShim } from '../../types';
export const licensePreRoutingFactory = (
server: ServerShim,
handler: RequestHandler<any, any, any>
): RequestHandler<any, any, any> => {
const xpackMainPlugin = server.plugins.xpack_main;
// License checking and enable/disable logic
return function licensePreRouting(
ctx: RequestHandlerContext,
request: KibanaRequest,
response: KibanaResponseFactory
) {
const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN.ID).getLicenseCheckResults();
const { status } = licenseCheckResults;
if (status !== LICENSE_STATUS_VALID) {
return response.customError({
body: {
message: licenseCheckResults.messsage,
},
statusCode: 403,
});
}
return handler(ctx, request, response);
};
};

View file

@ -6,9 +6,9 @@
import { mergeJobConfigurations } from './jobs_compatibility';
export function getCapabilitiesForRollupIndices(indices) {
export function getCapabilitiesForRollupIndices(indices: { [key: string]: any }) {
const indexNames = Object.keys(indices);
const capabilities = {};
const capabilities = {} as { [key: string]: any };
indexNames.forEach(index => {
try {

View file

@ -6,13 +6,18 @@
// Merge rollup capabilities information with field information
export interface Field {
name?: string;
[key: string]: any;
}
export const mergeCapabilitiesWithFields = (
rollupIndexCapabilities,
fieldsFromFieldCapsApi,
previousFields = []
rollupIndexCapabilities: { [key: string]: any },
fieldsFromFieldCapsApi: { [key: string]: any },
previousFields: Field[] = []
) => {
const rollupFields = [...previousFields];
const rollupFieldNames = [];
const rollupFieldNames: string[] = [];
Object.keys(rollupIndexCapabilities).forEach(agg => {
// Field names of the aggregation

View file

@ -1,24 +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 { mirrorPluginStatus } from '../../../../../server/lib/mirror_plugin_status';
import { checkLicense } from '../check_license';
import { PLUGIN } from '../../../common';
export function registerLicenseChecker(server) {
const xpackMainPlugin = server.plugins.xpack_main;
const rollupPlugin = server.plugins[PLUGIN.ID];
mirrorPluginStatus(xpackMainPlugin, rollupPlugin);
xpackMainPlugin.status.once('green', () => {
// Register a function that is called whenever the xpack info changes,
// to re-compute the license check results for this plugin
xpackMainPlugin.info.feature(PLUGIN.ID).registerLicenseCheckResultsGenerator(checkLicense);
});
}

View file

@ -4,9 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { unitsMap } from '@elastic/datemath';
import dateMath from '@elastic/datemath';
export type Unit = 'ms' | 's' | 'm' | 'h' | 'd' | 'w' | 'M' | 'y';
export const leastCommonInterval = (num = 0, base = 0) =>
Math.max(Math.ceil(num / base) * base, base);
export const isCalendarInterval = ({ unit, value }) =>
value === 1 && ['calendar', 'mixed'].includes(unitsMap[unit].type);
export const isCalendarInterval = ({ unit, value }: { unit: Unit; value: number }) => {
const { unitsMap } = dateMath;
return value === 1 && ['calendar', 'mixed'].includes(unitsMap[unit].type);
};

View file

@ -1,32 +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 { getRollupSearchStrategy } from './rollup_search_strategy';
import { getRollupSearchRequest } from './rollup_search_request';
import { getRollupSearchCapabilities } from './rollup_search_capabilities';
import {
AbstractSearchRequest,
DefaultSearchCapabilities,
AbstractSearchStrategy,
} from '../../../../../../../src/plugins/vis_type_timeseries/server';
export const registerRollupSearchStrategy = kbnServer =>
kbnServer.afterPluginsInit(() => {
if (!kbnServer.newPlatform.setup.plugins.metrics) {
return;
}
const { addSearchStrategy } = kbnServer.newPlatform.setup.plugins.metrics;
const RollupSearchRequest = getRollupSearchRequest(AbstractSearchRequest);
const RollupSearchCapabilities = getRollupSearchCapabilities(DefaultSearchCapabilities);
const RollupSearchStrategy = getRollupSearchStrategy(
AbstractSearchStrategy,
RollupSearchRequest,
RollupSearchCapabilities
);
addSearchStrategy(new RollupSearchStrategy(kbnServer));
});

View file

@ -6,45 +6,22 @@
import { registerRollupSearchStrategy } from './register_rollup_search_strategy';
describe('Register Rollup Search Strategy', () => {
let kbnServer;
let metrics;
let routeDependencies;
let addSearchStrategy;
beforeEach(() => {
const afterPluginsInit = jest.fn(callback => callback());
kbnServer = {
afterPluginsInit,
newPlatform: {
setup: { plugins: {} },
},
routeDependencies = {
router: jest.fn().mockName('router'),
elasticsearchService: jest.fn().mockName('elasticsearchService'),
elasticsearch: jest.fn().mockName('elasticsearch'),
};
metrics = {
addSearchStrategy: jest.fn().mockName('addSearchStrategy'),
AbstractSearchRequest: jest.fn().mockName('AbstractSearchRequest'),
AbstractSearchStrategy: jest.fn().mockName('AbstractSearchStrategy'),
DefaultSearchCapabilities: jest.fn().mockName('DefaultSearchCapabilities'),
};
addSearchStrategy = jest.fn().mockName('addSearchStrategy');
});
test('should run initialization on "afterPluginsInit" hook', () => {
registerRollupSearchStrategy(kbnServer);
test('should run initialization', () => {
registerRollupSearchStrategy(routeDependencies, addSearchStrategy);
expect(kbnServer.afterPluginsInit).toHaveBeenCalled();
});
test('should run initialization if metrics plugin available', () => {
registerRollupSearchStrategy({
...kbnServer,
newPlatform: { setup: { plugins: { metrics } } },
});
expect(metrics.addSearchStrategy).toHaveBeenCalled();
});
test('should not run initialization if metrics plugin unavailable', () => {
registerRollupSearchStrategy(kbnServer);
expect(metrics.addSearchStrategy).not.toHaveBeenCalled();
expect(addSearchStrategy).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,29 @@
/*
* 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 { getRollupSearchStrategy } from './rollup_search_strategy';
import { getRollupSearchRequest } from './rollup_search_request';
import { getRollupSearchCapabilities } from './rollup_search_capabilities';
import {
AbstractSearchRequest,
DefaultSearchCapabilities,
AbstractSearchStrategy,
} from '../../../../../../../src/plugins/vis_type_timeseries/server';
import { RouteDependencies } from '../../types';
export const registerRollupSearchStrategy = (
{ elasticsearchService }: RouteDependencies,
addSearchStrategy: (searchStrategy: any) => void
) => {
const RollupSearchRequest = getRollupSearchRequest(AbstractSearchRequest);
const RollupSearchCapabilities = getRollupSearchCapabilities(DefaultSearchCapabilities);
const RollupSearchStrategy = getRollupSearchStrategy(
AbstractSearchStrategy,
RollupSearchRequest,
RollupSearchCapabilities
);
addSearchStrategy(new RollupSearchStrategy(elasticsearchService));
};

View file

@ -4,24 +4,29 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { get, has } from 'lodash';
import { KibanaRequest } from 'kibana/server';
import { leastCommonInterval, isCalendarInterval } from './lib/interval_helper';
export const getRollupSearchCapabilities = DefaultSearchCapabilities =>
export const getRollupSearchCapabilities = (DefaultSearchCapabilities: any) =>
class RollupSearchCapabilities extends DefaultSearchCapabilities {
constructor(req, fieldsCapabilities, rollupIndex) {
constructor(
req: KibanaRequest,
fieldsCapabilities: { [key: string]: any },
rollupIndex: string
) {
super(req, fieldsCapabilities);
this.rollupIndex = rollupIndex;
this.availableMetrics = get(fieldsCapabilities, `${rollupIndex}.aggs`, {});
}
get dateHistogram() {
public get dateHistogram() {
const [dateHistogram] = Object.values(this.availableMetrics.date_histogram);
return dateHistogram;
}
get defaultTimeInterval() {
public get defaultTimeInterval() {
return (
this.dateHistogram.fixed_interval ||
this.dateHistogram.calendar_interval ||
@ -34,16 +39,16 @@ export const getRollupSearchCapabilities = DefaultSearchCapabilities =>
);
}
get searchTimezone() {
public get searchTimezone() {
return get(this.dateHistogram, 'time_zone', null);
}
get whiteListedMetrics() {
public get whiteListedMetrics() {
const baseRestrictions = this.createUiRestriction({
count: this.createUiRestriction(),
});
const getFields = fields =>
const getFields = (fields: { [key: string]: any }) =>
Object.keys(fields).reduce(
(acc, item) => ({
...acc,
@ -61,20 +66,20 @@ export const getRollupSearchCapabilities = DefaultSearchCapabilities =>
);
}
get whiteListedGroupByFields() {
public get whiteListedGroupByFields() {
return this.createUiRestriction({
everything: true,
terms: has(this.availableMetrics, 'terms'),
});
}
get whiteListedTimerangeModes() {
public get whiteListedTimerangeModes() {
return this.createUiRestriction({
last_value: true,
});
}
getValidTimeInterval(userIntervalString) {
getValidTimeInterval(userIntervalString: string) {
const parsedRollupJobInterval = this.parseInterval(this.defaultTimeInterval);
const inRollupJobUnit = this.convertIntervalToUnit(
userIntervalString,

View file

@ -5,9 +5,16 @@
*/
const SEARCH_METHOD = 'rollup.search';
export const getRollupSearchRequest = AbstractSearchRequest =>
interface Search {
index: string;
body: {
[key: string]: any;
};
}
export const getRollupSearchRequest = (AbstractSearchRequest: any) =>
class RollupSearchRequest extends AbstractSearchRequest {
async search(searches) {
async search(searches: Search[]) {
const requests = searches.map(({ body, index }) =>
this.callWithRequest(SEARCH_METHOD, {
body,

View file

@ -4,31 +4,32 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { indexBy, isString } from 'lodash';
import { ElasticsearchServiceSetup, KibanaRequest } from 'kibana/server';
import { callWithRequestFactory } from '../call_with_request_factory';
import { mergeCapabilitiesWithFields } from '../merge_capabilities_with_fields';
import { getCapabilitiesForRollupIndices } from '../map_capabilities';
const ROLLUP_INDEX_CAPABILITIES_METHOD = 'rollup.rollupIndexCapabilities';
const getRollupIndices = rollupData => Object.keys(rollupData);
const getRollupIndices = (rollupData: { [key: string]: any[] }) => Object.keys(rollupData);
const isIndexPatternContainsWildcard = indexPattern => indexPattern.includes('*');
const isIndexPatternValid = indexPattern =>
const isIndexPatternContainsWildcard = (indexPattern: string) => indexPattern.includes('*');
const isIndexPatternValid = (indexPattern: string) =>
indexPattern && isString(indexPattern) && !isIndexPatternContainsWildcard(indexPattern);
export const getRollupSearchStrategy = (
AbstractSearchStrategy,
RollupSearchRequest,
RollupSearchCapabilities
AbstractSearchStrategy: any,
RollupSearchRequest: any,
RollupSearchCapabilities: any
) =>
class RollupSearchStrategy extends AbstractSearchStrategy {
name = 'rollup';
constructor(server) {
super(server, callWithRequestFactory, RollupSearchRequest);
constructor(elasticsearchService: ElasticsearchServiceSetup) {
super(elasticsearchService, callWithRequestFactory, RollupSearchRequest);
}
getRollupData(req, indexPattern) {
getRollupData(req: KibanaRequest, indexPattern: string) {
const callWithRequest = this.getCallWithRequestInstance(req);
return callWithRequest(ROLLUP_INDEX_CAPABILITIES_METHOD, {
@ -36,7 +37,7 @@ export const getRollupSearchStrategy = (
}).catch(() => Promise.resolve({}));
}
async checkForViability(req, indexPattern) {
async checkForViability(req: KibanaRequest, indexPattern: string) {
let isViable = false;
let capabilities = null;
@ -60,7 +61,14 @@ export const getRollupSearchStrategy = (
};
}
async getFieldsForWildcard(req, indexPattern, { fieldsCapabilities, rollupIndex }) {
async getFieldsForWildcard(
req: KibanaRequest,
indexPattern: string,
{
fieldsCapabilities,
rollupIndex,
}: { fieldsCapabilities: { [key: string]: any }; rollupIndex: string }
) {
const fields = await super.getFieldsForWildcard(req, indexPattern);
const fieldsFromFieldCapsApi = indexBy(fields, 'name');
const rollupIndexCapabilities = fieldsCapabilities[rollupIndex].aggs;

View file

@ -0,0 +1,95 @@
/*
* 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 { CoreSetup, Plugin, PluginInitializerContext, Logger } from 'src/core/server';
import { first } from 'rxjs/operators';
import { i18n } from '@kbn/i18n';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { VisTypeTimeseriesSetup } from 'src/plugins/vis_type_timeseries/server';
import { registerLicenseChecker } from '../../../server/lib/register_license_checker';
import { PLUGIN } from '../common';
import { ServerShim, RouteDependencies } from './types';
import {
registerIndicesRoute,
registerFieldsForWildcardRoute,
registerSearchRoute,
registerJobsRoute,
} from './routes/api';
import { registerRollupUsageCollector } from './collectors';
import { rollupDataEnricher } from './rollup_data_enricher';
import { registerRollupSearchStrategy } from './lib/search_strategies';
export class RollupsServerPlugin implements Plugin<void, void, any, any> {
log: Logger;
constructor(private readonly initializerContext: PluginInitializerContext) {
this.log = initializerContext.logger.get();
}
async setup(
{ http, elasticsearch: elasticsearchService }: CoreSetup,
{
__LEGACY: serverShim,
usageCollection,
metrics,
}: {
__LEGACY: ServerShim;
usageCollection?: UsageCollectionSetup;
metrics?: VisTypeTimeseriesSetup;
}
) {
const elasticsearch = await elasticsearchService.adminClient;
const router = http.createRouter();
const routeDependencies: RouteDependencies = {
elasticsearch,
elasticsearchService,
router,
};
registerLicenseChecker(
serverShim as any,
PLUGIN.ID,
PLUGIN.getI18nName(i18n),
PLUGIN.MINIMUM_LICENSE_REQUIRED
);
registerIndicesRoute(routeDependencies, serverShim);
registerFieldsForWildcardRoute(routeDependencies, serverShim);
registerSearchRoute(routeDependencies, serverShim);
registerJobsRoute(routeDependencies, serverShim);
if (usageCollection) {
this.initializerContext.config.legacy.globalConfig$
.pipe(first())
.toPromise()
.then(config => {
registerRollupUsageCollector(usageCollection, config.kibana.index);
})
.catch(e => {
this.log.warn(`Registering Rollup collector failed: ${e}`);
});
}
if (
serverShim.plugins.index_management &&
serverShim.plugins.index_management.addIndexManagementDataEnricher
) {
serverShim.plugins.index_management.addIndexManagementDataEnricher(rollupDataEnricher);
}
if (metrics) {
const { addSearchStrategy } = metrics;
registerRollupSearchStrategy(routeDependencies, addSearchStrategy);
}
}
start() {}
stop() {}
}

View file

@ -4,14 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const rollupDataEnricher = async (indicesList, callWithRequest) => {
interface Index {
name: string;
[key: string]: unknown;
}
export const rollupDataEnricher = async (indicesList: Index[], callWithRequest: any) => {
if (!indicesList || !indicesList.length) {
return indicesList;
}
const params = {
path: '/_all/_rollup/data',
method: 'GET',
};
try {
const rollupJobData = await callWithRequest('transport.request', params);
return indicesList.map(index => {
@ -22,7 +29,7 @@ export const rollupDataEnricher = async (indicesList, callWithRequest) => {
};
});
} catch (e) {
//swallow exceptions and return original list
// swallow exceptions and return original list
return indicesList;
}
};

View file

@ -1,93 +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 Joi from 'joi';
import { callWithRequestFactory } from '../../lib/call_with_request_factory';
import { isEsErrorFactory } from '../../lib/is_es_error_factory';
import { wrapEsError, wrapUnknownError } from '../../lib/error_wrappers';
import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory';
import indexBy from 'lodash/collection/indexBy';
import { getCapabilitiesForRollupIndices } from '../../lib/map_capabilities';
import { mergeCapabilitiesWithFields } from '../../lib/merge_capabilities_with_fields';
import querystring from 'querystring';
/**
* Get list of fields for rollup index pattern, in the format of regular index pattern fields
*/
export function registerFieldsForWildcardRoute(server) {
const isEsError = isEsErrorFactory(server);
const licensePreRouting = licensePreRoutingFactory(server);
server.route({
path: '/api/index_patterns/rollup/_fields_for_wildcard',
method: 'GET',
config: {
pre: [licensePreRouting],
validate: {
query: Joi.object()
.keys({
pattern: Joi.string().required(),
meta_fields: Joi.array()
.items(Joi.string())
.default([]),
params: Joi.object()
.keys({
rollup_index: Joi.string().required(),
})
.required(),
})
.default(),
},
},
handler: async request => {
const { pattern, meta_fields: metaFields, params } = request.query;
// Format call to standard index pattern `fields for wildcard`
const standardRequestQuery = querystring.stringify({ pattern, meta_fields: metaFields });
const standardRequest = {
url: `${request.getBasePath()}/api/index_patterns/_fields_for_wildcard?${standardRequestQuery}`,
method: 'GET',
headers: request.headers,
};
try {
// Make call and use field information from response
const standardResponse = await server.inject(standardRequest);
const fields = standardResponse.result && standardResponse.result.fields;
const rollupIndex = params.rollup_index;
const callWithRequest = callWithRequestFactory(server, request);
const rollupFields = [];
const fieldsFromFieldCapsApi = indexBy(fields, 'name');
const rollupIndexCapabilities = getCapabilitiesForRollupIndices(
await callWithRequest('rollup.rollupIndexCapabilities', {
indexPattern: rollupIndex,
})
)[rollupIndex].aggs;
// Keep meta fields
metaFields.forEach(
field => fieldsFromFieldCapsApi[field] && rollupFields.push(fieldsFromFieldCapsApi[field])
);
const mergedRollupFields = mergeCapabilitiesWithFields(
rollupIndexCapabilities,
fieldsFromFieldCapsApi,
rollupFields
);
return {
fields: mergedRollupFields,
};
} catch (err) {
if (isEsError(err)) {
return wrapEsError(err);
}
return wrapUnknownError(err);
}
},
});
}

View file

@ -0,0 +1,131 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { RequestHandler } from 'src/core/server';
import { indexBy } from 'lodash';
import { IndexPatternsFetcher } from '../../shared_imports';
import { RouteDependencies, ServerShim } from '../../types';
import { callWithRequestFactory } from '../../lib/call_with_request_factory';
import { isEsError } from '../../lib/is_es_error';
import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory';
import { getCapabilitiesForRollupIndices } from '../../lib/map_capabilities';
import { mergeCapabilitiesWithFields, Field } from '../../lib/merge_capabilities_with_fields';
const parseMetaFields = (metaFields: string | string[]) => {
let parsedFields: string[] = [];
if (typeof metaFields === 'string') {
parsedFields = JSON.parse(metaFields);
} else {
parsedFields = metaFields;
}
return parsedFields;
};
const getFieldsForWildcardRequest = async (context: any, request: any, response: any) => {
const { callAsCurrentUser } = context.core.elasticsearch.dataClient;
const indexPatterns = new IndexPatternsFetcher(callAsCurrentUser);
const { pattern, meta_fields: metaFields } = request.query;
let parsedFields: string[] = [];
try {
parsedFields = parseMetaFields(metaFields);
} catch (error) {
return response.badRequest({
body: error,
});
}
try {
const fields = await indexPatterns.getFieldsForWildcard({
pattern,
metaFields: parsedFields,
});
return response.ok({
body: { fields },
headers: {
'content-type': 'application/json',
},
});
} catch (error) {
return response.notFound();
}
};
/**
* Get list of fields for rollup index pattern, in the format of regular index pattern fields
*/
export function registerFieldsForWildcardRoute(deps: RouteDependencies, legacy: ServerShim) {
const handler: RequestHandler<any, any, any> = async (ctx, request, response) => {
const { params, meta_fields: metaFields } = request.query;
try {
// Make call and use field information from response
const { payload } = await getFieldsForWildcardRequest(ctx, request, response);
const fields = payload.fields;
const parsedParams = JSON.parse(params);
const rollupIndex = parsedParams.rollup_index;
const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request);
const rollupFields: Field[] = [];
const fieldsFromFieldCapsApi: { [key: string]: any } = indexBy(fields, 'name');
const rollupIndexCapabilities = getCapabilitiesForRollupIndices(
await callWithRequest('rollup.rollupIndexCapabilities', {
indexPattern: rollupIndex,
})
)[rollupIndex].aggs;
// Keep meta fields
metaFields.forEach(
(field: string) =>
fieldsFromFieldCapsApi[field] && rollupFields.push(fieldsFromFieldCapsApi[field])
);
const mergedRollupFields = mergeCapabilitiesWithFields(
rollupIndexCapabilities,
fieldsFromFieldCapsApi,
rollupFields
);
return response.ok({ body: { fields: mergedRollupFields } });
} catch (err) {
if (isEsError(err)) {
return response.customError({ statusCode: err.statusCode, body: err });
}
return response.internalError({ body: err });
}
};
deps.router.get(
{
path: '/api/index_patterns/rollup/_fields_for_wildcard',
validate: {
query: schema.object({
pattern: schema.string(),
meta_fields: schema.arrayOf(schema.string(), {
defaultValue: [],
}),
params: schema.string({
validate(value) {
try {
const params = JSON.parse(value);
const keys = Object.keys(params);
const { rollup_index: rollupIndex } = params;
if (!rollupIndex) {
return '[request query.params]: "rollup_index" is required';
} else if (keys.length > 1) {
const invalidParams = keys.filter(key => key !== 'rollup_index');
return `[request query.params]: ${invalidParams.join(', ')} is not allowed`;
}
} catch (err) {
return '[request query.params]: expected JSON string';
}
},
}),
}),
},
},
licensePreRoutingFactory(legacy, handler)
);
}

View file

@ -1,128 +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 { callWithRequestFactory } from '../../lib/call_with_request_factory';
import { isEsErrorFactory } from '../../lib/is_es_error_factory';
import { wrapEsError, wrapUnknownError } from '../../lib/error_wrappers';
import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory';
import { getCapabilitiesForRollupIndices } from '../../lib/map_capabilities';
function isNumericField(fieldCapability) {
const numericTypes = [
'long',
'integer',
'short',
'byte',
'double',
'float',
'half_float',
'scaled_float',
];
return numericTypes.some(numericType => fieldCapability[numericType] != null);
}
export function registerIndicesRoute(server) {
const isEsError = isEsErrorFactory(server);
const licensePreRouting = licensePreRoutingFactory(server);
/**
* Returns a list of all rollup index names
*/
server.route({
path: '/api/rollup/indices',
method: 'GET',
config: {
pre: [licensePreRouting],
},
handler: async request => {
const callWithRequest = callWithRequestFactory(server, request);
try {
const data = await callWithRequest('rollup.rollupIndexCapabilities', {
indexPattern: '_all',
});
return getCapabilitiesForRollupIndices(data);
} catch (err) {
if (isEsError(err)) {
return wrapEsError(err);
}
return wrapUnknownError(err);
}
},
});
/**
* Returns information on validity of an index pattern for creating a rollup job:
* - Does the index pattern match any indices?
* - Does the index pattern match rollup indices?
* - Which date fields, numeric fields, and keyword fields are available in the matching indices?
*/
server.route({
path: '/api/rollup/index_pattern_validity/{indexPattern}',
method: 'GET',
config: {
pre: [licensePreRouting],
},
handler: async request => {
const callWithRequest = callWithRequestFactory(server, request);
try {
const { indexPattern } = request.params;
const [fieldCapabilities, rollupIndexCapabilities] = await Promise.all([
callWithRequest('rollup.fieldCapabilities', { indexPattern }),
callWithRequest('rollup.rollupIndexCapabilities', { indexPattern }),
]);
const doesMatchIndices = Object.entries(fieldCapabilities.fields).length !== 0;
const doesMatchRollupIndices = Object.entries(rollupIndexCapabilities).length !== 0;
const dateFields = [];
const numericFields = [];
const keywordFields = [];
const fieldCapabilitiesEntries = Object.entries(fieldCapabilities.fields);
fieldCapabilitiesEntries.forEach(([fieldName, fieldCapability]) => {
if (fieldCapability.date) {
dateFields.push(fieldName);
return;
}
if (isNumericField(fieldCapability)) {
numericFields.push(fieldName);
return;
}
if (fieldCapability.keyword) {
keywordFields.push(fieldName);
}
});
return {
doesMatchIndices,
doesMatchRollupIndices,
dateFields,
numericFields,
keywordFields,
};
} catch (err) {
// 404s are still valid results.
if (err.statusCode === 404) {
return {
doesMatchIndices: false,
doesMatchRollupIndices: false,
dateFields: [],
numericFields: [],
keywordFields: [],
};
}
if (isEsError(err)) {
return wrapEsError(err);
}
return wrapUnknownError(err);
}
},
});
}

View file

@ -0,0 +1,175 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { RequestHandler } from 'src/core/server';
import { callWithRequestFactory } from '../../lib/call_with_request_factory';
import { isEsError } from '../../lib/is_es_error';
import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory';
import { getCapabilitiesForRollupIndices } from '../../lib/map_capabilities';
import { API_BASE_PATH } from '../../../common';
import { RouteDependencies, ServerShim } from '../../types';
type NumericField =
| 'long'
| 'integer'
| 'short'
| 'byte'
| 'scaled_float'
| 'double'
| 'float'
| 'half_float';
interface FieldCapability {
date?: any;
keyword?: any;
long?: any;
integer?: any;
short?: any;
byte?: any;
double?: any;
float?: any;
half_float?: any;
scaled_float?: any;
}
interface FieldCapabilities {
fields: FieldCapability[];
}
function isNumericField(fieldCapability: FieldCapability) {
const numericTypes = [
'long',
'integer',
'short',
'byte',
'double',
'float',
'half_float',
'scaled_float',
];
return numericTypes.some(numericType => fieldCapability[numericType as NumericField] != null);
}
export function registerIndicesRoute(deps: RouteDependencies, legacy: ServerShim) {
const getIndicesHandler: RequestHandler<any, any, any> = async (ctx, request, response) => {
const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request);
try {
const data = await callWithRequest('rollup.rollupIndexCapabilities', {
indexPattern: '_all',
});
return response.ok({ body: getCapabilitiesForRollupIndices(data) });
} catch (err) {
if (isEsError(err)) {
return response.customError({ statusCode: err.statusCode, body: err });
}
return response.internalError({ body: err });
}
};
const validateIndexPatternHandler: RequestHandler<any, any, any> = async (
ctx,
request,
response
) => {
const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request);
try {
const { indexPattern } = request.params;
const [fieldCapabilities, rollupIndexCapabilities]: [
FieldCapabilities,
{ [key: string]: any }
] = await Promise.all([
callWithRequest('rollup.fieldCapabilities', { indexPattern }),
callWithRequest('rollup.rollupIndexCapabilities', { indexPattern }),
]);
const doesMatchIndices = Object.entries(fieldCapabilities.fields).length !== 0;
const doesMatchRollupIndices = Object.entries(rollupIndexCapabilities).length !== 0;
const dateFields: string[] = [];
const numericFields: string[] = [];
const keywordFields: string[] = [];
const fieldCapabilitiesEntries = Object.entries(fieldCapabilities.fields);
fieldCapabilitiesEntries.forEach(
([fieldName, fieldCapability]: [string, FieldCapability]) => {
if (fieldCapability.date) {
dateFields.push(fieldName);
return;
}
if (isNumericField(fieldCapability)) {
numericFields.push(fieldName);
return;
}
if (fieldCapability.keyword) {
keywordFields.push(fieldName);
}
}
);
const body = {
doesMatchIndices,
doesMatchRollupIndices,
dateFields,
numericFields,
keywordFields,
};
return response.ok({ body });
} catch (err) {
// 404s are still valid results.
if (err.statusCode === 404) {
const notFoundBody = {
doesMatchIndices: false,
doesMatchRollupIndices: false,
dateFields: [],
numericFields: [],
keywordFields: [],
};
return response.ok({ body: notFoundBody });
}
if (isEsError(err)) {
return response.customError({ statusCode: err.statusCode, body: err });
}
return response.internalError({ body: err });
}
};
/**
* Returns a list of all rollup index names
*/
deps.router.get(
{
path: `${API_BASE_PATH}/indices`,
validate: false,
},
licensePreRoutingFactory(legacy, getIndicesHandler)
);
/**
* Returns information on validity of an index pattern for creating a rollup job:
* - Does the index pattern match any indices?
* - Does the index pattern match rollup indices?
* - Which date fields, numeric fields, and keyword fields are available in the matching indices?
*/
deps.router.get(
{
path: `${API_BASE_PATH}/index_pattern_validity/{indexPattern}`,
validate: {
params: schema.object({
indexPattern: schema.string(),
}),
},
},
licensePreRoutingFactory(legacy, validateIndexPatternHandler)
);
}

View file

@ -1,153 +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 { callWithRequestFactory } from '../../lib/call_with_request_factory';
import { isEsErrorFactory } from '../../lib/is_es_error_factory';
import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory';
import { wrapEsError, wrapUnknownError } from '../../lib/error_wrappers';
export function registerJobsRoute(server) {
const isEsError = isEsErrorFactory(server);
const licensePreRouting = licensePreRoutingFactory(server);
server.route({
path: '/api/rollup/jobs',
method: 'GET',
config: {
pre: [licensePreRouting],
},
handler: async request => {
try {
const callWithRequest = callWithRequestFactory(server, request);
return await callWithRequest('rollup.jobs');
} catch (err) {
if (isEsError(err)) {
return wrapEsError(err);
}
return wrapUnknownError(err);
}
},
});
server.route({
path: '/api/rollup/create',
method: 'PUT',
config: {
pre: [licensePreRouting],
},
handler: async request => {
try {
const { id, ...rest } = request.payload.job;
const callWithRequest = callWithRequestFactory(server, request);
// Create job.
await callWithRequest('rollup.createJob', {
id,
body: rest,
});
// Then request the newly created job.
const results = await callWithRequest('rollup.job', { id });
return results.jobs[0];
} catch (err) {
if (isEsError(err)) {
return wrapEsError(err);
}
return wrapUnknownError(err);
}
},
});
server.route({
path: '/api/rollup/start',
method: 'POST',
config: {
pre: [licensePreRouting],
},
handler: async request => {
try {
const { jobIds } = request.payload;
const callWithRequest = callWithRequestFactory(server, request);
return await Promise.all(
jobIds.map(id => callWithRequest('rollup.startJob', { id }))
).then(() => ({ success: true }));
} catch (err) {
if (isEsError(err)) {
return wrapEsError(err);
}
return wrapUnknownError(err);
}
},
});
server.route({
path: '/api/rollup/stop',
method: 'POST',
config: {
pre: [licensePreRouting],
},
handler: async request => {
try {
const { jobIds } = request.payload;
// For our API integration tests we need to wait for the jobs to be stopped
// in order to be able to delete them sequencially.
const { waitForCompletion } = request.query;
const callWithRequest = callWithRequestFactory(server, request);
const stopRollupJob = id =>
callWithRequest('rollup.stopJob', {
id,
waitForCompletion: waitForCompletion === 'true',
});
return await Promise.all(jobIds.map(stopRollupJob)).then(() => ({ success: true }));
} catch (err) {
if (isEsError(err)) {
return wrapEsError(err);
}
return wrapUnknownError(err);
}
},
});
server.route({
path: '/api/rollup/delete',
method: 'POST',
config: {
pre: [licensePreRouting],
},
handler: async request => {
try {
const { jobIds } = request.payload;
const callWithRequest = callWithRequestFactory(server, request);
return await Promise.all(
jobIds.map(id => callWithRequest('rollup.deleteJob', { id }))
).then(() => ({ success: true }));
} catch (err) {
// There is an issue opened on ES to handle the following error correctly
// https://github.com/elastic/elasticsearch/issues/42908
// Until then we'll modify the response here.
if (err.response && err.response.includes('Job must be [STOPPED] before deletion')) {
err.status = 400;
err.statusCode = 400;
err.displayName = 'Bad request';
err.message = JSON.parse(err.response).task_failures[0].reason.reason;
}
if (isEsError(err)) {
throw wrapEsError(err);
}
throw wrapUnknownError(err);
}
},
});
}

View file

@ -0,0 +1,178 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { RequestHandler } from 'src/core/server';
import { callWithRequestFactory } from '../../lib/call_with_request_factory';
import { isEsError } from '../../lib/is_es_error';
import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory';
import { API_BASE_PATH } from '../../../common';
import { RouteDependencies, ServerShim } from '../../types';
export function registerJobsRoute(deps: RouteDependencies, legacy: ServerShim) {
const getJobsHandler: RequestHandler<any, any, any> = async (ctx, request, response) => {
const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request);
try {
const data = await callWithRequest('rollup.jobs');
return response.ok({ body: data });
} catch (err) {
if (isEsError(err)) {
return response.customError({ statusCode: err.statusCode, body: err });
}
return response.internalError({ body: err });
}
};
const createJobsHandler: RequestHandler<any, any, any> = async (ctx, request, response) => {
try {
const { id, ...rest } = request.body.job;
const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request);
// Create job.
await callWithRequest('rollup.createJob', {
id,
body: rest,
});
// Then request the newly created job.
const results = await callWithRequest('rollup.job', { id });
return response.ok({ body: results.jobs[0] });
} catch (err) {
if (isEsError(err)) {
return response.customError({ statusCode: err.statusCode, body: err });
}
return response.internalError({ body: err });
}
};
const startJobsHandler: RequestHandler<any, any, any> = async (ctx, request, response) => {
try {
const { jobIds } = request.body;
const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request);
const data = await Promise.all(
jobIds.map((id: string) => callWithRequest('rollup.startJob', { id }))
).then(() => ({ success: true }));
return response.ok({ body: data });
} catch (err) {
if (isEsError(err)) {
return response.customError({ statusCode: err.statusCode, body: err });
}
return response.internalError({ body: err });
}
};
const stopJobsHandler: RequestHandler<any, any, any> = async (ctx, request, response) => {
try {
const { jobIds } = request.body;
// For our API integration tests we need to wait for the jobs to be stopped
// in order to be able to delete them sequencially.
const { waitForCompletion } = request.query;
const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request);
const stopRollupJob = (id: string) =>
callWithRequest('rollup.stopJob', {
id,
waitForCompletion: waitForCompletion === 'true',
});
const data = await Promise.all(jobIds.map(stopRollupJob)).then(() => ({ success: true }));
return response.ok({ body: data });
} catch (err) {
if (isEsError(err)) {
return response.customError({ statusCode: err.statusCode, body: err });
}
return response.internalError({ body: err });
}
};
const deleteJobsHandler: RequestHandler<any, any, any> = async (ctx, request, response) => {
try {
const { jobIds } = request.body;
const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request);
const data = await Promise.all(
jobIds.map((id: string) => callWithRequest('rollup.deleteJob', { id }))
).then(() => ({ success: true }));
return response.ok({ body: data });
} catch (err) {
// There is an issue opened on ES to handle the following error correctly
// https://github.com/elastic/elasticsearch/issues/42908
// Until then we'll modify the response here.
if (err.response && err.response.includes('Job must be [STOPPED] before deletion')) {
err.status = 400;
err.statusCode = 400;
err.displayName = 'Bad request';
err.message = JSON.parse(err.response).task_failures[0].reason.reason;
}
if (isEsError(err)) {
return response.customError({ statusCode: err.statusCode, body: err });
}
return response.internalError({ body: err });
}
};
deps.router.get(
{
path: `${API_BASE_PATH}/jobs`,
validate: false,
},
licensePreRoutingFactory(legacy, getJobsHandler)
);
deps.router.put(
{
path: `${API_BASE_PATH}/create`,
validate: {
body: schema.object({
job: schema.object(
{
id: schema.string(),
},
{ allowUnknowns: true }
),
}),
},
},
licensePreRoutingFactory(legacy, createJobsHandler)
);
deps.router.post(
{
path: `${API_BASE_PATH}/start`,
validate: {
body: schema.object({
jobIds: schema.arrayOf(schema.string()),
}),
query: schema.maybe(
schema.object({
waitForCompletion: schema.maybe(schema.string()),
})
),
},
},
licensePreRoutingFactory(legacy, startJobsHandler)
);
deps.router.post(
{
path: `${API_BASE_PATH}/stop`,
validate: {
body: schema.object({
jobIds: schema.arrayOf(schema.string()),
}),
},
},
licensePreRoutingFactory(legacy, stopJobsHandler)
);
deps.router.post(
{
path: `${API_BASE_PATH}/delete`,
validate: {
body: schema.object({
jobIds: schema.arrayOf(schema.string()),
}),
},
},
licensePreRoutingFactory(legacy, deleteJobsHandler)
);
}

View file

@ -1,44 +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 { callWithRequestFactory } from '../../lib/call_with_request_factory';
import { isEsErrorFactory } from '../../lib/is_es_error_factory';
import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory';
import { wrapEsError, wrapUnknownError } from '../../lib/error_wrappers';
export function registerSearchRoute(server) {
const isEsError = isEsErrorFactory(server);
const licensePreRouting = licensePreRoutingFactory(server);
server.route({
path: '/api/rollup/search',
method: 'POST',
config: {
pre: [licensePreRouting],
},
handler: async request => {
const callWithRequest = callWithRequestFactory(server, request);
try {
const requests = request.payload.map(({ index, query }) =>
callWithRequest('rollup.search', {
index,
rest_total_hits_as_int: true,
body: query,
})
);
return await Promise.all(requests);
} catch (err) {
if (isEsError(err)) {
return wrapEsError(err);
}
return wrapUnknownError(err);
}
},
});
}

View file

@ -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 { schema } from '@kbn/config-schema';
import { RequestHandler } from 'src/core/server';
import { callWithRequestFactory } from '../../lib/call_with_request_factory';
import { isEsError } from '../../lib/is_es_error';
import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory';
import { API_BASE_PATH } from '../../../common';
import { RouteDependencies, ServerShim } from '../../types';
export function registerSearchRoute(deps: RouteDependencies, legacy: ServerShim) {
const handler: RequestHandler<any, any, any> = async (ctx, request, response) => {
const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request);
try {
const requests = request.body.map(({ index, query }: { index: string; query: any }) =>
callWithRequest('rollup.search', {
index,
rest_total_hits_as_int: true,
body: query,
})
);
const data = await Promise.all(requests);
return response.ok({ body: data });
} catch (err) {
if (isEsError(err)) {
return response.customError({ statusCode: err.statusCode, body: err });
}
return response.internalError({ body: err });
}
};
deps.router.post(
{
path: `${API_BASE_PATH}/search`,
validate: {
body: schema.arrayOf(
schema.object({
index: schema.string(),
query: schema.any(),
})
),
},
},
licensePreRoutingFactory(legacy, handler)
);
}

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { registerLicenseChecker } from './register_license_checker';
export { IndexPatternsFetcher } from '../../../../../src/plugins/data/server';

View file

@ -0,0 +1,22 @@
/*
* 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 { IRouter, ElasticsearchServiceSetup, IClusterClient } from 'src/core/server';
import { XPackMainPlugin } from '../../xpack_main/server/xpack_main';
export interface ServerShim {
plugins: {
xpack_main: XPackMainPlugin;
rollup: any;
index_management: any;
};
}
export interface RouteDependencies {
router: IRouter;
elasticsearchService: ElasticsearchServiceSetup;
elasticsearch: IClusterClient;
}

View file

@ -1,7 +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.
*/
export { registerRollupUsageCollector } from './collector';

View file

@ -0,0 +1,6 @@
{
"id": "rollup",
"version": "8.0.0",
"kibanaVersion": "kibana",
"server": true
}

View file

@ -0,0 +1,12 @@
/*
* 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 { PluginInitializerContext } from 'src/core/server';
import { RollupPlugin } from './plugin';
export const plugin = (initContext: PluginInitializerContext) => new RollupPlugin(initContext);
export { RollupSetup } from './plugin';

View file

@ -0,0 +1,35 @@
/*
* 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 { Plugin, PluginInitializerContext } from 'src/core/server';
export class RollupPlugin implements Plugin<RollupSetup> {
private readonly initContext: PluginInitializerContext;
constructor(initContext: PluginInitializerContext) {
this.initContext = initContext;
}
public setup() {
return {
__legacy: {
config: this.initContext.config,
logger: this.initContext.logger,
},
};
}
public start() {}
public stop() {}
}
export interface RollupSetup {
/** @deprecated */
__legacy: {
config: PluginInitializerContext['config'];
logger: PluginInitializerContext['logger'];
};
}

View file

@ -10253,9 +10253,6 @@
"xpack.reporting.shareContextMenu.csvReportsButtonLabel": "CSV レポート",
"xpack.reporting.shareContextMenu.pdfReportsButtonLabel": "PDF レポート",
"xpack.rollupJobs.appTitle": "ロールアップジョブ",
"xpack.rollupJobs.checkLicense.errorExpiredMessage": "{licenseType} ライセンスが期限切れのため {pluginName} を使用できません",
"xpack.rollupJobs.checkLicense.errorUnavailableMessage": "現在ライセンス情報が利用できないため {pluginName} を使用できません。",
"xpack.rollupJobs.checkLicense.errorUnsupportedMessage": "ご使用の {licenseType} ライセンスは {pluginName} をサポートしていません。ライセンスをアップグレードしてください。",
"xpack.rollupJobs.create.backButton.label": "戻る",
"xpack.rollupJobs.create.dateTypeField": "日付",
"xpack.rollupJobs.create.errors.dateHistogramFieldMissing": "日付フィールドが必要です。",

View file

@ -10252,9 +10252,6 @@
"xpack.reporting.shareContextMenu.csvReportsButtonLabel": "CSV 报告",
"xpack.reporting.shareContextMenu.pdfReportsButtonLabel": "PDF 报告",
"xpack.rollupJobs.appTitle": "汇总/打包作业",
"xpack.rollupJobs.checkLicense.errorExpiredMessage": "您不能使用 {pluginName},因为您的 {licenseType} 许可证已过期",
"xpack.rollupJobs.checkLicense.errorUnavailableMessage": "您不能使用 {pluginName},因为许可证信息当前不可用。",
"xpack.rollupJobs.checkLicense.errorUnsupportedMessage": "您的 {licenseType} 许可证不支持 {pluginName}。请升级您的许可。",
"xpack.rollupJobs.create.backButton.label": "上一步",
"xpack.rollupJobs.create.dateTypeField": "日期",
"xpack.rollupJobs.create.errors.dateHistogramFieldMissing": "“日期”字段必填。",

View file

@ -32,28 +32,42 @@ export default function({ getService }) {
it('"pattern" is required', async () => {
uri = `${BASE_URI}`;
({ body } = await supertest.get(uri).expect(400));
expect(body.message).to.contain('"pattern" is required');
expect(body.message).to.contain(
'[request query.pattern]: expected value of type [string]'
);
});
it('"params" is required', async () => {
params = { pattern: 'foo' };
uri = `${BASE_URI}?${querystring.stringify(params)}`;
({ body } = await supertest.get(uri).expect(400));
expect(body.message).to.contain('"params" is required');
expect(body.message).to.contain(
'[request query.params]: expected value of type [string]'
);
});
it('"params" must be an object', async () => {
params = { pattern: 'foo', params: 'bar' };
it('"params" must be a valid JSON string', async () => {
params = { pattern: 'foo', params: 'foobarbaz' };
uri = `${BASE_URI}?${querystring.stringify(params)}`;
({ body } = await supertest.get(uri).expect(400));
expect(body.message).to.contain('"params" must be an object');
expect(body.message).to.contain('[request query.params]: expected JSON string');
});
it('"params" must be an object that only accepts a "rollup_index" property', async () => {
params = { pattern: 'foo', params: JSON.stringify({ someProp: 'bar' }) };
it('"params" requires a "rollup_index" property', async () => {
params = { pattern: 'foo', params: JSON.stringify({}) };
uri = `${BASE_URI}?${querystring.stringify(params)}`;
({ body } = await supertest.get(uri).expect(400));
expect(body.message).to.contain('"someProp" is not allowed');
expect(body.message).to.contain('[request query.params]: "rollup_index" is required');
});
it('"params" only accepts a "rollup_index" property', async () => {
params = {
pattern: 'foo',
params: JSON.stringify({ rollup_index: 'my_index', someProp: 'bar' }),
};
uri = `${BASE_URI}?${querystring.stringify(params)}`;
({ body } = await supertest.get(uri).expect(400));
expect(body.message).to.contain('[request query.params]: someProp is not allowed');
});
it('"meta_fields" must be an Array', async () => {
@ -64,7 +78,9 @@ export default function({ getService }) {
};
uri = `${BASE_URI}?${querystring.stringify(params)}`;
({ body } = await supertest.get(uri).expect(400));
expect(body.message).to.contain('"meta_fields" must be an array');
expect(body.message).to.contain(
'[request query.meta_fields]: expected value of type [array]'
);
});
it('should return 404 the rollup index to query does not exist', async () => {
@ -73,7 +89,7 @@ export default function({ getService }) {
params: JSON.stringify({ rollup_index: 'bar' }),
})}`;
({ body } = await supertest.get(uri).expect(404));
expect(body.message).to.contain('no such index [bar]');
expect(body.message).to.contain('[index_not_found_exception] no such index [bar]');
});
});