diff --git a/x-pack/plugins/fleet/server/routes/epm/handlers.ts b/x-pack/plugins/fleet/server/routes/epm/handlers.ts index 38bacfc690194..c5ac0a6c8202e 100644 --- a/x-pack/plugins/fleet/server/routes/epm/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/epm/handlers.ts @@ -43,7 +43,7 @@ import { getCategories, getPackages, getFile, - getPackageInfoFromRegistry, + getPackageInfo, isBulkInstallError, installPackage, removeInstallation, @@ -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, diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts index 2f814a0da15d4..222408c6e0524 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts @@ -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()), }; }); 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 040bb79b9fc78..6c426c4efdf4f 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 @@ -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', @@ -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(); + }); }); }); }); 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 de4ce6a1e84b5..0ba42585c1601 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts @@ -6,6 +6,7 @@ */ import type { SavedObjectsClientContract, SavedObjectsFindOptions } from '@kbn/core/server'; +import semverGte from 'semver/functions/gte'; import { isPackageLimited, @@ -98,52 +99,18 @@ export async function getPackageSavedObjects( export const getInstallations = getPackageSavedObjects; -export async function getPackageInfoFromRegistry(options: { - savedObjectsClient: SavedObjectsClientContract; - pkgName: string; - pkgVersion: string; -}): Promise { - 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 { - const { savedObjectsClient, pkgName, pkgVersion } = options; - const [savedObject, latestPackage] = await Promise.all([ getInstallationObject({ savedObjectsClient, pkgName }), Registry.fetchFindLatestPackageOrUndefined(pkgName), @@ -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, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/index.ts b/x-pack/plugins/fleet/server/services/epm/packages/index.ts index 30d2b5ec3fd22..bfb09abcfaa28 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/index.ts @@ -20,7 +20,6 @@ export { getInstallation, getInstallations, getPackageInfo, - getPackageInfoFromRegistry, getPackages, getLimitedPackages, } from './get'; diff --git a/x-pack/plugins/graph/public/components/search_bar.test.tsx b/x-pack/plugins/graph/public/components/search_bar.test.tsx index cbb7896647712..c05da957599c9 100644 --- a/x-pack/plugins/graph/public/components/search_bar.test.tsx +++ b/x-pack/plugins/graph/public/components/search_bar.test.tsx @@ -19,7 +19,8 @@ import { import { act } from 'react-dom/test-utils'; import { QueryStringInput } from '@kbn/unified-search-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; - +import { setAutocomplete } from '@kbn/unified-search-plugin/public/services'; +import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { I18nProvider, InjectedIntl } from '@kbn/i18n-react'; @@ -103,6 +104,11 @@ describe('search_bar', () => { }, }; + beforeEach(() => { + const autocompleteStart = unifiedSearchPluginMock.createStartContract(); + setAutocomplete(autocompleteStart.autocomplete); + }); + beforeEach(() => { store = createMockGraphStore({ sagas: [submitSearchSaga], diff --git a/x-pack/test/fleet_api_integration/apis/epm/get.ts b/x-pack/test/fleet_api_integration/apis/epm/get.ts index 891bf7019eda3..a030a988656b9 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/get.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/get.ts @@ -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); }); diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_9999.0.0.zip b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_9999.0.0.zip new file mode 100644 index 0000000000000..67a7e09bfec89 Binary files /dev/null and b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_9999.0.0.zip differ diff --git a/yarn.lock b/yarn.lock index 9a4e679245f8a..ef9c03d4a2f3f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14747,7 +14747,7 @@ font-awesome@4.7.0: resolved "https://registry.yarnpkg.com/font-awesome/-/font-awesome-4.7.0.tgz#8fa8cf0411a1a31afd07b06d2902bb9fc815a133" integrity sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM= -for-each@^0.3.2, for-each@^0.3.3, for-each@~0.3.3: +for-each@^0.3.3, for-each@~0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== @@ -21998,12 +21998,9 @@ parse-filepath@^1.0.1: path-root "^0.1.1" parse-headers@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/parse-headers/-/parse-headers-2.0.1.tgz#6ae83a7aa25a9d9b700acc28698cd1f1ed7e9536" - integrity sha1-aug6eqJanZtwCswoaYzR8e1+lTY= - dependencies: - for-each "^0.3.2" - trim "0.0.1" + version "2.0.5" + resolved "https://registry.yarnpkg.com/parse-headers/-/parse-headers-2.0.5.tgz#069793f9356a54008571eb7f9761153e6c770da9" + integrity sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA== parse-json@^2.2.0: version "2.2.0" @@ -26410,11 +26407,9 @@ sourcemap-codec@^1.4.1: integrity sha512-1ZooVLYFxC448piVLBbtOxFcXwnymH9oUF8nRd3CuYDVvkRBxRl6pB4Mtas5a4drtL+E8LDgFkQNcgIw6tc8Hg== space-separated-tokens@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.2.tgz#e95ab9d19ae841e200808cd96bc7bd0adbbb3412" - integrity sha512-G3jprCEw+xFEs0ORweLmblJ3XLymGGr6hxZYTYZjIlvDti9vOBUjRQa1Rzjt012aRrocKstHwdNi+F7HguPsEA== - dependencies: - trim "0.0.1" + version "1.1.5" + resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899" + integrity sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA== sparkles@^1.0.0: version "1.0.0"