* Move Rollup out of legacy (#62891) * Put back 7.x-specific logic.
This commit is contained in:
parent
1d9eab9aa6
commit
ecdbece5d5
|
@ -4,7 +4,6 @@ files:
|
|||
- 'src/legacy/core_plugins/timelion/**/*.s+(a|c)ss'
|
||||
- 'src/legacy/core_plugins/vis_type_vislib/**/*.s+(a|c)ss'
|
||||
- 'src/legacy/core_plugins/vis_type_xy/**/*.s+(a|c)ss'
|
||||
- 'x-pack/legacy/plugins/rollup/**/*.s+(a|c)ss'
|
||||
- 'x-pack/legacy/plugins/security/**/*.s+(a|c)ss'
|
||||
- 'x-pack/legacy/plugins/canvas/**/*.s+(a|c)ss'
|
||||
- 'x-pack/plugins/triggers_actions_ui/**/*.s+(a|c)ss'
|
||||
|
|
|
@ -17,7 +17,6 @@ import { spaces } from './legacy/plugins/spaces';
|
|||
import { canvas } from './legacy/plugins/canvas';
|
||||
import { infra } from './legacy/plugins/infra';
|
||||
import { taskManager } from './legacy/plugins/task_manager';
|
||||
import { rollup } from './legacy/plugins/rollup';
|
||||
import { siem } from './legacy/plugins/siem';
|
||||
import { remoteClusters } from './legacy/plugins/remote_clusters';
|
||||
import { upgradeAssistant } from './legacy/plugins/upgrade_assistant';
|
||||
|
@ -43,7 +42,6 @@ module.exports = function(kibana) {
|
|||
indexManagement(kibana),
|
||||
infra(kibana),
|
||||
taskManager(kibana),
|
||||
rollup(kibana),
|
||||
siem(kibana),
|
||||
remoteClusters(kibana),
|
||||
upgradeAssistant(kibana),
|
||||
|
|
|
@ -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 { 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 * from '../../../../plugins/rollup/common';
|
|
@ -1,43 +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 { PluginInitializerContext } from 'src/core/server';
|
||||
import { RollupSetup } from '../../../plugins/rollup/server';
|
||||
import { PLUGIN } from './common';
|
||||
import { plugin } from './server';
|
||||
|
||||
export function rollup(kibana: any) {
|
||||
return new kibana.Plugin({
|
||||
id: PLUGIN.ID,
|
||||
configPrefix: 'xpack.rollup',
|
||||
require: ['kibana', 'elasticsearch', 'xpack_main'],
|
||||
init(server: any) {
|
||||
const { core: coreSetup, plugins } = server.newPlatform.setup;
|
||||
const { usageCollection, visTypeTimeseries, indexManagement } = plugins;
|
||||
|
||||
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,
|
||||
visTypeTimeseries,
|
||||
indexManagement,
|
||||
__LEGACY: {
|
||||
plugins: {
|
||||
xpack_main: server.plugins.xpack_main,
|
||||
rollup: server.plugins[PLUGIN.ID],
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"id": "rollup",
|
||||
"version": "kibana",
|
||||
"requiredPlugins": [
|
||||
"home",
|
||||
"index_management",
|
||||
"visTypeTimeseries",
|
||||
"indexPatternManagement"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"usageCollection"
|
||||
],
|
||||
"server": true,
|
||||
"ui": false
|
||||
}
|
|
@ -1,98 +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 const jobs = [
|
||||
{
|
||||
"job_id" : "foo1",
|
||||
"rollup_index" : "foo_rollup",
|
||||
"index_pattern" : "foo-*",
|
||||
"fields" : {
|
||||
"node" : [
|
||||
{
|
||||
"agg" : "terms"
|
||||
}
|
||||
],
|
||||
"temperature" : [
|
||||
{
|
||||
"agg" : "min"
|
||||
},
|
||||
{
|
||||
"agg" : "max"
|
||||
},
|
||||
{
|
||||
"agg" : "sum"
|
||||
}
|
||||
],
|
||||
"timestamp" : [
|
||||
{
|
||||
"agg" : "date_histogram",
|
||||
"time_zone" : "UTC",
|
||||
"interval" : "1h",
|
||||
"delay": "7d"
|
||||
}
|
||||
],
|
||||
"voltage" : [
|
||||
{
|
||||
"agg" : "histogram",
|
||||
"interval": 5
|
||||
},
|
||||
{
|
||||
"agg" : "sum"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"job_id" : "foo2",
|
||||
"rollup_index" : "foo_rollup",
|
||||
"index_pattern" : "foo-*",
|
||||
"fields" : {
|
||||
"host" : [
|
||||
{
|
||||
"agg" : "terms"
|
||||
}
|
||||
],
|
||||
"timestamp" : [
|
||||
{
|
||||
"agg" : "date_histogram",
|
||||
"time_zone" : "UTC",
|
||||
"interval" : "1h",
|
||||
"delay": "7d"
|
||||
}
|
||||
],
|
||||
"voltage" : [
|
||||
{
|
||||
"agg" : "histogram",
|
||||
"interval": 20
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"job_id" : "foo3",
|
||||
"rollup_index" : "foo_rollup",
|
||||
"index_pattern" : "foo-*",
|
||||
"fields" : {
|
||||
"timestamp" : [
|
||||
{
|
||||
"agg" : "date_histogram",
|
||||
"time_zone" : "PST",
|
||||
"interval" : "1h",
|
||||
"delay": "7d"
|
||||
}
|
||||
],
|
||||
"voltage" : [
|
||||
{
|
||||
"agg" : "histogram",
|
||||
"interval": 5
|
||||
},
|
||||
{
|
||||
"agg" : "sum"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
];
|
|
@ -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 { 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)
|
||||
);
|
||||
};
|
||||
};
|
|
@ -1,62 +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 '.';
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,43 +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 {
|
||||
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);
|
||||
};
|
||||
};
|
|
@ -1,95 +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 { 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 { IndexManagementPluginSetup } from '../../../../plugins/index_management/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,
|
||||
visTypeTimeseries,
|
||||
indexManagement,
|
||||
}: {
|
||||
__LEGACY: ServerShim;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
visTypeTimeseries?: VisTypeTimeseriesSetup;
|
||||
indexManagement?: IndexManagementPluginSetup;
|
||||
}
|
||||
) {
|
||||
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 (indexManagement && indexManagement.indexDataEnricher) {
|
||||
indexManagement.indexDataEnricher.add(rollupDataEnricher);
|
||||
}
|
||||
|
||||
if (visTypeTimeseries) {
|
||||
const { addSearchStrategy } = visTypeTimeseries;
|
||||
registerRollupSearchStrategy(routeDependencies, addSearchStrategy);
|
||||
}
|
||||
}
|
||||
|
||||
start() {}
|
||||
|
||||
stop() {}
|
||||
}
|
|
@ -1,10 +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 { registerIndicesRoute } from './indices';
|
||||
export { registerFieldsForWildcardRoute } from './index_patterns';
|
||||
export { registerSearchRoute } from './search';
|
||||
export { registerJobsRoute } from './jobs';
|
|
@ -1,131 +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 { 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)
|
||||
);
|
||||
}
|
|
@ -1,175 +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 { 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)
|
||||
);
|
||||
}
|
|
@ -1,188 +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 { 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) {
|
||||
// There is an issue opened on ES to handle the following error correctly
|
||||
// https://github.com/elastic/elasticsearch/issues/39845
|
||||
// Until then we'll modify the response here.
|
||||
if (err.message.includes('Cannot start task for Rollup Job')) {
|
||||
err.status = 400;
|
||||
err.statusCode = 400;
|
||||
err.body.error.status = 400;
|
||||
err.displayName = 'Bad request';
|
||||
}
|
||||
|
||||
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(),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
),
|
||||
}),
|
||||
},
|
||||
},
|
||||
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)
|
||||
);
|
||||
}
|
|
@ -1,50 +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 { 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)
|
||||
);
|
||||
}
|
|
@ -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 { IRouter, ElasticsearchServiceSetup, IClusterClient } from 'src/core/server';
|
||||
import { XPackMainPlugin } from '../../xpack_main/server/xpack_main';
|
||||
|
||||
export interface ServerShim {
|
||||
plugins: {
|
||||
xpack_main: XPackMainPlugin;
|
||||
rollup: any;
|
||||
};
|
||||
}
|
||||
|
||||
export interface RouteDependencies {
|
||||
router: IRouter;
|
||||
elasticsearchService: ElasticsearchServiceSetup;
|
||||
elasticsearch: IClusterClient;
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.json"
|
||||
}
|
|
@ -14,7 +14,7 @@ The rest of this doc dives into the implementation details of each of the above
|
|||
|
||||
## Create and manage rollup jobs
|
||||
|
||||
The most straight forward part of this plugin! A new app called Rollup Jobs is registered in the Management section and follows a typical CRUD UI pattern. This app allows users to create, start, stop, clone, and delete rollup jobs. There is no way to edit an existing rollup job; instead, the UI offers a cloning ability. The client-side portion of this app lives [here](../../../plugins/rollup/public/crud_app) and uses endpoints registered [here](server/routes/api/jobs.js).
|
||||
The most straight forward part of this plugin! A new app called Rollup Jobs is registered in the Management section and follows a typical CRUD UI pattern. This app allows users to create, start, stop, clone, and delete rollup jobs. There is no way to edit an existing rollup job; instead, the UI offers a cloning ability. The client-side portion of this app lives in [public/crud_app](public/crud_app) and uses endpoints registered in [(server/routes/api/jobs](server/routes/api/jobs).
|
||||
|
||||
Refer to the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-getting-started.html) to understand rollup indices and how to create rollup jobs.
|
||||
|
||||
|
@ -22,22 +22,22 @@ Refer to the [Elasticsearch documentation](https://www.elastic.co/guide/en/elast
|
|||
|
||||
Kibana uses index patterns to consume and visualize rollup indices. Typically, Kibana can inspect the indices captured by an index pattern, identify its aggregations and fields, and determine how to consume the data. Rollup indices don't contain this type of information, so we predefine how to consume a rollup index pattern with the type and typeMeta fields on the index pattern saved object. All rollup index patterns have `type` defined as "rollup" and `typeMeta` defined as an object of the index pattern's capabilities.
|
||||
|
||||
In the Index Pattern app, the "Create index pattern" button includes a context menu when a rollup index is detected. This menu offers items for creating a standard index pattern and a rollup index pattern. A [rollup config is registered to index pattern creation extension point](../../../plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js). The context menu behavior in particular uses the `getIndexPatternCreationOption()` method. When the user chooses to create a rollup index pattern, this config changes the behavior of the index pattern creation wizard:
|
||||
In the Index Pattern app, the "Create index pattern" button includes a context menu when a rollup index is detected. This menu offers items for creating a standard index pattern and a rollup index pattern. A [rollup config is registered to index pattern creation extension point](public/index_pattern_creation/rollup_index_pattern_creation_config.js). The context menu behavior in particular uses the `getIndexPatternCreationOption()` method. When the user chooses to create a rollup index pattern, this config changes the behavior of the index pattern creation wizard:
|
||||
|
||||
1. Adds a `Rollup` badge to rollup indices using `getIndexTags()`.
|
||||
2. Enforces index pattern rules using `checkIndicesForErrors()`. Rollup index patterns must match **one** rollup index, and optionally, any number of regular indices. A rollup index pattern configured with one or more regular indices is known as a "hybrid" index pattern. This allows the user to visualize historical (rollup) data and live (regular) data in the same visualization.
|
||||
3. Routes to this plugin's [rollup `_fields_for_wildcard` endpoint](server/routes/api/index_patterns.js), instead of the standard one, using `getFetchForWildcardOptions()`, so that the internal rollup data field names are mapped to the original field names.
|
||||
3. Routes to this plugin's [rollup `_fields_for_wildcard` endpoint](server/routes/api/index_patterns/register_fields_for_wildcard_route.ts), instead of the standard one, using `getFetchForWildcardOptions()`, so that the internal rollup data field names are mapped to the original field names.
|
||||
4. Writes additional information about aggregations, fields, histogram interval, and date histogram interval and timezone to the rollup index pattern saved object using `getIndexPatternMappings()`. This collection of information is referred to as its "capabilities".
|
||||
|
||||
Once a rollup index pattern is created, it is tagged with `Rollup` in the list of index patterns, and its details page displays capabilities information. This is done by registering [yet another config for the index pattern list](../../../plugins/rollup/public/index_pattern_list/rollup_index_pattern_list_config.js) extension points.
|
||||
Once a rollup index pattern is created, it is tagged with `Rollup` in the list of index patterns, and its details page displays capabilities information. This is done by registering [yet another config for the index pattern list](public/index_pattern_list/rollup_index_pattern_list_config.js) extension points.
|
||||
|
||||
## Create visualizations from rollup index patterns
|
||||
|
||||
This plugin enables the user to create visualizations from rollup data using the Visualize app, excluding TSVB, Vega, and Timelion. When Visualize sends search requests, this plugin routes the requests to the [Elasticsearch rollup search endpoint](https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-search.html), which searches the special document structure within rollup indices. The visualization options available to users are based on the capabilities of the rollup index pattern they're visualizing.
|
||||
|
||||
Routing to the Elasticsearch rollup search endpoint is done by creating an extension point in Courier, effectively allowing multiple "search strategies" to be registered. A [rollup search strategy](../../../plugins/rollup/public/search/register.js) is registered by this plugin that queries [this plugin's rollup search endpoint](server/routes/api/search.js).
|
||||
Routing to the Elasticsearch rollup search endpoint is done by creating an extension point in Courier, effectively allowing multiple "search strategies" to be registered. A [rollup search strategy](public/search/register.js) is registered by this plugin that queries [this plugin's rollup search endpoint](server/routes/api/search.js).
|
||||
|
||||
Limiting visualization editor options is done by [registering configs](../../../plugins/rollup/public/visualize/index.js) to various vis extension points. These configs use information stored on the rollup index pattern to limit:
|
||||
Limiting visualization editor options is done by [registering configs](public/visualize/index.js) to various vis extension points. These configs use information stored on the rollup index pattern to limit:
|
||||
* Available aggregation types
|
||||
* Available fields for a particular aggregation
|
||||
* Default and base interval for histogram aggregation
|
||||
|
@ -47,6 +47,6 @@ Limiting visualization editor options is done by [registering configs](../../../
|
|||
|
||||
In Index Management, similar to system indices, rollup indices are hidden by default. A toggle is provided to show rollup indices and add a badge to the table rows. This is done by using Index Management's extension points.
|
||||
|
||||
The toggle and badge are registered on client-side [here](../../../plugins/rollup/public/extend_index_management/index.js).
|
||||
The toggle and badge are registered on the client-side in [public/extend_index_management](public/extend_index_management).
|
||||
|
||||
Additional data needed to filter rollup indices in Index Management is provided with a [data enricher](rollup_data_enricher.js).
|
||||
Additional data needed to filter rollup indices in Index Management is provided with a [data enricher](rollup_data_enricher.ts).
|
|
@ -4,6 +4,15 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { LicenseType } from '../../licensing/common/types';
|
||||
|
||||
const basicLicense: LicenseType = 'basic';
|
||||
|
||||
export const PLUGIN = {
|
||||
ID: 'rollup',
|
||||
minimumLicenseType: basicLicense,
|
||||
};
|
||||
|
||||
export const CONFIG_ROLLUPS = 'rollups:enableIndexPatterns';
|
||||
|
||||
export const API_BASE_PATH = '/api/rollup';
|
||||
|
|
|
@ -4,6 +4,17 @@
|
|||
"kibanaVersion": "kibana",
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"optionalPlugins": ["home", "indexManagement", "indexPatternManagement", "usageCollection"],
|
||||
"requiredPlugins": ["management", "data"]
|
||||
"requiredPlugins": [
|
||||
"indexPatternManagement",
|
||||
"management",
|
||||
"licensing",
|
||||
"data"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"home",
|
||||
"indexManagement",
|
||||
"usageCollection",
|
||||
"visTypeTimeseries"
|
||||
],
|
||||
"configPath": ["xpack", "rollup"]
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ export const stepIds = [
|
|||
* 1. getDefaultFields: (overrides) => object
|
||||
* 2. fieldValidations
|
||||
*
|
||||
* See x-pack/plugins/rollup/public/crud_app/services/jobs.js for more information on override's shape
|
||||
* See rollup/public/crud_app/services/jobs.js for more information on override's shape
|
||||
*/
|
||||
export const stepIdToStepConfigMap = {
|
||||
[STEP_LOGISTICS]: {
|
||||
|
|
|
@ -16,6 +16,9 @@ export { METRIC_TYPE };
|
|||
export function trackUserRequest<TResponse>(request: Promise<TResponse>, actionType: string) {
|
||||
// Only track successful actions.
|
||||
return request.then(response => {
|
||||
// NOTE: METRIC_TYPE.LOADED is probably the wrong metric type here. The correct metric type
|
||||
// is more likely METRIC_TYPE.APPLICATION_USAGE. This change was introduced in
|
||||
// https://github.com/elastic/kibana/pull/41113/files#diff-58ac12bdd1a3a05a24e69ff20633c482R20
|
||||
trackUiMetric(METRIC_TYPE.LOADED, actionType);
|
||||
// We return the response immediately without waiting for the tracking request to resolve,
|
||||
// to avoid adding additional latency.
|
||||
|
|
|
@ -5,21 +5,34 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiCallOut } from '@elastic/eui';
|
||||
|
||||
export const RollupPrompt = () => (
|
||||
<EuiCallOut color="warning" iconType="help" title="Beta feature">
|
||||
<p>
|
||||
Kibana's support for rollup index patterns is in beta. You might encounter issues using
|
||||
these patterns in saved searches, visualizations, and dashboards. They are not supported in
|
||||
some advanced features, such as Timelion, and Machine Learning.
|
||||
{i18n.translate(
|
||||
'xpack.rollupJobs.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph1Text',
|
||||
{
|
||||
defaultMessage:
|
||||
"Kibana's support for rollup index patterns is in beta. You might encounter issues using " +
|
||||
'these patterns in saved searches, visualizations, and dashboards. They are not supported in ' +
|
||||
'some advanced features, such as Timelion, and Machine Learning.',
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
You can match a rollup index pattern against one rollup index and zero or more regular
|
||||
indices. A rollup index pattern has limited metrics, fields, intervals, and aggregations. A
|
||||
rollup index is limited to indices that have one job configuration, or multiple jobs with
|
||||
compatible configurations.
|
||||
{i18n.translate(
|
||||
'xpack.rollupJobs.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph2Text',
|
||||
{
|
||||
defaultMessage:
|
||||
'You can match a rollup index pattern against one rollup index and zero or more regular ' +
|
||||
'indices. A rollup index pattern has limited metrics, fields, intervals, and aggregations. A ' +
|
||||
'rollup index is limited to indices that have one job configuration, or multiple jobs with ' +
|
||||
'compatible configurations.',
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
);
|
||||
|
|
|
@ -4,4 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { isEsError } from './is_es_error';
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
|
||||
export const configSchema = schema.object({
|
||||
enabled: schema.boolean({ defaultValue: true }),
|
||||
});
|
||||
|
||||
export type RollupConfig = TypeOf<typeof configSchema>;
|
|
@ -4,9 +4,13 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { PluginInitializerContext } from 'src/core/server';
|
||||
import { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server';
|
||||
import { RollupPlugin } from './plugin';
|
||||
import { configSchema, RollupConfig } from './config';
|
||||
|
||||
export const plugin = (initContext: PluginInitializerContext) => new RollupPlugin(initContext);
|
||||
export const plugin = (pluginInitializerContext: PluginInitializerContext) =>
|
||||
new RollupPlugin(pluginInitializerContext);
|
||||
|
||||
export { RollupSetup } from './plugin';
|
||||
export const config: PluginConfigDescriptor<RollupConfig> = {
|
||||
schema: configSchema,
|
||||
};
|
||||
|
|
98
x-pack/plugins/rollup/server/lib/__tests__/fixtures/jobs.js
Normal file
98
x-pack/plugins/rollup/server/lib/__tests__/fixtures/jobs.js
Normal file
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const jobs = [
|
||||
{
|
||||
job_id: 'foo1',
|
||||
rollup_index: 'foo_rollup',
|
||||
index_pattern: 'foo-*',
|
||||
fields: {
|
||||
node: [
|
||||
{
|
||||
agg: 'terms',
|
||||
},
|
||||
],
|
||||
temperature: [
|
||||
{
|
||||
agg: 'min',
|
||||
},
|
||||
{
|
||||
agg: 'max',
|
||||
},
|
||||
{
|
||||
agg: 'sum',
|
||||
},
|
||||
],
|
||||
timestamp: [
|
||||
{
|
||||
agg: 'date_histogram',
|
||||
time_zone: 'UTC',
|
||||
interval: '1h',
|
||||
delay: '7d',
|
||||
},
|
||||
],
|
||||
voltage: [
|
||||
{
|
||||
agg: 'histogram',
|
||||
interval: 5,
|
||||
},
|
||||
{
|
||||
agg: 'sum',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
job_id: 'foo2',
|
||||
rollup_index: 'foo_rollup',
|
||||
index_pattern: 'foo-*',
|
||||
fields: {
|
||||
host: [
|
||||
{
|
||||
agg: 'terms',
|
||||
},
|
||||
],
|
||||
timestamp: [
|
||||
{
|
||||
agg: 'date_histogram',
|
||||
time_zone: 'UTC',
|
||||
interval: '1h',
|
||||
delay: '7d',
|
||||
},
|
||||
],
|
||||
voltage: [
|
||||
{
|
||||
agg: 'histogram',
|
||||
interval: 20,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
job_id: 'foo3',
|
||||
rollup_index: 'foo_rollup',
|
||||
index_pattern: 'foo-*',
|
||||
fields: {
|
||||
timestamp: [
|
||||
{
|
||||
agg: 'date_histogram',
|
||||
time_zone: 'PST',
|
||||
interval: '1h',
|
||||
delay: '7d',
|
||||
},
|
||||
],
|
||||
voltage: [
|
||||
{
|
||||
agg: 'histogram',
|
||||
interval: 5,
|
||||
},
|
||||
{
|
||||
agg: 'sum',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
78
x-pack/plugins/rollup/server/lib/format_es_error.ts
Normal file
78
x-pack/plugins/rollup/server/lib/format_es_error.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
function extractCausedByChain(
|
||||
causedBy: Record<string, any> = {},
|
||||
accumulator: string[] = []
|
||||
): string[] {
|
||||
const { reason, caused_by } = causedBy; // eslint-disable-line @typescript-eslint/camelcase
|
||||
|
||||
if (reason) {
|
||||
accumulator.push(reason);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/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
|
||||
*/
|
||||
export function wrapEsError(
|
||||
err: any,
|
||||
statusCodeToMessageMap: Record<string, string> = {}
|
||||
): { message: string; body?: { cause?: string[] }; statusCode: number } {
|
||||
const { statusCode, response } = err;
|
||||
|
||||
const {
|
||||
error: {
|
||||
root_cause = [], // eslint-disable-line @typescript-eslint/camelcase
|
||||
caused_by = undefined, // eslint-disable-line @typescript-eslint/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]) {
|
||||
// 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;
|
||||
|
||||
return {
|
||||
message: err.message,
|
||||
statusCode,
|
||||
body: {
|
||||
cause: causedByChain.length ? causedByChain : defaultCause,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise, use the custom message to create a Boom error response and
|
||||
// return it
|
||||
const message = statusCodeToMessageMap[statusCode];
|
||||
return { message, statusCode };
|
||||
}
|
||||
|
||||
export function formatEsError(err: any): any {
|
||||
const { statusCode, message, body } = wrapEsError(err);
|
||||
return {
|
||||
statusCode,
|
||||
body: {
|
||||
message,
|
||||
attributes: {
|
||||
cause: body?.cause,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -3,18 +3,19 @@
|
|||
* 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';
|
||||
} from '../../../../../../src/plugins/vis_type_timeseries/server';
|
||||
import { CallWithRequestFactoryShim } from '../../types';
|
||||
import { getRollupSearchStrategy } from './rollup_search_strategy';
|
||||
import { getRollupSearchRequest } from './rollup_search_request';
|
||||
import { getRollupSearchCapabilities } from './rollup_search_capabilities';
|
||||
|
||||
export const registerRollupSearchStrategy = (
|
||||
{ elasticsearchService }: RouteDependencies,
|
||||
callWithRequestFactory: CallWithRequestFactoryShim,
|
||||
addSearchStrategy: (searchStrategy: any) => void
|
||||
) => {
|
||||
const RollupSearchRequest = getRollupSearchRequest(AbstractSearchRequest);
|
||||
|
@ -22,8 +23,9 @@ export const registerRollupSearchStrategy = (
|
|||
const RollupSearchStrategy = getRollupSearchStrategy(
|
||||
AbstractSearchStrategy,
|
||||
RollupSearchRequest,
|
||||
RollupSearchCapabilities
|
||||
RollupSearchCapabilities,
|
||||
callWithRequestFactory
|
||||
);
|
||||
|
||||
addSearchStrategy(new RollupSearchStrategy(elasticsearchService));
|
||||
addSearchStrategy(new RollupSearchStrategy());
|
||||
};
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { get, has } from 'lodash';
|
||||
import { KibanaRequest } from 'kibana/server';
|
||||
import { KibanaRequest } from 'src/core/server';
|
||||
import { leastCommonInterval, isCalendarInterval } from './lib/interval_helper';
|
||||
|
||||
export const getRollupSearchCapabilities = (DefaultSearchCapabilities: any) =>
|
|
@ -4,8 +4,9 @@
|
|||
* 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 { KibanaRequest } from 'src/core/server';
|
||||
|
||||
import { CallWithRequestFactoryShim } from '../../types';
|
||||
import { mergeCapabilitiesWithFields } from '../merge_capabilities_with_fields';
|
||||
import { getCapabilitiesForRollupIndices } from '../map_capabilities';
|
||||
|
||||
|
@ -20,13 +21,16 @@ const isIndexPatternValid = (indexPattern: string) =>
|
|||
export const getRollupSearchStrategy = (
|
||||
AbstractSearchStrategy: any,
|
||||
RollupSearchRequest: any,
|
||||
RollupSearchCapabilities: any
|
||||
RollupSearchCapabilities: any,
|
||||
callWithRequestFactory: CallWithRequestFactoryShim
|
||||
) =>
|
||||
class RollupSearchStrategy extends AbstractSearchStrategy {
|
||||
name = 'rollup';
|
||||
|
||||
constructor(elasticsearchService: ElasticsearchServiceSetup) {
|
||||
super(elasticsearchService, callWithRequestFactory, RollupSearchRequest);
|
||||
constructor() {
|
||||
// TODO: When vis_type_timeseries and AbstractSearchStrategy are migrated to the NP, it
|
||||
// shouldn't require elasticsearchService to be injected, and we can remove this null argument.
|
||||
super(null, callWithRequestFactory, RollupSearchRequest);
|
||||
}
|
||||
|
||||
getRollupData(req: KibanaRequest, indexPattern: string) {
|
|
@ -4,20 +4,98 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/server';
|
||||
declare module 'src/core/server' {
|
||||
interface RequestHandlerContext {
|
||||
rollup?: RollupContext;
|
||||
}
|
||||
}
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { first } from 'rxjs/operators';
|
||||
import {
|
||||
CoreSetup,
|
||||
Plugin,
|
||||
Logger,
|
||||
KibanaRequest,
|
||||
PluginInitializerContext,
|
||||
IScopedClusterClient,
|
||||
APICaller,
|
||||
SharedGlobalConfig,
|
||||
} from 'src/core/server';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { CONFIG_ROLLUPS } from '../common';
|
||||
|
||||
export class RollupPlugin implements Plugin<RollupSetup> {
|
||||
private readonly initContext: PluginInitializerContext;
|
||||
import { PLUGIN, CONFIG_ROLLUPS } from '../common';
|
||||
import { Dependencies, CallWithRequestFactoryShim } from './types';
|
||||
import { registerApiRoutes } from './routes';
|
||||
import { License } from './services';
|
||||
import { registerRollupUsageCollector } from './collectors';
|
||||
import { rollupDataEnricher } from './rollup_data_enricher';
|
||||
import { IndexPatternsFetcher } from './shared_imports';
|
||||
import { registerRollupSearchStrategy } from './lib/search_strategies';
|
||||
import { elasticsearchJsPlugin } from './client/elasticsearch_rollup';
|
||||
import { isEsError } from './lib/is_es_error';
|
||||
import { formatEsError } from './lib/format_es_error';
|
||||
import { getCapabilitiesForRollupIndices } from './lib/map_capabilities';
|
||||
import { mergeCapabilitiesWithFields } from './lib/merge_capabilities_with_fields';
|
||||
|
||||
constructor(initContext: PluginInitializerContext) {
|
||||
this.initContext = initContext;
|
||||
interface RollupContext {
|
||||
client: IScopedClusterClient;
|
||||
}
|
||||
|
||||
export class RollupPlugin implements Plugin<void, void, any, any> {
|
||||
private readonly logger: Logger;
|
||||
private readonly globalConfig$: Observable<SharedGlobalConfig>;
|
||||
private readonly license: License;
|
||||
|
||||
constructor(initializerContext: PluginInitializerContext) {
|
||||
this.logger = initializerContext.logger.get();
|
||||
this.globalConfig$ = initializerContext.config.legacy.globalConfig$;
|
||||
this.license = new License();
|
||||
}
|
||||
|
||||
public setup(core: CoreSetup) {
|
||||
core.uiSettings.register({
|
||||
public setup(
|
||||
{ http, uiSettings, elasticsearch }: CoreSetup,
|
||||
{ licensing, indexManagement, visTypeTimeseries, usageCollection }: Dependencies
|
||||
) {
|
||||
this.license.setup(
|
||||
{
|
||||
pluginId: PLUGIN.ID,
|
||||
minimumLicenseType: PLUGIN.minimumLicenseType,
|
||||
defaultErrorMessage: i18n.translate('xpack.rollupJobs.licenseCheckErrorMessage', {
|
||||
defaultMessage: 'License check failed',
|
||||
}),
|
||||
},
|
||||
{
|
||||
licensing,
|
||||
logger: this.logger,
|
||||
}
|
||||
);
|
||||
|
||||
// Extend the elasticsearchJs client with additional endpoints.
|
||||
const esClientConfig = { plugins: [elasticsearchJsPlugin] };
|
||||
const rollupEsClient = elasticsearch.createClient('rollup', esClientConfig);
|
||||
http.registerRouteHandlerContext('rollup', (context, request) => {
|
||||
return {
|
||||
client: rollupEsClient.asScoped(request),
|
||||
};
|
||||
});
|
||||
|
||||
registerApiRoutes({
|
||||
router: http.createRouter(),
|
||||
license: this.license,
|
||||
lib: {
|
||||
isEsError,
|
||||
formatEsError,
|
||||
getCapabilitiesForRollupIndices,
|
||||
mergeCapabilitiesWithFields,
|
||||
},
|
||||
sharedImports: {
|
||||
IndexPatternsFetcher,
|
||||
},
|
||||
});
|
||||
|
||||
uiSettings.register({
|
||||
[CONFIG_ROLLUPS]: {
|
||||
name: i18n.translate('xpack.rollupJobs.rollupIndexPatternsTitle', {
|
||||
defaultMessage: 'Enable rollup index patterns',
|
||||
|
@ -33,22 +111,34 @@ export class RollupPlugin implements Plugin<RollupSetup> {
|
|||
},
|
||||
});
|
||||
|
||||
return {
|
||||
__legacy: {
|
||||
config: this.initContext.config,
|
||||
logger: this.initContext.logger,
|
||||
},
|
||||
};
|
||||
if (visTypeTimeseries) {
|
||||
// TODO: When vis_type_timeseries is fully migrated to the NP, it shouldn't require this shim.
|
||||
const callWithRequestFactoryShim = (
|
||||
elasticsearchServiceShim: CallWithRequestFactoryShim,
|
||||
request: KibanaRequest
|
||||
): APICaller => rollupEsClient.asScoped(request).callAsCurrentUser;
|
||||
|
||||
const { addSearchStrategy } = visTypeTimeseries;
|
||||
registerRollupSearchStrategy(callWithRequestFactoryShim, addSearchStrategy);
|
||||
}
|
||||
|
||||
if (usageCollection) {
|
||||
this.globalConfig$
|
||||
.pipe(first())
|
||||
.toPromise()
|
||||
.then(globalConfig => {
|
||||
registerRollupUsageCollector(usageCollection, globalConfig.kibana.index);
|
||||
})
|
||||
.catch((e: any) => {
|
||||
this.logger.warn(`Registering Rollup collector failed: ${e}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (indexManagement && indexManagement.indexDataEnricher) {
|
||||
indexManagement.indexDataEnricher.add(rollupDataEnricher);
|
||||
}
|
||||
}
|
||||
|
||||
public start() {}
|
||||
public stop() {}
|
||||
}
|
||||
|
||||
export interface RollupSetup {
|
||||
/** @deprecated */
|
||||
__legacy: {
|
||||
config: PluginInitializerContext['config'];
|
||||
logger: PluginInitializerContext['logger'];
|
||||
};
|
||||
start() {}
|
||||
stop() {}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Index } from '../../../../plugins/index_management/server';
|
||||
import { Index } from '../../../plugins/index_management/server';
|
||||
|
||||
export const rollupDataEnricher = async (indicesList: Index[], callWithRequest: any) => {
|
||||
if (!indicesList || !indicesList.length) {
|
|
@ -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 { RouteDependencies } from '../../../types';
|
||||
import { registerFieldsForWildcardRoute } from './register_fields_for_wildcard_route';
|
||||
|
||||
export function registerIndexPatternsRoutes(dependencies: RouteDependencies) {
|
||||
registerFieldsForWildcardRoute(dependencies);
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* 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 { indexBy } from 'lodash';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { Field } from '../../../lib/merge_capabilities_with_fields';
|
||||
import { RouteDependencies } from '../../../types';
|
||||
|
||||
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,
|
||||
IndexPatternsFetcher: 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 const registerFieldsForWildcardRoute = ({
|
||||
router,
|
||||
license,
|
||||
lib: { isEsError, formatEsError, getCapabilitiesForRollupIndices, mergeCapabilitiesWithFields },
|
||||
sharedImports: { IndexPatternsFetcher },
|
||||
}: RouteDependencies) => {
|
||||
const querySchema = 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';
|
||||
}
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
router.get(
|
||||
{
|
||||
path: '/api/index_patterns/rollup/_fields_for_wildcard',
|
||||
validate: {
|
||||
query: querySchema,
|
||||
},
|
||||
},
|
||||
license.guardApiRoute(async (context, request, response) => {
|
||||
const { params, meta_fields: metaFields } = request.query;
|
||||
|
||||
try {
|
||||
// Make call and use field information from response
|
||||
const { payload } = await getFieldsForWildcardRequest(
|
||||
context,
|
||||
request,
|
||||
response,
|
||||
IndexPatternsFetcher
|
||||
);
|
||||
const fields = payload.fields;
|
||||
const parsedParams = JSON.parse(params);
|
||||
const rollupIndex = parsedParams.rollup_index;
|
||||
const rollupFields: Field[] = [];
|
||||
const fieldsFromFieldCapsApi: { [key: string]: any } = indexBy(fields, 'name');
|
||||
const rollupIndexCapabilities = getCapabilitiesForRollupIndices(
|
||||
await context.rollup!.client.callAsCurrentUser('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 });
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
14
x-pack/plugins/rollup/server/routes/api/indices/index.ts
Normal file
14
x-pack/plugins/rollup/server/routes/api/indices/index.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 { RouteDependencies } from '../../../types';
|
||||
import { registerGetRoute } from './register_get_route';
|
||||
import { registerValidateIndexPatternRoute } from './register_validate_index_pattern_route';
|
||||
|
||||
export function registerIndicesRoutes(dependencies: RouteDependencies) {
|
||||
registerGetRoute(dependencies);
|
||||
registerValidateIndexPatternRoute(dependencies);
|
||||
}
|
|
@ -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 { addBasePath } from '../../../services';
|
||||
import { RouteDependencies } from '../../../types';
|
||||
|
||||
/**
|
||||
* Returns a list of all rollup index names
|
||||
*/
|
||||
export const registerGetRoute = ({
|
||||
router,
|
||||
license,
|
||||
lib: { isEsError, formatEsError, getCapabilitiesForRollupIndices },
|
||||
}: RouteDependencies) => {
|
||||
router.get(
|
||||
{
|
||||
path: addBasePath('/indices'),
|
||||
validate: false,
|
||||
},
|
||||
license.guardApiRoute(async (context, request, response) => {
|
||||
try {
|
||||
const data = await context.rollup!.client.callAsCurrentUser(
|
||||
'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 });
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* 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 { addBasePath } from '../../../services';
|
||||
import { RouteDependencies } 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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?
|
||||
*/
|
||||
export const registerValidateIndexPatternRoute = ({
|
||||
router,
|
||||
license,
|
||||
lib: { isEsError, formatEsError },
|
||||
}: RouteDependencies) => {
|
||||
router.get(
|
||||
{
|
||||
path: addBasePath('/index_pattern_validity/{indexPattern}'),
|
||||
validate: {
|
||||
params: schema.object({
|
||||
indexPattern: schema.string(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
license.guardApiRoute(async (context, request, response) => {
|
||||
try {
|
||||
const { indexPattern } = request.params;
|
||||
const [fieldCapabilities, rollupIndexCapabilities]: [
|
||||
FieldCapabilities,
|
||||
{ [key: string]: any }
|
||||
] = await Promise.all([
|
||||
context.rollup!.client.callAsCurrentUser('rollup.fieldCapabilities', { indexPattern }),
|
||||
context.rollup!.client.callAsCurrentUser('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 });
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
20
x-pack/plugins/rollup/server/routes/api/jobs/index.ts
Normal file
20
x-pack/plugins/rollup/server/routes/api/jobs/index.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 { RouteDependencies } from '../../../types';
|
||||
import { registerCreateRoute } from './register_create_route';
|
||||
import { registerDeleteRoute } from './register_delete_route';
|
||||
import { registerGetRoute } from './register_get_route';
|
||||
import { registerStartRoute } from './register_start_route';
|
||||
import { registerStopRoute } from './register_stop_route';
|
||||
|
||||
export function registerJobsRoutes(dependencies: RouteDependencies) {
|
||||
registerCreateRoute(dependencies);
|
||||
registerDeleteRoute(dependencies);
|
||||
registerGetRoute(dependencies);
|
||||
registerStartRoute(dependencies);
|
||||
registerStopRoute(dependencies);
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 { addBasePath } from '../../../services';
|
||||
import { RouteDependencies } from '../../../types';
|
||||
|
||||
export const registerCreateRoute = ({
|
||||
router,
|
||||
license,
|
||||
lib: { isEsError, formatEsError },
|
||||
}: RouteDependencies) => {
|
||||
router.put(
|
||||
{
|
||||
path: addBasePath('/create'),
|
||||
validate: {
|
||||
body: schema.object({
|
||||
job: schema.object(
|
||||
{
|
||||
id: schema.string(),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
),
|
||||
}),
|
||||
},
|
||||
},
|
||||
license.guardApiRoute(async (context, request, response) => {
|
||||
try {
|
||||
const { id, ...rest } = request.body.job;
|
||||
// Create job.
|
||||
await context.rollup!.client.callAsCurrentUser('rollup.createJob', {
|
||||
id,
|
||||
body: rest,
|
||||
});
|
||||
// Then request the newly created job.
|
||||
const results = await context.rollup!.client.callAsCurrentUser('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 });
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 { addBasePath } from '../../../services';
|
||||
import { RouteDependencies } from '../../../types';
|
||||
|
||||
export const registerDeleteRoute = ({
|
||||
router,
|
||||
license,
|
||||
lib: { isEsError, formatEsError },
|
||||
}: RouteDependencies) => {
|
||||
router.post(
|
||||
{
|
||||
path: addBasePath('/delete'),
|
||||
validate: {
|
||||
body: schema.object({
|
||||
jobIds: schema.arrayOf(schema.string()),
|
||||
}),
|
||||
},
|
||||
},
|
||||
license.guardApiRoute(async (context, request, response) => {
|
||||
try {
|
||||
const { jobIds } = request.body;
|
||||
const data = await Promise.all(
|
||||
jobIds.map((id: string) =>
|
||||
context.rollup!.client.callAsCurrentUser('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 });
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { addBasePath } from '../../../services';
|
||||
import { RouteDependencies } from '../../../types';
|
||||
|
||||
export const registerGetRoute = ({
|
||||
router,
|
||||
license,
|
||||
lib: { isEsError, formatEsError },
|
||||
}: RouteDependencies) => {
|
||||
router.get(
|
||||
{
|
||||
path: addBasePath('/jobs'),
|
||||
validate: false,
|
||||
},
|
||||
license.guardApiRoute(async (context, request, response) => {
|
||||
try {
|
||||
const data = await context.rollup!.client.callAsCurrentUser('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 });
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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 { addBasePath } from '../../../services';
|
||||
import { RouteDependencies } from '../../../types';
|
||||
|
||||
export const registerStartRoute = ({
|
||||
router,
|
||||
license,
|
||||
lib: { isEsError, formatEsError },
|
||||
}: RouteDependencies) => {
|
||||
router.post(
|
||||
{
|
||||
path: addBasePath('/start'),
|
||||
validate: {
|
||||
body: schema.object({
|
||||
jobIds: schema.arrayOf(schema.string()),
|
||||
}),
|
||||
query: schema.maybe(
|
||||
schema.object({
|
||||
waitForCompletion: schema.maybe(schema.string()),
|
||||
})
|
||||
),
|
||||
},
|
||||
},
|
||||
license.guardApiRoute(async (context, request, response) => {
|
||||
try {
|
||||
const { jobIds } = request.body;
|
||||
|
||||
const data = await Promise.all(
|
||||
jobIds.map((id: string) =>
|
||||
context.rollup!.client.callAsCurrentUser('rollup.startJob', { 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/39845, which was addressed in 8.0
|
||||
// but not backported to 7.x because it's breaking. So we need to modify the response
|
||||
// here for 7.x.
|
||||
if (err.message.includes('Cannot start task for Rollup Job')) {
|
||||
err.status = 400;
|
||||
err.statusCode = 400;
|
||||
err.body.error.status = 400;
|
||||
err.displayName = 'Bad request';
|
||||
}
|
||||
|
||||
if (isEsError(err)) {
|
||||
return response.customError({ statusCode: err.statusCode, body: err });
|
||||
}
|
||||
return response.internalError({ body: err });
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 { addBasePath } from '../../../services';
|
||||
import { RouteDependencies } from '../../../types';
|
||||
|
||||
export const registerStopRoute = ({
|
||||
router,
|
||||
license,
|
||||
lib: { isEsError, formatEsError },
|
||||
}: RouteDependencies) => {
|
||||
router.post(
|
||||
{
|
||||
path: addBasePath('/stop'),
|
||||
validate: {
|
||||
body: schema.object({
|
||||
jobIds: schema.arrayOf(schema.string()),
|
||||
}),
|
||||
query: schema.object({
|
||||
waitForCompletion: schema.maybe(schema.string()),
|
||||
}),
|
||||
},
|
||||
},
|
||||
license.guardApiRoute(async (context, 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 sequentially.
|
||||
const { waitForCompletion } = request.query;
|
||||
const stopRollupJob = (id: string) =>
|
||||
context.rollup!.client.callAsCurrentUser('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 });
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
|
@ -3,7 +3,10 @@
|
|||
* 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 const plugin = (ctx: PluginInitializerContext) => new RollupsServerPlugin(ctx);
|
||||
import { RouteDependencies } from '../../../types';
|
||||
import { registerSearchRoute } from './register_search_route';
|
||||
|
||||
export function registerSearchRoutes(dependencies: RouteDependencies) {
|
||||
registerSearchRoute(dependencies);
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 { addBasePath } from '../../../services';
|
||||
import { RouteDependencies } from '../../../types';
|
||||
|
||||
export const registerSearchRoute = ({
|
||||
router,
|
||||
license,
|
||||
lib: { isEsError, formatEsError },
|
||||
}: RouteDependencies) => {
|
||||
router.post(
|
||||
{
|
||||
path: addBasePath('/search'),
|
||||
validate: {
|
||||
body: schema.arrayOf(
|
||||
schema.object({
|
||||
index: schema.string(),
|
||||
query: schema.any(),
|
||||
})
|
||||
),
|
||||
},
|
||||
},
|
||||
license.guardApiRoute(async (context, request, response) => {
|
||||
try {
|
||||
const requests = request.body.map(({ index, query }: { index: string; query?: any }) =>
|
||||
context.rollup!.client.callAsCurrentUser('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 });
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
19
x-pack/plugins/rollup/server/routes/index.ts
Normal file
19
x-pack/plugins/rollup/server/routes/index.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 { RouteDependencies } from '../types';
|
||||
|
||||
import { registerIndexPatternsRoutes } from './api/index_patterns';
|
||||
import { registerIndicesRoutes } from './api/indices';
|
||||
import { registerJobsRoutes } from './api/jobs';
|
||||
import { registerSearchRoutes } from './api/search';
|
||||
|
||||
export function registerApiRoutes(dependencies: RouteDependencies) {
|
||||
registerIndexPatternsRoutes(dependencies);
|
||||
registerIndicesRoutes(dependencies);
|
||||
registerJobsRoutes(dependencies);
|
||||
registerSearchRoutes(dependencies);
|
||||
}
|
|
@ -4,4 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { callWithRequestFactory } from './call_with_request_factory';
|
||||
import { API_BASE_PATH } from '../../common';
|
||||
|
||||
export const addBasePath = (uri: string): string => `${API_BASE_PATH}${uri}`;
|
|
@ -4,4 +4,5 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { licensePreRoutingFactory } from './license_pre_routing_factory';
|
||||
export { addBasePath } from './add_base_path';
|
||||
export { License } from './license';
|
93
x-pack/plugins/rollup/server/services/license.ts
Normal file
93
x-pack/plugins/rollup/server/services/license.ts
Normal file
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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 { Logger } from 'src/core/server';
|
||||
import {
|
||||
KibanaRequest,
|
||||
KibanaResponseFactory,
|
||||
RequestHandler,
|
||||
RequestHandlerContext,
|
||||
} from 'src/core/server';
|
||||
|
||||
import { LicensingPluginSetup } from '../../../licensing/server';
|
||||
import { LicenseType } from '../../../licensing/common/types';
|
||||
|
||||
export interface LicenseStatus {
|
||||
isValid: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface SetupSettings {
|
||||
pluginId: string;
|
||||
minimumLicenseType: LicenseType;
|
||||
defaultErrorMessage: string;
|
||||
}
|
||||
|
||||
export class License {
|
||||
private licenseStatus: LicenseStatus = {
|
||||
isValid: false,
|
||||
message: 'Invalid License',
|
||||
};
|
||||
|
||||
private _isEsSecurityEnabled: boolean = false;
|
||||
|
||||
setup(
|
||||
{ pluginId, minimumLicenseType, defaultErrorMessage }: SetupSettings,
|
||||
{ licensing, logger }: { licensing: LicensingPluginSetup; logger: Logger }
|
||||
) {
|
||||
licensing.license$.subscribe(license => {
|
||||
const { state, message } = license.check(pluginId, minimumLicenseType);
|
||||
const hasRequiredLicense = state === 'valid';
|
||||
|
||||
// Retrieving security checks the results of GET /_xpack as well as license state,
|
||||
// so we're also checking whether the security is disabled in elasticsearch.yml.
|
||||
this._isEsSecurityEnabled = license.getFeature('security').isEnabled;
|
||||
|
||||
if (hasRequiredLicense) {
|
||||
this.licenseStatus = { isValid: true };
|
||||
} else {
|
||||
this.licenseStatus = {
|
||||
isValid: false,
|
||||
message: message || defaultErrorMessage,
|
||||
};
|
||||
if (message) {
|
||||
logger.info(message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
guardApiRoute<P, Q, B>(handler: RequestHandler<P, Q, B>) {
|
||||
const license = this;
|
||||
|
||||
return function licenseCheck(
|
||||
ctx: RequestHandlerContext,
|
||||
request: KibanaRequest<P, Q, B>,
|
||||
response: KibanaResponseFactory
|
||||
) {
|
||||
const licenseStatus = license.getStatus();
|
||||
|
||||
if (!licenseStatus.isValid) {
|
||||
return response.customError({
|
||||
body: {
|
||||
message: licenseStatus.message || '',
|
||||
},
|
||||
statusCode: 403,
|
||||
});
|
||||
}
|
||||
|
||||
return handler(ctx, request, response);
|
||||
};
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return this.licenseStatus;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-member-accessibility
|
||||
get isEsSecurityEnabled() {
|
||||
return this._isEsSecurityEnabled;
|
||||
}
|
||||
}
|
|
@ -4,4 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { IndexPatternsFetcher } from '../../../../../src/plugins/data/server';
|
||||
export { IndexPatternsFetcher } from '../../../../src/plugins/data/server';
|
45
x-pack/plugins/rollup/server/types.ts
Normal file
45
x-pack/plugins/rollup/server/types.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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, APICaller, KibanaRequest } from 'src/core/server';
|
||||
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
|
||||
import { VisTypeTimeseriesSetup } from 'src/plugins/vis_type_timeseries/server';
|
||||
|
||||
import { IndexManagementPluginSetup } from '../../index_management/server';
|
||||
import { LicensingPluginSetup } from '../../licensing/server';
|
||||
import { License } from './services';
|
||||
import { IndexPatternsFetcher } from './shared_imports';
|
||||
import { isEsError } from './lib/is_es_error';
|
||||
import { formatEsError } from './lib/format_es_error';
|
||||
import { getCapabilitiesForRollupIndices } from './lib/map_capabilities';
|
||||
import { mergeCapabilitiesWithFields } from './lib/merge_capabilities_with_fields';
|
||||
|
||||
export interface Dependencies {
|
||||
indexManagement?: IndexManagementPluginSetup;
|
||||
visTypeTimeseries?: VisTypeTimeseriesSetup;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
licensing: LicensingPluginSetup;
|
||||
}
|
||||
|
||||
export interface RouteDependencies {
|
||||
router: IRouter;
|
||||
license: License;
|
||||
lib: {
|
||||
isEsError: typeof isEsError;
|
||||
formatEsError: typeof formatEsError;
|
||||
getCapabilitiesForRollupIndices: typeof getCapabilitiesForRollupIndices;
|
||||
mergeCapabilitiesWithFields: typeof mergeCapabilitiesWithFields;
|
||||
};
|
||||
sharedImports: {
|
||||
IndexPatternsFetcher: typeof IndexPatternsFetcher;
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: When vis_type_timeseries is fully migrated to the NP, it shouldn't require this shim.
|
||||
export type CallWithRequestFactoryShim = (
|
||||
elasticsearchServiceShim: CallWithRequestFactoryShim,
|
||||
request: KibanaRequest
|
||||
) => APICaller;
|
|
@ -12362,7 +12362,6 @@
|
|||
"xpack.reporting.shareContextMenu.csvReportsButtonLabel": "CSV レポート",
|
||||
"xpack.reporting.shareContextMenu.pdfReportsButtonLabel": "PDF レポート",
|
||||
"xpack.reporting.shareContextMenu.pngReportsButtonLabel": "PNG レポート",
|
||||
"xpack.rollupJobs.appName": "ロールアップジョブ",
|
||||
"xpack.rollupJobs.appTitle": "ロールアップジョブ",
|
||||
"xpack.rollupJobs.breadcrumbsTitle": "ロールアップジョブ",
|
||||
"xpack.rollupJobs.create.backButton.label": "戻る",
|
||||
|
|
|
@ -12366,7 +12366,6 @@
|
|||
"xpack.reporting.shareContextMenu.csvReportsButtonLabel": "CSV 报告",
|
||||
"xpack.reporting.shareContextMenu.pdfReportsButtonLabel": "PDF 报告",
|
||||
"xpack.reporting.shareContextMenu.pngReportsButtonLabel": "PNG 报告",
|
||||
"xpack.rollupJobs.appName": "汇总/打包作业",
|
||||
"xpack.rollupJobs.appTitle": "汇总/打包作业",
|
||||
"xpack.rollupJobs.breadcrumbsTitle": "汇总/打包作业",
|
||||
"xpack.rollupJobs.create.backButton.label": "上一步",
|
||||
|
|
Loading…
Reference in a new issue