[Fleet] catching only mapper errors (#167044)

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Julia Bardi 2023-09-26 17:00:54 +02:00 committed by GitHub
parent 01f4d61d00
commit 08d44fe52b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 447 additions and 16 deletions

View file

@ -572,6 +572,24 @@
"parameters": [
{
"$ref": "#/components/parameters/kbn_xsrf"
},
{
"in": "query",
"name": "ignoreMappingUpdateErrors",
"schema": {
"type": "boolean",
"default": false
},
"description": "avoid erroring out on unexpected mapping update errors"
},
{
"in": "query",
"name": "skipDataStreamRollover",
"schema": {
"type": "boolean",
"default": false
},
"description": "skip data stream rollover during index template mapping or settings update"
}
],
"requestBody": {
@ -810,6 +828,24 @@
"name": "pkgkey",
"in": "path",
"required": true
},
{
"in": "query",
"name": "ignoreMappingUpdateErrors",
"schema": {
"type": "boolean",
"default": false
},
"description": "avoid erroring out on unexpected mapping update errors"
},
{
"in": "query",
"name": "skipDataStreamRollover",
"schema": {
"type": "boolean",
"default": false
},
"description": "skip data stream rollover during index template mapping or settings update"
}
],
"requestBody": {
@ -1090,6 +1126,24 @@
"parameters": [
{
"$ref": "#/components/parameters/kbn_xsrf"
},
{
"in": "query",
"name": "ignoreMappingUpdateErrors",
"schema": {
"type": "boolean",
"default": false
},
"description": "avoid erroring out on unexpected mapping update errors"
},
{
"in": "query",
"name": "skipDataStreamRollover",
"schema": {
"type": "boolean",
"default": false
},
"description": "skip data stream rollover during index template mapping or settings update"
}
],
"requestBody": {

View file

@ -368,6 +368,20 @@ paths:
description: ''
parameters:
- $ref: '#/components/parameters/kbn_xsrf'
- in: query
name: ignoreMappingUpdateErrors
schema:
type: boolean
default: false
description: avoid erroring out on unexpected mapping update errors
- in: query
name: skipDataStreamRollover
schema:
type: boolean
default: false
description: >-
skip data stream rollover during index template mapping or settings
update
requestBody:
content:
application/zip:
@ -516,6 +530,20 @@ paths:
name: pkgkey
in: path
required: true
- in: query
name: ignoreMappingUpdateErrors
schema:
type: boolean
default: false
description: avoid erroring out on unexpected mapping update errors
- in: query
name: skipDataStreamRollover
schema:
type: boolean
default: false
description: >-
skip data stream rollover during index template mapping or settings
update
requestBody:
content:
application/json:
@ -689,6 +717,20 @@ paths:
description: ''
parameters:
- $ref: '#/components/parameters/kbn_xsrf'
- in: query
name: ignoreMappingUpdateErrors
schema:
type: boolean
default: false
description: avoid erroring out on unexpected mapping update errors
- in: query
name: skipDataStreamRollover
schema:
type: boolean
default: false
description: >-
skip data stream rollover during index template mapping or settings
update
requestBody:
content:
application/json:

View file

@ -83,6 +83,18 @@ post:
description: ''
parameters:
- $ref: ../components/headers/kbn_xsrf.yaml
- in: query
name: ignoreMappingUpdateErrors
schema:
type: boolean
default: false
description: avoid erroring out on unexpected mapping update errors
- in: query
name: skipDataStreamRollover
schema:
type: boolean
default: false
description: skip data stream rollover during index template mapping or settings update
requestBody:
content:
application/zip:

View file

@ -111,6 +111,18 @@ post:
description: ''
parameters:
- $ref: ../components/headers/kbn_xsrf.yaml
- in: query
name: ignoreMappingUpdateErrors
schema:
type: boolean
default: false
description: avoid erroring out on unexpected mapping update errors
- in: query
name: skipDataStreamRollover
schema:
type: boolean
default: false
description: skip data stream rollover during index template mapping or settings update
requestBody:
content:
application/json:

View file

@ -84,6 +84,18 @@ post:
name: pkgkey
in: path
required: true
- in: query
name: ignoreMappingUpdateErrors
schema:
type: boolean
default: false
description: avoid erroring out on unexpected mapping update errors
- in: query
name: skipDataStreamRollover
schema:
type: boolean
default: false
description: skip data stream rollover during index template mapping or settings update
requestBody:
content:
application/json:

View file

@ -392,6 +392,8 @@ export const installPackageFromRegistryHandler: FleetRequestHandler<
ignoreConstraints: request.body?.ignore_constraints,
prerelease: request.query?.prerelease,
authorizationHeader,
ignoreMappingUpdateErrors: request.query?.ignoreMappingUpdateErrors,
skipDataStreamRollover: request.query?.skipDataStreamRollover,
});
if (!res.error) {
@ -509,7 +511,7 @@ export const bulkInstallPackagesFromRegistryHandler: FleetRequestHandler<
export const installPackageByUploadHandler: FleetRequestHandler<
undefined,
undefined,
TypeOf<typeof InstallPackageByUploadRequestSchema.query>,
TypeOf<typeof InstallPackageByUploadRequestSchema.body>
> = async (context, request, response) => {
const coreContext = await context.core;
@ -531,6 +533,8 @@ export const installPackageByUploadHandler: FleetRequestHandler<
spaceId,
contentType,
authorizationHeader,
ignoreMappingUpdateErrors: request.query?.ignoreMappingUpdateErrors,
skipDataStreamRollover: request.query?.skipDataStreamRollover,
});
if (!res.error) {
const body: InstallPackageResponse = {

View file

@ -12,6 +12,8 @@ import { safeLoad } from 'js-yaml';
import { loggerMock } from '@kbn/logging-mocks';
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
import { errors } from '@elastic/elasticsearch';
import { createAppContextStartContractMock } from '../../../../mocks';
import { appContextService } from '../../..';
import type { RegistryDataStream } from '../../../../types';
@ -1261,5 +1263,134 @@ describe('EPM template', () => {
},
});
});
it('should rollover on expected error', async () => {
const esClient = elasticsearchServiceMock.createElasticsearchClient();
esClient.indices.getDataStream.mockResponse({
data_streams: [{ name: 'test.prefix1-default' }],
} as any);
esClient.indices.simulateTemplate.mockImplementation(() => {
throw new errors.ResponseError({
statusCode: 400,
body: {
error: {
type: 'illegal_argument_exception',
},
},
} as any);
});
const logger = loggerMock.create();
await updateCurrentWriteIndices(esClient, logger, [
{
templateName: 'test',
indexTemplate: {
index_patterns: ['test.*-*'],
template: {
settings: { index: {} },
mappings: { properties: {} },
},
} as any,
},
]);
expect(esClient.indices.rollover).toHaveBeenCalled();
});
it('should skip rollover on expected error when flag is on', async () => {
const esClient = elasticsearchServiceMock.createElasticsearchClient();
esClient.indices.getDataStream.mockResponse({
data_streams: [{ name: 'test.prefix1-default' }],
} as any);
esClient.indices.simulateTemplate.mockImplementation(() => {
throw new errors.ResponseError({
statusCode: 400,
body: {
error: {
type: 'illegal_argument_exception',
},
},
} as any);
});
const logger = loggerMock.create();
await updateCurrentWriteIndices(
esClient,
logger,
[
{
templateName: 'test',
indexTemplate: {
index_patterns: ['test.*-*'],
template: {
settings: { index: {} },
mappings: { properties: {} },
},
} as any,
},
],
{
skipDataStreamRollover: true,
}
);
expect(esClient.indices.rollover).not.toHaveBeenCalled();
});
it('should not rollover on unexpected error', async () => {
const esClient = elasticsearchServiceMock.createElasticsearchClient();
esClient.indices.getDataStream.mockResponse({
data_streams: [{ name: 'test.prefix1-default' }],
} as any);
esClient.indices.simulateTemplate.mockImplementation(() => {
throw new Error();
});
const logger = loggerMock.create();
try {
await updateCurrentWriteIndices(esClient, logger, [
{
templateName: 'test',
indexTemplate: {
index_patterns: ['test.*-*'],
template: {
settings: { index: {} },
mappings: { properties: {} },
},
} as any,
},
]);
fail('expected updateCurrentWriteIndices to throw error');
} catch (err) {
// noop
}
expect(esClient.indices.rollover).not.toHaveBeenCalled();
});
it('should not throw on unexpected error when flag is on', async () => {
const esClient = elasticsearchServiceMock.createElasticsearchClient();
esClient.indices.getDataStream.mockResponse({
data_streams: [{ name: 'test.prefix1-default' }],
} as any);
esClient.indices.simulateTemplate.mockImplementation(() => {
throw new Error();
});
const logger = loggerMock.create();
await updateCurrentWriteIndices(
esClient,
logger,
[
{
templateName: 'test',
indexTemplate: {
index_patterns: ['test.*-*'],
template: {
settings: { index: {} },
mappings: { properties: {} },
},
} as any,
},
],
{
ignoreMappingUpdateErrors: true,
}
);
expect(esClient.indices.rollover).not.toHaveBeenCalled();
});
});
});

View file

@ -12,6 +12,7 @@ import type {
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import pMap from 'p-map';
import { isResponseError } from '@kbn/es-errors';
import type { Field, Fields } from '../../fields/field';
import type {
@ -662,7 +663,11 @@ function getBaseTemplate({
export const updateCurrentWriteIndices = async (
esClient: ElasticsearchClient,
logger: Logger,
templates: IndexTemplateEntry[]
templates: IndexTemplateEntry[],
options?: {
ignoreMappingUpdateErrors?: boolean;
skipDataStreamRollover?: boolean;
}
): Promise<void> => {
if (!templates.length) return;
@ -677,7 +682,7 @@ export const updateCurrentWriteIndices = async (
return true;
});
if (!allUpdatablesIndices.length) return;
return updateAllDataStreams(allUpdatablesIndices, esClient, logger);
return updateAllDataStreams(allUpdatablesIndices, esClient, logger, options);
};
function isCurrentDataStream(item: CurrentDataStream[] | undefined): item is CurrentDataStream[] {
@ -729,7 +734,11 @@ const rolloverDataStream = (dataStreamName: string, esClient: ElasticsearchClien
const updateAllDataStreams = async (
indexNameWithTemplates: CurrentDataStream[],
esClient: ElasticsearchClient,
logger: Logger
logger: Logger,
options?: {
ignoreMappingUpdateErrors?: boolean;
skipDataStreamRollover?: boolean;
}
): Promise<void> => {
await pMap(
indexNameWithTemplates,
@ -738,6 +747,7 @@ const updateAllDataStreams = async (
esClient,
logger,
dataStreamName: templateEntry.dataStreamName,
options,
});
},
{
@ -751,10 +761,15 @@ const updateExistingDataStream = async ({
dataStreamName,
esClient,
logger,
options,
}: {
dataStreamName: string;
esClient: ElasticsearchClient;
logger: Logger;
options?: {
ignoreMappingUpdateErrors?: boolean;
skipDataStreamRollover?: boolean;
};
}) => {
const existingDs = await esClient.indices.get({
index: dataStreamName,
@ -804,18 +819,45 @@ const updateExistingDataStream = async ({
// if update fails, rollover data stream and bail out
} catch (err) {
logger.info(`Mappings update for ${dataStreamName} failed due to ${err}`);
logger.info(`Triggering a rollover for ${dataStreamName}`);
await rolloverDataStream(dataStreamName, esClient);
return;
if (
isResponseError(err) &&
err.statusCode === 400 &&
err.body?.error?.type === 'illegal_argument_exception'
) {
logger.info(`Mappings update for ${dataStreamName} failed due to ${err}`);
if (options?.skipDataStreamRollover === true) {
logger.info(
`Skipping rollover for ${dataStreamName} as "skipDataStreamRollover" is enabled`
);
return;
} else {
logger.info(`Triggering a rollover for ${dataStreamName}`);
await rolloverDataStream(dataStreamName, esClient);
return;
}
}
logger.error(`Mappings update for ${dataStreamName} failed due to unexpected error: ${err}`);
if (options?.ignoreMappingUpdateErrors === true) {
logger.info(`Ignore mapping update errors as "ignoreMappingUpdateErrors" is enabled`);
return;
} else {
throw err;
}
}
// Trigger a rollover if the index mode or source type has changed
if (currentIndexMode !== settings?.index?.mode || currentSourceType !== mappings?._source?.mode) {
logger.info(
`Index mode or source type has changed for ${dataStreamName}, triggering a rollover`
);
await rolloverDataStream(dataStreamName, esClient);
if (options?.skipDataStreamRollover === true) {
logger.info(
`Index mode or source type has changed for ${dataStreamName}, skipping rollover as "skipDataStreamRollover" is enabled`
);
return;
} else {
logger.info(
`Index mode or source type has changed for ${dataStreamName}, triggering a rollover`
);
await rolloverDataStream(dataStreamName, esClient);
}
}
if (lifecycle?.data_retention) {

View file

@ -77,6 +77,8 @@ export async function _installPackage({
force,
verificationResult,
authorizationHeader,
ignoreMappingUpdateErrors,
skipDataStreamRollover,
}: {
savedObjectsClient: SavedObjectsClientContract;
savedObjectsImporter: Pick<ISavedObjectsImporter, 'import' | 'resolveImportErrors'>;
@ -93,6 +95,8 @@ export async function _installPackage({
force?: boolean;
verificationResult?: PackageVerificationResult;
authorizationHeader?: HTTPAuthorizationHeader | null;
ignoreMappingUpdateErrors?: boolean;
skipDataStreamRollover?: boolean;
}): Promise<AssetReference[]> {
const { name: pkgName, version: pkgVersion, title: pkgTitle } = packageInfo;
@ -250,7 +254,10 @@ export async function _installPackage({
// update current backing indices of each data stream
await withPackageSpan('Update write indices', () =>
updateCurrentWriteIndices(esClient, logger, indexTemplates)
updateCurrentWriteIndices(esClient, logger, indexTemplates, {
ignoreMappingUpdateErrors,
skipDataStreamRollover,
})
);
({ esReferences } = await withPackageSpan('Install transforms', () =>

View file

@ -300,6 +300,8 @@ interface InstallRegistryPackageParams {
ignoreConstraints?: boolean;
prerelease?: boolean;
authorizationHeader?: HTTPAuthorizationHeader | null;
ignoreMappingUpdateErrors?: boolean;
skipDataStreamRollover?: boolean;
}
export interface CustomPackageDatasetConfiguration {
@ -324,6 +326,8 @@ interface InstallUploadedArchiveParams {
spaceId: string;
version?: string;
authorizationHeader?: HTTPAuthorizationHeader | null;
ignoreMappingUpdateErrors?: boolean;
skipDataStreamRollover?: boolean;
}
function getTelemetryEvent(pkgName: string, pkgVersion: string): PackageUpdateEvent {
@ -356,6 +360,8 @@ async function installPackageFromRegistry({
ignoreConstraints = false,
neverIgnoreVerificationError = false,
prerelease = false,
ignoreMappingUpdateErrors = false,
skipDataStreamRollover = false,
}: InstallRegistryPackageParams): Promise<InstallResult> {
const logger = appContextService.getLogger();
// TODO: change epm API to /packageName/version so we don't need to do this
@ -429,6 +435,8 @@ async function installPackageFromRegistry({
paths,
verificationResult,
authorizationHeader,
ignoreMappingUpdateErrors,
skipDataStreamRollover,
});
} catch (e) {
sendEvent({
@ -464,6 +472,8 @@ async function installPackageCommon(options: {
verificationResult?: PackageVerificationResult;
telemetryEvent?: PackageUpdateEvent;
authorizationHeader?: HTTPAuthorizationHeader | null;
ignoreMappingUpdateErrors?: boolean;
skipDataStreamRollover?: boolean;
}): Promise<InstallResult> {
const {
pkgName,
@ -479,6 +489,8 @@ async function installPackageCommon(options: {
paths,
verificationResult,
authorizationHeader,
ignoreMappingUpdateErrors,
skipDataStreamRollover,
} = options;
let { telemetryEvent } = options;
const logger = appContextService.getLogger();
@ -568,6 +580,8 @@ async function installPackageCommon(options: {
installSource,
authorizationHeader,
force,
ignoreMappingUpdateErrors,
skipDataStreamRollover,
})
.then(async (assets) => {
await removeOldAssets({
@ -622,6 +636,8 @@ async function installPackageByUpload({
spaceId,
version,
authorizationHeader,
ignoreMappingUpdateErrors,
skipDataStreamRollover,
}: InstallUploadedArchiveParams): Promise<InstallResult> {
// if an error happens during getInstallType, report that we don't know
let installType: InstallType = 'unknown';
@ -670,6 +686,8 @@ async function installPackageByUpload({
packageInfo,
paths,
authorizationHeader,
ignoreMappingUpdateErrors,
skipDataStreamRollover,
});
} catch (e) {
return {
@ -703,8 +721,16 @@ export async function installPackage(args: InstallPackageParams): Promise<Instal
const bundledPackages = await getBundledPackages();
if (args.installSource === 'registry') {
const { pkgkey, force, ignoreConstraints, spaceId, neverIgnoreVerificationError, prerelease } =
args;
const {
pkgkey,
force,
ignoreConstraints,
spaceId,
neverIgnoreVerificationError,
prerelease,
ignoreMappingUpdateErrors,
skipDataStreamRollover,
} = args;
const matchingBundledPackage = bundledPackages.find(
(pkg) => Registry.pkgToPkgKey(pkg) === pkgkey
@ -723,6 +749,8 @@ export async function installPackage(args: InstallPackageParams): Promise<Instal
spaceId,
version: matchingBundledPackage.version,
authorizationHeader,
ignoreMappingUpdateErrors,
skipDataStreamRollover,
});
return { ...response, installSource: 'bundled' };
@ -739,10 +767,18 @@ export async function installPackage(args: InstallPackageParams): Promise<Instal
ignoreConstraints,
prerelease,
authorizationHeader,
ignoreMappingUpdateErrors,
skipDataStreamRollover,
});
return response;
} else if (args.installSource === 'upload') {
const { archiveBuffer, contentType, spaceId } = args;
const {
archiveBuffer,
contentType,
spaceId,
ignoreMappingUpdateErrors,
skipDataStreamRollover,
} = args;
const response = await installPackageByUpload({
savedObjectsClient,
esClient,
@ -750,6 +786,8 @@ export async function installPackage(args: InstallPackageParams): Promise<Instal
contentType,
spaceId,
authorizationHeader,
ignoreMappingUpdateErrors,
skipDataStreamRollover,
});
return response;
} else if (args.installSource === 'custom') {

View file

@ -138,6 +138,8 @@ export const InstallPackageFromRegistryRequestSchema = {
}),
query: schema.object({
prerelease: schema.maybe(schema.boolean()),
ignoreMappingUpdateErrors: schema.boolean({ defaultValue: false }),
skipDataStreamRollover: schema.boolean({ defaultValue: false }),
}),
body: schema.nullable(
schema.object({
@ -166,6 +168,8 @@ export const InstallPackageFromRegistryRequestSchemaDeprecated = {
}),
query: schema.object({
prerelease: schema.maybe(schema.boolean()),
ignoreMappingUpdateErrors: schema.boolean({ defaultValue: false }),
skipDataStreamRollover: schema.boolean({ defaultValue: false }),
}),
body: schema.nullable(
schema.object({
@ -191,6 +195,10 @@ export const BulkInstallPackagesFromRegistryRequestSchema = {
};
export const InstallPackageByUploadRequestSchema = {
query: schema.object({
ignoreMappingUpdateErrors: schema.boolean({ defaultValue: false }),
skipDataStreamRollover: schema.boolean({ defaultValue: false }),
}),
body: schema.buffer(),
};

View file

@ -143,5 +143,74 @@ export default function (providerContext: FtrProviderContext) {
// datastream did not roll over
expect(indicesAfter).equal(indicesBefore);
});
it('should not rollover datastream when failed to update mappings and skipDataStreamRollover is true', async function () {
await supertest
.post(`/api/fleet/epm/packages/apm/8.7.0`)
.set('kbn-xsrf', 'xxxx')
.send({ force: true })
.expect(200);
await es.index({
index: 'metrics-apm.service_summary.10m-default',
document: {
'@timestamp': '2023-05-30T07:50:00.000Z',
agent: {
name: 'go',
},
data_stream: {
dataset: 'apm.service_summary.10m',
namespace: 'default',
type: 'metrics',
},
ecs: {
version: '8.6.0-dev',
},
event: {
agent_id_status: 'missing',
ingested: '2023-05-30T07:57:12Z',
},
metricset: {
interval: '10m',
name: 'service_summary',
},
observer: {
hostname: '047e282994fb',
type: 'apm-server',
version: '8.7.0',
},
processor: {
event: 'metric',
name: 'metric',
},
service: {
language: {
name: 'go',
},
name: '___main_elastic_cloud_87_ilm_fix',
},
},
});
let ds = await es.indices.get({
index: 'metrics-apm.service_summary*',
expand_wildcards: ['open', 'hidden'],
});
const indicesBefore = Object.keys(ds).length;
await supertest
.post(`/api/fleet/epm/packages/apm/8.8.0?skipDataStreamRollover=true`)
.set('kbn-xsrf', 'xxxx')
.send({ force: true })
.expect(200);
ds = await es.indices.get({
index: 'metrics-apm.service_summary*',
expand_wildcards: ['open', 'hidden'],
});
const indicesAfter = Object.keys(ds).length;
// datastream did not roll over
expect(indicesAfter).equal(indicesBefore);
});
});
}