Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fleet] refactor install registry and upload to extract common logic #150444

Merged
merged 3 commits into from
Feb 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions x-pack/plugins/fleet/server/services/epm/packages/install.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,50 @@ describe('install', () => {
expect.objectContaining({ installSource: 'upload' })
);
});

it('should fetch latest version if version not provided', async () => {
jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true);
const response = await installPackage({
spaceId: DEFAULT_SPACE_ID,
installSource: 'registry',
pkgkey: 'test_package',
savedObjectsClient: savedObjectsClientMock.create(),
esClient: {} as ElasticsearchClient,
});

expect(response.status).toEqual('installed');

expect(sendTelemetryEvents).toHaveBeenCalledWith(
expect.anything(),
undefined,
expect.objectContaining({
newVersion: '1.3.0',
})
);
});

it('should do nothing if same version is installed', async () => {
jest.spyOn(obj, 'getInstallationObject').mockImplementationOnce(() =>
Promise.resolve({
attributes: {
version: '1.2.0',
install_status: 'installed',
installed_es: [],
installed_kibana: [],
},
} as any)
);
jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true);
const response = await installPackage({
spaceId: DEFAULT_SPACE_ID,
installSource: 'registry',
pkgkey: 'apache-1.2.0',
savedObjectsClient: savedObjectsClientMock.create(),
esClient: {} as ElasticsearchClient,
});

expect(response.status).toEqual('already_installed');
});
});

describe('upload', () => {
Expand Down
226 changes: 125 additions & 101 deletions x-pack/plugins/fleet/server/services/epm/packages/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { FLEET_INSTALL_FORMAT_VERSION } from '../../../constants/fleet_es_assets
import { generateESIndexPatterns } from '../elasticsearch/template/template';

import type {
ArchivePackage,
BulkInstallPackageInfo,
EpmPackageInstallStatus,
EsAssetReference,
Expand Down Expand Up @@ -293,27 +294,20 @@ async function installPackageFromRegistry({
const logger = appContextService.getLogger();
// TODO: change epm API to /packageName/version so we don't need to do this
const { pkgName, pkgVersion: version } = Registry.splitPkgKey(pkgkey);
let pkgVersion = version;

// 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 registry ${pkgName}@${pkgVersion}`, 'package');
let pkgVersion = version ?? '';

// if an error happens during getInstallType, report that we don't know
let installType: InstallType = 'unknown';

const installSource = 'registry';
const telemetryEvent: PackageUpdateEvent = getTelemetryEvent(pkgName, pkgVersion);

try {
// get the currently installed package
const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName });
installType = getInstallType({ pkgVersion, installedPkg });

span?.addLabels({
packageName: pkgName,
packageVersion: pkgVersion,
installType,
});
telemetryEvent.installType = installType;
telemetryEvent.currentVersion = installedPkg?.attributes.version || 'not_installed';

const queryLatest = () =>
Registry.fetchFindLatestPackageOrThrow(pkgName, {
Expand All @@ -340,6 +334,100 @@ async function installPackageFromRegistry({
const installOutOfDateVersionOk =
force || ['reinstall', 'reupdate', 'rollback'].includes(installType);

// if the requested version is out-of-date of the latest package version, check if we allow it
// if we don't allow it, return an error
if (semverLt(pkgVersion, latestPackage.version)) {
if (!installOutOfDateVersionOk) {
throw new PackageOutdatedError(
`${pkgkey} is out-of-date and cannot be installed or updated`
);
}
logger.debug(
`${pkgkey} is out-of-date, installing anyway due to ${
force ? 'force flag' : `install type ${installType}`
}`
);
}

return await installPackageCommon({
pkgName,
pkgVersion,
installSource,
installedPkg,
installType,
savedObjectsClient,
esClient,
spaceId,
force,
packageInfo,
paths,
verificationResult,
});
} catch (e) {
sendEvent({
...telemetryEvent,
errorMessage: e.message,
});
return {
error: e,
installType,
installSource,
};
}
}

async function installPackageCommon(options: {
pkgName: string;
pkgVersion: string;
installSource: 'registry' | 'upload';
installedPkg?: SavedObject<Installation>;
installType: InstallType;
savedObjectsClient: SavedObjectsClientContract;
esClient: ElasticsearchClient;
spaceId: string;
force?: boolean;
packageInfo: ArchivePackage;
paths: string[];
verificationResult?: PackageVerificationResult;
telemetryEvent?: PackageUpdateEvent;
}): Promise<InstallResult> {
const {
pkgName,
pkgVersion,
installSource,
installedPkg,
installType,
savedObjectsClient,
force,
esClient,
spaceId,
packageInfo,
paths,
verificationResult,
} = options;
let { telemetryEvent } = options;
const logger = appContextService.getLogger();

// 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,
});

// 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
Expand All @@ -348,44 +436,26 @@ async function installPackageFromRegistry({
installedPkg?.attributes.install_status === 'installed'
) {
if (!force) {
logger.debug(`${pkgkey} is already installed, skipping installation`);
logger.debug(`${pkgName}-${pkgVersion} is already installed, skipping installation`);
return {
assets: [
...installedPkg.attributes.installed_es,
...installedPkg.attributes.installed_kibana,
],
status: 'already_installed',
installType,
installSource: 'registry',
installSource,
};
}
}

telemetryEvent.installType = installType;
telemetryEvent.currentVersion = installedPkg?.attributes.version || 'not_installed';

// if the requested version is out-of-date of the latest package version, check if we allow it
// if we don't allow it, return an error
if (semverLt(pkgVersion, latestPackage.version)) {
if (!installOutOfDateVersionOk) {
throw new PackageOutdatedError(
`${pkgkey} is out-of-date and cannot be installed or updated`
);
}
logger.debug(
`${pkgkey} is out-of-date, installing anyway due to ${
force ? 'force flag' : `install type ${installType}`
}`
);
}

if (!licenseService.hasAtLeast(packageInfo.license || 'basic')) {
const err = new Error(`Requires ${packageInfo.license} license`);
sendEvent({
...telemetryEvent,
errorMessage: err.message,
});
return { error: err, installType, installSource: 'registry' };
return { error: err, installType, installSource };
}

const savedObjectsImporter = appContextService
Expand Down Expand Up @@ -415,7 +485,7 @@ async function installPackageFromRegistry({
installType,
spaceId,
verificationResult,
installSource: 'registry',
installSource,
})
.then(async (assets) => {
await removeOldAssets({
Expand All @@ -424,10 +494,10 @@ async function installPackageFromRegistry({
currentVersion: packageInfo.version,
});
sendEvent({
...telemetryEvent,
...telemetryEvent!,
status: 'success',
});
return { assets, status: 'installed', installType, installSource: 'registry' };
return { assets, status: 'installed', installType, installSource };
})
.catch(async (err: Error) => {
logger.warn(`Failure to install package [${pkgName}]: [${err.toString()}]`);
Expand All @@ -441,10 +511,10 @@ async function installPackageFromRegistry({
esClient,
});
sendEvent({
...telemetryEvent,
...telemetryEvent!,
errorMessage: err.message,
});
return { error: err, installType, installSource: 'registry' };
return { error: err, installType, installSource };
});
} catch (e) {
sendEvent({
Expand All @@ -454,7 +524,7 @@ async function installPackageFromRegistry({
return {
error: e,
installType,
installSource: 'registry',
installSource,
};
} finally {
span?.end();
Expand All @@ -469,16 +539,12 @@ async function installPackageByUpload({
spaceId,
version,
}: InstallUploadedArchiveParams): Promise<InstallResult> {
// 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 upload`, 'package');

const logger = appContextService.getLogger();
// if an error happens during getInstallType, report that we don't know
let installType: InstallType = 'unknown';
const telemetryEvent: PackageUpdateEvent = getTelemetryEvent('', '');
const installSource = 'upload';
try {
const { packageInfo } = await generatePackageInfoFromArchiveBuffer(archiveBuffer, contentType);
const pkgName = packageInfo.name;

// Allow for overriding the version in the manifest for cases where we install
// stack-aligned bundled packages to support special cases around the
Expand All @@ -487,23 +553,11 @@ async function installPackageByUpload({

const installedPkg = await getInstallationObject({
savedObjectsClient,
pkgName: packageInfo.name,
pkgName,
});

installType = getInstallType({ pkgVersion, installedPkg });

span?.addLabels({
packageName: packageInfo.name,
packageVersion: pkgVersion,
installType,
});

telemetryEvent.packageName = packageInfo.name;
telemetryEvent.newVersion = pkgVersion;
telemetryEvent.installType = installType;
telemetryEvent.currentVersion = installedPkg?.attributes.version || 'not_installed';

const installSource = 'upload';
// as we do not verify uploaded packages, we must invalidate the verification cache
deleteVerificationResult(packageInfo);
const paths = await unpackBufferToCache({
Expand All @@ -519,55 +573,25 @@ async function installPackageByUpload({
packageInfo,
});

const savedObjectsImporter = appContextService
.getSavedObjects()
.createImporter(savedObjectsClient);

const savedObjectTagAssignmentService = appContextService
.getSavedObjectsTagging()
.createInternalAssignmentService({ client: savedObjectsClient });

const savedObjectTagClient = appContextService
.getSavedObjectsTagging()
.createTagClient({ client: savedObjectsClient });

// @ts-expect-error status is string instead of InstallResult.status 'installed' | 'already_installed'
return await _installPackage({
return await installPackageCommon({
pkgName,
pkgVersion,
installSource,
installedPkg,
installType,
savedObjectsClient,
savedObjectsImporter,
savedObjectTagAssignmentService,
savedObjectTagClient,
esClient,
logger,
installedPkg,
spaceId,
force: true, // upload has implicit force
packageInfo,
paths,
packageInfo: { ...packageInfo, version: pkgVersion },
});
} catch (e) {
return {
error: e,
installType,
installSource,
spaceId,
})
.then((assets) => {
sendEvent({
...telemetryEvent,
status: 'success',
});
return { assets, status: 'installed', installType };
})
.catch(async (err: Error) => {
sendEvent({
...telemetryEvent,
errorMessage: err.message,
});
return { error: err, installType };
});
} catch (e) {
sendEvent({
...telemetryEvent,
errorMessage: e.message,
});
return { error: e, installType, installSource: 'upload' };
} finally {
span?.end();
};
}
}

Expand Down