From efcf7d1a25f4df487829489b631edd39b75ae3e5 Mon Sep 17 00:00:00 2001 From: Sonja Krause-Harder Date: Mon, 19 Apr 2021 16:01:10 +0200 Subject: [PATCH] [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. --- .../plugins/fleet/common/types/models/epm.ts | 5 ++++ .../common/types/rest_spec/ingest_setup.ts | 3 ++ .../fleet/public/applications/fleet/app.tsx | 7 +++++ .../server/routes/setup/handlers.test.ts | 6 +++- .../fleet/server/routes/setup/handlers.ts | 10 +++++-- .../epm/packages/bulk_install_packages.ts | 12 ++++++-- .../ensure_installed_default_packages.test.ts | 2 +- .../server/services/epm/packages/install.ts | 28 ++++++++++++++++--- x-pack/plugins/fleet/server/services/setup.ts | 13 ++++++--- 9 files changed, 72 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 1a594e77f4857..eab13fe5819f9 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -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'; diff --git a/x-pack/plugins/fleet/common/types/rest_spec/ingest_setup.ts b/x-pack/plugins/fleet/common/types/rest_spec/ingest_setup.ts index 2180b66908498..6f64f1c48336d 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/ingest_setup.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/ingest_setup.ts @@ -5,7 +5,10 @@ * 2.0. */ +import type { DefaultPackagesInstallationError } from '../models/epm'; + export interface PostIngestSetupResponse { isInitialized: boolean; preconfigurationError?: { name: string; message: string }; + nonFatalPackageUpgradeErrors?: DefaultPackagesInstallationError[]; } diff --git a/x-pack/plugins/fleet/public/applications/fleet/app.tsx b/x-pack/plugins/fleet/public/applications/fleet/app.tsx index 5663bd4768d5c..f2eee6228906a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/app.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/app.tsx @@ -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); } diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts index 2cf9bbc3b91e3..fd32d699ae45e 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts @@ -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); diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.ts index b6aa9e29de9ee..a6d7acccfb4fe 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.ts @@ -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, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts b/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts index baaaaf6c6b0cf..2c5b072aa3979 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts @@ -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; } diff --git a/x-pack/plugins/fleet/server/services/epm/packages/ensure_installed_default_packages.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/ensure_installed_default_packages.test.ts index f8c91e55fbbb6..60e2e5ea2cbf8 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/ensure_installed_default_packages.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/ensure_installed_default_packages.test.ts @@ -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 {} diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index 31d0732096790..ec1cc322475b0 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -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 { +): Promise { 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; diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index c906dc73e6df2..de6876c7f6fda 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -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 { - 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(