From 9372100516143825d30f683a877d686241e1c293 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 2 Jan 2020 11:38:19 -0700 Subject: [PATCH] [ML] New Platform server shim: update analytics routes to use new platform router (#53521) * update dfAnalytics routes to use np router * add route schemas and only show error message * convert route file to ts and set handlers inline * update df analytics param type * update mlClient type and assert mlClient is not null * handle errors correctly * ensure error status gets passed correctly to wrapper --- x-pack/legacy/plugins/ml/index.ts | 3 +- .../analytics_panel/analytics_panel.tsx | 2 +- .../plugins/ml/server/client/error_wrapper.ts | 17 ++ .../new_platform/data_analytics_schema.ts | 43 +++ .../licence_check_pre_routing_factory.ts | 37 +++ .../plugins/ml/server/new_platform/plugin.ts | 41 ++- .../ml/server/routes/data_frame_analytics.js | 174 ----------- .../ml/server/routes/data_frame_analytics.ts | 289 ++++++++++++++++++ 8 files changed, 426 insertions(+), 180 deletions(-) create mode 100644 x-pack/legacy/plugins/ml/server/client/error_wrapper.ts create mode 100644 x-pack/legacy/plugins/ml/server/new_platform/data_analytics_schema.ts create mode 100644 x-pack/legacy/plugins/ml/server/new_platform/licence_check_pre_routing_factory.ts delete mode 100644 x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.js create mode 100644 x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts diff --git a/x-pack/legacy/plugins/ml/index.ts b/x-pack/legacy/plugins/ml/index.ts index 9fe55d15d34a7..c4289389b0d56 100755 --- a/x-pack/legacy/plugins/ml/index.ts +++ b/x-pack/legacy/plugins/ml/index.ts @@ -81,10 +81,11 @@ export const ml = (kibana: any) => { injectUiAppVars: server.injectUiAppVars, http: mlHttpService, savedObjects: server.savedObjects, + elasticsearch: kbnServer.newPlatform.setup.core.elasticsearch, // NP }; const { usageCollection, cloud, home } = kbnServer.newPlatform.setup.plugins; const plugins = { - elasticsearch: server.plugins.elasticsearch, + elasticsearch: server.plugins.elasticsearch, // legacy security: server.plugins.security, xpackMain: server.plugins.xpack_main, spaces: server.plugins.spaces, diff --git a/x-pack/legacy/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx b/x-pack/legacy/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx index 72f4332728e94..b2eda12abc578 100644 --- a/x-pack/legacy/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx +++ b/x-pack/legacy/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx @@ -76,7 +76,7 @@ export const AnalyticsPanel: FC = ({ jobCreationDisabled }) => { )}      - {isInitialized === true && analytics.length === 0 && ( + {errorMessage === undefined && isInitialized === true && analytics.length === 0 && ( { + const boom = isBoom(error) ? error : boomify(error, { statusCode: error.status }); + return { + body: boom, + headers: boom.output.headers, + statusCode: boom.output.statusCode, + }; +} diff --git a/x-pack/legacy/plugins/ml/server/new_platform/data_analytics_schema.ts b/x-pack/legacy/plugins/ml/server/new_platform/data_analytics_schema.ts new file mode 100644 index 0000000000000..f5d72c51dc070 --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/new_platform/data_analytics_schema.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; + +export const dataAnalyticsJobConfigSchema = { + description: schema.maybe(schema.string()), + dest: schema.object({ + index: schema.string(), + results_field: schema.maybe(schema.string()), + }), + source: schema.object({ + index: schema.string(), + }), + analysis: schema.any(), + analyzed_fields: schema.any(), + model_memory_limit: schema.string(), +}; + +export const dataAnalyticsEvaluateSchema = { + index: schema.string(), + query: schema.maybe(schema.any()), + evaluation: schema.maybe( + schema.object({ + regression: schema.maybe(schema.any()), + classification: schema.maybe(schema.any()), + }) + ), +}; + +export const dataAnalyticsExplainSchema = { + description: schema.maybe(schema.string()), + dest: schema.maybe(schema.any()), + source: schema.object({ + index: schema.string(), + }), + analysis: schema.any(), + analyzed_fields: schema.maybe(schema.any()), + model_memory_limit: schema.maybe(schema.string()), +}; diff --git a/x-pack/legacy/plugins/ml/server/new_platform/licence_check_pre_routing_factory.ts b/x-pack/legacy/plugins/ml/server/new_platform/licence_check_pre_routing_factory.ts new file mode 100644 index 0000000000000..cc77d2872fb90 --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/new_platform/licence_check_pre_routing_factory.ts @@ -0,0 +1,37 @@ +/* + * 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_ID, MlXpackMainPlugin } from './plugin'; + +export const licensePreRoutingFactory = ( + xpackMainPlugin: MlXpackMainPlugin, + handler: RequestHandler +): RequestHandler => { + // License checking and enable/disable logic + return function licensePreRouting( + ctx: RequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory + ) { + const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN_ID).getLicenseCheckResults(); + + if (!licenseCheckResults.isAvailable) { + return response.forbidden({ + body: { + message: licenseCheckResults.message, + }, + }); + } + + return handler(ctx, request, response); + }; +}; diff --git a/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts b/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts index 7e22a9a5a4c8b..681b2ff20c8aa 100644 --- a/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts +++ b/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts @@ -8,9 +8,16 @@ import Boom from 'boom'; import { i18n } from '@kbn/i18n'; import { ServerRoute } from 'hapi'; import { KibanaConfig, SavedObjectsLegacyService } from 'src/legacy/server/kbn_server'; -import { Logger, PluginInitializerContext, CoreSetup } from 'src/core/server'; +import { + Logger, + PluginInitializerContext, + CoreSetup, + IRouter, + IScopedClusterClient, +} from 'src/core/server'; import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { ElasticsearchServiceSetup } from 'src/core/server'; import { CloudSetup } from '../../../../../plugins/cloud/server'; import { XPackMainPlugin } from '../../../xpack_main/server/xpack_main'; import { addLinksToSampleDatasets } from '../lib/sample_data_sets'; @@ -56,6 +63,10 @@ import { jobAuditMessagesRoutes } from '../routes/job_audit_messages'; import { fileDataVisualizerRoutes } from '../routes/file_data_visualizer'; import { initMlServerLog, LogInitialization } from '../client/log'; import { HomeServerPluginSetup } from '../../../../../../src/plugins/home/server'; +// @ts-ignore: could not find declaration file for module +import { elasticsearchJsPlugin } from '../client/elasticsearch_ml'; + +export const PLUGIN_ID = 'ml'; type CoreHttpSetup = CoreSetup['http']; export interface MlHttpServiceSetup extends CoreHttpSetup { @@ -70,6 +81,7 @@ export interface MlCoreSetup { injectUiAppVars: (id: string, callback: () => {}) => any; http: MlHttpServiceSetup; savedObjects: SavedObjectsLegacyService; + elasticsearch: ElasticsearchServiceSetup; } export interface MlInitializerContext extends PluginInitializerContext { legacyConfig: KibanaConfig; @@ -86,12 +98,15 @@ export interface PluginsSetup { // TODO: this is temporary for `mirrorPluginStatus` ml: any; } + export interface RouteInitialization { commonRouteConfig: any; config?: any; elasticsearchPlugin: ElasticsearchPlugin; + elasticsearchService: ElasticsearchServiceSetup; route(route: ServerRoute | ServerRoute[]): void; - xpackMainPlugin?: MlXpackMainPlugin; + router: IRouter; + xpackMainPlugin: MlXpackMainPlugin; savedObjects?: SavedObjectsLegacyService; spacesPlugin: any; cloud?: CloudSetup; @@ -101,8 +116,16 @@ export interface UsageInitialization { savedObjects: SavedObjectsLegacyService; } +declare module 'kibana/server' { + interface RequestHandlerContext { + ml?: { + mlClient: IScopedClusterClient; + }; + } +} + export class Plugin { - private readonly pluginId: string = 'ml'; + private readonly pluginId: string = PLUGIN_ID; private config: any; private log: Logger; @@ -183,17 +206,27 @@ export class Plugin { }; }); + // Can access via new platform router's handler function 'context' parameter - context.ml.mlClient + const mlClient = core.elasticsearch.createClient('ml', { plugins: [elasticsearchJsPlugin] }); + http.registerRouteHandlerContext('ml', (context, request) => { + return { + mlClient: mlClient.asScoped(request), + }; + }); + const routeInitializationDeps: RouteInitialization = { commonRouteConfig, route: http.route, + router: http.createRouter(), elasticsearchPlugin: plugins.elasticsearch, + elasticsearchService: core.elasticsearch, + xpackMainPlugin: plugins.xpackMain, spacesPlugin: plugins.spaces, }; const extendedRouteInitializationDeps: RouteInitialization = { ...routeInitializationDeps, config: this.config, - xpackMainPlugin: plugins.xpackMain, savedObjects: core.savedObjects, spacesPlugin: plugins.spaces, cloud: plugins.cloud, diff --git a/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.js b/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.js deleted file mode 100644 index 567d7e1856a85..0000000000000 --- a/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.js +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { callWithRequestFactory } from '../client/call_with_request_factory'; -import { wrapError } from '../client/errors'; -import { analyticsAuditMessagesProvider } from '../models/data_frame_analytics/analytics_audit_messages'; - -export function dataFrameAnalyticsRoutes({ commonRouteConfig, elasticsearchPlugin, route }) { - route({ - method: 'GET', - path: '/api/ml/data_frame/analytics', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - return callWithRequest('ml.getDataFrameAnalytics').catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'GET', - path: '/api/ml/data_frame/analytics/{analyticsId}', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { analyticsId } = request.params; - return callWithRequest('ml.getDataFrameAnalytics', { analyticsId }).catch(resp => - wrapError(resp) - ); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'GET', - path: '/api/ml/data_frame/analytics/_stats', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - return callWithRequest('ml.getDataFrameAnalyticsStats').catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'GET', - path: '/api/ml/data_frame/analytics/{analyticsId}/_stats', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { analyticsId } = request.params; - return callWithRequest('ml.getDataFrameAnalyticsStats', { analyticsId }).catch(resp => - wrapError(resp) - ); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'PUT', - path: '/api/ml/data_frame/analytics/{analyticsId}', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { analyticsId } = request.params; - return callWithRequest('ml.createDataFrameAnalytics', { - body: request.payload, - analyticsId, - }).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/data_frame/_evaluate', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - return callWithRequest('ml.evaluateDataFrameAnalytics', { - body: request.payload, - }).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/data_frame/analytics/_explain', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - return callWithRequest('ml.estimateDataFrameAnalyticsMemoryUsage', { - body: request.payload, - }).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'DELETE', - path: '/api/ml/data_frame/analytics/{analyticsId}', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { analyticsId } = request.params; - return callWithRequest('ml.deleteDataFrameAnalytics', { analyticsId }).catch(resp => - wrapError(resp) - ); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/data_frame/analytics/{analyticsId}/_start', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const options = { - analyticsId: request.params.analyticsId, - }; - - return callWithRequest('ml.startDataFrameAnalytics', options).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/data_frame/analytics/{analyticsId}/_stop', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const options = { - analyticsId: request.params.analyticsId, - }; - - if (request.query.force !== undefined) { - options.force = request.query.force; - } - - return callWithRequest('ml.stopDataFrameAnalytics', options).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'GET', - path: '/api/ml/data_frame/analytics/{analyticsId}/messages', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { getAnalyticsAuditMessages } = analyticsAuditMessagesProvider(callWithRequest); - const { analyticsId } = request.params; - return getAnalyticsAuditMessages(analyticsId).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); -} diff --git a/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts new file mode 100644 index 0000000000000..2f8db45d9739e --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts @@ -0,0 +1,289 @@ +/* + * 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 { wrapError } from '../client/error_wrapper'; +import { analyticsAuditMessagesProvider } from '../models/data_frame_analytics/analytics_audit_messages'; +import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; +import { RouteInitialization } from '../new_platform/plugin'; +import { + dataAnalyticsJobConfigSchema, + dataAnalyticsEvaluateSchema, + dataAnalyticsExplainSchema, +} from '../new_platform/data_analytics_schema'; + +export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteInitialization) { + router.get( + { + path: '/api/ml/data_frame/analytics', + validate: { + params: schema.object({ analyticsId: schema.maybe(schema.string()) }), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const results = await context.ml!.mlClient.callAsCurrentUser('ml.getDataFrameAnalytics'); + return response.ok({ + body: { ...results }, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + router.get( + { + path: '/api/ml/data_frame/analytics/{analyticsId}', + validate: { + params: schema.object({ analyticsId: schema.string() }), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { analyticsId } = request.params; + const results = await context.ml!.mlClient.callAsCurrentUser('ml.getDataFrameAnalytics', { + analyticsId, + }); + return response.ok({ + body: { ...results }, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + router.get( + { + path: '/api/ml/data_frame/analytics/_stats', + validate: false, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const results = await context.ml!.mlClient.callAsCurrentUser( + 'ml.getDataFrameAnalyticsStats' + ); + return response.ok({ + body: { ...results }, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + router.get( + { + path: '/api/ml/data_frame/analytics/{analyticsId}/_stats', + validate: { + params: schema.object({ analyticsId: schema.string() }), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { analyticsId } = request.params; + const results = await context.ml!.mlClient.callAsCurrentUser( + 'ml.getDataFrameAnalyticsStats', + { + analyticsId, + } + ); + return response.ok({ + body: { ...results }, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + router.put( + { + path: '/api/ml/data_frame/analytics/{analyticsId}', + validate: { + params: schema.object({ + analyticsId: schema.string(), + }), + body: schema.object({ ...dataAnalyticsJobConfigSchema }), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { analyticsId } = request.params; + const results = await context.ml!.mlClient.callAsCurrentUser( + 'ml.createDataFrameAnalytics', + { + body: request.body, + analyticsId, + } + ); + return response.ok({ + body: { ...results }, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + router.post( + { + path: '/api/ml/data_frame/_evaluate', + validate: { + body: schema.object({ ...dataAnalyticsEvaluateSchema }), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const results = await context.ml!.mlClient.callAsCurrentUser( + 'ml.evaluateDataFrameAnalytics', + { + body: request.body, + } + ); + return response.ok({ + body: { ...results }, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + router.post( + { + path: '/api/ml/data_frame/analytics/_explain', + validate: { + body: schema.object({ ...dataAnalyticsExplainSchema }), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const results = await context.ml!.mlClient.callAsCurrentUser( + 'ml.estimateDataFrameAnalyticsMemoryUsage', + { + body: request.body, + } + ); + return response.ok({ + body: { ...results }, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + router.delete( + { + path: '/api/ml/data_frame/analytics/{analyticsId}', + validate: { + params: schema.object({ + analyticsId: schema.string(), + }), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { analyticsId } = request.params; + const results = await context.ml!.mlClient.callAsCurrentUser( + 'ml.deleteDataFrameAnalytics', + { + analyticsId, + } + ); + return response.ok({ + body: { ...results }, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + router.post( + { + path: '/api/ml/data_frame/analytics/{analyticsId}/_start', + validate: { + params: schema.object({ + analyticsId: schema.string(), + }), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { analyticsId } = request.params; + const results = await context.ml!.mlClient.callAsCurrentUser('ml.startDataFrameAnalytics', { + analyticsId, + }); + return response.ok({ + body: { ...results }, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + router.post( + { + path: '/api/ml/data_frame/analytics/{analyticsId}/_stop', + validate: { + params: schema.object({ + analyticsId: schema.string(), + force: schema.maybe(schema.boolean()), + }), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const options: { analyticsId: string; force?: boolean | undefined } = { + analyticsId: request.params.analyticsId, + }; + // @ts-ignore TODO: update types + if (request.url?.query?.force !== undefined) { + // @ts-ignore TODO: update types + options.force = request.url.query.force; + } + + const results = await context.ml!.mlClient.callAsCurrentUser( + 'ml.stopDataFrameAnalytics', + options + ); + return response.ok({ + body: { ...results }, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + router.get( + { + path: '/api/ml/data_frame/analytics/{analyticsId}/messages', + validate: { + params: schema.object({ analyticsId: schema.string() }), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { analyticsId } = request.params; + const { getAnalyticsAuditMessages } = analyticsAuditMessagesProvider( + context.ml!.mlClient.callAsCurrentUser + ); + + const results = await getAnalyticsAuditMessages(analyticsId); + return response.ok({ + body: results, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); +}