From 70f24874ad510d23fefc80e54636e217c03ddb56 Mon Sep 17 00:00:00 2001 From: Dmitrii Date: Wed, 10 Jul 2024 17:31:06 +0200 Subject: [PATCH] Avoid loading package saved objects into memory before deleting them --- .../apis/internals/internal_bulk_resolve.ts | 3 +- .../src/apis/resolve.ts | 1 + .../services/epm/kibana/assets/install.ts | 49 +++++++------------ .../server/services/epm/packages/remove.ts | 27 ++++++++-- 4 files changed, 43 insertions(+), 37 deletions(-) diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve.ts index debbe734992ca59..6525abeb393d032 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve.ts @@ -47,6 +47,7 @@ import { } from '../utils'; import type { ApiExecutionContext } from '../types'; import type { RepositoryEsClient } from '../../repository_es_client'; +import { includedFields } from '../../utils'; const MAX_CONCURRENT_RESOLVE = 10; @@ -140,7 +141,7 @@ export async function internalBulkResolve( const bulkGetResponse = docsToBulkGet.length ? await client.mget( - { body: { docs: docsToBulkGet } }, + { body: { docs: docsToBulkGet }, _source: includedFields(allowedTypes, options.fields) }, { ignore: [404], meta: true } ) : undefined; diff --git a/packages/core/saved-objects/core-saved-objects-api-server/src/apis/resolve.ts b/packages/core/saved-objects/core-saved-objects-api-server/src/apis/resolve.ts index d32f36bdcce4fcc..e5ed61fa38da093 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server/src/apis/resolve.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server/src/apis/resolve.ts @@ -17,6 +17,7 @@ import type { SavedObject } from '../..'; export interface SavedObjectsResolveOptions extends SavedObjectsBaseOptions { /** {@link SavedObjectsRawDocParseOptions.migrationVersionCompatibility} */ migrationVersionCompatibility?: 'compatible' | 'raw'; + fields?: string[]; } /** diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts index 2096820e82dadeb..58385fb544a574a 100644 --- a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts @@ -21,7 +21,7 @@ import { partition } from 'lodash'; import { getAssetFromAssetsMap, getPathParts } from '../../archive'; import { KibanaAssetType, KibanaSavedObjectType } from '../../../../types'; -import type { AssetReference, AssetParts, Installation, PackageSpecTags } from '../../../../types'; +import type { AssetReference, Installation, PackageSpecTags } from '../../../../types'; import type { KibanaAssetReference, PackageInstallContext } from '../../../../../common/types'; import { indexPatternTypes, @@ -242,7 +242,8 @@ export async function installKibanaAssetsAndReferences({ }) { const { savedObjectsImporter, savedObjectTagAssignmentService, savedObjectTagClient } = getSpaceAwareSaveobjectsClients(spaceId); - const kibanaAssets = await getKibanaAssets(packageInstallContext); + // This is where the memory consumption is rising up in the first place + const kibanaAssets = getKibanaAssets(packageInstallContext); if (installedPkg) { await deleteKibanaSavedObjectsAssets({ savedObjectsClient, installedPkg, spaceId }); } @@ -323,9 +324,9 @@ export async function deleteKibanaAssetsAndReferencesForSpace({ await saveKibanaAssetsRefs(savedObjectsClient, pkgName, [], true); } -export async function getKibanaAssets( +export function getKibanaAssets( packageInstallContext: PackageInstallContext -): Promise> { +): Record { const kibanaAssetTypes = Object.values(KibanaAssetType); const isKibanaAssetType = (path: string) => { const parts = getPathParts(path); @@ -333,36 +334,20 @@ export async function getKibanaAssets( return parts.service === 'kibana' && (kibanaAssetTypes as string[]).includes(parts.type); }; - const filteredPaths = packageInstallContext.paths - .filter(isKibanaAssetType) - .map<[string, AssetParts]>((path) => [path, getPathParts(path)]); + const result = Object.fromEntries( + kibanaAssetTypes.map((type) => [type, []]) + ) as Record; - const assetArrays: Array> = []; - for (const assetType of kibanaAssetTypes) { - const matching = filteredPaths.filter(([path, parts]) => parts.type === assetType); + packageInstallContext.paths.filter(isKibanaAssetType).forEach((path) => { + const buffer = getAssetFromAssetsMap(packageInstallContext.assetsMap, path); + const asset = JSON.parse(buffer.toString('utf8')); - assetArrays.push( - Promise.all( - matching.map(([path]) => { - const buffer = getAssetFromAssetsMap(packageInstallContext.assetsMap, path); - - // cache values are buffers. convert to string / JSON - return JSON.parse(buffer.toString('utf8')); - }) - ) - ); - } - - const resolvedAssets = await Promise.all(assetArrays); - - const result = {} as Record; - - for (const [index, assetType] of kibanaAssetTypes.entries()) { - const expectedType = KibanaSavedObjectTypeMapping[assetType]; - const properlyTypedAssets = resolvedAssets[index].filter(({ type }) => type === expectedType); - - result[assetType] = properlyTypedAssets; - } + const assetType = getPathParts(path).type as KibanaAssetType; + const soType = KibanaSavedObjectTypeMapping[assetType]; + if (asset.type === soType) { + result[assetType].push(asset); + } + }); return result; } diff --git a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts index 5369da9da82e20e..6ffb675bab5d106 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts @@ -14,6 +14,7 @@ import { SavedObjectsClient } from '@kbn/core/server'; import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants'; import { SavedObjectsUtils, SavedObjectsErrorHelpers } from '@kbn/core/server'; +import minVersion from 'semver/ranges/min-version'; import { updateIndexSettings } from '../elasticsearch/index/update_settings'; @@ -43,6 +44,7 @@ import { auditLoggingService } from '../../audit_logging'; import { FleetError, PackageRemovalError } from '../../../errors'; import { populatePackagePolicyAssignedAgentsCount } from '../../package_policies/populate_package_policy_assigned_agents_count'; +import * as Registry from '../registry'; import { getInstallation, kibanaSavedObjectTypes } from '.'; @@ -112,7 +114,7 @@ export async function removeInstallation(options: { return installedAssets; } -async function deleteKibanaAssets( +async function resolveAndDeleteKibanaAssets( installedObjects: KibanaAssetReference[], spaceId: string = DEFAULT_SPACE_ID ) { @@ -237,9 +239,9 @@ async function deleteAssets( // then the other asset types await Promise.all([ ...deleteESAssets(otherAssets, esClient), - deleteKibanaAssets(installedKibana, spaceId), + resolveAndDeleteKibanaAssets(installedKibana, spaceId), Object.entries(installedInAdditionalSpacesKibana).map(([additionalSpaceId, kibanaAssets]) => - deleteKibanaAssets(kibanaAssets, additionalSpaceId) + resolveAndDeleteKibanaAssets(kibanaAssets, additionalSpaceId) ), ]); } catch (err) { @@ -299,8 +301,25 @@ export async function deleteKibanaSavedObjectsAssets({ .filter(({ type }) => kibanaSavedObjectTypes.includes(type)) .map(({ id, type }) => ({ id, type } as KibanaAssetReference)); + const registryInfo = await Registry.fetchInfo( + installedPkg.attributes.name, + installedPkg.attributes.version + ); + + const minKibana = registryInfo.conditions?.kibana?.version + ? minVersion(registryInfo.conditions.kibana.version) + : null; + try { - await deleteKibanaAssets(assetsToDelete, spaceIdToDelete); + // Compare Kibana versions to determine if the package could have been + // installed in 7.x. If so, we need to resolve legacy SO references before + // deleting them. + if (minKibana && minKibana.major >= 8) { + const namespace = SavedObjectsUtils.namespaceStringToId(spaceIdToDelete ?? DEFAULT_SPACE_ID); + await savedObjectsClient.bulkDelete(assetsToDelete, { namespace }); + } else { + await resolveAndDeleteKibanaAssets(assetsToDelete, spaceIdToDelete); + } } catch (err) { // in the rollback case, partial installs are likely, so missing assets are not an error if (!SavedObjectsErrorHelpers.isNotFoundError(err)) {