[Fleet] Fix reinstalling bundled package during setup (#171321)

This commit is contained in:
Nicolas Chaulet 2023-11-15 14:04:12 -05:00 committed by GitHub
parent 9686d57daa
commit 9626900d5e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 193 additions and 43 deletions

View file

@ -8,7 +8,7 @@
import fs from 'fs/promises';
import path from 'path';
import type { BundledPackage } from '../../../types';
import type { BundledPackage, Installation } from '../../../types';
import { FleetError } from '../../../errors';
import { appContextService } from '../../app_context';
import { splitPkgKey, pkgToPkgKey } from '../registry';
@ -57,24 +57,33 @@ export async function getBundledPackages(): Promise<BundledPackage[]> {
}
}
export async function getBundledPackageForInstallation(
installation: Installation
): Promise<BundledPackage | undefined> {
const bundledPackages = await getBundledPackages();
return bundledPackages.find(
(bundledPkg: BundledPackage) =>
bundledPkg.name === installation.name && bundledPkg.version === installation.version
);
}
export async function getBundledPackageByPkgKey(
pkgKey: string
): Promise<BundledPackage | undefined> {
const bundledPackages = await getBundledPackages();
const bundledPackage = bundledPackages.find((pkg) => {
return bundledPackages.find((pkg) => {
if (pkgKey.includes('-')) {
return pkgToPkgKey(pkg) === pkgKey;
} else {
return pkg.name === pkgKey;
}
});
return bundledPackage;
}
export async function getBundledPackageByName(name: string): Promise<BundledPackage | undefined> {
const bundledPackages = await getBundledPackages();
const bundledPackage = bundledPackages.find((pkg) => pkg.name === name);
return bundledPackage;
return bundledPackages.find((pkg) => pkg.name === name);
}

View file

@ -282,7 +282,7 @@ describe('install', () => {
expect(response.error).toBeUndefined();
expect(install._installPackage).toHaveBeenCalledWith(
expect.objectContaining({ installSource: 'upload' })
expect.objectContaining({ installSource: 'bundled' })
);
});

View file

@ -328,6 +328,7 @@ interface InstallUploadedArchiveParams {
authorizationHeader?: HTTPAuthorizationHeader | null;
ignoreMappingUpdateErrors?: boolean;
skipDataStreamRollover?: boolean;
isBundledPackage?: boolean;
}
function getTelemetryEvent(pkgName: string, pkgVersion: string): PackageUpdateEvent {
@ -460,7 +461,7 @@ function getElasticSubscription(packageInfo: ArchivePackage) {
async function installPackageCommon(options: {
pkgName: string;
pkgVersion: string;
installSource: 'registry' | 'upload' | 'custom';
installSource: InstallSource;
installedPkg?: SavedObject<Installation>;
installType: InstallType;
savedObjectsClient: SavedObjectsClientContract;
@ -638,10 +639,11 @@ async function installPackageByUpload({
authorizationHeader,
ignoreMappingUpdateErrors,
skipDataStreamRollover,
isBundledPackage,
}: InstallUploadedArchiveParams): Promise<InstallResult> {
// if an error happens during getInstallType, report that we don't know
let installType: InstallType = 'unknown';
const installSource = 'upload';
const installSource = isBundledPackage ? 'bundled' : 'upload';
try {
const { packageInfo } = await generatePackageInfoFromArchiveBuffer(archiveBuffer, contentType);
const pkgName = packageInfo.name;
@ -747,6 +749,7 @@ export async function installPackage(args: InstallPackageParams): Promise<Instal
authorizationHeader,
ignoreMappingUpdateErrors,
skipDataStreamRollover,
isBundledPackage: true,
});
return { ...response, installSource: 'bundled' };

View file

@ -11,14 +11,22 @@ import type { Installation } from '../../../../common';
import { reinstallPackageForInstallation } from './reinstall';
import { installPackage } from './install';
import { getBundledPackageForInstallation } from './bundled_packages';
jest.mock('./install');
jest.mock('./bundled_packages');
const mockedInstallPackage = installPackage as jest.MockedFunction<typeof installPackage>;
const mockedInstallPackage = jest.mocked(installPackage);
const mockedGetBundledPackageForInstallation = jest.mocked(getBundledPackageForInstallation);
describe('reinstallPackageForInstallation', () => {
beforeEach(() => {
mockedInstallPackage.mockReset();
mockedGetBundledPackageForInstallation.mockImplementation(async ({ name }) => {
if (name === 'test_bundled') {
return {} as any;
}
});
});
it('should throw an error for uploaded package', async () => {
const soClient = savedObjectsClientMock.create();
@ -67,7 +75,7 @@ describe('reinstallPackageForInstallation', () => {
esClient,
installation: {
install_source: 'bundled',
name: 'test',
name: 'test_bundled',
version: '1.0.0',
} as Installation,
})
@ -76,7 +84,32 @@ describe('reinstallPackageForInstallation', () => {
expect(mockedInstallPackage).toHaveBeenCalledWith(
expect.objectContaining({
installSource: 'registry',
pkgkey: 'test-1.0.0',
pkgkey: 'test_bundled-1.0.0',
force: true,
})
);
});
// Pre 8.12.0 bundled package install where saved with install_source_upload
it('should install bundled package saved with install_source:upload ', async () => {
const soClient = savedObjectsClientMock.create();
const esClient = elasticsearchServiceMock.createInternalClient();
await expect(
reinstallPackageForInstallation({
soClient,
esClient,
installation: {
install_source: 'upload',
name: 'test_bundled',
version: '1.0.0',
} as Installation,
})
);
expect(mockedInstallPackage).toHaveBeenCalledWith(
expect.objectContaining({
installSource: 'registry',
pkgkey: 'test_bundled-1.0.0',
force: true,
})
);

View file

@ -11,6 +11,8 @@ import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants';
import type { Installation } from '../../../types';
import { pkgToPkgKey } from '../registry';
import { getBundledPackageForInstallation } from './bundled_packages';
import { installPackage } from './install';
export async function reinstallPackageForInstallation({
@ -22,9 +24,18 @@ export async function reinstallPackageForInstallation({
esClient: ElasticsearchClient;
installation: Installation;
}) {
if (installation.install_source === 'upload') {
throw new Error('Cannot reinstall an uploaded package');
if (installation.install_source === 'upload' || installation.install_source === 'bundled') {
// If there is a matching bundled package
const matchingBundledPackage = await getBundledPackageForInstallation(installation);
if (!matchingBundledPackage) {
if (installation.install_source === 'bundled') {
throw new Error(`Cannot reinstall: ${installation.name}, bundled package not found`);
} else {
throw new Error('Cannot reinstall an uploaded package');
}
}
}
return installPackage({
// If the package is bundled reinstall from the registry will still use the bundled package.
installSource: 'registry',

View file

@ -14,11 +14,7 @@ import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants';
import { AUTO_UPDATE_PACKAGES } from '../../common/constants';
import type { PreconfigurationError } from '../../common/constants';
import type {
DefaultPackagesInstallationError,
BundledPackage,
Installation,
} from '../../common/types';
import type { DefaultPackagesInstallationError } from '../../common/types';
import { SO_SEARCH_LIMIT } from '../constants';
@ -45,7 +41,6 @@ import { getInstallations, reinstallPackageForInstallation } from './epm/package
import { isPackageInstalled } from './epm/packages/install';
import type { UpgradeManagedPackagePoliciesResult } from './managed_package_policies';
import { upgradeManagedPackagePolicies } from './managed_package_policies';
import { getBundledPackages } from './epm/packages';
import { upgradePackageInstallVersion } from './setup/upgrade_package_install_version';
import { upgradeAgentPolicySchemaVersion } from './setup/upgrade_agent_policy_schema_version';
import { migrateSettingsToFleetServerHost } from './fleet_server_host';
@ -219,24 +214,9 @@ export async function ensureFleetGlobalEsAssets(
if (assetResults.some((asset) => asset.isCreated)) {
// Update existing index template
const installedPackages = await getInstallations(soClient);
const bundledPackages = await getBundledPackages();
const findMatchingBundledPkg = (pkg: Installation) =>
bundledPackages.find(
(bundledPkg: BundledPackage) =>
bundledPkg.name === pkg.name && bundledPkg.version === pkg.version
);
await pMap(
installedPackages.saved_objects,
async ({ attributes: installation }) => {
if (installation.install_source !== 'registry') {
const matchingBundledPackage = findMatchingBundledPkg(installation);
if (!matchingBundledPackage) {
logger.error(
`Package needs to be manually reinstalled ${installation.name} after installing Fleet global assets`
);
return;
}
}
await reinstallPackageForInstallation({
soClient,
esClient,

View file

@ -0,0 +1,113 @@
/*
* 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 {
elasticsearchServiceMock,
loggingSystemMock,
savedObjectsClientMock,
} from '@kbn/core/server/mocks';
import { reinstallPackageForInstallation } from '../epm/packages';
import { upgradePackageInstallVersion } from './upgrade_package_install_version';
jest.mock('../epm/packages');
const mockedReinstallPackageForInstallation = jest.mocked(reinstallPackageForInstallation);
describe('upgradePackageInstallVersion', () => {
beforeEach(() => {
mockedReinstallPackageForInstallation.mockReset();
mockedReinstallPackageForInstallation.mockResolvedValue({} as any);
});
it('should upgrade outdated package version', async () => {
const logger = loggingSystemMock.createLogger();
const esClient = elasticsearchServiceMock.createInternalClient();
const soClient = savedObjectsClientMock.create();
soClient.find.mockResolvedValue({
total: 2,
saved_objects: [
{
attributes: { name: 'test1' },
},
{
attributes: { name: 'test2' },
},
],
} as any);
await upgradePackageInstallVersion({
esClient,
soClient,
logger,
});
expect(mockedReinstallPackageForInstallation).toBeCalledTimes(2);
expect(mockedReinstallPackageForInstallation).toBeCalledWith(
expect.objectContaining({
installation: expect.objectContaining({ name: 'test1' }),
})
);
expect(mockedReinstallPackageForInstallation).toBeCalledWith(
expect.objectContaining({
installation: expect.objectContaining({ name: 'test2' }),
})
);
expect(logger.warn).not.toBeCalled();
expect(logger.error).not.toBeCalled();
});
it('should log at error level when an error happens while reinstalling package', async () => {
const logger = loggingSystemMock.createLogger();
const esClient = elasticsearchServiceMock.createInternalClient();
const soClient = savedObjectsClientMock.create();
mockedReinstallPackageForInstallation.mockRejectedValue(new Error('test error'));
soClient.find.mockResolvedValue({
total: 2,
saved_objects: [
{
attributes: { name: 'test1' },
},
],
} as any);
await upgradePackageInstallVersion({
esClient,
soClient,
logger,
});
expect(logger.error).toBeCalled();
});
it('should log a warn level when an error happens while reinstalling an uploaded package', async () => {
const logger = loggingSystemMock.createLogger();
const esClient = elasticsearchServiceMock.createInternalClient();
const soClient = savedObjectsClientMock.create();
mockedReinstallPackageForInstallation.mockRejectedValue(new Error('test error'));
soClient.find.mockResolvedValue({
total: 2,
saved_objects: [
{
attributes: { name: 'test1', install_source: 'upload' },
},
],
} as any);
await upgradePackageInstallVersion({
esClient,
soClient,
logger,
});
expect(logger.warn).toBeCalled();
});
});

View file

@ -35,7 +35,6 @@ export async function upgradePackageInstallVersion({
logger: Logger;
}) {
const res = await findOutdatedInstallations(soClient);
if (res.total === 0) {
return;
}
@ -44,18 +43,20 @@ export async function upgradePackageInstallVersion({
res.saved_objects,
({ attributes: installation }) => {
// Uploaded package cannot be reinstalled
if (installation.install_source === 'upload') {
logger.warn(`Uploaded package needs to be manually reinstalled ${installation.name}.`);
return;
}
return reinstallPackageForInstallation({
soClient,
esClient,
installation,
}).catch((err: Error) => {
logger.error(
`Package needs to be manually reinstalled ${installation.name} updating install_version failed. ${err.message}`
);
if (installation.install_source === 'upload') {
logger.warn(
`Uploaded package needs to be manually reinstalled ${installation.name}. ${err.message}`
);
} else {
logger.error(
`Package needs to be manually reinstalled ${installation.name} updating install_version failed. ${err.message}`
);
}
});
},
{ concurrency: 10 }