mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Serverless/Connector] Fix bug with index name generation logic (#216293)
## Summary Improve index name generation logic. Allow any ingex name. Use combination of lodash kebabCase + some custom checks to safely map this into valid ES index name. Added unit tests ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [x] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [x] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
c7218a3fdb
commit
6adc005809
2 changed files with 175 additions and 16 deletions
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { generateConnectorName, toValidIndexName } from './generate_connector_name';
|
||||
import { indexOrAliasExists } from './exists_index';
|
||||
import { MANAGED_CONNECTOR_INDEX_PREFIX } from '../constants';
|
||||
|
||||
jest.mock('./exists_index');
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn(() => '12345678-abcd-1234-efgh-123456789012'),
|
||||
}));
|
||||
|
||||
describe('generateConnectorName', () => {
|
||||
const mockClient = {} as any;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// Default behavior: index doesn't exist
|
||||
(indexOrAliasExists as jest.Mock).mockResolvedValue(false);
|
||||
});
|
||||
|
||||
describe('toValidIndexName function', () => {
|
||||
it('converts strings to valid index names', () => {
|
||||
const testCases = [
|
||||
{ input: 'Test String', expected: 'test-string' },
|
||||
{ input: 'test/invalid*chars?', expected: 'test-invalid-chars' },
|
||||
{ input: '_leadingUnderscore', expected: 'leading-underscore' },
|
||||
{ input: 'camelCase', expected: 'camel-case' },
|
||||
{ input: '< My Connector 1234!#$$>', expected: 'my-connector-1234' },
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, expected }) => {
|
||||
expect(toValidIndexName(input)).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with provided connector name', () => {
|
||||
it('uses original name for connector and sanitized name for index', async () => {
|
||||
const result = await generateConnectorName(mockClient, 'test-type', false, 'My Connector!');
|
||||
|
||||
expect(result).toEqual({
|
||||
connectorName: 'My Connector!',
|
||||
indexName: 'connector-my-connector',
|
||||
});
|
||||
|
||||
expect(indexOrAliasExists).toHaveBeenCalledWith(mockClient, 'connector-my-connector');
|
||||
});
|
||||
|
||||
it('appends a suffix if index name already exists', async () => {
|
||||
// First call: index exists, second call: index doesn't exist
|
||||
(indexOrAliasExists as jest.Mock).mockResolvedValueOnce(true).mockResolvedValueOnce(false);
|
||||
|
||||
const result = await generateConnectorName(mockClient, 'test-type', false, 'My Connector!');
|
||||
|
||||
expect(result).toEqual({
|
||||
connectorName: 'My Connector!',
|
||||
indexName: 'connector-my-connector-abcd',
|
||||
});
|
||||
|
||||
expect(indexOrAliasExists).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('uses managed prefix for native connectors', async () => {
|
||||
const result = await generateConnectorName(mockClient, 'test-type', true, 'My Connector!');
|
||||
|
||||
expect(result.indexName).toBe(`${MANAGED_CONNECTOR_INDEX_PREFIX}connector-my-connector`);
|
||||
});
|
||||
|
||||
it('throws error after 20 failed attempts to generate unique name', async () => {
|
||||
// Always return true (index exists) for all calls
|
||||
(indexOrAliasExists as jest.Mock).mockResolvedValue(true);
|
||||
|
||||
await expect(
|
||||
generateConnectorName(mockClient, 'test-type', false, 'My Connector!')
|
||||
).rejects.toThrow('generate_index_name_error');
|
||||
|
||||
expect(indexOrAliasExists).toHaveBeenCalledTimes(21); // Initial check + 20 attempts
|
||||
});
|
||||
});
|
||||
|
||||
describe('without provided connector name', () => {
|
||||
it('auto-generates connector and index names', async () => {
|
||||
const result = await generateConnectorName(mockClient, 'testType', false);
|
||||
|
||||
expect(result).toEqual({
|
||||
connectorName: 'testtype-abcd',
|
||||
indexName: 'connector-testtype-abcd',
|
||||
});
|
||||
});
|
||||
|
||||
it('uses managed prefix for native connectors', async () => {
|
||||
const result = await generateConnectorName(mockClient, 'testType', true);
|
||||
|
||||
expect(result.indexName).toBe(`${MANAGED_CONNECTOR_INDEX_PREFIX}connector-testtype-abcd`);
|
||||
});
|
||||
|
||||
it('tries different suffixes if index name already exists', async () => {
|
||||
(indexOrAliasExists as jest.Mock).mockResolvedValueOnce(true).mockResolvedValueOnce(false);
|
||||
|
||||
const result = await generateConnectorName(mockClient, 'testType', false);
|
||||
|
||||
expect(result.connectorName).toMatch(/testtype-/);
|
||||
expect(indexOrAliasExists).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('throws error if connectorType is empty', async () => {
|
||||
await expect(generateConnectorName(mockClient, '', false)).rejects.toThrow(
|
||||
'Connector type or connectorName is required'
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error after 20 failed attempts to generate unique name', async () => {
|
||||
(indexOrAliasExists as jest.Mock).mockResolvedValue(true);
|
||||
|
||||
await expect(generateConnectorName(mockClient, 'testType', false)).rejects.toThrow(
|
||||
'generate_index_name_error'
|
||||
);
|
||||
|
||||
expect(indexOrAliasExists).toHaveBeenCalledTimes(20);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -9,6 +9,8 @@
|
|||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { kebabCase } from 'lodash';
|
||||
|
||||
import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||
|
||||
import { toAlphanumeric } from '../utils/to_alphanumeric';
|
||||
|
@ -17,6 +19,26 @@ import { MANAGED_CONNECTOR_INDEX_PREFIX } from '../constants';
|
|||
|
||||
const GENERATE_INDEX_NAME_ERROR = 'generate_index_name_error';
|
||||
|
||||
export const toValidIndexName = (str: string): string => {
|
||||
if (!str || str.trim() === '') {
|
||||
return 'index';
|
||||
}
|
||||
|
||||
// Start with kebabCase to handle most transformations
|
||||
let result = kebabCase(str);
|
||||
|
||||
// Additional processing for ES index name requirements
|
||||
result = result
|
||||
// ES doesn't allow \, /, *, ?, ", <, >, |, comma, #, :
|
||||
.replace(/[\\/*?"<>|,#:]/g, '-')
|
||||
// Cannot start with -, _, +
|
||||
.replace(/^[-_+]/, '')
|
||||
// Remove trailing hyphens
|
||||
.replace(/-+$/, '');
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const generateConnectorName = async (
|
||||
client: ElasticsearchClient,
|
||||
connectorType: string,
|
||||
|
@ -30,37 +52,44 @@ export const generateConnectorName = async (
|
|||
|
||||
const nativePrefix = isNative ? MANAGED_CONNECTOR_INDEX_PREFIX : '';
|
||||
|
||||
// Handle user-provided connector name
|
||||
if (userConnectorName) {
|
||||
let indexName = `${nativePrefix}connector-${userConnectorName}`;
|
||||
const resultSameName = await indexOrAliasExists(client, indexName);
|
||||
// index with same name doesn't exist
|
||||
if (!resultSameName) {
|
||||
// Keep original connector name, but sanitize it for index name
|
||||
const sanitizedName = toValidIndexName(userConnectorName);
|
||||
|
||||
// First try with the sanitized name directly
|
||||
let indexName = `${nativePrefix}connector-${sanitizedName}`;
|
||||
const baseNameExists = await indexOrAliasExists(client, indexName);
|
||||
|
||||
if (!baseNameExists) {
|
||||
return {
|
||||
connectorName: userConnectorName,
|
||||
connectorName: userConnectorName, // Keep original connector name
|
||||
indexName,
|
||||
};
|
||||
}
|
||||
// if the index name already exists, we will generate until it doesn't for 20 times
|
||||
for (let i = 0; i < 20; i++) {
|
||||
indexName = `${nativePrefix}connector-${userConnectorName}-${uuidv4()
|
||||
.split('-')[1]
|
||||
.slice(0, 4)}`;
|
||||
|
||||
const result = await indexOrAliasExists(client, indexName);
|
||||
if (!result) {
|
||||
// If base name exists, try with random suffixes
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const uniqueSuffix = uuidv4().split('-')[1].slice(0, 4);
|
||||
indexName = `${nativePrefix}connector-${sanitizedName}-${uniqueSuffix}`;
|
||||
|
||||
const exists = await indexOrAliasExists(client, indexName);
|
||||
if (!exists) {
|
||||
return {
|
||||
connectorName: userConnectorName,
|
||||
connectorName: userConnectorName, // Keep original connector name
|
||||
indexName,
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Auto-generate a connector name
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const connectorName = `${prefix}-${uuidv4().split('-')[1].slice(0, 4)}`;
|
||||
const uniqueSuffix = uuidv4().split('-')[1].slice(0, 4);
|
||||
const connectorName = `${toValidIndexName(prefix)}-${uniqueSuffix}`;
|
||||
const indexName = `${nativePrefix}connector-${connectorName}`;
|
||||
|
||||
const result = await indexOrAliasExists(client, indexName);
|
||||
if (!result) {
|
||||
const exists = await indexOrAliasExists(client, indexName);
|
||||
if (!exists) {
|
||||
return {
|
||||
connectorName,
|
||||
indexName,
|
||||
|
@ -68,5 +97,6 @@ export const generateConnectorName = async (
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(GENERATE_INDEX_NAME_ERROR);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue