[Enterprise Search] Prevents users from creating indices already managed elsewhere (#137108)

This commit is contained in:
Sander Philipse 2022-07-26 18:06:47 +02:00 committed by GitHub
parent e3eaf4615a
commit 967ce6d4de
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 192 additions and 80 deletions

View file

@ -7,6 +7,7 @@
export enum ErrorCode {
CONNECTOR_DOCUMENT_ALREADY_EXISTS = 'connector_document_already_exists',
CRAWLER_ALREADY_EXISTS = 'crawler_already_exists',
INDEX_ALREADY_EXISTS = 'index_already_exists',
INDEX_NOT_FOUND = 'index_not_found',
RESOURCE_NOT_FOUND = 'resource_not_found',

View file

@ -9,7 +9,6 @@ import { LogicMounter } from '../../../__mocks__/kea_logic';
import { nextTick } from '@kbn/test-jest-helpers';
import { Status } from '../../../../../common/types/api';
import { IndexExistsApiLogic } from '../../api/index/index_exists_api_logic';
import { UNIVERSAL_LANGUAGE_VALUE } from './constants';
@ -20,11 +19,9 @@ const DEFAULT_VALUES: NewSearchIndexValues = {
fullIndexName: 'search-',
fullIndexNameExists: false,
fullIndexNameIsValid: true,
isLoading: false,
language: null,
languageSelectValue: UNIVERSAL_LANGUAGE_VALUE,
rawName: '',
status: Status.IDLE,
};
describe('NewSearchIndexLogic', () => {
@ -107,19 +104,7 @@ describe('NewSearchIndexLogic', () => {
data: { exists: true, indexName: 'search-indexname' },
fullIndexName: 'search-indexname',
fullIndexNameExists: true,
isLoading: false,
rawName: 'indexname',
status: Status.SUCCESS,
});
});
});
describe('isLoading', () => {
it('should set to true when status is loading', () => {
IndexExistsApiLogic.actions.makeRequest({ indexName: 'search-indexname' });
expect(NewSearchIndexLogic.values).toEqual({
...DEFAULT_VALUES,
isLoading: true,
status: Status.LOADING,
});
});
});

View file

@ -7,8 +7,6 @@
import { kea, MakeLogicType } from 'kea';
import { Status } from '../../../../../common/types/api';
import { Actions } from '../../../shared/api_logic/create_api_logic';
import {
@ -28,11 +26,9 @@ export interface NewSearchIndexValues {
fullIndexName: string;
fullIndexNameExists: boolean;
fullIndexNameIsValid: boolean;
isLoading: boolean;
language: LanguageForOptimization;
languageSelectValue: string;
rawName: string;
status: Status;
}
export type NewSearchIndexActions = Pick<
@ -50,7 +46,7 @@ export const NewSearchIndexLogic = kea<MakeLogicType<NewSearchIndexValues, NewSe
},
connect: {
actions: [IndexExistsApiLogic, ['makeRequest']],
values: [IndexExistsApiLogic, ['data', 'status']],
values: [IndexExistsApiLogic, ['data']],
},
listeners: ({ actions, values }) => ({
setRawName: async (_, breakpoint) => {
@ -84,7 +80,6 @@ export const NewSearchIndexLogic = kea<MakeLogicType<NewSearchIndexValues, NewSe
() => [selectors.fullIndexName],
(fullIndexName) => isValidIndexName(fullIndexName),
],
isLoading: [() => [selectors.status], (status: Status) => status === Status.LOADING],
language: [
() => [selectors.languageSelectValue],
(languageSelectValue) => getLanguageForOptimization(languageSelectValue),

View file

@ -57,7 +57,6 @@ export const NewSearchIndexTemplate: React.FC<Props> = ({
fullIndexName,
fullIndexNameExists,
fullIndexNameIsValid,
isLoading,
language,
rawName,
languageSelectValue,
@ -197,8 +196,8 @@ export const NewSearchIndexTemplate: React.FC<Props> = ({
<EuiFlexItem grow={false}>
<EuiButton
fill
isDisabled={!rawName || buttonLoading || isLoading || formInvalid}
isLoading={buttonLoading || isLoading}
isDisabled={!rawName || buttonLoading || formInvalid}
isLoading={buttonLoading}
type="submit"
>
{i18n.translate(

View file

@ -15,9 +15,10 @@ export const plugin = (initializerContext: PluginInitializerContext) => {
};
export const configSchema = schema.object({
host: schema.maybe(schema.string()),
accessCheckTimeout: schema.number({ defaultValue: 5000 }),
accessCheckTimeoutWarning: schema.number({ defaultValue: 300 }),
customHeaders: schema.maybe(schema.object({}, { unknowns: 'allow' })),
host: schema.maybe(schema.string()),
ssl: schema.object({
certificateAuthorities: schema.maybe(
schema.oneOf([schema.arrayOf(schema.string(), { minSize: 1 }), schema.string()])
@ -27,17 +28,17 @@ export const configSchema = schema.object({
{ defaultValue: 'full' }
),
}),
customHeaders: schema.maybe(schema.object({}, { unknowns: 'allow' })),
});
export type ConfigType = TypeOf<typeof configSchema>;
export const config: PluginConfigDescriptor<ConfigType> = {
schema: configSchema,
exposeToBrowser: {
host: true,
},
schema: configSchema,
};
export const CONNECTORS_INDEX = '.elastic-connectors';
export const CONNECTORS_JOBS_INDEX = '.elastic-connectors-sync-jobs';
export const CONNECTORS_VERSION = '1';
export const CRAWLERS_INDEX = '.ent-search-actastic-crawler2_configurations';

View file

@ -13,6 +13,8 @@ import { ErrorCode } from '../../../common/types/error_codes';
import { setupConnectorsIndices } from '../../index_management/setup_indices';
import { fetchCrawlerByIndexName } from '../crawler/fetch_crawlers';
import { addConnector } from './add_connector';
import { fetchConnectorByIndexName } from './fetch_connectors';
@ -21,6 +23,7 @@ jest.mock('../../index_management/setup_indices', () => ({
}));
jest.mock('./fetch_connectors', () => ({ fetchConnectorByIndexName: jest.fn() }));
jest.mock('../crawler/fetch_crawlers', () => ({ fetchCrawlerByIndexName: jest.fn() }));
describe('addConnector lib function', () => {
const mockClient = {
@ -44,6 +47,7 @@ describe('addConnector lib function', () => {
mockClient.asCurrentUser.index.mockImplementation(() => ({ _id: 'fakeId' }));
mockClient.asCurrentUser.indices.exists.mockImplementation(() => false);
(fetchConnectorByIndexName as jest.Mock).mockImplementation(() => undefined);
(fetchCrawlerByIndexName as jest.Mock).mockImplementation(() => undefined);
await expect(
addConnector(mockClient as unknown as IScopedClusterClient, {
@ -76,6 +80,7 @@ describe('addConnector lib function', () => {
mockClient.asCurrentUser.index.mockImplementation(() => ({ _id: 'fakeId' }));
mockClient.asCurrentUser.indices.exists.mockImplementation(() => true);
(fetchConnectorByIndexName as jest.Mock).mockImplementation(() => undefined);
(fetchCrawlerByIndexName as jest.Mock).mockImplementation(() => undefined);
await expect(
addConnector(mockClient as unknown as IScopedClusterClient, {
@ -90,6 +95,7 @@ describe('addConnector lib function', () => {
mockClient.asCurrentUser.index.mockImplementation(() => ({ _id: 'fakeId' }));
mockClient.asCurrentUser.indices.exists.mockImplementation(() => false);
(fetchConnectorByIndexName as jest.Mock).mockImplementation(() => true);
(fetchCrawlerByIndexName as jest.Mock).mockImplementation(() => undefined);
await expect(
addConnector(mockClient as unknown as IScopedClusterClient, {
@ -104,6 +110,7 @@ describe('addConnector lib function', () => {
mockClient.asCurrentUser.index.mockImplementation(() => ({ _id: 'fakeId' }));
mockClient.asCurrentUser.indices.exists.mockImplementation(() => true);
(fetchConnectorByIndexName as jest.Mock).mockImplementation(() => true);
(fetchCrawlerByIndexName as jest.Mock).mockImplementation(() => undefined);
await expect(
addConnector(mockClient as unknown as IScopedClusterClient, {
@ -118,6 +125,7 @@ describe('addConnector lib function', () => {
mockClient.asCurrentUser.index.mockImplementation(() => ({ _id: 'fakeId' }));
mockClient.asCurrentUser.indices.exists.mockImplementation(() => false);
(fetchConnectorByIndexName as jest.Mock).mockImplementation(() => ({ id: 'connectorId' }));
(fetchCrawlerByIndexName as jest.Mock).mockImplementation(() => undefined);
await expect(
addConnector(mockClient as unknown as IScopedClusterClient, {
@ -160,6 +168,7 @@ describe('addConnector lib function', () => {
});
mockClient.asCurrentUser.indices.exists.mockImplementation(() => false);
(fetchConnectorByIndexName as jest.Mock).mockImplementation(() => false);
(fetchCrawlerByIndexName as jest.Mock).mockImplementation(() => undefined);
await expect(
addConnector(mockClient as unknown as IScopedClusterClient, {
index_name: 'search-index_name',
@ -195,6 +204,7 @@ describe('addConnector lib function', () => {
});
mockClient.asCurrentUser.indices.exists.mockImplementation(() => false);
(fetchConnectorByIndexName as jest.Mock).mockImplementation(() => false);
(fetchCrawlerByIndexName as jest.Mock).mockImplementation(() => undefined);
await expect(
addConnector(mockClient as unknown as IScopedClusterClient, {
index_name: 'index_name',
@ -204,4 +214,20 @@ describe('addConnector lib function', () => {
expect(setupConnectorsIndices).not.toHaveBeenCalled();
expect(mockClient.asCurrentUser.index).toHaveBeenCalledTimes(1);
});
it('should not create index if crawler exists', async () => {
mockClient.asCurrentUser.index.mockImplementationOnce(() => {
return 'connector ';
});
mockClient.asCurrentUser.indices.exists.mockImplementation(() => false);
(fetchConnectorByIndexName as jest.Mock).mockImplementation(() => false);
(fetchCrawlerByIndexName as jest.Mock).mockImplementation(() => 'crawler');
await expect(
addConnector(mockClient as unknown as IScopedClusterClient, {
index_name: 'index_name',
language: 'en',
})
).rejects.toEqual(new Error(ErrorCode.CRAWLER_ALREADY_EXISTS));
expect(setupConnectorsIndices).not.toHaveBeenCalled();
expect(mockClient.asCurrentUser.index).not.toHaveBeenCalled();
});
});

View file

@ -13,6 +13,8 @@ import { ErrorCode } from '../../../common/types/error_codes';
import { setupConnectorsIndices } from '../../index_management/setup_indices';
import { isIndexNotFoundException } from '../../utils/identify_exceptions';
import { fetchCrawlerByIndexName } from '../crawler/fetch_crawlers';
import { deleteConnectorById } from './delete_connector';
import { fetchConnectorByIndexName } from './fetch_connectors';
@ -39,6 +41,12 @@ const createConnector = async (
throw new Error(ErrorCode.CONNECTOR_DOCUMENT_ALREADY_EXISTS);
}
}
const crawler = await fetchCrawlerByIndexName(client, index);
if (crawler) {
throw new Error(ErrorCode.CRAWLER_ALREADY_EXISTS);
}
const result = await client.asCurrentUser.index({
document,
index: CONNECTORS_INDEX,

View file

@ -26,12 +26,6 @@ describe('crawler routes', () => {
});
});
it('creates a request to enterprise search', () => {
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
path: '/api/ent/v1/internal/indices',
});
});
it('validates correctly with name and language', () => {
const request = { body: { index_name: 'index-name', language: 'en' } };
mockRouter.shouldValidate(request);
@ -109,7 +103,7 @@ describe('crawler routes', () => {
});
it('validates correctly with name and id', () => {
const request = { params: { indexName: 'index-name', crawlRequestId: '12345' } };
const request = { params: { crawlRequestId: '12345', indexName: 'index-name' } };
mockRouter.shouldValidate(request);
});
@ -153,46 +147,46 @@ describe('crawler routes', () => {
it('validates correctly with domain urls', () => {
const request = {
params: { indexName: 'index-name' },
body: { overrides: { domain_allowlist: ['https://www.elastic.co'] } },
params: { indexName: 'index-name' },
};
mockRouter.shouldValidate(request);
});
it('validates correctly with max crawl depth', () => {
const request = {
params: { indexName: 'index-name' },
body: { overrides: { max_crawl_depth: 10 } },
params: { indexName: 'index-name' },
};
mockRouter.shouldValidate(request);
});
it('validates correctly with seed urls', () => {
const request = {
params: { indexName: 'index-name' },
body: { overrides: { seed_urls: ['https://www.elastic.co/guide'] } },
params: { indexName: 'index-name' },
};
mockRouter.shouldValidate(request);
});
it('validates correctly with sitemap urls', () => {
const request = {
params: { indexName: 'index-name' },
body: { overrides: { sitemap_urls: ['https://www.elastic.co/sitemap1.xml'] } },
params: { indexName: 'index-name' },
};
mockRouter.shouldValidate(request);
});
it('validates correctly when we set sitemap discovery', () => {
const request = {
params: { indexName: 'index-name' },
body: { overrides: { sitemap_discovery_disabled: true } },
params: { indexName: 'index-name' },
};
mockRouter.shouldValidate(request);
});
it('validates correctly with empty overrides', () => {
const request = { params: { indexName: 'index-name' }, body: { overrides: {} } };
const request = { body: { overrides: {} }, params: { indexName: 'index-name' } };
mockRouter.shouldValidate(request);
});
@ -298,24 +292,24 @@ describe('crawler routes', () => {
it('validates correctly with params and body', () => {
const request = {
body: { entry_points: [{ value: '/guide' }], name: 'https://elastic.co/guide' },
params: { indexName: 'index-name' },
body: { name: 'https://elastic.co/guide', entry_points: [{ value: '/guide' }] },
};
mockRouter.shouldValidate(request);
});
it('fails validation without a name param', () => {
const request = {
body: { entry_points: [{ value: '/guide' }], name: 'https://elastic.co/guide' },
params: {},
body: { name: 'https://elastic.co/guide', entry_points: [{ value: '/guide' }] },
};
mockRouter.shouldThrow(request);
});
it('fails validation without a body', () => {
const request = {
params: { indexName: 'index-name' },
body: {},
params: { indexName: 'index-name' },
};
mockRouter.shouldThrow(request);
});
@ -344,7 +338,7 @@ describe('crawler routes', () => {
});
it('validates correctly with name and id', () => {
const request = { params: { indexName: 'index-name', domainId: '1234' } };
const request = { params: { domainId: '1234', indexName: 'index-name' } };
mockRouter.shouldValidate(request);
});
@ -383,35 +377,35 @@ describe('crawler routes', () => {
it('validates correctly with crawl rules', () => {
const request = {
params: { indexName: 'index-name', domainId: '1234' },
body: {
crawl_rules: [
{
order: 1,
id: '5678',
order: 1,
},
],
},
params: { domainId: '1234', indexName: 'index-name' },
};
mockRouter.shouldValidate(request);
});
it('validates correctly with deduplication enabled', () => {
const request = {
params: { indexName: 'index-name', domainId: '1234' },
body: {
deduplication_enabled: true,
},
params: { domainId: '1234', indexName: 'index-name' },
};
mockRouter.shouldValidate(request);
});
it('validates correctly with deduplication fields', () => {
const request = {
params: { indexName: 'index-name', domainId: '1234' },
body: {
deduplication_fields: ['title', 'description'],
},
params: { domainId: '1234', indexName: 'index-name' },
};
mockRouter.shouldValidate(request);
});
@ -440,7 +434,7 @@ describe('crawler routes', () => {
});
it('validates correctly with name and id', () => {
const request = { params: { indexName: 'index-name', domainId: '1234' } };
const request = { params: { domainId: '1234', indexName: 'index-name' } };
mockRouter.shouldValidate(request);
});
@ -479,7 +473,7 @@ describe('crawler routes', () => {
it('validates correctly with body', () => {
const request = {
body: { url: 'elastic.co', checks: ['tcp', 'url_request'] },
body: { checks: ['tcp', 'url_request'], url: 'elastic.co' },
};
mockRouter.shouldValidate(request);
});
@ -516,24 +510,24 @@ describe('crawler routes', () => {
it('validates correctly', () => {
const request = {
params: { indexName: 'index-name' },
body: { domains: ['https://elastic.co', 'https://swiftype.com'] },
params: { indexName: 'index-name' },
};
mockRouter.shouldValidate(request);
});
it('validates correctly without body', () => {
const request = {
params: { indexName: 'index-name' },
body: {},
params: { indexName: 'index-name' },
};
mockRouter.shouldValidate(request);
});
it('fails validation without a name param', () => {
const request = {
params: {},
body: { domains: ['https://elastic.co', 'https://swiftype.com'] },
params: {},
};
mockRouter.shouldThrow(request);
});
@ -600,32 +594,32 @@ describe('crawler routes', () => {
it('validates correctly', () => {
const request = {
body: { frequency: 7, unit: 'day' },
params: { indexName: 'index-name' },
body: { unit: 'day', frequency: 7 },
};
mockRouter.shouldValidate(request);
});
it('fails validation without a name param', () => {
const request = {
body: { frequency: 7, unit: 'day' },
params: {},
body: { unit: 'day', frequency: 7 },
};
mockRouter.shouldThrow(request);
});
it('fails validation without a unit property in body', () => {
const request = {
params: { indexName: 'index-name' },
body: { frequency: 7 },
params: { indexName: 'index-name' },
};
mockRouter.shouldThrow(request);
});
it('fails validation without a frequency property in body', () => {
const request = {
params: { indexName: 'index-name' },
body: { unit: 'day' },
params: { indexName: 'index-name' },
};
mockRouter.shouldThrow(request);
});

View file

@ -7,7 +7,14 @@
import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
import { ErrorCode } from '../../../../common/types/error_codes';
import { fetchConnectorByIndexName } from '../../../lib/connectors/fetch_connectors';
import { fetchCrawlerByIndexName } from '../../../lib/crawler/fetch_crawlers';
import { RouteDependencies } from '../../../plugin';
import { createError } from '../../../utils/create_error';
import { registerCrawlerCrawlRulesRoutes } from './crawler_crawl_rules';
import { registerCrawlerEntryPointRoutes } from './crawler_entry_points';
@ -26,9 +33,58 @@ export function registerCrawlerRoutes(routeDependencies: RouteDependencies) {
}),
},
},
enterpriseSearchRequestHandler.createRequest({
path: '/api/ent/v1/internal/indices',
})
async (context, request, response) => {
const { client } = (await context.core).elasticsearch;
const indexExists = await client.asCurrentUser.indices.exists({
index: request.body.index_name,
});
if (indexExists) {
return createError({
errorCode: ErrorCode.INDEX_ALREADY_EXISTS,
message: i18n.translate(
'xpack.enterpriseSearch.server.routes.addCrawler.indexExistsError',
{
defaultMessage: 'This index already exists',
}
),
response,
statusCode: 409,
});
}
const crawler = await fetchCrawlerByIndexName(client, request.body.index_name);
if (crawler) {
return createError({
errorCode: ErrorCode.CRAWLER_ALREADY_EXISTS,
message: i18n.translate(
'xpack.enterpriseSearch.server.routes.addCrawler.crawlerExistsError',
{
defaultMessage: 'A crawler for this index already exists',
}
),
response,
statusCode: 409,
});
}
const connector = await fetchConnectorByIndexName(client, request.body.index_name);
if (connector) {
return createError({
errorCode: ErrorCode.CONNECTOR_DOCUMENT_ALREADY_EXISTS,
message: i18n.translate(
'xpack.enterpriseSearch.server.routes.addCrawler.connectorExistsError',
{
defaultMessage: 'A connector for this index already exists',
}
),
response,
statusCode: 409,
});
}
return enterpriseSearchRequestHandler.createRequest({
path: '/api/ent/v1/internal/indices',
})(context, request, response);
}
);
router.post(
@ -36,8 +92,8 @@ export function registerCrawlerRoutes(routeDependencies: RouteDependencies) {
path: '/internal/enterprise_search/crawler/validate_url',
validate: {
body: schema.object({
url: schema.string(),
checks: schema.arrayOf(schema.string()),
url: schema.string(),
}),
},
},
@ -64,20 +120,20 @@ export function registerCrawlerRoutes(routeDependencies: RouteDependencies) {
{
path: '/internal/enterprise_search/indices/{indexName}/crawler/crawl_requests',
validate: {
params: schema.object({
indexName: schema.string(),
}),
body: schema.object({
overrides: schema.maybe(
schema.object({
domain_allowlist: schema.maybe(schema.arrayOf(schema.string())),
max_crawl_depth: schema.maybe(schema.number()),
seed_urls: schema.maybe(schema.arrayOf(schema.string())),
sitemap_urls: schema.maybe(schema.arrayOf(schema.string())),
sitemap_discovery_disabled: schema.maybe(schema.boolean()),
sitemap_urls: schema.maybe(schema.arrayOf(schema.string())),
})
),
}),
params: schema.object({
indexName: schema.string(),
}),
},
},
enterpriseSearchRequestHandler.createRequest({
@ -104,8 +160,8 @@ export function registerCrawlerRoutes(routeDependencies: RouteDependencies) {
path: '/internal/enterprise_search/indices/{indexName}/crawler/crawl_requests/{crawlRequestId}',
validate: {
params: schema.object({
indexName: schema.string(),
crawlRequestId: schema.string(),
indexName: schema.string(),
}),
},
},
@ -173,22 +229,22 @@ export function registerCrawlerRoutes(routeDependencies: RouteDependencies) {
{
path: '/internal/enterprise_search/indices/{indexName}/crawler/domains/{domainId}',
validate: {
params: schema.object({
indexName: schema.string(),
domainId: schema.string(),
}),
body: schema.object({
crawl_rules: schema.maybe(
schema.arrayOf(
schema.object({
order: schema.number(),
id: schema.string(),
order: schema.number(),
})
)
),
deduplication_enabled: schema.maybe(schema.boolean()),
deduplication_fields: schema.maybe(schema.arrayOf(schema.string())),
}),
params: schema.object({
domainId: schema.string(),
indexName: schema.string(),
}),
},
},
enterpriseSearchRequestHandler.createRequest({
@ -229,12 +285,12 @@ export function registerCrawlerRoutes(routeDependencies: RouteDependencies) {
{
path: '/internal/enterprise_search/indices/{indexName}/crawler/process_crawls',
validate: {
params: schema.object({
indexName: schema.string(),
}),
body: schema.object({
domains: schema.maybe(schema.arrayOf(schema.string())),
}),
params: schema.object({
indexName: schema.string(),
}),
},
},
enterpriseSearchRequestHandler.createRequest({
@ -260,13 +316,13 @@ export function registerCrawlerRoutes(routeDependencies: RouteDependencies) {
{
path: '/internal/enterprise_search/indices/{indexName}/crawler/crawl_schedule',
validate: {
body: schema.object({
frequency: schema.number(),
unit: schema.string(),
}),
params: schema.object({
indexName: schema.string(),
}),
body: schema.object({
unit: schema.string(),
frequency: schema.number(),
}),
},
},
enterpriseSearchRequestHandler.createRequest({

View file

@ -6,11 +6,12 @@
*/
import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
import { ErrorCode } from '../../../common/types/error_codes';
import { fetchConnectors } from '../../lib/connectors/fetch_connectors';
import { fetchCrawlers } from '../../lib/crawler/fetch_crawlers';
import { fetchConnectorByIndexName, fetchConnectors } from '../../lib/connectors/fetch_connectors';
import { fetchCrawlerByIndexName, fetchCrawlers } from '../../lib/crawler/fetch_crawlers';
import { createApiIndex } from '../../lib/indices/create_index';
import { fetchIndex } from '../../lib/indices/fetch_index';
@ -199,6 +200,52 @@ export function registerIndexRoutes({ router }: RouteDependencies) {
const { ['index_name']: indexName, language } = request.body;
const { client } = (await context.core).elasticsearch;
try {
const indexExists = await client.asCurrentUser.indices.exists({
index: request.body.index_name,
});
if (indexExists) {
return createError({
errorCode: ErrorCode.INDEX_ALREADY_EXISTS,
message: i18n.translate(
'xpack.enterpriseSearch.server.routes.createApiIndex.indexExistsError',
{
defaultMessage: 'This index already exists',
}
),
response,
statusCode: 409,
});
}
const crawler = await fetchCrawlerByIndexName(client, request.body.index_name);
if (crawler) {
return createError({
errorCode: ErrorCode.CRAWLER_ALREADY_EXISTS,
message: i18n.translate(
'xpack.enterpriseSearch.server.routes.createApiIndex.crawlerExistsError',
{
defaultMessage: 'A crawler for this index already exists',
}
),
response,
statusCode: 409,
});
}
const connector = await fetchConnectorByIndexName(client, request.body.index_name);
if (connector) {
return createError({
errorCode: ErrorCode.CONNECTOR_DOCUMENT_ALREADY_EXISTS,
message: i18n.translate(
'xpack.enterpriseSearch.server.routes.createApiIndex.connectorExistsError',
{
defaultMessage: 'A connector for this index already exists',
}
),
response,
statusCode: 409,
});
}
const createIndexResponse = await createApiIndex(client, indexName, language);
return response.ok({
body: createIndexResponse,