diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts index 102b05951515..c900cd85c438 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts @@ -9,7 +9,11 @@ import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/serve import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; -import type { PreconfiguredAgentPolicy, PreconfiguredOutput } from '../../common/types'; +import type { + InstallResult, + PreconfiguredAgentPolicy, + PreconfiguredOutput, +} from '../../common/types'; import type { AgentPolicy, NewPackagePolicy, Output } from '../types'; import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../constants'; @@ -30,6 +34,7 @@ jest.mock('./output'); const mockedOutputService = outputService as jest.Mocked; const mockInstalledPackages = new Map(); +const mockInstallPackageErrors = new Map(); const mockConfiguredPolicies = new Map(); const mockDefaultOutput: Output = { @@ -99,8 +104,22 @@ function getPutPreconfiguredPackagesMock() { } jest.mock('./epm/packages/install', () => ({ - installPackage({ pkgkey, force }: { pkgkey: string; force?: boolean }) { + async installPackage({ + pkgkey, + force, + }: { + pkgkey: string; + force?: boolean; + }): Promise { const [pkgName, pkgVersion] = pkgkey.split('-'); + const installError = mockInstallPackageErrors.get(pkgName); + if (installError) { + return { + error: new Error(installError), + installType: 'install', + }; + } + const installedPackage = mockInstalledPackages.get(pkgName); if (installedPackage) { if (installedPackage.version === pkgVersion) return installedPackage; @@ -109,7 +128,10 @@ jest.mock('./epm/packages/install', () => ({ const packageInstallation = { name: pkgName, version: pkgVersion, title: pkgName }; mockInstalledPackages.set(pkgName, packageInstallation); - return packageInstallation; + return { + status: 'installed', + installType: 'install', + }; }, ensurePackagesCompletedInstall() { return []; @@ -133,6 +155,8 @@ jest.mock('./epm/packages/get', () => ({ }, })); +jest.mock('./epm/kibana/index_pattern/install'); + jest.mock('./package_policy', () => ({ ...jest.requireActual('./package_policy'), packagePolicyService: { @@ -177,6 +201,7 @@ const spyAgentPolicyServicBumpAllAgentPoliciesForOutput = jest.spyOn( describe('policy preconfiguration', () => { beforeEach(() => { mockInstalledPackages.clear(); + mockInstallPackageErrors.clear(); mockConfiguredPolicies.clear(); spyAgentPolicyServiceUpdate.mockClear(); spyAgentPolicyServicBumpAllAgentPoliciesForOutput.mockClear(); @@ -266,7 +291,38 @@ describe('policy preconfiguration', () => { ); }); - it('should not create a policy if we are not able to add packages ', async () => { + it('should not create a policy and throw an error if install fails for required package', async () => { + const soClient = getPutPreconfiguredPackagesMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + const policies: PreconfiguredAgentPolicy[] = [ + { + name: 'Test policy', + namespace: 'default', + id: 'test-id', + package_policies: [ + { + package: { name: 'test_package' }, + name: 'Test package', + }, + ], + }, + ]; + mockInstallPackageErrors.set('test_package', 'REGISTRY ERROR'); + + await expect( + ensurePreconfiguredPackagesAndPolicies( + soClient, + esClient, + policies, + [{ name: 'test_package', version: '3.0.0' }], + mockDefaultOutput + ) + ).rejects.toThrow( + '[Test policy] could not be added. [test_package] could not be installed due to error: [Error: REGISTRY ERROR]' + ); + }); + + it('should not create a policy and throw an error if package is not installed for an unknown reason', async () => { const soClient = getPutPreconfiguredPackagesMock(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; const policies: PreconfiguredAgentPolicy[] = [ @@ -283,22 +339,16 @@ describe('policy preconfiguration', () => { }, ]; - let error; - try { - await ensurePreconfiguredPackagesAndPolicies( + await expect( + ensurePreconfiguredPackagesAndPolicies( soClient, esClient, policies, [{ name: 'CANNOT_MATCH', version: 'x.y.z' }], mockDefaultOutput - ); - } catch (err) { - error = err; - } - - expect(error).toBeDefined(); - expect(error.message).toEqual( - 'Test policy could not be added. test_package is not installed, add test_package to `xpack.fleet.packages` or remove it from Test package.' + ) + ).rejects.toThrow( + '[Test policy] could not be added. [test_package] is not installed, add [test_package] to [xpack.fleet.packages] or remove it from [Test package].' ); }); it('should not attempt to recreate or modify an agent policy if its ID is unchanged', async () => { diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts index 3b322e1112d6..d171030b06a8 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts @@ -147,6 +147,8 @@ export async function ensurePreconfiguredPackagesAndPolicies( packages: PreconfiguredPackage[] = [], defaultOutput: Output ): Promise { + const logger = appContextService.getLogger(); + // Validate configured packages to ensure there are no version conflicts const packageNames = groupBy(packages, (pkg) => pkg.name); const duplicatePackages = Object.entries(packageNames).filter( @@ -181,15 +183,20 @@ export async function ensurePreconfiguredPackagesAndPolicies( }); const fulfilledPackages = []; - const rejectedPackages = []; + const rejectedPackages: PreconfigurationError[] = []; for (let i = 0; i < preconfiguredPackages.length; i++) { const packageResult = preconfiguredPackages[i]; - if ('error' in packageResult) + if ('error' in packageResult) { + logger.warn( + `Failed installing package [${packages[i].name}] due to error: [${packageResult.error}]` + ); rejectedPackages.push({ package: { name: packages[i].name, version: packages[i].version }, error: packageResult.error, - } as PreconfigurationError); - else fulfilledPackages.push(packageResult); + }); + } else { + fulfilledPackages.push(packageResult); + } } // Keeping this outside of the Promise.all because it introduces a race condition. @@ -264,14 +271,14 @@ export async function ensurePreconfiguredPackagesAndPolicies( ); const fulfilledPolicies = []; - const rejectedPolicies = []; + const rejectedPolicies: PreconfigurationError[] = []; for (let i = 0; i < preconfiguredPolicies.length; i++) { const policyResult = preconfiguredPolicies[i]; if (policyResult.status === 'rejected') { rejectedPolicies.push({ error: policyResult.reason as Error, agentPolicy: { name: policies[i].name }, - } as PreconfigurationError); + }); continue; } fulfilledPolicies.push(policyResult.value); @@ -288,10 +295,25 @@ export async function ensurePreconfiguredPackagesAndPolicies( pkgName: pkg.name, }); if (!installedPackage) { + const rejectedPackage = rejectedPackages.find((rp) => rp.package?.name === pkg.name); + + if (rejectedPackage) { + throw new Error( + i18n.translate('xpack.fleet.preconfiguration.packageRejectedError', { + defaultMessage: `[{agentPolicyName}] could not be added. [{pkgName}] could not be installed due to error: [{errorMessage}]`, + values: { + agentPolicyName: preconfiguredAgentPolicy.name, + pkgName: pkg.name, + errorMessage: rejectedPackage.error.toString(), + }, + }) + ); + } + throw new Error( i18n.translate('xpack.fleet.preconfiguration.packageMissingError', { defaultMessage: - '{agentPolicyName} could not be added. {pkgName} is not installed, add {pkgName} to `{packagesConfigValue}` or remove it from {packagePolicyName}.', + '[{agentPolicyName}] could not be added. [{pkgName}] is not installed, add [{pkgName}] to [{packagesConfigValue}] or remove it from [{packagePolicyName}].', values: { agentPolicyName: preconfiguredAgentPolicy.name, packagePolicyName: name,