[Console] Set up a folder for manual definitions files (#162652)

## Summary
Fixes https://github.com/elastic/kibana/issues/162564

This PR adds a new folder to Console server side where manually created
definitions for endpoint will be stored. This is important before we
switch to the new script. The logic in the new script is to clear the
folder with generated definitions before re-generating them. That is not
the case in the current script. The downside of that is when an endpoint
is removed from the specifications, it won't be removed from
autocomplete definitions automatically. Displaying autocomplete
suggestions for unavailable endpoints might be confusing for users.

Currently, the `manual` folder is empty because there were no
definitions present in the folder `generated` that would not be
re-generated if the script would clear the folder and create all
endpoints defined in ES json specs. I first suspected that endpoints
from these 2 PRs (https://github.com/elastic/kibana/pull/162503,
https://github.com/elastic/kibana/pull/158674) would need to be moved to
the manual folder, but the definitions added there manually can be
re-generated using the script.

I also removed several deprecated/deleted/renamed endpoints from the
`generated` folder. Several files in the `overrides` folder needed
renaming and one was deleted as deprecated.

There are also smaller renaming changes in this PR because I think the
code is more difficult to understand when it's using
"spec/specification" and "definition" interchangeably or even together
as "SpecDefinitions". I believe we should use "specification" for the ES
specifications (i.e. the source) and "definitions" or even better
"autocomplete definitions" for the files that are used for Console
autocomplete engine. The renaming is to be continued in follow up PRs.

I also added a unit test file for the SpecDefinitionsService since it
contains a lot of important logic for endpoints such as loading
generated definitions, overrides and manual ones and filtering out
endpoints not available in the current context.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Alison Goryachev <alisonmllr20@gmail.com>
This commit is contained in:
Yulia Čech 2023-08-02 18:37:06 +02:00 committed by GitHub
parent c07aac5ff4
commit fbfd3ed0dd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 481 additions and 484 deletions

View file

@ -54,7 +54,14 @@ Autocomplete definitions are all created in the form of javascript objects loade
### Creating definitions
The [`generated`](https://github.com/elastic/kibana/blob/main/src/plugins/console/server/lib/spec_definitions/json/generated) folder contains definitions created automatically from Elasticsearch REST API specifications. See this [README](https://github.com/elastic/kibana/blob/main/packages/kbn-spec-to-console/README.md) file for more information on the `spec-to-console` script.
Manually created override files in the [`overrides`](https://github.com/elastic/kibana/blob/main/src/plugins/console/server/lib/spec_definitions/json/overrides) folder contain fixes for generated files and additions for request body parameters.
Manually created override files in the [`overrides`](https://github.com/elastic/kibana/blob/main/src/plugins/console/server/lib/spec_definitions/json/overrides) folder contain additions for request body parameters since those
are not created by the script. Any other fixes such as documentation links, request methods and patterns and url parameters
should be addressed at the source. That means this should be fixed in Elasticsearch REST API specifications and then
autocomplete definitions can be re-generated with the script.
If there are any endpoints missing completely from the `generated` folder, this should also be addressed at the source, i.e.
Elasticsearch REST API specifications. If for some reason, that is not possible, then additional definitions files
can be placed in the folder [`manual`]((https://github.com/elastic/kibana/blob/main/src/plugins/console/server/lib/spec_definitions/json/manual)).
### Top level keys
Use following top level keys in the definitions objects.

View file

@ -12,3 +12,7 @@ export const AUTOCOMPLETE_DEFINITIONS_FOLDER = resolve(
__dirname,
'../../server/lib/spec_definitions/json'
);
export const GENERATED_SUBFOLDER = 'generated';
export const OVERRIDES_SUBFOLDER = 'overrides';
export const MANUAL_SUBFOLDER = 'manual';

View file

@ -9,4 +9,9 @@
export { MAJOR_VERSION } from './plugin';
export { API_BASE_PATH, KIBANA_API_PREFIX } from './api';
export { DEFAULT_VARIABLES } from './variables';
export { AUTOCOMPLETE_DEFINITIONS_FOLDER } from './autocomplete_definitions';
export {
AUTOCOMPLETE_DEFINITIONS_FOLDER,
GENERATED_SUBFOLDER,
OVERRIDES_SUBFOLDER,
MANUAL_SUBFOLDER,
} from './autocomplete_definitions';

View file

@ -0,0 +1,23 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export type EndpointsAvailability = 'stack' | 'serverless';
export interface EndpointDescription {
methods?: string[];
patterns?: string | string[];
url_params?: Record<string, unknown>;
data_autocomplete_rules?: Record<string, unknown>;
url_components?: Record<string, unknown>;
priority?: number;
availability?: Record<EndpointsAvailability, boolean>;
}
export interface EndpointDefinition {
[endpointName: string]: EndpointDescription;
}

View file

@ -8,3 +8,4 @@
export * from './models';
export * from './plugin_config';
export * from './autocomplete_definitions';

View file

@ -26,8 +26,9 @@ const schemaLatest = schema.object(
}),
autocompleteDefinitions: schema.object({
// Only displays the endpoints that are available in the specified environment
// Current supported values are 'stack' and 'serverless'
endpointsAvailability: schema.string({ defaultValue: 'stack' }),
endpointsAvailability: schema.oneOf([schema.literal('stack'), schema.literal('serverless')], {
defaultValue: 'stack',
}),
}),
},
{ defaultValue: undefined }

View file

@ -0,0 +1 @@
Please refer to this [README](https://github.com/elastic/kibana/blob/main/src/plugins/console/README.md#creating-definitions) file before adding/editing definitions files in this folder.

View file

@ -1,11 +0,0 @@
{
"autoscaling.get_autoscaling_decision": {
"methods": [
"GET"
],
"patterns": [
"_autoscaling/decision"
],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/autoscaling-get-autoscaling-decision.html"
}
}

View file

@ -1,10 +0,0 @@
{
"data_frame_transform_deprecated.delete_transform": {
"url_params": {
"force": "__flag__"
},
"methods": [],
"patterns": [],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/delete-transform.html"
}
}

View file

@ -1,13 +0,0 @@
{
"data_frame_transform_deprecated.get_transform": {
"url_params": {
"from": 0,
"size": 0,
"allow_no_match": "__flag__",
"exclude_generated": "__flag__"
},
"methods": [],
"patterns": [],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/get-transform.html"
}
}

View file

@ -1,12 +0,0 @@
{
"data_frame_transform_deprecated.get_transform_stats": {
"url_params": {
"from": "",
"size": "",
"allow_no_match": "__flag__"
},
"methods": [],
"patterns": [],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/get-transform-stats.html"
}
}

View file

@ -1,7 +0,0 @@
{
"data_frame_transform_deprecated.preview_transform": {
"methods": [],
"patterns": [],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/preview-transform.html"
}
}

View file

@ -1,10 +0,0 @@
{
"data_frame_transform_deprecated.put_transform": {
"url_params": {
"defer_validation": "__flag__"
},
"methods": [],
"patterns": [],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/put-transform.html"
}
}

View file

@ -1,10 +0,0 @@
{
"data_frame_transform_deprecated.start_transform": {
"url_params": {
"timeout": ""
},
"methods": [],
"patterns": [],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/start-transform.html"
}
}

View file

@ -1,12 +0,0 @@
{
"data_frame_transform_deprecated.stop_transform": {
"url_params": {
"wait_for_completion": "__flag__",
"timeout": "",
"allow_no_match": "__flag__"
},
"methods": [],
"patterns": [],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/stop-transform.html"
}
}

View file

@ -1,10 +0,0 @@
{
"data_frame_transform_deprecated.update_transform": {
"url_params": {
"defer_validation": "__flag__"
},
"methods": [],
"patterns": [],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/update-transform.html"
}
}

View file

@ -1,11 +0,0 @@
{
"ilm.set_policy": {
"methods": [
"PUT"
],
"patterns": [
"{indices}/_ilm/{new_policy}"
],
"documentation": "http://www.elastic.co/guide/en/index_lifecycle/current/index_lifecycle.html"
}
}

View file

@ -1,23 +0,0 @@
{
"indices.exists_type": {
"url_params": {
"ignore_unavailable": "__flag__",
"allow_no_indices": "__flag__",
"expand_wildcards": [
"open",
"closed",
"hidden",
"none",
"all"
],
"local": "__flag__"
},
"methods": [
"HEAD"
],
"patterns": [
"{indices}/_mapping/{type}"
],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-types-exists.html"
}
}

View file

@ -1,23 +0,0 @@
{
"indices.flush_synced": {
"url_params": {
"ignore_unavailable": "__flag__",
"allow_no_indices": "__flag__",
"expand_wildcards": [
"open",
"closed",
"none",
"all"
]
},
"methods": [
"POST",
"GET"
],
"patterns": [
"_flush/synced",
"{indices}/_flush/synced"
],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-synced-flush-api.html"
}
}

View file

@ -1,21 +0,0 @@
{
"indices.freeze": {
"url_params": {
"timeout": "",
"master_timeout": "",
"ignore_unavailable": "__flag__",
"allow_no_indices": "__flag__",
"expand_wildcards": [
"open",
"closed",
"hidden",
"none",
"all"
],
"wait_for_active_shards": ""
},
"methods": [],
"patterns": [],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/freeze-index-api.html"
}
}

View file

@ -1,23 +0,0 @@
{
"indices.get_upgrade": {
"url_params": {
"ignore_unavailable": "__flag__",
"allow_no_indices": "__flag__",
"expand_wildcards": [
"open",
"closed",
"hidden",
"none",
"all"
]
},
"methods": [
"GET"
],
"patterns": [
"_upgrade",
"{indices}/_upgrade"
],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-upgrade.html"
}
}

View file

@ -1,25 +0,0 @@
{
"indices.upgrade": {
"url_params": {
"allow_no_indices": "__flag__",
"expand_wildcards": [
"open",
"closed",
"hidden",
"none",
"all"
],
"ignore_unavailable": "__flag__",
"wait_for_completion": "__flag__",
"only_ancient_segments": "__flag__"
},
"methods": [
"POST"
],
"patterns": [
"_upgrade",
"{indices}/_upgrade"
],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-upgrade.html"
}
}

View file

@ -1,22 +0,0 @@
{
"xpack.migration.get_assistance": {
"url_params": {
"allow_no_indices": "__flag__",
"expand_wildcards": [
"open",
"closed",
"none",
"all"
],
"ignore_unavailable": "__flag__"
},
"methods": [
"GET"
],
"patterns": [
"_migration/assistance",
"_migration/assistance/{indices}"
],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/migration-api-assistance.html"
}
}

View file

@ -1,14 +0,0 @@
{
"xpack.migration.upgrade": {
"url_params": {
"wait_for_completion": "__flag__"
},
"methods": [
"POST"
],
"patterns": [
"_migration/upgrade/{indices}"
],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/migration-api-upgrade.html"
}
}

View file

@ -1,32 +0,0 @@
{
"ml.find_file_structure": {
"url_params": {
"lines_to_sample": 0,
"line_merge_size_limit": 0,
"timeout": "",
"charset": "",
"format": [
"ndjson",
"xml",
"delimited",
"semi_structured_text"
],
"has_header_row": "__flag__",
"column_names": [],
"delimiter": "",
"quote": "",
"should_trim_fields": "__flag__",
"grok_pattern": "",
"timestamp_field": "",
"timestamp_format": "",
"explain": "__flag__"
},
"methods": [
"POST"
],
"patterns": [
"_text_structure/find_structure"
],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/find-structure.html"
}
}

View file

@ -1,14 +0,0 @@
{
"ml.upgrade": {
"url_params": {
"wait_for_completion": "__flag__"
},
"methods": [
"POST"
],
"patterns": [
"_ml/_upgrade"
],
"documentation": "TODO"
}
}

View file

@ -1,11 +0,0 @@
{
"rollup.rollup": {
"methods": [
"POST"
],
"patterns": [
"{indices}/_rollup/{rollup_index}"
],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/xpack-rollup.html"
}
}

View file

@ -1,31 +0,0 @@
{
"transform.cat_transform": {
"url_params": {
"from": 0,
"size": 0,
"allow_no_match": "__flag__",
"format": "",
"h": [],
"help": "__flag__",
"s": [],
"time": [
"d",
"h",
"m",
"s",
"ms",
"micros",
"nanos"
],
"v": "__flag__"
},
"methods": [
"GET"
],
"patterns": [
"_cat/transforms",
"_cat/transforms/{transform_id}"
],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/cat-transforms.html"
}
}

View file

@ -1,11 +0,0 @@
{
"xpack.ssl.certificates": {
"methods": [
"GET"
],
"patterns": [
"_xpack/ssl/certificates"
],
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-ssl.html"
}
}

View file

@ -1,14 +0,0 @@
{
"indices.analyze": {
"data_autocomplete_rules": {
"text": [],
"field": "{field}",
"analyzer": "",
"tokenizer": "",
"char_filter": [],
"filter": [],
"explain": { "__one_of": [false, true] },
"attributes": []
}
}
}

View file

@ -1,7 +0,0 @@
{
"xpack.security.authenticate": {
"data_autocomplete_rules": {
"password": ""
}
}
}

View file

@ -1,7 +0,0 @@
{
"xpack.security.change_password": {
"data_autocomplete_rules": {
"password": ""
}
}
}

View file

@ -1,10 +0,0 @@
{
"xpack.security.get_token": {
"data_autocomplete_rules": {
"grant_type": "",
"password": "",
"scope": "",
"username": ""
}
}
}

View file

@ -1,7 +0,0 @@
{
"xpack.security.invalidate_token": {
"data_autocomplete_rules": {
"token": ""
}
}
}

View file

@ -1,17 +0,0 @@
{
"xpack.security.put_role": {
"data_autocomplete_rules": {
"cluster": [],
"indices": [
{
"field_security": {},
"names": [],
"privileges": [],
"query": ""
}
],
"run_as": [],
"metadata": {}
}
}
}

View file

@ -1,10 +0,0 @@
{
"xpack.security.put_role_mapping": {
"data_autocomplete_rules": {
"enabled": true,
"metadata": {},
"roles": [],
"rules": {}
}
}
}

View file

@ -1,11 +0,0 @@
{
"xpack.security.put_user": {
"data_autocomplete_rules": {
"metadata": {},
"password": "",
"full_name": "",
"roles": []
},
"documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-user.html"
}
}

View file

@ -0,0 +1,366 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import globby from 'globby';
import fs from 'fs';
import { SpecDefinitionsService } from '.';
import { EndpointDefinition, EndpointsAvailability } from '../../common/types';
const mockReadFilySync = jest.spyOn(fs, 'readFileSync');
const mockGlobbySync = jest.spyOn(globby, 'sync');
const mockJsLoadersGetter = jest.fn();
jest.mock('../lib', () => {
return {
...jest.requireActual('../lib'),
get jsSpecLoaders() {
return mockJsLoadersGetter();
},
};
});
const getMockEndpoint = ({
endpointName,
methods,
patterns,
// eslint-disable-next-line @typescript-eslint/naming-convention
data_autocomplete_rules,
availability,
}: {
endpointName: string;
methods?: string[];
patterns?: string[];
data_autocomplete_rules?: Record<string, unknown>;
availability?: Record<EndpointsAvailability, boolean>;
}): EndpointDefinition => ({
[endpointName]: {
methods: methods ?? ['GET'],
patterns: patterns ?? ['/endpoint'],
data_autocomplete_rules: data_autocomplete_rules ?? undefined,
availability: availability ?? undefined,
},
});
describe('SpecDefinitionsService', () => {
beforeAll(() => {
jest.useFakeTimers().setSystemTime(new Date(1577836800000));
});
afterAll(() => {
jest.useRealTimers();
});
beforeEach(() => {
// mock the function that lists files in the definitions folders
mockGlobbySync.mockImplementation(() => []);
// mock the function that reads files
mockReadFilySync.mockImplementation(() => '');
// mock the function that returns the list of js definitions loaders
mockJsLoadersGetter.mockImplementation(() => []);
});
afterEach(() => {
jest.resetAllMocks();
});
it('initializes with empty definitions when folders and global rules are empty', () => {
const specDefinitionsService = new SpecDefinitionsService();
specDefinitionsService.start({
endpointsAvailability: 'stack',
});
const definitions = specDefinitionsService.asJson();
expect(definitions).toEqual({
endpoints: {},
globals: {},
name: 'es',
});
});
it('loads globals rules', () => {
const loadMockAliasRule = (service: SpecDefinitionsService) => {
service.addGlobalAutocompleteRules('alias', {
param1: 1,
param2: 'test',
});
};
const loadMockIndicesRule = (service: SpecDefinitionsService) => {
service.addGlobalAutocompleteRules('indices', {
test1: 'param1',
test2: 'param2',
});
};
mockJsLoadersGetter.mockImplementation(() => [loadMockAliasRule, loadMockIndicesRule]);
const specDefinitionsService = new SpecDefinitionsService();
specDefinitionsService.start({
endpointsAvailability: 'stack',
});
const globals = specDefinitionsService.asJson().globals;
expect(globals).toEqual({
alias: {
param1: 1,
param2: 'test',
},
indices: {
test1: 'param1',
test2: 'param2',
},
});
});
it('loads generated endpoints definition', () => {
mockGlobbySync.mockImplementation((pattern) => {
if (pattern.includes('generated')) {
return ['/generated/endpoint1.json', '/generated/endpoint2.json'];
}
return [];
});
mockReadFilySync.mockImplementation((path) => {
if (path.toString() === '/generated/endpoint1.json') {
return JSON.stringify(getMockEndpoint({ endpointName: 'endpoint1' }));
}
if (path.toString() === '/generated/endpoint2.json') {
return JSON.stringify(
getMockEndpoint({
endpointName: 'endpoint2',
methods: ['POST'],
patterns: ['/endpoint2'],
})
);
}
return '';
});
const specDefinitionsService = new SpecDefinitionsService();
specDefinitionsService.start({
endpointsAvailability: 'stack',
});
const endpoints = specDefinitionsService.asJson().endpoints;
expect(endpoints).toEqual({
endpoint1: {
id: 'endpoint1',
methods: ['GET'],
patterns: ['/endpoint'],
},
endpoint2: {
id: 'endpoint2',
methods: ['POST'],
patterns: ['/endpoint2'],
},
});
});
it('overrides an endpoint if override file is present', () => {
mockGlobbySync.mockImplementation((pattern) => {
if (pattern.includes('generated')) {
return ['/generated/endpoint1.json', '/generated/endpoint2.json'];
}
if (pattern.includes('overrides')) {
return ['/overrides/endpoint1.json'];
}
return [];
});
mockReadFilySync.mockImplementation((path) => {
if (path.toString() === '/generated/endpoint1.json') {
return JSON.stringify(getMockEndpoint({ endpointName: 'endpoint1' }));
}
if (path.toString() === '/generated/endpoint2.json') {
return JSON.stringify(
getMockEndpoint({
endpointName: 'endpoint2',
methods: ['POST'],
patterns: ['/endpoint2'],
})
);
}
if (path.toString() === '/overrides/endpoint1.json') {
return JSON.stringify(
getMockEndpoint({
endpointName: 'endpoint1',
data_autocomplete_rules: {
param1: 'test',
param2: 2,
},
})
);
}
return '';
});
const specDefinitionsService = new SpecDefinitionsService();
specDefinitionsService.start({
endpointsAvailability: 'stack',
});
const endpoints = specDefinitionsService.asJson().endpoints;
expect(endpoints).toEqual({
endpoint1: {
data_autocomplete_rules: {
param1: 'test',
param2: 2,
},
id: 'endpoint1',
methods: ['GET'],
patterns: ['/endpoint'],
},
endpoint2: {
id: 'endpoint2',
methods: ['POST'],
patterns: ['/endpoint2'],
},
});
});
it('loads manual definitions if any', () => {
mockGlobbySync.mockImplementation((pattern) => {
if (pattern.includes('manual')) {
return ['manual_endpoint.json'];
}
return [];
});
mockReadFilySync.mockImplementation((path) => {
if (path.toString() === 'manual_endpoint.json') {
return JSON.stringify(getMockEndpoint({ endpointName: 'manual_endpoint' }));
}
return '';
});
const specDefinitionsService = new SpecDefinitionsService();
specDefinitionsService.start({
endpointsAvailability: 'stack',
});
const endpoints = specDefinitionsService.asJson().endpoints;
expect(endpoints).toEqual({
manual_endpoint: {
id: 'manual_endpoint',
methods: ['GET'],
patterns: ['/endpoint'],
},
});
});
it("manual definitions don't override generated files even when the same endpoint name is used", () => {
mockGlobbySync.mockImplementation((pattern) => {
if (pattern.includes('generated')) {
return ['generated_endpoint.json'];
}
if (pattern.includes('manual')) {
return ['manual_endpoint.json'];
}
return [];
});
mockReadFilySync.mockImplementation((path) => {
if (path.toString() === 'generated_endpoint.json') {
return JSON.stringify(getMockEndpoint({ endpointName: 'test', methods: ['GET'] }));
}
if (path.toString() === 'manual_endpoint.json') {
return JSON.stringify(
getMockEndpoint({ endpointName: 'test', methods: ['POST'], patterns: ['/manual_test'] })
);
}
return '';
});
const specDefinitionsService = new SpecDefinitionsService();
specDefinitionsService.start({
endpointsAvailability: 'stack',
});
const endpoints = specDefinitionsService.asJson().endpoints;
expect(endpoints).toEqual({
test: {
id: 'test',
methods: ['GET'],
patterns: ['/endpoint'],
},
test1577836800000: {
id: 'test1577836800000',
methods: ['POST'],
patterns: ['/manual_test'],
},
});
});
it('filters out endpoints not available in stack', () => {
mockGlobbySync.mockImplementation((pattern) => {
if (pattern.includes('generated')) {
return ['/generated/endpoint1.json', '/generated/endpoint2.json'];
}
return [];
});
mockReadFilySync.mockImplementation((path) => {
if (path.toString() === '/generated/endpoint1.json') {
return JSON.stringify(
getMockEndpoint({
endpointName: 'endpoint1',
availability: { stack: false, serverless: true },
})
);
}
if (path.toString() === '/generated/endpoint2.json') {
return JSON.stringify(
getMockEndpoint({
endpointName: 'endpoint2',
methods: ['POST'],
patterns: ['/endpoint2'],
})
);
}
return '';
});
const specDefinitionsService = new SpecDefinitionsService();
specDefinitionsService.start({
endpointsAvailability: 'stack',
});
const endpoints = specDefinitionsService.asJson().endpoints;
expect(endpoints).toEqual({
endpoint2: {
id: 'endpoint2',
methods: ['POST'],
patterns: ['/endpoint2'],
},
});
});
it('filters out endpoints not available in serverless', () => {
mockGlobbySync.mockImplementation((pattern) => {
if (pattern.includes('generated')) {
return ['/generated/endpoint1.json', '/generated/endpoint2.json'];
}
return [];
});
mockReadFilySync.mockImplementation((path) => {
if (path.toString() === '/generated/endpoint1.json') {
return JSON.stringify(
getMockEndpoint({
endpointName: 'endpoint1',
availability: { stack: true, serverless: false },
})
);
}
if (path.toString() === '/generated/endpoint2.json') {
return JSON.stringify(
getMockEndpoint({
endpointName: 'endpoint2',
methods: ['POST'],
patterns: ['/endpoint2'],
})
);
}
return '';
});
const specDefinitionsService = new SpecDefinitionsService();
specDefinitionsService.start({
endpointsAvailability: 'serverless',
});
const endpoints = specDefinitionsService.asJson().endpoints;
expect(endpoints).toEqual({
endpoint2: {
id: 'endpoint2',
methods: ['POST'],
patterns: ['/endpoint2'],
},
});
});
});

View file

@ -12,37 +12,37 @@ import { basename, join } from 'path';
import normalizePath from 'normalize-path';
import { readFileSync } from 'fs';
import { AUTOCOMPLETE_DEFINITIONS_FOLDER } from '../../common/constants';
import {
AUTOCOMPLETE_DEFINITIONS_FOLDER,
GENERATED_SUBFOLDER,
MANUAL_SUBFOLDER,
OVERRIDES_SUBFOLDER,
} from '../../common/constants';
import { jsSpecLoaders } from '../lib';
interface EndpointDescription {
methods?: string[];
patterns?: string | string[];
url_params?: Record<string, unknown>;
data_autocomplete_rules?: Record<string, unknown>;
url_components?: Record<string, unknown>;
priority?: number;
availability?: Record<string, boolean>;
}
import type {
EndpointsAvailability,
EndpointDescription,
EndpointDefinition,
} from '../../common/types';
export interface SpecDefinitionsDependencies {
endpointsAvailability: string;
endpointsAvailability: EndpointsAvailability;
}
export class SpecDefinitionsService {
private readonly name = 'es';
private readonly globalRules: Record<string, any> = {};
private readonly endpoints: Record<string, any> = {};
private readonly endpoints: Record<string, EndpointDescription> = {};
private hasLoadedSpec = false;
private hasLoadedDefinitions = false;
public addGlobalAutocompleteRules(parentNode: string, rules: unknown) {
this.globalRules[parentNode] = rules;
}
public addEndpointDescription(endpoint: string, description: EndpointDescription = {}) {
let copiedDescription: { patterns?: string; url_params?: Record<string, unknown> } = {};
let copiedDescription: EndpointDescription = {};
if (this.endpoints[endpoint]) {
copiedDescription = { ...this.endpoints[endpoint] };
}
@ -87,42 +87,70 @@ export class SpecDefinitionsService {
}
public start({ endpointsAvailability }: SpecDefinitionsDependencies) {
if (!this.hasLoadedSpec) {
this.loadJsonSpec(endpointsAvailability);
this.loadJSSpec();
this.hasLoadedSpec = true;
if (!this.hasLoadedDefinitions) {
this.loadJsonDefinitions(endpointsAvailability);
this.loadJSDefinitions();
this.hasLoadedDefinitions = true;
} else {
throw new Error('Service has already started!');
}
}
private loadJSONSpecInDir(dirname: string) {
private loadJSONDefinitionsFiles() {
// we need to normalize paths otherwise they don't work on windows, see https://github.com/elastic/kibana/issues/151032
const generatedFiles = globby.sync(normalizePath(join(dirname, 'generated', '*.json')));
const overrideFiles = globby.sync(normalizePath(join(dirname, 'overrides', '*.json')));
const generatedFiles = globby.sync(
normalizePath(join(AUTOCOMPLETE_DEFINITIONS_FOLDER, GENERATED_SUBFOLDER, '*.json'))
);
const overrideFiles = globby.sync(
normalizePath(join(AUTOCOMPLETE_DEFINITIONS_FOLDER, OVERRIDES_SUBFOLDER, '*.json'))
);
const manualFiles = globby.sync(
normalizePath(join(AUTOCOMPLETE_DEFINITIONS_FOLDER, MANUAL_SUBFOLDER, '*.json'))
);
return generatedFiles.reduce((acc, file) => {
// definitions files contain only 1 definition per endpoint name { "endpointName": { endpointDescription }}
// all endpoints need to be merged into 1 object with endpoint names as keys and endpoint definitions as values
const jsonDefinitions = {} as Record<string, EndpointDescription>;
generatedFiles.forEach((file) => {
const overrideFile = overrideFiles.find((f) => basename(f) === basename(file));
const loadedSpec: Record<string, EndpointDescription> = JSON.parse(
readFileSync(file, 'utf8')
);
const loadedDefinition: EndpointDefinition = JSON.parse(readFileSync(file, 'utf8'));
if (overrideFile) {
merge(loadedSpec, JSON.parse(readFileSync(overrideFile, 'utf8')));
merge(loadedDefinition, JSON.parse(readFileSync(overrideFile, 'utf8')));
}
Object.entries(loadedSpec).forEach(([key, value]) => {
if (acc[key]) {
// add time to remove key collision
acc[`${key}${Date.now()}`] = value;
} else {
acc[key] = value;
}
});
return acc;
}, {} as Record<string, EndpointDescription>);
this.addToJsonDefinitions({ loadedDefinition, jsonDefinitions });
});
// add manual definitions
manualFiles.forEach((file) => {
const loadedDefinition: EndpointDefinition = JSON.parse(readFileSync(file, 'utf8'));
this.addToJsonDefinitions({ loadedDefinition, jsonDefinitions });
});
return jsonDefinitions;
}
private loadJsonSpec(endpointsAvailability: string) {
const result = this.loadJSONSpecInDir(AUTOCOMPLETE_DEFINITIONS_FOLDER);
private addToJsonDefinitions({
loadedDefinition,
jsonDefinitions,
}: {
loadedDefinition: EndpointDefinition;
jsonDefinitions: Record<string, EndpointDescription>;
}) {
// iterate over EndpointDefinition for a safe and easy access to the only property in this object
Object.entries(loadedDefinition).forEach(([endpointName, endpointDescription]) => {
// endpoints should all have unique names, but in case that happens unintentionally
// don't silently overwrite the definition but create a new unique endpoint name
if (jsonDefinitions[endpointName]) {
// add time to create a unique key
jsonDefinitions[`${endpointName}${Date.now()}`] = endpointDescription;
} else {
jsonDefinitions[endpointName] = endpointDescription;
}
});
return jsonDefinitions;
}
private loadJsonDefinitions(endpointsAvailability: string) {
const result = this.loadJSONDefinitionsFiles();
Object.keys(result).forEach((endpoint) => {
const description = result[endpoint];
@ -137,7 +165,7 @@ export class SpecDefinitionsService {
});
}
private loadJSSpec() {
private loadJSDefinitions() {
jsSpecLoaders.forEach((addJsSpec) => addJsSpec(this));
}
}

View file

@ -83,7 +83,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
// what types of config settings can be exposed to the browser.
// When plugin owners make a change that exposes additional config values, the changes will be reflected in this test assertion.
// Ensure that your change does not unintentionally expose any sensitive values!
'console.autocompleteDefinitions.endpointsAvailability (string)',
'console.autocompleteDefinitions.endpointsAvailability (alternatives)',
'console.ui.enabled (boolean)',
'dashboard.allowByValueEmbeddables (boolean)',
'unifiedSearch.autocomplete.querySuggestions.enabled (boolean)',