mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Fleet] Fix reinstalling bundled package during setup (#171321)
This commit is contained in:
parent
9686d57daa
commit
9626900d5e
8 changed files with 193 additions and 43 deletions
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -282,7 +282,7 @@ describe('install', () => {
|
|||
expect(response.error).toBeUndefined();
|
||||
|
||||
expect(install._installPackage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ installSource: 'upload' })
|
||||
expect.objectContaining({ installSource: 'bundled' })
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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' };
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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 }
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue