[Fleet] Don't fail on errors in 'update' or 'reupdate' operation in /setup (#97404)

* Don't fail on, just report, update and reupdate errors.

* Show error toast on update and reupdate errors.

* Don't return empty error array.

* Adjust mock.

* Adjust test.
This commit is contained in:
Sonja Krause-Harder 2021-04-19 16:01:10 +02:00 committed by GitHub
parent 17ecb04dce
commit efcf7d1a25
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 72 additions and 14 deletions

View file

@ -30,6 +30,11 @@ export enum InstallStatus {
uninstalling = 'uninstalling',
}
export interface DefaultPackagesInstallationError {
installType: InstallType;
error: Error;
}
export type InstallType = 'reinstall' | 'reupdate' | 'rollback' | 'update' | 'install' | 'unknown';
export type InstallSource = 'registry' | 'upload';

View file

@ -5,7 +5,10 @@
* 2.0.
*/
import type { DefaultPackagesInstallationError } from '../models/epm';
export interface PostIngestSetupResponse {
isInitialized: boolean;
preconfigurationError?: { name: string; message: string };
nonFatalPackageUpgradeErrors?: DefaultPackagesInstallationError[];
}

View file

@ -90,6 +90,13 @@ export const WithPermissionsAndSetup: React.FC = memo(({ children }) => {
}),
});
}
if (setupResponse.data.nonFatalPackageUpgradeErrors) {
notifications.toasts.addError(setupResponse.data.nonFatalPackageUpgradeErrors, {
title: i18n.translate('xpack.fleet.setup.nonFatalPackageErrorsTitle', {
defaultMessage: 'One or more packages could not be successfully upgraded',
}),
});
}
} catch (err) {
setInitializationError(err);
}

View file

@ -46,7 +46,11 @@ describe('FleetSetupHandler', () => {
it('POST /setup succeeds w/200 and body of resolved value', async () => {
mockSetupIngestManager.mockImplementation(() =>
Promise.resolve({ isInitialized: true, preconfigurationError: undefined })
Promise.resolve({
isInitialized: true,
preconfigurationError: undefined,
nonFatalPackageUpgradeErrors: [],
})
);
await fleetSetupHandler(context, request, response);

View file

@ -46,8 +46,14 @@ export const fleetSetupHandler: RequestHandler = async (context, request, respon
try {
const soClient = context.core.savedObjects.client;
const esClient = context.core.elasticsearch.client.asCurrentUser;
const body: PostIngestSetupResponse = { isInitialized: true };
await setupIngestManager(soClient, esClient);
const setupStatus = await setupIngestManager(soClient, esClient);
const body: PostIngestSetupResponse = {
isInitialized: true,
};
if (setupStatus.nonFatalPackageUpgradeErrors.length > 0) {
body.nonFatalPackageUpgradeErrors = setupStatus.nonFatalPackageUpgradeErrors;
}
return response.ok({
body,

View file

@ -45,7 +45,11 @@ export async function bulkInstallPackages({
skipPostInstall: true,
});
if (installResult.error) {
return { name: packageName, error: installResult.error };
return {
name: packageName,
error: installResult.error,
installType: installResult.installType,
};
} else {
return {
name: packageName,
@ -75,7 +79,11 @@ export async function bulkInstallPackages({
const packageName = packagesToInstall[index];
if (result.status === 'fulfilled') {
if (result.value && result.value.error) {
return { name: packageName, error: result.value.error };
return {
name: packageName,
error: result.value.error,
installType: result.value.installType,
};
} else {
return result.value;
}

View file

@ -84,7 +84,7 @@ describe('ensureInstalledDefaultPackages', () => {
];
});
const resp = await ensureInstalledDefaultPackages(soClient, jest.fn());
expect(resp).toEqual([mockInstallation.attributes]);
expect(resp.installations).toEqual([mockInstallation.attributes]);
});
it('should throw the first Error it finds', async () => {
class SomeCustomError extends Error {}

View file

@ -12,7 +12,12 @@ import type { ElasticsearchClient, SavedObject, SavedObjectsClientContract } fro
import { generateESIndexPatterns } from '../elasticsearch/template/template';
import { defaultPackages } from '../../../../common';
import type { BulkInstallPackageInfo, InstallablePackage, InstallSource } from '../../../../common';
import type {
BulkInstallPackageInfo,
InstallablePackage,
InstallSource,
DefaultPackagesInstallationError,
} from '../../../../common';
import {
IngestManagerError,
PackageOperationNotSupportedError,
@ -45,11 +50,17 @@ import { removeInstallation } from './remove';
import { getPackageSavedObjects } from './get';
import { _installPackage } from './_install_package';
export interface DefaultPackagesInstallationResult {
installations: Installation[];
nonFatalPackageUpgradeErrors: DefaultPackagesInstallationError[];
}
export async function ensureInstalledDefaultPackages(
savedObjectsClient: SavedObjectsClientContract,
esClient: ElasticsearchClient
): Promise<Installation[]> {
): Promise<DefaultPackagesInstallationResult> {
const installations = [];
const nonFatalPackageUpgradeErrors = [];
const bulkResponse = await bulkInstallPackages({
savedObjectsClient,
packagesToInstall: Object.values(defaultPackages),
@ -58,19 +69,27 @@ export async function ensureInstalledDefaultPackages(
for (const resp of bulkResponse) {
if (isBulkInstallError(resp)) {
throw resp.error;
if (resp.installType && (resp.installType === 'update' || resp.installType === 'reupdate')) {
nonFatalPackageUpgradeErrors.push({ installType: resp.installType, error: resp.error });
} else {
throw resp.error;
}
} else {
installations.push(getInstallation({ savedObjectsClient, pkgName: resp.name }));
}
}
const retrievedInstallations = await Promise.all(installations);
return retrievedInstallations.map((installation, index) => {
const verifiedInstallations = retrievedInstallations.map((installation, index) => {
if (!installation) {
throw new Error(`could not get installation ${bulkResponse[index].name}`);
}
return installation;
});
return {
installations: verifiedInstallations,
nonFatalPackageUpgradeErrors,
};
}
async function isPackageVersionOrLaterInstalled(options: {
@ -181,6 +200,7 @@ export async function handleInstallPackageFailure({
export interface IBulkInstallPackageError {
name: string;
error: Error;
installType?: InstallType;
}
export type BulkInstallResponse = BulkInstallPackageInfo | IBulkInstallPackageError;

View file

@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n';
import { DEFAULT_AGENT_POLICIES_PACKAGES, FLEET_SERVER_PACKAGE } from '../../common';
import type { PackagePolicy } from '../../common';
import type { PackagePolicy, DefaultPackagesInstallationError } from '../../common';
import { SO_SEARCH_LIMIT } from '../constants';
@ -33,6 +33,7 @@ import { awaitIfFleetServerSetupPending } from './fleet_server';
export interface SetupStatus {
isInitialized: boolean;
preconfigurationError: { name: string; message: string } | undefined;
nonFatalPackageUpgradeErrors: DefaultPackagesInstallationError[];
}
export async function setupIngestManager(
@ -46,7 +47,7 @@ async function createSetupSideEffects(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient
): Promise<SetupStatus> {
const [installedPackages, defaultOutput] = await Promise.all([
const [defaultPackagesResult, defaultOutput] = await Promise.all([
// packages installed by default
ensureInstalledDefaultPackages(soClient, esClient),
outputService.ensureDefaultOutput(soClient),
@ -142,7 +143,7 @@ async function createSetupSideEffects(
);
}
for (const installedPackage of installedPackages) {
for (const installedPackage of defaultPackagesResult.installations) {
const packageShouldBeInstalled = DEFAULT_AGENT_POLICIES_PACKAGES.some(
(packageName) => installedPackage.name === packageName
);
@ -172,7 +173,11 @@ async function createSetupSideEffects(
await ensureAgentActionPolicyChangeExists(soClient, esClient);
return { isInitialized: true, preconfigurationError };
return {
isInitialized: true,
preconfigurationError,
nonFatalPackageUpgradeErrors: defaultPackagesResult.nonFatalPackageUpgradeErrors,
};
}
export async function ensureDefaultEnrollmentAPIKeysExists(