From f0aec813a2a8c004abe09ad56f0be00b860a3c30 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Sat, 21 Dec 2019 01:32:49 -0800 Subject: [PATCH] Modify flow of the service map initialization to handle less permissive default security settings. The kibana user is responsible for creating and index to the `apm-service-connections` data index, while the apm user is resposible for kicking off the scheduled task and reading from `apm-*` indices. --- x-pack/legacy/plugins/apm/index.ts | 8 - .../components/app/ServiceMap/index.tsx | 53 ++++-- .../apm/server/lib/helpers/es_client.ts | 7 +- .../create_service_connections_index.ts | 47 ++--- .../service_map/initialize_service_maps.ts | 170 ++++++++++++++---- .../lib/service_map/run_service_map_task.ts | 28 ++- .../apm/server/routes/create_apm_api.ts | 6 +- .../plugins/apm/server/routes/service_map.ts | 33 ++++ .../plugins/apm/server/routes/services.ts | 23 --- x-pack/plugins/apm/server/plugin.ts | 2 + 10 files changed, 243 insertions(+), 134 deletions(-) create mode 100644 x-pack/legacy/plugins/apm/server/routes/service_map.ts diff --git a/x-pack/legacy/plugins/apm/index.ts b/x-pack/legacy/plugins/apm/index.ts index 1a78a61254bf0..f2f243cf62c24 100644 --- a/x-pack/legacy/plugins/apm/index.ts +++ b/x-pack/legacy/plugins/apm/index.ts @@ -11,7 +11,6 @@ import { APMPluginContract } from '../../../plugins/apm/server'; import { LegacyPluginInitializer } from '../../../../src/legacy/types'; import mappings from './mappings.json'; import { makeApmUsageCollector } from './server/lib/apm_telemetry'; -import { initializeServiceMaps } from './server/lib/service_map/initialize_service_maps'; export const apm: LegacyPluginInitializer = kibana => { return new kibana.Plugin({ @@ -78,15 +77,12 @@ export const apm: LegacyPluginInitializer = kibana => { autocreateApmIndexPattern: Joi.boolean().default(true), // service map - serviceMapEnabled: Joi.boolean().default(false) }).default(); }, // TODO: get proper types init(server: Server) { - const config = server.config(); - server.plugins.xpack_main.registerFeature({ id: 'apm', name: i18n.translate('xpack.apm.featureRegistry.apmFeatureName', { @@ -123,10 +119,6 @@ export const apm: LegacyPluginInitializer = kibana => { .apm as APMPluginContract; apmPlugin.registerLegacyAPI({ server }); - - if (config.get('xpack.apm.serviceMapEnabled')) { - initializeServiceMaps(server); - } } }); }; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx index cb4f93527d5c5..54decf15e160c 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -12,6 +12,8 @@ import { useUrlParams } from '../../../hooks/useUrlParams'; import { Controls } from './Controls'; import { Cytoscape } from './Cytoscape'; import { PlatinumLicensePrompt } from './PlatinumLicensePrompt'; +import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { callApi } from '../../../services/rest/callApi'; interface ServiceMapProps { serviceName?: string; @@ -51,9 +53,21 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { [uiFilters] ); + const { http } = useApmPluginContext().core; + const { data: serviceMapStartResponse } = useFetcher(async () => { + const response = await callApi<{ + taskStatus: 'initialized' | 'active'; + }>(http, { + method: 'GET', + pathname: `/api/apm/service-map-start-task` + }); + return response; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [http]); + const { data } = useFetcher( callApmApi => { - if (start && end) { + if (start && end && serviceMapStartResponse) { return callApmApi({ pathname: '/api/apm/service-map', params: { @@ -68,23 +82,36 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { }); } }, - [start, end, uiFiltersOmitEnv, environment, serviceName] + [ + start, + end, + uiFiltersOmitEnv, + environment, + serviceName, + serviceMapStartResponse + ] ); const elements = Array.isArray(data) ? data : []; const license = useLicense(); const isValidPlatinumLicense = - license?.isActive && license?.type === 'platinum'; + true || + (license?.isActive && + (license?.type === 'platinum' || license?.type === 'trial')); - return isValidPlatinumLicense ? ( - - - - ) : ( - + return ( + <> + {isValidPlatinumLicense ? ( + + + + ) : ( + + )} + ); } diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts index d3af158687391..db371834b6a63 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts @@ -9,7 +9,8 @@ import { IndexDocumentParams, IndicesDeleteParams, IndicesCreateParams, - BulkIndexDocumentsParams + BulkIndexDocumentsParams, + IndicesExistsParams } from 'elasticsearch'; import { merge } from 'lodash'; import { cloneDeep, isString } from 'lodash'; @@ -143,6 +144,10 @@ export function getESClient( onRequest('indices.create', params); return esClient('indices.create', params); }, + indexExists: (params: IndicesExistsParams) => { + onRequest('indices.exists', params); + return esClient('indices.exists', params); + }, bulk: (params: BulkIndexDocumentsParams) => { onRequest('bulk', params); return esClient('bulk', params); diff --git a/x-pack/legacy/plugins/apm/server/lib/service_map/create_service_connections_index.ts b/x-pack/legacy/plugins/apm/server/lib/service_map/create_service_connections_index.ts index 34c7a413789cd..eb6ee5d385063 100644 --- a/x-pack/legacy/plugins/apm/server/lib/service_map/create_service_connections_index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/service_map/create_service_connections_index.ts @@ -4,45 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Server } from 'hapi'; -import { APMPluginContract } from '../../../../../../plugins/apm/server'; -import { getInternalSavedObjectsClient } from '../helpers/saved_objects_client'; -import { CallCluster } from '../../../../../../../src/legacy/core_plugins/elasticsearch'; import { TIMESTAMP } from '../../../common/elasticsearch_fieldnames'; +import { Setup } from '../helpers/setup_request'; -export async function createServiceConnectionsIndex(server: Server) { - const callCluster = server.plugins.elasticsearch.getCluster('data') - .callWithInternalUser; - const apmPlugin = server.newPlatform.setup.plugins.apm as APMPluginContract; +export async function createServiceConnectionsIndex(setup: Setup) { + const { internalClient, indices } = setup; + const index = indices.apmServiceConnectionsIndex; - try { - const apmIndices = await apmPlugin.getApmIndices( - getInternalSavedObjectsClient(server) - ); - const index = apmIndices.apmServiceConnectionsIndex; - const indexExists = await callCluster('indices.exists', { index }); - if (!indexExists) { - const result = await createNewIndex(index, callCluster); + const indexExists = await internalClient.indexExists({ index }); - if (!result.acknowledged) { - const resultError = - result && result.error && JSON.stringify(result.error); - throw new Error( - `Unable to create APM Service Connections index '${index}': ${resultError}` - ); - } + if (!indexExists) { + const result = await createNewIndex(index, internalClient); + + if (!result.acknowledged) { + const resultError = + result && result.error && JSON.stringify(result.error); + throw new Error( + `Unable to create APM Service Connections index '${index}': ${resultError}` + ); } - } catch (error) { - server.log( - ['apm', 'error'], - `Could not create APM Service Connections: ${error.message}` - ); - throw error; } } -function createNewIndex(index: string, callWithInternalUser: CallCluster) { - return callWithInternalUser('indices.create', { +function createNewIndex(index: string, client: Setup['client']) { + return client.indicesCreate({ index, body: { settings: { 'index.auto_expand_replicas': '0-1' }, diff --git a/x-pack/legacy/plugins/apm/server/lib/service_map/initialize_service_maps.ts b/x-pack/legacy/plugins/apm/server/lib/service_map/initialize_service_maps.ts index de4223cb0e7f7..9ae076d5ade6c 100644 --- a/x-pack/legacy/plugins/apm/server/lib/service_map/initialize_service_maps.ts +++ b/x-pack/legacy/plugins/apm/server/lib/service_map/initialize_service_maps.ts @@ -5,57 +5,149 @@ */ import { Server } from 'hapi'; -// @ts-ignore -import { TaskManager, RunContext } from '../legacy/plugins/task_manager'; +import { CoreSetup, Logger } from 'src/core/server'; +import { Observable } from 'rxjs'; +import { PluginSetupContract as TaskManagerPluginContract } from '../../../../task_manager/plugin'; +import { RunContext, ConcreteTaskInstance } from '../../../../task_manager'; +import { RunFunction } from '../../../../task_manager/task'; +import { APMConfig } from '../../../../../../plugins/apm/server'; import { runServiceMapTask } from './run_service_map_task'; import { SERVICE_MAP_TASK_TYPE, SERVICE_MAP_TASK_ID } from '../../../common/service_map_constants'; import { createServiceConnectionsIndex } from './create_service_connections_index'; +import { setupRequest, Setup } from '../helpers/setup_request'; function isLessThan1Hour(unixTimestamp = 0) { const hourMilliseconds = 60 * 60 * 1000; return Date.now() - unixTimestamp < hourMilliseconds; } -export async function initializeServiceMaps(server: Server) { - await createServiceConnectionsIndex(server); - - const taskManager = server.plugins.task_manager; - if (taskManager) { - taskManager.registerTaskDefinitions({ - [SERVICE_MAP_TASK_TYPE]: { - title: 'ApmServiceMapTask', - type: SERVICE_MAP_TASK_TYPE, - description: 'Extract connections in traces for APM service maps', - timeout: '5m', - createTaskRunner({ taskInstance }: RunContext) { - return { - async run() { - const { state } = taskInstance; - const { latestTransactionTime } = await runServiceMapTask( - server, - isLessThan1Hour(state.latestTransactionTime) - ? state.latestTransactionTime - : 'now-1h' - ); - return { state: { latestTransactionTime } }; +let scopedRunFunction: + | ((taskInstance: ConcreteTaskInstance) => ReturnType) + | undefined; + +function isTaskActive() { + return scopedRunFunction !== undefined; +} + +async function runTask(setup: Setup, state: Record = {}) { + const nextState: typeof state = { + ...state, + isActive: true + }; + try { + const { latestTransactionTime } = await runServiceMapTask( + setup, + isLessThan1Hour(state.latestTransactionTime) + ? state.latestTransactionTime + : 'now-1h' + ); + nextState.latestTransactionTime = latestTransactionTime; + } catch (error) { + scopedRunFunction = undefined; + return { state: nextState, error }; + } + return { state: nextState }; +} + +async function scheduleTask( + taskManager: TaskManagerPluginContract, + runFn: NonNullable, + initialState: Record = {} +) { + scopedRunFunction = runFn; + return await taskManager.ensureScheduled({ + id: SERVICE_MAP_TASK_ID, + taskType: SERVICE_MAP_TASK_TYPE, + schedule: { interval: '1m' }, + scope: ['apm'], + params: {}, + state: initialState + }); +} + +export async function initializeServiceMaps( + core: CoreSetup, + { + config$, + logger, + __LEGACY + }: { + config$: Observable; + logger: Logger; + __LEGACY: { server: Server }; + } +) { + config$.subscribe(config => { + const server = __LEGACY.server; + const router = core.http.createRouter(); + + if (!config['xpack.apm.serviceMapEnabled']) { + return; + } + + const taskManager = server.plugins.task_manager; + if (taskManager) { + taskManager.registerTaskDefinitions({ + [SERVICE_MAP_TASK_TYPE]: { + title: 'ApmServiceMapTask', + type: SERVICE_MAP_TASK_TYPE, + description: 'Extract connections in traces for APM service maps', + timeout: '2m', + maxAttempts: 1, + createTaskRunner({ taskInstance }: RunContext) { + return { + run: async () => { + if (!scopedRunFunction) { + return; + } + return await scopedRunFunction(taskInstance); + } + }; + } + } + }); + + router.get( + { + path: '/api/apm/service-map-start-task', + validate: false + }, + async (context, request, response) => { + if (isTaskActive()) { + return response.ok({ body: { taskStatus: 'active' } }); + } + try { + const setup = await setupRequest( + { + ...context, + __LEGACY, + params: { query: { _debug: false } }, + config, + logger + }, + request + ); + await createServiceConnectionsIndex(setup); + const { state: initialState } = await runTask(setup); // initial task run + await scheduleTask( + taskManager, + (taskInstance: ConcreteTaskInstance) => + runTask(setup, taskInstance.state), // maintain scope in subsequent task runs + initialState + ); + return response.ok({ body: { taskStatus: 'initialized' } }); + } catch (error) { + logger.error(error); + if (error.statusCode === 403) { + return response.forbidden({ body: error }); } - }; + return response.internalError({ body: error }); + } } - } - }); - - return await taskManager.ensureScheduled({ - id: SERVICE_MAP_TASK_ID, - taskType: SERVICE_MAP_TASK_TYPE, - schedule: { - interval: '1m' - }, - scope: ['apm'], - params: {}, - state: {} - }); - } + ); + } + }); } diff --git a/x-pack/legacy/plugins/apm/server/lib/service_map/run_service_map_task.ts b/x-pack/legacy/plugins/apm/server/lib/service_map/run_service_map_task.ts index 87528d0a8ad10..998a52fa278b3 100644 --- a/x-pack/legacy/plugins/apm/server/lib/service_map/run_service_map_task.ts +++ b/x-pack/legacy/plugins/apm/server/lib/service_map/run_service_map_task.ts @@ -4,15 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Server } from 'hapi'; import { uniq } from 'lodash'; -import { APMPluginContract } from '../../../../../../plugins/apm/server'; -import { getESClient, ESClient } from '../helpers/es_client'; +import { ESClient } from '../helpers/es_client'; import { getNextTransactionSamples } from './get_next_transaction_samples'; import { getServiceConnections } from './get_service_connections'; import { mapTraceToBulkServiceConnection } from './map_trace_to_bulk_service_connection'; import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values'; -import { getInternalSavedObjectsClient } from '../helpers/saved_objects_client'; import { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; import { SPAN_TYPE, @@ -25,6 +22,7 @@ import { SERVICE_ENVIRONMENT, DESTINATION_ADDRESS } from '../../../common/elasticsearch_fieldnames'; +import { Setup } from '../helpers/setup_request'; interface MappedDocument { [TIMESTAMP]: string; @@ -47,6 +45,7 @@ export interface TraceConnection { async function indexLatestConnections( apmIndices: ApmIndicesConfig, esClient: ESClient, + internalEsClient: ESClient, startTimeInterval?: string | number, latestTransactionTime = 0, afterKey?: object @@ -92,7 +91,7 @@ async function indexLatestConnections( ); }); if (bulkIndexConnectionDocs.length > 0) { - await esClient.bulk({ + await internalEsClient.bulk({ body: bulkIndexConnectionDocs .map(bulkObject => JSON.stringify(bulkObject)) .join('\n') @@ -101,6 +100,7 @@ async function indexLatestConnections( return await indexLatestConnections( apmIndices, esClient, + internalEsClient, startTimeInterval, nextLatestTransactionTime, nextAfterKey @@ -108,19 +108,13 @@ async function indexLatestConnections( } export async function runServiceMapTask( - server: Server, + setup: Setup, startTimeInterval?: string | number ) { - const callCluster = server.plugins.elasticsearch.getCluster('data') - .callWithInternalUser; - const apmPlugin = server.newPlatform.setup.plugins.apm as APMPluginContract; - const savedObjectsClient = getInternalSavedObjectsClient(server); - const apmIndices = await apmPlugin.getApmIndices(savedObjectsClient); - const esClient: ESClient = getESClient( - apmIndices, - server.uiSettingsServiceFactory({ savedObjectsClient }), - callCluster + return await indexLatestConnections( + setup.indices, + setup.client, + setup.internalClient, + startTimeInterval ); - - return await indexLatestConnections(apmIndices, esClient, startTimeInterval); } diff --git a/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts b/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts index a9da61c5c1f43..353b723289bf0 100644 --- a/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts @@ -58,7 +58,7 @@ import { uiFiltersEnvironmentsRoute } from './ui_filters'; import { createApi } from './create_api'; -import { serviceMapRoute } from './services'; +import { serviceMapRoute } from './service_map'; const createApmApi = () => { const api = createApi() @@ -76,9 +76,11 @@ const createApmApi = () => { .add(serviceTransactionTypesRoute) .add(servicesRoute) .add(serviceNodeMetadataRoute) - .add(serviceMapRoute) .add(serviceAnnotationsRoute) + // Service Map + .add(serviceMapRoute) + // Agent configuration .add(agentConfigurationAgentNameRoute) .add(agentConfigurationRoute) diff --git a/x-pack/legacy/plugins/apm/server/routes/service_map.ts b/x-pack/legacy/plugins/apm/server/routes/service_map.ts new file mode 100644 index 0000000000000..cdb239362551c --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/routes/service_map.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; +import Boom from 'boom'; +import { setupRequest } from '../lib/helpers/setup_request'; +import { createRoute } from './create_route'; +import { uiFiltersRt, rangeRt } from './default_api_types'; +import { getServiceMap } from '../lib/service_map/get_service_map'; + +export const serviceMapRoute = createRoute(() => ({ + path: '/api/apm/service-map', + params: { + query: t.intersection([ + t.partial({ environment: t.string, serviceName: t.string }), + uiFiltersRt, + rangeRt + ]) + }, + handler: async ({ context, request }) => { + if (!context.config['xpack.apm.serviceMapEnabled']) { + return new Boom('Not found', { statusCode: 404 }); + } + const setup = await setupRequest(context, request); + const { + query: { serviceName, environment } + } = context.params; + return getServiceMap({ setup, serviceName, environment }); + } +})); diff --git a/x-pack/legacy/plugins/apm/server/routes/services.ts b/x-pack/legacy/plugins/apm/server/routes/services.ts index af3dd025b70b9..18777183ea1de 100644 --- a/x-pack/legacy/plugins/apm/server/routes/services.ts +++ b/x-pack/legacy/plugins/apm/server/routes/services.ts @@ -5,7 +5,6 @@ */ import * as t from 'io-ts'; -import Boom from 'boom'; import { AgentName } from '../../typings/es_schemas/ui/fields/Agent'; import { createApmTelementry, @@ -18,7 +17,6 @@ import { getServiceTransactionTypes } from '../lib/services/get_service_transact import { getServiceNodeMetadata } from '../lib/services/get_service_node_metadata'; import { createRoute } from './create_route'; import { uiFiltersRt, rangeRt } from './default_api_types'; -import { getServiceMap } from '../lib/service_map/get_service_map'; import { getServiceAnnotations } from '../lib/services/annotations'; export const servicesRoute = createRoute(() => ({ @@ -87,27 +85,6 @@ export const serviceNodeMetadataRoute = createRoute(() => ({ } })); -export const serviceMapRoute = createRoute(() => ({ - path: '/api/apm/service-map', - params: { - query: t.intersection([ - t.partial({ environment: t.string, serviceName: t.string }), - uiFiltersRt, - rangeRt - ]) - }, - handler: async ({ context, request }) => { - if (!context.config['xpack.apm.serviceMapEnabled']) { - return new Boom('Not found', { statusCode: 404 }); - } - const setup = await setupRequest(context, request); - const { - query: { serviceName, environment } - } = context.params; - return getServiceMap({ setup, serviceName, environment }); - } -})); - export const serviceAnnotationsRoute = createRoute(() => ({ path: '/api/apm/services/{serviceName}/annotations', params: { diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index a1cf2ae4e8ead..c1e8e37a9f81c 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -18,6 +18,7 @@ import { createApmAgentConfigurationIndex } from '../../../legacy/plugins/apm/se import { createApmApi } from '../../../legacy/plugins/apm/server/routes/create_apm_api'; import { getApmIndices } from '../../../legacy/plugins/apm/server/lib/settings/apm_indices/get_apm_indices'; import { APMConfig, mergeConfigs, APMXPackConfig } from '.'; +import { initializeServiceMaps } from '../../../legacy/plugins/apm/server/lib/service_map/initialize_service_maps'; export interface LegacySetup { server: Server; @@ -55,6 +56,7 @@ export class APMPlugin implements Plugin { this.legacySetup$.subscribe(__LEGACY => { createApmApi().init(core, { config$: mergedConfig$, logger, __LEGACY }); + initializeServiceMaps(core, { config$: mergedConfig$, logger, __LEGACY }); }); await new Promise(resolve => {