Skip to content

Commit

Permalink
[Search][ES3] Auto generate connector name and index_name (#202149)
Browse files Browse the repository at this point in the history
## Summary

Adds Connector name and index_name auto-generation to ES3. This is taken
from the [ESS implementation
here](https://github.com/elastic/kibana/blob/main/x-pack/plugins/enterprise_search/server/lib/connectors/generate_connector_name.ts).

The ES3 implementation functions a little differently, because the ES3
Connector creation flow is different.
For ES3, the auto-generated Connector `name` and `index_name` are
automatically saved to the Connector document when a `service_type` is
selected. This is because the selection of a `service_type` already
creates the Connector record, so it made the most sense to piggyback on
that process.

If the user defines a name before selecting a service type, the
user-defined name is kept.
  • Loading branch information
navarone-feekery authored Nov 29, 2024
1 parent 2ed1fdf commit 1749c88
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 2 deletions.
16 changes: 16 additions & 0 deletions packages/kbn-search-connectors/lib/exists_index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* 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 type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';

export const indexOrAliasExists = async (
client: ElasticsearchClient,
index: string
): Promise<boolean> =>
(await client.indices.exists({ index })) || (await client.indices.existsAlias({ name: index }));
72 changes: 72 additions & 0 deletions packages/kbn-search-connectors/lib/generate_connector_name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* 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 { v4 as uuidv4 } from 'uuid';

import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';

import { toAlphanumeric } from '../utils/to_alphanumeric';
import { indexOrAliasExists } from './exists_index';
import { MANAGED_CONNECTOR_INDEX_PREFIX } from '../constants';

const GENERATE_INDEX_NAME_ERROR = 'generate_index_name_error';

export const generateConnectorName = async (
client: ElasticsearchClient,
connectorType: string,
isNative: boolean,
userConnectorName?: string
): Promise<{ connectorName: string; indexName: string }> => {
const prefix = toAlphanumeric(connectorType);
if (!prefix || prefix.length === 0) {
throw new Error('Connector type or connectorName is required');
}

const nativePrefix = isNative ? MANAGED_CONNECTOR_INDEX_PREFIX : '';

if (userConnectorName) {
let indexName = `${nativePrefix}connector-${userConnectorName}`;
const resultSameName = await indexOrAliasExists(client, indexName);
// index with same name doesn't exist
if (!resultSameName) {
return {
connectorName: userConnectorName,
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) {
return {
connectorName: userConnectorName,
indexName,
};
}
}
} else {
for (let i = 0; i < 20; i++) {
const connectorName = `${prefix}-${uuidv4().split('-')[1].slice(0, 4)}`;
const indexName = `${nativePrefix}connector-${connectorName}`;

const result = await indexOrAliasExists(client, indexName);
if (!result) {
return {
connectorName,
indexName,
};
}
}
}
throw new Error(GENERATE_INDEX_NAME_ERROR);
};
1 change: 1 addition & 0 deletions packages/kbn-search-connectors/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export * from './delete_connector';
export * from './delete_connector_secret';
export * from './fetch_connectors';
export * from './fetch_sync_jobs';
export * from './generate_connector_name';
export * from './update_filtering';
export * from './update_filtering_draft';
export * from './update_native';
Expand Down
28 changes: 28 additions & 0 deletions packages/kbn-search-connectors/utils/to_alphanumeric.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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 { toAlphanumeric } from './to_alphanumeric';

describe('toAlphanumeric', () => {
it('replaces non-alphanumeric characters with dashes', () => {
expect(toAlphanumeric('f1 &&o$ 1 2 *&%da')).toEqual('f1-o-1-2-da');
});

it('strips leading and trailing non-alphanumeric characters', () => {
expect(toAlphanumeric('$$hello world**')).toEqual('hello-world');
});

it('strips leading and trailing whitespace', () => {
expect(toAlphanumeric(' test ')).toEqual('test');
});

it('lowercases text', () => {
expect(toAlphanumeric('SomeName')).toEqual('somename');
});
});
15 changes: 15 additions & 0 deletions packages/kbn-search-connectors/utils/to_alphanumeric.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* 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".
*/

export const toAlphanumeric = (input: string) =>
input
.trim()
.replace(/[^a-zA-Z0-9]+/g, '-') // Replace all special/non-alphanumerical characters with dashes
.replace(/^[-]+|[-]+$/g, '') // Strip all leading and trailing dashes
.toLowerCase();
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ interface EditServiceTypeProps {
isDisabled?: boolean;
}

interface GeneratedConnectorNameResult {
connectorName: string;
indexName: string;
}

export const EditServiceType: React.FC<EditServiceTypeProps> = ({ connector, isDisabled }) => {
const { http } = useKibanaServices();
const connectorTypes = useConnectorTypes();
Expand Down Expand Up @@ -52,11 +57,43 @@ export const EditServiceType: React.FC<EditServiceTypeProps> = ({ connector, isD
await http.post(`/internal/serverless_search/connectors/${connector.id}/service_type`, {
body: JSON.stringify(body),
});
return inputServiceType;

// if name is empty, auto generate it and a similar index name
const results: Record<string, GeneratedConnectorNameResult> = await http.post(
`/internal/serverless_search/connectors/${connector.id}/generate_name`,
{
body: JSON.stringify({
name: connector.name,
is_native: connector.is_native,
service_type: inputServiceType,
}),
}
);

const connectorName = results.result.connectorName;
const indexName = results.result.indexName;

// save the generated connector name
await http.post(`/internal/serverless_search/connectors/${connector.id}/name`, {
body: JSON.stringify({ name: connectorName || '' }),
});

// save the generated index name (this does not create an index)
try {
// this can fail if another connector has an identical index_name value despite no index being created yet.
// in this case we just won't update the index_name, the user can do that manually when they reach that step.
await http.post(`/internal/serverless_search/connectors/${connector.id}/index_name`, {
body: JSON.stringify({ index_name: indexName }),
});
} catch {
// do nothing
}

return { serviceType: inputServiceType, name: connectorName };
},
onSuccess: (successData) => {
queryClient.setQueryData(queryKey, {
connector: { ...connector, service_type: successData },
connector: { ...connector, service_type: successData.serviceType, name: successData.name },
});
queryClient.invalidateQueries(queryKey);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
fetchConnectorById,
fetchConnectors,
fetchSyncJobs,
generateConnectorName,
IngestPipelineParams,
startConnectorSync,
updateConnectorConfiguration,
Expand Down Expand Up @@ -353,4 +354,36 @@ export const registerConnectorsRoutes = ({ logger, http, router }: RouteDependen
return response.ok();
})
);

router.post(
{
path: '/internal/serverless_search/connectors/{connectorId}/generate_name',
validate: {
body: schema.object({
name: schema.string(),
is_native: schema.boolean(),
service_type: schema.string(),
}),
params: schema.object({
connectorId: schema.string(),
}),
},
},
errorHandler(logger)(async (context, request, response) => {
const { client } = (await context.core).elasticsearch;
const result = await generateConnectorName(
client.asCurrentUser,
request.body.service_type,
request.body.is_native,
request.body.name
);

return response.ok({
body: {
result,
},
headers: { 'content-type': 'application/json' },
});
})
);
};

0 comments on commit 1749c88

Please sign in to comment.