From da88f873de4acf63e5e549a42d9ff8b0bf934043 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Mon, 28 Sep 2020 16:12:34 -0400 Subject: [PATCH] [Ingest Manager] Ingest setup upgrade (#78081) * 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 * Adding upgrade to setup route * Adding setup integration test * Clean up error handling * Beginning to add tests * Failing jest mock tests * Break up tests & modules for easier testing. Deal with issue described in https://github.com/facebook/jest/issues/1075#issuecomment-221771095 epm/packages/install has functions a, b, c which are independent but a can also call b and c function a() { b(); c(); } The linked FB issue describes the cause and rationale (Jest works on "module" boundary) but TL;DR: it's easier if you split up your files Some related links I found during this journey * https://medium.com/@qjli/how-to-mock-specific-module-function-in-jest-715e39a391f4 * https://stackoverflow.com/questions/52650367/jestjs-how-to-test-function-being-called-inside-another-function * https://stackoverflow.com/questions/50854440/spying-on-an-imported-function-that-calls-another-function-in-jest/50855968#50855968 * Add test confirming update error result will throw * Keep orig error. Add status code in http handler * Leave error as-is * Removing accidental code changes. File rename. * Missed a function when moving to a new file * Add missing type imports * Lift .map lambda into named outer function * Adding additional test * Fixing type error Co-authored-by: John Schulz Co-authored-by: Elastic Machine --- .../common/types/rest_spec/epm.ts | 4 +- .../server/routes/epm/handlers.ts | 32 +++- .../epm/packages/bulk_install_packages.ts | 61 ++++++++ .../ensure_installed_default_packages.test.ts | 144 ++++++++++++++++++ .../epm/packages/get_install_type.test.ts | 101 ++++++++++++ .../server/services/epm/packages/index.ts | 10 +- .../services/epm/packages/install.test.ts | 103 ------------- .../server/services/epm/packages/install.ts | 109 ++++--------- .../apis/epm/bulk_upgrade.ts | 4 +- .../apis/epm/index.js | 1 + .../apis/epm/setup.ts | 48 ++++++ 11 files changed, 426 insertions(+), 191 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/server/services/epm/packages/bulk_install_packages.ts create mode 100644 x-pack/plugins/ingest_manager/server/services/epm/packages/ensure_installed_default_packages.test.ts create mode 100644 x-pack/plugins/ingest_manager/server/services/epm/packages/get_install_type.test.ts delete mode 100644 x-pack/plugins/ingest_manager/server/services/epm/packages/install.test.ts create mode 100644 x-pack/test/ingest_manager_api_integration/apis/epm/setup.ts 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 7ed2fed91aa93c..0709eddaa52ec9 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,7 +71,7 @@ export interface InstallPackageResponse { response: AssetReference[]; } -export interface IBulkInstallPackageError { +export interface IBulkInstallPackageHTTPError { name: string; statusCode: number; error: string | Error; @@ -86,7 +86,7 @@ export interface BulkInstallPackageInfo { } export interface BulkInstallPackagesResponse { - response: Array; + response: Array; } export interface BulkInstallPackagesRequest { 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 7ae896c1f30a6a..c55979d187f9df 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts @@ -13,7 +13,9 @@ import { GetCategoriesResponse, GetPackagesResponse, GetLimitedPackagesResponse, + BulkInstallPackageInfo, BulkInstallPackagesResponse, + IBulkInstallPackageHTTPError, } from '../../../common'; import { GetCategoriesRequestSchema, @@ -26,21 +28,21 @@ import { BulkUpgradePackagesFromRegistryRequestSchema, } from '../../types'; import { + BulkInstallResponse, + bulkInstallPackages, getCategories, getPackages, getFile, getPackageInfo, + handleInstallPackageFailure, installPackage, + isBulkInstallError, removeInstallation, getLimitedPackages, getInstallationObject, } from '../../services/epm/packages'; -import { defaultIngestErrorHandler } from '../../errors'; +import { defaultIngestErrorHandler, ingestErrorToResponseOptions } from '../../errors'; import { splitPkgKey } from '../../services/epm/registry'; -import { - handleInstallPackageFailure, - bulkInstallPackages, -} from '../../services/epm/packages/install'; export const getCategoriesHandler: RequestHandler< undefined, @@ -171,6 +173,21 @@ export const installPackageFromRegistryHandler: RequestHandler< } }; +const bulkInstallServiceResponseToHttpEntry = ( + result: BulkInstallResponse +): BulkInstallPackageInfo | IBulkInstallPackageHTTPError => { + if (isBulkInstallError(result)) { + const { statusCode, body } = ingestErrorToResponseOptions(result.error); + return { + name: result.name, + statusCode, + error: body.message, + }; + } else { + return result; + } +}; + export const bulkInstallPackagesFromRegistryHandler: RequestHandler< undefined, undefined, @@ -178,13 +195,14 @@ export const bulkInstallPackagesFromRegistryHandler: RequestHandler< > = async (context, request, response) => { const savedObjectsClient = context.core.savedObjects.client; const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; - const res = await bulkInstallPackages({ + const bulkInstalledResponses = await bulkInstallPackages({ savedObjectsClient, callCluster, packagesToUpgrade: request.body.packages, }); + const payload = bulkInstalledResponses.map(bulkInstallServiceResponseToHttpEntry); const body: BulkInstallPackagesResponse = { - response: res, + response: payload, }; return response.ok({ body }); }; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/bulk_install_packages.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/bulk_install_packages.ts new file mode 100644 index 00000000000000..af937c55930825 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/bulk_install_packages.ts @@ -0,0 +1,61 @@ +/* + * 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 { SavedObjectsClientContract } from 'src/core/server'; +import { CallESAsCurrentUser } from '../../../types'; +import * as Registry from '../registry'; +import { getInstallationObject } from './index'; +import { BulkInstallResponse, IBulkInstallPackageError, upgradePackage } from './install'; + +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 { name: 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 { name: pkgToUpgrade, error: result.reason }; + } + }); + + return installResponses; +} + +export function isBulkInstallError(test: any): test is IBulkInstallPackageError { + return 'error' in test && test.error instanceof Error; +} diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/ensure_installed_default_packages.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/ensure_installed_default_packages.test.ts new file mode 100644 index 00000000000000..f0b487ad597745 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/ensure_installed_default_packages.test.ts @@ -0,0 +1,144 @@ +/* + * 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 { ElasticsearchAssetType, Installation, KibanaAssetType } from '../../../types'; +import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; + +jest.mock('./install'); +jest.mock('./bulk_install_packages'); +jest.mock('./get'); + +import { bulkInstallPackages, isBulkInstallError } from './bulk_install_packages'; +const { ensureInstalledDefaultPackages } = jest.requireActual('./install'); +const { isBulkInstallError: actualIsBulkInstallError } = jest.requireActual( + './bulk_install_packages' +); +import { getInstallation } from './get'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { appContextService } from '../../app_context'; +import { createAppContextStartContractMock } from '../../../mocks'; + +// if we add this assertion, TS will type check the return value +// and the editor will also know about .mockImplementation, .mock.calls, etc +const mockedBulkInstallPackages = bulkInstallPackages as jest.MockedFunction< + typeof bulkInstallPackages +>; +const mockedIsBulkInstallError = isBulkInstallError as jest.MockedFunction< + typeof isBulkInstallError +>; +const mockedGetInstallation = getInstallation as jest.MockedFunction; + +// I was unable to get the actual implementation set in the `jest.mock()` call at the top to work +// so this will set the `isBulkInstallError` function back to the actual implementation +mockedIsBulkInstallError.mockImplementation(actualIsBulkInstallError); + +const mockInstallation: SavedObject = { + id: 'test-pkg', + references: [], + type: 'epm-packages', + attributes: { + id: 'test-pkg', + installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }], + installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], + es_index_patterns: { pattern: 'pattern-name' }, + name: 'test package', + version: '1.0.0', + install_status: 'installed', + install_version: '1.0.0', + install_started_at: new Date().toISOString(), + }, +}; + +describe('ensureInstalledDefaultPackages', () => { + let soClient: jest.Mocked; + beforeEach(async () => { + soClient = savedObjectsClientMock.create(); + appContextService.start(createAppContextStartContractMock()); + }); + afterEach(async () => { + appContextService.stop(); + }); + it('should return an array of Installation objects when successful', async () => { + mockedGetInstallation.mockImplementation(async () => { + return mockInstallation.attributes; + }); + mockedBulkInstallPackages.mockImplementationOnce(async function () { + return [ + { + name: mockInstallation.attributes.name, + assets: [], + newVersion: '', + oldVersion: '', + statusCode: 200, + }, + ]; + }); + const resp = await ensureInstalledDefaultPackages(soClient, jest.fn()); + expect(resp).toEqual([mockInstallation.attributes]); + }); + it('should throw the first Error it finds', async () => { + class SomeCustomError extends Error {} + mockedGetInstallation.mockImplementation(async () => { + return mockInstallation.attributes; + }); + mockedBulkInstallPackages.mockImplementationOnce(async function () { + return [ + { + name: 'success one', + assets: [], + newVersion: '', + oldVersion: '', + statusCode: 200, + }, + { + name: 'success two', + assets: [], + newVersion: '', + oldVersion: '', + statusCode: 200, + }, + { + name: 'failure one', + error: new SomeCustomError('abc 123'), + }, + { + name: 'success three', + assets: [], + newVersion: '', + oldVersion: '', + statusCode: 200, + }, + { + name: 'failure two', + error: new Error('zzz'), + }, + ]; + }); + const installPromise = ensureInstalledDefaultPackages(soClient, jest.fn()); + expect.assertions(2); + expect(installPromise).rejects.toThrow(SomeCustomError); + expect(installPromise).rejects.toThrow('abc 123'); + }); + it('should throw an error when get installation returns undefined', async () => { + mockedGetInstallation.mockImplementation(async () => { + return undefined; + }); + mockedBulkInstallPackages.mockImplementationOnce(async function () { + return [ + { + name: 'undefined package', + assets: [], + newVersion: '', + oldVersion: '', + statusCode: 200, + }, + ]; + }); + const installPromise = ensureInstalledDefaultPackages(soClient, jest.fn()); + expect.assertions(1); + expect(installPromise).rejects.toThrow(); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_install_type.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_install_type.test.ts new file mode 100644 index 00000000000000..cce4b7fee8fd7c --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_install_type.test.ts @@ -0,0 +1,101 @@ +/* + * 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 { SavedObject } from 'src/core/server'; +import { ElasticsearchAssetType, Installation, KibanaAssetType } from '../../../types'; +import { getInstallType } from './install'; + +const mockInstallation: SavedObject = { + id: 'test-pkg', + references: [], + type: 'epm-packages', + attributes: { + id: 'test-pkg', + installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }], + installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], + es_index_patterns: { pattern: 'pattern-name' }, + name: 'test packagek', + version: '1.0.0', + install_status: 'installed', + install_version: '1.0.0', + install_started_at: new Date().toISOString(), + }, +}; +const mockInstallationUpdateFail: SavedObject = { + id: 'test-pkg', + references: [], + type: 'epm-packages', + attributes: { + id: 'test-pkg', + installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }], + installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], + es_index_patterns: { pattern: 'pattern-name' }, + name: 'test packagek', + version: '1.0.0', + install_status: 'installing', + install_version: '1.0.1', + install_started_at: new Date().toISOString(), + }, +}; + +describe('getInstallType', () => { + it('should return correct type when installing and no other version is currently installed', () => { + const installTypeInstall = getInstallType({ pkgVersion: '1.0.0', installedPkg: undefined }); + expect(installTypeInstall).toBe('install'); + + // @ts-expect-error can only be 'install' if no installedPkg given + expect(installTypeInstall === 'update').toBe(false); + // @ts-expect-error can only be 'install' if no installedPkg given + expect(installTypeInstall === 'reinstall').toBe(false); + // @ts-expect-error can only be 'install' if no installedPkg given + expect(installTypeInstall === 'reupdate').toBe(false); + // @ts-expect-error can only be 'install' if no installedPkg given + expect(installTypeInstall === 'rollback').toBe(false); + }); + + it('should return correct type when installing the same version', () => { + const installTypeReinstall = getInstallType({ + pkgVersion: '1.0.0', + installedPkg: mockInstallation, + }); + expect(installTypeReinstall).toBe('reinstall'); + + // @ts-expect-error cannot be 'install' if given installedPkg + expect(installTypeReinstall === 'install').toBe(false); + }); + + it('should return correct type when moving from one version to another', () => { + const installTypeUpdate = getInstallType({ + pkgVersion: '1.0.1', + installedPkg: mockInstallation, + }); + expect(installTypeUpdate).toBe('update'); + + // @ts-expect-error cannot be 'install' if given installedPkg + expect(installTypeUpdate === 'install').toBe(false); + }); + + it('should return correct type when update fails and trys again', () => { + const installTypeReupdate = getInstallType({ + pkgVersion: '1.0.1', + installedPkg: mockInstallationUpdateFail, + }); + expect(installTypeReupdate).toBe('reupdate'); + + // @ts-expect-error cannot be 'install' if given installedPkg + expect(installTypeReupdate === 'install').toBe(false); + }); + + it('should return correct type when attempting to rollback from a failed update', () => { + const installTypeRollback = getInstallType({ + pkgVersion: '1.0.0', + installedPkg: mockInstallationUpdateFail, + }); + expect(installTypeRollback).toBe('rollback'); + + // @ts-expect-error cannot be 'install' if given installedPkg + expect(installTypeRollback === 'install').toBe(false); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts index 57c4f77432455d..94aa969c2d2b88 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts @@ -12,6 +12,8 @@ import { InstallationStatus, KibanaAssetType, } from '../../../types'; + +export { bulkInstallPackages, isBulkInstallError } from './bulk_install_packages'; export { getCategories, getFile, @@ -23,7 +25,13 @@ export { SearchParams, } from './get'; -export { installPackage, ensureInstalledPackage } from './install'; +export { + BulkInstallResponse, + handleInstallPackageFailure, + installPackage, + IBulkInstallPackageError, + ensureInstalledPackage, +} from './install'; export { removeInstallation } from './remove'; type RequiredPackage = 'system' | 'endpoint'; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.test.ts deleted file mode 100644 index 2f60c74d3514f9..00000000000000 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* - * 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 { ElasticsearchAssetType, Installation, KibanaAssetType } from '../../../types'; -import { SavedObject } from 'src/core/server'; -import { getInstallType } from './install'; - -const mockInstallation: SavedObject = { - id: 'test-pkg', - references: [], - type: 'epm-packages', - attributes: { - id: 'test-pkg', - installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }], - installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], - es_index_patterns: { pattern: 'pattern-name' }, - name: 'test packagek', - version: '1.0.0', - install_status: 'installed', - install_version: '1.0.0', - install_started_at: new Date().toISOString(), - }, -}; -const mockInstallationUpdateFail: SavedObject = { - id: 'test-pkg', - references: [], - type: 'epm-packages', - attributes: { - id: 'test-pkg', - installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }], - installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], - es_index_patterns: { pattern: 'pattern-name' }, - name: 'test packagek', - version: '1.0.0', - install_status: 'installing', - install_version: '1.0.1', - install_started_at: new Date().toISOString(), - }, -}; -describe('install', () => { - describe('getInstallType', () => { - it('should return correct type when installing and no other version is currently installed', () => { - const installTypeInstall = getInstallType({ pkgVersion: '1.0.0', installedPkg: undefined }); - expect(installTypeInstall).toBe('install'); - - // @ts-expect-error can only be 'install' if no installedPkg given - expect(installTypeInstall === 'update').toBe(false); - // @ts-expect-error can only be 'install' if no installedPkg given - expect(installTypeInstall === 'reinstall').toBe(false); - // @ts-expect-error can only be 'install' if no installedPkg given - expect(installTypeInstall === 'reupdate').toBe(false); - // @ts-expect-error can only be 'install' if no installedPkg given - expect(installTypeInstall === 'rollback').toBe(false); - }); - - it('should return correct type when installing the same version', () => { - const installTypeReinstall = getInstallType({ - pkgVersion: '1.0.0', - installedPkg: mockInstallation, - }); - expect(installTypeReinstall).toBe('reinstall'); - - // @ts-expect-error cannot be 'install' if given installedPkg - expect(installTypeReinstall === 'install').toBe(false); - }); - - it('should return correct type when moving from one version to another', () => { - const installTypeUpdate = getInstallType({ - pkgVersion: '1.0.1', - installedPkg: mockInstallation, - }); - expect(installTypeUpdate).toBe('update'); - - // @ts-expect-error cannot be 'install' if given installedPkg - expect(installTypeUpdate === 'install').toBe(false); - }); - - it('should return correct type when update fails and trys again', () => { - const installTypeReupdate = getInstallType({ - pkgVersion: '1.0.1', - installedPkg: mockInstallationUpdateFail, - }); - expect(installTypeReupdate).toBe('reupdate'); - - // @ts-expect-error cannot be 'install' if given installedPkg - expect(installTypeReupdate === 'install').toBe(false); - }); - - it('should return correct type when attempting to rollback from a failed update', () => { - const installTypeRollback = getInstallType({ - pkgVersion: '1.0.0', - installedPkg: mockInstallationUpdateFail, - }); - expect(installTypeRollback).toBe('rollback'); - - // @ts-expect-error cannot be 'install' if given installedPkg - expect(installTypeRollback === 'install').toBe(false); - }); - }); -}); 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 ad4f613b7f8c0c..a88da350d2c530 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 @@ -8,7 +8,7 @@ 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 { BulkInstallPackageInfo } from '../../../../common'; import { PACKAGES_SAVED_OBJECT_TYPE, MAX_TIME_COMPLETE_INSTALL } from '../../../constants'; import { AssetReference, @@ -24,7 +24,13 @@ import { import { appContextService } from '../../index'; import { installIndexPatterns } from '../kibana/index_pattern/install'; import * as Registry from '../registry'; -import { getInstallation, getInstallationObject, isRequiredPackage } from './index'; +import { + getInstallation, + getInstallationObject, + isRequiredPackage, + bulkInstallPackages, + isBulkInstallError, +} from './index'; import { installTemplates } from '../elasticsearch/template/install'; import { generateESIndexPatterns } from '../elasticsearch/template/template'; import { installPipelines, deletePreviousPipelines } from '../elasticsearch/ingest_pipeline/'; @@ -37,11 +43,7 @@ import { } from '../kibana/assets/install'; import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; import { deleteKibanaSavedObjectsAssets, removeInstallation } from './remove'; -import { - IngestManagerError, - PackageOutdatedError, - ingestErrorToResponseOptions, -} from '../../../errors'; +import { IngestManagerError, PackageOutdatedError } from '../../../errors'; import { getPackageSavedObjects } from './get'; import { installTransformForDataset } from '../elasticsearch/transform/install'; @@ -68,17 +70,27 @@ export async function ensureInstalledDefaultPackages( callCluster: CallESAsCurrentUser ): Promise { const installations = []; - for (const pkgName in DefaultPackages) { - if (!DefaultPackages.hasOwnProperty(pkgName)) continue; - const installation = ensureInstalledPackage({ - savedObjectsClient, - pkgName, - callCluster, - }); - installations.push(installation); + const bulkResponse = await bulkInstallPackages({ + savedObjectsClient, + packagesToUpgrade: Object.values(DefaultPackages), + callCluster, + }); + + for (const resp of bulkResponse) { + if (isBulkInstallError(resp)) { + throw resp.error; + } else { + installations.push(getInstallation({ savedObjectsClient, pkgName: resp.name })); + } } - return Promise.all(installations); + const retrievedInstallations = await Promise.all(installations); + return retrievedInstallations.map((installation, index) => { + if (!installation) { + throw new Error(`could not get installation ${bulkResponse[index].name}`); + } + return installation; + }); } export async function ensureInstalledPackage(options: { @@ -154,21 +166,11 @@ export async function handleInstallPackageFailure({ } } -type BulkInstallResponse = BulkInstallPackageInfo | IBulkInstallPackageError; -function bulkInstallErrorToOptions({ - pkgToUpgrade, - error, -}: { - pkgToUpgrade: string; +export interface IBulkInstallPackageError { + name: string; error: Error; -}): IBulkInstallPackageError { - const { statusCode, body } = ingestErrorToResponseOptions(error); - return { - name: pkgToUpgrade, - statusCode, - error: body.message, - }; } +export type BulkInstallResponse = BulkInstallPackageInfo | IBulkInstallPackageError; interface UpgradePackageParams { savedObjectsClient: SavedObjectsClientContract; @@ -177,7 +179,7 @@ interface UpgradePackageParams { latestPkg: UnwrapPromise>; pkgToUpgrade: string; } -async function upgradePackage({ +export async function upgradePackage({ savedObjectsClient, callCluster, installedPkg, @@ -207,7 +209,7 @@ async function upgradePackage({ installedPkg, callCluster, }); - return bulkInstallErrorToOptions({ pkgToUpgrade, error: installFailed }); + return { name: pkgToUpgrade, error: installFailed }; } } else { // package was already at the latest version @@ -223,51 +225,6 @@ async function upgradePackage({ } } -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; 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 index e377ea5a762f95..bafcb79a419c20 100644 --- 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 @@ -10,7 +10,7 @@ import { skipIfNoDockerRegistry } from '../../helpers'; import { BulkInstallPackageInfo, BulkInstallPackagesResponse, - IBulkInstallPackageError, + IBulkInstallPackageHTTPError, } from '../../../../plugins/ingest_manager/common'; export default function (providerContext: FtrProviderContext) { @@ -68,7 +68,7 @@ export default function (providerContext: FtrProviderContext) { expect(entry.oldVersion).equal('0.1.0'); expect(entry.newVersion).equal('0.3.0'); - const err = body.response[1] as IBulkInstallPackageError; + const err = body.response[1] as IBulkInstallPackageHTTPError; expect(err.statusCode).equal(404); expect(body.response[1].name).equal('blahblah'); }); 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 e509babc9828b6..0cb998b9b7c35e 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 @@ -7,6 +7,7 @@ export default function loadTests({ loadTestFile }) { describe('EPM Endpoints', () => { loadTestFile(require.resolve('./list')); + loadTestFile(require.resolve('./setup')); loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./file')); //loadTestFile(require.resolve('./template')); diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/setup.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/setup.ts new file mode 100644 index 00000000000000..da06f49dd61397 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/setup.ts @@ -0,0 +1,48 @@ +/* + * 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 { GetInfoResponse, Installed } from '../../../../plugins/ingest_manager/common'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + const log = getService('log'); + + describe('setup api', async () => { + skipIfNoDockerRegistry(providerContext); + describe('setup performs upgrades', async () => { + const oldEndpointVersion = '0.13.0'; + beforeEach(async () => { + await supertest + .post(`/api/ingest_manager/epm/packages/endpoint-${oldEndpointVersion}`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }) + .expect(200); + }); + it('upgrades the endpoint package from 0.13.0 to the latest version available', async function () { + let { body }: { body: GetInfoResponse } = await supertest + .get(`/api/ingest_manager/epm/packages/endpoint-${oldEndpointVersion}`) + .expect(200); + const latestEndpointVersion = body.response.latestVersion; + log.info(`Endpoint package latest version: ${latestEndpointVersion}`); + // make sure we're actually doing an upgrade + expect(latestEndpointVersion).not.eql(oldEndpointVersion); + await supertest.post(`/api/ingest_manager/setup`).set('kbn-xsrf', 'xxxx').expect(200); + + ({ body } = await supertest + .get(`/api/ingest_manager/epm/packages/endpoint-${latestEndpointVersion}`) + .expect(200)); + expect(body.response).to.have.property('savedObject'); + expect((body.response as Installed).savedObject.attributes.install_version).to.eql( + latestEndpointVersion + ); + }); + }); + }); +}