Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Response Ops][Event Log] Updating event log mappings if data stream and index template already exist #193205

Merged
merged 10 commits into from
Sep 20, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ const createClusterClientMock = () => {
indexDocuments: jest.fn(),
doesIndexTemplateExist: jest.fn(),
createIndexTemplate: jest.fn(),
updateIndexTemplate: jest.fn(),
doesDataStreamExist: jest.fn(),
createDataStream: jest.fn(),
updateConcreteIndices: jest.fn(),
getExistingLegacyIndexTemplates: jest.fn(),
setLegacyIndexTemplateToHidden: jest.fn(),
getExistingIndices: jest.fn(),
Expand Down
202 changes: 201 additions & 1 deletion x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,117 @@ describe('createIndexTemplate', () => {
});
});

describe('updateIndexTemplate', () => {
test('should call cluster with given template', async () => {
clusterClient.indices.simulateTemplate.mockImplementationOnce(async () => ({
template: {
aliases: {
alias_name_1: {
is_hidden: true,
},
alias_name_2: {
is_hidden: true,
},
},
settings: {
hidden: true,
number_of_shards: 1,
auto_expand_replicas: '0-1',
},
mappings: { dynamic: false, properties: { '@timestamp': { type: 'date' } } },
},
}));

await clusterClientAdapter.updateIndexTemplate('foo', { args: true });

expect(clusterClient.indices.simulateTemplate).toHaveBeenCalledWith({
name: 'foo',
body: { args: true },
});
expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledWith({
name: 'foo',
body: { args: true },
});
});

test(`should throw error if simulate mappings response is empty`, async () => {
clusterClient.indices.simulateTemplate.mockImplementationOnce(async () => ({
template: {
aliases: {
alias_name_1: {
is_hidden: true,
},
alias_name_2: {
is_hidden: true,
},
},
settings: {
hidden: true,
number_of_shards: 1,
auto_expand_replicas: '0-1',
},
mappings: {},
},
}));

await expect(() =>
clusterClientAdapter.updateIndexTemplate('foo', { name: 'template', args: true })
).rejects.toThrowErrorMatchingInlineSnapshot(
`"No mappings would be generated for template, possibly due to failed/misconfigured bootstrapping"`
);

expect(logger.error).toHaveBeenCalledWith(
`Error updating index template foo: No mappings would be generated for template, possibly due to failed/misconfigured bootstrapping`
);
});

test(`should throw error if simulateTemplate throws error`, async () => {
clusterClient.indices.simulateTemplate.mockImplementationOnce(() => {
throw new Error('failed to simulate');
});

await expect(() =>
clusterClientAdapter.updateIndexTemplate('foo', { name: 'template', args: true })
).rejects.toThrowErrorMatchingInlineSnapshot(`"failed to simulate"`);

expect(logger.error).toHaveBeenCalledWith(
`Error updating index template foo: failed to simulate`
);
});

test(`should throw error if putIndexTemplate throws error`, async () => {
clusterClient.indices.simulateTemplate.mockImplementationOnce(async () => ({
template: {
aliases: {
alias_name_1: {
is_hidden: true,
},
alias_name_2: {
is_hidden: true,
},
},
settings: {
hidden: true,
number_of_shards: 1,
auto_expand_replicas: '0-1',
},
mappings: { dynamic: false, properties: { '@timestamp': { type: 'date' } } },
},
}));
clusterClient.indices.putIndexTemplate.mockImplementationOnce(() => {
throw new Error('failed to update index template');
});

await expect(() =>
clusterClientAdapter.updateIndexTemplate('foo', { name: 'template', args: true })
).rejects.toThrowErrorMatchingInlineSnapshot(`"failed to update index template"`);

expect(logger.error).toHaveBeenCalledWith(
`Error updating index template foo: failed to update index template`
);
});
});

describe('getExistingLegacyIndexTemplates', () => {
test('should call cluster with given index template pattern', async () => {
await clusterClientAdapter.getExistingLegacyIndexTemplates('foo*');
Expand Down Expand Up @@ -497,7 +608,7 @@ describe('doesDataStreamExist', () => {
});
});

describe('createIndex', () => {
describe('createDataStream', () => {
test('should call cluster with proper arguments', async () => {
await clusterClientAdapter.createDataStream('foo');
expect(clusterClient.indices.createDataStream).toHaveBeenCalledWith({
Expand Down Expand Up @@ -526,6 +637,95 @@ describe('createIndex', () => {
});
});

describe('updateConcreteIndices', () => {
test('should call cluster with proper arguments', async () => {
clusterClient.indices.simulateIndexTemplate.mockImplementationOnce(async () => ({
template: {
aliases: { alias_name_1: { is_hidden: true } },
settings: {
hidden: true,
number_of_shards: 1,
auto_expand_replicas: '0-1',
},
mappings: { dynamic: false, properties: { '@timestamp': { type: 'date' } } },
},
}));

await clusterClientAdapter.updateConcreteIndices('foo');
expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledWith({
name: 'foo',
});
expect(clusterClient.indices.putMapping).toHaveBeenCalledWith({
index: 'foo',
body: { dynamic: false, properties: { '@timestamp': { type: 'date' } } },
});
});

test('should not update mapping if simulate response does not contain mappings', async () => {
// @ts-ignore
clusterClient.indices.simulateIndexTemplate.mockImplementationOnce(async () => ({
template: {
aliases: { alias_name_1: { is_hidden: true } },
settings: {
hidden: true,
number_of_shards: 1,
auto_expand_replicas: '0-1',
},
},
}));

await clusterClientAdapter.updateConcreteIndices('foo');
expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledWith({
name: 'foo',
});
expect(clusterClient.indices.putMapping).not.toHaveBeenCalled();
});

test('should throw error if simulateIndexTemplate throws error', async () => {
clusterClient.indices.simulateIndexTemplate.mockImplementationOnce(() => {
throw new Error('failed to simulate');
});

await expect(() =>
clusterClientAdapter.updateConcreteIndices('foo')
).rejects.toThrowErrorMatchingInlineSnapshot(`"failed to simulate"`);

expect(clusterClient.indices.putMapping).not.toHaveBeenCalled();
expect(logger.error).toHaveBeenCalledWith(
`Error updating index mappings for foo: failed to simulate`
);
});

test('should throw error if putMapping throws error', async () => {
clusterClient.indices.simulateIndexTemplate.mockImplementationOnce(async () => ({
template: {
aliases: { alias_name_1: { is_hidden: true } },
settings: {
hidden: true,
number_of_shards: 1,
auto_expand_replicas: '0-1',
},
mappings: { dynamic: false, properties: { '@timestamp': { type: 'date' } } },
},
}));
clusterClient.indices.putMapping.mockImplementationOnce(() => {
throw new Error('failed to put mappings');
});

await expect(() =>
clusterClientAdapter.updateConcreteIndices('foo')
).rejects.toThrowErrorMatchingInlineSnapshot(`"failed to put mappings"`);

expect(clusterClient.indices.putMapping).toHaveBeenCalledWith({
index: 'foo',
body: { dynamic: false, properties: { '@timestamp': { type: 'date' } } },
});
expect(logger.error).toHaveBeenCalledWith(
`Error updating index mappings for foo: failed to put mappings`
);
});
});

describe('queryEventsBySavedObject', () => {
const DEFAULT_OPTIONS = queryOptionsSchema.validate({});

Expand Down
42 changes: 40 additions & 2 deletions x-pack/plugins/event_log/server/es/cluster_client_adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import { Subject } from 'rxjs';
import { bufferTime, filter as rxFilter, concatMap } from 'rxjs';
import { reject, isUndefined, isNumber, pick } from 'lodash';
import { reject, isUndefined, isNumber, pick, isEmpty, get } from 'lodash';
import type { PublicMethodsOf } from '@kbn/utility-types';
import { Logger, ElasticsearchClient } from '@kbn/core/server';
import util from 'util';
Expand Down Expand Up @@ -213,6 +213,28 @@ export class ClusterClientAdapter<TDoc extends { body: AliasAny; index: string }
}
}

public async updateIndexTemplate(name: string, template: Record<string, unknown>): Promise<void> {
this.logger.info(`Updating index template ${name}`);

try {
const esClient = await this.elasticsearchClientPromise;

// Simulate the index template to proactively identify any issues with the mappings
const simulateResponse = await esClient.indices.simulateTemplate({ name, body: template });
const mappings: estypes.MappingTypeMapping = simulateResponse.template.mappings;

if (isEmpty(mappings)) {
throw new Error(
`No mappings would be generated for ${template.name}, possibly due to failed/misconfigured bootstrapping`
);
}
await esClient.indices.putIndexTemplate({ name, body: template });
} catch (err) {
this.logger.error(`Error updating index template ${name}: ${err.message}`);
throw err;
}
}

public async getExistingLegacyIndexTemplates(
indexTemplatePattern: string
): Promise<estypes.IndicesGetTemplateResponse> {
Expand Down Expand Up @@ -335,7 +357,7 @@ export class ClusterClientAdapter<TDoc extends { body: AliasAny; index: string }
}
}

public async createDataStream(name: string, body: Record<string, unknown> = {}): Promise<void> {
public async createDataStream(name: string): Promise<void> {
this.logger.info(`Creating datastream ${name}`);
try {
const esClient = await this.elasticsearchClientPromise;
Expand All @@ -347,6 +369,22 @@ export class ClusterClientAdapter<TDoc extends { body: AliasAny; index: string }
}
}

public async updateConcreteIndices(name: string): Promise<void> {
this.logger.info(`Updating concrete index mappings for ${name}`);
try {
const esClient = await this.elasticsearchClientPromise;
const simulatedIndexMapping = await esClient.indices.simulateIndexTemplate({ name });
const simulatedMapping = get(simulatedIndexMapping, ['template', 'mappings']);

if (simulatedMapping != null) {
await esClient.indices.putMapping({ index: name, body: simulatedMapping });
}
} catch (err) {
this.logger.error(`Error updating index mappings for ${name}: ${err.message}`);
throw err;
}
}

public async queryEventsBySavedObjects(
queryOptions: FindEventsOptionsBySavedObjectFilter
): Promise<QueryEventsBySavedObjectResult> {
Expand Down
10 changes: 7 additions & 3 deletions x-pack/plugins/event_log/server/es/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('initializeEs', () => {
esContext.esAdapter.getExistingIndexAliases.mockResolvedValue({});
});

test(`should update existing index templates if any exist and are not hidden`, async () => {
test(`should update existing index templates to hidden if any exist and are not hidden`, async () => {
const testTemplate = {
order: 0,
index_patterns: ['foo-bar-*'],
Expand Down Expand Up @@ -393,14 +393,16 @@ describe('initializeEs', () => {
await initializeEs(esContext);
expect(esContext.esAdapter.doesIndexTemplateExist).toHaveBeenCalled();
expect(esContext.esAdapter.createIndexTemplate).toHaveBeenCalled();
expect(esContext.esAdapter.updateIndexTemplate).not.toHaveBeenCalled();
});

test(`shouldn't create index template if it already exists`, async () => {
test(`should update index template if it already exists`, async () => {
esContext.esAdapter.doesIndexTemplateExist.mockResolvedValue(true);

await initializeEs(esContext);
expect(esContext.esAdapter.doesIndexTemplateExist).toHaveBeenCalled();
expect(esContext.esAdapter.createIndexTemplate).not.toHaveBeenCalled();
expect(esContext.esAdapter.updateIndexTemplate).toHaveBeenCalled();
});

test(`should create data stream if it doesn't exist`, async () => {
Expand All @@ -409,14 +411,16 @@ describe('initializeEs', () => {
await initializeEs(esContext);
expect(esContext.esAdapter.doesDataStreamExist).toHaveBeenCalled();
expect(esContext.esAdapter.createDataStream).toHaveBeenCalled();
expect(esContext.esAdapter.updateConcreteIndices).not.toHaveBeenCalled();
});

test(`shouldn't create data stream if it already exists`, async () => {
test(`should update indices of data stream if it already exists`, async () => {
esContext.esAdapter.doesDataStreamExist.mockResolvedValue(true);

await initializeEs(esContext);
expect(esContext.esAdapter.doesDataStreamExist).toHaveBeenCalled();
expect(esContext.esAdapter.createDataStream).not.toHaveBeenCalled();
expect(esContext.esAdapter.updateConcreteIndices).toHaveBeenCalled();
});
});

Expand Down
19 changes: 10 additions & 9 deletions x-pack/plugins/event_log/server/es/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,12 +216,17 @@ class EsInitializationSteps {
const exists = await this.esContext.esAdapter.doesIndexTemplateExist(
this.esContext.esNames.indexTemplate
);
const templateBody = getIndexTemplate(this.esContext.esNames);
if (!exists) {
const templateBody = getIndexTemplate(this.esContext.esNames);
await this.esContext.esAdapter.createIndexTemplate(
this.esContext.esNames.indexTemplate,
templateBody
);
} else {
await this.esContext.esAdapter.updateIndexTemplate(
this.esContext.esNames.indexTemplate,
templateBody
);
}
}

Expand All @@ -230,14 +235,10 @@ class EsInitializationSteps {
this.esContext.esNames.dataStream
);
if (!exists) {
await this.esContext.esAdapter.createDataStream(this.esContext.esNames.dataStream, {
aliases: {
[this.esContext.esNames.dataStream]: {
is_write_index: true,
is_hidden: true,
},
},
});
await this.esContext.esAdapter.createDataStream(this.esContext.esNames.dataStream);
} else {
// apply current mappings to existing data stream
await this.esContext.esAdapter.updateConcreteIndices(this.esContext.esNames.dataStream);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we end up testing this path in FTR? I'm thinking no, but not positive.

Could we do this with a jest integration test? Just start a Kibana, then start another or kill the first and restart. Maybe we could add a debug log to ensure we made it here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a debug log when the PUT mappings call is successful and added a jest integration test that starts up Kibana, tests for the Creating datastream info log, then restarts Kibana and tests for the Updating concrete indices info log and the success debug log

}
}
}