From cc21b64cd00d26072ef541ea3e3fd94fe1fe8689 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 13 Feb 2020 12:12:28 -0500 Subject: [PATCH] [ML] New Platform server shim: update job service routes to use new platform router (#57403) * wip: convert jobService route file to TS and use NP router * add schema definitions for route params * add api docs description for routes * update schema and rename client * update calendarManager * fix typo in schema * use NP context savedObjectsClient for rollup true * request no longer passed to JobServiceProvider * update anomalyDetectors schema for job update * add missing key to anomalydetectors schema --- .../services/ml_api_service/jobs.js | 2 +- .../models/calendar/calendar_manager.ts | 10 +- .../ml/server/models/job_service/groups.js | 2 +- .../ml/server/models/job_service/index.js | 16 +- .../ml/server/models/job_service/jobs.js | 2 +- .../job_service/new_job_caps/field_service.ts | 19 +- .../new_job_caps/new_job_caps.test.ts | 30 +- .../job_service/new_job_caps/new_job_caps.ts | 14 +- .../models/job_service/new_job_caps/rollup.ts | 9 +- .../new_platform/anomaly_detectors_schema.ts | 24 +- .../server/new_platform/job_service_schema.ts | 75 +++ .../plugins/ml/server/routes/apidoc.json | 21 +- .../plugins/ml/server/routes/calendars.ts | 12 +- .../plugins/ml/server/routes/job_service.js | 319 --------- .../plugins/ml/server/routes/job_service.ts | 610 ++++++++++++++++++ 15 files changed, 781 insertions(+), 384 deletions(-) create mode 100644 x-pack/legacy/plugins/ml/server/new_platform/job_service_schema.ts delete mode 100644 x-pack/legacy/plugins/ml/server/routes/job_service.js create mode 100644 x-pack/legacy/plugins/ml/server/routes/job_service.ts diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js index cc9593d946bd1a..1ac391c7f84ae3 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js @@ -21,7 +21,7 @@ export const jobs = { jobsWithTimerange(dateFormatTz) { return http({ - url: `${basePath()}/jobs/jobs_with_timerange`, + url: `${basePath()}/jobs/jobs_with_time_range`, method: 'POST', data: { dateFormatTz, diff --git a/x-pack/legacy/plugins/ml/server/models/calendar/calendar_manager.ts b/x-pack/legacy/plugins/ml/server/models/calendar/calendar_manager.ts index 2487943b5efc07..61f21c316be233 100644 --- a/x-pack/legacy/plugins/ml/server/models/calendar/calendar_manager.ts +++ b/x-pack/legacy/plugins/ml/server/models/calendar/calendar_manager.ts @@ -6,6 +6,7 @@ import { difference } from 'lodash'; import Boom from 'boom'; +import { IScopedClusterClient } from 'src/core/server'; import { EventManager, CalendarEvent } from './event_manager'; interface BasicCalendar { @@ -23,13 +24,12 @@ export interface FormCalendar extends BasicCalendar { } export class CalendarManager { - private _client: any; + private _client: IScopedClusterClient['callAsCurrentUser']; private _eventManager: any; - constructor(isLegacy: boolean, client: any) { - const actualClient = isLegacy === true ? client : client.ml!.mlClient.callAsCurrentUser; - this._client = actualClient; - this._eventManager = new EventManager(actualClient); + constructor(client: any) { + this._client = client; + this._eventManager = new EventManager(client); } async getCalendar(calendarId: string) { diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/groups.js b/x-pack/legacy/plugins/ml/server/models/job_service/groups.js index 58237b2a8a7301..91f82f04a9a0c4 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/groups.js +++ b/x-pack/legacy/plugins/ml/server/models/job_service/groups.js @@ -7,7 +7,7 @@ import { CalendarManager } from '../calendar'; export function groupsProvider(callWithRequest) { - const calMngr = new CalendarManager(true, callWithRequest); + const calMngr = new CalendarManager(callWithRequest); async function getAllGroups() { const groups = {}; diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/index.js b/x-pack/legacy/plugins/ml/server/models/job_service/index.js index 5c0eff3112a53e..6f409e70e68b8f 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/index.js +++ b/x-pack/legacy/plugins/ml/server/models/job_service/index.js @@ -14,14 +14,14 @@ import { topCategoriesProvider, } from './new_job'; -export function jobServiceProvider(callWithRequest, request) { +export function jobServiceProvider(callAsCurrentUser) { return { - ...datafeedsProvider(callWithRequest), - ...jobsProvider(callWithRequest), - ...groupsProvider(callWithRequest), - ...newJobCapsProvider(callWithRequest, request), - ...newJobChartsProvider(callWithRequest, request), - ...categorizationExamplesProvider(callWithRequest, request), - ...topCategoriesProvider(callWithRequest, request), + ...datafeedsProvider(callAsCurrentUser), + ...jobsProvider(callAsCurrentUser), + ...groupsProvider(callAsCurrentUser), + ...newJobCapsProvider(callAsCurrentUser), + ...newJobChartsProvider(callAsCurrentUser), + ...categorizationExamplesProvider(callAsCurrentUser), + ...topCategoriesProvider(callAsCurrentUser), }; } diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/jobs.js b/x-pack/legacy/plugins/ml/server/models/job_service/jobs.js index e60593c9f0ed59..b4b476c1f926ea 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/jobs.js +++ b/x-pack/legacy/plugins/ml/server/models/job_service/jobs.js @@ -22,7 +22,7 @@ export function jobsProvider(callWithRequest) { const { forceDeleteDatafeed, getDatafeedIdsByJobId } = datafeedsProvider(callWithRequest); const { getAuditMessagesSummary } = jobAuditMessagesProvider(callWithRequest); const { getLatestBucketTimestampByJob } = resultsServiceProvider(callWithRequest); - const calMngr = new CalendarManager(true, callWithRequest); + const calMngr = new CalendarManager(callWithRequest); async function forceDeleteJob(jobId) { return callWithRequest('ml.deleteJob', { jobId, force: true }); diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/field_service.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/field_service.ts index 3cfb5521890628..5827201a636619 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/field_service.ts +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/field_service.ts @@ -5,7 +5,7 @@ */ import { cloneDeep } from 'lodash'; -import { Request } from 'src/legacy/server/kbn_server'; +import { SavedObjectsClientContract } from 'kibana/server'; import { Field, Aggregation, @@ -40,22 +40,27 @@ export function fieldServiceProvider( indexPattern: string, isRollup: boolean, callWithRequest: any, - request: Request + savedObjectsClient: SavedObjectsClientContract ) { - return new FieldsService(indexPattern, isRollup, callWithRequest, request); + return new FieldsService(indexPattern, isRollup, callWithRequest, savedObjectsClient); } class FieldsService { private _indexPattern: string; private _isRollup: boolean; private _callWithRequest: any; - private _request: Request; + private _savedObjectsClient: SavedObjectsClientContract; - constructor(indexPattern: string, isRollup: boolean, callWithRequest: any, request: Request) { + constructor( + indexPattern: string, + isRollup: boolean, + callWithRequest: any, + savedObjectsClient: any + ) { this._indexPattern = indexPattern; this._isRollup = isRollup; this._callWithRequest = callWithRequest; - this._request = request; + this._savedObjectsClient = savedObjectsClient; } private async loadFieldCaps(): Promise { @@ -104,7 +109,7 @@ class FieldsService { const rollupService = await rollupServiceProvider( this._indexPattern, this._callWithRequest, - this._request + this._savedObjectsClient ); const rollupConfigs: RollupJob[] | null = await rollupService.getRollupJobs(); diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts index 2c8f8a8f82fb8a..f1af7614b42321 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts @@ -18,7 +18,7 @@ import cloudwatchJobCaps from './__mocks__/results/cloudwatch_rollup_job_caps.js describe('job_service - job_caps', () => { let callWithRequestNonRollupMock: jest.Mock; let callWithRequestRollupMock: jest.Mock; - let requestMock: any; + let savedObjectsClientMock: any; beforeEach(() => { callWithRequestNonRollupMock = jest.fn((action: string) => { @@ -37,14 +37,10 @@ describe('job_service - job_caps', () => { } }); - requestMock = { - getSavedObjectsClient: jest.fn(() => { - return { - async find() { - return Promise.resolve(kibanaSavedObjects); - }, - }; - }), + savedObjectsClientMock = { + async find() { + return Promise.resolve(kibanaSavedObjects); + }, }; }); @@ -52,8 +48,8 @@ describe('job_service - job_caps', () => { it('can get job caps for index pattern', async done => { const indexPattern = 'farequote-*'; const isRollup = false; - const { newJobCaps } = newJobCapsProvider(callWithRequestNonRollupMock, requestMock); - const response = await newJobCaps(indexPattern, isRollup); + const { newJobCaps } = newJobCapsProvider(callWithRequestNonRollupMock); + const response = await newJobCaps(indexPattern, isRollup, savedObjectsClientMock); expect(response).toEqual(farequoteJobCaps); done(); }); @@ -61,8 +57,8 @@ describe('job_service - job_caps', () => { it('can get rollup job caps for non rollup index pattern', async done => { const indexPattern = 'farequote-*'; const isRollup = true; - const { newJobCaps } = newJobCapsProvider(callWithRequestNonRollupMock, requestMock); - const response = await newJobCaps(indexPattern, isRollup); + const { newJobCaps } = newJobCapsProvider(callWithRequestNonRollupMock); + const response = await newJobCaps(indexPattern, isRollup, savedObjectsClientMock); expect(response).toEqual(farequoteJobCapsEmpty); done(); }); @@ -72,8 +68,8 @@ describe('job_service - job_caps', () => { it('can get rollup job caps for rollup index pattern', async done => { const indexPattern = 'cloud_roll_index'; const isRollup = true; - const { newJobCaps } = newJobCapsProvider(callWithRequestRollupMock, requestMock); - const response = await newJobCaps(indexPattern, isRollup); + const { newJobCaps } = newJobCapsProvider(callWithRequestRollupMock); + const response = await newJobCaps(indexPattern, isRollup, savedObjectsClientMock); expect(response).toEqual(cloudwatchJobCaps); done(); }); @@ -81,8 +77,8 @@ describe('job_service - job_caps', () => { it('can get non rollup job caps for rollup index pattern', async done => { const indexPattern = 'cloud_roll_index'; const isRollup = false; - const { newJobCaps } = newJobCapsProvider(callWithRequestRollupMock, requestMock); - const response = await newJobCaps(indexPattern, isRollup); + const { newJobCaps } = newJobCapsProvider(callWithRequestRollupMock); + const response = await newJobCaps(indexPattern, isRollup, savedObjectsClientMock); expect(response).not.toEqual(cloudwatchJobCaps); done(); }); diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts index cbb249be09aa0c..3a9d979ccb22ca 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Request } from 'src/legacy/server/kbn_server'; +import { SavedObjectsClientContract } from 'kibana/server'; import { Aggregation, Field, NewJobCaps } from '../../../../common/types/fields'; import { fieldServiceProvider } from './field_service'; @@ -12,12 +12,18 @@ interface NewJobCapsResponse { [indexPattern: string]: NewJobCaps; } -export function newJobCapsProvider(callWithRequest: any, request: Request) { +export function newJobCapsProvider(callWithRequest: any) { async function newJobCaps( indexPattern: string, - isRollup: boolean = false + isRollup: boolean = false, + savedObjectsClient: SavedObjectsClientContract ): Promise { - const fieldService = fieldServiceProvider(indexPattern, isRollup, callWithRequest, request); + const fieldService = fieldServiceProvider( + indexPattern, + isRollup, + callWithRequest, + savedObjectsClient + ); const { aggs, fields } = await fieldService.getData(); convertForStringify(aggs, fields); diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/rollup.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/rollup.ts index 5f8d8ae5c1f258..11b0802192e1f9 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/rollup.ts +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/rollup.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Request } from 'src/legacy/server/kbn_server'; import { SavedObject } from 'src/core/server'; +import { SavedObjectsClientContract } from 'kibana/server'; import { FieldId } from '../../../../common/types/fields'; import { ES_AGGREGATION } from '../../../../common/constants/aggregation_types'; @@ -21,9 +21,9 @@ export interface RollupJob { export async function rollupServiceProvider( indexPattern: string, callWithRequest: any, - request: Request + savedObjectsClient: SavedObjectsClientContract ) { - const rollupIndexPatternObject = await loadRollupIndexPattern(indexPattern, request); + const rollupIndexPatternObject = await loadRollupIndexPattern(indexPattern, savedObjectsClient); let jobIndexPatterns: string[] = [indexPattern]; async function getRollupJobs(): Promise { @@ -57,9 +57,8 @@ export async function rollupServiceProvider( async function loadRollupIndexPattern( indexPattern: string, - request: Request + savedObjectsClient: SavedObjectsClientContract ): Promise { - const savedObjectsClient = request.getSavedObjectsClient(); const resp = await savedObjectsClient.find({ type: 'index-pattern', fields: ['title', 'type', 'typeMeta'], diff --git a/x-pack/legacy/plugins/ml/server/new_platform/anomaly_detectors_schema.ts b/x-pack/legacy/plugins/ml/server/new_platform/anomaly_detectors_schema.ts index 392d3bfd847684..d728fbf312d767 100644 --- a/x-pack/legacy/plugins/ml/server/new_platform/anomaly_detectors_schema.ts +++ b/x-pack/legacy/plugins/ml/server/new_platform/anomaly_detectors_schema.ts @@ -6,6 +6,18 @@ import { schema } from '@kbn/config-schema'; +const customRulesSchema = schema.maybe( + schema.arrayOf( + schema.maybe( + schema.object({ + actions: schema.arrayOf(schema.string()), + conditions: schema.arrayOf(schema.any()), + scope: schema.maybe(schema.any()), + }) + ) + ) +); + const detectorSchema = schema.object({ identifier: schema.maybe(schema.string()), function: schema.string(), @@ -14,6 +26,7 @@ const detectorSchema = schema.object({ over_field_name: schema.maybe(schema.string()), partition_field_name: schema.maybe(schema.string()), detector_description: schema.maybe(schema.string()), + custom_rules: customRulesSchema, }); const customUrlSchema = { @@ -34,15 +47,8 @@ export const anomalyDetectionUpdateJobSchema = { schema.maybe( schema.object({ detector_index: schema.number(), - custom_rules: schema.arrayOf( - schema.maybe( - schema.object({ - actions: schema.arrayOf(schema.string()), - conditions: schema.arrayOf(schema.any()), - scope: schema.maybe(schema.any()), - }) - ) - ), + description: schema.maybe(schema.string()), + custom_rules: customRulesSchema, }) ) ) diff --git a/x-pack/legacy/plugins/ml/server/new_platform/job_service_schema.ts b/x-pack/legacy/plugins/ml/server/new_platform/job_service_schema.ts new file mode 100644 index 00000000000000..3adc3302345318 --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/new_platform/job_service_schema.ts @@ -0,0 +1,75 @@ +/* + * 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'; + +const analyzerSchema = { + tokenizer: schema.string(), + filter: schema.arrayOf( + schema.object({ + type: schema.string(), + stopwords: schema.arrayOf(schema.maybe(schema.string())), + }) + ), +}; + +export const categorizationFieldExamplesSchema = { + indexPatternTitle: schema.string(), + query: schema.any(), + size: schema.number(), + field: schema.string(), + timeField: schema.maybe(schema.string()), + start: schema.number(), + end: schema.number(), + analyzer: schema.object(analyzerSchema), +}; + +export const chartSchema = { + indexPatternTitle: schema.string(), + timeField: schema.maybe(schema.string()), + start: schema.maybe(schema.number()), + end: schema.maybe(schema.number()), + intervalMs: schema.number(), + query: schema.any(), + aggFieldNamePairs: schema.arrayOf(schema.any()), + splitFieldName: schema.maybe(schema.nullable(schema.string())), + splitFieldValue: schema.maybe(schema.nullable(schema.string())), +}; + +export const datafeedIdsSchema = { datafeedIds: schema.arrayOf(schema.maybe(schema.string())) }; + +export const forceStartDatafeedSchema = { + datafeedIds: schema.arrayOf(schema.maybe(schema.string())), + start: schema.maybe(schema.number()), + end: schema.maybe(schema.number()), +}; + +export const jobIdsSchema = { + jobIds: schema.maybe( + schema.oneOf([schema.string(), schema.arrayOf(schema.maybe(schema.string()))]) + ), +}; + +export const jobsWithTimerangeSchema = { dateFormatTz: schema.maybe(schema.string()) }; + +export const lookBackProgressSchema = { + jobId: schema.string(), + start: schema.maybe(schema.number()), + end: schema.maybe(schema.number()), +}; + +export const topCategoriesSchema = { jobId: schema.string(), count: schema.number() }; + +export const updateGroupsSchema = { + jobs: schema.maybe( + schema.arrayOf( + schema.object({ + job_id: schema.maybe(schema.string()), + groups: schema.arrayOf(schema.maybe(schema.string())), + }) + ) + ), +}; diff --git a/x-pack/legacy/plugins/ml/server/routes/apidoc.json b/x-pack/legacy/plugins/ml/server/routes/apidoc.json index 919592f8ed62af..3fac715fef85a0 100644 --- a/x-pack/legacy/plugins/ml/server/routes/apidoc.json +++ b/x-pack/legacy/plugins/ml/server/routes/apidoc.json @@ -50,6 +50,25 @@ "Annotations", "GetAnnotations", "IndexAnnotations", - "DeleteAnnotation" + "DeleteAnnotation", + "JobService", + "ForceStartDatafeeds", + "StopDatafeeds", + "DeleteJobs", + "CloseJobs", + "JobsSummary", + "JobsWithTimerange", + "CreateFullJobsList", + "GetAllGroups", + "UpdateGroups", + "DeletingJobTasks", + "JobsExist", + "NewJobCaps", + "NewJobLineChart", + "NewJobPopulationChart", + "GetAllJobAndGroupIds", + "GetLookBackProgress", + "ValidateCategoryExamples", + "TopCategories" ] } diff --git a/x-pack/legacy/plugins/ml/server/routes/calendars.ts b/x-pack/legacy/plugins/ml/server/routes/calendars.ts index 19d614a4e6a228..8e4e1c4c14751d 100644 --- a/x-pack/legacy/plugins/ml/server/routes/calendars.ts +++ b/x-pack/legacy/plugins/ml/server/routes/calendars.ts @@ -13,32 +13,32 @@ import { calendarSchema } from '../new_platform/calendars_schema'; import { CalendarManager, Calendar, FormCalendar } from '../models/calendar'; function getAllCalendars(context: RequestHandlerContext) { - const cal = new CalendarManager(false, context); + const cal = new CalendarManager(context.ml!.mlClient.callAsCurrentUser); return cal.getAllCalendars(); } function getCalendar(context: RequestHandlerContext, calendarId: string) { - const cal = new CalendarManager(false, context); + const cal = new CalendarManager(context.ml!.mlClient.callAsCurrentUser); return cal.getCalendar(calendarId); } function newCalendar(context: RequestHandlerContext, calendar: FormCalendar) { - const cal = new CalendarManager(false, context); + const cal = new CalendarManager(context.ml!.mlClient.callAsCurrentUser); return cal.newCalendar(calendar); } function updateCalendar(context: RequestHandlerContext, calendarId: string, calendar: Calendar) { - const cal = new CalendarManager(false, context); + const cal = new CalendarManager(context.ml!.mlClient.callAsCurrentUser); return cal.updateCalendar(calendarId, calendar); } function deleteCalendar(context: RequestHandlerContext, calendarId: string) { - const cal = new CalendarManager(false, context); + const cal = new CalendarManager(context.ml!.mlClient.callAsCurrentUser); return cal.deleteCalendar(calendarId); } function getCalendarsByIds(context: RequestHandlerContext, calendarIds: string) { - const cal = new CalendarManager(false, context); + const cal = new CalendarManager(context.ml!.mlClient.callAsCurrentUser); return cal.getCalendarsByIds(calendarIds); } diff --git a/x-pack/legacy/plugins/ml/server/routes/job_service.js b/x-pack/legacy/plugins/ml/server/routes/job_service.js deleted file mode 100644 index a83b4fa403f65c..00000000000000 --- a/x-pack/legacy/plugins/ml/server/routes/job_service.js +++ /dev/null @@ -1,319 +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 { jobServiceProvider } from '../models/job_service'; - -export function jobServiceRoutes({ commonRouteConfig, elasticsearchPlugin, route }) { - route({ - method: 'POST', - path: '/api/ml/jobs/force_start_datafeeds', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { forceStartDatafeeds } = jobServiceProvider(callWithRequest); - const { datafeedIds, start, end } = request.payload; - return forceStartDatafeeds(datafeedIds, start, end).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/stop_datafeeds', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { stopDatafeeds } = jobServiceProvider(callWithRequest); - const { datafeedIds } = request.payload; - return stopDatafeeds(datafeedIds).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/delete_jobs', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { deleteJobs } = jobServiceProvider(callWithRequest); - const { jobIds } = request.payload; - return deleteJobs(jobIds).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/close_jobs', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { closeJobs } = jobServiceProvider(callWithRequest); - const { jobIds } = request.payload; - return closeJobs(jobIds).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/jobs_summary', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { jobsSummary } = jobServiceProvider(callWithRequest); - const { jobIds } = request.payload; - return jobsSummary(jobIds).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/jobs_with_timerange', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { jobsWithTimerange } = jobServiceProvider(callWithRequest); - const { dateFormatTz } = request.payload; - return jobsWithTimerange(dateFormatTz).catch(resp => { - wrapError(resp); - }); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/jobs', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { createFullJobsList } = jobServiceProvider(callWithRequest); - const { jobIds } = request.payload; - return createFullJobsList(jobIds).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'GET', - path: '/api/ml/jobs/groups', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { getAllGroups } = jobServiceProvider(callWithRequest); - return getAllGroups().catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/update_groups', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { updateGroups } = jobServiceProvider(callWithRequest); - const { jobs } = request.payload; - return updateGroups(jobs).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'GET', - path: '/api/ml/jobs/deleting_jobs_tasks', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { deletingJobTasks } = jobServiceProvider(callWithRequest); - return deletingJobTasks().catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/jobs_exist', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { jobsExist } = jobServiceProvider(callWithRequest); - const { jobIds } = request.payload; - return jobsExist(jobIds).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'GET', - path: '/api/ml/jobs/new_job_caps/{indexPattern}', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { indexPattern } = request.params; - const isRollup = request.query.rollup === 'true'; - const { newJobCaps } = jobServiceProvider(callWithRequest, request); - return newJobCaps(indexPattern, isRollup).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/new_job_line_chart', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { - indexPatternTitle, - timeField, - start, - end, - intervalMs, - query, - aggFieldNamePairs, - splitFieldName, - splitFieldValue, - } = request.payload; - const { newJobLineChart } = jobServiceProvider(callWithRequest, request); - return newJobLineChart( - indexPatternTitle, - timeField, - start, - end, - intervalMs, - query, - aggFieldNamePairs, - splitFieldName, - splitFieldValue - ).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/new_job_population_chart', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { - indexPatternTitle, - timeField, - start, - end, - intervalMs, - query, - aggFieldNamePairs, - splitFieldName, - } = request.payload; - const { newJobPopulationChart } = jobServiceProvider(callWithRequest, request); - return newJobPopulationChart( - indexPatternTitle, - timeField, - start, - end, - intervalMs, - query, - aggFieldNamePairs, - splitFieldName - ).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'GET', - path: '/api/ml/jobs/all_jobs_and_group_ids', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { getAllJobAndGroupIds } = jobServiceProvider(callWithRequest); - return getAllJobAndGroupIds().catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/look_back_progress', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { getLookBackProgress } = jobServiceProvider(callWithRequest); - const { jobId, start, end } = request.payload; - return getLookBackProgress(jobId, start, end).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/categorization_field_examples', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { validateCategoryExamples } = jobServiceProvider(callWithRequest); - const { - indexPatternTitle, - timeField, - query, - size, - field, - start, - end, - analyzer, - } = request.payload; - return validateCategoryExamples( - indexPatternTitle, - query, - size, - field, - timeField, - start, - end, - analyzer - ).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/top_categories', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { topCategories } = jobServiceProvider(callWithRequest); - const { jobId, count } = request.payload; - return topCategories(jobId, count).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); -} diff --git a/x-pack/legacy/plugins/ml/server/routes/job_service.ts b/x-pack/legacy/plugins/ml/server/routes/job_service.ts new file mode 100644 index 00000000000000..3af651c92353b2 --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/routes/job_service.ts @@ -0,0 +1,610 @@ +/* + * 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 { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; +import { wrapError } from '../client/error_wrapper'; +import { RouteInitialization } from '../new_platform/plugin'; +import { + categorizationFieldExamplesSchema, + chartSchema, + datafeedIdsSchema, + forceStartDatafeedSchema, + jobIdsSchema, + jobsWithTimerangeSchema, + lookBackProgressSchema, + topCategoriesSchema, + updateGroupsSchema, +} from '../new_platform/job_service_schema'; +// @ts-ignore no declaration module +import { jobServiceProvider } from '../models/job_service'; + +/** + * Routes for job service + */ +export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitialization) { + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/force_start_datafeeds + * @apiName ForceStartDatafeeds + * @apiDescription Starts one or more datafeeds + */ + router.post( + { + path: '/api/ml/jobs/force_start_datafeeds', + validate: { + body: schema.object(forceStartDatafeedSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { forceStartDatafeeds } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { datafeedIds, start, end } = request.body; + const resp = await forceStartDatafeeds(datafeedIds, start, end); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/stop_datafeeds + * @apiName StopDatafeeds + * @apiDescription Stops one or more datafeeds + */ + router.post( + { + path: '/api/ml/jobs/stop_datafeeds', + validate: { + body: schema.object(datafeedIdsSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { stopDatafeeds } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { datafeedIds } = request.body; + const resp = await stopDatafeeds(datafeedIds); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/delete_jobs + * @apiName DeleteJobs + * @apiDescription Deletes an existing anomaly detection job + */ + router.post( + { + path: '/api/ml/jobs/delete_jobs', + validate: { + body: schema.object(jobIdsSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { deleteJobs } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobIds } = request.body; + const resp = await deleteJobs(jobIds); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/close_jobs + * @apiName CloseJobs + * @apiDescription Closes one or more anomaly detection jobs + */ + router.post( + { + path: '/api/ml/jobs/close_jobs', + validate: { + body: schema.object(jobIdsSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { closeJobs } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobIds } = request.body; + const resp = await closeJobs(jobIds); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/jobs_summary + * @apiName JobsSummary + * @apiDescription Creates a summary jobs list. Jobs include job stats, datafeed stats, and calendars. + */ + router.post( + { + path: '/api/ml/jobs/jobs_summary', + validate: { + body: schema.object(jobIdsSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { jobsSummary } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobIds } = request.body; + const resp = await jobsSummary(jobIds); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/jobs_with_time_range + * @apiName JobsWithTimerange + * @apiDescription Creates a list of jobs with data about the job's timerange + */ + router.post( + { + path: '/api/ml/jobs/jobs_with_time_range', + validate: { + body: schema.object(jobsWithTimerangeSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { jobsWithTimerange } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { dateFormatTz } = request.body; + const resp = await jobsWithTimerange(dateFormatTz); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/jobs + * @apiName CreateFullJobsList + * @apiDescription Creates a list of jobs + */ + router.post( + { + path: '/api/ml/jobs/jobs', + validate: { + body: schema.object(jobIdsSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { createFullJobsList } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobIds } = request.body; + const resp = await createFullJobsList(jobIds); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {get} /api/ml/jobs/groups + * @apiName GetAllGroups + * @apiDescription Returns array of group objects with job ids listed for each group + */ + router.get( + { + path: '/api/ml/jobs/groups', + validate: false, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { getAllGroups } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const resp = await getAllGroups(); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/update_groups + * @apiName UpdateGroups + * @apiDescription Updates 'groups' property of an anomaly detection job + */ + router.post( + { + path: '/api/ml/jobs/update_groups', + validate: { + body: schema.object(updateGroupsSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { updateGroups } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobs } = request.body; + const resp = await updateGroups(jobs); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {get} /api/ml/jobs/deleting_jobs_tasks + * @apiName DeletingJobTasks + * @apiDescription Gets the ids of deleting anomaly detection jobs + */ + router.get( + { + path: '/api/ml/jobs/deleting_jobs_tasks', + validate: false, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { deletingJobTasks } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const resp = await deletingJobTasks(); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/jobs_exist + * @apiName JobsExist + * @apiDescription Checks if each of the jobs in the specified list of IDs exist + */ + router.post( + { + path: '/api/ml/jobs/jobs_exist', + validate: { + body: schema.object(jobIdsSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { jobsExist } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobIds } = request.body; + const resp = await jobsExist(jobIds); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {get} /api/ml/jobs/new_job_caps/:indexPattern + * @apiName NewJobCaps + * @apiDescription Retrieve the capabilities of fields for indices + */ + router.get( + { + path: '/api/ml/jobs/new_job_caps/{indexPattern}', + validate: { + params: schema.object({ indexPattern: schema.string() }), + query: schema.maybe(schema.object({ rollup: schema.maybe(schema.string()) })), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { indexPattern } = request.params; + const isRollup = request.query.rollup === 'true'; + const savedObjectsClient = context.core.savedObjects.client; + const { newJobCaps } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const resp = await newJobCaps(indexPattern, isRollup, savedObjectsClient); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/new_job_line_chart + * @apiName NewJobLineChart + * @apiDescription Returns line chart data for anomaly detection job + */ + router.post( + { + path: '/api/ml/jobs/new_job_line_chart', + validate: { + body: schema.object(chartSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { + indexPatternTitle, + timeField, + start, + end, + intervalMs, + query, + aggFieldNamePairs, + splitFieldName, + splitFieldValue, + } = request.body; + + const { newJobLineChart } = jobServiceProvider( + context.ml!.mlClient.callAsCurrentUser, + request + ); + const resp = await newJobLineChart( + indexPatternTitle, + timeField, + start, + end, + intervalMs, + query, + aggFieldNamePairs, + splitFieldName, + splitFieldValue + ); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/new_job_population_chart + * @apiName NewJobPopulationChart + * @apiDescription Returns population job chart data + */ + router.post( + { + path: '/api/ml/jobs/new_job_population_chart', + validate: { + body: schema.object(chartSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { + indexPatternTitle, + timeField, + start, + end, + intervalMs, + query, + aggFieldNamePairs, + splitFieldName, + } = request.body; + + const { newJobPopulationChart } = jobServiceProvider( + context.ml!.mlClient.callAsCurrentUser + ); + const resp = await newJobPopulationChart( + indexPatternTitle, + timeField, + start, + end, + intervalMs, + query, + aggFieldNamePairs, + splitFieldName + ); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {get} /api/ml/jobs/all_jobs_and_group_ids + * @apiName GetAllJobAndGroupIds + * @apiDescription Returns a list of all job IDs and all group IDs + */ + router.get( + { + path: '/api/ml/jobs/all_jobs_and_group_ids', + validate: false, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { getAllJobAndGroupIds } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const resp = await getAllJobAndGroupIds(); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/look_back_progress + * @apiName GetLookBackProgress + * @apiDescription Returns current progress of anomaly detection job + */ + router.post( + { + path: '/api/ml/jobs/look_back_progress', + validate: { + body: schema.object(lookBackProgressSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { getLookBackProgress } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobId, start, end } = request.body; + const resp = await getLookBackProgress(jobId, start, end); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/categorization_field_examples + * @apiName ValidateCategoryExamples + * @apiDescription Validates category examples + */ + router.post( + { + path: '/api/ml/jobs/categorization_field_examples', + validate: { + body: schema.object(categorizationFieldExamplesSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { validateCategoryExamples } = jobServiceProvider( + context.ml!.mlClient.callAsCurrentUser + ); + const { + indexPatternTitle, + timeField, + query, + size, + field, + start, + end, + analyzer, + } = request.body; + + const resp = await validateCategoryExamples( + indexPatternTitle, + query, + size, + field, + timeField, + start, + end, + analyzer + ); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/top_categories + * @apiName TopCategories + * @apiDescription Returns list of top categories + */ + router.post( + { + path: '/api/ml/jobs/top_categories', + validate: { + body: schema.object(topCategoriesSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { topCategories } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobId, count } = request.body; + const resp = await topCategories(jobId, count); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); +}