From 311805a57d8109832648db6e3b7fe4143e18e030 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Tue, 22 Sep 2020 12:50:44 -0400 Subject: [PATCH] [Ingest Manager] Adding bulk packages upgrade api (#77827) * Adding bulk upgrade api * Addressing comments * Removing todo * Changing body field * Adding helper for getting the bulk install route * Adding request spec * Pulling in Johns changes * Removing test for same package upgraded multiple times * Pulling in John's error handling changes * Fixing type error --- .../ingest_manager/common/constants/routes.ts | 2 + .../ingest_manager/common/services/routes.ts | 4 + .../common/types/rest_spec/epm.ts | 24 +++ .../ingest_manager/server/errors/handlers.ts | 29 +-- .../ingest_manager/server/errors/index.ts | 2 +- .../server/routes/epm/handlers.ts | 62 +++--- .../ingest_manager/server/routes/epm/index.ts | 11 + .../server/services/epm/packages/install.ts | 188 +++++++++++++++++- .../server/types/rest_spec/epm.ts | 6 + .../apis/epm/bulk_upgrade.ts | 113 +++++++++++ .../apis/epm/index.js | 1 + 11 files changed, 394 insertions(+), 48 deletions(-) create mode 100644 x-pack/test/ingest_manager_api_integration/apis/epm/bulk_upgrade.ts diff --git a/x-pack/plugins/ingest_manager/common/constants/routes.ts b/x-pack/plugins/ingest_manager/common/constants/routes.ts index 3e065142ea101..378a6c6c12159 100644 --- a/x-pack/plugins/ingest_manager/common/constants/routes.ts +++ b/x-pack/plugins/ingest_manager/common/constants/routes.ts @@ -15,9 +15,11 @@ export const LIMITED_CONCURRENCY_ROUTE_TAG = 'ingest:limited-concurrency'; // EPM API routes const EPM_PACKAGES_MANY = `${EPM_API_ROOT}/packages`; +const EPM_PACKAGES_BULK = `${EPM_PACKAGES_MANY}/_bulk`; const EPM_PACKAGES_ONE = `${EPM_PACKAGES_MANY}/{pkgkey}`; const EPM_PACKAGES_FILE = `${EPM_PACKAGES_MANY}/{pkgName}/{pkgVersion}`; export const EPM_API_ROUTES = { + BULK_INSTALL_PATTERN: EPM_PACKAGES_BULK, LIST_PATTERN: EPM_PACKAGES_MANY, LIMITED_LIST_PATTERN: `${EPM_PACKAGES_MANY}/limited`, INFO_PATTERN: EPM_PACKAGES_ONE, diff --git a/x-pack/plugins/ingest_manager/common/services/routes.ts b/x-pack/plugins/ingest_manager/common/services/routes.ts index b7521f95b4f83..ec7c0ee850834 100644 --- a/x-pack/plugins/ingest_manager/common/services/routes.ts +++ b/x-pack/plugins/ingest_manager/common/services/routes.ts @@ -46,6 +46,10 @@ export const epmRouteService = { ); // trim trailing slash }, + getBulkInstallPath: () => { + return EPM_API_ROUTES.BULK_INSTALL_PATTERN; + }, + getRemovePath: (pkgkey: string) => { return EPM_API_ROUTES.DELETE_PATTERN.replace('{pkgkey}', pkgkey).replace(/\/$/, ''); // trim trailing slash }, diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts index 54e767fee4b22..7ed2fed91aa93 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts @@ -71,6 +71,30 @@ export interface InstallPackageResponse { response: AssetReference[]; } +export interface IBulkInstallPackageError { + name: string; + statusCode: number; + error: string | Error; +} + +export interface BulkInstallPackageInfo { + name: string; + newVersion: string; + // this will be null if no package was present before the upgrade (aka it was an install) + oldVersion: string | null; + assets: AssetReference[]; +} + +export interface BulkInstallPackagesResponse { + response: Array; +} + +export interface BulkInstallPackagesRequest { + body: { + packages: string[]; + }; +} + export interface MessageResponse { response: string; } diff --git a/x-pack/plugins/ingest_manager/server/errors/handlers.ts b/x-pack/plugins/ingest_manager/server/errors/handlers.ts index 9f776565cf262..b621f2dd29331 100644 --- a/x-pack/plugins/ingest_manager/server/errors/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/errors/handlers.ts @@ -56,10 +56,7 @@ const getHTTPResponseCode = (error: IngestManagerError): number => { return 400; // Bad Request }; -export const defaultIngestErrorHandler: IngestErrorHandler = async ({ - error, - response, -}: IngestErrorHandlerParams): Promise => { +export function ingestErrorToResponseOptions(error: IngestErrorHandlerParams['error']) { const logger = appContextService.getLogger(); if (isLegacyESClientError(error)) { // there was a problem communicating with ES (e.g. via `callCluster`) @@ -72,36 +69,44 @@ export const defaultIngestErrorHandler: IngestErrorHandler = async ({ logger.error(message); - return response.customError({ + return { statusCode: error?.statusCode || error.status, body: { message }, - }); + }; } // our "expected" errors if (error instanceof IngestManagerError) { // only log the message logger.error(error.message); - return response.customError({ + return { statusCode: getHTTPResponseCode(error), body: { message: error.message }, - }); + }; } // handle any older Boom-based errors or the few places our app uses them if (isBoom(error)) { // only log the message logger.error(error.output.payload.message); - return response.customError({ + return { statusCode: error.output.statusCode, body: { message: error.output.payload.message }, - }); + }; } // not sure what type of error this is. log as much as possible logger.error(error); - return response.customError({ + return { statusCode: 500, body: { message: error.message }, - }); + }; +} + +export const defaultIngestErrorHandler: IngestErrorHandler = async ({ + error, + response, +}: IngestErrorHandlerParams): Promise => { + const options = ingestErrorToResponseOptions(error); + return response.customError(options); }; diff --git a/x-pack/plugins/ingest_manager/server/errors/index.ts b/x-pack/plugins/ingest_manager/server/errors/index.ts index 5e36a2ec9a884..f495bf551dcff 100644 --- a/x-pack/plugins/ingest_manager/server/errors/index.ts +++ b/x-pack/plugins/ingest_manager/server/errors/index.ts @@ -5,7 +5,7 @@ */ /* eslint-disable max-classes-per-file */ -export { defaultIngestErrorHandler } from './handlers'; +export { defaultIngestErrorHandler, ingestErrorToResponseOptions } from './handlers'; export class IngestManagerError extends Error { constructor(message?: string) { diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts index c40e0e4ac5c0b..7ae896c1f30a6 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts @@ -5,7 +5,6 @@ */ import { TypeOf } from '@kbn/config-schema'; import { RequestHandler, CustomHttpResponseOptions } from 'src/core/server'; -import { appContextService } from '../../services'; import { GetInfoResponse, InstallPackageResponse, @@ -14,6 +13,7 @@ import { GetCategoriesResponse, GetPackagesResponse, GetLimitedPackagesResponse, + BulkInstallPackagesResponse, } from '../../../common'; import { GetCategoriesRequestSchema, @@ -23,6 +23,7 @@ import { InstallPackageFromRegistryRequestSchema, InstallPackageByUploadRequestSchema, DeletePackageRequestSchema, + BulkUpgradePackagesFromRegistryRequestSchema, } from '../../types'; import { getCategories, @@ -34,9 +35,12 @@ import { getLimitedPackages, getInstallationObject, } from '../../services/epm/packages'; -import { IngestManagerError, defaultIngestErrorHandler } from '../../errors'; +import { defaultIngestErrorHandler } from '../../errors'; import { splitPkgKey } from '../../services/epm/registry'; -import { getInstallType } from '../../services/epm/packages/install'; +import { + handleInstallPackageFailure, + bulkInstallPackages, +} from '../../services/epm/packages/install'; export const getCategoriesHandler: RequestHandler< undefined, @@ -136,13 +140,11 @@ export const installPackageFromRegistryHandler: RequestHandler< undefined, TypeOf > = async (context, request, response) => { - const logger = appContextService.getLogger(); const savedObjectsClient = context.core.savedObjects.client; const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; const { pkgkey } = request.params; const { pkgName, pkgVersion } = splitPkgKey(pkgkey); const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); - const installType = getInstallType({ pkgVersion, installedPkg }); try { const res = await installPackage({ savedObjectsClient, @@ -155,36 +157,38 @@ export const installPackageFromRegistryHandler: RequestHandler< }; return response.ok({ body }); } catch (e) { - // could have also done `return defaultIngestErrorHandler({ error: e, response })` at each of the returns, - // but doing it this way will log the outer/install errors before any inner/rollback errors const defaultResult = await defaultIngestErrorHandler({ error: e, response }); - if (e instanceof IngestManagerError) { - return defaultResult; - } + await handleInstallPackageFailure({ + savedObjectsClient, + error: e, + pkgName, + pkgVersion, + installedPkg, + callCluster, + }); - // if there is an unknown server error, uninstall any package assets or reinstall the previous version if update - try { - if (installType === 'install' || installType === 'reinstall') { - logger.error(`uninstalling ${pkgkey} after error installing`); - await removeInstallation({ savedObjectsClient, pkgkey, callCluster }); - } - if (installType === 'update') { - // @ts-ignore getInstallType ensures we have installedPkg - const prevVersion = `${pkgName}-${installedPkg.attributes.version}`; - logger.error(`rolling back to ${prevVersion} after error installing ${pkgkey}`); - await installPackage({ - savedObjectsClient, - pkgkey: prevVersion, - callCluster, - }); - } - } catch (error) { - logger.error(`failed to uninstall or rollback package after installation error ${error}`); - } return defaultResult; } }; +export const bulkInstallPackagesFromRegistryHandler: RequestHandler< + undefined, + undefined, + TypeOf +> = async (context, request, response) => { + const savedObjectsClient = context.core.savedObjects.client; + const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; + const res = await bulkInstallPackages({ + savedObjectsClient, + callCluster, + packagesToUpgrade: request.body.packages, + }); + const body: BulkInstallPackagesResponse = { + response: res, + }; + return response.ok({ body }); +}; + export const installPackageByUploadHandler: RequestHandler< undefined, undefined, diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/index.ts b/x-pack/plugins/ingest_manager/server/routes/epm/index.ts index 9048652f0e8a9..eaf61335b5e06 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/index.ts @@ -14,6 +14,7 @@ import { installPackageFromRegistryHandler, installPackageByUploadHandler, deletePackageHandler, + bulkInstallPackagesFromRegistryHandler, } from './handlers'; import { GetCategoriesRequestSchema, @@ -23,6 +24,7 @@ import { InstallPackageFromRegistryRequestSchema, InstallPackageByUploadRequestSchema, DeletePackageRequestSchema, + BulkUpgradePackagesFromRegistryRequestSchema, } from '../../types'; const MAX_FILE_SIZE_BYTES = 104857600; // 100MB @@ -82,6 +84,15 @@ export const registerRoutes = (router: IRouter) => { installPackageFromRegistryHandler ); + router.post( + { + path: EPM_API_ROUTES.BULK_INSTALL_PATTERN, + validate: BulkUpgradePackagesFromRegistryRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + bulkInstallPackagesFromRegistryHandler + ); + router.post( { path: EPM_API_ROUTES.INSTALL_BY_UPLOAD_PATTERN, diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index 54b9c4d3fbb17..800151a41a429 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -6,6 +6,9 @@ import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; import semver from 'semver'; +import Boom from 'boom'; +import { UnwrapPromise } from '@kbn/utility-types'; +import { BulkInstallPackageInfo, IBulkInstallPackageError } from '../../../../common'; import { PACKAGES_SAVED_OBJECT_TYPE, MAX_TIME_COMPLETE_INSTALL } from '../../../constants'; import { AssetReference, @@ -32,10 +35,15 @@ import { ArchiveAsset, } from '../kibana/assets/install'; import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; -import { deleteKibanaSavedObjectsAssets } from './remove'; -import { PackageOutdatedError } from '../../../errors'; +import { deleteKibanaSavedObjectsAssets, removeInstallation } from './remove'; +import { + IngestManagerError, + PackageOutdatedError, + ingestErrorToResponseOptions, +} from '../../../errors'; import { getPackageSavedObjects } from './get'; import { installTransformForDataset } from '../elasticsearch/transform/install'; +import { appContextService } from '../../app_context'; export async function installLatestPackage(options: { savedObjectsClient: SavedObjectsClientContract; @@ -94,17 +102,185 @@ export async function ensureInstalledPackage(options: { return installation; } -export async function installPackage({ +export async function handleInstallPackageFailure({ savedObjectsClient, - pkgkey, + error, + pkgName, + pkgVersion, + installedPkg, callCluster, - force = false, }: { + savedObjectsClient: SavedObjectsClientContract; + error: IngestManagerError | Boom | Error; + pkgName: string; + pkgVersion: string; + installedPkg: SavedObject | undefined; + callCluster: CallESAsCurrentUser; +}) { + if (error instanceof IngestManagerError) { + return; + } + const logger = appContextService.getLogger(); + const pkgkey = Registry.pkgToPkgKey({ + name: pkgName, + version: pkgVersion, + }); + + // if there is an unknown server error, uninstall any package assets or reinstall the previous version if update + try { + const installType = getInstallType({ pkgVersion, installedPkg }); + if (installType === 'install' || installType === 'reinstall') { + logger.error(`uninstalling ${pkgkey} after error installing`); + await removeInstallation({ savedObjectsClient, pkgkey, callCluster }); + } + + if (installType === 'update') { + if (!installedPkg) { + logger.error( + `failed to rollback package after installation error ${error} because saved object was undefined` + ); + return; + } + const prevVersion = `${pkgName}-${installedPkg.attributes.version}`; + logger.error(`rolling back to ${prevVersion} after error installing ${pkgkey}`); + await installPackage({ + savedObjectsClient, + pkgkey: prevVersion, + callCluster, + }); + } + } catch (e) { + logger.error(`failed to uninstall or rollback package after installation error ${e}`); + } +} + +type BulkInstallResponse = BulkInstallPackageInfo | IBulkInstallPackageError; +function bulkInstallErrorToOptions({ + pkgToUpgrade, + error, +}: { + pkgToUpgrade: string; + error: Error; +}): IBulkInstallPackageError { + const { statusCode, body } = ingestErrorToResponseOptions(error); + return { + name: pkgToUpgrade, + statusCode, + error: body.message, + }; +} + +interface UpgradePackageParams { + savedObjectsClient: SavedObjectsClientContract; + callCluster: CallESAsCurrentUser; + installedPkg: UnwrapPromise>; + latestPkg: UnwrapPromise>; + pkgToUpgrade: string; +} +async function upgradePackage({ + savedObjectsClient, + callCluster, + installedPkg, + latestPkg, + pkgToUpgrade, +}: UpgradePackageParams): Promise { + if (!installedPkg || semver.gt(latestPkg.version, installedPkg.attributes.version)) { + const pkgkey = Registry.pkgToPkgKey({ + name: latestPkg.name, + version: latestPkg.version, + }); + + try { + const assets = await installPackage({ savedObjectsClient, pkgkey, callCluster }); + return { + name: pkgToUpgrade, + newVersion: latestPkg.version, + oldVersion: installedPkg?.attributes.version ?? null, + assets, + }; + } catch (installFailed) { + await handleInstallPackageFailure({ + savedObjectsClient, + error: installFailed, + pkgName: latestPkg.name, + pkgVersion: latestPkg.version, + installedPkg, + callCluster, + }); + return bulkInstallErrorToOptions({ pkgToUpgrade, error: installFailed }); + } + } else { + // package was already at the latest version + return { + name: pkgToUpgrade, + newVersion: latestPkg.version, + oldVersion: latestPkg.version, + assets: [ + ...installedPkg.attributes.installed_es, + ...installedPkg.attributes.installed_kibana, + ], + }; + } +} + +interface BulkInstallPackagesParams { + savedObjectsClient: SavedObjectsClientContract; + packagesToUpgrade: string[]; + callCluster: CallESAsCurrentUser; +} +export async function bulkInstallPackages({ + savedObjectsClient, + packagesToUpgrade, + callCluster, +}: BulkInstallPackagesParams): Promise { + const installedAndLatestPromises = packagesToUpgrade.map((pkgToUpgrade) => + Promise.all([ + getInstallationObject({ savedObjectsClient, pkgName: pkgToUpgrade }), + Registry.fetchFindLatestPackage(pkgToUpgrade), + ]) + ); + const installedAndLatestResults = await Promise.allSettled(installedAndLatestPromises); + const installResponsePromises = installedAndLatestResults.map(async (result, index) => { + const pkgToUpgrade = packagesToUpgrade[index]; + if (result.status === 'fulfilled') { + const [installedPkg, latestPkg] = result.value; + return upgradePackage({ + savedObjectsClient, + callCluster, + installedPkg, + latestPkg, + pkgToUpgrade, + }); + } else { + return bulkInstallErrorToOptions({ pkgToUpgrade, error: result.reason }); + } + }); + const installResults = await Promise.allSettled(installResponsePromises); + const installResponses = installResults.map((result, index) => { + const pkgToUpgrade = packagesToUpgrade[index]; + if (result.status === 'fulfilled') { + return result.value; + } else { + return bulkInstallErrorToOptions({ pkgToUpgrade, error: result.reason }); + } + }); + + return installResponses; +} + +interface InstallPackageParams { savedObjectsClient: SavedObjectsClientContract; pkgkey: string; callCluster: CallESAsCurrentUser; force?: boolean; -}): Promise { +} + +export async function installPackage({ + savedObjectsClient, + pkgkey, + callCluster, + force = false, +}: InstallPackageParams): Promise { // TODO: change epm API to /packageName/version so we don't need to do this const { pkgName, pkgVersion } = Registry.splitPkgKey(pkgkey); // TODO: calls to getInstallationObject, Registry.fetchInfo, and Registry.fetchFindLatestPackge diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts index d7a801feec34f..5d2a078374854 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts @@ -43,6 +43,12 @@ export const InstallPackageFromRegistryRequestSchema = { ), }; +export const BulkUpgradePackagesFromRegistryRequestSchema = { + body: schema.object({ + packages: schema.arrayOf(schema.string(), { minSize: 1 }), + }), +}; + export const InstallPackageByUploadRequestSchema = { body: schema.buffer(), }; diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/bulk_upgrade.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/bulk_upgrade.ts new file mode 100644 index 0000000000000..e377ea5a762f9 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/bulk_upgrade.ts @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../../helpers'; +import { + BulkInstallPackageInfo, + BulkInstallPackagesResponse, + IBulkInstallPackageError, +} from '../../../../plugins/ingest_manager/common'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + + const deletePackage = async (pkgkey: string) => { + await supertest.delete(`/api/ingest_manager/epm/packages/${pkgkey}`).set('kbn-xsrf', 'xxxx'); + }; + + describe('bulk package upgrade api', async () => { + skipIfNoDockerRegistry(providerContext); + + describe('bulk package upgrade with a package already installed', async () => { + beforeEach(async () => { + await supertest + .post(`/api/ingest_manager/epm/packages/multiple_versions-0.1.0`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }) + .expect(200); + }); + afterEach(async () => { + await deletePackage('multiple_versions-0.1.0'); + await deletePackage('multiple_versions-0.3.0'); + await deletePackage('overrides-0.1.0'); + }); + + it('should return 400 if no packages are requested for upgrade', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/_bulk`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + }); + it('should return 200 and an array for upgrading a package', async function () { + const { body }: { body: BulkInstallPackagesResponse } = await supertest + .post(`/api/ingest_manager/epm/packages/_bulk`) + .set('kbn-xsrf', 'xxxx') + .send({ packages: ['multiple_versions'] }) + .expect(200); + expect(body.response.length).equal(1); + expect(body.response[0].name).equal('multiple_versions'); + const entry = body.response[0] as BulkInstallPackageInfo; + expect(entry.oldVersion).equal('0.1.0'); + expect(entry.newVersion).equal('0.3.0'); + }); + it('should return an error for packages that do not exist', async function () { + const { body }: { body: BulkInstallPackagesResponse } = await supertest + .post(`/api/ingest_manager/epm/packages/_bulk`) + .set('kbn-xsrf', 'xxxx') + .send({ packages: ['multiple_versions', 'blahblah'] }) + .expect(200); + expect(body.response.length).equal(2); + expect(body.response[0].name).equal('multiple_versions'); + const entry = body.response[0] as BulkInstallPackageInfo; + expect(entry.oldVersion).equal('0.1.0'); + expect(entry.newVersion).equal('0.3.0'); + + const err = body.response[1] as IBulkInstallPackageError; + expect(err.statusCode).equal(404); + expect(body.response[1].name).equal('blahblah'); + }); + it('should upgrade multiple packages', async function () { + const { body }: { body: BulkInstallPackagesResponse } = await supertest + .post(`/api/ingest_manager/epm/packages/_bulk`) + .set('kbn-xsrf', 'xxxx') + .send({ packages: ['multiple_versions', 'overrides'] }) + .expect(200); + expect(body.response.length).equal(2); + expect(body.response[0].name).equal('multiple_versions'); + let entry = body.response[0] as BulkInstallPackageInfo; + expect(entry.oldVersion).equal('0.1.0'); + expect(entry.newVersion).equal('0.3.0'); + + entry = body.response[1] as BulkInstallPackageInfo; + expect(entry.oldVersion).equal(null); + expect(entry.newVersion).equal('0.1.0'); + expect(entry.name).equal('overrides'); + }); + }); + + describe('bulk upgrade without package already installed', async () => { + afterEach(async () => { + await deletePackage('multiple_versions-0.3.0'); + }); + + it('should return 200 and an array for upgrading a package', async function () { + const { body }: { body: BulkInstallPackagesResponse } = await supertest + .post(`/api/ingest_manager/epm/packages/_bulk`) + .set('kbn-xsrf', 'xxxx') + .send({ packages: ['multiple_versions'] }) + .expect(200); + expect(body.response.length).equal(1); + expect(body.response[0].name).equal('multiple_versions'); + const entry = body.response[0] as BulkInstallPackageInfo; + expect(entry.oldVersion).equal(null); + expect(entry.newVersion).equal('0.3.0'); + }); + }); + }); +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/index.js b/x-pack/test/ingest_manager_api_integration/apis/epm/index.js index 28743ee5f43c2..e509babc9828b 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/index.js +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/index.js @@ -16,6 +16,7 @@ export default function loadTests({ loadTestFile }) { loadTestFile(require.resolve('./install_prerelease')); loadTestFile(require.resolve('./install_remove_assets')); loadTestFile(require.resolve('./install_update')); + loadTestFile(require.resolve('./bulk_upgrade')); loadTestFile(require.resolve('./update_assets')); loadTestFile(require.resolve('./data_stream')); loadTestFile(require.resolve('./package_install_complete'));