diff --git a/x-pack/plugins/apm/server/lib/fleet/register_fleet_policy_callbacks.ts b/x-pack/plugins/apm/server/lib/fleet/register_fleet_policy_callbacks.ts index 35c7f0dfdfd73..935e55330b051 100644 --- a/x-pack/plugins/apm/server/lib/fleet/register_fleet_policy_callbacks.ts +++ b/x-pack/plugins/apm/server/lib/fleet/register_fleet_policy_callbacks.ts @@ -12,6 +12,7 @@ import { APMPluginStartDependencies } from '../../types'; import { ExternalCallback } from '../../../../fleet/server'; import { AGENT_NAME } from '../../../common/elasticsearch_fieldnames'; import { AgentConfiguration } from '../../../common/agent_configuration/configuration_types'; +import { getPackagePolicyWithSourceMap, listArtifacts } from './source_maps'; export async function registerFleetPolicyCallbacks({ plugins, @@ -31,7 +32,7 @@ export async function registerFleetPolicyCallbacks({ // Registers a callback invoked when a policy is created to populate the APM // integration policy with pre-existing agent configurations - registerAgentConfigExternalCallback({ + registerPackagePolicyExternalCallback({ fleetPluginStart, callbackName: 'packagePolicyCreate', plugins, @@ -42,7 +43,7 @@ export async function registerFleetPolicyCallbacks({ // Registers a callback invoked when a policy is updated to populate the APM // integration policy with existing agent configurations - registerAgentConfigExternalCallback({ + registerPackagePolicyExternalCallback({ fleetPluginStart, callbackName: 'packagePolicyUpdate', plugins, @@ -53,11 +54,11 @@ export async function registerFleetPolicyCallbacks({ } type ExternalCallbackParams = Parameters; -type PackagePolicy = ExternalCallbackParams[0]; +export type PackagePolicy = ExternalCallbackParams[0]; type Context = ExternalCallbackParams[1]; type Request = ExternalCallbackParams[2]; -function registerAgentConfigExternalCallback({ +function registerPackagePolicyExternalCallback({ fleetPluginStart, callbackName, plugins, @@ -91,16 +92,23 @@ function registerAgentConfigExternalCallback({ ruleDataClient, }); const agentConfigurations = await listConfigurations({ setup }); - return getPackagePolicyWithAgentConfigurations( - packagePolicy, - agentConfigurations - ); + const artifacts = await listArtifacts({ fleetPluginStart }); + return { + ...getPackagePolicyWithAgentConfigurations( + packagePolicy, + agentConfigurations + ), + ...getPackagePolicyWithSourceMap({ + packagePolicy, + artifacts, + }), + }; }; fleetPluginStart.registerExternalCallback(callbackName, callbackFn); } -const APM_SERVER = 'apm-server'; +export const APM_SERVER = 'apm-server'; // Immutable function applies the given package policy with a set of agent configurations export function getPackagePolicyWithAgentConfigurations( diff --git a/x-pack/plugins/apm/server/lib/fleet/source_maps.test.ts b/x-pack/plugins/apm/server/lib/fleet/source_maps.test.ts new file mode 100644 index 0000000000000..8d171322e8292 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/fleet/source_maps.test.ts @@ -0,0 +1,173 @@ +/* + * 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 { SavedObjectsClientContract, ElasticsearchClient } from 'kibana/server'; +import { + ArtifactSourceMap, + getPackagePolicyWithSourceMap, + updateSourceMapsOnFleetPolicies, + FleetPluginStart, +} from './source_maps'; + +const packagePolicy = { + id: '123', + version: 'WzMxNDI2LDFd', + name: 'apm-1', + description: '', + namespace: 'default', + policy_id: '7a87c160-c961-11eb-81e2-f7327d61c92a', + enabled: true, + output_id: '', + inputs: [ + { + policy_template: 'apmserver', + streams: [], + vars: {}, + type: 'apm', + enabled: true, + compiled_input: { + 'apm-server': { + capture_personal_data: true, + max_event_size: 307200, + api_key: { limit: 100, enabled: false }, + default_service_environment: null, + host: 'localhost:8200', + kibana: { api_key: null }, + secret_token: null, + }, + }, + }, + ], + package: { name: 'apm', title: 'Elastic APM', version: '0.2.0' }, + created_at: '2021-06-16T14:54:32.195Z', + created_by: 'elastic', +}; + +const artifacts = [ + { + type: 'sourcemap', + identifier: 'service_name-1.0.0', + relative_url: '/api/fleet/artifacts/service_name-1.0.0/my-id-1', + body: { + serviceName: 'service_name', + serviceVersion: '1.0.0', + bundleFilepath: 'http://localhost:3000/static/js/main.chunk.js', + sourceMap: { + version: 3, + file: 'static/js/main.chunk.js', + sources: ['foo'], + sourcesContent: ['foo'], + mappings: 'foo', + sourceRoot: '', + }, + }, + created: '2021-06-16T15:03:55.049Z', + id: 'apm:service_name-1.0.0-my-id-1', + compressionAlgorithm: 'zlib', + decodedSha256: 'my-id-1', + decodedSize: 9440, + encodedSha256: 'sha123', + encodedSize: 2622, + encryptionAlgorithm: 'none', + packageName: 'apm', + }, + { + type: 'sourcemap', + identifier: 'service_name-2.0.0', + relative_url: '/api/fleet/artifacts/service_name-2.0.0/my-id-2', + body: { + serviceName: 'service_name', + serviceVersion: '2.0.0', + bundleFilepath: 'http://localhost:3000/static/js/main.chunk.js', + sourceMap: { + version: 3, + file: 'static/js/main.chunk.js', + sources: ['foo'], + sourcesContent: ['foo'], + mappings: 'foo', + sourceRoot: '', + }, + }, + created: '2021-06-16T15:03:55.049Z', + id: 'apm:service_name-2.0.0-my-id-2', + compressionAlgorithm: 'zlib', + decodedSha256: 'my-id-2', + decodedSize: 9440, + encodedSha256: 'sha456', + encodedSize: 2622, + encryptionAlgorithm: 'none', + packageName: 'apm', + }, +] as ArtifactSourceMap[]; + +describe('Source maps', () => { + describe('getPackagePolicyWithSourceMap', () => { + it('returns unchanged package policy when artifacts is empty', () => { + const updatedPackagePolicy = getPackagePolicyWithSourceMap({ + packagePolicy, + artifacts: [], + }); + expect(updatedPackagePolicy).toEqual(packagePolicy); + }); + it('adds source maps into the package policy', () => { + const updatedPackagePolicy = getPackagePolicyWithSourceMap({ + packagePolicy, + artifacts, + }); + expect(updatedPackagePolicy.inputs[0].config).toEqual({ + 'apm-server': { + value: { + rum: { + source_mapping: { + metadata: [ + { + 'service.name': 'service_name', + 'service.version': '1.0.0', + 'bundle.filepath': + 'http://localhost:3000/static/js/main.chunk.js', + 'sourcemap.url': + '/api/fleet/artifacts/service_name-1.0.0/my-id-1', + }, + { + 'service.name': 'service_name', + 'service.version': '2.0.0', + 'bundle.filepath': + 'http://localhost:3000/static/js/main.chunk.js', + 'sourcemap.url': + '/api/fleet/artifacts/service_name-2.0.0/my-id-2', + }, + ], + }, + }, + }, + }, + }); + }); + }); + describe('updateSourceMapsOnFleetPolicies', () => { + it('saves source maps into the package policy', () => {}); + it.only('removes source maps from the package policy', () => { + const update = jest.fn(); + + updateSourceMapsOnFleetPolicies({ + fleetPluginStart: ({ + createArtifactsClient: jest.fn().mockReturnValue({ + listArtifacts: jest.fn().mockReturnValue({ items: [] }), + }), + packagePolicyService: { + update, + list: jest.fn().mockReturnValue({ items: [packagePolicy] }), + }, + } as unknown) as FleetPluginStart, + savedObjectsClient: ({} as unknown) as SavedObjectsClientContract, + elasticsearchClient: ({} as unknown) as ElasticsearchClient, + }); + + expect(update).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/fleet/source_maps.ts b/x-pack/plugins/apm/server/lib/fleet/source_maps.ts index 06fb810714868..0445221970e81 100644 --- a/x-pack/plugins/apm/server/lib/fleet/source_maps.ts +++ b/x-pack/plugins/apm/server/lib/fleet/source_maps.ts @@ -8,6 +8,7 @@ import * as t from 'io-ts'; import { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; import { promisify } from 'util'; import { unzip } from 'zlib'; +import { PackagePolicy, APM_SERVER } from './register_fleet_policy_callbacks'; import { Artifact } from '../../../../fleet/server'; import { sourceMapRt } from '../../routes/source_maps'; import { APMPluginStartDependencies } from '../../types'; @@ -18,14 +19,14 @@ export interface ApmArtifactBody { bundleFilepath: string; sourceMap: t.TypeOf; } -type ArtifactSourceMap = Omit & { body: ApmArtifactBody }; +export type ArtifactSourceMap = Omit & { + body: ApmArtifactBody; +}; -type FleetPluginStart = NonNullable; +export type FleetPluginStart = NonNullable; const doUnzip = promisify(unzip); -const APM_SERVER = 'apm-server'; - function decodeArtifacts(artifacts: Artifact[]): Promise { return Promise.all( artifacts.map(async (artifact) => { @@ -42,7 +43,7 @@ function getApmArtifactClient(fleetPluginStart: FleetPluginStart) { return fleetPluginStart.createArtifactsClient('apm'); } -export async function getArtifacts({ +export async function listArtifacts({ fleetPluginStart, }: { fleetPluginStart: FleetPluginStart; @@ -83,7 +84,47 @@ export async function deleteApmArtifact({ return apmArtifactClient.deleteArtifact(id); } -export async function updateSourceMapsToFleetPolicies({ +export function getPackagePolicyWithSourceMap({ + packagePolicy, + artifacts, +}: { + packagePolicy: PackagePolicy; + artifacts: ArtifactSourceMap[]; +}) { + if (!artifacts.length) { + return packagePolicy; + } + const [firstInput, ...restInputs] = packagePolicy.inputs; + return { + ...packagePolicy, + inputs: [ + { + ...firstInput, + config: { + ...firstInput.config, + [APM_SERVER]: { + value: { + ...firstInput?.config?.[APM_SERVER].value, + rum: { + source_mapping: { + metadata: artifacts.map((artifact) => ({ + 'service.name': artifact.body.serviceName, + 'service.version': artifact.body.serviceVersion, + 'bundle.filepath': artifact.body.bundleFilepath, + 'sourcemap.url': artifact.relative_url, + })), + }, + }, + }, + }, + }, + }, + ...restInputs, + ], + }; +} + +export async function updateSourceMapsOnFleetPolicies({ fleetPluginStart, savedObjectsClient, elasticsearchClient, @@ -92,57 +133,32 @@ export async function updateSourceMapsToFleetPolicies({ savedObjectsClient: SavedObjectsClientContract; elasticsearchClient: ElasticsearchClient; }) { - const artifacts = await getArtifacts({ fleetPluginStart }); + const artifacts = await listArtifacts({ fleetPluginStart }); - const apmPolicies = await fleetPluginStart.packagePolicyService.list( + const apmFleetPolicies = await fleetPluginStart.packagePolicyService.list( savedObjectsClient, { kuery: 'ingest-package-policies.package.name:apm' } ); return Promise.all( - apmPolicies.items.map(async (packagePolicy) => { + apmFleetPolicies.items.map(async (item) => { const { id, revision, updated_at: updatedAt, updated_by: updatedBy, - ...policyRest - } = packagePolicy; - - const [firstInput, ...restInputs] = packagePolicy.inputs; - const updatedPackagePolicy = { - ...policyRest, - inputs: [ - { - ...firstInput, - config: { - ...firstInput.config, - [APM_SERVER]: { - value: { - ...firstInput?.config?.[APM_SERVER].value, - rum: { - source_mapping: { - metadata: artifacts.map((artifact) => ({ - 'service.name': artifact.body.serviceName, - 'service.version': artifact.body.serviceVersion, - 'bundle.filepath': artifact.body.bundleFilepath, - 'sourcemap.url': artifact.relative_url, - })), - }, - }, - }, - }, - }, - }, - ...restInputs, - ], - }; + ...packagePolicy + } = item; + + const updatedPackagePolicy = getPackagePolicyWithSourceMap({ + packagePolicy, + artifacts, + }); await fleetPluginStart.packagePolicyService.update( savedObjectsClient, elasticsearchClient, id, - // @ts-ignore updatedPackagePolicy ); }) diff --git a/x-pack/plugins/apm/server/routes/source_maps.ts b/x-pack/plugins/apm/server/routes/source_maps.ts index 44f43263be853..8efa840874ed3 100644 --- a/x-pack/plugins/apm/server/routes/source_maps.ts +++ b/x-pack/plugins/apm/server/routes/source_maps.ts @@ -9,8 +9,8 @@ import * as t from 'io-ts'; import { createApmArtifact, deleteApmArtifact, - getArtifacts, - updateSourceMapsToFleetPolicies, + listArtifacts, + updateSourceMapsOnFleetPolicies, } from '../lib/fleet/source_maps'; import { createApmServerRoute } from './create_apm_server_route'; import { createApmServerRouteRepository } from './create_apm_server_route_repository'; @@ -36,7 +36,8 @@ const listSourceMapRoute = createApmServerRoute({ try { const fleetPluginStart = await plugins.fleet?.start(); if (fleetPluginStart) { - return { artifacts: await getArtifacts({ fleetPluginStart }) }; + const artifacts = await listArtifacts({ fleetPluginStart }); + return { artifacts }; } } catch (e) { throw Boom.internal( @@ -75,7 +76,7 @@ const uploadSourceMapRoute = createApmServerRoute({ sourceMap, }, }); - await updateSourceMapsToFleetPolicies({ + await updateSourceMapsOnFleetPolicies({ fleetPluginStart, savedObjectsClient: context.core.savedObjects.client, elasticsearchClient: context.core.elasticsearch.client.asInternalUser, @@ -106,7 +107,7 @@ const deleteSourceMapRoute = createApmServerRoute({ try { if (fleetPluginStart) { await deleteApmArtifact({ id, fleetPluginStart }); - await updateSourceMapsToFleetPolicies({ + await updateSourceMapsOnFleetPolicies({ fleetPluginStart, savedObjectsClient: context.core.savedObjects.client, elasticsearchClient: context.core.elasticsearch.client.asInternalUser,