[Ingest Manager] improve error handling of package install (#73728)

* refactor to add assets refs to SO before install

* get ingest pipeline refs by looping through datasets because names are diff than path dir

* fix bug and improve error handling

* bump package number from merge conflict

* add integration test for the epm-package saved object

* accidentally pasted line of code

* rename errors for consistency

* pass custom error when IngestManagerError

* rename package from outdated to update
This commit is contained in:
Sandra Gonzales 2020-08-03 10:57:32 -05:00 committed by GitHub
parent b9e5ae9c77
commit 5cabe9c95d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 343 additions and 101 deletions

View file

@ -15,9 +15,17 @@ export class IngestManagerError extends Error {
export const getHTTPResponseCode = (error: IngestManagerError): number => {
if (error instanceof RegistryError) {
return 502; // Bad Gateway
}
if (error instanceof PackageNotFoundError) {
return 404;
}
if (error instanceof PackageOutdatedError) {
return 400;
} else {
return 400; // Bad Request
}
};
export class RegistryError extends IngestManagerError {}
export class PackageNotFoundError extends IngestManagerError {}
export class PackageOutdatedError extends IngestManagerError {}

View file

@ -32,6 +32,7 @@ import {
getLimitedPackages,
getInstallationObject,
} from '../../services/epm/packages';
import { IngestManagerError, getHTTPResponseCode } from '../../errors';
export const getCategoriesHandler: RequestHandler<
undefined,
@ -165,23 +166,25 @@ export const installPackageHandler: RequestHandler<TypeOf<
};
return response.ok({ body });
} catch (e) {
if (e instanceof IngestManagerError) {
logger.error(e);
return response.customError({
statusCode: getHTTPResponseCode(e),
body: { message: e.message },
});
}
// if there is an unknown server error, uninstall any package assets
try {
const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName });
const isUpdate = installedPkg && installedPkg.attributes.version < pkgVersion ? true : false;
// if this is a failed install, remove any assets installed
if (!isUpdate) {
await removeInstallation({ savedObjectsClient, pkgkey, callCluster });
}
} catch (error) {
logger.error(`could not remove assets from failed installation attempt for ${pkgkey}`);
}
if (e.isBoom) {
return response.customError({
statusCode: e.output.statusCode,
body: { message: e.output.payload.message },
});
logger.error(`could not remove failed installation ${error}`);
}
logger.error(e);
return response.customError({
statusCode: 500,
body: { message: e.message },

View file

@ -31,25 +31,38 @@ export const installPipelines = async (
// it can be created pointing to the new template, without removing the old one and effecting data
// so do not remove the currently installed pipelines here
const datasets = registryPackage.datasets;
if (!datasets?.length) return [];
const pipelinePaths = paths.filter((path) => isPipeline(path));
if (datasets) {
const pipelines = datasets.reduce<Array<Promise<EsAssetReference[]>>>((acc, dataset) => {
if (dataset.ingest_pipeline) {
acc.push(
installPipelinesForDataset({
dataset,
callCluster,
paths: pipelinePaths,
pkgVersion: registryPackage.version,
})
);
}
return acc;
}, []);
const pipelinesToSave = await Promise.all(pipelines).then((results) => results.flat());
return saveInstalledEsRefs(savedObjectsClient, registryPackage.name, pipelinesToSave);
}
return [];
// get and save pipeline refs before installing pipelines
const pipelineRefs = datasets.reduce<EsAssetReference[]>((acc, dataset) => {
const filteredPaths = pipelinePaths.filter((path) => isDatasetPipeline(path, dataset.path));
const pipelineObjectRefs = filteredPaths.map((path) => {
const { name } = getNameAndExtension(path);
const nameForInstallation = getPipelineNameForInstallation({
pipelineName: name,
dataset,
packageVersion: registryPackage.version,
});
return { id: nameForInstallation, type: ElasticsearchAssetType.ingestPipeline };
});
acc.push(...pipelineObjectRefs);
return acc;
}, []);
await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, pipelineRefs);
const pipelines = datasets.reduce<Array<Promise<EsAssetReference[]>>>((acc, dataset) => {
if (dataset.ingest_pipeline) {
acc.push(
installPipelinesForDataset({
dataset,
callCluster,
paths: pipelinePaths,
pkgVersion: registryPackage.version,
})
);
}
return acc;
}, []);
return await Promise.all(pipelines).then((results) => results.flat());
};
export function rewriteIngestPipeline(

View file

@ -41,6 +41,16 @@ export const installTemplates = async (
);
// build templates per dataset from yml files
const datasets = registryPackage.datasets;
if (!datasets) return [];
// get template refs to save
const installedTemplateRefs = datasets.map((dataset) => ({
id: generateTemplateName(dataset),
type: ElasticsearchAssetType.indexTemplate,
}));
// add package installation's references to index templates
await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, installedTemplateRefs);
if (datasets) {
const installTemplatePromises = datasets.reduce<Array<Promise<TemplateRef>>>((acc, dataset) => {
acc.push(
@ -55,14 +65,6 @@ export const installTemplates = async (
const res = await Promise.all(installTemplatePromises);
const installedTemplates = res.flat();
// get template refs to save
const installedTemplateRefs = installedTemplates.map((template) => ({
id: template.templateName,
type: ElasticsearchAssetType.indexTemplate,
}));
// add package installation's references to index templates
await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, installedTemplateRefs);
return installedTemplates;
}

View file

@ -11,14 +11,8 @@ import {
} from 'src/core/server';
import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common';
import * as Registry from '../../registry';
import {
AssetType,
KibanaAssetType,
AssetReference,
KibanaAssetReference,
} from '../../../../types';
import { deleteKibanaSavedObjectsAssets } from '../../packages/remove';
import { getInstallationObject, savedObjectTypes } from '../../packages';
import { AssetType, KibanaAssetType, AssetReference } from '../../../../types';
import { savedObjectTypes } from '../../packages';
type SavedObjectToBe = Required<SavedObjectsBulkCreateObject> & { type: AssetType };
export type ArchiveAsset = Pick<
@ -28,7 +22,7 @@ export type ArchiveAsset = Pick<
type: AssetType;
};
export async function getKibanaAsset(key: string) {
export async function getKibanaAsset(key: string): Promise<ArchiveAsset> {
const buffer = Registry.getAsset(key);
// cache values are buffers. convert to string / JSON
@ -51,31 +45,18 @@ export function createSavedObjectKibanaAsset(asset: ArchiveAsset): SavedObjectTo
export async function installKibanaAssets(options: {
savedObjectsClient: SavedObjectsClientContract;
pkgName: string;
paths: string[];
kibanaAssets: ArchiveAsset[];
isUpdate: boolean;
}): Promise<KibanaAssetReference[]> {
const { savedObjectsClient, paths, pkgName, isUpdate } = options;
}): Promise<SavedObject[]> {
const { savedObjectsClient, kibanaAssets } = options;
if (isUpdate) {
// delete currently installed kibana saved objects and installation references
const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName });
const installedKibanaRefs = installedPkg?.attributes.installed_kibana;
if (installedKibanaRefs?.length) {
await deleteKibanaSavedObjectsAssets(savedObjectsClient, installedKibanaRefs);
await deleteKibanaInstalledRefs(savedObjectsClient, pkgName, installedKibanaRefs);
}
}
// install the new assets and save installation references
// install the assets
const kibanaAssetTypes = Object.values(KibanaAssetType);
const installedAssets = await Promise.all(
kibanaAssetTypes.map((assetType) =>
installKibanaSavedObjects({ savedObjectsClient, assetType, paths })
installKibanaSavedObjects({ savedObjectsClient, assetType, kibanaAssets })
)
);
// installKibanaSavedObjects returns AssetReference[], so .map creates AssetReference[][]
// call .flat to flatten into one dimensional array
return installedAssets.flat();
}
export const deleteKibanaInstalledRefs = async (
@ -92,21 +73,25 @@ export const deleteKibanaInstalledRefs = async (
installed_kibana: installedAssetsToSave,
});
};
export async function getKibanaAssets(paths: string[]) {
const isKibanaAssetType = (path: string) => Registry.pathParts(path).type in KibanaAssetType;
const filteredPaths = paths.filter(isKibanaAssetType);
const kibanaAssets = await Promise.all(filteredPaths.map((path) => getKibanaAsset(path)));
return kibanaAssets;
}
async function installKibanaSavedObjects({
savedObjectsClient,
assetType,
paths,
kibanaAssets,
}: {
savedObjectsClient: SavedObjectsClientContract;
assetType: KibanaAssetType;
paths: string[];
kibanaAssets: ArchiveAsset[];
}) {
const isSameType = (path: string) => assetType === Registry.pathParts(path).type;
const pathsOfType = paths.filter((path) => isSameType(path));
const kibanaAssets = await Promise.all(pathsOfType.map((path) => getKibanaAsset(path)));
const isSameType = (asset: ArchiveAsset) => assetType === asset.type;
const filteredKibanaAssets = kibanaAssets.filter((asset) => isSameType(asset));
const toBeSavedObjects = await Promise.all(
kibanaAssets.map((asset) => createSavedObjectKibanaAsset(asset))
filteredKibanaAssets.map((asset) => createSavedObjectKibanaAsset(asset))
);
if (toBeSavedObjects.length === 0) {
@ -115,13 +100,11 @@ async function installKibanaSavedObjects({
const createResults = await savedObjectsClient.bulkCreate(toBeSavedObjects, {
overwrite: true,
});
const createdObjects = createResults.saved_objects;
const installed = createdObjects.map(toAssetReference);
return installed;
return createResults.saved_objects;
}
}
function toAssetReference({ id, type }: SavedObject) {
export function toAssetReference({ id, type }: SavedObject) {
const reference: AssetReference = { id, type: type as KibanaAssetType };
return reference;

View file

@ -5,7 +5,6 @@
*/
import { SavedObjectsClientContract } from 'src/core/server';
import Boom from 'boom';
import semver from 'semver';
import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants';
import {
@ -25,8 +24,15 @@ import { installTemplates } from '../elasticsearch/template/install';
import { generateESIndexPatterns } from '../elasticsearch/template/template';
import { installPipelines, deletePipelines } from '../elasticsearch/ingest_pipeline/';
import { installILMPolicy } from '../elasticsearch/ilm/install';
import { installKibanaAssets } from '../kibana/assets/install';
import {
installKibanaAssets,
getKibanaAssets,
toAssetReference,
ArchiveAsset,
} from '../kibana/assets/install';
import { updateCurrentWriteIndices } from '../elasticsearch/template/template';
import { deleteKibanaSavedObjectsAssets } from './remove';
import { PackageOutdatedError } from '../../../errors';
export async function installLatestPackage(options: {
savedObjectsClient: SavedObjectsClientContract;
@ -97,7 +103,7 @@ export async function installPackage(options: {
// and be replaced by getPackageInfo after adjusting for it to not group/use archive assets
const latestPackage = await Registry.fetchFindLatestPackage(pkgName);
if (semver.lt(pkgVersion, latestPackage.version))
throw Boom.badRequest('Cannot install or update to an out-of-date package');
throw new PackageOutdatedError(`${pkgkey} is out-of-date and cannot be installed or updated`);
const paths = await Registry.getArchiveInfo(pkgName, pkgVersion);
const registryPackageInfo = await Registry.fetchInfo(pkgName, pkgVersion);
@ -124,12 +130,23 @@ export async function installPackage(options: {
toSaveESIndexPatterns,
});
}
const installIndexPatternPromise = installIndexPatterns(savedObjectsClient, pkgName, pkgVersion);
const kibanaAssets = await getKibanaAssets(paths);
if (installedPkg)
await deleteKibanaSavedObjectsAssets(
savedObjectsClient,
installedPkg.attributes.installed_kibana
);
// save new kibana refs before installing the assets
const installedKibanaAssetsRefs = await saveKibanaAssetsRefs(
savedObjectsClient,
pkgName,
kibanaAssets
);
const installKibanaAssetsPromise = installKibanaAssets({
savedObjectsClient,
pkgName,
paths,
kibanaAssets,
isUpdate,
});
@ -169,21 +186,14 @@ export async function installPackage(options: {
);
}
// get template refs to save
const installedTemplateRefs = installedTemplates.map((template) => ({
id: template.templateName,
type: ElasticsearchAssetType.indexTemplate,
}));
const [installedKibanaAssets] = await Promise.all([
installKibanaAssetsPromise,
installIndexPatternPromise,
]);
await saveInstalledKibanaRefs(savedObjectsClient, pkgName, installedKibanaAssets);
await Promise.all([installKibanaAssetsPromise, installIndexPatternPromise]);
// update to newly installed version when all assets are successfully installed
if (isUpdate) await updateVersion(savedObjectsClient, pkgName, pkgVersion);
return [...installedKibanaAssets, ...installedPipelines, ...installedTemplateRefs];
return [...installedKibanaAssetsRefs, ...installedPipelines, ...installedTemplateRefs];
}
const updateVersion = async (
savedObjectsClient: SavedObjectsClientContract,
@ -230,15 +240,16 @@ export async function createInstallation(options: {
return [...installedKibana, ...installedEs];
}
export const saveInstalledKibanaRefs = async (
export const saveKibanaAssetsRefs = async (
savedObjectsClient: SavedObjectsClientContract,
pkgName: string,
installedAssets: KibanaAssetReference[]
kibanaAssets: ArchiveAsset[]
) => {
const assetRefs = kibanaAssets.map(toAssetReference);
await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, {
installed_kibana: installedAssets,
installed_kibana: assetRefs,
});
return installedAssets;
return assetRefs;
};
export const saveInstalledEsRefs = async (

View file

@ -102,10 +102,12 @@ async function deleteTemplate(callCluster: CallESAsCurrentUser, name: string): P
export async function deleteKibanaSavedObjectsAssets(
savedObjectsClient: SavedObjectsClientContract,
installedObjects: AssetReference[]
installedRefs: AssetReference[]
) {
if (!installedRefs.length) return;
const logger = appContextService.getLogger();
const deletePromises = installedObjects.map(({ id, type }) => {
const deletePromises = installedRefs.map(({ id, type }) => {
const assetType = type as AssetType;
if (savedObjectTypes.includes(assetType)) {

View file

@ -22,6 +22,7 @@ import { fetchUrl, getResponse, getResponseStream } from './requests';
import { streamToBuffer } from './streams';
import { getRegistryUrl } from './registry_url';
import { appContextService } from '../..';
import { PackageNotFoundError } from '../../../errors';
export { ArchiveEntry } from './extract';
@ -76,7 +77,7 @@ export async function fetchFindLatestPackage(packageName: string): Promise<Regis
if (searchResults.length) {
return searchResults[0];
} else {
throw new Error('package not found');
throw new PackageNotFoundError(`${packageName} not found`);
}
}

View file

@ -0,0 +1,17 @@
/*
* 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.
*/
export default function loadTests({ loadTestFile }) {
describe('EPM Endpoints', () => {
loadTestFile(require.resolve('./list'));
loadTestFile(require.resolve('./file'));
//loadTestFile(require.resolve('./template'));
loadTestFile(require.resolve('./ilm'));
loadTestFile(require.resolve('./install_overrides'));
loadTestFile(require.resolve('./install_remove_assets'));
loadTestFile(require.resolve('./install_errors'));
});
}

View file

@ -0,0 +1,51 @@
/*
* 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';
export default function (providerContext: FtrProviderContext) {
const { getService } = providerContext;
const kibanaServer = getService('kibanaServer');
const supertest = getService('supertest');
describe('package error handling', async () => {
skipIfNoDockerRegistry(providerContext);
it('should return 404 if package does not exist', async function () {
await supertest
.post(`/api/ingest_manager/epm/packages/nonexistent-0.1.0`)
.set('kbn-xsrf', 'xxxx')
.expect(404);
let res;
try {
res = await kibanaServer.savedObjects.get({
type: 'epm-package',
id: 'nonexistent',
});
} catch (err) {
res = err;
}
expect(res.response.data.statusCode).equal(404);
});
it('should return 400 if trying to update/install to an out-of-date package', async function () {
await supertest
.post(`/api/ingest_manager/epm/packages/update-0.1.0`)
.set('kbn-xsrf', 'xxxx')
.expect(400);
let res;
try {
res = await kibanaServer.savedObjects.get({
type: 'epm-package',
id: 'update',
});
} catch (err) {
res = err;
}
expect(res.response.data.statusCode).equal(404);
});
});
}

View file

@ -108,6 +108,54 @@ export default function (providerContext: FtrProviderContext) {
});
expect(resSearch.id).equal('sample_search');
});
it('should have created the correct saved object', async function () {
const res = await kibanaServer.savedObjects.get({
type: 'epm-packages',
id: 'all_assets',
});
expect(res.attributes).eql({
installed_kibana: [
{
id: 'sample_dashboard',
type: 'dashboard',
},
{
id: 'sample_dashboard2',
type: 'dashboard',
},
{
id: 'sample_search',
type: 'search',
},
{
id: 'sample_visualization',
type: 'visualization',
},
],
installed_es: [
{
id: 'logs-all_assets.test_logs-0.1.0',
type: 'ingest_pipeline',
},
{
id: 'logs-all_assets.test_logs',
type: 'index_template',
},
{
id: 'metrics-all_assets.test_metrics',
type: 'index_template',
},
],
es_index_patterns: {
test_logs: 'logs-all_assets.test_logs-*',
test_metrics: 'metrics-all_assets.test_metrics-*',
},
name: 'all_assets',
version: '0.1.0',
internal: false,
removable: true,
});
});
});
describe('uninstalls all assets when uninstalling a package', async () => {
@ -192,6 +240,18 @@ export default function (providerContext: FtrProviderContext) {
}
expect(resSearch.response.data.statusCode).equal(404);
});
it('should have removed the saved object', async function () {
let res;
try {
res = await kibanaServer.savedObjects.get({
type: 'epm-packages',
id: 'all_assets',
});
} catch (err) {
res = err;
}
expect(res.response.data.statusCode).equal(404);
});
});
});
}

View file

@ -29,7 +29,7 @@ export default function ({ getService }: FtrProviderContext) {
return response.body;
};
const listResponse = await fetchPackageList();
expect(listResponse.response.length).to.be(13);
expect(listResponse.response.length).to.be(14);
} else {
warnAndSkipTest(this, log);
}

View file

@ -0,0 +1,16 @@
- name: dataset.type
type: constant_keyword
description: >
Dataset type.
- name: dataset.name
type: constant_keyword
description: >
Dataset name.
- name: dataset.namespace
type: constant_keyword
description: >
Dataset namespace.
- name: '@timestamp'
type: date
description: >
Event timestamp.

View file

@ -0,0 +1,9 @@
title: Test Dataset
type: logs
elasticsearch:
index_template.mappings:
dynamic: false
index_template.settings:
index.lifecycle.name: reference

View file

@ -0,0 +1,3 @@
# Test package
This is a test package for testing installing or updating to an out-of-date package

View file

@ -0,0 +1,20 @@
format_version: 1.0.0
name: update
title: Package update test
description: This is a test package for updating a package
version: 0.1.0
categories: []
release: beta
type: integration
license: basic
requirement:
elasticsearch:
versions: '>7.7.0'
kibana:
versions: '>7.7.0'
icons:
- src: '/img/logo_overrides_64_color.svg'
size: '16x16'
type: 'image/svg+xml'

View file

@ -0,0 +1,16 @@
- name: dataset.type
type: constant_keyword
description: >
Dataset type.
- name: dataset.name
type: constant_keyword
description: >
Dataset name.
- name: dataset.namespace
type: constant_keyword
description: >
Dataset namespace.
- name: '@timestamp'
type: date
description: >
Event timestamp.

View file

@ -0,0 +1,9 @@
title: Test Dataset
type: logs
elasticsearch:
index_template.mappings:
dynamic: false
index_template.settings:
index.lifecycle.name: reference

View file

@ -0,0 +1,3 @@
# Test package
This is a test package for testing installing or updating to an out-of-date package

View file

@ -0,0 +1,20 @@
format_version: 1.0.0
name: update
title: Package update test
description: This is a test package for updating a package
version: 0.2.0
categories: []
release: beta
type: integration
license: basic
requirement:
elasticsearch:
versions: '>7.7.0'
kibana:
versions: '>7.7.0'
icons:
- src: '/img/logo_overrides_64_color.svg'
size: '16x16'
type: 'image/svg+xml'

View file

@ -12,12 +12,7 @@ export default function ({ loadTestFile }) {
loadTestFile(require.resolve('./fleet/index'));
// EPM
loadTestFile(require.resolve('./epm/list'));
loadTestFile(require.resolve('./epm/file'));
//loadTestFile(require.resolve('./epm/template'));
loadTestFile(require.resolve('./epm/ilm'));
loadTestFile(require.resolve('./epm/install_overrides'));
loadTestFile(require.resolve('./epm/install_remove_assets'));
loadTestFile(require.resolve('./epm/index'));
// Package configs
loadTestFile(require.resolve('./package_config/create'));