diff --git a/x-pack/plugins/fleet/common/openapi/bundled.json b/x-pack/plugins/fleet/common/openapi/bundled.json index 0be8b335ed549..432e72db05e8c 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.json +++ b/x-pack/plugins/fleet/common/openapi/bundled.json @@ -646,6 +646,9 @@ "properties": { "force": { "type": "boolean" + }, + "ignore_constraints": { + "type": "boolean" } } } diff --git a/x-pack/plugins/fleet/common/openapi/bundled.yaml b/x-pack/plugins/fleet/common/openapi/bundled.yaml index 0659352deb1d9..439f56da63e5e 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.yaml +++ b/x-pack/plugins/fleet/common/openapi/bundled.yaml @@ -396,6 +396,8 @@ paths: properties: force: type: boolean + ignore_constraints: + type: boolean put: summary: Packages - Update tags: [] diff --git a/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkg_name}@{pkg_version}.yaml b/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkg_name}@{pkg_version}.yaml index 401237008626b..ef0964b66e045 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkg_name}@{pkg_version}.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkg_name}@{pkg_version}.yaml @@ -78,6 +78,8 @@ post: properties: force: type: boolean + ignore_constraints: + type: boolean put: summary: Packages - Update tags: [] diff --git a/x-pack/plugins/fleet/server/routes/epm/handlers.ts b/x-pack/plugins/fleet/server/routes/epm/handlers.ts index 16f2d2e13e18c..9bfcffa04bf35 100644 --- a/x-pack/plugins/fleet/server/routes/epm/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/epm/handlers.ts @@ -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 = { diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.test.ts index 76e01ed8b2f27..53b4d341beec2 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.test.ts @@ -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; @@ -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', + }); + }); + }); }); }); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts index a7cbea4d6462a..c78f107cce715 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts @@ -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'; @@ -145,17 +145,17 @@ export async function getPackageInfo(options: { const { savedObjectsClient, pkgName, pkgVersion } = options; const [savedObject, latestPackage] = await Promise.all([ getInstallationObject({ savedObjectsClient, pkgName }), - Registry.fetchFindLatestPackage(pkgName), + Registry.fetchFindLatestPackage(pkgName, { throwIfNotFound: false }), ]); - // 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; + 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, pkgVersion: responsePkgVersion, @@ -166,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, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index 21f0ae25d6faf..9ffae48cb02d8 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -205,6 +205,7 @@ interface InstallRegistryPackageParams { esClient: ElasticsearchClient; spaceId: string; force?: boolean; + ignoreConstraints?: boolean; } function getTelemetryEvent(pkgName: string, pkgVersion: string): PackageUpdateEvent { @@ -233,6 +234,7 @@ async function installPackageFromRegistry({ esClient, spaceId, force = false, + ignoreConstraints = false, }: InstallRegistryPackageParams): Promise { const logger = appContextService.getLogger(); // TODO: change epm API to /packageName/version so we don't need to do this @@ -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 = @@ -469,7 +471,7 @@ 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, @@ -477,6 +479,7 @@ export async function installPackage(args: InstallPackageParams) { esClient, spaceId, force, + ignoreConstraints, }); return response; } else if (args.installSource === 'upload') { diff --git a/x-pack/plugins/fleet/server/services/epm/registry/index.ts b/x-pack/plugins/fleet/server/services/epm/registry/index.ts index 5996ce5404b70..12712905b1d36 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/index.ts @@ -65,18 +65,33 @@ export async function fetchList(params?: SearchParams): Promise { +// When `throwIfNotFound` is true or undefined, return type will never be undefined. +export async function fetchFindLatestPackage( + packageName: string, + options?: { ignoreConstraints?: boolean; throwIfNotFound?: true } +): Promise; +export async function fetchFindLatestPackage( + packageName: string, + options: { ignoreConstraints?: boolean; throwIfNotFound: false } +): Promise; +export async function fetchFindLatestPackage( + packageName: string, + options?: { ignoreConstraints?: boolean; throwIfNotFound?: boolean } +): Promise { + 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`); } } diff --git a/x-pack/plugins/fleet/server/types/rest_spec/epm.ts b/x-pack/plugins/fleet/server/types/rest_spec/epm.ts index 390d5dea792cb..c51a0127c2e29 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/epm.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/epm.ts @@ -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 }), }) ), }; diff --git a/x-pack/test/fleet_api_integration/apis/epm/setup.ts b/x-pack/test/fleet_api_integration/apis/epm/setup.ts index 44e582b445f96..eb29920b83036 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/setup.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/setup.ts @@ -52,6 +52,40 @@ export default function (providerContext: FtrProviderContext) { }); }); + it('does not fail when package is no longer compatible in registry', async () => { + await supertest + .post(`/api/fleet/epm/packages/deprecated/0.1.0`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true, ignore_constraints: true }) + .expect(200); + + const agentPolicyResponse = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'deprecated-ap-1', + namespace: 'default', + monitoring_enabled: [], + }) + .expect(200); + + await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'deprecated-1', + policy_id: agentPolicyResponse.body.item.id, + package: { + name: 'deprecated', + version: '0.1.0', + }, + inputs: [], + }) + .expect(200); + + await supertest.post('/api/fleet/setup').set('kbn-xsrf', 'xxxx').expect(200); + }); + it('allows elastic/fleet-server user to call required APIs', async () => { const { token, diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/deprecated/0.1.0/data_stream/test/fields/fields.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/deprecated/0.1.0/data_stream/test/fields/fields.yml new file mode 100644 index 0000000000000..6e003ed0ad147 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/deprecated/0.1.0/data_stream/test/fields/fields.yml @@ -0,0 +1,16 @@ +- name: data_stream.type + type: constant_keyword + description: > + Data stream type. +- name: data_stream.dataset + type: constant_keyword + description: > + Data stream dataset. +- name: data_stream.namespace + type: constant_keyword + description: > + Data stream namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/deprecated/0.1.0/data_stream/test/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/deprecated/0.1.0/data_stream/test/manifest.yml new file mode 100644 index 0000000000000..9ac3c68a0be9e --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/deprecated/0.1.0/data_stream/test/manifest.yml @@ -0,0 +1,9 @@ +title: Test Dataset + +type: logs + +elasticsearch: + index_template.mappings: + dynamic: false + index_template.settings: + index.lifecycle.name: reference diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/deprecated/0.1.0/docs/README.md b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/deprecated/0.1.0/docs/README.md new file mode 100644 index 0000000000000..13ef3f4fa9152 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/deprecated/0.1.0/docs/README.md @@ -0,0 +1,3 @@ +# Test package + +This is a test package for testing installing or updating to an out-of-date package \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/deprecated/0.1.0/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/deprecated/0.1.0/manifest.yml new file mode 100644 index 0000000000000..755c49e1af388 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/deprecated/0.1.0/manifest.yml @@ -0,0 +1,16 @@ +format_version: 1.0.0 +name: deprecated +title: Package install/update test +description: This is a package for testing deprecated packages +version: 0.1.0 +categories: [] +release: beta +type: integration +license: basic + +conditions: + # Version number is not compatible with current version + elasticsearch: + version: '^1.0.0' + kibana: + version: '^1.0.0'