import { schema } from '@kbn/config-schema' ; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey' ; import { ElasticsearchClient , KibanaResponseFactory , RequestHandler , RequestHandlerContext , SavedObjectsClientContract , } from 'kibana/server' ; import { TRANSFORM_STATE } from '../../../common/constants' ; import { transformIdParamSchema , ResponseStatus , TransformIdParamSchema , } from '../../../common/api_schemas/common' ; import { deleteTransformsRequestSchema , DeleteTransformsRequestSchema , DeleteTransformsResponseSchema , } from '../../../common/api_schemas/delete_transforms' ; import { startTransformsRequestSchema , StartTransformsRequestSchema , StartTransformsResponseSchema , } from '../../../common/api_schemas/start_transforms' ; import { stopTransformsRequestSchema , StopTransformsRequestSchema , StopTransformsResponseSchema , } from '../../../common/api_schemas/stop_transforms' ; import { postTransformsUpdateRequestSchema , PostTransformsUpdateRequestSchema , } from '../../../common/api_schemas/update_transforms' ; import { postTransformsPreviewRequestSchema , PostTransformsPreviewRequestSchema , putTransformsRequestSchema , PutTransformsRequestSchema , PutTransformsResponseSchema , } from '../../../common/api_schemas/transforms' ; import { RouteDependencies } from '../../types' ; import { addBasePath } from '../index' ; import { isRequestTimeout , fillResultsWithTimeouts , wrapError , wrapEsError } from './error_utils' ; import { registerTransformsAuditMessagesRoutes } from './transforms_audit_messages' ; import { registerTransformNodesRoutes } from './transforms_nodes' ; import { IIndexPattern } from '../../../../../../src/plugins/data/common' ; import { isLatestTransform } from '../../../common/types/transform' ; import { isKeywordDuplicate } from '../../../common/utils/field_utils' ; import { transformHealthServiceProvider } from '../../lib/alerting/transform_health_rule_type/transform_health_service' ; enum TRANSFORM_ACTIONS { STOP = 'stop' , START = 'start' , DELETE = 'delete' } export function registerTransformsRoutes ( routeDependencies ) { const { router , license } = routeDependencies ; apiGroup api apiName apiDescription apiSchema apiSchema router . get ( { path : addBasePath ( 'transforms' ) , validate : false } , license . guardApiRoute < estypes . TransformGetTransformRequest , undefined , undefined > ( async ( ctx , req , res ) => { try { const { body } = await ctx . core . elasticsearch . client . asCurrentUser . transform . getTransform ( { size : 1000 , ... req . params , } ) ; if ( ctx . alerting ) { const transformHealthService = transformHealthServiceProvider ( ctx . core . elasticsearch . client . asCurrentUser , ctx . alerting . getRulesClient ( ) ) ; await transformHealthService . populateTransformsWithAssignedRules ( body . transforms ) ; } return res . ok ( { body } ) ; } catch ( e ) { return res . customError ( wrapError ( wrapEsError ( e ) ) ) ; } } ) ) ; apiGroup api apiName apiDescription apiSchema router . get < TransformIdParamSchema , undefined , undefined > ( { path : addBasePath ( 'transforms/{transformId}' ) , validate : { params : transformIdParamSchema } , } , license . guardApiRoute < TransformIdParamSchema , undefined , undefined > ( async ( ctx , req , res ) => { const { transformId } = req . params ; try { const { body } = await ctx . core . elasticsearch . client . asCurrentUser . transform . getTransform ( { transform_id : transformId , } ) ; return res . ok ( { body } ) ; } catch ( e ) { return res . customError ( wrapError ( wrapEsError ( e ) ) ) ; } } ) ) ; apiGroup api apiName apiDescription router . get ( { path : addBasePath ( 'transforms/_stats' ) , validate : false } , license . guardApiRoute < estypes . TransformGetTransformStatsResponse , undefined , undefined > ( async ( ctx , req , res ) => { try { const { body } = await ctx . core . elasticsearch . client . asCurrentUser . transform . getTransformStats ( { size : 1000 , transform_id : '_all' , } ) ; return res . ok ( { body } ) ; } catch ( e ) { return res . customError ( wrapError ( wrapEsError ( e ) ) ) ; } } ) ) ; apiGroup api apiName apiDescription apiSchema router . get < TransformIdParamSchema , undefined , undefined > ( { path : addBasePath ( 'transforms/{transformId}/_stats' ) , validate : { params : transformIdParamSchema } , } , license . guardApiRoute < TransformIdParamSchema , undefined , undefined > ( async ( ctx , req , res ) => { const { transformId } = req . params ; try { const { body } = await ctx . core . elasticsearch . client . asCurrentUser . transform . getTransformStats ( { transform_id : transformId , } ) ; return res . ok ( { body } ) ; } catch ( e ) { return res . customError ( wrapError ( wrapEsError ( e ) ) ) ; } } ) ) ; apiGroup api apiName apiDescription apiSchema apiSchema router . put < TransformIdParamSchema , undefined , PutTransformsRequestSchema > ( { path : addBasePath ( 'transforms/{transformId}' ) , validate : { params : transformIdParamSchema , body : putTransformsRequestSchema , } , } , license . guardApiRoute < TransformIdParamSchema , undefined , PutTransformsRequestSchema > ( async ( ctx , req , res ) => { const { transformId } = req . params ; const response = { transformsCreated : [ ] , errors : [ ] , } ; await ctx . core . elasticsearch . client . asCurrentUser . transform . putTransform ( { body : req . body , transform_id : transformId , } ) . then ( ( ) => { response . transformsCreated . push ( { transform : transformId } ) ; } ) . catch ( ( e ) => response . errors . push ( { id : transformId , error : wrapEsError ( e ) , } ) ) ; return res . ok ( { body : response } ) ; } ) ) ; apiGroup api apiName apiDescription apiSchema apiSchema router . post < TransformIdParamSchema , undefined , PostTransformsUpdateRequestSchema > ( { path : addBasePath ( 'transforms/{transformId}/_update' ) , validate : { params : transformIdParamSchema , body : postTransformsUpdateRequestSchema , } , } , license . guardApiRoute < TransformIdParamSchema , undefined , PostTransformsUpdateRequestSchema > ( async ( ctx , req , res ) => { const { transformId } = req . params ; try { const { body } = await ctx . core . elasticsearch . client . asCurrentUser . transform . updateTransform ( { body : req . body , transform_id : transformId , } ) ; return res . ok ( { body , } ) ; } catch ( e ) { return res . customError ( wrapError ( e ) ) ; } } ) ) ; apiGroup api apiName apiDescription apiSchema router . post < undefined , undefined , DeleteTransformsRequestSchema > ( { path : addBasePath ( 'delete_transforms' ) , validate : { body : deleteTransformsRequestSchema , } , } , license . guardApiRoute < undefined , undefined , DeleteTransformsRequestSchema > ( async ( ctx , req , res ) => { try { const body = await deleteTransforms ( req . body , ctx , res ) ; if ( body && body . status ) { if ( body . status === 404 ) { return res . notFound ( ) ; } if ( body . status === 403 ) { return res . forbidden ( ) ; } } return res . ok ( { body , } ) ; } catch ( e ) { return res . customError ( wrapError ( wrapEsError ( e ) ) ) ; } } ) ) ; apiGroup api apiName apiDescription apiSchema router . post < undefined , undefined , PostTransformsPreviewRequestSchema > ( { path : addBasePath ( 'transforms/_preview' ) , validate : { body : postTransformsPreviewRequestSchema , } , } , license . guardApiRoute < undefined , undefined , PostTransformsPreviewRequestSchema > ( previewTransformHandler ) ) ; apiGroup api apiName apiDescription apiSchema router . post < undefined , undefined , StartTransformsRequestSchema > ( { path : addBasePath ( 'start_transforms' ) , validate : { body : startTransformsRequestSchema , } , } , license . guardApiRoute < undefined , undefined , StartTransformsRequestSchema > ( startTransformsHandler ) ) ; apiGroup api apiName apiDescription apiSchema router . post < undefined , undefined , StopTransformsRequestSchema > ( { path : addBasePath ( 'stop_transforms' ) , validate : { body : stopTransformsRequestSchema , } , } , license . guardApiRoute < undefined , undefined , StopTransformsRequestSchema > ( stopTransformsHandler ) ) ; apiGroup api apiName apiDescription apiSchema router . post ( { path : addBasePath ( 'es_search' ) , validate : { body : schema . maybe ( schema . any ( ) ) , } , } , license . guardApiRoute ( async ( ctx , req , res ) => { try { const { body } = await ctx . core . elasticsearch . client . asCurrentUser . search ( req . body ) ; return res . ok ( { body } ) ; } catch ( e ) { return res . customError ( wrapError ( wrapEsError ( e ) ) ) ; } } ) ) ; registerTransformsAuditMessagesRoutes ( routeDependencies ) ; registerTransformNodesRoutes ( routeDependencies ) ; } async function getIndexPatternId ( indexName , savedObjectsClient ) { const response = await savedObjectsClient . find < IIndexPattern > ( { type : 'index-pattern' , perPage : 1 , search : `"${ indexName }"` , searchFields : [ 'title' ] , fields : [ 'title' ] , } ) ; const ip = response . saved_objects . find ( ( obj ) => obj . attributes . title === indexName ) ; return ip ?. id ; } async function deleteDestIndexPatternById ( indexPatternId , savedObjectsClient ) { return await savedObjectsClient . delete ( 'index-pattern' , indexPatternId ) ; } async function deleteTransforms ( reqBody , ctx , response ) { const { transformsInfo } = reqBody ; const deleteDestIndex = ! ! reqBody . deleteDestIndex ; const deleteDestIndexPattern = ! ! reqBody . deleteDestIndexPattern ; const shouldForceDelete = ! ! reqBody . forceDelete ; const results = { } ; for ( const transformInfo of transformsInfo ) { let destinationIndex ; const transformDeleted = { success : false } ; const destIndexDeleted = { success : false } ; const destIndexPatternDeleted = { success : false , } ; const transformId = transformInfo . id ; let needToForceDelete = false ; try { if ( transformInfo . state === TRANSFORM_STATE . FAILED ) { needToForceDelete = true ; } try { const { body } = await ctx . core . elasticsearch . client . asCurrentUser . transform . getTransform ( { transform_id : transformId , } ) ; const transformConfig = body . transforms [ 0 ] ; destinationIndex = Array . isArray ( transformConfig . dest . index ) ? transformConfig . dest . index [ 0 ] : transformConfig . dest . index ; } catch ( getTransformConfigError ) { transformDeleted . error = getTransformConfigError . meta . body . error ; results [ transformId ] = { transformDeleted , destIndexDeleted , destIndexPatternDeleted , destinationIndex , } ; continue ; } if ( destinationIndex && deleteDestIndex ) { try { await ctx . core . elasticsearch . client . asCurrentUser . indices . delete ( { index : destinationIndex , } ) ; destIndexDeleted . success = true ; } catch ( deleteIndexError ) { destIndexDeleted . error = deleteIndexError . meta . body . error ; } } if ( destinationIndex && deleteDestIndexPattern ) { try { const indexPatternId = await getIndexPatternId ( destinationIndex , ctx . core . savedObjects . client ) ; if ( indexPatternId ) { await deleteDestIndexPatternById ( indexPatternId , ctx . core . savedObjects . client ) ; destIndexPatternDeleted . success = true ; } } catch ( deleteDestIndexPatternError ) { destIndexPatternDeleted . error = deleteDestIndexPatternError . meta . body . error ; } } try { await ctx . core . elasticsearch . client . asCurrentUser . transform . deleteTransform ( { transform_id : transformId , force : shouldForceDelete && needToForceDelete , } ) ; transformDeleted . success = true ; } catch ( deleteTransformJobError ) { transformDeleted . error = deleteTransformJobError . meta . body . error ; if ( deleteTransformJobError . statusCode === 403 ) { return response . forbidden ( ) ; } } results [ transformId ] = { transformDeleted , destIndexDeleted , destIndexPatternDeleted , destinationIndex , } ; } catch ( e ) { if ( isRequestTimeout ( e ) ) { return fillResultsWithTimeouts ( { results , id : transformInfo . id , items : transformsInfo , action : TRANSFORM_ACTIONS . DELETE , } ) ; } results [ transformId ] = { transformDeleted : { success : false , error : e . meta . body . error } } ; } } return results ; } const previewTransformHandler = async ( ctx , req , res ) => { try { const reqBody = req . body ; const { body } = await ctx . core . elasticsearch . client . asCurrentUser . transform . previewTransform ( { body : reqBody , } ) ; if ( isLatestTransform ( reqBody ) ) { const fieldCapsResponse = await ctx . core . elasticsearch . client . asCurrentUser . fieldCaps ( { index : reqBody . source . index , fields : '*' , include_unmapped : false , } ) ; const fieldNamesSet = new Set ( Object . keys ( fieldCapsResponse . body . fields ) ) ; const fields = Object . entries ( fieldCapsResponse . body . fields as Record < string , Record < string , { type : string ; } > > ) . reduce ( ( acc , [ fieldName , fieldCaps ] ) => { const fieldDefinition = Object . values ( fieldCaps ) [ 0 ] ; const isMetaField = fieldDefinition . type . startsWith ( '_' ) || fieldName === '_doc_count' ; if ( isMetaField || isKeywordDuplicate ( fieldName , fieldNamesSet ) ) { return acc ; } acc [ fieldName ] = { ... fieldDefinition } ; return acc ; } , { } as Record < string , { type : string ; } > ) ; body . generated_dest_index . mappings ! . properties = fields as Record < string , estypes . MappingProperty > ; } return res . ok ( { body } ) ; } catch ( e ) { return res . customError ( wrapError ( wrapEsError ( e ) ) ) ; } } ; const startTransformsHandler = async ( ctx , req , res ) => { const transformsInfo = req . body ; try { const body = await startTransforms ( transformsInfo , ctx . core . elasticsearch . client . asCurrentUser ) ; return res . ok ( { body , } ) ; } catch ( e ) { return res . customError ( wrapError ( wrapEsError ( e ) ) ) ; } } ; async function startTransforms ( transformsInfo , esClient ) { const results = { } ; for ( const transformInfo of transformsInfo ) { const transformId = transformInfo . id ; try { await esClient . transform . startTransform ( { transform_id : transformId , } ) ; results [ transformId ] = { success : true } ; } catch ( e ) { if ( isRequestTimeout ( e ) ) { return fillResultsWithTimeouts ( { results , id : transformId , items : transformsInfo , action : TRANSFORM_ACTIONS . START , } ) ; } results [ transformId ] = { success : false , error : e . meta . body . error } ; } } return results ; } const stopTransformsHandler = async ( ctx , req , res ) => { const transformsInfo = req . body ; try { return res . ok ( { body : await stopTransforms ( transformsInfo , ctx . core . elasticsearch . client . asCurrentUser ) , } ) ; } catch ( e ) { return res . customError ( wrapError ( wrapEsError ( e ) ) ) ; } } ; async function stopTransforms ( transformsInfo , esClient ) { const results = { } ; for ( const transformInfo of transformsInfo ) { const transformId = transformInfo . id ; try { await esClient . transform . stopTransform ( { transform_id : transformId , force : transformInfo . state !== undefined ? transformInfo . state === TRANSFORM_STATE . FAILED : false , wait_for_completion : true , } ) ; results [ transformId ] = { success : true } ; } catch ( e ) { if ( isRequestTimeout ( e ) ) { return fillResultsWithTimeouts ( { results , id : transformId , items : transformsInfo , action : TRANSFORM_ACTIONS . STOP , } ) ; } results [ transformId ] = { success : false , error : e . meta . body . error } ; } } return results ; }