Skip to content

Commit c5ea18d

Browse files
[Ingest Manager] install previous version of package if update fails (#76694) (#76972)
* add InstallType and getInstallType helper * rollback to previous version if update fails or unisntall if not an update * fix pipelines from failed update not removing if update fails after installing pipelines and rolling back to previous version * change type from enum to union, improve getInstallType, add ts-config comment * throw error if no install type, update logging errors Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
1 parent 3c76124 commit c5ea18d

File tree

5 files changed

+139
-13
lines changed

5 files changed

+139
-13
lines changed

x-pack/plugins/ingest_manager/common/types/models/epm.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export enum InstallStatus {
1919
uninstalling = 'uninstalling',
2020
}
2121

22+
export type InstallType = 'reinstall' | 'reupdate' | 'rollback' | 'update' | 'install';
23+
2224
export type EpmPackageInstallStatus = 'installed' | 'installing';
2325

2426
export type DetailViewPanelName = 'overview' | 'usages' | 'settings';

x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
} from '../../services/epm/packages';
3535
import { IngestManagerError, defaultIngestErrorHandler } from '../../errors';
3636
import { splitPkgKey } from '../../services/epm/registry';
37+
import { getInstallType } from '../../services/epm/packages/install';
3738

3839
export const getCategoriesHandler: RequestHandler<
3940
undefined,
@@ -138,6 +139,8 @@ export const installPackageHandler: RequestHandler<
138139
const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser;
139140
const { pkgkey } = request.params;
140141
const { pkgName, pkgVersion } = splitPkgKey(pkgkey);
142+
const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName });
143+
const installType = getInstallType({ pkgVersion, installedPkg });
141144
try {
142145
const res = await installPackage({
143146
savedObjectsClient,
@@ -156,15 +159,25 @@ export const installPackageHandler: RequestHandler<
156159
if (e instanceof IngestManagerError) {
157160
return defaultResult;
158161
}
159-
// if there is an unknown server error, uninstall any package assets
162+
163+
// if there is an unknown server error, uninstall any package assets or reinstall the previous version if update
160164
try {
161-
const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName });
162-
const isUpdate = installedPkg && installedPkg.attributes.version < pkgVersion ? true : false;
163-
if (!isUpdate) {
165+
if (installType === 'install' || installType === 'reinstall') {
166+
logger.error(`uninstalling ${pkgkey} after error installing`);
164167
await removeInstallation({ savedObjectsClient, pkgkey, callCluster });
165168
}
169+
if (installType === 'update') {
170+
// @ts-ignore installType conditions already check for existence of installedPkg
171+
const prevVersion = `${pkgName}-${installedPkg.attributes.version}`;
172+
logger.error(`rolling back to ${prevVersion} after error installing ${pkgkey}`);
173+
await installPackage({
174+
savedObjectsClient,
175+
pkgkey: prevVersion,
176+
callCluster,
177+
});
178+
}
166179
} catch (error) {
167-
logger.error(`could not remove failed installation ${error}`);
180+
logger.error(`failed to uninstall or rollback package after installation error ${error}`);
168181
}
169182
return defaultResult;
170183
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { ElasticsearchAssetType, Installation, KibanaAssetType } from '../../../types';
8+
import { SavedObject } from 'src/core/server';
9+
import { getInstallType } from './install';
10+
11+
const mockInstallation: SavedObject<Installation> = {
12+
id: 'test-pkg',
13+
references: [],
14+
type: 'epm-packages',
15+
attributes: {
16+
id: 'test-pkg',
17+
installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }],
18+
installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }],
19+
es_index_patterns: { pattern: 'pattern-name' },
20+
name: 'test packagek',
21+
version: '1.0.0',
22+
install_status: 'installed',
23+
install_version: '1.0.0',
24+
install_started_at: new Date().toISOString(),
25+
},
26+
};
27+
const mockInstallationUpdateFail: SavedObject<Installation> = {
28+
id: 'test-pkg',
29+
references: [],
30+
type: 'epm-packages',
31+
attributes: {
32+
id: 'test-pkg',
33+
installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }],
34+
installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }],
35+
es_index_patterns: { pattern: 'pattern-name' },
36+
name: 'test packagek',
37+
version: '1.0.0',
38+
install_status: 'installing',
39+
install_version: '1.0.1',
40+
install_started_at: new Date().toISOString(),
41+
},
42+
};
43+
describe('install', () => {
44+
describe('getInstallType', () => {
45+
it('should return correct type when installing and no other version is currently installed', () => {});
46+
const installTypeInstall = getInstallType({ pkgVersion: '1.0.0', installedPkg: undefined });
47+
expect(installTypeInstall).toBe('install');
48+
49+
it('should return correct type when installing the same version', () => {});
50+
const installTypeReinstall = getInstallType({
51+
pkgVersion: '1.0.0',
52+
installedPkg: mockInstallation,
53+
});
54+
expect(installTypeReinstall).toBe('reinstall');
55+
56+
it('should return correct type when moving from one version to another', () => {});
57+
const installTypeUpdate = getInstallType({
58+
pkgVersion: '1.0.1',
59+
installedPkg: mockInstallation,
60+
});
61+
expect(installTypeUpdate).toBe('update');
62+
63+
it('should return correct type when update fails and trys again', () => {});
64+
const installTypeReupdate = getInstallType({
65+
pkgVersion: '1.0.1',
66+
installedPkg: mockInstallationUpdateFail,
67+
});
68+
expect(installTypeReupdate).toBe('reupdate');
69+
70+
it('should return correct type when attempting to rollback from a failed update', () => {});
71+
const installTypeRollback = getInstallType({
72+
pkgVersion: '1.0.0',
73+
installedPkg: mockInstallationUpdateFail,
74+
});
75+
expect(installTypeRollback).toBe('rollback');
76+
});
77+
});

x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import { SavedObjectsClientContract } from 'src/core/server';
7+
import { SavedObject, SavedObjectsClientContract } from 'src/core/server';
88
import semver from 'semver';
99
import { PACKAGES_SAVED_OBJECT_TYPE, MAX_TIME_COMPLETE_INSTALL } from '../../../constants';
1010
import {
@@ -16,6 +16,7 @@ import {
1616
KibanaAssetReference,
1717
EsAssetReference,
1818
ElasticsearchAssetType,
19+
InstallType,
1920
} from '../../../types';
2021
import { installIndexPatterns } from '../kibana/index_pattern/install';
2122
import * as Registry from '../registry';
@@ -110,11 +111,13 @@ export async function installPackage({
110111
const latestPackage = await Registry.fetchFindLatestPackage(pkgName);
111112
// get the currently installed package
112113
const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName });
113-
const reinstall = pkgVersion === installedPkg?.attributes.version;
114-
const reupdate = pkgVersion === installedPkg?.attributes.install_version;
115114

116-
// let the user install if using the force flag or this is a reinstall or reupdate due to intallation interruption
117-
if (semver.lt(pkgVersion, latestPackage.version) && !force && !reinstall && !reupdate) {
115+
const installType = getInstallType({ pkgVersion, installedPkg });
116+
117+
// let the user install if using the force flag or needing to reinstall or install a previous version due to failed update
118+
const installOutOfDateVersionOk =
119+
installType === 'reinstall' || installType === 'reupdate' || installType === 'rollback';
120+
if (semver.lt(pkgVersion, latestPackage.version) && !force && !installOutOfDateVersionOk) {
118121
throw new PackageOutdatedError(`${pkgkey} is out-of-date and cannot be installed or updated`);
119122
}
120123
const paths = await Registry.getArchiveInfo(pkgName, pkgVersion);
@@ -188,16 +191,26 @@ export async function installPackage({
188191
// update current backing indices of each data stream
189192
await updateCurrentWriteIndices(callCluster, installedTemplates);
190193

191-
// if this is an update, delete the previous version's pipelines
192-
if (installedPkg && !reinstall) {
194+
// if this is an update or retrying an update, delete the previous version's pipelines
195+
if (installType === 'update' || installType === 'reupdate') {
193196
await deletePreviousPipelines(
194197
callCluster,
195198
savedObjectsClient,
196199
pkgName,
200+
// @ts-ignore installType conditions already check for existence of installedPkg
197201
installedPkg.attributes.version
198202
);
199203
}
200-
204+
// pipelines from a different version may have installed during a failed update
205+
if (installType === 'rollback') {
206+
await deletePreviousPipelines(
207+
callCluster,
208+
savedObjectsClient,
209+
pkgName,
210+
// @ts-ignore installType conditions already check for existence of installedPkg
211+
installedPkg.attributes.install_version
212+
);
213+
}
201214
const installedTemplateRefs = installedTemplates.map((template) => ({
202215
id: template.templateName,
203216
type: ElasticsearchAssetType.indexTemplate,
@@ -326,3 +339,23 @@ export async function ensurePackagesCompletedInstall(
326339
await Promise.all(installingPromises);
327340
return installingPackages;
328341
}
342+
343+
export function getInstallType({
344+
pkgVersion,
345+
installedPkg,
346+
}: {
347+
pkgVersion: string;
348+
installedPkg: SavedObject<Installation> | undefined;
349+
}): InstallType {
350+
const isInstalledPkg = !!installedPkg;
351+
const currentPkgVersion = installedPkg?.attributes.version;
352+
const lastStartedInstallVersion = installedPkg?.attributes.install_version;
353+
if (!isInstalledPkg) return 'install';
354+
if (pkgVersion === currentPkgVersion && pkgVersion !== lastStartedInstallVersion)
355+
return 'rollback';
356+
if (pkgVersion === currentPkgVersion) return 'reinstall';
357+
if (pkgVersion === lastStartedInstallVersion && pkgVersion !== currentPkgVersion)
358+
return 'reupdate';
359+
if (pkgVersion !== lastStartedInstallVersion && pkgVersion !== currentPkgVersion) return 'update';
360+
throw new Error('unknown install type');
361+
}

x-pack/plugins/ingest_manager/server/types/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export {
6363
IndexTemplateMappings,
6464
Settings,
6565
SettingsSOAttributes,
66+
InstallType,
6667
// Agent Request types
6768
PostAgentEnrollRequest,
6869
PostAgentCheckinRequest,

0 commit comments

Comments
 (0)