diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts deleted file mode 100644 index dec78b3be7d5c..0000000000000 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts +++ /dev/null @@ -1,439 +0,0 @@ -/* - * 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 type { - SavedObjectsClientContract, - ElasticsearchClient, - SavedObject, -} from '@kbn/core/server'; -import { savedObjectsClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks'; -import { loggerMock } from '@kbn/logging-mocks'; -import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants'; - -import { ConcurrentInstallOperationError, PackageSavedObjectConflictError } from '../../../errors'; - -import type { Installation } from '../../../../common'; - -import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../common'; - -import { appContextService } from '../../app_context'; -import { createAppContextStartContractMock } from '../../../mocks'; -import { saveArchiveEntriesFromAssetsMap } from '../archive/storage'; -import { installILMPolicy } from '../elasticsearch/ilm/install'; -import { installIlmForDataStream } from '../elasticsearch/datastream_ilm/install'; - -jest.mock('../elasticsearch/template/template'); -jest.mock('../kibana/assets/install'); -jest.mock('../kibana/index_pattern/install'); -jest.mock('./install'); -jest.mock('./get'); -jest.mock('./install_index_template_pipeline'); - -jest.mock('../archive/storage'); -jest.mock('../elasticsearch/ilm/install'); -jest.mock('../elasticsearch/datastream_ilm/install'); - -import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; -import { installKibanaAssetsAndReferences } from '../kibana/assets/install'; - -import { MAX_TIME_COMPLETE_INSTALL } from '../../../../common/constants'; - -import { restartInstallation } from './install'; -import { installIndexTemplatesAndPipelines } from './install_index_template_pipeline'; - -import { _installPackage } from './_install_package'; - -const mockedInstallIndexTemplatesAndPipelines = - installIndexTemplatesAndPipelines as jest.MockedFunction< - typeof installIndexTemplatesAndPipelines - >; -const mockedUpdateCurrentWriteIndices = updateCurrentWriteIndices as jest.MockedFunction< - typeof updateCurrentWriteIndices ->; -const mockedInstallKibanaAssetsAndReferences = - installKibanaAssetsAndReferences as jest.MockedFunction; - -function sleep(millis: number) { - return new Promise((resolve) => setTimeout(resolve, millis)); -} - -describe('_installPackage', () => { - let soClient: jest.Mocked; - let esClient: jest.Mocked; - - beforeEach(async () => { - soClient = savedObjectsClientMock.create(); - - soClient.update.mockImplementation(async (type, id, attributes) => { - return { id, attributes } as any; - }); - soClient.get.mockImplementation(async (type, id) => { - return { id, attributes: {} } as any; - }); - - esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - appContextService.start(createAppContextStartContractMock()); - jest.mocked(installILMPolicy).mockReset(); - jest.mocked(installIlmForDataStream).mockReset(); - jest.mocked(installIlmForDataStream).mockResolvedValue({ - esReferences: [], - installedIlms: [], - }); - jest.mocked(saveArchiveEntriesFromAssetsMap).mockResolvedValue({ - saved_objects: [], - }); - jest.mocked(restartInstallation).mockReset(); - }); - it('handles errors from installKibanaAssets', async () => { - // force errors from this function - mockedInstallKibanaAssetsAndReferences.mockImplementation(async () => { - throw new Error('mocked async error A: should be caught'); - }); - - // pick any function between when those are called and when await Promise.all is defined later - // and force it to take long enough for the errors to occur - // @ts-expect-error about call signature - mockedUpdateCurrentWriteIndices.mockImplementation(async () => await sleep(1000)); - mockedInstallIndexTemplatesAndPipelines.mockResolvedValue({ - installedTemplates: [], - esReferences: [], - }); - const installationPromise = _installPackage({ - savedObjectsClient: soClient, - // @ts-ignore - savedObjectsImporter: jest.fn(), - esClient, - logger: loggerMock.create(), - packageInstallContext: { - assetsMap: new Map(), - paths: [], - packageInfo: { - title: 'title', - name: 'xyz', - version: '4.5.6', - description: 'test', - type: 'integration', - categories: ['cloud', 'custom'], - format_version: 'string', - release: 'experimental', - conditions: { kibana: { version: 'x.y.z' } }, - owner: { github: 'elastic/fleet' }, - }, - }, - installType: 'install', - installSource: 'registry', - spaceId: DEFAULT_SPACE_ID, - }); - - // if we have a .catch this will fail nicely (test pass) - // otherwise the test will fail with either of the mocked errors - await expect(installationPromise).rejects.toThrow('mocked'); - await expect(installationPromise).rejects.toThrow('should be caught'); - }); - - it('do not install ILM policies if disabled in config', async () => { - appContextService.start( - createAppContextStartContractMock({ - internal: { - disableILMPolicies: true, - fleetServerStandalone: false, - onlyAllowAgentUpgradeToKnownVersions: false, - retrySetupOnBoot: false, - registry: { - kibanaVersionCheckEnabled: true, - capabilities: [], - excludePackages: [], - }, - }, - }) - ); - // force errors from this function - mockedInstallKibanaAssetsAndReferences.mockResolvedValue([]); - // pick any function between when those are called and when await Promise.all is defined later - // and force it to take long enough for the errors to occur - // @ts-expect-error about call signature - mockedUpdateCurrentWriteIndices.mockImplementation(async () => await sleep(1000)); - mockedInstallIndexTemplatesAndPipelines.mockResolvedValue({ - installedTemplates: [], - esReferences: [], - }); - await _installPackage({ - savedObjectsClient: soClient, - // @ts-ignore - savedObjectsImporter: jest.fn(), - esClient, - logger: loggerMock.create(), - packageInstallContext: { - assetsMap: new Map(), - paths: [], - packageInfo: { - title: 'title', - name: 'xyz', - version: '4.5.6', - description: 'test', - type: 'integration', - categories: ['cloud', 'custom'], - format_version: 'string', - release: 'experimental', - conditions: { kibana: { version: 'x.y.z' } }, - owner: { github: 'elastic/fleet' }, - }, - }, - installType: 'install', - installSource: 'registry', - spaceId: DEFAULT_SPACE_ID, - }); - - expect(installILMPolicy).not.toBeCalled(); - expect(installIlmForDataStream).not.toBeCalled(); - // if we have a .catch this will fail nicely (test pass) - // otherwise the test will fail with either of the mocked errors - // await expect(installationPromise).rejects.toThrow('mocked'); - // await expect(installationPromise).rejects.toThrow('should be caught'); - }); - - it('install ILM policies if not disabled in config', async () => { - appContextService.start( - createAppContextStartContractMock({ - internal: { - disableILMPolicies: false, - fleetServerStandalone: false, - onlyAllowAgentUpgradeToKnownVersions: false, - retrySetupOnBoot: false, - registry: { - kibanaVersionCheckEnabled: true, - capabilities: [], - excludePackages: [], - }, - }, - }) - ); - // force errors from this function - mockedInstallKibanaAssetsAndReferences.mockResolvedValue([]); - // pick any function between when those are called and when await Promise.all is defined later - // and force it to take long enough for the errors to occur - // @ts-expect-error about call signature - mockedUpdateCurrentWriteIndices.mockImplementation(async () => await sleep(1000)); - mockedInstallIndexTemplatesAndPipelines.mockResolvedValue({ - installedTemplates: [], - esReferences: [], - }); - await _installPackage({ - savedObjectsClient: soClient, - // @ts-ignore - savedObjectsImporter: jest.fn(), - esClient, - logger: loggerMock.create(), - packageInstallContext: { - packageInfo: { - title: 'title', - name: 'xyz', - version: '4.5.6', - description: 'test', - type: 'integration', - categories: ['cloud', 'custom'], - format_version: 'string', - release: 'experimental', - conditions: { kibana: { version: 'x.y.z' } }, - owner: { github: 'elastic/fleet' }, - } as any, - assetsMap: new Map(), - paths: [], - }, - installType: 'install', - installSource: 'registry', - spaceId: DEFAULT_SPACE_ID, - }); - - expect(installILMPolicy).toBeCalled(); - expect(installIlmForDataStream).toBeCalled(); - }); - - describe('when package is stuck in `installing`', () => { - const mockInstalledPackageSo: SavedObject = { - id: 'mocked-package', - attributes: { - name: 'test-package', - version: '1.0.0', - install_status: 'installing', - install_version: '1.0.0', - install_started_at: new Date().toISOString(), - install_source: 'registry', - verification_status: 'verified', - installed_kibana: [] as any, - installed_es: [] as any, - es_index_patterns: {}, - }, - type: PACKAGES_SAVED_OBJECT_TYPE, - references: [], - }; - - beforeEach(() => { - appContextService.start( - createAppContextStartContractMock({ - internal: { - disableILMPolicies: true, - fleetServerStandalone: false, - onlyAllowAgentUpgradeToKnownVersions: false, - retrySetupOnBoot: false, - registry: { - kibanaVersionCheckEnabled: true, - capabilities: [], - excludePackages: [], - }, - }, - }) - ); - }); - - describe('timeout reached', () => { - it('restarts installation', async () => { - await _installPackage({ - savedObjectsClient: soClient, - // @ts-ignore - savedObjectsImporter: jest.fn(), - esClient, - logger: loggerMock.create(), - packageInstallContext: { - paths: [], - assetsMap: new Map(), - packageInfo: { - name: mockInstalledPackageSo.attributes.name, - version: mockInstalledPackageSo.attributes.version, - title: mockInstalledPackageSo.attributes.name, - } as any, - }, - installedPkg: { - ...mockInstalledPackageSo, - attributes: { - ...mockInstalledPackageSo.attributes, - install_started_at: new Date( - Date.now() - MAX_TIME_COMPLETE_INSTALL * 2 - ).toISOString(), - }, - }, - }); - - expect(restartInstallation).toBeCalled(); - }); - }); - - describe('timeout not reached', () => { - describe('force flag not provided', () => { - it('throws concurrent installation error if force flag is not provided', async () => { - await expect( - _installPackage({ - savedObjectsClient: soClient, - // @ts-ignore - savedObjectsImporter: jest.fn(), - esClient, - logger: loggerMock.create(), - packageInstallContext: { - paths: [], - assetsMap: new Map(), - packageInfo: { - name: mockInstalledPackageSo.attributes.name, - version: mockInstalledPackageSo.attributes.version, - title: mockInstalledPackageSo.attributes.name, - } as any, - }, - installedPkg: { - ...mockInstalledPackageSo, - attributes: { - ...mockInstalledPackageSo.attributes, - install_started_at: new Date(Date.now() - 1000).toISOString(), - }, - }, - }) - ).rejects.toThrowError(ConcurrentInstallOperationError); - }); - }); - - describe('force flag provided', () => { - it('restarts installation', async () => { - await _installPackage({ - savedObjectsClient: soClient, - // @ts-ignore - savedObjectsImporter: jest.fn(), - esClient, - logger: loggerMock.create(), - packageInstallContext: { - paths: [], - assetsMap: new Map(), - packageInfo: { - name: mockInstalledPackageSo.attributes.name, - version: mockInstalledPackageSo.attributes.version, - title: mockInstalledPackageSo.attributes.name, - } as any, - }, - installedPkg: { - ...mockInstalledPackageSo, - attributes: { - ...mockInstalledPackageSo.attributes, - install_started_at: new Date(Date.now() - 1000).toISOString(), - }, - }, - force: true, - }); - - expect(restartInstallation).toBeCalled(); - }); - }); - }); - }); - - it('surfaces saved object conflicts error', async () => { - appContextService.start( - createAppContextStartContractMock({ - internal: { - disableILMPolicies: false, - fleetServerStandalone: false, - onlyAllowAgentUpgradeToKnownVersions: false, - retrySetupOnBoot: false, - registry: { - kibanaVersionCheckEnabled: true, - capabilities: [], - excludePackages: [], - }, - }, - }) - ); - - mockedInstallKibanaAssetsAndReferences.mockRejectedValueOnce( - new PackageSavedObjectConflictError('test') - ); - - await expect( - _installPackage({ - savedObjectsClient: soClient, - // @ts-ignore - savedObjectsImporter: jest.fn(), - esClient, - logger: loggerMock.create(), - packageInstallContext: { - packageInfo: { - title: 'title', - name: 'xyz', - version: '4.5.6', - description: 'test', - type: 'integration', - categories: ['cloud', 'custom'], - format_version: 'string', - release: 'experimental', - conditions: { kibana: { version: 'x.y.z' } }, - owner: { github: 'elastic/fleet' }, - } as any, - assetsMap: new Map(), - paths: [], - }, - installType: 'install', - installSource: 'registry', - spaceId: DEFAULT_SPACE_ID, - }) - ).rejects.toThrowError(PackageSavedObjectConflictError); - }); -}); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts deleted file mode 100644 index 43563682094c4..0000000000000 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts +++ /dev/null @@ -1,388 +0,0 @@ -/* - * 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 type { - ElasticsearchClient, - Logger, - SavedObject, - SavedObjectsClientContract, -} from '@kbn/core/server'; -import { SavedObjectsErrorHelpers } from '@kbn/core/server'; - -import type { HTTPAuthorizationHeader } from '../../../../common/http_authorization_header'; -import type { PackageInstallContext } from '../../../../common/types'; -import { getNormalizedDataStreams } from '../../../../common/services'; - -import { - MAX_TIME_COMPLETE_INSTALL, - ASSETS_SAVED_OBJECT_TYPE, - LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE, - SO_SEARCH_LIMIT, -} from '../../../../common/constants'; -import { PACKAGES_SAVED_OBJECT_TYPE, FLEET_INSTALL_FORMAT_VERSION } from '../../../constants'; -import type { - AssetReference, - Installation, - InstallType, - InstallSource, - PackageAssetReference, - PackageVerificationResult, - IndexTemplateEntry, -} from '../../../types'; -import { removeLegacyTemplates } from '../elasticsearch/template/remove_legacy'; -import { isTopLevelPipeline, deletePreviousPipelines } from '../elasticsearch/ingest_pipeline'; -import { installILMPolicy } from '../elasticsearch/ilm/install'; -import { installKibanaAssetsAndReferences } from '../kibana/assets/install'; -import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; -import { installTransforms } from '../elasticsearch/transform/install'; -import { installMlModel } from '../elasticsearch/ml_model'; -import { installIlmForDataStream } from '../elasticsearch/datastream_ilm/install'; -import { saveArchiveEntriesFromAssetsMap } from '../archive/storage'; -import { ConcurrentInstallOperationError, PackageSavedObjectConflictError } from '../../../errors'; -import { appContextService, packagePolicyService } from '../..'; - -import { auditLoggingService } from '../../audit_logging'; - -import { createInstallation, restartInstallation } from './install'; -import { withPackageSpan } from './utils'; -import { clearLatestFailedAttempts } from './install_errors_helpers'; -import { installIndexTemplatesAndPipelines } from './install_index_template_pipeline'; - -// TODO: when installByUpload and installBundle are migrated, remove this function in favor of _state_machine_package_install -// this is only exported for testing -// use a leading underscore to indicate it's not the supported path -// only the more explicit `installPackage*` functions should be used -export async function _installPackage({ - savedObjectsClient, - esClient, - logger, - installedPkg, - packageInstallContext, - installType, - installSource, - spaceId, - force, - verificationResult, - authorizationHeader, - ignoreMappingUpdateErrors, - skipDataStreamRollover, -}: { - savedObjectsClient: SavedObjectsClientContract; - esClient: ElasticsearchClient; - logger: Logger; - installedPkg?: SavedObject; - packageInstallContext: PackageInstallContext; - installType: InstallType; - installSource: InstallSource; - spaceId: string; - force?: boolean; - verificationResult?: PackageVerificationResult; - authorizationHeader?: HTTPAuthorizationHeader | null; - ignoreMappingUpdateErrors?: boolean; - skipDataStreamRollover?: boolean; -}): Promise { - const { packageInfo, paths } = packageInstallContext; - const { name: pkgName, version: pkgVersion, title: pkgTitle } = packageInfo; - - try { - // if some installation already exists - if (installedPkg) { - const isStatusInstalling = installedPkg.attributes.install_status === 'installing'; - const hasExceededTimeout = - Date.now() - Date.parse(installedPkg.attributes.install_started_at) < - MAX_TIME_COMPLETE_INSTALL; - logger.debug( - `Package install - Install status ${pkgName}-${pkgVersion}: ${installedPkg.attributes.install_status}` - ); - - // if the installation is currently running, don't try to install - // instead, only return already installed assets - if (isStatusInstalling && hasExceededTimeout) { - // If this is a forced installation, ignore the timeout and restart the installation anyway - logger.debug(`Package install - Installation is running and has exceeded timeout`); - - if (force) { - logger.debug(`Package install - Forced installation, restarting`); - await restartInstallation({ - savedObjectsClient, - pkgName, - pkgVersion, - installSource, - verificationResult, - }); - } else { - throw new ConcurrentInstallOperationError( - `Concurrent installation or upgrade of ${pkgName || 'unknown'}-${ - pkgVersion || 'unknown' - } detected, aborting.` - ); - } - } else { - // if no installation is running, or the installation has been running longer than MAX_TIME_COMPLETE_INSTALL - // (it might be stuck) update the saved object and proceed - logger.debug( - `Package install - no installation running or the installation has been running longer than ${MAX_TIME_COMPLETE_INSTALL}, restarting` - ); - await restartInstallation({ - savedObjectsClient, - pkgName, - pkgVersion, - installSource, - verificationResult, - }); - } - } else { - logger.debug(`Package install - Create installation ${pkgName}-${pkgVersion}`); - await createInstallation({ - savedObjectsClient, - packageInfo, - installSource, - spaceId, - verificationResult, - }); - } - logger.debug(`Package install - Installing Kibana assets`); - const kibanaAssetPromise = withPackageSpan('Install Kibana assets', () => - installKibanaAssetsAndReferences({ - savedObjectsClient, - pkgName, - pkgTitle, - packageInstallContext, - installedPkg, - logger, - spaceId, - assetTags: packageInfo?.asset_tags, - }) - ); - // Necessary to avoid async promise rejection warning - // See https://stackoverflow.com/questions/40920179/should-i-refrain-from-handling-promise-rejection-asynchronously - kibanaAssetPromise.catch(() => {}); - - // Use a shared array that is updated by each operation. This allows each operation to accurately update the - // installation object with it's references without requiring a refresh of the SO index on each update (faster). - let esReferences = installedPkg?.attributes.installed_es ?? []; - - // the rest of the installation must happen in sequential order - // currently only the base package has an ILM policy - // at some point ILM policies can be installed/modified - // per data stream and we should then save them - const isILMPoliciesDisabled = - appContextService.getConfig()?.internal?.disableILMPolicies ?? false; - if (!isILMPoliciesDisabled) { - esReferences = await withPackageSpan('Install ILM policies', () => - installILMPolicy(packageInstallContext, esClient, savedObjectsClient, logger, esReferences) - ); - logger.debug(`Package install - Installing Data Stream ILM policies`); - ({ esReferences } = await withPackageSpan('Install Data Stream ILM policies', () => - installIlmForDataStream( - packageInstallContext, - esClient, - savedObjectsClient, - logger, - esReferences - ) - )); - } - - // installs ml models - logger.debug(`Package install - installing ML models`); - esReferences = await withPackageSpan('Install ML models', () => - installMlModel(packageInstallContext, esClient, savedObjectsClient, logger, esReferences) - ); - - let indexTemplates: IndexTemplateEntry[] = []; - - if (packageInfo.type === 'integration') { - logger.debug( - `Package install - Installing index templates and pipelines, packageInfo.type ${packageInfo.type}` - ); - const { installedTemplates, esReferences: templateEsReferences } = - await installIndexTemplatesAndPipelines({ - installedPkg: installedPkg ? installedPkg.attributes : undefined, - packageInstallContext, - esClient, - savedObjectsClient, - logger, - esReferences, - }); - esReferences = templateEsReferences; - indexTemplates = installedTemplates; - } - - if (packageInfo.type === 'input' && installedPkg) { - // input packages create their data streams during package policy creation - // we must use installed_es to infer which streams exist first then - // we can install the new index templates - logger.debug(`Package install - packageInfo.type ${packageInfo.type}`); - const dataStreamNames = installedPkg.attributes.installed_es - .filter((ref) => ref.type === 'index_template') - // index templates are named {type}-{dataset}, remove everything before first hyphen - .map((ref) => ref.id.replace(/^[^-]+-/, '')); - - const dataStreams = dataStreamNames.flatMap((dataStreamName) => - getNormalizedDataStreams(packageInfo, dataStreamName) - ); - - if (dataStreams.length) { - logger.debug( - `Package install - installing index templates and pipelines with datastreams length ${dataStreams.length}` - ); - const { installedTemplates, esReferences: templateEsReferences } = - await installIndexTemplatesAndPipelines({ - installedPkg: installedPkg ? installedPkg.attributes : undefined, - packageInstallContext, - esClient, - savedObjectsClient, - logger, - esReferences, - onlyForDataStreams: dataStreams, - }); - esReferences = templateEsReferences; - indexTemplates = installedTemplates; - } - } - - try { - logger.debug(`Package install - Removing legacy templates`); - await removeLegacyTemplates({ packageInfo, esClient, logger }); - } catch (e) { - logger.warn(`Error removing legacy templates: ${e.message}`); - } - - // update current backing indices of each data stream - logger.debug(`Package install - Updating backing indices of each data stream`); - await withPackageSpan('Update write indices', () => - updateCurrentWriteIndices(esClient, logger, indexTemplates, { - ignoreMappingUpdateErrors, - skipDataStreamRollover, - }) - ); - logger.debug(`Package install - Installing transforms`); - ({ esReferences } = await withPackageSpan('Install transforms', () => - installTransforms({ - packageInstallContext, - esClient, - savedObjectsClient, - logger, - esReferences, - force, - authorizationHeader, - }) - )); - - // If this is an update or retrying an update, delete the previous version's pipelines - // Top-level pipeline assets will not be removed on upgrade as of ml model package addition which requires previous - // assets to remain installed. This is a temporary solution - more robust solution tracked here https://github.com/elastic/kibana/issues/115035 - if ( - paths.filter((path) => isTopLevelPipeline(path)).length === 0 && - (installType === 'update' || installType === 'reupdate') && - installedPkg - ) { - logger.debug( - `Package install - installType ${installType} Deleting previous ingest pipelines` - ); - esReferences = await withPackageSpan('Delete previous ingest pipelines', () => - deletePreviousPipelines( - esClient, - savedObjectsClient, - pkgName, - installedPkg!.attributes.version, - esReferences - ) - ); - } - // pipelines from a different version may have installed during a failed update - if (installType === 'rollback' && installedPkg) { - logger.debug( - `Package install - installType ${installType} Deleting previous ingest pipelines` - ); - esReferences = await withPackageSpan('Delete previous ingest pipelines', () => - deletePreviousPipelines( - esClient, - savedObjectsClient, - pkgName, - installedPkg!.attributes.install_version, - esReferences - ) - ); - } - - const installedKibanaAssetsRefs = await kibanaAssetPromise; - logger.debug(`Package install - Updating archive entries`); - const packageAssetResults = await withPackageSpan('Update archive entries', () => - saveArchiveEntriesFromAssetsMap({ - savedObjectsClient, - assetsMap: packageInstallContext.assetsMap, - paths: packageInstallContext.paths, - packageInfo, - installSource, - }) - ); - const packageAssetRefs: PackageAssetReference[] = packageAssetResults.saved_objects.map( - (result) => ({ - id: result.id, - type: ASSETS_SAVED_OBJECT_TYPE, - }) - ); - - auditLoggingService.writeCustomSoAuditLog({ - action: 'update', - id: pkgName, - savedObjectType: PACKAGES_SAVED_OBJECT_TYPE, - }); - logger.debug(`Package install - Updating install status`); - await withPackageSpan('Update install status', () => - savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { - version: pkgVersion, - install_version: pkgVersion, - install_status: 'installed', - package_assets: packageAssetRefs, - install_format_schema_version: FLEET_INSTALL_FORMAT_VERSION, - latest_install_failed_attempts: clearLatestFailedAttempts( - pkgVersion, - installedPkg?.attributes.latest_install_failed_attempts ?? [] - ), - }) - ); - - // Need to refetch the installation again to retrieve all the attributes - const updatedPackage = await savedObjectsClient.get( - PACKAGES_SAVED_OBJECT_TYPE, - pkgName - ); - logger.debug(`Package install - Install status ${updatedPackage?.attributes?.install_status}`); - // If the package is flagged with the `keep_policies_up_to_date` flag, upgrade its - // associated package policies after installation - if (updatedPackage.attributes.keep_policies_up_to_date) { - await withPackageSpan('Upgrade package policies', async () => { - const policyIdsToUpgrade = await packagePolicyService.listIds(savedObjectsClient, { - page: 1, - perPage: SO_SEARCH_LIMIT, - kuery: `${LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${pkgName}`, - }); - logger.debug( - `Package install - Package is flagged with keep_policies_up_to_date, upgrading its associated package policies ${policyIdsToUpgrade}` - ); - await packagePolicyService.upgrade(savedObjectsClient, esClient, policyIdsToUpgrade.items); - }); - } - logger.debug(`Package install - Installation complete`); - return [...installedKibanaAssetsRefs, ...esReferences]; - } catch (err) { - if (SavedObjectsErrorHelpers.isConflictError(err)) { - throw new PackageSavedObjectConflictError( - `Saved Object conflict encountered while installing ${pkgName || 'unknown'}-${ - pkgVersion || 'unknown' - }. There may be a conflicting Saved Object saved to another Space. Original error: ${ - err.message - }` - ); - } else { - throw err; - } - } -} diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts index afd368d61497e..a0bd8c8d77fe6 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts @@ -28,7 +28,6 @@ import { installPackage, isPackageVersionOrLaterInstalled, } from './install'; -import * as install from './_install_package'; import * as installStateMachine from './install_state_machine/_state_machine_package_install'; import { getBundledPackageByPkgKey } from './bundled_packages'; @@ -76,11 +75,6 @@ jest.mock('../../license'); jest.mock('../../upgrade_sender'); jest.mock('./cleanup'); jest.mock('./bundled_packages'); -jest.mock('./_install_package', () => { - return { - _installPackage: jest.fn(() => Promise.resolve()), - }; -}); jest.mock('./install_state_machine/_state_machine_package_install', () => { return { _stateMachineInstallPackage: jest.fn(() => Promise.resolve()), @@ -173,7 +167,6 @@ describe('install', () => { ); mockGetBundledPackageByPkgKey.mockReset(); - (install._installPackage as jest.Mock).mockClear(); (installStateMachine._stateMachineInstallPackage as jest.Mock).mockClear(); jest.mocked(appContextService.getInternalUserSOClientForSpaceId).mockReset(); }); @@ -182,9 +175,6 @@ describe('install', () => { beforeEach(() => { mockGetBundledPackageByPkgKey.mockResolvedValue(undefined); }); - afterEach(() => { - (install._installPackage as jest.Mock).mockClear(); - }); it('should send telemetry on install failure, out of date', async () => { await installPackage({ @@ -320,7 +310,7 @@ describe('install', () => { expect(response.error).toBeUndefined(); - expect(install._installPackage).toHaveBeenCalledWith( + expect(installStateMachine._stateMachineInstallPackage).toHaveBeenCalledWith( expect.objectContaining({ installSource: 'bundled' }) ); }); @@ -435,7 +425,9 @@ describe('install', () => { }); it('should send telemetry on install failure, async error', async () => { - jest.mocked(install._installPackage).mockRejectedValue(new Error('error')); + jest + .mocked(installStateMachine._stateMachineInstallPackage) + .mockRejectedValue(new Error('error')); jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true); await installPackage({ spaceId: DEFAULT_SPACE_ID, @@ -556,11 +548,11 @@ describe('handleInstallPackageFailure', () => { beforeEach(() => { mockedLogger.error.mockClear(); - jest.mocked(install._installPackage).mockClear(); + jest.mocked(installStateMachine._stateMachineInstallPackage).mockClear(); jest.mocked(installStateMachine._stateMachineInstallPackage).mockClear(); mockGetBundledPackageByPkgKey.mockReset(); - jest.mocked(install._installPackage).mockResolvedValue({} as any); + jest.mocked(installStateMachine._stateMachineInstallPackage).mockResolvedValue({} as any); mockGetBundledPackageByPkgKey.mockResolvedValue(undefined); jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true); jest.spyOn(Registry, 'splitPkgKey').mockImplementation((pkgKey: string) => { @@ -610,7 +602,7 @@ describe('handleInstallPackageFailure', () => { }); expect(mockedLogger.error).not.toBeCalled(); - expect(install._installPackage).not.toBeCalled(); + expect(installStateMachine._stateMachineInstallPackage).not.toBeCalled(); }); it('should rollback on upgrade on FleetError', async () => { diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index 7b49158712f6a..65f1a75f76f84 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -87,7 +87,6 @@ import { _stateMachineInstallPackage } from './install_state_machine/_state_mach import { formatVerificationResultForSO } from './package_verification'; import { getInstallation, getInstallationObject } from './get'; import { getInstalledPackageWithAssets, getPackageSavedObjects } from './get'; -import { _installPackage } from './_install_package'; import { removeOldAssets } from './cleanup'; import { getBundledPackageByPkgKey } from './bundled_packages'; import { convertStringToTitle, generateDescription } from './custom_integrations/utils'; @@ -545,167 +544,6 @@ function getElasticSubscription(packageInfo: ArchivePackage) { return subscription || packageInfo.license || 'basic'; } -// TODO: when installByUpload and installBundle are migrated, remove this function in favor of installWithStateMachine -async function installPackageCommon(options: { - pkgName: string; - pkgVersion: string; - installSource: InstallSource; - installedPkg?: SavedObject; - installType: InstallType; - savedObjectsClient: SavedObjectsClientContract; - esClient: ElasticsearchClient; - spaceId: string; - force?: boolean; - packageInstallContext: PackageInstallContext; - paths: string[]; - verificationResult?: PackageVerificationResult; - telemetryEvent?: PackageUpdateEvent; - authorizationHeader?: HTTPAuthorizationHeader | null; - ignoreMappingUpdateErrors?: boolean; - skipDataStreamRollover?: boolean; -}): Promise { - const packageInfo = options.packageInstallContext.packageInfo; - - const { - pkgName, - pkgVersion, - installSource, - installedPkg, - installType, - savedObjectsClient, - force, - esClient, - spaceId, - verificationResult, - authorizationHeader, - ignoreMappingUpdateErrors, - skipDataStreamRollover, - packageInstallContext, - } = options; - let { telemetryEvent } = options; - const logger = appContextService.getLogger(); - logger.info(`Install - Starting installation of ${pkgName}@${pkgVersion} from ${installSource} `); - - // Workaround apm issue with async spans: https://github.com/elastic/apm-agent-nodejs/issues/2611 - await Promise.resolve(); - const span = apm.startSpan( - `Install package from ${installSource} ${pkgName}@${pkgVersion}`, - 'package' - ); - - if (!telemetryEvent) { - telemetryEvent = getTelemetryEvent(pkgName, pkgVersion); - telemetryEvent.installType = installType; - telemetryEvent.currentVersion = installedPkg?.attributes.version || 'not_installed'; - } - - try { - span?.addLabels({ - packageName: pkgName, - packageVersion: pkgVersion, - installType, - }); - - const filteredPackages = getFilteredInstallPackages(); - if (filteredPackages.includes(pkgName)) { - throw new FleetUnauthorizedError(`${pkgName} installation is not authorized`); - } - - // if the requested version is the same as installed version, check if we allow it based on - // current installed package status and force flag, if we don't allow it, - // just return the asset references from the existing installation - if ( - installedPkg?.attributes.version === pkgVersion && - installedPkg?.attributes.install_status === 'installed' - ) { - if (!force) { - logger.debug(`${pkgName}-${pkgVersion} is already installed, skipping installation`); - return { - assets: [ - ...installedPkg.attributes.installed_es, - ...installedPkg.attributes.installed_kibana, - ], - status: 'already_installed', - installType, - installSource, - }; - } - } - const elasticSubscription = getElasticSubscription(packageInfo); - if (!licenseService.hasAtLeast(elasticSubscription)) { - logger.error(`Installation requires ${elasticSubscription} license`); - const err = new FleetError(`Installation requires ${elasticSubscription} license`); - sendEvent({ - ...telemetryEvent, - errorMessage: err.message, - }); - return { error: err, installType, installSource }; - } - - // try installing the package, if there was an error, call error handler and rethrow - return await _installPackage({ - savedObjectsClient, - esClient, - logger, - installedPkg, - packageInstallContext, - installType, - spaceId, - verificationResult, - installSource, - authorizationHeader, - force, - ignoreMappingUpdateErrors, - skipDataStreamRollover, - }) - .then(async (assets) => { - logger.debug(`Removing old assets from previous versions of ${pkgName}`); - await removeOldAssets({ - soClient: savedObjectsClient, - pkgName: packageInfo.name, - currentVersion: packageInfo.version, - }); - sendEvent({ - ...telemetryEvent!, - status: 'success', - }); - return { assets, status: 'installed' as InstallResultStatus, installType, installSource }; - }) - .catch(async (err: Error) => { - logger.warn(`Failure to install package [${pkgName}]: [${err.toString()}]`, { - error: { stack_trace: err.stack }, - }); - await handleInstallPackageFailure({ - savedObjectsClient, - error: err, - pkgName, - pkgVersion, - installedPkg, - spaceId, - esClient, - authorizationHeader, - }); - sendEvent({ - ...telemetryEvent!, - errorMessage: err.message, - }); - return { error: err, installType, installSource }; - }); - } catch (e) { - sendEvent({ - ...telemetryEvent, - errorMessage: e.message, - }); - return { - error: e, - installType, - installSource, - }; - } finally { - span?.end(); - } -} - async function installPackageWithStateMachine(options: { pkgName: string; pkgVersion: string; @@ -966,8 +804,7 @@ async function installPackageByUpload({ // update the timestamp of latest installation setLastUploadInstallCache(); - // TODO: use installPackageWithStateMachine instead of installPackageCommon https://github.com/elastic/kibana/issues/189346 - return await installPackageCommon({ + return await installPackageWithStateMachine({ packageInstallContext, pkgName, pkgVersion, @@ -1156,8 +993,7 @@ export async function installCustomPackage( paths, packageInfo, }; - // TODO: use installPackageWithStateMachine instead of installPackageCommon https://github.com/elastic/kibana/issues/189347 - return await installPackageCommon({ + return await installPackageWithStateMachine({ packageInstallContext, pkgName, pkgVersion: INITIAL_VERSION,