Skip to content

Commit

Permalink
refactor install registry and upload to extract common logic
Browse files Browse the repository at this point in the history
  • Loading branch information
juliaElastic committed Feb 7, 2023
1 parent 8476ee1 commit e589f08
Showing 1 changed file with 124 additions and 100 deletions.
224 changes: 124 additions & 100 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 @@ -295,25 +296,18 @@ async function installPackageFromRegistry({
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');

// 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: false,
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

0 comments on commit e589f08

Please sign in to comment.