Skip to content

Commit

Permalink
[8.1] [Fleet] Avoid breaking setup when compatible package is not ava…
Browse files Browse the repository at this point in the history
…ilable in registry (#125525) (#125671)

* [Fleet] Avoid breaking setup when compatible package is not available in registry (#125525)

(cherry picked from commit 928638e)

* [Fleet] Get package info should not store the whole package (#123509)

Co-authored-by: Josh Dover <1813008+joshdover@users.noreply.github.com>
Co-authored-by: Nicolas Chaulet <nicolas.chaulet@elastic.co>
  • Loading branch information
3 people authored Feb 16, 2022
1 parent 4cbc2da commit 1a3ea1b
Show file tree
Hide file tree
Showing 19 changed files with 205 additions and 744 deletions.
3 changes: 3 additions & 0 deletions x-pack/plugins/fleet/common/openapi/bundled.json
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,9 @@
"properties": {
"force": {
"type": "boolean"
},
"ignore_constraints": {
"type": "boolean"
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/fleet/common/openapi/bundled.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,8 @@ paths:
properties:
force:
type: boolean
ignore_constraints:
type: boolean
put:
summary: Packages - Update
tags: []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ post:
properties:
force:
type: boolean
ignore_constraints:
type: boolean
put:
summary: Packages - Update
tags: []
Expand Down
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,
getPackageInfo,
getPackageInfoFromRegistry,
isBulkInstallError,
installPackage,
removeInstallation,
Expand Down Expand Up @@ -199,7 +199,7 @@ export const getInfoHandler: FleetRequestHandler<
if (pkgVersion && !semverValid(pkgVersion)) {
throw new IngestManagerError('Package version is not a valid semver');
}
const res = await getPackageInfo({
const res = await getPackageInfoFromRegistry({
savedObjectsClient,
pkgName,
pkgVersion: pkgVersion || '',
Expand Down Expand Up @@ -265,6 +265,7 @@ export const installPackageFromRegistryHandler: FleetRequestHandler<
esClient,
spaceId,
force: request.body?.force,
ignoreConstraints: request.body?.ignore_constraints,
});
if (!res.error) {
const body: InstallPackageResponse = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ jest.mock(
jest.mock('../../services/epm/packages', () => {
return {
ensureInstalledPackage: jest.fn(() => Promise.resolve()),
getPackageInfo: jest.fn(() => Promise.resolve()),
getPackageInfoFromRegistry: jest.fn(() => Promise.resolve()),
};
});

Expand Down
42 changes: 42 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 @@ -19,6 +19,8 @@ import * as Registry from '../registry';
import { createAppContextStartContractMock } from '../../../mocks';
import { appContextService } from '../../app_context';

import { PackageNotFoundError } from '../../../errors';

import { getPackageInfo, getPackageUsageStats } from './get';

const MockRegistry = Registry as jest.Mocked<typeof Registry>;
Expand Down Expand Up @@ -279,5 +281,45 @@ describe('When using EPM `get` services', () => {
});
});
});

describe('registry fetch errors', () => {
it('throws when a package that is not installed is not available in the registry', async () => {
MockRegistry.fetchFindLatestPackage.mockResolvedValue(undefined);
const soClient = savedObjectsClientMock.create();
soClient.get.mockRejectedValue(SavedObjectsErrorHelpers.createGenericNotFoundError());

await expect(
getPackageInfo({
savedObjectsClient: soClient,
pkgName: 'my-package',
pkgVersion: '1.0.0',
})
).rejects.toThrowError(PackageNotFoundError);
});

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

await expect(
getPackageInfo({
savedObjectsClient: soClient,
pkgName: 'my-package',
pkgVersion: '1.0.0',
})
).resolves.toMatchObject({
latestVersion: '1.0.0',
status: 'installed',
});
});
});
});
});
47 changes: 43 additions & 4 deletions x-pack/plugins/fleet/server/services/epm/packages/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import type {
GetCategoriesRequest,
} from '../../../../common/types';
import type { Installation, PackageInfo } from '../../../types';
import { IngestManagerError } from '../../../errors';
import { IngestManagerError, PackageNotFoundError } from '../../../errors';
import { appContextService } from '../../';
import * as Registry from '../registry';
import { getEsPackage } from '../archive/storage';
Expand Down Expand Up @@ -98,7 +98,7 @@ export async function getPackageSavedObjects(

export const getInstallations = getPackageSavedObjects;

export async function getPackageInfo(options: {
export async function getPackageInfoFromRegistry(options: {
savedObjectsClient: SavedObjectsClientContract;
pkgName: string;
pkgVersion: string;
Expand All @@ -111,11 +111,50 @@ export async function getPackageInfo(options: {

// 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: {
savedObjectsClient: SavedObjectsClientContract;
pkgName: string;
pkgVersion: string;
}): Promise<PackageInfo> {
const { savedObjectsClient, pkgName, pkgVersion } = options;
const [savedObject, latestPackage] = await Promise.all([
getInstallationObject({ savedObjectsClient, pkgName }),
Registry.fetchFindLatestPackage(pkgName, { throwIfNotFound: false }),
]);

if (!savedObject && !latestPackage) {
throw new PackageNotFoundError(`[${pkgName}] package not installed or found in registry`);
}

// 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,
Expand All @@ -127,7 +166,7 @@ export async function getPackageInfo(options: {

// add properties that aren't (or aren't yet) on the package
const additions: EpmPackageAdditions = {
latestVersion: latestPackage.version,
latestVersion: latestPackage?.version ?? responsePkgVersion,
title: packageInfo.title || nameAsTitle(packageInfo.name),
assets: Registry.groupPathsByService(paths || []),
removable: true,
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/server/services/epm/packages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export {
getInstallation,
getInstallations,
getPackageInfo,
getPackageInfoFromRegistry,
getPackages,
getLimitedPackages,
} from './get';
Expand Down
7 changes: 5 additions & 2 deletions x-pack/plugins/fleet/server/services/epm/packages/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ interface InstallRegistryPackageParams {
esClient: ElasticsearchClient;
spaceId: string;
force?: boolean;
ignoreConstraints?: boolean;
}

function getTelemetryEvent(pkgName: string, pkgVersion: string): PackageUpdateEvent {
Expand Down Expand Up @@ -233,6 +234,7 @@ async function installPackageFromRegistry({
esClient,
spaceId,
force = false,
ignoreConstraints = false,
}: InstallRegistryPackageParams): Promise<InstallResult> {
const logger = appContextService.getLogger();
// TODO: change epm API to /packageName/version so we don't need to do this
Expand All @@ -249,7 +251,7 @@ async function installPackageFromRegistry({
installType = getInstallType({ pkgVersion, installedPkg });

// get latest package version
const latestPackage = await Registry.fetchFindLatestPackage(pkgName);
const latestPackage = await Registry.fetchFindLatestPackage(pkgName, { ignoreConstraints });

// let the user install if using the force flag or needing to reinstall or install a previous version due to failed update
const installOutOfDateVersionOk =
Expand Down Expand Up @@ -469,14 +471,15 @@ export async function installPackage(args: InstallPackageParams) {
const { savedObjectsClient, esClient } = args;

if (args.installSource === 'registry') {
const { pkgkey, force, spaceId } = args;
const { pkgkey, force, ignoreConstraints, spaceId } = args;
logger.debug(`kicking off install of ${pkgkey} from registry`);
const response = installPackageFromRegistry({
savedObjectsClient,
pkgkey,
esClient,
spaceId,
force,
ignoreConstraints,
});
return response;
} else if (args.installSource === 'upload') {
Expand Down
23 changes: 19 additions & 4 deletions x-pack/plugins/fleet/server/services/epm/registry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,33 @@ export async function fetchList(params?: SearchParams): Promise<RegistrySearchRe
return fetchUrl(url.toString()).then(JSON.parse);
}

export async function fetchFindLatestPackage(packageName: string): Promise<RegistrySearchResult> {
// When `throwIfNotFound` is true or undefined, return type will never be undefined.
export async function fetchFindLatestPackage(
packageName: string,
options?: { ignoreConstraints?: boolean; throwIfNotFound?: true }
): Promise<RegistrySearchResult>;
export async function fetchFindLatestPackage(
packageName: string,
options: { ignoreConstraints?: boolean; throwIfNotFound: false }
): Promise<RegistrySearchResult | undefined>;
export async function fetchFindLatestPackage(
packageName: string,
options?: { ignoreConstraints?: boolean; throwIfNotFound?: boolean }
): Promise<RegistrySearchResult | undefined> {
const { ignoreConstraints = false, throwIfNotFound = true } = options ?? {};
const registryUrl = getRegistryUrl();
const url = new URL(`${registryUrl}/search?package=${packageName}&experimental=true`);

setKibanaVersion(url);
if (!ignoreConstraints) {
setKibanaVersion(url);
}

const res = await fetchUrl(url.toString());
const searchResults = JSON.parse(res);
if (searchResults.length) {
return searchResults[0];
} else {
throw new PackageNotFoundError(`${packageName} not found`);
} else if (throwIfNotFound) {
throw new PackageNotFoundError(`[${packageName}] package not found in registry`);
}
}

Expand Down
3 changes: 2 additions & 1 deletion x-pack/plugins/fleet/server/types/rest_spec/epm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ export const InstallPackageFromRegistryRequestSchema = {
}),
body: schema.nullable(
schema.object({
force: schema.boolean(),
force: schema.boolean({ defaultValue: false }),
ignore_constraints: schema.boolean({ defaultValue: false }),
})
),
};
Expand Down
Loading

0 comments on commit 1a3ea1b

Please sign in to comment.