= ({
{baseText}
= ({
{baseText}
{i18n.translate(
- 'xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.targetField.helpText.textEmbeddingModel',
+ 'xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.fields.targetField.helpText.textEmbeddingModel',
{
defaultMessage: 'Additionally the predicted_value will be copied to "{fieldName}"',
values: {
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/types.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/types.ts
index 0e7070e2385e9..3b155f8b32310 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/types.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/types.ts
@@ -25,6 +25,7 @@ export interface AddInferencePipelineFormErrors {
export enum AddInferencePipelineSteps {
Configuration,
+ Fields,
Test,
Review,
}
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/utils.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/utils.ts
index e83cd35992f77..31a11c40770ae 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/utils.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/utils.ts
@@ -45,6 +45,14 @@ export const validateInferencePipelineConfiguration = (
if (config.modelID.trim().length === 0) {
errors.modelID = FIELD_REQUIRED_ERROR;
}
+
+ return errors;
+};
+
+export const validateInferencePipelineFields = (
+ config: InferencePipelineConfiguration
+): AddInferencePipelineFormErrors => {
+ const errors: AddInferencePipelineFormErrors = {};
if (config.sourceField.trim().length === 0) {
errors.sourceField = FIELD_REQUIRED_ERROR;
}
diff --git a/x-pack/plugins/enterprise_search/server/index.ts b/x-pack/plugins/enterprise_search/server/index.ts
index 40e2c349b9605..19019841976d4 100644
--- a/x-pack/plugins/enterprise_search/server/index.ts
+++ b/x-pack/plugins/enterprise_search/server/index.ts
@@ -49,5 +49,3 @@ export const CURRENT_CONNECTORS_INDEX = '.elastic-connectors-v1';
export const CONNECTORS_JOBS_INDEX = '.elastic-connectors-sync-jobs';
export const CONNECTORS_VERSION = 1;
export const CRAWLERS_INDEX = '.ent-search-actastic-crawler2_configurations_v2';
-export const ANALYTICS_COLLECTIONS_INDEX = '.elastic-analytics-collections';
-export const ANALYTICS_VERSION = '1';
diff --git a/x-pack/plugins/enterprise_search/server/lib/analytics/add_analytics_collection.test.ts b/x-pack/plugins/enterprise_search/server/lib/analytics/add_analytics_collection.test.ts
index 242ed2b087106..ed6aa16b26a35 100644
--- a/x-pack/plugins/enterprise_search/server/lib/analytics/add_analytics_collection.test.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/analytics/add_analytics_collection.test.ts
@@ -8,26 +8,18 @@
import { IScopedClusterClient } from '@kbn/core-elasticsearch-server';
import { DataViewsService } from '@kbn/data-views-plugin/common';
-import { ANALYTICS_COLLECTIONS_INDEX } from '../..';
import { ErrorCode } from '../../../common/types/error_codes';
import { addAnalyticsCollection } from './add_analytics_collection';
-import { fetchAnalyticsCollectionById } from './fetch_analytics_collection';
-import { setupAnalyticsCollectionIndex } from './setup_indices';
+import { fetchAnalyticsCollections } from './fetch_analytics_collection';
-jest.mock('./fetch_analytics_collection', () => ({ fetchAnalyticsCollectionById: jest.fn() }));
-jest.mock('./setup_indices', () => ({
- setupAnalyticsCollectionIndex: jest.fn(),
-}));
+jest.mock('./fetch_analytics_collection', () => ({ fetchAnalyticsCollections: jest.fn() }));
describe('add analytics collection lib function', () => {
const mockClient = {
asCurrentUser: {
- index: jest.fn(),
- indices: {
- create: jest.fn(),
- exists: jest.fn(),
- refresh: jest.fn(),
+ transport: {
+ request: jest.fn(),
},
},
asInternalUser: {},
@@ -42,114 +34,65 @@ describe('add analytics collection lib function', () => {
});
it('should add analytics collection', async () => {
- mockClient.asCurrentUser.index.mockImplementation(() => ({ _id: 'example' }));
- mockClient.asCurrentUser.indices.exists.mockImplementation(() => false);
+ mockClient.asCurrentUser.transport.request.mockImplementation(() => ({
+ acknowledged: true,
+ name: `example`,
+ }));
+
+ (fetchAnalyticsCollections as jest.Mock).mockImplementation(() => [
+ {
+ events_datastream: 'example-datastream',
+ name: 'example',
+ },
+ ]);
await expect(
addAnalyticsCollection(
mockClient as unknown as IScopedClusterClient,
mockDataViewsService as unknown as DataViewsService,
- {
- name: 'example',
- }
+ 'example'
)
).resolves.toEqual({
- event_retention_day_length: 180,
- events_datastream: 'logs-elastic_analytics.events-example',
- id: 'example',
+ events_datastream: 'example-datastream',
name: 'example',
});
- expect(mockClient.asCurrentUser.index).toHaveBeenCalledWith({
- document: {
- event_retention_day_length: 180,
- events_datastream: 'logs-elastic_analytics.events-example',
- name: 'example',
- },
- id: 'example',
- index: ANALYTICS_COLLECTIONS_INDEX,
+ expect(mockClient.asCurrentUser.transport.request).toHaveBeenCalledWith({
+ method: 'PUT',
+ path: '/_application/analytics/example',
});
expect(mockDataViewsService.createAndSave).toHaveBeenCalledWith(
{
allowNoIndex: true,
- name: 'elastic_analytics.events-example',
- title: 'logs-elastic_analytics.events-example',
+ name: 'behavioral_analytics.events-example',
+ title: 'example-datastream',
timeFieldName: '@timestamp',
},
true
);
});
- it('should reject if index already exists', async () => {
- mockClient.asCurrentUser.index.mockImplementation(() => ({ _id: 'fakeId' }));
- (fetchAnalyticsCollectionById as jest.Mock).mockImplementation(() => true);
+ it('should reject if analytics collection already exists', async () => {
+ mockClient.asCurrentUser.transport.request.mockImplementation(() =>
+ Promise.reject({
+ meta: {
+ body: {
+ error: {
+ type: 'resource_already_exists_exception',
+ },
+ },
+ },
+ })
+ );
await expect(
addAnalyticsCollection(
mockClient as unknown as IScopedClusterClient,
mockDataViewsService as unknown as DataViewsService,
- {
- name: 'index_name',
- }
+ 'index_name'
)
).rejects.toEqual(new Error(ErrorCode.ANALYTICS_COLLECTION_ALREADY_EXISTS));
- expect(mockClient.asCurrentUser.index).not.toHaveBeenCalled();
- expect(mockDataViewsService.createAndSave).not.toHaveBeenCalled();
- });
-
- it('should create index if no analytics collection index exists', async () => {
- mockClient.asCurrentUser.indices.exists.mockImplementation(() => false);
-
- (fetchAnalyticsCollectionById as jest.Mock).mockImplementation(() => undefined);
-
- mockClient.asCurrentUser.index.mockImplementation(() => ({ _id: 'example' }));
-
- await expect(
- addAnalyticsCollection(
- mockClient as unknown as IScopedClusterClient,
- mockDataViewsService as unknown as DataViewsService,
- {
- name: 'example',
- }
- )
- ).resolves.toEqual({
- event_retention_day_length: 180,
- events_datastream: 'logs-elastic_analytics.events-example',
- id: 'example',
- name: 'example',
- });
-
- expect(mockClient.asCurrentUser.index).toHaveBeenCalledWith({
- document: {
- event_retention_day_length: 180,
- events_datastream: 'logs-elastic_analytics.events-example',
- name: 'example',
- },
- id: 'example',
- index: ANALYTICS_COLLECTIONS_INDEX,
- });
-
- expect(setupAnalyticsCollectionIndex).toHaveBeenCalledWith(mockClient.asCurrentUser);
- });
-
- it('should not create index if status code is not 404', async () => {
- mockClient.asCurrentUser.index.mockImplementationOnce(() => {
- return Promise.reject({ statusCode: 500 });
- });
- mockClient.asCurrentUser.indices.exists.mockImplementation(() => true);
- (fetchAnalyticsCollectionById as jest.Mock).mockImplementation(() => false);
- await expect(
- addAnalyticsCollection(
- mockClient as unknown as IScopedClusterClient,
- mockDataViewsService as unknown as DataViewsService,
- {
- name: 'example',
- }
- )
- ).rejects.toEqual({ statusCode: 500 });
- expect(setupAnalyticsCollectionIndex).not.toHaveBeenCalled();
- expect(mockClient.asCurrentUser.index).toHaveBeenCalledTimes(1);
expect(mockDataViewsService.createAndSave).not.toHaveBeenCalled();
});
});
diff --git a/x-pack/plugins/enterprise_search/server/lib/analytics/add_analytics_collection.ts b/x-pack/plugins/enterprise_search/server/lib/analytics/add_analytics_collection.ts
index fb048c802a681..f85732136d454 100644
--- a/x-pack/plugins/enterprise_search/server/lib/analytics/add_analytics_collection.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/analytics/add_analytics_collection.ts
@@ -7,50 +7,28 @@
import { IScopedClusterClient } from '@kbn/core/server';
import { DataView, DataViewsService } from '@kbn/data-views-plugin/common';
-import { ANALYTICS_COLLECTIONS_INDEX } from '../..';
-
-import { AnalyticsCollectionDocument, AnalyticsCollection } from '../../../common/types/analytics';
+import { AnalyticsCollection } from '../../../common/types/analytics';
import { ErrorCode } from '../../../common/types/error_codes';
-import { toAlphanumeric } from '../../../common/utils/to_alphanumeric';
-import { fetchAnalyticsCollectionById } from './fetch_analytics_collection';
-import { setupAnalyticsCollectionIndex } from './setup_indices';
+import { isResourceAlreadyExistsException } from '../../utils/identify_exceptions';
+
+import { fetchAnalyticsCollections } from './fetch_analytics_collection';
-interface AddAnalyticsCollectionRequestBody {
+interface CollectionsPutResponse {
+ acknowledged: boolean;
name: string;
}
const createAnalyticsCollection = async (
client: IScopedClusterClient,
- document: AnalyticsCollectionDocument,
- id: string
-): Promise => {
- const analyticsCollection = await fetchAnalyticsCollectionById(client, id);
-
- if (analyticsCollection) {
- throw new Error(ErrorCode.ANALYTICS_COLLECTION_ALREADY_EXISTS);
- }
-
- const result = await client.asCurrentUser.index({
- document,
- id,
- index: ANALYTICS_COLLECTIONS_INDEX,
+ name: string
+): Promise => {
+ const response = await client.asCurrentUser.transport.request({
+ method: 'PUT',
+ path: `/_application/analytics/${name}`,
});
- await client.asCurrentUser.indices.refresh({ index: ANALYTICS_COLLECTIONS_INDEX });
-
- return {
- id: result._id,
- ...document,
- };
-};
-
-const getDataViewName = (collectionId: string): string => {
- return `elastic_analytics.events-${collectionId}`;
-};
-
-const getDataStreamName = (collectionId: string): string => {
- return `logs-${getDataViewName(collectionId)}`;
+ return response;
};
const createDataView = async (
@@ -60,9 +38,9 @@ const createDataView = async (
return dataViewsService.createAndSave(
{
allowNoIndex: true,
- title: getDataStreamName(analyticsCollection.id),
- name: getDataViewName(analyticsCollection.id),
+ name: `behavioral_analytics.events-${analyticsCollection.name}`,
timeFieldName: '@timestamp',
+ title: analyticsCollection.events_datastream,
},
true
);
@@ -71,28 +49,18 @@ const createDataView = async (
export const addAnalyticsCollection = async (
client: IScopedClusterClient,
dataViewsService: DataViewsService,
- { name: collectionName }: AddAnalyticsCollectionRequestBody
+ name: string
): Promise => {
- const id = toAlphanumeric(collectionName);
- const eventsDataStreamName = getDataStreamName(id);
-
- const document: AnalyticsCollectionDocument = {
- event_retention_day_length: 180,
- events_datastream: eventsDataStreamName,
- name: collectionName,
- };
-
- const analyticsCollectionIndexExists = await client.asCurrentUser.indices.exists({
- index: ANALYTICS_COLLECTIONS_INDEX,
- });
-
- if (!analyticsCollectionIndexExists) {
- await setupAnalyticsCollectionIndex(client.asCurrentUser);
+ try {
+ await createAnalyticsCollection(client, name);
+ } catch (error) {
+ if (isResourceAlreadyExistsException(error)) {
+ throw new Error(ErrorCode.ANALYTICS_COLLECTION_ALREADY_EXISTS);
+ }
}
+ const analyticsCollections = await fetchAnalyticsCollections(client, name);
- const analyticsCollection = await createAnalyticsCollection(client, document, id);
-
+ const analyticsCollection = analyticsCollections[0];
await createDataView(dataViewsService, analyticsCollection);
-
return analyticsCollection;
};
diff --git a/x-pack/plugins/enterprise_search/server/lib/analytics/analytics_events_index_exists.test.ts b/x-pack/plugins/enterprise_search/server/lib/analytics/analytics_events_index_exists.test.ts
index 5a9a64420b8ff..a851f21eaa151 100644
--- a/x-pack/plugins/enterprise_search/server/lib/analytics/analytics_events_index_exists.test.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/analytics/analytics_events_index_exists.test.ts
@@ -9,15 +9,11 @@ import { IScopedClusterClient } from '@kbn/core-elasticsearch-server';
import { analyticsEventsIndexExists } from './analytics_events_index_exists';
-// jest.mock('./analytics_events_index_exists', () => ({
-// analyticsEventsIndexExists: jest.fn(),
-// }));
-
describe('analytics collection events exists function', () => {
const mockClient = {
asCurrentUser: {
indices: {
- exists: jest.fn(),
+ getDataStream: jest.fn(),
},
},
};
@@ -28,13 +24,14 @@ describe('analytics collection events exists function', () => {
describe('checking if analytics events index exists', () => {
it('should call exists endpoint', async () => {
- mockClient.asCurrentUser.indices.exists.mockImplementationOnce(() => Promise.resolve(true));
+ mockClient.asCurrentUser.indices.getDataStream.mockImplementationOnce(() => ({
+ data_streams: [{ name: 'example' }],
+ }));
await expect(
analyticsEventsIndexExists(mockClient as unknown as IScopedClusterClient, 'example')
).resolves.toEqual(true);
- expect(mockClient.asCurrentUser.indices.exists).toHaveBeenCalledWith({
- index: '.ds-logs-elastic_analytics.events-example-*',
- allow_no_indices: false,
+ expect(mockClient.asCurrentUser.indices.getDataStream).toHaveBeenCalledWith({
+ name: 'example',
});
});
});
diff --git a/x-pack/plugins/enterprise_search/server/lib/analytics/analytics_events_index_exists.ts b/x-pack/plugins/enterprise_search/server/lib/analytics/analytics_events_index_exists.ts
index 0d5ffad5a291a..d9549c42494f6 100644
--- a/x-pack/plugins/enterprise_search/server/lib/analytics/analytics_events_index_exists.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/analytics/analytics_events_index_exists.ts
@@ -7,16 +7,21 @@
import { IScopedClusterClient } from '@kbn/core-elasticsearch-server';
-const getFullIndexName = (indexName: string): string => {
- return `.ds-logs-elastic_analytics.events-${indexName}-*`;
-};
+import { isIndexNotFoundException } from '../../utils/identify_exceptions';
export const analyticsEventsIndexExists = async (
client: IScopedClusterClient,
- indexName: string
+ datastreamName: string
): Promise => {
- return await client.asCurrentUser.indices.exists({
- index: getFullIndexName(indexName),
- allow_no_indices: false,
- });
+ try {
+ const response = await client.asCurrentUser.indices.getDataStream({
+ name: datastreamName,
+ });
+ return response.data_streams.length > 0;
+ } catch (error) {
+ if (isIndexNotFoundException(error)) {
+ return false;
+ }
+ throw error;
+ }
};
diff --git a/x-pack/plugins/enterprise_search/server/lib/analytics/delete_analytics_collection.test.ts b/x-pack/plugins/enterprise_search/server/lib/analytics/delete_analytics_collection.test.ts
index 70fcd3f416431..06a08f1cc8848 100644
--- a/x-pack/plugins/enterprise_search/server/lib/analytics/delete_analytics_collection.test.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/analytics/delete_analytics_collection.test.ts
@@ -7,22 +7,16 @@
import { IScopedClusterClient } from '@kbn/core-elasticsearch-server';
-import { ANALYTICS_COLLECTIONS_INDEX } from '../..';
-import { AnalyticsCollection } from '../../../common/types/analytics';
-
import { ErrorCode } from '../../../common/types/error_codes';
import { deleteAnalyticsCollectionById } from './delete_analytics_collection';
-import { fetchAnalyticsCollectionById } from './fetch_analytics_collection';
-
-jest.mock('./fetch_analytics_collection', () => ({
- fetchAnalyticsCollectionById: jest.fn(),
-}));
describe('delete analytics collection lib function', () => {
const mockClient = {
asCurrentUser: {
- delete: jest.fn(),
+ transport: {
+ request: jest.fn(),
+ },
},
asInternalUser: {},
};
@@ -33,34 +27,32 @@ describe('delete analytics collection lib function', () => {
describe('deleting analytics collections', () => {
it('should delete an analytics collection', async () => {
- (fetchAnalyticsCollectionById as jest.Mock).mockImplementationOnce(() => {
- return Promise.resolve({
- event_retention_day_length: 180,
- id: 'example',
- name: 'example',
- } as AnalyticsCollection);
- });
-
await expect(
deleteAnalyticsCollectionById(mockClient as unknown as IScopedClusterClient, 'example')
).resolves.toBeUndefined();
- expect(mockClient.asCurrentUser.delete).toHaveBeenCalledWith({
- id: 'example',
- index: ANALYTICS_COLLECTIONS_INDEX,
+ expect(mockClient.asCurrentUser.transport.request).toHaveBeenCalledWith({
+ method: 'DELETE',
+ path: '/_application/analytics/example',
});
});
it('should throw an exception when analytics collection does not exist', async () => {
- (fetchAnalyticsCollectionById as jest.Mock).mockImplementationOnce(() =>
- Promise.resolve(undefined)
+ mockClient.asCurrentUser.transport.request.mockImplementation(() =>
+ Promise.reject({
+ meta: {
+ body: {
+ error: {
+ type: 'resource_not_found_exception',
+ },
+ },
+ },
+ })
);
await expect(
deleteAnalyticsCollectionById(mockClient as unknown as IScopedClusterClient, 'example')
).rejects.toEqual(new Error(ErrorCode.ANALYTICS_COLLECTION_NOT_FOUND));
-
- expect(mockClient.asCurrentUser.delete).not.toHaveBeenCalled();
});
});
});
diff --git a/x-pack/plugins/enterprise_search/server/lib/analytics/delete_analytics_collection.ts b/x-pack/plugins/enterprise_search/server/lib/analytics/delete_analytics_collection.ts
index eeaf7a73d8760..c305d1a4b1031 100644
--- a/x-pack/plugins/enterprise_search/server/lib/analytics/delete_analytics_collection.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/analytics/delete_analytics_collection.ts
@@ -7,21 +7,23 @@
import { IScopedClusterClient } from '@kbn/core-elasticsearch-server';
-import { ANALYTICS_COLLECTIONS_INDEX } from '../..';
-
import { ErrorCode } from '../../../common/types/error_codes';
+import { isResourceNotFoundException } from '../../utils/identify_exceptions';
-import { fetchAnalyticsCollectionById } from './fetch_analytics_collection';
-
-export const deleteAnalyticsCollectionById = async (client: IScopedClusterClient, id: string) => {
- const analyticsCollection = await fetchAnalyticsCollectionById(client, id);
+interface CollectionsDeleteResponse {
+ acknowledged: boolean;
+}
- if (!analyticsCollection) {
- throw new Error(ErrorCode.ANALYTICS_COLLECTION_NOT_FOUND);
+export const deleteAnalyticsCollectionById = async (client: IScopedClusterClient, name: string) => {
+ try {
+ await client.asCurrentUser.transport.request({
+ method: 'DELETE',
+ path: `/_application/analytics/${name}`,
+ });
+ } catch (error) {
+ if (isResourceNotFoundException(error)) {
+ throw new Error(ErrorCode.ANALYTICS_COLLECTION_NOT_FOUND);
+ }
+ throw error;
}
-
- await client.asCurrentUser.delete({
- id: analyticsCollection.id,
- index: ANALYTICS_COLLECTIONS_INDEX,
- });
};
diff --git a/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection.test.ts b/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection.test.ts
index 6b2b612e6f8c2..83ef91599f554 100644
--- a/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection.test.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection.test.ts
@@ -7,23 +7,14 @@
import { IScopedClusterClient } from '@kbn/core-elasticsearch-server';
-import { ANALYTICS_COLLECTIONS_INDEX } from '../..';
-
-import {
- fetchAnalyticsCollectionById,
- fetchAnalyticsCollections,
-} from './fetch_analytics_collection';
-import { setupAnalyticsCollectionIndex } from './setup_indices';
-
-jest.mock('./setup_indices', () => ({
- setupAnalyticsCollectionIndex: jest.fn(),
-}));
+import { fetchAnalyticsCollections } from './fetch_analytics_collection';
describe('fetch analytics collection lib function', () => {
const mockClient = {
asCurrentUser: {
- get: jest.fn(),
- search: jest.fn(),
+ transport: {
+ request: jest.fn(),
+ },
},
asInternalUser: {},
};
@@ -34,131 +25,43 @@ describe('fetch analytics collection lib function', () => {
describe('fetch collections', () => {
it('should return a list of analytics collections', async () => {
- mockClient.asCurrentUser.search.mockImplementationOnce(() =>
+ mockClient.asCurrentUser.transport.request.mockImplementation(() =>
Promise.resolve({
- hits: {
- hits: [
- { _id: '2', _source: { name: 'example' } },
- { _id: '1', _source: { name: 'example2' } },
- ],
+ example: {
+ event_data_stream: {
+ name: 'datastream-example',
+ },
},
- })
- );
- await expect(
- fetchAnalyticsCollections(mockClient as unknown as IScopedClusterClient)
- ).resolves.toEqual([
- { id: '2', name: 'example' },
- { id: '1', name: 'example2' },
- ]);
- });
-
- it('should setup the indexes if none exist and return an empty array', async () => {
- mockClient.asCurrentUser.search.mockImplementationOnce(() =>
- Promise.reject({
- meta: {
- body: {
- error: {
- type: 'index_not_found_exception',
- },
+ exampleTwo: {
+ event_data_stream: {
+ name: 'datastream-exampleTwo',
},
},
})
);
-
- await expect(
- fetchAnalyticsCollections(mockClient as unknown as IScopedClusterClient)
- ).resolves.toEqual([]);
-
- expect(setupAnalyticsCollectionIndex as jest.Mock).toHaveBeenCalledWith(
- mockClient.asCurrentUser
- );
- });
-
- it('should not call setup analytics index on other errors and return error', async () => {
- const error = {
- meta: {
- body: {
- error: {
- type: 'other error',
- },
- },
- },
- };
- mockClient.asCurrentUser.search.mockImplementationOnce(() => Promise.reject(error));
await expect(
fetchAnalyticsCollections(mockClient as unknown as IScopedClusterClient)
- ).rejects.toMatchObject(error);
-
- expect(mockClient.asCurrentUser.search).toHaveBeenCalledWith({
- from: 0,
- index: ANALYTICS_COLLECTIONS_INDEX,
- query: {
- match_all: {},
- },
- size: 1000,
- });
- expect(setupAnalyticsCollectionIndex as jest.Mock).not.toHaveBeenCalled();
+ ).resolves.toEqual([
+ { name: 'example', events_datastream: 'datastream-example' },
+ { name: 'exampleTwo', events_datastream: 'datastream-exampleTwo' },
+ ]);
});
});
describe('fetch collection by Id', () => {
it('should fetch analytics collection by Id', async () => {
- mockClient.asCurrentUser.get.mockImplementationOnce(() =>
- Promise.resolve({ _id: 'example', _source: { name: 'example' } })
- );
-
- await expect(
- fetchAnalyticsCollectionById(mockClient as unknown as IScopedClusterClient, 'example')
- ).resolves.toEqual({ id: 'example', name: 'example' });
-
- expect(mockClient.asCurrentUser.get).toHaveBeenCalledWith({
- id: 'example',
- index: ANALYTICS_COLLECTIONS_INDEX,
- });
- });
-
- it('should call setup analytics collection index on index not found error', async () => {
- mockClient.asCurrentUser.get.mockImplementationOnce(() =>
- Promise.reject({
- meta: {
- body: {
- error: { type: 'index_not_found_exception' },
+ mockClient.asCurrentUser.transport.request.mockImplementation(() =>
+ Promise.resolve({
+ example: {
+ event_data_stream: {
+ name: 'datastream-example',
},
},
})
);
await expect(
- fetchAnalyticsCollectionById(mockClient as unknown as IScopedClusterClient, 'example')
- ).resolves.toEqual(undefined);
- expect(mockClient.asCurrentUser.get).toHaveBeenCalledWith({
- id: 'example',
- index: ANALYTICS_COLLECTIONS_INDEX,
- });
- expect(setupAnalyticsCollectionIndex as jest.Mock).toHaveBeenCalledWith(
- mockClient.asCurrentUser
- );
- });
-
- it('should not call setup analytics indices on other errors', async () => {
- mockClient.asCurrentUser.get.mockImplementationOnce(() =>
- Promise.reject({
- meta: {
- body: {
- error: {
- type: 'other error',
- },
- },
- },
- })
- );
- await expect(fetchAnalyticsCollectionById(mockClient as any, 'example')).resolves.toEqual(
- undefined
- );
- expect(mockClient.asCurrentUser.get).toHaveBeenCalledWith({
- id: 'example',
- index: ANALYTICS_COLLECTIONS_INDEX,
- });
- expect(setupAnalyticsCollectionIndex as jest.Mock).not.toHaveBeenCalled();
+ fetchAnalyticsCollections(mockClient as unknown as IScopedClusterClient, 'example')
+ ).resolves.toEqual([{ name: 'example', events_datastream: 'datastream-example' }]);
});
});
});
diff --git a/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection.ts b/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection.ts
index 9244f2f10d01a..8baff2c85de44 100644
--- a/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection.ts
@@ -5,49 +5,41 @@
* 2.0.
*/
-import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { IScopedClusterClient } from '@kbn/core-elasticsearch-server';
-import { ANALYTICS_COLLECTIONS_INDEX } from '../..';
import { AnalyticsCollection } from '../../../common/types/analytics';
+import { ErrorCode } from '../../../common/types/error_codes';
-import { isIndexNotFoundException } from '../../utils/identify_exceptions';
-import { fetchAll } from '../fetch_all';
+import { isResourceNotFoundException } from '../../utils/identify_exceptions';
-import { setupAnalyticsCollectionIndex } from './setup_indices';
+interface CollectionsListResponse {
+ [name: string]: {
+ event_data_stream: {
+ name: string;
+ };
+ };
+}
-export const fetchAnalyticsCollectionById = async (
+export const fetchAnalyticsCollections = async (
client: IScopedClusterClient,
- id: string
-): Promise => {
+ query: string = ''
+): Promise => {
try {
- const hit = await client.asCurrentUser.get({
- id,
- index: ANALYTICS_COLLECTIONS_INDEX,
+ const collections = await client.asCurrentUser.transport.request({
+ method: 'GET',
+ path: `/_application/analytics/${query}`,
});
- const result = hit._source ? { ...hit._source, id: hit._id } : undefined;
-
- return result;
- } catch (error) {
- if (isIndexNotFoundException(error)) {
- await setupAnalyticsCollectionIndex(client.asCurrentUser);
- }
- return undefined;
- }
-};
-
-export const fetchAnalyticsCollections = async (
- client: IScopedClusterClient
-): Promise => {
- const query: QueryDslQueryContainer = { match_all: {} };
-
- try {
- return await fetchAll(client, ANALYTICS_COLLECTIONS_INDEX, query);
+ return Object.keys(collections).map((value) => {
+ const entry = collections[value];
+ return {
+ events_datastream: entry.event_data_stream.name,
+ name: value,
+ };
+ });
} catch (error) {
- if (isIndexNotFoundException(error)) {
- await setupAnalyticsCollectionIndex(client.asCurrentUser);
- return [];
+ if (isResourceNotFoundException(error)) {
+ throw new Error(ErrorCode.ANALYTICS_COLLECTION_NOT_FOUND);
}
throw error;
}
diff --git a/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection_data_view_id.test.ts b/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection_data_view_id.test.ts
index 1db6ccedfd920..abfecbe742ac5 100644
--- a/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection_data_view_id.test.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection_data_view_id.test.ts
@@ -11,15 +11,22 @@ import { DataViewsService } from '@kbn/data-views-plugin/common';
import { ErrorCode } from '../../../common/types/error_codes';
-import { fetchAnalyticsCollectionById } from './fetch_analytics_collection';
+import { fetchAnalyticsCollections } from './fetch_analytics_collection';
import { fetchAnalyticsCollectionDataViewId } from './fetch_analytics_collection_data_view_id';
jest.mock('./fetch_analytics_collection', () => ({
- fetchAnalyticsCollectionById: jest.fn(),
+ fetchAnalyticsCollections: jest.fn(),
}));
describe('fetch analytics collection data view id', () => {
- const mockClient = {};
+ const mockClient = {
+ asCurrentUser: {
+ transport: {
+ request: jest.fn(),
+ },
+ },
+ asInternalUser: {},
+ };
const dataViewService = { find: jest.fn() };
beforeEach(() => {
@@ -29,8 +36,8 @@ describe('fetch analytics collection data view id', () => {
it('should return data view id of analytics collection by Id', async () => {
const mockCollectionId = 'collectionId';
const mockDataViewId = 'dataViewId';
- const mockCollection = { events_datastream: 'log-collection-data-stream' };
- (fetchAnalyticsCollectionById as jest.Mock).mockImplementationOnce(() =>
+ const mockCollection = [{ name: 'example', events_datastream: 'log-collection-data-stream' }];
+ (fetchAnalyticsCollections as jest.Mock).mockImplementationOnce(() =>
Promise.resolve(mockCollection)
);
@@ -43,14 +50,14 @@ describe('fetch analytics collection data view id', () => {
mockCollectionId
)
).resolves.toEqual({ data_view_id: mockDataViewId });
- expect(fetchAnalyticsCollectionById).toHaveBeenCalledWith(mockClient, mockCollectionId);
- expect(dataViewService.find).toHaveBeenCalledWith(mockCollection.events_datastream, 1);
+ expect(fetchAnalyticsCollections).toHaveBeenCalledWith(mockClient, mockCollectionId);
+ expect(dataViewService.find).toHaveBeenCalledWith(mockCollection[0].events_datastream, 1);
});
it('should return null when data view not found', async () => {
const mockCollectionId = 'collectionId';
- const mockCollection = { events_datastream: 'log-collection-data-stream' };
- (fetchAnalyticsCollectionById as jest.Mock).mockImplementationOnce(() =>
+ const mockCollection = [{ events_datastream: 'log-collection-data-stream' }];
+ (fetchAnalyticsCollections as jest.Mock).mockImplementationOnce(() =>
Promise.resolve(mockCollection)
);
@@ -63,13 +70,16 @@ describe('fetch analytics collection data view id', () => {
mockCollectionId
)
).resolves.toEqual({ data_view_id: null });
- expect(fetchAnalyticsCollectionById).toHaveBeenCalledWith(mockClient, mockCollectionId);
- expect(dataViewService.find).toHaveBeenCalledWith(mockCollection.events_datastream, 1);
+ expect(fetchAnalyticsCollections).toHaveBeenCalledWith(mockClient, mockCollectionId);
+ expect(dataViewService.find).toHaveBeenCalledWith(mockCollection[0].events_datastream, 1);
});
it('should throw an error when analytics collection not found', async () => {
const mockCollectionId = 'collectionId';
- (fetchAnalyticsCollectionById as jest.Mock).mockImplementationOnce(() => Promise.resolve(null));
+
+ (fetchAnalyticsCollections as jest.Mock).mockImplementation(() => {
+ throw new Error(ErrorCode.ANALYTICS_COLLECTION_NOT_FOUND);
+ });
await expect(
fetchAnalyticsCollectionDataViewId(
@@ -78,7 +88,7 @@ describe('fetch analytics collection data view id', () => {
mockCollectionId
)
).rejects.toThrowError(ErrorCode.ANALYTICS_COLLECTION_NOT_FOUND);
- expect(fetchAnalyticsCollectionById).toHaveBeenCalledWith(mockClient, mockCollectionId);
+ expect(fetchAnalyticsCollections).toHaveBeenCalledWith(mockClient, mockCollectionId);
expect(dataViewService.find).not.toHaveBeenCalled();
});
});
diff --git a/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection_data_view_id.ts b/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection_data_view_id.ts
index 0ec07139d7b41..5d6967523ca9e 100644
--- a/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection_data_view_id.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection_data_view_id.ts
@@ -10,22 +10,16 @@ import { DataViewsService } from '@kbn/data-views-plugin/common';
import { AnalyticsCollectionDataViewId } from '../../../common/types/analytics';
-import { ErrorCode } from '../../../common/types/error_codes';
-
-import { fetchAnalyticsCollectionById } from './fetch_analytics_collection';
+import { fetchAnalyticsCollections } from './fetch_analytics_collection';
export const fetchAnalyticsCollectionDataViewId = async (
elasticsearchClient: IScopedClusterClient,
dataViewsService: DataViewsService,
- collectionId: string
+ collectionName: string
): Promise => {
- const collection = await fetchAnalyticsCollectionById(elasticsearchClient, collectionId);
-
- if (!collection) {
- throw new Error(ErrorCode.ANALYTICS_COLLECTION_NOT_FOUND);
- }
+ const collections = await fetchAnalyticsCollections(elasticsearchClient, collectionName);
- const collectionDataView = await dataViewsService.find(collection.events_datastream, 1);
+ const collectionDataView = await dataViewsService.find(collections[0].events_datastream, 1);
return { data_view_id: collectionDataView?.[0]?.id || null };
};
diff --git a/x-pack/plugins/enterprise_search/server/lib/analytics/setup_indices.test.ts b/x-pack/plugins/enterprise_search/server/lib/analytics/setup_indices.test.ts
deleted file mode 100644
index 0647022f23fa5..0000000000000
--- a/x-pack/plugins/enterprise_search/server/lib/analytics/setup_indices.test.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * 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; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { ANALYTICS_VERSION } from '../..';
-
-import { setupAnalyticsCollectionIndex } from './setup_indices';
-
-describe('setup analytics collection index', () => {
- const mockClient = {
- asCurrentUser: {
- indices: {
- create: jest.fn(),
- updateAliases: jest.fn(),
- },
- },
- asInternalUser: {},
- };
-
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- it("should create the analytics collection index when it doesn't exist", async () => {
- const indexName = '.elastic-analytics-collections';
- const analyticCollectionsMappings = {
- _meta: {
- version: ANALYTICS_VERSION,
- },
- properties: {
- event_retention_day_length: {
- type: 'long',
- },
- eventsDatastream: {
- type: 'keyword',
- },
- name: {
- type: 'keyword',
- },
- },
- };
-
- mockClient.asCurrentUser.indices.create.mockImplementation(() => Promise.resolve());
- mockClient.asCurrentUser.indices.updateAliases.mockImplementation(() => Promise.resolve());
- await expect(setupAnalyticsCollectionIndex(mockClient.asCurrentUser as any)).resolves.toEqual(
- undefined
- );
- expect(mockClient.asCurrentUser.indices.create).toHaveBeenCalledWith({
- index: `${indexName}-v${1}`,
- mappings: analyticCollectionsMappings,
- settings: { auto_expand_replicas: '0-3', hidden: true, number_of_replicas: 0 },
- });
- expect(mockClient.asCurrentUser.indices.updateAliases).toHaveBeenCalledWith({
- actions: [
- {
- add: {
- aliases: [indexName],
- index: `${indexName}-v${1}`,
- is_hidden: true,
- is_write_index: true,
- },
- },
- ],
- });
- });
-
- it('should do nothing if it hits that resource already exists', async () => {
- mockClient.asCurrentUser.indices.create.mockImplementation(() =>
- Promise.reject({ meta: { body: { error: { type: 'resource_already_exists_exception' } } } })
- );
- await expect(setupAnalyticsCollectionIndex(mockClient.asCurrentUser as any)).resolves.toEqual(
- undefined
- );
- expect(mockClient.asCurrentUser.indices.updateAliases).not.toHaveBeenCalled();
- expect(mockClient.asCurrentUser.indices.create).toHaveBeenCalled();
- });
-});
diff --git a/x-pack/plugins/enterprise_search/server/lib/analytics/setup_indices.ts b/x-pack/plugins/enterprise_search/server/lib/analytics/setup_indices.ts
deleted file mode 100644
index e007fdecad458..0000000000000
--- a/x-pack/plugins/enterprise_search/server/lib/analytics/setup_indices.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * 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; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import {
- IndicesIndexSettings,
- MappingProperty,
- MappingTypeMapping,
-} from '@elastic/elasticsearch/lib/api/types';
-import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
-
-import { ANALYTICS_COLLECTIONS_INDEX, ANALYTICS_VERSION } from '../..';
-import { isResourceAlreadyExistsException } from '../../utils/identify_exceptions';
-
-const analyticsCollectionMappingsProperties: Record = {
- event_retention_day_length: {
- type: 'long',
- },
- eventsDatastream: {
- type: 'keyword',
- },
- name: {
- type: 'keyword',
- },
-};
-
-const defaultSettings: IndicesIndexSettings = {
- auto_expand_replicas: '0-3',
- hidden: true,
- number_of_replicas: 0,
-};
-
-interface IndexDefinition {
- aliases: string[];
- mappings: MappingTypeMapping;
- name: string;
- settings: IndicesIndexSettings;
-}
-
-export const setupAnalyticsCollectionIndex = async (client: ElasticsearchClient) => {
- const indexConfiguration: IndexDefinition = {
- aliases: [ANALYTICS_COLLECTIONS_INDEX],
- mappings: {
- _meta: {
- version: ANALYTICS_VERSION,
- },
- properties: analyticsCollectionMappingsProperties,
- },
- name: `${ANALYTICS_COLLECTIONS_INDEX}-v${ANALYTICS_VERSION}`,
- settings: defaultSettings,
- };
-
- try {
- const { mappings, aliases, name: index, settings } = indexConfiguration;
- await client.indices.create({
- index,
- mappings,
- settings,
- });
- await client.indices.updateAliases({
- actions: [
- {
- add: {
- aliases,
- index,
- is_hidden: true,
- is_write_index: true,
- },
- },
- ],
- });
- } catch (error) {
- if (isResourceAlreadyExistsException(error)) {
- // index already exists, swallow error
- return;
- }
- return error;
- }
-};
diff --git a/x-pack/plugins/enterprise_search/server/lib/engines/create_api_key.test.ts b/x-pack/plugins/enterprise_search/server/lib/engines/create_api_key.test.ts
index ca076fe436248..474a1c84e16fa 100644
--- a/x-pack/plugins/enterprise_search/server/lib/engines/create_api_key.test.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/engines/create_api_key.test.ts
@@ -11,7 +11,7 @@ import { createApiKey } from './create_api_key';
describe('createApiKey lib function', () => {
const engineName = 'my-index';
- const keyName = 'Engine read only key';
+ const keyName = 'Search alias read only key';
const createResponse = {
api_key: 'ui2lp2axTNmsyakw9tvNnw',
@@ -38,14 +38,14 @@ describe('createApiKey lib function', () => {
).resolves.toEqual(createResponse);
expect(mockClient.asCurrentUser.security.createApiKey).toHaveBeenCalledWith({
- name: 'Engine read only key',
+ name: 'Search alias read only key',
role_descriptors: {
'my-index-key-role': {
- applications: [
+ cluster: [],
+ indices: [
{
- application: 'enterprise-search',
- privileges: ['engine:read'],
- resources: ['engine:my-index'],
+ names: [`${engineName}`],
+ privileges: ['read'],
},
],
},
diff --git a/x-pack/plugins/enterprise_search/server/lib/engines/create_api_key.ts b/x-pack/plugins/enterprise_search/server/lib/engines/create_api_key.ts
index 508ed6d56bd59..8c8c780dc461d 100644
--- a/x-pack/plugins/enterprise_search/server/lib/engines/create_api_key.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/engines/create_api_key.ts
@@ -16,11 +16,11 @@ export const createApiKey = async (
name: keyName,
role_descriptors: {
[`${engineName}-key-role`]: {
- applications: [
+ cluster: [],
+ indices: [
{
- application: 'enterprise-search',
- privileges: ['engine:read'],
- resources: [`engine:${engineName}`],
+ names: [`${engineName}`],
+ privileges: ['read'],
},
],
},
diff --git a/x-pack/plugins/enterprise_search/server/lib/engines/field_capabilities.test.ts b/x-pack/plugins/enterprise_search/server/lib/engines/field_capabilities.test.ts
index 3e250e4ec9109..ba3bfe3b96a25 100644
--- a/x-pack/plugins/enterprise_search/server/lib/engines/field_capabilities.test.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/engines/field_capabilities.test.ts
@@ -8,9 +8,9 @@
import { FieldCapsResponse } from '@elastic/elasticsearch/lib/api/types';
import { IScopedClusterClient } from '@kbn/core-elasticsearch-server';
-import { EnterpriseSearchEngineDetails } from '../../../common/types/engines';
+import { EnterpriseSearchEngineDetails, SchemaField } from '../../../common/types/engines';
-import { fetchEngineFieldCapabilities } from './field_capabilities';
+import { fetchEngineFieldCapabilities, parseFieldsCapabilities } from './field_capabilities';
describe('engines field_capabilities', () => {
const mockClient = {
@@ -20,33 +20,1088 @@ describe('engines field_capabilities', () => {
asInternalUser: {},
};
const mockEngine: EnterpriseSearchEngineDetails = {
- created: '1999-12-31T23:59:59.999Z',
indices: [],
name: 'unit-test-engine',
- updated: '1999-12-31T23:59:59.999Z',
+ updated_at_millis: 2202018295,
};
beforeEach(() => {
jest.clearAllMocks();
});
- it('gets engine alias field capabilities', async () => {
- const fieldCapsResponse = {} as FieldCapsResponse;
+ describe('fetchEngineFieldCapabilities', () => {
+ it('gets engine alias field capabilities', async () => {
+ const fieldCapsResponse: FieldCapsResponse = {
+ fields: {
+ body: {
+ text: {
+ aggregatable: false,
+ metadata_field: false,
+ searchable: true,
+ type: 'text',
+ },
+ },
+ },
+ indices: ['index-001'],
+ };
- mockClient.asCurrentUser.fieldCaps.mockResolvedValueOnce(fieldCapsResponse);
- await expect(
- fetchEngineFieldCapabilities(mockClient as unknown as IScopedClusterClient, mockEngine)
- ).resolves.toEqual({
- created: mockEngine.created,
- field_capabilities: fieldCapsResponse,
- name: mockEngine.name,
- updated: mockEngine.updated,
+ mockClient.asCurrentUser.fieldCaps.mockResolvedValueOnce(fieldCapsResponse);
+ await expect(
+ fetchEngineFieldCapabilities(mockClient as unknown as IScopedClusterClient, mockEngine)
+ ).resolves.toEqual({
+ field_capabilities: fieldCapsResponse,
+ fields: [
+ {
+ fields: [],
+ indices: [
+ {
+ name: 'index-001',
+ type: 'text',
+ },
+ ],
+ name: 'body',
+ type: 'text',
+ },
+ ],
+ name: mockEngine.name,
+ updated_at_millis: mockEngine.updated_at_millis,
+ });
+
+ expect(mockClient.asCurrentUser.fieldCaps).toHaveBeenCalledTimes(1);
+ expect(mockClient.asCurrentUser.fieldCaps).toHaveBeenCalledWith({
+ fields: '*',
+ include_unmapped: true,
+ index: 'search-engine-unit-test-engine',
+ });
});
+ });
- expect(mockClient.asCurrentUser.fieldCaps).toHaveBeenCalledTimes(1);
- expect(mockClient.asCurrentUser.fieldCaps).toHaveBeenCalledWith({
- fields: '*',
- include_unmapped: true,
- index: 'search-engine-unit-test-engine',
+ describe('parseFieldsCapabilities', () => {
+ it('parse field capabilities to a list of fields', () => {
+ const fieldCapabilities: FieldCapsResponse = {
+ fields: {
+ body: {
+ text: {
+ aggregatable: false,
+ metadata_field: false,
+ searchable: true,
+ type: 'text',
+ },
+ },
+ views: {
+ number: {
+ aggregatable: false,
+ metadata_field: false,
+ searchable: false,
+ type: 'number',
+ },
+ },
+ },
+ indices: ['index-001'],
+ };
+ const expectedFields: SchemaField[] = [
+ {
+ fields: [],
+ indices: [
+ {
+ name: 'index-001',
+ type: 'text',
+ },
+ ],
+ name: 'body',
+ type: 'text',
+ },
+ {
+ fields: [],
+ indices: [
+ {
+ name: 'index-001',
+ type: 'number',
+ },
+ ],
+ name: 'views',
+ type: 'number',
+ },
+ ];
+ expect(parseFieldsCapabilities(fieldCapabilities)).toEqual(expectedFields);
+ });
+ it('handles multi-fields', () => {
+ const fieldCapabilities: FieldCapsResponse = {
+ fields: {
+ body: {
+ text: {
+ aggregatable: false,
+ metadata_field: false,
+ searchable: true,
+ type: 'text',
+ },
+ },
+ 'body.keyword': {
+ keyword: {
+ aggregatable: true,
+ metadata_field: false,
+ searchable: true,
+ type: 'keyword',
+ },
+ },
+ },
+ indices: ['index-001'],
+ };
+ const expectedFields: SchemaField[] = [
+ {
+ fields: [
+ {
+ fields: [],
+ indices: [
+ {
+ name: 'index-001',
+ type: 'keyword',
+ },
+ ],
+ name: 'keyword',
+ type: 'keyword',
+ },
+ ],
+ indices: [
+ {
+ name: 'index-001',
+ type: 'text',
+ },
+ ],
+ name: 'body',
+ type: 'text',
+ },
+ ];
+ expect(parseFieldsCapabilities(fieldCapabilities)).toEqual(expectedFields);
+ });
+ it('handles object fields', () => {
+ const fieldCapabilities: FieldCapsResponse = {
+ fields: {
+ name: {
+ object: {
+ aggregatable: false,
+ metadata_field: false,
+ searchable: false,
+ type: 'object',
+ },
+ },
+ 'name.first': {
+ text: {
+ aggregatable: false,
+ metadata_field: false,
+ searchable: true,
+ type: 'text',
+ },
+ },
+ 'name.last': {
+ text: {
+ aggregatable: false,
+ metadata_field: false,
+ searchable: true,
+ type: 'text',
+ },
+ },
+ },
+ indices: ['index-001'],
+ };
+ const expectedFields: SchemaField[] = [
+ {
+ fields: [
+ {
+ fields: [],
+ indices: [
+ {
+ name: 'index-001',
+ type: 'text',
+ },
+ ],
+ name: 'first',
+ type: 'text',
+ },
+ {
+ fields: [],
+ indices: [
+ {
+ name: 'index-001',
+ type: 'text',
+ },
+ ],
+ name: 'last',
+ type: 'text',
+ },
+ ],
+ indices: [
+ {
+ name: 'index-001',
+ type: 'object',
+ },
+ ],
+ name: 'name',
+ type: 'object',
+ },
+ ];
+ expect(parseFieldsCapabilities(fieldCapabilities)).toEqual(expectedFields);
+ });
+ it('handles nested fields', () => {
+ const fieldCapabilities: FieldCapsResponse = {
+ fields: {
+ name: {
+ nested: {
+ aggregatable: false,
+ metadata_field: false,
+ searchable: false,
+ type: 'nested',
+ },
+ },
+ 'name.first': {
+ text: {
+ aggregatable: false,
+ metadata_field: false,
+ searchable: true,
+ type: 'text',
+ },
+ },
+ 'name.last': {
+ text: {
+ aggregatable: false,
+ metadata_field: false,
+ searchable: true,
+ type: 'text',
+ },
+ },
+ },
+ indices: ['index-001'],
+ };
+ const expectedFields: SchemaField[] = [
+ {
+ fields: [
+ {
+ fields: [],
+ indices: [
+ {
+ name: 'index-001',
+ type: 'text',
+ },
+ ],
+ name: 'first',
+ type: 'text',
+ },
+ {
+ fields: [],
+ indices: [
+ {
+ name: 'index-001',
+ type: 'text',
+ },
+ ],
+ name: 'last',
+ type: 'text',
+ },
+ ],
+ indices: [
+ {
+ name: 'index-001',
+ type: 'nested',
+ },
+ ],
+ name: 'name',
+ type: 'nested',
+ },
+ ];
+ expect(parseFieldsCapabilities(fieldCapabilities)).toEqual(expectedFields);
+ });
+ it('handles unmapped fields', () => {
+ const fieldCapabilities: FieldCapsResponse = {
+ fields: {
+ body: {
+ text: {
+ aggregatable: false,
+ indices: ['index-001'],
+ metadata_field: false,
+ searchable: true,
+ type: 'text',
+ },
+ unmapped: {
+ aggregatable: false,
+ indices: ['index-002'],
+ metadata_field: false,
+ searchable: true,
+ type: 'unmapped',
+ },
+ },
+ },
+ indices: ['index-001', 'index-002'],
+ };
+ const expectedFields: SchemaField[] = [
+ {
+ fields: [],
+ indices: [
+ {
+ name: 'index-001',
+ type: 'text',
+ },
+ {
+ name: 'index-002',
+ type: 'unmapped',
+ },
+ ],
+ name: 'body',
+ type: 'text',
+ },
+ ];
+ expect(parseFieldsCapabilities(fieldCapabilities)).toEqual(expectedFields);
+ });
+ it('handles conflicts in top-level fields', () => {
+ const fieldCapabilities: FieldCapsResponse = {
+ fields: {
+ name: {
+ object: {
+ aggregatable: false,
+ indices: ['index-002'],
+ metadata_field: false,
+ searchable: false,
+ type: 'object',
+ },
+ text: {
+ aggregatable: false,
+ indices: ['index-001'],
+ metadata_field: false,
+ searchable: true,
+ type: 'text',
+ },
+ },
+ 'name.first': {
+ text: {
+ aggregatable: false,
+ indices: ['index-002'],
+ metadata_field: false,
+ searchable: true,
+ type: 'text',
+ },
+ unmapped: {
+ aggregatable: false,
+ indices: ['index-001'],
+ metadata_field: false,
+ searchable: true,
+ type: 'unmapped',
+ },
+ },
+ 'name.last': {
+ text: {
+ aggregatable: false,
+ indices: ['index-002'],
+ metadata_field: false,
+ searchable: true,
+ type: 'text',
+ },
+ unmapped: {
+ aggregatable: false,
+ indices: ['index-001'],
+ metadata_field: false,
+ searchable: true,
+ type: 'unmapped',
+ },
+ },
+ },
+ indices: ['index-001', 'index-002'],
+ };
+ const expectedFields: SchemaField[] = [
+ {
+ fields: [
+ {
+ fields: [],
+ indices: [
+ {
+ name: 'index-002',
+ type: 'text',
+ },
+ {
+ name: 'index-001',
+ type: 'unmapped',
+ },
+ ],
+ name: 'first',
+ type: 'text',
+ },
+ {
+ fields: [],
+ indices: [
+ {
+ name: 'index-002',
+ type: 'text',
+ },
+ {
+ name: 'index-001',
+ type: 'unmapped',
+ },
+ ],
+ name: 'last',
+ type: 'text',
+ },
+ ],
+ indices: [
+ {
+ name: 'index-002',
+ type: 'object',
+ },
+ {
+ name: 'index-001',
+ type: 'text',
+ },
+ ],
+ name: 'name',
+ type: 'conflict',
+ },
+ ];
+ expect(parseFieldsCapabilities(fieldCapabilities)).toEqual(expectedFields);
+ });
+ it('handles conflicts of more than two indices', () => {
+ const fieldCapabilities: FieldCapsResponse = {
+ fields: {
+ name: {
+ keyword: {
+ aggregatable: false,
+ indices: ['index-003'],
+ metadata_field: false,
+ searchable: true,
+ type: 'keyword',
+ },
+ object: {
+ aggregatable: false,
+ indices: ['index-002'],
+ metadata_field: false,
+ searchable: false,
+ type: 'object',
+ },
+ text: {
+ aggregatable: false,
+ indices: ['index-001'],
+ metadata_field: false,
+ searchable: true,
+ type: 'text',
+ },
+ },
+ 'name.first': {
+ text: {
+ aggregatable: false,
+ indices: ['index-002', 'index-003'],
+ metadata_field: false,
+ searchable: true,
+ type: 'text',
+ },
+ unmapped: {
+ aggregatable: false,
+ indices: ['index-001'],
+ metadata_field: false,
+ searchable: true,
+ type: 'unmapped',
+ },
+ },
+ 'name.last': {
+ text: {
+ aggregatable: false,
+ indices: ['index-002'],
+ metadata_field: false,
+ searchable: true,
+ type: 'text',
+ },
+ unmapped: {
+ aggregatable: false,
+ indices: ['index-001'],
+ metadata_field: false,
+ searchable: true,
+ type: 'unmapped',
+ },
+ },
+ },
+ indices: ['index-001', 'index-002', 'index-003'],
+ };
+
+ const expectedFields: SchemaField[] = [
+ {
+ fields: [
+ {
+ fields: [],
+ indices: [
+ {
+ name: 'index-002',
+ type: 'text',
+ },
+ {
+ name: 'index-003',
+ type: 'text',
+ },
+ {
+ name: 'index-001',
+ type: 'unmapped',
+ },
+ ],
+ name: 'first',
+ type: 'text',
+ },
+ {
+ fields: [],
+ indices: [
+ {
+ name: 'index-002',
+ type: 'text',
+ },
+ {
+ name: 'index-001',
+ type: 'unmapped',
+ },
+ ],
+ name: 'last',
+ type: 'text',
+ },
+ ],
+ indices: [
+ {
+ name: 'index-003',
+ type: 'keyword',
+ },
+ {
+ name: 'index-002',
+ type: 'object',
+ },
+ {
+ name: 'index-001',
+ type: 'text',
+ },
+ ],
+ name: 'name',
+ type: 'conflict',
+ },
+ ];
+ expect(parseFieldsCapabilities(fieldCapabilities)).toEqual(expectedFields);
+ });
+ it('handles conflicts & unmapped fields together', () => {
+ const fieldCapabilities: FieldCapsResponse = {
+ fields: {
+ body: {
+ text: {
+ aggregatable: false,
+ indices: ['index-003'],
+ metadata_field: false,
+ searchable: true,
+ type: 'text',
+ },
+ unmapped: {
+ aggregatable: false,
+ indices: ['index-001', 'index-002'],
+ metadata_field: false,
+ searchable: true,
+ type: 'unmapped',
+ },
+ },
+ name: {
+ object: {
+ aggregatable: false,
+ indices: ['index-002'],
+ metadata_field: false,
+ searchable: false,
+ type: 'object',
+ },
+ text: {
+ aggregatable: false,
+ indices: ['index-001'],
+ metadata_field: false,
+ searchable: true,
+ type: 'text',
+ },
+ unmapped: {
+ aggregatable: false,
+ indices: ['index-003'],
+ metadata_field: false,
+ searchable: true,
+ type: 'unmapped',
+ },
+ },
+ 'name.first': {
+ text: {
+ aggregatable: false,
+ indices: ['index-002'],
+ metadata_field: false,
+ searchable: true,
+ type: 'text',
+ },
+ unmapped: {
+ aggregatable: false,
+ indices: ['index-001', 'index-003'],
+ metadata_field: false,
+ searchable: true,
+ type: 'unmapped',
+ },
+ },
+ 'name.last': {
+ text: {
+ aggregatable: false,
+ indices: ['index-002'],
+ metadata_field: false,
+ searchable: true,
+ type: 'text',
+ },
+ unmapped: {
+ aggregatable: false,
+ indices: ['index-001', 'index-003'],
+ metadata_field: false,
+ searchable: true,
+ type: 'unmapped',
+ },
+ },
+ },
+ indices: ['index-001', 'index-002', 'index-003'],
+ };
+ const expectedFields: SchemaField[] = [
+ {
+ fields: [],
+ indices: [
+ {
+ name: 'index-003',
+ type: 'text',
+ },
+ {
+ name: 'index-001',
+ type: 'unmapped',
+ },
+ {
+ name: 'index-002',
+ type: 'unmapped',
+ },
+ ],
+ name: 'body',
+ type: 'text',
+ },
+ {
+ fields: [
+ {
+ fields: [],
+ indices: [
+ {
+ name: 'index-002',
+ type: 'text',
+ },
+ {
+ name: 'index-001',
+ type: 'unmapped',
+ },
+ {
+ name: 'index-003',
+ type: 'unmapped',
+ },
+ ],
+ name: 'first',
+ type: 'text',
+ },
+ {
+ fields: [],
+ indices: [
+ {
+ name: 'index-002',
+ type: 'text',
+ },
+ {
+ name: 'index-001',
+ type: 'unmapped',
+ },
+ {
+ name: 'index-003',
+ type: 'unmapped',
+ },
+ ],
+ name: 'last',
+ type: 'text',
+ },
+ ],
+ indices: [
+ {
+ name: 'index-002',
+ type: 'object',
+ },
+ {
+ name: 'index-001',
+ type: 'text',
+ },
+ {
+ name: 'index-003',
+ type: 'unmapped',
+ },
+ ],
+ name: 'name',
+ type: 'conflict',
+ },
+ ];
+ expect(parseFieldsCapabilities(fieldCapabilities)).toEqual(expectedFields);
+ });
+ it('handles unmapped sub-fields in object fields', () => {
+ const fieldCapabilities: FieldCapsResponse = {
+ fields: {
+ name: {
+ object: {
+ aggregatable: false,
+ metadata_field: false,
+ searchable: false,
+ type: 'object',
+ },
+ },
+ 'name.first': {
+ text: {
+ aggregatable: false,
+ metadata_field: false,
+ searchable: true,
+ type: 'text',
+ },
+ },
+ 'name.last': {
+ text: {
+ aggregatable: false,
+ indices: ['index-001'],
+ metadata_field: false,
+ searchable: true,
+ type: 'text',
+ },
+ unmapped: {
+ aggregatable: false,
+ indices: ['index-002'],
+ metadata_field: false,
+ searchable: true,
+ type: 'unmapped',
+ },
+ },
+ },
+ indices: ['index-001', 'index-002'],
+ };
+ const expectedFields: SchemaField[] = [
+ {
+ fields: [
+ {
+ fields: [],
+ indices: [
+ {
+ name: 'index-001',
+ type: 'text',
+ },
+ {
+ name: 'index-002',
+ type: 'text',
+ },
+ ],
+ name: 'first',
+ type: 'text',
+ },
+ {
+ fields: [],
+ indices: [
+ {
+ name: 'index-001',
+ type: 'text',
+ },
+ {
+ name: 'index-002',
+ type: 'unmapped',
+ },
+ ],
+ name: 'last',
+ type: 'text',
+ },
+ ],
+ indices: [
+ {
+ name: 'index-001',
+ type: 'object',
+ },
+ {
+ name: 'index-002',
+ type: 'object',
+ },
+ ],
+ name: 'name',
+ type: 'object',
+ },
+ ];
+ expect(parseFieldsCapabilities(fieldCapabilities)).toEqual(expectedFields);
+ });
+ it('handles unmapped sub-fields in nested fields', () => {
+ const fieldCapabilities: FieldCapsResponse = {
+ fields: {
+ name: {
+ nested: {
+ aggregatable: false,
+ metadata_field: false,
+ searchable: false,
+ type: 'nested',
+ },
+ },
+ 'name.first': {
+ text: {
+ aggregatable: false,
+ metadata_field: false,
+ searchable: true,
+ type: 'text',
+ },
+ },
+ 'name.last': {
+ text: {
+ aggregatable: false,
+ indices: ['index-001'],
+ metadata_field: false,
+ searchable: true,
+ type: 'text',
+ },
+ unmapped: {
+ aggregatable: false,
+ indices: ['index-002'],
+ metadata_field: false,
+ searchable: true,
+ type: 'unmapped',
+ },
+ },
+ },
+ indices: ['index-001', 'index-002'],
+ };
+ const expectedFields: SchemaField[] = [
+ {
+ fields: [
+ {
+ fields: [],
+ indices: [
+ {
+ name: 'index-001',
+ type: 'text',
+ },
+ {
+ name: 'index-002',
+ type: 'text',
+ },
+ ],
+ name: 'first',
+ type: 'text',
+ },
+ {
+ fields: [],
+ indices: [
+ {
+ name: 'index-001',
+ type: 'text',
+ },
+ {
+ name: 'index-002',
+ type: 'unmapped',
+ },
+ ],
+ name: 'last',
+ type: 'text',
+ },
+ ],
+ indices: [
+ {
+ name: 'index-001',
+ type: 'nested',
+ },
+ {
+ name: 'index-002',
+ type: 'nested',
+ },
+ ],
+ name: 'name',
+ type: 'nested',
+ },
+ ];
+ expect(parseFieldsCapabilities(fieldCapabilities)).toEqual(expectedFields);
+ });
+ it('handles unmapped multi fields', () => {
+ const fieldCapabilities: FieldCapsResponse = {
+ fields: {
+ body: {
+ text: {
+ aggregatable: false,
+ metadata_field: false,
+ searchable: true,
+ type: 'text',
+ },
+ },
+ 'body.keyword': {
+ keyword: {
+ aggregatable: true,
+ indices: ['index-001'],
+ metadata_field: false,
+ searchable: true,
+ type: 'keyword',
+ },
+ unmapped: {
+ aggregatable: false,
+ indices: ['index-002'],
+ metadata_field: false,
+ searchable: true,
+ type: 'unmapped',
+ },
+ },
+ },
+ indices: ['index-001', 'index-002'],
+ };
+ const expectedFields: SchemaField[] = [
+ {
+ fields: [
+ {
+ fields: [],
+ indices: [
+ {
+ name: 'index-001',
+ type: 'keyword',
+ },
+ {
+ name: 'index-002',
+ type: 'unmapped',
+ },
+ ],
+ name: 'keyword',
+ type: 'keyword',
+ },
+ ],
+ indices: [
+ {
+ name: 'index-001',
+ type: 'text',
+ },
+ {
+ name: 'index-002',
+ type: 'text',
+ },
+ ],
+ name: 'body',
+ type: 'text',
+ },
+ ];
+ expect(parseFieldsCapabilities(fieldCapabilities)).toEqual(expectedFields);
+ });
+ it('handles conflicts in object fields', () => {
+ const fieldCapabilities: FieldCapsResponse = {
+ fields: {
+ order: {
+ object: {
+ aggregatable: false,
+ metadata_field: false,
+ searchable: false,
+ type: 'object',
+ },
+ },
+ 'order.id': {
+ number: {
+ aggregatable: false,
+ indices: ['index-002'],
+ metadata_field: false,
+ searchable: false,
+ type: 'number',
+ },
+ text: {
+ aggregatable: false,
+ indices: ['index-001'],
+ metadata_field: false,
+ searchable: true,
+ type: 'text',
+ },
+ },
+ },
+ indices: ['index-001', 'index-002'],
+ };
+ const expectedFields: SchemaField[] = [
+ {
+ fields: [
+ {
+ fields: [],
+ indices: [
+ {
+ name: 'index-002',
+ type: 'number',
+ },
+ {
+ name: 'index-001',
+ type: 'text',
+ },
+ ],
+ name: 'id',
+ type: 'conflict',
+ },
+ ],
+ indices: [
+ {
+ name: 'index-001',
+ type: 'object',
+ },
+ {
+ name: 'index-002',
+ type: 'object',
+ },
+ ],
+ name: 'order',
+ type: 'object', // Should this be 'conflict' too?
+ },
+ ];
+ expect(parseFieldsCapabilities(fieldCapabilities)).toEqual(expectedFields);
+ });
+ it('handles conflicts in nested fields', () => {
+ const fieldCapabilities: FieldCapsResponse = {
+ fields: {
+ order: {
+ nested: {
+ aggregatable: false,
+ metadata_field: false,
+ searchable: false,
+ type: 'nested',
+ },
+ },
+ 'order.id': {
+ number: {
+ aggregatable: false,
+ indices: ['index-002'],
+ metadata_field: false,
+ searchable: false,
+ type: 'number',
+ },
+ text: {
+ aggregatable: false,
+ indices: ['index-001'],
+ metadata_field: false,
+ searchable: true,
+ type: 'text',
+ },
+ },
+ },
+ indices: ['index-001', 'index-002'],
+ };
+ const expectedFields: SchemaField[] = [
+ {
+ fields: [
+ {
+ fields: [],
+ indices: [
+ {
+ name: 'index-002',
+ type: 'number',
+ },
+ {
+ name: 'index-001',
+ type: 'text',
+ },
+ ],
+ name: 'id',
+ type: 'conflict',
+ },
+ ],
+ indices: [
+ {
+ name: 'index-001',
+ type: 'nested',
+ },
+ {
+ name: 'index-002',
+ type: 'nested',
+ },
+ ],
+ name: 'order',
+ type: 'nested', // Should this be 'conflict' too?
+ },
+ ];
+ expect(parseFieldsCapabilities(fieldCapabilities)).toEqual(expectedFields);
});
});
});
diff --git a/x-pack/plugins/enterprise_search/server/lib/engines/field_capabilities.ts b/x-pack/plugins/enterprise_search/server/lib/engines/field_capabilities.ts
index ed42ab744621c..a9d1025123b53 100644
--- a/x-pack/plugins/enterprise_search/server/lib/engines/field_capabilities.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/engines/field_capabilities.ts
@@ -5,30 +5,88 @@
* 2.0.
*/
+import { FieldCapsResponse, FieldCapsFieldCapability } from '@elastic/elasticsearch/lib/api/types';
import { IScopedClusterClient } from '@kbn/core-elasticsearch-server';
import {
EnterpriseSearchEngineDetails,
EnterpriseSearchEngineFieldCapabilities,
+ SchemaField,
} from '../../../common/types/engines';
export const fetchEngineFieldCapabilities = async (
client: IScopedClusterClient,
engine: EnterpriseSearchEngineDetails
): Promise => {
- const { created, name, updated } = engine;
+ const { name, updated_at_millis } = engine;
const fieldCapabilities = await client.asCurrentUser.fieldCaps({
fields: '*',
include_unmapped: true,
index: getEngineIndexAliasName(name),
});
+ const fields = parseFieldsCapabilities(fieldCapabilities);
return {
- created,
field_capabilities: fieldCapabilities,
+ fields,
name,
- updated,
+ updated_at_millis,
};
};
+const ensureIndices = (indices: string | string[] | undefined): string[] => {
+ if (!indices) return [];
+ return Array.isArray(indices) ? indices : [indices];
+};
+
+export const parseFieldsCapabilities = (
+ fieldCapsResponse: FieldCapsResponse,
+ prefix: string = ''
+): SchemaField[] => {
+ const { fields, indices: indexOrIndices } = fieldCapsResponse;
+ const inThisPass: Array<[string, Record]> = Object.entries(
+ fields
+ )
+ .filter(([key]) => key.startsWith(prefix))
+ .map(([key, value]) => [key.replace(prefix, ''), value]);
+
+ const atThisLevel = inThisPass.filter(([key]) => !key.includes('.'));
+
+ return atThisLevel.map(([name, value]) => {
+ const type = calculateType(Object.keys(value));
+ let indices = Object.values(value).flatMap((fieldCaps) => {
+ return ensureIndices(fieldCaps.indices).map((index) => ({
+ name: index,
+ type: fieldCaps.type,
+ }));
+ });
+
+ indices =
+ indices.length === 0
+ ? ensureIndices(indexOrIndices).map((index) => ({ name: index, type }))
+ : indices;
+
+ const subFields = parseFieldsCapabilities(fieldCapsResponse, `${prefix}${name}.`);
+ return {
+ fields: subFields,
+ indices,
+ name,
+ type,
+ };
+ });
+};
+
+const calculateType = (types: string[]): string => {
+ // If there is only one type, return it
+ if (types.length === 1) return types[0];
+
+ // Unmapped types are ignored for the purposes of determining the type
+ // If all of the mapped types are the same, return that type
+ const mapped = types.filter((t) => t !== 'unmapped');
+ if (new Set(mapped).size === 1) return mapped[0];
+
+ // Otherwise there is a conflict
+ return 'conflict';
+};
+
// Note: This will likely need to be modified when engines move to es module
const getEngineIndexAliasName = (engineName: string): string => `search-engine-${engineName}`;
diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts
index 8135997afb79d..9f4c17f3915d8 100644
--- a/x-pack/plugins/enterprise_search/server/plugin.ts
+++ b/x-pack/plugins/enterprise_search/server/plugin.ts
@@ -266,7 +266,7 @@ export class EnterpriseSearchPlugin implements Plugin {
infra.logViews.defineInternalLogView(ENTERPRISE_SEARCH_ANALYTICS_LOGS_SOURCE_ID, {
logIndices: {
- indexName: 'logs-elastic_analytics.events-*',
+ indexName: 'behavioral_analytics-events-*',
type: 'index_name',
},
name: 'Enterprise Search Behavioral Analytics Logs',
diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.test.ts
index f216a23e58e0a..99dec4653e229 100644
--- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.test.ts
@@ -12,7 +12,7 @@ import { RequestHandlerContext } from '@kbn/core/server';
import { DataPluginStart } from '@kbn/data-plugin/server/plugin';
jest.mock('../../lib/analytics/fetch_analytics_collection', () => ({
- fetchAnalyticsCollectionById: jest.fn(),
+ fetchAnalyticsCollections: jest.fn(),
}));
jest.mock('../../lib/analytics/fetch_analytics_collection_data_view_id', () => ({
@@ -24,7 +24,7 @@ import {
AnalyticsCollectionDataViewId,
} from '../../../common/types/analytics';
import { ErrorCode } from '../../../common/types/error_codes';
-import { fetchAnalyticsCollectionById } from '../../lib/analytics/fetch_analytics_collection';
+import { fetchAnalyticsCollections } from '../../lib/analytics/fetch_analytics_collection';
import { fetchAnalyticsCollectionDataViewId } from '../../lib/analytics/fetch_analytics_collection_data_view_id';
import { registerAnalyticsRoutes } from './analytics';
@@ -42,7 +42,7 @@ describe('Enterprise Search Analytics API', () => {
mockRouter = new MockRouter({
context,
method: 'get',
- path: '/internal/enterprise_search/analytics/collections/{id}',
+ path: '/internal/enterprise_search/analytics/collections/{name}',
});
const mockDataPlugin = {
@@ -64,29 +64,29 @@ describe('Enterprise Search Analytics API', () => {
});
it('fetches a defined analytics collection name', async () => {
- const mockData: AnalyticsCollection = {
- event_retention_day_length: 30,
- events_datastream: 'logs-elastic_analytics.events-example',
- id: '1',
- name: 'my_collection',
- };
+ const mockData: AnalyticsCollection[] = [
+ {
+ events_datastream: 'logs-elastic_analytics.events-example',
+ name: 'my_collection',
+ },
+ ];
- (fetchAnalyticsCollectionById as jest.Mock).mockImplementationOnce(() => {
+ (fetchAnalyticsCollections as jest.Mock).mockImplementationOnce(() => {
return Promise.resolve(mockData);
});
- await mockRouter.callRoute({ params: { id: '1' } });
+ await mockRouter.callRoute({ params: { name: '1' } });
expect(mockRouter.response.ok).toHaveBeenCalledWith({
- body: mockData,
+ body: mockData[0],
});
});
it('throws a 404 error if data returns an empty obj', async () => {
- (fetchAnalyticsCollectionById as jest.Mock).mockImplementationOnce(() => {
- return Promise.resolve(undefined);
+ (fetchAnalyticsCollections as jest.Mock).mockImplementationOnce(() => {
+ throw new Error(ErrorCode.ANALYTICS_COLLECTION_NOT_FOUND);
});
await mockRouter.callRoute({
- params: { id: 'my_collection' },
+ params: { name: 'my_collection' },
});
expect(mockRouter.response.customError).toHaveBeenCalledWith({
@@ -101,7 +101,7 @@ describe('Enterprise Search Analytics API', () => {
});
});
- describe('GET /internal/enterprise_search/analytics/collections/{id}/data_view_id', () => {
+ describe('GET /internal/enterprise_search/analytics/collections/{name}/data_view_id', () => {
beforeEach(() => {
const context = {
core: Promise.resolve({ elasticsearch: { client: mockClient } }),
@@ -110,7 +110,7 @@ describe('Enterprise Search Analytics API', () => {
mockRouter = new MockRouter({
context,
method: 'get',
- path: '/internal/enterprise_search/analytics/collections/{id}/data_view_id',
+ path: '/internal/enterprise_search/analytics/collections/{name}/data_view_id',
});
const mockDataPlugin = {
@@ -131,7 +131,7 @@ describe('Enterprise Search Analytics API', () => {
});
});
- it('fetches a defined data view id by collection id', async () => {
+ it('fetches a defined data view id by collection name', async () => {
const mockData: AnalyticsCollectionDataViewId = {
data_view_id: '03fca-1234-5678-9abc-1234',
};
@@ -139,19 +139,19 @@ describe('Enterprise Search Analytics API', () => {
(fetchAnalyticsCollectionDataViewId as jest.Mock).mockImplementationOnce(() => {
return Promise.resolve(mockData);
});
- await mockRouter.callRoute({ params: { id: '1' } });
+ await mockRouter.callRoute({ params: { name: '1' } });
expect(mockRouter.response.ok).toHaveBeenCalledWith({
body: mockData,
});
});
- it('throws a 404 error if collection not found by id', async () => {
+ it('throws a 404 error if collection not found by name', async () => {
(fetchAnalyticsCollectionDataViewId as jest.Mock).mockImplementationOnce(() => {
throw new Error(ErrorCode.ANALYTICS_COLLECTION_NOT_FOUND);
});
await mockRouter.callRoute({
- params: { id: '1' },
+ params: { name: '1' },
});
expect(mockRouter.response.customError).toHaveBeenCalledWith({
diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.ts
index 724e159618df6..73bd29c455da6 100644
--- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.ts
@@ -16,10 +16,7 @@ import { ErrorCode } from '../../../common/types/error_codes';
import { addAnalyticsCollection } from '../../lib/analytics/add_analytics_collection';
import { analyticsEventsIndexExists } from '../../lib/analytics/analytics_events_index_exists';
import { deleteAnalyticsCollectionById } from '../../lib/analytics/delete_analytics_collection';
-import {
- fetchAnalyticsCollectionById,
- fetchAnalyticsCollections,
-} from '../../lib/analytics/fetch_analytics_collection';
+import { fetchAnalyticsCollections } from '../../lib/analytics/fetch_analytics_collection';
import { fetchAnalyticsCollectionDataViewId } from '../../lib/analytics/fetch_analytics_collection_data_view_id';
import { RouteDependencies } from '../../plugin';
import { createError } from '../../utils/create_error';
@@ -64,10 +61,10 @@ export function registerAnalyticsRoutes({
router.get(
{
- path: '/internal/enterprise_search/analytics/collections/{id}',
+ path: '/internal/enterprise_search/analytics/collections/{name}',
validate: {
params: schema.object({
- id: schema.string(),
+ name: schema.string(),
}),
},
},
@@ -75,13 +72,9 @@ export function registerAnalyticsRoutes({
const { client } = (await context.core).elasticsearch;
try {
- const collection = await fetchAnalyticsCollectionById(client, request.params.id);
-
- if (!collection) {
- throw new Error(ErrorCode.ANALYTICS_COLLECTION_NOT_FOUND);
- }
+ const collections = await fetchAnalyticsCollections(client, request.params.name);
- return response.ok({ body: collection });
+ return response.ok({ body: collections[0] });
} catch (error) {
if ((error as Error).message === ErrorCode.ANALYTICS_COLLECTION_NOT_FOUND) {
return createIndexNotFoundError(error, response);
@@ -114,7 +107,7 @@ export function registerAnalyticsRoutes({
const body = await addAnalyticsCollection(
elasticsearchClient,
dataViewsService,
- request.body
+ request.body.name
);
return response.ok({ body });
} catch (error) {
@@ -139,17 +132,17 @@ export function registerAnalyticsRoutes({
router.delete(
{
- path: '/internal/enterprise_search/analytics/collections/{id}',
+ path: '/internal/enterprise_search/analytics/collections/{name}',
validate: {
params: schema.object({
- id: schema.string(),
+ name: schema.string(),
}),
},
},
elasticsearchErrorHandler(log, async (context, request, response) => {
const { client } = (await context.core).elasticsearch;
try {
- await deleteAnalyticsCollectionById(client, request.params.id);
+ await deleteAnalyticsCollectionById(client, request.params.name);
return response.ok();
} catch (error) {
if ((error as Error).message === ErrorCode.ANALYTICS_COLLECTION_NOT_FOUND) {
@@ -162,17 +155,17 @@ export function registerAnalyticsRoutes({
router.get(
{
- path: '/internal/enterprise_search/analytics/events/{id}/exists',
+ path: '/internal/enterprise_search/analytics/events/{name}/exists',
validate: {
params: schema.object({
- id: schema.string(),
+ name: schema.string(),
}),
},
},
elasticsearchErrorHandler(log, async (context, request, response) => {
const { client } = (await context.core).elasticsearch;
- const eventsIndexExists = await analyticsEventsIndexExists(client, request.params.id);
+ const eventsIndexExists = await analyticsEventsIndexExists(client, request.params.name);
if (!eventsIndexExists) {
return response.ok({ body: { exists: false } });
@@ -184,10 +177,10 @@ export function registerAnalyticsRoutes({
router.get(
{
- path: '/internal/enterprise_search/analytics/collections/{id}/data_view_id',
+ path: '/internal/enterprise_search/analytics/collections/{name}/data_view_id',
validate: {
params: schema.object({
- id: schema.string(),
+ name: schema.string(),
}),
},
},
@@ -204,7 +197,7 @@ export function registerAnalyticsRoutes({
const dataViewId = await fetchAnalyticsCollectionDataViewId(
elasticsearchClient,
dataViewsService,
- request.params.id
+ request.params.name
);
return response.ok({ body: dataViewId });
diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/engines.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/engines.test.ts
index 019cca3a7acc2..14c5082c366fb 100644
--- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/engines.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/engines.test.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { mockDependencies, mockRequestHandler, MockRouter } from '../../__mocks__';
+import { mockDependencies, MockRouter } from '../../__mocks__';
jest.mock('../../utils/fetch_enterprise_search', () => ({
...jest.requireActual('../../utils/fetch_enterprise_search'),
@@ -26,22 +26,41 @@ describe('engines routes', () => {
describe('GET /internal/enterprise_search/engines', () => {
let mockRouter: MockRouter;
+ const mockClient = {
+ asCurrentUser: {
+ transport: {
+ request: jest.fn(),
+ },
+ },
+ };
beforeEach(() => {
jest.clearAllMocks();
+ const context = {
+ core: Promise.resolve({ elasticsearch: { client: mockClient } }),
+ } as unknown as jest.Mocked;
+
mockRouter = new MockRouter({
+ context,
method: 'get',
path: '/internal/enterprise_search/engines',
});
-
registerEnginesRoutes({
...mockDependencies,
router: mockRouter.router,
});
});
- it('creates a request to enterprise search', () => {
- expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/api/engines',
+ it('GET search applications API creates request', async () => {
+ mockClient.asCurrentUser.transport.request.mockImplementation(() => ({}));
+ const request = { query: {} };
+ await mockRouter.callRoute({});
+ expect(mockClient.asCurrentUser.transport.request).toHaveBeenCalledWith({
+ method: 'GET',
+ path: '/_application/search_application',
+ querystring: request.query,
+ });
+ expect(mockRouter.response.ok).toHaveBeenCalledWith({
+ body: {},
});
});
@@ -64,72 +83,23 @@ describe('engines routes', () => {
});
});
- describe('POST /internal/enterprise_search/engines', () => {
- let mockRouter: MockRouter;
-
- beforeEach(() => {
- jest.clearAllMocks();
- mockRouter = new MockRouter({
- method: 'post',
- path: '/internal/enterprise_search/engines',
- });
-
- registerEnginesRoutes({
- ...mockDependencies,
- router: mockRouter.router,
- });
- });
-
- it('creates a request to enterprise search', () => {
- expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/api/engines',
- });
- });
-
- it('validates correctly with engine_name', () => {
- const request = {
- body: {
- indices: ['search-unit-test'],
- name: 'some-engine',
- },
- };
-
- mockRouter.shouldValidate(request);
- });
-
- it('fails validation without body', () => {
- const request = { params: {} };
-
- mockRouter.shouldThrow(request);
- });
-
- it('fails validation without name', () => {
- const request = {
- body: {
- indices: ['search-unit-test'],
- },
- };
-
- mockRouter.shouldThrow(request);
- });
-
- it('fails validation without indices', () => {
- const request = {
- body: {
- name: 'some-engine',
- },
- };
-
- mockRouter.shouldThrow(request);
- });
- });
-
describe('GET /internal/enterprise_search/engines/{engine_name}', () => {
let mockRouter: MockRouter;
-
+ const mockClient = {
+ asCurrentUser: {
+ transport: {
+ request: jest.fn(),
+ },
+ },
+ };
beforeEach(() => {
jest.clearAllMocks();
+ const context = {
+ core: Promise.resolve({ elasticsearch: { client: mockClient } }),
+ } as unknown as jest.Mocked;
+
mockRouter = new MockRouter({
+ context,
method: 'get',
path: '/internal/enterprise_search/engines/{engine_name}',
});
@@ -140,9 +110,18 @@ describe('engines routes', () => {
});
});
- it('creates a request to enterprise search', () => {
- expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/api/engines/:engine_name',
+ it('GET search application API creates request', async () => {
+ mockClient.asCurrentUser.transport.request.mockImplementation(() => ({}));
+ await mockRouter.callRoute({
+ params: { engine_name: 'engine-name' },
+ });
+
+ expect(mockClient.asCurrentUser.transport.request).toHaveBeenCalledWith({
+ method: 'GET',
+ path: '/_application/search_application/engine-name',
+ });
+ expect(mockRouter.response.ok).toHaveBeenCalledWith({
+ body: {},
});
});
@@ -161,10 +140,21 @@ describe('engines routes', () => {
describe('PUT /internal/enterprise_search/engines/{engine_name}', () => {
let mockRouter: MockRouter;
-
+ const mockClient = {
+ asCurrentUser: {
+ transport: {
+ request: jest.fn(),
+ },
+ },
+ };
beforeEach(() => {
jest.clearAllMocks();
+ const context = {
+ core: Promise.resolve({ elasticsearch: { client: mockClient } }),
+ } as unknown as jest.Mocked;
+
mockRouter = new MockRouter({
+ context,
method: 'put',
path: '/internal/enterprise_search/engines/{engine_name}',
});
@@ -175,9 +165,65 @@ describe('engines routes', () => {
});
});
- it('creates a request to enterprise search', () => {
- expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/api/engines/:engine_name',
+ it('PUT - Upsert API creates request - create', async () => {
+ mockClient.asCurrentUser.transport.request.mockImplementation(() => ({
+ acknowledged: true,
+ }));
+
+ await mockRouter.callRoute({
+ params: {
+ engine_name: 'engine-name',
+ },
+ query: { create: true },
+ body: {
+ indices: ['test-indices-1'],
+ },
+ });
+ expect(mockClient.asCurrentUser.transport.request).toHaveBeenCalledWith({
+ method: 'PUT',
+ path: '/_application/search_application/engine-name',
+ body: {
+ indices: ['test-indices-1'],
+ },
+ querystring: { create: true },
+ });
+ const mock = jest.fn();
+ const mockResponse = mock({ result: 'created' });
+ expect(mockRouter.response.ok).toHaveReturnedWith(mockResponse);
+ expect(mockRouter.response.ok).toHaveBeenCalledWith({
+ body: {
+ acknowledged: true,
+ },
+ });
+ });
+ it('PUT - Upsert API creates request - update', async () => {
+ mockClient.asCurrentUser.transport.request.mockImplementation(() => ({
+ acknowledged: true,
+ }));
+
+ await mockRouter.callRoute({
+ params: {
+ engine_name: 'engine-name',
+ },
+ body: {
+ indices: ['test-indices-1'],
+ },
+ });
+ expect(mockClient.asCurrentUser.transport.request).toHaveBeenCalledWith({
+ method: 'PUT',
+ path: '/_application/search_application/engine-name',
+ body: {
+ indices: ['test-indices-1'],
+ },
+ querystring: {},
+ });
+ const mock = jest.fn();
+ const mockResponse = mock({ result: 'updated' });
+ expect(mockRouter.response.ok).toHaveReturnedWith(mockResponse);
+ expect(mockRouter.response.ok).toHaveBeenCalledWith({
+ body: {
+ acknowledged: true,
+ },
});
});
@@ -212,10 +258,21 @@ describe('engines routes', () => {
describe('DELETE /internal/enterprise_search/engines/{engine_name}', () => {
let mockRouter: MockRouter;
-
+ const mockClient = {
+ asCurrentUser: {
+ transport: {
+ request: jest.fn(),
+ },
+ },
+ };
beforeEach(() => {
jest.clearAllMocks();
+ const context = {
+ core: Promise.resolve({ elasticsearch: { client: mockClient } }),
+ } as unknown as jest.Mocked;
+
mockRouter = new MockRouter({
+ context,
method: 'delete',
path: '/internal/enterprise_search/engines/{engine_name}',
});
@@ -226,9 +283,24 @@ describe('engines routes', () => {
});
});
- it('creates a request to enterprise search', () => {
- expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/api/engines/:engine_name',
+ it('Delete API creates request', async () => {
+ mockClient.asCurrentUser.transport.request.mockImplementation(() => ({
+ acknowledged: true,
+ }));
+
+ await mockRouter.callRoute({
+ params: {
+ engine_name: 'engine-name',
+ },
+ });
+ expect(mockClient.asCurrentUser.transport.request).toHaveBeenCalledWith({
+ method: 'DELETE',
+ path: '_application/search_application/engine-name',
+ });
+ expect(mockRouter.response.ok).toHaveBeenCalledWith({
+ body: {
+ acknowledged: true,
+ },
});
});
@@ -245,12 +317,23 @@ describe('engines routes', () => {
});
});
- describe('GET /internal/enterprise_search/engines/{engine_name}/search', () => {
+ describe('POST /internal/enterprise_search/engines/{engine_name}/search', () => {
let mockRouter: MockRouter;
-
+ const mockClient = {
+ asCurrentUser: {
+ transport: {
+ request: jest.fn(),
+ },
+ },
+ };
beforeEach(() => {
jest.clearAllMocks();
+ const context = {
+ core: Promise.resolve({ elasticsearch: { client: mockClient } }),
+ } as unknown as jest.Mocked;
+
mockRouter = new MockRouter({
+ context,
method: 'post',
path: '/internal/enterprise_search/engines/{engine_name}/search',
});
@@ -260,9 +343,25 @@ describe('engines routes', () => {
router: mockRouter.router,
});
});
- it('creates a request to enterprise search', () => {
- expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/api/engines/:engine_name/_search',
+ it('POST - Search preview API creates a request', async () => {
+ mockClient.asCurrentUser.transport.request.mockImplementation(() => ({
+ acknowledged: true,
+ }));
+
+ await mockRouter.callRoute({
+ params: {
+ engine_name: 'engine-name',
+ },
+ });
+ expect(mockClient.asCurrentUser.transport.request).toHaveBeenCalledWith({
+ method: 'POST',
+ path: '/engine-name/_search',
+ body: {},
+ });
+ expect(mockRouter.response.ok).toHaveBeenCalledWith({
+ body: {
+ acknowledged: true,
+ },
});
});
diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/engines.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/engines.ts
index 111c04e6cd42e..b422d968c5d23 100644
--- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/engines.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/engines.ts
@@ -4,9 +4,14 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
+import { SearchResponse, AcknowledgedResponseBase } from '@elastic/elasticsearch/lib/api/types';
import { schema } from '@kbn/config-schema';
-import { EnterpriseSearchEngineDetails } from '../../../common/types/engines';
+import {
+ EnterpriseSearchEngineDetails,
+ EnterpriseSearchEnginesResponse,
+ EnterpriseSearchEngineUpsertResponse,
+} from '../../../common/types/engines';
import { ErrorCode } from '../../../common/types/error_codes';
import { createApiKey } from '../../lib/engines/create_api_key';
@@ -17,12 +22,7 @@ import { createError } from '../../utils/create_error';
import { elasticsearchErrorHandler } from '../../utils/elasticsearch_error_handler';
import { fetchEnterpriseSearch, isResponseError } from '../../utils/fetch_enterprise_search';
-export function registerEnginesRoutes({
- config,
- enterpriseSearchRequestHandler,
- log,
- router,
-}: RouteDependencies) {
+export function registerEnginesRoutes({ config, log, router }: RouteDependencies) {
router.get(
{
path: '/internal/enterprise_search/engines',
@@ -34,20 +34,18 @@ export function registerEnginesRoutes({
}),
},
},
- enterpriseSearchRequestHandler.createRequest({ path: '/api/engines' })
- );
+ elasticsearchErrorHandler(log, async (context, request, response) => {
+ const { client } = (await context.core).elasticsearch;
+ const engines = await client.asCurrentUser.transport.request(
+ {
+ method: 'GET',
+ path: `/_application/search_application`,
+ querystring: request.query,
+ }
+ );
- router.post(
- {
- path: '/internal/enterprise_search/engines',
- validate: {
- body: schema.object({
- indices: schema.arrayOf(schema.string()),
- name: schema.string(),
- }),
- },
- },
- enterpriseSearchRequestHandler.createRequest({ path: '/api/engines' })
+ return response.ok({ body: engines });
+ })
);
router.get(
@@ -59,7 +57,14 @@ export function registerEnginesRoutes({
}),
},
},
- enterpriseSearchRequestHandler.createRequest({ path: '/api/engines/:engine_name' })
+ elasticsearchErrorHandler(log, async (context, request, response) => {
+ const { client } = (await context.core).elasticsearch;
+ const engine = await client.asCurrentUser.transport.request({
+ method: 'GET',
+ path: `/_application/search_application/${request.params.engine_name}`,
+ });
+ return response.ok({ body: engine });
+ })
);
router.put(
@@ -70,12 +75,25 @@ export function registerEnginesRoutes({
indices: schema.arrayOf(schema.string()),
name: schema.maybe(schema.string()),
}),
+ query: schema.object({
+ create: schema.maybe(schema.boolean()),
+ }),
params: schema.object({
engine_name: schema.string(),
}),
},
},
- enterpriseSearchRequestHandler.createRequest({ path: '/api/engines/:engine_name' })
+ elasticsearchErrorHandler(log, async (context, request, response) => {
+ const { client } = (await context.core).elasticsearch;
+ const engine =
+ await client.asCurrentUser.transport.request({
+ method: 'PUT',
+ path: `/_application/search_application/${request.params.engine_name}`,
+ body: { indices: request.body.indices },
+ querystring: request.query,
+ });
+ return response.ok({ body: engine });
+ })
);
router.delete(
@@ -87,12 +105,38 @@ export function registerEnginesRoutes({
}),
},
},
- enterpriseSearchRequestHandler.createRequest({
- hasJsonResponse: false,
- path: '/api/engines/:engine_name',
+ elasticsearchErrorHandler(log, async (context, request, response) => {
+ const { client } = (await context.core).elasticsearch;
+ const engine = await client.asCurrentUser.transport.request({
+ method: 'DELETE',
+ path: `_application/search_application/${request.params.engine_name}`,
+ });
+ return response.ok({ body: engine });
})
);
+ router.post(
+ {
+ path: '/internal/enterprise_search/engines/{engine_name}/search',
+ validate: {
+ body: schema.object({}, { unknowns: 'allow' }),
+ params: schema.object({
+ engine_name: schema.string(),
+ from: schema.maybe(schema.number()),
+ size: schema.maybe(schema.number()),
+ }),
+ },
+ },
+ elasticsearchErrorHandler(log, async (context, request, response) => {
+ const { client } = (await context.core).elasticsearch;
+ const engines = await client.asCurrentUser.transport.request({
+ method: 'POST',
+ path: `/${request.params.engine_name}/_search`,
+ body: {},
+ });
+ return response.ok({ body: engines });
+ })
+ );
router.post(
{
path: '/internal/enterprise_search/engines/{engine_name}/api_key',
@@ -118,24 +162,6 @@ export function registerEnginesRoutes({
});
})
);
-
- router.post(
- {
- path: '/internal/enterprise_search/engines/{engine_name}/search',
- validate: {
- body: schema.object({}, { unknowns: 'allow' }),
- params: schema.object({
- engine_name: schema.string(),
- from: schema.maybe(schema.number()),
- size: schema.maybe(schema.number()),
- }),
- },
- },
- enterpriseSearchRequestHandler.createRequest({
- path: '/api/engines/:engine_name/_search',
- })
- );
-
router.get(
{
path: '/internal/enterprise_search/engines/{engine_name}/field_capabilities',
@@ -150,6 +176,7 @@ export function registerEnginesRoutes({
request,
`/api/engines/${engineName}`
);
+
if (!engine || (isResponseError(engine) && engine.responseStatus === 404)) {
return createError({
errorCode: ErrorCode.ENGINE_NOT_FOUND,
diff --git a/x-pack/plugins/fleet/common/constants/agent_policy.ts b/x-pack/plugins/fleet/common/constants/agent_policy.ts
index 50cef1fbe43cf..5308a3ec09b4c 100644
--- a/x-pack/plugins/fleet/common/constants/agent_policy.ts
+++ b/x-pack/plugins/fleet/common/constants/agent_policy.ts
@@ -30,3 +30,5 @@ export const AGENT_POLICY_DEFAULT_MONITORING_DATASETS = [
];
export const LICENSE_FOR_SCHEDULE_UPGRADE = 'platinum';
+
+export const DEFAULT_MAX_AGENT_POLICIES_WITH_INACTIVITY_TIMEOUT = 750;
diff --git a/x-pack/plugins/fleet/common/openapi/bundled.json b/x-pack/plugins/fleet/common/openapi/bundled.json
index 9b9e9a64f2acf..90e0459cb2e54 100644
--- a/x-pack/plugins/fleet/common/openapi/bundled.json
+++ b/x-pack/plugins/fleet/common/openapi/bundled.json
@@ -3858,6 +3858,9 @@
},
"400": {
"$ref": "#/components/responses/error"
+ },
+ "409": {
+ "$ref": "#/components/responses/error"
}
},
"requestBody": {
@@ -4066,6 +4069,9 @@
},
"400": {
"$ref": "#/components/responses/error"
+ },
+ "409": {
+ "$ref": "#/components/responses/error"
}
}
}
diff --git a/x-pack/plugins/fleet/common/openapi/bundled.yaml b/x-pack/plugins/fleet/common/openapi/bundled.yaml
index 8f5436703f6f7..e50525db886d0 100644
--- a/x-pack/plugins/fleet/common/openapi/bundled.yaml
+++ b/x-pack/plugins/fleet/common/openapi/bundled.yaml
@@ -2406,6 +2406,8 @@ paths:
- item
'400':
$ref: '#/components/responses/error'
+ '409':
+ $ref: '#/components/responses/error'
requestBody:
description: >-
You should use inputs as an object and not use the deprecated inputs
@@ -2537,6 +2539,8 @@ paths:
- success
'400':
$ref: '#/components/responses/error'
+ '409':
+ $ref: '#/components/responses/error'
/package_policies/upgrade/dryrun:
post:
summary: Dry run package policy upgrade
diff --git a/x-pack/plugins/fleet/common/openapi/paths/package_policies.yaml b/x-pack/plugins/fleet/common/openapi/paths/package_policies.yaml
index 0fe987e1727de..89a54608d2310 100644
--- a/x-pack/plugins/fleet/common/openapi/paths/package_policies.yaml
+++ b/x-pack/plugins/fleet/common/openapi/paths/package_policies.yaml
@@ -47,6 +47,8 @@ post:
- item
'400':
$ref: ../components/responses/error.yaml
+ '409':
+ $ref: ../components/responses/error.yaml
requestBody:
description: You should use inputs as an object and not use the deprecated inputs array.
content:
diff --git a/x-pack/plugins/fleet/common/openapi/paths/package_policies@upgrade.yaml b/x-pack/plugins/fleet/common/openapi/paths/package_policies@upgrade.yaml
index 2b6e69d49c44e..1837675a15f22 100644
--- a/x-pack/plugins/fleet/common/openapi/paths/package_policies@upgrade.yaml
+++ b/x-pack/plugins/fleet/common/openapi/paths/package_policies@upgrade.yaml
@@ -36,3 +36,5 @@ post:
- success
'400':
$ref: ../components/responses/error.yaml
+ '409':
+ $ref: ../components/responses/error.yaml
diff --git a/x-pack/plugins/fleet/common/services/agent_policies_helpers.ts b/x-pack/plugins/fleet/common/services/agent_policies_helpers.ts
new file mode 100644
index 0000000000000..9db799997fad3
--- /dev/null
+++ b/x-pack/plugins/fleet/common/services/agent_policies_helpers.ts
@@ -0,0 +1,26 @@
+/*
+ * 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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { AgentPolicy } from '../types';
+import { FLEET_SERVER_PACKAGE, FLEET_APM_PACKAGE } from '../constants';
+
+export function policyHasFleetServer(agentPolicy: AgentPolicy) {
+ if (!agentPolicy.package_policies) {
+ return false;
+ }
+ return (
+ agentPolicy.package_policies?.some((p) => p.package?.name === FLEET_SERVER_PACKAGE) ||
+ !!agentPolicy.has_fleet_server
+ );
+}
+
+export function policyHasAPMIntegration(agentPolicy: AgentPolicy) {
+ if (!agentPolicy.package_policies) {
+ return false;
+ }
+ return agentPolicy.package_policies?.some((p) => p.package?.name === FLEET_APM_PACKAGE);
+}
diff --git a/x-pack/plugins/fleet/common/services/index.ts b/x-pack/plugins/fleet/common/services/index.ts
index 2b700a6aaca6f..6f799c4a01b0e 100644
--- a/x-pack/plugins/fleet/common/services/index.ts
+++ b/x-pack/plugins/fleet/common/services/index.ts
@@ -63,3 +63,5 @@ export {
export { getAllowedOutputTypeForPolicy } from './output_helpers';
export { agentStatusesToSummary } from './agent_statuses_to_summary';
+
+export { policyHasFleetServer, policyHasAPMIntegration } from './agent_policies_helpers';
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/hooks.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/hooks.tsx
index 8150b0dd96e2b..869034dca74c5 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/hooks.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/hooks.tsx
@@ -17,7 +17,10 @@ import {
useGetFleetServerHosts,
} from '../../../../hooks';
import { LICENCE_FOR_PER_POLICY_OUTPUT } from '../../../../../../../common/constants';
-import { getAllowedOutputTypeForPolicy } from '../../../../../../../common/services';
+import {
+ getAllowedOutputTypeForPolicy,
+ policyHasFleetServer,
+} from '../../../../../../../common/services';
import type { NewAgentPolicy, AgentPolicy } from '../../../../types';
// The super select component do not support null or '' as a value
@@ -59,7 +62,11 @@ export function useOutputOptions(agentPolicy: Partial getAllowedOutputTypeForPolicy(agentPolicy as AgentPolicy),
[agentPolicy]
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx
index cf3d6e84137e8..6409114839bc6 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx
@@ -29,7 +29,11 @@ import {
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
-import { AGENT_POLICY_SAVED_OBJECT_TYPE, dataTypes } from '../../../../../../../common/constants';
+import {
+ AGENT_POLICY_SAVED_OBJECT_TYPE,
+ dataTypes,
+ DEFAULT_MAX_AGENT_POLICIES_WITH_INACTIVITY_TIMEOUT,
+} from '../../../../../../../common/constants';
import type { NewAgentPolicy, AgentPolicy } from '../../../../types';
import { useStartServices, useConfig, useGetAgentPolicies } from '../../../../hooks';
@@ -66,7 +70,8 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent =
const { docLinks } = useStartServices();
const config = useConfig();
const maxAgentPoliciesWithInactivityTimeout =
- config.developer?.maxAgentPoliciesWithInactivityTimeout ?? 0;
+ config.developer?.maxAgentPoliciesWithInactivityTimeout ??
+ DEFAULT_MAX_AGENT_POLICIES_WITH_INACTIVITY_TIMEOUT;
const [touchedFields, setTouchedFields] = useState<{ [key: string]: boolean }>({});
const {
dataOutputOptions,
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_diagnostics/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_diagnostics/index.tsx
index de3600142d8ca..c52eddd3cafb9 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_diagnostics/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_diagnostics/index.tsx
@@ -249,7 +249,7 @@ export const AgentDiagnosticsTab: React.FunctionComponent
>
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_request_diagnostics_modal/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_request_diagnostics_modal/index.tsx
index d5bb9adf040b2..0d3ccbd804e52 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_request_diagnostics_modal/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_request_diagnostics_modal/index.tsx
@@ -119,7 +119,7 @@ export const AgentRequestDiagnosticsModal: React.FunctionComponent = ({
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.test.tsx
index 0d81a9bef62ae..393b6519fc6c1 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.test.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.test.tsx
@@ -7,6 +7,8 @@
import React from 'react';
+import { waitFor } from '@testing-library/react';
+
import { createFleetTestRendererMock } from '../../../../../../mock';
import { AgentUpgradeAgentModal } from '.';
@@ -26,6 +28,17 @@ jest.mock('@elastic/eui', () => {
};
});
+jest.mock('../../../../hooks', () => {
+ return {
+ ...jest.requireActual('../../../../hooks'),
+ sendGetAgentsAvailableVersions: jest.fn().mockResolvedValue({
+ data: {
+ items: ['8.7.0'],
+ },
+ }),
+ };
+});
+
function renderAgentUpgradeAgentModal(props: Partial) {
const renderer = createFleetTestRendererMock();
@@ -75,4 +88,16 @@ describe('AgentUpgradeAgentModal', () => {
expect(el).not.toBeNull();
expect(el?.textContent).toBe('1 hour');
});
+
+ it('should enable the version combo if agents is a query', async () => {
+ const { utils } = renderAgentUpgradeAgentModal({
+ agents: '*',
+ agentCount: 30,
+ });
+
+ const el = utils.getByTestId('agentUpgradeModal.VersionCombobox');
+ await waitFor(() => {
+ expect(el.classList.contains('euiComboBox-isDisabled')).toBe(false);
+ });
+ });
});
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx
index c852d9b1ad2a2..e9e96cb54cac4 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx
@@ -98,6 +98,10 @@ export const AgentUpgradeAgentModal: React.FunctionComponent {
if (!Array.isArray(agents)) {
+ // when agent is a query, don't set minVersion, so the versions are available to select
+ if (typeof agents === 'string') {
+ return undefined;
+ }
return getMinVersion(availableVersions);
}
const versions = (agents as Agent[]).map(
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/confirm_update.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/confirm_update.tsx
index 61addb3e6d3ea..8d96c5f314547 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/confirm_update.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/confirm_update.tsx
@@ -7,6 +7,7 @@
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
+import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import type { Output } from '../../../../types';
import type { useConfirmModal } from '../../hooks/use_confirm_modal';
@@ -30,35 +31,60 @@ const ConfirmDescription: React.FunctionComponent = ({
agentCount,
agentPolicyCount,
}) => (
- {output.name},
- agents: (
-
-
-
- ),
- policies: (
-
+ <>
+ {output.name},
+ agents: (
+
+
+
+ ),
+ policies: (
+
+
+
+ ),
+ }}
+ />
+
+ {output.type === 'logstash' ? (
+ <>
+
+
+ }
+ >
-
- ),
- }}
- />
+ {' '}
+ >
+ ) : null}
+ >
);
export async function confirmUpdate(
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/outputs_table/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/outputs_table/index.tsx
index 331c7dc192ecb..464c512ecf8e0 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/outputs_table/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/outputs_table/index.tsx
@@ -61,7 +61,7 @@ export const OutputsTable: React.FunctionComponent = ({
{
};
});
-jest.mock('../../services/has_fleet_server', () => {
+jest.mock('../../../common/services/agent_policies_helpers', () => {
return {
policyHasFleetServer: jest.fn().mockReturnValue(true),
};
diff --git a/x-pack/plugins/fleet/public/services/has_fleet_server.ts b/x-pack/plugins/fleet/public/services/has_fleet_server.ts
deleted file mode 100644
index 43724d121b90f..0000000000000
--- a/x-pack/plugins/fleet/public/services/has_fleet_server.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * 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; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { FLEET_SERVER_PACKAGE } from '../constants';
-import type { AgentPolicy, PackagePolicy } from '../types';
-
-export function policyHasFleetServer(agentPolicy: AgentPolicy) {
- if (!agentPolicy.package_policies) {
- return false;
- }
-
- return agentPolicy.package_policies.some(
- (ap: PackagePolicy) => ap.package?.name === FLEET_SERVER_PACKAGE
- );
-}
diff --git a/x-pack/plugins/fleet/public/services/index.ts b/x-pack/plugins/fleet/public/services/index.ts
index ad5dab5d5868d..6961069ebdce7 100644
--- a/x-pack/plugins/fleet/public/services/index.ts
+++ b/x-pack/plugins/fleet/public/services/index.ts
@@ -41,11 +41,11 @@ export {
countValidationErrors,
getStreamsForInputType,
downloadSourceRoutesService,
+ policyHasFleetServer,
} from '../../common/services';
export { isPackageUnverified, isVerificationError } from './package_verification';
export { isPackageUpdatable } from './is_package_updatable';
export { pkgKeyFromPackageInfo } from './pkg_key_from_package_info';
export { createExtensionRegistrationCallback } from './ui_extensions';
export { incrementPolicyName } from './increment_policy_name';
-export { policyHasFleetServer } from './has_fleet_server';
export { generateNewAgentPolicyWithDefaults } from './generate_new_agent_policy';
diff --git a/x-pack/plugins/fleet/server/errors/handlers.ts b/x-pack/plugins/fleet/server/errors/handlers.ts
index 89811ca2bc7aa..6a86f9b9f2c61 100644
--- a/x-pack/plugins/fleet/server/errors/handlers.ts
+++ b/x-pack/plugins/fleet/server/errors/handlers.ts
@@ -31,6 +31,7 @@ import {
PackageFailedVerificationError,
PackagePolicyNotFoundError,
FleetUnauthorizedError,
+ PackagePolicyNameExistsError,
} from '.';
type IngestErrorHandler = (
@@ -78,6 +79,9 @@ const getHTTPResponseCode = (error: FleetError): number => {
if (error instanceof FleetUnauthorizedError) {
return 403; // Unauthorized
}
+ if (error instanceof PackagePolicyNameExistsError) {
+ return 409; // Conflict
+ }
return 400; // Bad Request
};
diff --git a/x-pack/plugins/fleet/server/errors/index.ts b/x-pack/plugins/fleet/server/errors/index.ts
index 4856ff5294e44..84cf87d799f4a 100644
--- a/x-pack/plugins/fleet/server/errors/index.ts
+++ b/x-pack/plugins/fleet/server/errors/index.ts
@@ -50,6 +50,7 @@ export class ConcurrentInstallOperationError extends FleetError {}
export class AgentReassignmentError extends FleetError {}
export class PackagePolicyIneligibleForUpgradeError extends FleetError {}
export class PackagePolicyValidationError extends FleetError {}
+export class PackagePolicyNameExistsError extends FleetError {}
export class PackagePolicyNotFoundError extends FleetError {}
export class BundledPackageNotFoundError extends FleetError {}
export class HostedAgentPolicyRestrictionRelatedError extends FleetError {
diff --git a/x-pack/plugins/fleet/server/routes/fleet_proxies/handler.ts b/x-pack/plugins/fleet/server/routes/fleet_proxies/handler.ts
index 5b00baf9bf2c1..ab5750e187bbc 100644
--- a/x-pack/plugins/fleet/server/routes/fleet_proxies/handler.ts
+++ b/x-pack/plugins/fleet/server/routes/fleet_proxies/handler.ts
@@ -148,7 +148,7 @@ export const deleteFleetProxyHandler: RequestHandler<
const { fleetServerHosts, outputs } = await getFleetProxyRelatedSavedObjects(soClient, proxyId);
- await deleteFleetProxy(soClient, request.params.itemId);
+ await deleteFleetProxy(soClient, esClient, request.params.itemId);
await bumpRelatedPolicies(soClient, esClient, fleetServerHosts, outputs);
diff --git a/x-pack/plugins/fleet/server/routes/output/handler.ts b/x-pack/plugins/fleet/server/routes/output/handler.ts
index 9a597be874bd6..71e30b84677c5 100644
--- a/x-pack/plugins/fleet/server/routes/output/handler.ts
+++ b/x-pack/plugins/fleet/server/routes/output/handler.ts
@@ -75,7 +75,7 @@ export const putOuputHandler: RequestHandler<
const soClient = coreContext.savedObjects.client;
const esClient = coreContext.elasticsearch.client.asInternalUser;
try {
- await outputService.update(soClient, request.params.outputId, request.body);
+ await outputService.update(soClient, esClient, request.params.outputId, request.body);
const output = await outputService.get(soClient, request.params.outputId);
if (output.is_default || output.is_default_monitoring) {
await agentPolicyService.bumpAllAgentPolicies(soClient, esClient);
@@ -109,7 +109,7 @@ export const postOuputHandler: RequestHandler<
const esClient = coreContext.elasticsearch.client.asInternalUser;
try {
const { id, ...data } = request.body;
- const output = await outputService.create(soClient, data, { id });
+ const output = await outputService.create(soClient, esClient, data, { id });
if (output.is_default || output.is_default_monitoring) {
await agentPolicyService.bumpAllAgentPolicies(soClient, esClient);
}
diff --git a/x-pack/plugins/fleet/server/services/agent_policies/output_helpers.test.ts b/x-pack/plugins/fleet/server/services/agent_policies/output_helpers.test.ts
index 99437480b86f9..629640c574b09 100644
--- a/x-pack/plugins/fleet/server/services/agent_policies/output_helpers.test.ts
+++ b/x-pack/plugins/fleet/server/services/agent_policies/output_helpers.test.ts
@@ -199,13 +199,16 @@ describe('validateOutputForPolicy', () => {
validateOutputForPolicy(
savedObjectsClientMock.create(),
{
+ name: 'Fleet server policy',
data_output_id: 'test1',
monitoring_output_id: 'test1',
},
{ data_output_id: 'newdataoutput', monitoring_output_id: 'test1' },
['elasticsearch']
)
- ).rejects.toThrow(/logstash output is not usable with that policy./);
+ ).rejects.toThrow(
+ 'Output of type "logstash" is not usable with policy "Fleet server policy".'
+ );
});
it('should allow elasticsearch output to be used with a policy using fleet server or APM', async () => {
diff --git a/x-pack/plugins/fleet/server/services/agent_policies/outputs_helpers.ts b/x-pack/plugins/fleet/server/services/agent_policies/outputs_helpers.ts
index b127788533594..4873d2aa21426 100644
--- a/x-pack/plugins/fleet/server/services/agent_policies/outputs_helpers.ts
+++ b/x-pack/plugins/fleet/server/services/agent_policies/outputs_helpers.ts
@@ -7,8 +7,9 @@
import type { SavedObjectsClientContract } from '@kbn/core/server';
-import type { AgentPolicySOAttributes } from '../../types';
+import type { AgentPolicySOAttributes, AgentPolicy } from '../../types';
import { LICENCE_FOR_PER_POLICY_OUTPUT, outputType } from '../../../common/constants';
+import { policyHasFleetServer } from '../../../common/services';
import { appContextService } from '..';
import { outputService } from '../output';
import { OutputInvalidError, OutputLicenceError } from '../../errors';
@@ -59,7 +60,9 @@ export async function validateOutputForPolicy(
if (isOutputTypeRestricted) {
const dataOutput = await getDataOutputForAgentPolicy(soClient, data);
if (!allowedOutputTypeForPolicy.includes(dataOutput.type)) {
- throw new OutputInvalidError(`${dataOutput.type} output is not usable with that policy.`);
+ throw new OutputInvalidError(
+ `Output of type "${dataOutput.type}" is not usable with policy "${data.name}".`
+ );
}
}
@@ -71,6 +74,8 @@ export async function validateOutputForPolicy(
if (data.is_managed && data.is_preconfigured) {
return;
}
+ // Validate output when the policy has fleet server
+ if (policyHasFleetServer(data as AgentPolicy)) return;
const hasLicence = appContextService
.getSecurityLicense()
diff --git a/x-pack/plugins/fleet/server/services/agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policy.test.ts
index b223e88102d9c..1d2241696704d 100644
--- a/x-pack/plugins/fleet/server/services/agent_policy.test.ts
+++ b/x-pack/plugins/fleet/server/services/agent_policy.test.ts
@@ -101,6 +101,7 @@ function getAgentPolicyCreateMock() {
});
return soClient;
}
+
describe('agent policy', () => {
beforeEach(() => {
getAgentPolicyUpdateMock().mockClear();
@@ -108,9 +109,9 @@ describe('agent policy', () => {
describe('create', () => {
it('is_managed present and false by default', async () => {
+ const soClient = getAgentPolicyCreateMock();
// ignore unrelated unique name constraint
agentPolicyService.requireUniqueName = async () => {};
- const soClient = getAgentPolicyCreateMock();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
await expect(
@@ -169,7 +170,7 @@ describe('agent policy', () => {
]);
});
- it('should throw error for agent policy which has managed package poolicy', async () => {
+ it('should throw error for agent policy which has managed package policy', async () => {
mockedPackagePolicyService.findAllForAgentPolicy.mockReturnValue([
{
id: 'package-1',
@@ -212,6 +213,20 @@ describe('agent policy', () => {
await agentPolicyService.bumpAllAgentPolicies(soClient, esClient, undefined);
+ expect(soClient.bulkUpdate).toHaveBeenCalledWith([
+ {
+ attributes: expect.objectContaining({
+ fleet_server_hosts: ['http://fleetserver:8220'],
+ revision: NaN,
+ updated_by: 'system',
+ }),
+ id: '93f74c0-e876-11ea-b7d3-8b2acec6f75c',
+ references: [],
+ score: 1,
+ type: 'ingest_manager_settings',
+ },
+ ]);
+
expect(agentPolicyUpdateEventHandler).toHaveBeenCalledTimes(1);
});
});
diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts
index 5912e022370de..2720701f0b29c 100644
--- a/x-pack/plugins/fleet/server/services/agent_policy.ts
+++ b/x-pack/plugins/fleet/server/services/agent_policy.ts
@@ -38,12 +38,16 @@ import type {
ListWithKuery,
NewPackagePolicy,
} from '../types';
-import { getAllowedOutputTypeForPolicy, packageToPackagePolicy } from '../../common/services';
+import {
+ getAllowedOutputTypeForPolicy,
+ packageToPackagePolicy,
+ policyHasFleetServer,
+ policyHasAPMIntegration,
+} from '../../common/services';
import {
agentPolicyStatuses,
AGENT_POLICY_INDEX,
UUID_V5_NAMESPACE,
- FLEET_APM_PACKAGE,
FLEET_ELASTIC_AGENT_PACKAGE,
} from '../../common/constants';
import type {
@@ -186,10 +190,11 @@ class AgentPolicyService {
}
public hasAPMIntegration(agentPolicy: AgentPolicy) {
- return (
- agentPolicy.package_policies &&
- agentPolicy.package_policies.some((p) => p.package?.name === FLEET_APM_PACKAGE)
- );
+ return policyHasAPMIntegration(agentPolicy);
+ }
+
+ public hasFleetServerIntegration(agentPolicy: AgentPolicy) {
+ return policyHasFleetServer(agentPolicy);
}
public async create(
@@ -1054,6 +1059,7 @@ class AgentPolicyService {
export const agentPolicyService = new AgentPolicyService();
+// TODO: remove unused parameters
export async function addPackageToAgentPolicy(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
diff --git a/x-pack/plugins/fleet/server/services/agents/build_status_runtime_field.ts b/x-pack/plugins/fleet/server/services/agents/build_status_runtime_field.ts
index f0f70139c5437..e98043ba44694 100644
--- a/x-pack/plugins/fleet/server/services/agents/build_status_runtime_field.ts
+++ b/x-pack/plugins/fleet/server/services/agents/build_status_runtime_field.ts
@@ -9,12 +9,13 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/types';
import type { SavedObjectsClientContract } from '@kbn/core/server';
import type { Logger } from '@kbn/core/server';
+import { DEFAULT_MAX_AGENT_POLICIES_WITH_INACTIVITY_TIMEOUT } from '../../../common/constants';
+
import { AGENT_POLLING_THRESHOLD_MS } from '../../constants';
import { agentPolicyService } from '../agent_policy';
import { appContextService } from '../app_context';
const MISSED_INTERVALS_BEFORE_OFFLINE = 10;
const MS_BEFORE_OFFLINE = MISSED_INTERVALS_BEFORE_OFFLINE * AGENT_POLLING_THRESHOLD_MS;
-const DEFAULT_MAX_AGENT_POLICIES_WITH_INACTIVITY_TIMEOUT = 750;
export type InactivityTimeouts = Awaited<
ReturnType
>;
diff --git a/x-pack/plugins/fleet/server/services/agents/uploads.ts b/x-pack/plugins/fleet/server/services/agents/uploads.ts
index 3683b9b0d90a7..0bb62e71e98f3 100644
--- a/x-pack/plugins/fleet/server/services/agents/uploads.ts
+++ b/x-pack/plugins/fleet/server/services/agents/uploads.ts
@@ -186,6 +186,7 @@ export async function getAgentUploadFile(
metadataIndex: FILE_STORAGE_METADATA_AGENT_INDEX,
elasticsearchClient: esClient,
logger: appContextService.getLogger(),
+ indexIsAlias: true,
});
const file = await fileClient.get({
diff --git a/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts b/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts
index 1488d81b327cd..093dc20e2be52 100644
--- a/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts
+++ b/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts
@@ -73,8 +73,13 @@ spec:
value: "/etc/elastic-agent"
securityContext:
runAsUser: 0
- # Needed for 'Defend for containers' integration (cloud-defend)
- #privileged: true
+ capabilities:
+ add:
+ # The following capabilities are needed for 'Defend for containers' integration (cloud-defend)
+ # If you are not using this integration, then these capabilites can be removed.
+ - BPF # (since Linux 5.8) allows loading of BPF programs, create most map types, load BTF, iterate programs and maps.
+ - PERFMON # (since Linux 5.8) allows attaching of BPF programs used for performance metrics and observability operations.
+ - SYS_RESOURCE # Allow use of special resources or raising of resource limits. Used by Defend for Containers to modify rlimit_memlock
resources:
limits:
memory: 700Mi
@@ -107,17 +112,8 @@ spec:
- name: var-lib
mountPath: /hostfs/var/lib
readOnly: true
- # Needed for 'Defend for containers' integration (cloud-defend)
- #- name: boot
- # mountPath: /boot
- # readOnly: true
- #- name: sys-kernel-debug
- # mountPath: /sys/kernel/debug
- #- name: sys-fs-bpf
- # mountPath: /sys/fs/bpf
- #- name: sys-kernel-security
- # mountPath: /sys/kernel/security
- # readOnly: true
+ - name: sys-kernel-debug
+ mountPath: /sys/kernel/debug
volumes:
- name: datastreams
configMap:
@@ -148,18 +144,11 @@ spec:
hostPath:
path: /var/lib
# Needed for 'Defend for containers' integration (cloud-defend)
- #- name: boot
- # hostPath:
- # path: /boot
- #- name: sys-kernel-debug
- # hostPath:
- # path: /sys/kernel/debug
- #- name: sys-fs-bpf
- # hostPath:
- # path: /sys/fs/bpf
- #- name: sys-kernel-security
- # hostPath:
- # path: /sys/kernel/security
+ # If you are not using this integration, then these volumes and the corresponding
+ # mounts can be removed.
+ - name: sys-kernel-debug
+ hostPath:
+ path: /sys/kernel/debug
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
@@ -379,8 +368,13 @@ spec:
fieldPath: metadata.name
securityContext:
runAsUser: 0
- # Needed for 'Defend for containers' integration (cloud-defend)
- #privileged: true
+ capabilities:
+ add:
+ # The following capabilities are needed for 'Defend for containers' integration (cloud-defend)
+ # If you are not using this integration, then these capabilites can be removed.
+ - BPF # (since Linux 5.8) allows loading of BPF programs, create most map types, load BTF, iterate programs and maps.
+ - PERFMON # (since Linux 5.8) allows attaching of BPF programs used for performance metrics and observability operations.
+ - SYS_RESOURCE # Allow use of special resources or raising of resource limits. Used by Defend for Containers to modify rlimit_memlock
resources:
limits:
memory: 700Mi
@@ -409,17 +403,8 @@ spec:
- name: etc-mid
mountPath: /etc/machine-id
readOnly: true
- # Needed for 'Defend for containers' integration (cloud-defend)
- #- name: boot
- # mountPath: /boot
- # readOnly: true
- #- name: sys-kernel-debug
- # mountPath: /sys/kernel/debug
- #- name: sys-fs-bpf
- # mountPath: /sys/fs/bpf
- #- name: sys-kernel-security
- # mountPath: /sys/kernel/security
- # readOnly: true
+ - name: sys-kernel-debug
+ mountPath: /sys/kernel/debug
volumes:
- name: proc
hostPath:
@@ -449,18 +434,11 @@ spec:
path: /etc/machine-id
type: File
# Needed for 'Defend for containers' integration (cloud-defend)
- #- name: boot
- # hostPath:
- # path: /boot
- #- name: sys-kernel-debug
- # hostPath:
- # path: /sys/kernel/debug
- #- name: sys-fs-bpf
- # hostPath:
- # path: /sys/fs/bpf
- #- name: sys-kernel-security
- # hostPath:
- # path: /sys/kernel/security
+ # If you are not using this integration, then these volumes and the corresponding
+ # mounts can be removed.
+ - name: sys-kernel-debug
+ hostPath:
+ path: /sys/kernel/debug
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/mappings.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/mappings.ts
index 925aba5644839..f872405a31ef5 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/mappings.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/mappings.ts
@@ -58,7 +58,6 @@ export function keyword(field: Field): Properties {
fieldProps.normalizer = field.normalizer;
}
if (field.dimension) {
- fieldProps.time_series_dimension = field.dimension;
delete fieldProps.ignore_above;
}
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts
index 2f148a2f10805..2201ad47b9c28 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts
@@ -802,7 +802,7 @@ describe('EPM template', () => {
expect(JSON.stringify(mappings)).toEqual(JSON.stringify(constantKeywordMapping));
});
- it('tests processing dimension field', () => {
+ it('tests processing dimension field on a keyword', () => {
const literalYml = `
- name: example.id
type: keyword
@@ -822,7 +822,60 @@ describe('EPM template', () => {
};
const fields: Field[] = safeLoad(literalYml);
const processedFields = processFields(fields);
- const mappings = generateMappings(processedFields);
+ const mappings = generateMappings(processedFields, {
+ isIndexModeTimeSeries: true,
+ });
+ expect(mappings).toEqual(expectedMapping);
+ });
+
+ it('tests processing dimension field on an long', () => {
+ const literalYml = `
+- name: example.id
+ type: long
+ dimension: true
+ `;
+ const expectedMapping = {
+ properties: {
+ example: {
+ properties: {
+ id: {
+ time_series_dimension: true,
+ type: 'long',
+ },
+ },
+ },
+ },
+ };
+ const fields: Field[] = safeLoad(literalYml);
+ const processedFields = processFields(fields);
+ const mappings = generateMappings(processedFields, {
+ isIndexModeTimeSeries: true,
+ });
+ expect(mappings).toEqual(expectedMapping);
+ });
+
+ it('tests processing dimension field on an long without timeserie enabled', () => {
+ const literalYml = `
+- name: example.id
+ type: long
+ dimension: true
+ `;
+ const expectedMapping = {
+ properties: {
+ example: {
+ properties: {
+ id: {
+ type: 'long',
+ },
+ },
+ },
+ },
+ };
+ const fields: Field[] = safeLoad(literalYml);
+ const processedFields = processFields(fields);
+ const mappings = generateMappings(processedFields, {
+ isIndexModeTimeSeries: false,
+ });
expect(mappings).toEqual(expectedMapping);
});
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts
index dacdfae720ccf..4594960e79e57 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts
@@ -347,6 +347,9 @@ function _generateMappings(
if (options?.isIndexModeTimeSeries && 'metric_type' in field) {
fieldProps.time_series_metric = field.metric_type;
}
+ if (options?.isIndexModeTimeSeries && field.dimension) {
+ fieldProps.time_series_dimension = field.dimension;
+ }
props[field.name] = fieldProps;
hasNonDynamicTemplateMappings = true;
diff --git a/x-pack/plugins/fleet/server/services/fleet_proxies.test.ts b/x-pack/plugins/fleet/server/services/fleet_proxies.test.ts
index 024860e0b6490..bed55ec26d0f6 100644
--- a/x-pack/plugins/fleet/server/services/fleet_proxies.test.ts
+++ b/x-pack/plugins/fleet/server/services/fleet_proxies.test.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { savedObjectsClientMock } from '@kbn/core/server/mocks';
+import { savedObjectsClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks';
import { FLEET_PROXY_SAVED_OBJECT_TYPE } from '../constants';
@@ -33,6 +33,7 @@ const PROXY_IDS = {
describe('Fleet proxies service', () => {
const soClientMock = savedObjectsClientMock.create();
+ const esClientMock = elasticsearchServiceMock.createElasticsearchClient();
beforeEach(() => {
mockedOutputService.update.mockReset();
@@ -125,26 +126,28 @@ describe('Fleet proxies service', () => {
describe('delete', () => {
it('should not allow to delete preconfigured proxy', async () => {
await expect(() =>
- deleteFleetProxy(soClientMock, PROXY_IDS.PRECONFIGURED)
+ deleteFleetProxy(soClientMock, esClientMock, PROXY_IDS.PRECONFIGURED)
).rejects.toThrowError(/Cannot delete test-preconfigured preconfigured proxy/);
});
it('should allow to delete preconfigured proxy with option fromPreconfiguration:true', async () => {
- await deleteFleetProxy(soClientMock, PROXY_IDS.PRECONFIGURED, { fromPreconfiguration: true });
+ await deleteFleetProxy(soClientMock, esClientMock, PROXY_IDS.PRECONFIGURED, {
+ fromPreconfiguration: true,
+ });
expect(soClientMock.delete).toBeCalled();
});
it('should not allow to delete proxy wiht related preconfigured saved object', async () => {
await expect(() =>
- deleteFleetProxy(soClientMock, PROXY_IDS.RELATED_PRECONFIGURED)
+ deleteFleetProxy(soClientMock, esClientMock, PROXY_IDS.RELATED_PRECONFIGURED)
).rejects.toThrowError(
/Cannot delete a proxy used in a preconfigured fleet server hosts or output./
);
});
it('should allow to delete proxy wiht related preconfigured saved object option fromPreconfiguration:true', async () => {
- await deleteFleetProxy(soClientMock, PROXY_IDS.RELATED_PRECONFIGURED, {
+ await deleteFleetProxy(soClientMock, esClientMock, PROXY_IDS.RELATED_PRECONFIGURED, {
fromPreconfiguration: true,
});
expect(mockedOutputService.update).toBeCalled();
diff --git a/x-pack/plugins/fleet/server/services/fleet_proxies.ts b/x-pack/plugins/fleet/server/services/fleet_proxies.ts
index 09868568e84a8..2677714d6c436 100644
--- a/x-pack/plugins/fleet/server/services/fleet_proxies.ts
+++ b/x-pack/plugins/fleet/server/services/fleet_proxies.ts
@@ -5,7 +5,11 @@
* 2.0.
*/
-import type { SavedObjectsClientContract, SavedObject } from '@kbn/core/server';
+import type {
+ SavedObjectsClientContract,
+ SavedObject,
+ ElasticsearchClient,
+} from '@kbn/core/server';
import { omit } from 'lodash';
import pMap from 'p-map';
@@ -87,6 +91,7 @@ export async function getFleetProxy(
export async function deleteFleetProxy(
soClient: SavedObjectsClientContract,
+ esClient: ElasticsearchClient,
id: string,
options?: { fromPreconfiguration?: boolean }
) {
@@ -108,7 +113,7 @@ export async function deleteFleetProxy(
);
}
- await updateRelatedSavedObject(soClient, fleetServerHosts, outputs);
+ await updateRelatedSavedObject(soClient, esClient, fleetServerHosts, outputs);
return await soClient.delete(FLEET_PROXY_SAVED_OBJECT_TYPE, id);
}
@@ -172,6 +177,7 @@ export async function bulkGetFleetProxies(
async function updateRelatedSavedObject(
soClient: SavedObjectsClientContract,
+ esClient: ElasticsearchClient,
fleetServerHosts: FleetServerHost[],
outputs: Output[]
) {
@@ -189,7 +195,7 @@ async function updateRelatedSavedObject(
await pMap(
outputs,
(output) => {
- outputService.update(soClient, output.id, {
+ outputService.update(soClient, esClient, output.id, {
...omit(output, 'id'),
proxy_id: null,
});
diff --git a/x-pack/plugins/fleet/server/services/output.test.ts b/x-pack/plugins/fleet/server/services/output.test.ts
index 0e063fa667ec1..e8adefe47e27b 100644
--- a/x-pack/plugins/fleet/server/services/output.test.ts
+++ b/x-pack/plugins/fleet/server/services/output.test.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { savedObjectsClientMock } from '@kbn/core/server/mocks';
+import { savedObjectsClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks';
import type { OutputSOAttributes } from '../types';
@@ -168,9 +168,12 @@ function getMockedSoClient(
}
describe('Output Service', () => {
+ const esClientMock = elasticsearchServiceMock.createElasticsearchClient();
+
beforeEach(() => {
mockedAgentPolicyService.list.mockClear();
mockedAgentPolicyService.hasAPMIntegration.mockClear();
+ mockedAgentPolicyService.hasFleetServerIntegration.mockClear();
mockedAgentPolicyService.removeOutputFromAll.mockReset();
mockedAppContextService.getInternalUserSOClient.mockReset();
mockedAppContextService.getEncryptedSavedObjectsSetup.mockReset();
@@ -181,6 +184,7 @@ describe('Output Service', () => {
await outputService.create(
soClient,
+ esClientMock,
{
is_default: false,
is_default_monitoring: false,
@@ -204,6 +208,7 @@ describe('Output Service', () => {
await outputService.create(
soClient,
+ esClientMock,
{
is_default: true,
is_default_monitoring: false,
@@ -223,6 +228,7 @@ describe('Output Service', () => {
await outputService.create(
soClient,
+ esClientMock,
{
is_default: true,
is_default_monitoring: false,
@@ -245,6 +251,7 @@ describe('Output Service', () => {
await outputService.create(
soClient,
+ esClientMock,
{
is_default: false,
is_default_monitoring: true,
@@ -264,6 +271,7 @@ describe('Output Service', () => {
await outputService.create(
soClient,
+ esClientMock,
{
is_default: true,
is_default_monitoring: true,
@@ -290,6 +298,7 @@ describe('Output Service', () => {
await expect(
outputService.create(
soClient,
+ esClientMock,
{
is_default: true,
is_default_monitoring: false,
@@ -310,6 +319,7 @@ describe('Output Service', () => {
await outputService.create(
soClient,
+ esClientMock,
{
is_default: true,
is_default_monitoring: true,
@@ -334,6 +344,7 @@ describe('Output Service', () => {
await expect(
outputService.create(
soClient,
+ esClientMock,
{
is_default: false,
is_default_monitoring: false,
@@ -345,13 +356,14 @@ describe('Output Service', () => {
).rejects.toThrow(`Logstash output needs encrypted saved object api key to be set`);
});
- it('should work if encryptedSavedObject is configured', async () => {
+ it('should work if encryptedSavedObject is configured', async () => {
const soClient = getMockedSoClient({});
mockedAppContextService.getEncryptedSavedObjectsSetup.mockReturnValue({
canEncrypt: true,
} as any);
await outputService.create(
soClient,
+ esClientMock,
{
is_default: false,
is_default_monitoring: false,
@@ -362,6 +374,66 @@ describe('Output Service', () => {
);
expect(soClient.create).toBeCalled();
});
+
+ it('Should update fleet server policies with data_output_id=default_output_id if a default new logstash output is created', async () => {
+ const soClient = getMockedSoClient({
+ defaultOutputId: 'output-test',
+ });
+ mockedAppContextService.getEncryptedSavedObjectsSetup.mockReturnValue({
+ canEncrypt: true,
+ } as any);
+ mockedAgentPolicyService.list.mockResolvedValue({
+ items: [
+ {
+ name: 'fleet server policy',
+ id: 'fleet_server_policy',
+ is_default_fleet_server: true,
+ package_policies: [
+ {
+ name: 'fleet-server-123',
+ package: {
+ name: 'fleet_server',
+ },
+ },
+ ],
+ },
+ {
+ name: 'agent policy 1',
+ id: 'agent_policy_1',
+ is_managed: false,
+ package_policies: [
+ {
+ name: 'nginx',
+ package: {
+ name: 'nginx',
+ },
+ },
+ ],
+ },
+ ],
+ } as unknown as ReturnType);
+ mockedAgentPolicyService.hasFleetServerIntegration.mockReturnValue(true);
+
+ await outputService.create(
+ soClient,
+ esClientMock,
+ {
+ is_default: true,
+ is_default_monitoring: false,
+ name: 'Test',
+ type: 'logstash',
+ },
+ { id: 'output-1' }
+ );
+
+ expect(mockedAgentPolicyService.update).toBeCalledWith(
+ expect.anything(),
+ expect.anything(),
+ 'fleet_server_policy',
+ { data_output_id: 'output-test' },
+ { force: true }
+ );
+ });
});
describe('update', () => {
@@ -370,7 +442,7 @@ describe('Output Service', () => {
defaultOutputId: 'existing-default-output',
});
- await outputService.update(soClient, 'output-test', {
+ await outputService.update(soClient, esClientMock, 'output-test', {
is_default: true,
});
@@ -390,7 +462,7 @@ describe('Output Service', () => {
defaultOutputId: 'existing-default-output',
});
- await outputService.update(soClient, 'existing-default-output', {
+ await outputService.update(soClient, esClientMock, 'existing-default-output', {
is_default: true,
name: 'Test',
});
@@ -408,7 +480,7 @@ describe('Output Service', () => {
defaultOutputMonitoringId: 'existing-default-monitoring-output',
});
- await outputService.update(soClient, 'output-test', {
+ await outputService.update(soClient, esClientMock, 'output-test', {
is_default_monitoring: true,
});
@@ -427,7 +499,7 @@ describe('Output Service', () => {
it('Do not allow to update a preconfigured output outisde from preconfiguration', async () => {
const soClient = getMockedSoClient();
await expect(
- outputService.update(soClient, 'existing-preconfigured-default-output', {
+ outputService.update(soClient, esClientMock, 'existing-preconfigured-default-output', {
config_yaml: '',
})
).rejects.toThrow(
@@ -439,6 +511,7 @@ describe('Output Service', () => {
const soClient = getMockedSoClient();
await outputService.update(
soClient,
+ esClientMock,
'existing-preconfigured-default-output',
{
config_yaml: '',
@@ -457,7 +530,7 @@ describe('Output Service', () => {
});
await expect(
- outputService.update(soClient, 'output-test', {
+ outputService.update(soClient, esClientMock, 'output-test', {
is_default: true,
is_default_monitoring: false,
name: 'Test',
@@ -475,6 +548,7 @@ describe('Output Service', () => {
await outputService.update(
soClient,
+ esClientMock,
'output-test',
{
is_default: true,
@@ -501,7 +575,7 @@ describe('Output Service', () => {
} as unknown as ReturnType);
mockedAgentPolicyService.hasAPMIntegration.mockReturnValue(false);
- await outputService.update(soClient, 'existing-logstash-output', {
+ await outputService.update(soClient, esClientMock, 'existing-logstash-output', {
type: 'elasticsearch',
hosts: ['http://test:4343'],
});
@@ -521,12 +595,13 @@ describe('Output Service', () => {
} as unknown as ReturnType);
mockedAgentPolicyService.hasAPMIntegration.mockReturnValue(false);
- await outputService.update(soClient, 'existing-logstash-output', {
+ await outputService.update(soClient, esClientMock, 'existing-logstash-output', {
is_default: true,
});
expect(soClient.update).toBeCalled();
});
+
it('Should call update with null fields if', async () => {
const soClient = getMockedSoClient({});
mockedAgentPolicyService.list.mockResolvedValue({
@@ -534,7 +609,7 @@ describe('Output Service', () => {
} as unknown as ReturnType);
mockedAgentPolicyService.hasAPMIntegration.mockReturnValue(false);
- await outputService.update(soClient, 'existing-logstash-output', {
+ await outputService.update(soClient, esClientMock, 'existing-logstash-output', {
is_default: true,
ca_sha256: null,
ca_trusted_fingerprint: null,
@@ -551,6 +626,7 @@ describe('Output Service', () => {
ssl: null,
});
});
+
it('Should throw if you try to make that output the default output and somne policies using default output has APM integration', async () => {
const soClient = getMockedSoClient({});
mockedAgentPolicyService.list.mockResolvedValue({
@@ -559,29 +635,133 @@ describe('Output Service', () => {
mockedAgentPolicyService.hasAPMIntegration.mockReturnValue(true);
await expect(
- outputService.update(soClient, 'existing-logstash-output', {
+ outputService.update(soClient, esClientMock, 'existing-logstash-output', {
is_default: true,
})
).rejects.toThrow(`Logstash output cannot be used with APM integration.`);
});
- it('Should delete ES specific fields if the output type change to logstash', async () => {
+
+ it('Should delete ES specific fields if the output type changes to logstash', async () => {
const soClient = getMockedSoClient({});
mockedAgentPolicyService.list.mockResolvedValue({
items: [{}],
} as unknown as ReturnType);
mockedAgentPolicyService.hasAPMIntegration.mockReturnValue(false);
+ mockedAgentPolicyService.hasFleetServerIntegration.mockReturnValue(false);
+
+ await outputService.update(soClient, esClientMock, 'existing-es-output', {
+ type: 'logstash',
+ hosts: ['test:4343'],
+ });
+
+ expect(soClient.update).toBeCalledWith(expect.anything(), expect.anything(), {
+ type: 'logstash',
+ hosts: ['test:4343'],
+ ca_sha256: null,
+ ca_trusted_fingerprint: null,
+ });
+ });
- await outputService.update(soClient, 'existing-es-output', {
+ it('Should update fleet server policies with data_output_id=default_output_id if a default ES output is changed to logstash', async () => {
+ const soClient = getMockedSoClient({
+ defaultOutputId: 'output-test',
+ });
+ mockedAgentPolicyService.list.mockResolvedValue({
+ items: [
+ {
+ name: 'fleet server policy',
+ id: 'fleet_server_policy',
+ is_default_fleet_server: true,
+ package_policies: [
+ {
+ name: 'fleet-server-123',
+ package: {
+ name: 'fleet_server',
+ },
+ },
+ ],
+ },
+ {
+ name: 'agent policy 1',
+ id: 'agent_policy_1',
+ is_managed: false,
+ package_policies: [
+ {
+ name: 'nginx',
+ package: {
+ name: 'nginx',
+ },
+ },
+ ],
+ },
+ ],
+ } as unknown as ReturnType);
+ mockedAgentPolicyService.hasFleetServerIntegration.mockReturnValue(true);
+
+ await outputService.update(soClient, esClientMock, 'output-test', {
type: 'logstash',
hosts: ['test:4343'],
+ is_default: true,
});
expect(soClient.update).toBeCalledWith(expect.anything(), expect.anything(), {
type: 'logstash',
hosts: ['test:4343'],
+ is_default: true,
ca_sha256: null,
ca_trusted_fingerprint: null,
});
+ expect(mockedAgentPolicyService.update).toBeCalledWith(
+ expect.anything(),
+ expect.anything(),
+ 'fleet_server_policy',
+ { data_output_id: 'output-test' },
+ { force: true }
+ );
+ });
+
+ it('Should return an error if trying to change the output to logstash for fleet server policy', async () => {
+ const soClient = getMockedSoClient({});
+ mockedAgentPolicyService.list.mockResolvedValue({
+ items: [
+ {
+ name: 'fleet server policy',
+ id: 'fleet_server_policy',
+ is_default_fleet_server: true,
+ package_policies: [
+ {
+ name: 'fleet-server-123',
+ package: {
+ name: 'fleet_server',
+ },
+ },
+ ],
+ },
+ {
+ name: 'agent policy 1',
+ id: 'agent_policy_1',
+ is_managed: false,
+ package_policies: [
+ {
+ name: 'nginx',
+ package: {
+ name: 'nginx',
+ },
+ },
+ ],
+ },
+ ],
+ } as unknown as ReturnType);
+ mockedAgentPolicyService.hasFleetServerIntegration.mockReturnValue(true);
+
+ await expect(
+ outputService.update(soClient, esClientMock, 'existing-es-output', {
+ type: 'logstash',
+ hosts: ['test:4343'],
+ })
+ ).rejects.toThrowError(
+ 'Logstash output cannot be used with Fleet Server integration in fleet server policy. Please create a new ElasticSearch output.'
+ );
});
});
diff --git a/x-pack/plugins/fleet/server/services/output.ts b/x-pack/plugins/fleet/server/services/output.ts
index 41f699ab5e318..86fadfc7524ed 100644
--- a/x-pack/plugins/fleet/server/services/output.ts
+++ b/x-pack/plugins/fleet/server/services/output.ts
@@ -5,12 +5,17 @@
* 2.0.
*/
-import type { KibanaRequest, SavedObject, SavedObjectsClientContract } from '@kbn/core/server';
+import type {
+ KibanaRequest,
+ SavedObject,
+ SavedObjectsClientContract,
+ ElasticsearchClient,
+} from '@kbn/core/server';
import { v5 as uuidv5 } from 'uuid';
import { omit } from 'lodash';
import { safeLoad } from 'js-yaml';
-import type { NewOutput, Output, OutputSOAttributes } from '../types';
+import type { NewOutput, Output, OutputSOAttributes, AgentPolicy } from '../types';
import {
DEFAULT_OUTPUT,
DEFAULT_OUTPUT_ID,
@@ -78,12 +83,11 @@ function outputSavedObjectToOutput(so: SavedObject): Output
};
}
-async function validateLogstashOutputNotUsedInAPMPolicy(
+async function getAgentPoliciesPerOutput(
soClient: SavedObjectsClientContract,
outputId?: string,
isDefault?: boolean
) {
- // Validate no policy with APM use that policy
let kuery: string;
if (outputId) {
if (isDefault) {
@@ -104,9 +108,86 @@ async function validateLogstashOutputNotUsedInAPMPolicy(
perPage: SO_SEARCH_LIMIT,
withPackagePolicies: true,
});
- for (const agentPolicy of agentPolicySO.items) {
- if (agentPolicyService.hasAPMIntegration(agentPolicy)) {
- throw new OutputInvalidError('Logstash output cannot be used with APM integration.');
+ return agentPolicySO?.items;
+}
+
+async function validateLogstashOutputNotUsedInAPMPolicy(
+ soClient: SavedObjectsClientContract,
+ outputId?: string,
+ isDefault?: boolean
+) {
+ const agentPolicies = await getAgentPoliciesPerOutput(soClient, outputId, isDefault);
+
+ // Validate no policy with APM use that policy
+ if (agentPolicies) {
+ for (const agentPolicy of agentPolicies) {
+ if (agentPolicyService.hasAPMIntegration(agentPolicy)) {
+ throw new OutputInvalidError('Logstash output cannot be used with APM integration.');
+ }
+ }
+ }
+}
+
+async function findPoliciesWithFleetServer(
+ soClient: SavedObjectsClientContract,
+ outputId?: string,
+ isDefault?: boolean
+) {
+ // find agent policies by outputId
+ // otherwise query all the policies
+ const agentPolicies = outputId
+ ? await getAgentPoliciesPerOutput(soClient, outputId, isDefault)
+ : (await agentPolicyService.list(soClient, { withPackagePolicies: true }))?.items;
+
+ if (agentPolicies) {
+ const policiesWithFleetServer = agentPolicies.filter((policy) =>
+ agentPolicyService.hasFleetServerIntegration(policy)
+ );
+ return policiesWithFleetServer;
+ }
+ return [];
+}
+
+function validateLogstashOutputNotUsedInFleetServerPolicy(agentPolicies: AgentPolicy[]) {
+ // Validate no policy with fleet server use that policy
+ for (const agentPolicy of agentPolicies) {
+ throw new OutputInvalidError(
+ `Logstash output cannot be used with Fleet Server integration in ${agentPolicy.name}. Please create a new ElasticSearch output.`
+ );
+ }
+}
+
+async function validateTypeChanges(
+ soClient: SavedObjectsClientContract,
+ esClient: ElasticsearchClient,
+ id: string,
+ data: Partial