[Fleet] Replace call to registry when deleting kibana assets for custom packages (#224886)

Fixes https://github.com/elastic/kibana/issues/224191

## Summary

Bugfix - Replace call to registry when deleting kibana assets for
packages of type "custom" and "bundled". Also replaced the call to
`fetchInfo.registry` on another code path to avoid errors in the same
situation -
- These calls are replaced with `getPackageInfo`, that has some internal
functionalities to decide when the packageInfo should be fetched from
the cache, ES or the registry.
- Added additional logging to the delete assets functions

### Testing
- Install a custom integration that has some assets (a dashboard for
instance)
- Uninstall it and check that the asset is correctly removed and there
are no errors:

<img width="1453" alt="Screenshot 2025-06-25 at 11 02 39"
src="https://github.com/user-attachments/assets/32fb07f3-2628-4e30-be92-16610043b3ae"
/>


### Checklist

- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Cristina Amico 2025-06-25 14:21:41 +02:00 committed by GitHub
parent e31f1a584f
commit 550b9d58ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 70 additions and 33 deletions

View file

@ -279,7 +279,7 @@ export async function installKibanaAssetsAndReferences({
const kibanaAssetsArchiveIterator = getKibanaAssetsArchiveIterator(packageInstallContext);
if (installedPkg) {
await deleteKibanaSavedObjectsAssets({ installedPkg, spaceId });
await deleteKibanaSavedObjectsAssets({ savedObjectsClient, installedPkg, spaceId });
}
let installedKibanaAssetsRefs: KibanaAssetReference[] = [];
@ -344,7 +344,7 @@ export async function deleteKibanaAssetsAndReferencesForSpace({
'Impossible to delete kibana assets from the space where the package was installed, you must uninstall the package.'
);
}
await deleteKibanaSavedObjectsAssets({ installedPkg, spaceId });
await deleteKibanaSavedObjectsAssets({ savedObjectsClient, installedPkg, spaceId });
await saveKibanaAssetsRefs(savedObjectsClient, pkgName, null, true);
}

View file

@ -269,7 +269,8 @@ describe('cleanUpKibanaAssetsStep', () => {
expect(mockedDeleteKibanaAssets).toBeCalledWith({
installedObjects: installedKibana,
spaceId: 'default',
packageInfo: packageInstallContext.packageInfo,
packageSpecConditions: { kibana: { version: 'x.y.z' } },
logger: expect.anything(),
});
});
@ -449,7 +450,8 @@ describe('cleanUpUnusedKibanaAssetsStep', () => {
expect(mockedDeleteKibanaAssets).toBeCalledWith({
installedObjects: [installedAssets[1]],
spaceId: 'default',
packageInfo: packageInstallContext.packageInfo,
packageSpecConditions: { kibana: { version: 'x.y.z' } },
logger: expect.anything(),
});
});
});

View file

@ -82,7 +82,12 @@ export async function cleanUpKibanaAssetsStep(context: InstallContext) {
logger.debug('Retry transition - clean up Kibana assets first');
await withPackageSpan('Retry transition - clean up Kibana assets first', async () => {
await deleteKibanaAssets({ installedObjects, spaceId, packageInfo });
await deleteKibanaAssets({
installedObjects,
spaceId,
packageSpecConditions: packageInfo?.conditions,
logger,
});
});
}
}
@ -124,6 +129,11 @@ export async function cleanUpUnusedKibanaAssetsStep(context: InstallContext) {
}
await withPackageSpan('Clean up Kibana assets that are no longer in the package', async () => {
await deleteKibanaAssets({ installedObjects: assetsToRemove, spaceId, packageInfo });
await deleteKibanaAssets({
installedObjects: assetsToRemove,
spaceId,
packageSpecConditions: packageInfo?.conditions,
logger,
});
});
}

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server';
import { differenceBy } from 'lodash';
import type { ElasticsearchClient, SavedObjectsClientContract, Logger } from '@kbn/core/server';
import { differenceBy, chunk } from 'lodash';
import type { SavedObject } from '@kbn/core/server';
@ -17,7 +17,6 @@ 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 { chunk } from 'lodash';
import pMap from 'p-map';
import { updateIndexSettings } from '../elasticsearch/index/update_settings';
@ -36,8 +35,7 @@ import type {
EsAssetReference,
KibanaAssetReference,
Installation,
ArchivePackage,
RegistryPackage,
InstallSource,
} from '../../../types';
import { deletePipeline } from '../elasticsearch/ingest_pipeline';
import { removeUnusedIndexPatterns } from '../kibana/index_pattern/install';
@ -52,9 +50,10 @@ 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 '.';
import type { PackageSpecConditions } from '../../../../common';
import { getInstallation, getPackageInfo, kibanaSavedObjectTypes } from '.';
import { updateUninstallFailedAttempts } from './uninstall_errors_helpers';
const MAX_ASSETS_TO_DELETE = 1000;
@ -65,6 +64,7 @@ export async function removeInstallation(options: {
pkgVersion?: string;
esClient: ElasticsearchClient;
force?: boolean;
installSource?: InstallSource;
}): Promise<AssetReference[]> {
const { savedObjectsClient, pkgName, pkgVersion, esClient } = options;
const installation = await getInstallation({ savedObjectsClient, pkgName });
@ -103,7 +103,7 @@ export async function removeInstallation(options: {
// Delete the installed assets. Don't include installation.package_assets. Those are irrelevant to users
const installedAssets = [...installation.installed_kibana, ...installation.installed_es];
await deleteAssets(installation, esClient);
await deleteAssets(savedObjectsClient, installation, esClient);
// Delete the manager saved object with references to the asset objects
// could also update with [] or some other state
@ -144,22 +144,26 @@ export async function removeInstallation(options: {
*/
export async function deleteKibanaAssets({
installedObjects,
packageInfo,
packageSpecConditions,
logger,
spaceId = DEFAULT_SPACE_ID,
}: {
installedObjects: KibanaAssetReference[];
logger: Logger;
packageSpecConditions?: PackageSpecConditions;
spaceId?: string;
packageInfo: RegistryPackage | ArchivePackage;
}) {
const savedObjectsClient = new SavedObjectsClient(
appContextService.getSavedObjects().createInternalRepository()
);
const namespace = SavedObjectsUtils.namespaceStringToId(spaceId);
if (namespace) {
logger.debug(`Deleting Kibana assets in namespace: ${namespace}`);
}
// TODO this should be the installed package info, not the package that is being installed
const minKibana = packageInfo.conditions?.kibana?.version
? minVersion(packageInfo.conditions.kibana.version)
const minKibana = packageSpecConditions?.kibana?.version
? minVersion(packageSpecConditions.kibana.version)
: null;
// Compare Kibana versions to determine if the package could been installed
@ -167,7 +171,7 @@ export async function deleteKibanaAssets({
// and delete the assets directly. Otherwise, we need to resolve the assets
// which might create high memory pressure if a package has a lot of assets.
if (minKibana && minKibana.major >= 8) {
await bulkDeleteSavedObjects(installedObjects, namespace, savedObjectsClient);
await bulkDeleteSavedObjects(installedObjects, namespace, savedObjectsClient, logger);
} else {
const { resolved_objects: resolvedObjects } = await savedObjectsClient.bulkResolve(
installedObjects,
@ -190,23 +194,25 @@ export async function deleteKibanaAssets({
// we filter these out before calling delete
const assetsToDelete = foundObjects.map(({ saved_object: { id, type } }) => ({ id, type }));
await bulkDeleteSavedObjects(assetsToDelete, namespace, savedObjectsClient);
await bulkDeleteSavedObjects(assetsToDelete, namespace, savedObjectsClient, logger);
}
}
async function bulkDeleteSavedObjects(
assetsToDelete: Array<{ id: string; type: string }>,
namespace: string | undefined,
savedObjectsClient: SavedObjectsClientContract
savedObjectsClient: SavedObjectsClientContract,
logger: Logger
) {
logger.debug(`Starting bulk deletion of assets and saved objects`);
for (const asset of assetsToDelete) {
logger.debug(`Delete asset - id: ${asset?.id}, type: ${asset?.type},`);
auditLoggingService.writeCustomSoAuditLog({
action: 'delete',
id: asset.id,
savedObjectType: asset.type,
});
}
// Delete assets in chunks to avoid high memory pressure. This is mostly
// relevant for packages containing many assets, as large payload and response
// objects are created in memory during the delete operation. While chunking
@ -333,6 +339,7 @@ export async function deletePrerequisiteAssets(
}
async function deleteAssets(
savedObjectsClient: SavedObjectsClientContract,
{
installed_es: installedEs,
installed_kibana: installedKibana,
@ -340,6 +347,7 @@ async function deleteAssets(
additional_spaces_installed_kibana: installedInAdditionalSpacesKibana = {},
name,
version,
install_source: installSource,
}: Installation,
esClient: ElasticsearchClient
) {
@ -357,17 +365,29 @@ async function deleteAssets(
esClient
);
// delete the other asset types
try {
const packageInfo = await Registry.fetchInfo(name, version);
const packageInfo = await getPackageInfo({
savedObjectsClient,
pkgName: name,
pkgVersion: version,
skipArchive: installSource !== 'registry',
});
// delete the other asset types
await Promise.all([
...deleteESAssets(otherAssets, esClient),
deleteKibanaAssets({ installedObjects: installedKibana, spaceId, packageInfo }),
deleteKibanaAssets({
installedObjects: installedKibana,
spaceId,
packageSpecConditions: packageInfo?.conditions,
logger,
}),
Object.entries(installedInAdditionalSpacesKibana).map(([additionalSpaceId, kibanaAssets]) =>
deleteKibanaAssets({
installedObjects: kibanaAssets,
spaceId: additionalSpaceId,
packageInfo,
logger,
packageSpecConditions: packageInfo?.conditions,
})
),
]);
@ -402,9 +422,11 @@ async function deleteComponentTemplate(esClient: ElasticsearchClient, name: stri
}
export async function deleteKibanaSavedObjectsAssets({
savedObjectsClient,
installedPkg,
spaceId,
}: {
savedObjectsClient: SavedObjectsClientContract;
installedPkg: SavedObject<Installation>;
spaceId?: string;
}) {
@ -427,15 +449,18 @@ export async function deleteKibanaSavedObjectsAssets({
.map(({ id, type }) => ({ id, type } as KibanaAssetReference));
try {
const packageInfo = await Registry.fetchInfo(
installedPkg.attributes.name,
installedPkg.attributes.version
);
const packageInfo = await getPackageInfo({
savedObjectsClient,
pkgName: installedPkg.attributes.name,
pkgVersion: installedPkg.attributes.version,
skipArchive: installedPkg.attributes.install_source !== 'registry',
});
await deleteKibanaAssets({
installedObjects: assetsToDelete,
spaceId: spaceIdToDelete,
packageInfo,
packageSpecConditions: packageInfo?.conditions,
logger,
});
} catch (err) {
// in the rollback case, partial installs are likely, so missing assets are not an error
@ -502,7 +527,7 @@ export async function cleanupAssets(
esClient: ElasticsearchClient,
soClient: SavedObjectsClientContract
) {
await deleteAssets(installationToDelete, esClient);
await deleteAssets(soClient, installationToDelete, esClient);
const {
installed_es: installedEs,