Skip to content

Commit

Permalink
[Fleet] Fallback to bundled packages on GET package route (elastic#12…
Browse files Browse the repository at this point in the history
  • Loading branch information
joshdover authored Apr 25, 2022
1 parent 780333b commit 63e95ab
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 57 deletions.
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
51 changes: 51 additions & 0 deletions x-pack/plugins/fleet/server/services/epm/packages/get.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ describe('When using EPM `get` services', () => {
beforeEach(() => {
const mockContract = createAppContextStartContractMock();
appContextService.start(mockContract);
jest.clearAllMocks();
MockRegistry.fetchFindLatestPackageOrUndefined.mockResolvedValue({
name: 'my-package',
version: '1.0.0',
Expand Down Expand Up @@ -321,6 +322,56 @@ describe('When using EPM `get` services', () => {
status: 'installed',
});
});

it('sets the latestVersion to installed version when an installed package is newer than package in registry', async () => {
const soClient = savedObjectsClientMock.create();
soClient.get.mockResolvedValue({
id: 'my-package',
type: PACKAGES_SAVED_OBJECT_TYPE,
references: [],
attributes: {
version: '2.0.0',
install_status: 'installed',
},
});

await expect(
getPackageInfo({
savedObjectsClient: soClient,
pkgName: 'my-package',
pkgVersion: '1.0.0',
})
).resolves.toMatchObject({
latestVersion: '1.0.0',
status: 'installed',
});
});
});

describe('skipArchive', () => {
it('avoids loading archive when skipArchive = true', async () => {
const soClient = savedObjectsClientMock.create();
soClient.get.mockRejectedValue(SavedObjectsErrorHelpers.createGenericNotFoundError());
MockRegistry.fetchInfo.mockResolvedValue({
name: 'my-package',
version: '1.0.0',
assets: [],
} as unknown as RegistryPackage);

await expect(
getPackageInfo({
savedObjectsClient: soClient,
pkgName: 'my-package',
pkgVersion: '1.0.0',
skipArchive: true,
})
).resolves.toMatchObject({
latestVersion: '1.0.0',
status: 'not_installed',
});

expect(MockRegistry.getRegistryPackage).not.toHaveBeenCalled();
});
});
});
});
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 '@kbn/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,
}: {
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
1 change: 0 additions & 1 deletion x-pack/plugins/fleet/server/services/epm/packages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export {
getInstallation,
getInstallations,
getPackageInfo,
getPackageInfoFromRegistry,
getPackages,
getLimitedPackages,
} from './get';
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.

0 comments on commit 63e95ab

Please sign in to comment.