From e52975249205a780d732d6907cef920996c140ef Mon Sep 17 00:00:00 2001 From: Andrew Wilkins Date: Thu, 18 Mar 2021 12:28:42 +0800 Subject: [PATCH] [APM] sourcemap routes --- x-pack/plugins/apm/kibana.json | 3 +- x-pack/plugins/apm/server/plugin.ts | 3 + .../apm/server/routes/create_apm_api.ts | 12 +- .../plugins/apm/server/routes/sourcemaps.ts | 118 ++++++++++++++++++ x-pack/plugins/fleet/server/plugin.ts | 14 ++- 5 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/apm/server/routes/sourcemaps.ts diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json index a9a0149e72ce7..b0c178f535005 100644 --- a/x-pack/plugins/apm/kibana.json +++ b/x-pack/plugins/apm/kibana.json @@ -21,7 +21,8 @@ "security", "ml", "home", - "maps" + "maps", + "fleet" ], "server": true, "ui": true, diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index f556374179c51..1a0d5c0e4aeb6 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -28,6 +28,7 @@ import { CloudSetup } from '../../cloud/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { MlPluginSetup } from '../../ml/server'; +import { FleetSetupContract, FleetStartContract } from '../../fleet/server'; import { ObservabilityPluginSetup } from '../../observability/server'; import { SecurityPluginSetup } from '../../security/server'; import { TaskManagerSetupContract } from '../../task_manager/server'; @@ -77,6 +78,7 @@ export class APMPlugin implements Plugin { features: FeaturesPluginSetup; security?: SecurityPluginSetup; ml?: MlPluginSetup; + fleet?: FleetSetupContract; } ) { this.logger = this.initContext.logger.get(); @@ -152,6 +154,7 @@ export class APMPlugin implements Plugin { observability: plugins.observability, security: plugins.security, ml: plugins.ml, + fleet: plugins.fleet, }, }); diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index 2bd7e25e848c8..ca5748c7f2529 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -106,6 +106,11 @@ import { transactionErrorCountChartPreview, transactionDurationChartPreview, } from './alerts/chart_preview'; +import { + deleteSourceMapRoute, + listSourceMapsRoute, + uploadSourceMapRoute, +} from './sourcemaps'; const createApmApi = () => { const api = createApi() @@ -214,7 +219,12 @@ const createApmApi = () => { // Alerting .add(transactionErrorCountChartPreview) .add(transactionDurationChartPreview) - .add(transactionErrorRateChartPreview); + .add(transactionErrorRateChartPreview) + + // Source maps + .add(deleteSourceMapRoute) + .add(listSourceMapsRoute) + .add(uploadSourceMapRoute); return api; }; diff --git a/x-pack/plugins/apm/server/routes/sourcemaps.ts b/x-pack/plugins/apm/server/routes/sourcemaps.ts new file mode 100644 index 0000000000000..d12978abc6da7 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/sourcemaps.ts @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as t from 'io-ts'; +import { createStaticIndexPattern } from '../lib/index_pattern/create_static_index_pattern'; +import { createRoute } from './create_route'; +import { setupRequest } from '../lib/helpers/setup_request'; +import { getInternalSavedObjectsClient } from '../lib/helpers/get_internal_saved_objects_client'; +import { getApmIndexPatternTitle } from '../lib/index_pattern/get_apm_index_pattern_title'; +import { getDynamicIndexPattern } from '../lib/index_pattern/get_dynamic_index_pattern'; +import { FleetArtifactsClient } from '../../../fleet/server/services/artifacts'; + +async function updateFleetPolicies(packagePolicyService, savedObjectsClient, elasticsearchClient, logger) { + const client = new FleetArtifactsClient(elasticsearchClient.asInternalUser, "apm"); + // TODO(axw) iterate through all pages of artifacts. + // TODO(axw) only record the minimally required artifact fields in the policies. + const artifacts = await client.listArtifacts("type:sourcemap"); + const policies = await packagePolicyService.list(savedObjectsClient, { + // TODO(axw) iterate through all pages. Unlikely to be more than a handful, + // if even more than one, but better to be on the safe side. + page: 1, + perPage: 20, + kuery: 'ingest-package-policies.package.name:apm' + }); + for (let item of policies.items) { + const { id, revision, updated_at, updated_by, ...policy } = item; + policy.inputs[0].config = { + sourcemap_artifacts: { + value: artifacts, + }, + }; + await packagePolicyService.update(savedObjectsClient, elasticsearchClient, id, policy); + } +} + +export const listSourceMapsRoute = createRoute((core) => ({ + endpoint: 'GET /api/apm/sourcemaps', + options: {tags: ['access:apm']}, + handler: async ({ context, request }) => { + // TODO(axw) add query params to support pagination + const client = new FleetArtifactsClient(context.core.elasticsearch.client.asInternalUser, "apm"); + return client.listArtifacts("type: sourcemap"); + }, +})); + +export const deleteSourceMapRoute = createRoute((core) => ({ + endpoint: 'DELETE /api/apm/sourcemaps/{id}', + options: {tags: ['access:apm']}, + params: t.type({ + path: t.type({ + id: t.string, + }), + }), + handler: async ({ context, request }) => { + // TODO(axw) there's no guarantee the ID refers to a sourcemap artifact. + // We should investigate adding a kuery param to the deleteArtifact method, + // to require that. + const client = new FleetArtifactsClient(context.core.elasticsearch.client.asInternalUser, "apm"); + await client.deleteArtifact(context.params.path.id); + await updateFleetPolicies( + context.plugins.fleet.packagePolicyService, + context.core.savedObjects.client, + context.core.elasticsearch.client, + context.logger, + ); + return undefined; + }, +})); + +export const uploadSourceMapRoute = createRoute((core) => ({ + endpoint: 'POST /api/apm/sourcemaps/{serviceName}/{serviceVersion}/{bundleFilepath}', + options: {tags: ['access:apm']}, + params: t.type({ + path: t.type({ + serviceName: t.string, + serviceVersion: t.string, + bundleFilepath: t.string, + }), + body: t.intersection([ + t.type({ + version: t.number, + sources: t.array(t.string), + mappings: t.string, + names: t.array(t.string), + }), + t.partial({ + file: t.string, + sourceRoot: t.string, + sourcesContent: t.array(t.string), + }) + ]), + }), + handler: async ({ context, request }) => { + const pathParams = context.params.path; + const content = JSON.stringify({ + service_name: pathParams.serviceName, + service_version: pathParams.serviceVersion, + bundle_filepath: pathParams.bundleFilepath, + sourcemap: context.params.body, + }); + const identifier = `${pathParams.serviceName}-${pathParams.serviceVersion}-${pathParams.bundleFilepath}`; + const client = new FleetArtifactsClient(context.core.elasticsearch.client.asInternalUser, "apm"); + // TODO(axw) "identifier" is not used as the document _id, and multiple artifacts with the same + // identifier are possible. It might we worth investigating if this can be changed. + const artifact = await client.createArtifact({content: content, type: "sourcemap", identifier}); + await updateFleetPolicies( + context.plugins.fleet.packagePolicyService, + context.core.savedObjects.client, + context.core.elasticsearch.client, + context.logger, + ); + return artifact; + }, +})); diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 3289a762e57cb..a23b8accec3b6 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -119,7 +119,10 @@ export interface FleetAppContext { httpSetup?: HttpServiceSetup; } -export type FleetSetupContract = void; +export interface FleetSetupContract { + packagePolicyService: typeof packagePolicyService; + createArtifactsClient: (packageName: string) => FleetArtifactsClient; +}; const allSavedObjectTypes = [ OUTPUT_SAVED_OBJECT_TYPE, @@ -200,7 +203,7 @@ export class FleetPlugin this.logger = this.initializerContext.logger.get(); } - public async setup(core: CoreSetup, deps: FleetSetupDeps) { + public async setup(core: CoreSetup, deps: FleetSetupDeps): Promise { this.httpSetup = core.http; this.licensing$ = deps.licensing.license$; this.encryptedSavedObjectsSetup = deps.encryptedSavedObjects; @@ -288,6 +291,13 @@ export class FleetPlugin } } } + + return { + packagePolicyService, + createArtifactsClient(packageName: string) { + return new FleetArtifactsClient(core.elasticsearch.client.asInternalUser, packageName); + }, + }; } public async start(core: CoreStart, plugins: FleetStartDeps): Promise {