Skip to content

Commit

Permalink
[Ingest Manager] Ingest setup upgrade (#78081) (#78680)
Browse files Browse the repository at this point in the history
* 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 jestjs/jest#1075 (comment)

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 <john.schulz@elastic.co>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>

Co-authored-by: John Schulz <john.schulz@elastic.co>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
  • Loading branch information
3 people authored Sep 28, 2020
1 parent f9abd17 commit 0ea9480
Show file tree
Hide file tree
Showing 11 changed files with 426 additions and 191 deletions.
4 changes: 2 additions & 2 deletions x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export interface InstallPackageResponse {
response: AssetReference[];
}

export interface IBulkInstallPackageError {
export interface IBulkInstallPackageHTTPError {
name: string;
statusCode: number;
error: string | Error;
Expand All @@ -86,7 +86,7 @@ export interface BulkInstallPackageInfo {
}

export interface BulkInstallPackagesResponse {
response: Array<BulkInstallPackageInfo | IBulkInstallPackageError>;
response: Array<BulkInstallPackageInfo | IBulkInstallPackageHTTPError>;
}

export interface BulkInstallPackagesRequest {
Expand Down
32 changes: 25 additions & 7 deletions x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import {
GetCategoriesResponse,
GetPackagesResponse,
GetLimitedPackagesResponse,
BulkInstallPackageInfo,
BulkInstallPackagesResponse,
IBulkInstallPackageHTTPError,
} from '../../../common';
import {
GetCategoriesRequestSchema,
Expand All @@ -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,
Expand Down Expand Up @@ -171,20 +173,36 @@ 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,
TypeOf<typeof BulkUpgradePackagesFromRegistryRequestSchema.body>
> = 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 });
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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<BulkInstallResponse[]> {
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;
}
Original file line number Diff line number Diff line change
@@ -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<typeof getInstallation>;

// 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<Installation> = {
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<SavedObjectsClientContract>;
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();
});
});
Loading

0 comments on commit 0ea9480

Please sign in to comment.