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] Fallback to bundled packages on GET package route #129999

Merged
merged 7 commits into from
Apr 25, 2022
Merged
Show file tree
Hide file tree
Changes from 4 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
5 changes: 3 additions & 2 deletions x-pack/plugins/fleet/server/routes/epm/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import {
getCategories,
getPackages,
getFile,
getPackageInfoFromRegistry,
getPackageInfo,
isBulkInstallError,
installPackage,
removeInstallation,
Expand Down Expand Up @@ -199,10 +199,11 @@ export const getInfoHandler: FleetRequestHandler<
if (pkgVersion && !semverValid(pkgVersion)) {
throw new IngestManagerError('Package version is not a valid semver');
}
const res = await getPackageInfoFromRegistry({
const res = await getPackageInfo({
savedObjectsClient,
pkgName,
pkgVersion: pkgVersion || '',
skipArchive: true,
});
const body: GetInfoResponse = {
item: res,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ jest.mock(
jest.mock('../../services/epm/packages', () => {
return {
ensureInstalledPackage: jest.fn(() => Promise.resolve()),
getPackageInfoFromRegistry: jest.fn(() => Promise.resolve()),
getPackageInfo: jest.fn(() => Promise.resolve()),
};
});

Expand Down
92 changes: 39 additions & 53 deletions x-pack/plugins/fleet/server/services/epm/packages/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import type { SavedObjectsClientContract, SavedObjectsFindOptions } from 'src/core/server';
import semverGte from 'semver/functions/gte';

import {
isPackageLimited,
Expand Down Expand Up @@ -98,52 +99,18 @@ export async function getPackageSavedObjects(

export const getInstallations = getPackageSavedObjects;

export async function getPackageInfoFromRegistry(options: {
savedObjectsClient: SavedObjectsClientContract;
pkgName: string;
pkgVersion: string;
}): Promise<PackageInfo> {
const { savedObjectsClient, pkgName, pkgVersion } = options;
const [savedObject, latestPackage] = await Promise.all([
getInstallationObject({ savedObjectsClient, pkgName }),
Registry.fetchFindLatestPackageOrThrow(pkgName),
]);

// If no package version is provided, use the installed version in the response
let responsePkgVersion = pkgVersion || savedObject?.attributes.install_version;
// If no installed version of the given package exists, default to the latest version of the package
if (!responsePkgVersion) {
responsePkgVersion = latestPackage.version;
}
const packageInfo = await Registry.fetchInfo(pkgName, responsePkgVersion);

// Fix the paths
const paths =
packageInfo?.assets?.map((path) =>
path.replace(`/package/${pkgName}/${pkgVersion}`, `${pkgName}-${pkgVersion}`)
) ?? [];

// add properties that aren't (or aren't yet) on the package
const additions: EpmPackageAdditions = {
latestVersion: latestPackage.version,
title: packageInfo.title || nameAsTitle(packageInfo.name),
assets: Registry.groupPathsByService(paths || []),
removable: true,
notice: Registry.getNoticePath(paths || []),
keepPoliciesUpToDate: savedObject?.attributes.keep_policies_up_to_date ?? false,
};
const updated = { ...packageInfo, ...additions };

return createInstallableFrom(updated, savedObject);
}

export async function getPackageInfo(options: {
export async function getPackageInfo({
savedObjectsClient,
pkgName,
pkgVersion,
skipArchive = false,
Copy link
Contributor Author

@joshdover joshdover Apr 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new flag is a pretty ugly hack. I think I'll be able to remove this as part of the work I'm planning for #115032. This is necessary for use cases that depend on the archive having been loaded into the archive cache as a side effect of this function.

}: {
savedObjectsClient: SavedObjectsClientContract;
pkgName: string;
pkgVersion: string;
/** Avoid loading the registry archive into the cache (only use for performance reasons). Defaults to `false` */
skipArchive?: boolean;
}): Promise<PackageInfo> {
const { savedObjectsClient, pkgName, pkgVersion } = options;

const [savedObject, latestPackage] = await Promise.all([
getInstallationObject({ savedObjectsClient, pkgName }),
Registry.fetchFindLatestPackageOrUndefined(pkgName),
Expand All @@ -154,20 +121,39 @@ export async function getPackageInfo(options: {
}

// If no package version is provided, use the installed version in the response, fallback to package from registry
const responsePkgVersion =
pkgVersion ?? savedObject?.attributes.install_version ?? latestPackage!.version;

const getPackageRes = await getPackageFromSource({
pkgName,
pkgVersion: responsePkgVersion,
savedObjectsClient,
installedPkg: savedObject?.attributes,
});
const { paths, packageInfo } = getPackageRes;
const resolvedPkgVersion =
pkgVersion !== ''
? pkgVersion
: savedObject?.attributes.install_version ?? latestPackage!.version;

// If same version is available in registry and skipArchive is true, use the info from the registry (faster),
// otherwise build it from the archive
let paths: string[];
let packageInfo: RegistryPackage | ArchivePackage | undefined = skipArchive
? await Registry.fetchInfo(pkgName, pkgVersion).catch(() => undefined)
: undefined;

if (packageInfo) {
// Fix the paths
paths =
packageInfo.assets?.map((path) =>
path.replace(`/package/${pkgName}/${pkgVersion}`, `${pkgName}-${pkgVersion}`)
) ?? [];
} else {
({ paths, packageInfo } = await getPackageFromSource({
pkgName,
pkgVersion: resolvedPkgVersion,
savedObjectsClient,
installedPkg: savedObject?.attributes,
}));
}

// add properties that aren't (or aren't yet) on the package
const additions: EpmPackageAdditions = {
latestVersion: latestPackage?.version ?? responsePkgVersion,
latestVersion:
latestPackage?.version && semverGte(latestPackage.version, resolvedPkgVersion)
? latestPackage.version
: resolvedPkgVersion,
title: packageInfo.title || nameAsTitle(packageInfo.name),
assets: Registry.groupPathsByService(paths || []),
removable: true,
Expand Down
20 changes: 20 additions & 0 deletions x-pack/test/fleet_api_integration/apis/epm/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,26 @@ export default function (providerContext: FtrProviderContext) {
await uninstallPackage(testPkgName, testPkgVersion);
});

it('returns correct package info from upload if a uploaded version is not in registry', async function () {
const testPkgArchiveZipV9999 = path.join(
path.dirname(__filename),
'../fixtures/direct_upload_packages/apache_9999.0.0.zip'
);
const buf = fs.readFileSync(testPkgArchiveZipV9999);
await supertest
.post(`/api/fleet/epm/packages`)
.set('kbn-xsrf', 'xxxx')
.type('application/zip')
.send(buf)
.expect(200);

const res = await supertest.get(`/api/fleet/epm/packages/apache/9999.0.0`).expect(200);
const packageInfo = res.body.item;
expect(packageInfo.description).to.equal('Apache Uploaded Test Integration');
expect(packageInfo.download).to.equal(undefined);
await uninstallPackage(testPkgName, '9999.0.0');
});

it('returns a 404 for a package that do not exists', async function () {
await supertest.get('/api/fleet/epm/packages/notexists/99.99.99').expect(404);
});
Expand Down
Binary file not shown.