[Index management] Server-side NP ready (#56829)

This commit is contained in:
Sébastien Loix 2020-02-11 11:36:17 +05:30 committed by GitHub
parent bb7e152211
commit 7e9d79754c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 1185 additions and 535 deletions

View file

@ -9,7 +9,7 @@ import { PLUGIN } from './common/constants';
import { registerLicenseChecker } from './server/lib/register_license_checker';
import { registerRoutes } from './server/routes/register_routes';
import { ccrDataEnricher } from './cross_cluster_replication_data';
import { addIndexManagementDataEnricher } from '../index_management/server/index_management_data';
export function crossClusterReplication(kibana) {
return new kibana.Plugin({
id: PLUGIN.ID,
@ -49,8 +49,13 @@ export function crossClusterReplication(kibana) {
init: function initCcrPlugin(server) {
registerLicenseChecker(server);
registerRoutes(server);
if (server.config().get('xpack.ccr.ui.enabled')) {
addIndexManagementDataEnricher(ccrDataEnricher);
if (
server.config().get('xpack.ccr.ui.enabled') &&
server.plugins.index_management &&
server.plugins.index_management.addIndexManagementDataEnricher
) {
server.plugins.index_management.addIndexManagementDataEnricher(ccrDataEnricher);
}
},
});

View file

@ -4,13 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { LICENSE_TYPE_BASIC } from '../../../../common/constants';
import { LicenseType } from '../../../../../plugins/licensing/common/types';
const basicLicense: LicenseType = 'basic';
export const PLUGIN = {
ID: 'index_management',
id: 'index_management',
minimumLicenseType: basicLicense,
getI18nName: (i18n: any): string =>
i18n.translate('xpack.idxMgmt.appTitle', {
defaultMessage: 'Index Management',
}),
MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_BASIC,
};

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { PLUGIN, API_BASE_PATH } from './constants';

View file

@ -5,19 +5,15 @@
*/
import { resolve } from 'path';
import { i18n } from '@kbn/i18n';
import { Legacy } from 'kibana';
import { createRouter } from '../../server/lib/create_router';
import { registerLicenseChecker } from '../../server/lib/register_license_checker';
import { PLUGIN, API_BASE_PATH } from './common/constants';
import { LegacySetup } from './server/plugin';
import { plugin as initServerPlugin } from './server';
import { PLUGIN } from './common/constants';
import { plugin as initServerPlugin, Dependencies } from './server';
export type ServerFacade = Legacy.Server;
export function indexManagement(kibana: any) {
return new kibana.Plugin({
id: PLUGIN.ID,
id: PLUGIN.id,
configPrefix: 'xpack.index_management',
publicDir: resolve(__dirname, 'public'),
require: ['kibana', 'elasticsearch', 'xpack_main'],
@ -29,32 +25,15 @@ export function indexManagement(kibana: any) {
init(server: ServerFacade) {
const coreSetup = server.newPlatform.setup.core;
const pluginsSetup = {};
const __LEGACY: LegacySetup = {
router: createRouter(server, PLUGIN.ID, `${API_BASE_PATH}/`),
plugins: {
license: {
registerLicenseChecker: registerLicenseChecker.bind(
null,
server,
PLUGIN.ID,
PLUGIN.getI18nName(i18n),
PLUGIN.MINIMUM_LICENSE_REQUIRED as 'basic'
),
},
elasticsearch: server.plugins.elasticsearch,
},
const coreInitializerContext = server.newPlatform.coreContext;
const pluginsSetup: Dependencies = {
licensing: server.newPlatform.setup.plugins.licensing as any,
};
const serverPlugin = initServerPlugin();
const indexMgmtSetup = serverPlugin.setup(coreSetup, pluginsSetup, __LEGACY);
const serverPlugin = initServerPlugin(coreInitializerContext as any);
const serverPublicApi = serverPlugin.setup(coreSetup, pluginsSetup);
server.expose(
'addIndexManagementDataEnricher',
indexMgmtSetup.addIndexManagementDataEnricher
);
server.expose('addIndexManagementDataEnricher', serverPublicApi.indexDataEnricher.add);
},
});
}

View file

@ -3,8 +3,10 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { IndexMgmtPlugin } from './plugin';
export function plugin() {
return new IndexMgmtPlugin();
}
import { PluginInitializerContext } from 'src/core/server';
import { IndexMgmtServerPlugin } from './plugin';
export const plugin = (ctx: PluginInitializerContext) => new IndexMgmtServerPlugin(ctx);
export { Dependencies } from './types';

View file

@ -1,15 +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;
* you may not use this file except in compliance with the Elastic License.
*/
const indexManagementDataEnrichers: any[] = [];
export const addIndexManagementDataEnricher = (enricher: any) => {
indexManagementDataEnrichers.push(enricher);
};
export const getIndexManagementDataEnrichers = () => {
return indexManagementDataEnrichers;
};

View file

@ -3,8 +3,10 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { IndexDataEnricher } from '../services';
import { Index, CallAsCurrentUser } from '../types';
import { fetchAliases } from './fetch_aliases';
import { getIndexManagementDataEnrichers } from '../index_management_data';
interface Hit {
health: string;
status: string;
@ -27,22 +29,7 @@ interface Params {
index?: string[];
}
const enrichResponse = async (response: any, callWithRequest: any) => {
let enrichedResponse = response;
const dataEnrichers = getIndexManagementDataEnrichers();
for (let i = 0; i < dataEnrichers.length; i++) {
const dataEnricher = dataEnrichers[i];
try {
const dataEnricherResponse = await dataEnricher(enrichedResponse, callWithRequest);
enrichedResponse = dataEnricherResponse;
} catch (e) {
// silently swallow enricher response errors
}
}
return enrichedResponse;
};
function formatHits(hits: Hit[], aliases: Aliases) {
function formatHits(hits: Hit[], aliases: Aliases): Index[] {
return hits.map((hit: Hit) => {
return {
health: hit.health,
@ -59,7 +46,7 @@ function formatHits(hits: Hit[], aliases: Aliases) {
});
}
async function fetchIndicesCall(callWithRequest: any, indexNames?: string[]) {
async function fetchIndicesCall(callAsCurrentUser: CallAsCurrentUser, indexNames?: string[]) {
const params: Params = {
format: 'json',
h: 'health,status,index,uuid,pri,rep,docs.count,sth,store.size',
@ -69,13 +56,17 @@ async function fetchIndicesCall(callWithRequest: any, indexNames?: string[]) {
params.index = indexNames;
}
return await callWithRequest('cat.indices', params);
return await callAsCurrentUser('cat.indices', params);
}
export const fetchIndices = async (callWithRequest: any, indexNames?: string[]) => {
const aliases = await fetchAliases(callWithRequest);
const hits = await fetchIndicesCall(callWithRequest, indexNames);
let response = formatHits(hits, aliases);
response = await enrichResponse(response, callWithRequest);
return response;
export const fetchIndices = async (
callAsCurrentUser: CallAsCurrentUser,
indexDataEnricher: IndexDataEnricher,
indexNames?: string[]
) => {
const aliases = await fetchAliases(callAsCurrentUser);
const hits = await fetchIndicesCall(callAsCurrentUser, indexNames);
const indices = formatHits(hits, aliases);
return await indexDataEnricher.enrichIndices(indices, callAsCurrentUser);
};

View file

@ -7,10 +7,10 @@
// Cloud has its own system for managing templates and we want to make
// this clear in the UI when a template is used in a Cloud deployment.
export const getManagedTemplatePrefix = async (
callWithInternalUser: any
callAsCurrentUser: any
): Promise<string | undefined> => {
try {
const { persistent, transient, defaults } = await callWithInternalUser('cluster.getSettings', {
const { persistent, transient, defaults } = await callAsCurrentUser('cluster.getSettings', {
filterPath: '*.*managed_index_templates',
flatSettings: true,
includeDefaults: true,

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as legacyElasticsearch from 'elasticsearch';
const esErrorsParent = legacyElasticsearch.errors._Abstract;
export function isEsError(err: Error) {
return err instanceof esErrorsParent;
}

View file

@ -3,48 +3,67 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { CoreSetup } from 'src/core/server';
import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch';
import { Router } from '../../../server/lib/create_router';
import { addIndexManagementDataEnricher } from './index_management_data';
import { registerIndicesRoutes } from './routes/api/indices';
import { registerTemplateRoutes } from './routes/api/templates';
import { registerMappingRoute } from './routes/api/mapping';
import { registerSettingsRoutes } from './routes/api/settings';
import { registerStatsRoute } from './routes/api/stats';
import { i18n } from '@kbn/i18n';
import { CoreSetup, Plugin, Logger, PluginInitializerContext } from 'src/core/server';
export interface LegacySetup {
router: Router;
plugins: {
elasticsearch: ElasticsearchPlugin;
license: {
registerLicenseChecker: () => void;
};
import { PLUGIN } from '../common';
import { Dependencies } from './types';
import { ApiRoutes } from './routes';
import { License, IndexDataEnricher } from './services';
import { isEsError } from './lib/is_es_error';
export interface IndexMgmtSetup {
indexDataEnricher: {
add: IndexDataEnricher['add'];
};
}
export interface IndexMgmtSetup {
addIndexManagementDataEnricher: (enricher: any) => void;
}
export class IndexMgmtServerPlugin implements Plugin<IndexMgmtSetup, void, any, any> {
private readonly apiRoutes: ApiRoutes;
private readonly license: License;
private readonly logger: Logger;
private readonly indexDataEnricher: IndexDataEnricher;
export class IndexMgmtPlugin {
public setup(core: CoreSetup, plugins: {}, __LEGACY: LegacySetup): IndexMgmtSetup {
const serverFacade = {
plugins: {
elasticsearch: __LEGACY.plugins.elasticsearch,
constructor({ logger }: PluginInitializerContext) {
this.logger = logger.get();
this.apiRoutes = new ApiRoutes();
this.license = new License();
this.indexDataEnricher = new IndexDataEnricher();
}
setup({ http }: CoreSetup, { licensing }: Dependencies): IndexMgmtSetup {
const router = http.createRouter();
this.license.setup(
{
pluginId: PLUGIN.id,
minimumLicenseType: PLUGIN.minimumLicenseType,
defaultErrorMessage: i18n.translate('xpack.idxMgmt.licenseCheckErrorMessage', {
defaultMessage: 'License check failed',
}),
},
};
{
licensing,
logger: this.logger,
}
);
__LEGACY.plugins.license.registerLicenseChecker();
registerIndicesRoutes(__LEGACY.router);
registerTemplateRoutes(__LEGACY.router, serverFacade);
registerSettingsRoutes(__LEGACY.router);
registerStatsRoute(__LEGACY.router);
registerMappingRoute(__LEGACY.router);
this.apiRoutes.setup({
router,
license: this.license,
indexDataEnricher: this.indexDataEnricher,
lib: {
isEsError,
},
});
return {
addIndexManagementDataEnricher,
indexDataEnricher: {
add: this.indexDataEnricher.add.bind(this.indexDataEnricher),
},
};
}
start() {}
stop() {}
}

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { API_BASE_PATH } from '../../../common';
export const addBasePath = (uri: string): string => API_BASE_PATH + uri;

View file

@ -3,26 +3,41 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Router, RouterRouteHandler } from '../../../../../../server/lib/create_router';
import { schema } from '@kbn/config-schema';
interface ReqPayload {
indices: string[];
}
const handler: RouterRouteHandler = async (request, callWithRequest, h) => {
const payload = request.payload as ReqPayload;
const { indices = [] } = payload;
const params = {
expandWildcards: 'none',
format: 'json',
index: indices,
};
await callWithRequest('indices.clearCache', params);
return h.response();
};
export function registerClearCacheRoute(router: Router) {
router.post('indices/clear_cache', handler);
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
const bodySchema = schema.object({
indices: schema.arrayOf(schema.string()),
});
export function registerClearCacheRoute({ router, license, lib }: RouteDependencies) {
router.post(
{ path: addBasePath('/indices/clear_cache'), validate: { body: bodySchema } },
license.guardApiRoute(async (ctx, req, res) => {
const payload = req.body as typeof bodySchema.type;
const { indices = [] } = payload;
const params = {
expandWildcards: 'none',
format: 'json',
index: indices,
};
try {
await ctx.core.elasticsearch.dataClient.callAsCurrentUser('indices.clearCache', params);
return res.ok();
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
return res.internalError({ body: e });
}
})
);
}

View file

@ -3,26 +3,41 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Router, RouterRouteHandler } from '../../../../../../server/lib/create_router';
import { schema } from '@kbn/config-schema';
interface ReqPayload {
indices: string[];
}
const handler: RouterRouteHandler = async (request, callWithRequest, h) => {
const payload = request.payload as ReqPayload;
const { indices = [] } = payload;
const params = {
expandWildcards: 'none',
format: 'json',
index: indices,
};
await callWithRequest('indices.close', params);
return h.response();
};
export function registerCloseRoute(router: Router) {
router.post('indices/close', handler);
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
const bodySchema = schema.object({
indices: schema.arrayOf(schema.string()),
});
export function registerCloseRoute({ router, license, lib }: RouteDependencies) {
router.post(
{ path: addBasePath('/indices/close'), validate: { body: bodySchema } },
license.guardApiRoute(async (ctx, req, res) => {
const payload = req.body as typeof bodySchema.type;
const { indices = [] } = payload;
const params = {
expandWildcards: 'none',
format: 'json',
index: indices,
};
try {
await ctx.core.elasticsearch.dataClient.callAsCurrentUser('indices.close', params);
return res.ok();
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
return res.internalError({ body: e });
}
})
);
}

View file

@ -4,25 +4,41 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Router, RouterRouteHandler } from '../../../../../../server/lib/create_router';
import { schema } from '@kbn/config-schema';
interface ReqPayload {
indices: string[];
}
const handler: RouterRouteHandler = async (request, callWithRequest, h) => {
const payload = request.payload as ReqPayload;
const { indices = [] } = payload;
const params = {
expandWildcards: 'none',
format: 'json',
index: indices,
};
await callWithRequest('indices.delete', params);
return h.response();
};
export function registerDeleteRoute(router: Router) {
router.post('indices/delete', handler);
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
const bodySchema = schema.object({
indices: schema.arrayOf(schema.string()),
});
export function registerDeleteRoute({ router, license, lib }: RouteDependencies) {
router.post(
{ path: addBasePath('/indices/delete'), validate: { body: bodySchema } },
license.guardApiRoute(async (ctx, req, res) => {
const body = req.body as typeof bodySchema.type;
const { indices = [] } = body;
const params = {
expandWildcards: 'none',
format: 'json',
index: indices,
};
try {
await ctx.core.elasticsearch.dataClient.callAsCurrentUser('indices.delete', params);
return res.ok();
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
return res.internalError({ body: e });
}
})
);
}

View file

@ -4,26 +4,41 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Router, RouterRouteHandler } from '../../../../../../server/lib/create_router';
import { schema } from '@kbn/config-schema';
interface ReqPayload {
indices: string[];
}
const handler: RouterRouteHandler = async (request, callWithRequest, h) => {
const payload = request.payload as ReqPayload;
const { indices = [] } = payload;
const params = {
expandWildcards: 'none',
format: 'json',
index: indices,
};
await callWithRequest('indices.flush', params);
return h.response();
};
export function registerFlushRoute(router: Router) {
router.post('indices/flush', handler);
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
const bodySchema = schema.object({
indices: schema.arrayOf(schema.string()),
});
export function registerFlushRoute({ router, license, lib }: RouteDependencies) {
router.post(
{ path: addBasePath('/indices/flush'), validate: { body: bodySchema } },
license.guardApiRoute(async (ctx, req, res) => {
const body = req.body as typeof bodySchema.type;
const { indices = [] } = body;
const params = {
expandWildcards: 'none',
format: 'json',
index: indices,
};
try {
await ctx.core.elasticsearch.dataClient.callAsCurrentUser('indices.flush', params);
return res.ok();
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
return res.internalError({ body: e });
}
})
);
}

View file

@ -4,34 +4,48 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Router, RouterRouteHandler } from '../../../../../../server/lib/create_router';
import { schema } from '@kbn/config-schema';
interface ForceMergeReqPayload {
maxNumSegments: number;
indices: string[];
}
interface Params {
expandWildcards: string;
index: ForceMergeReqPayload['indices'];
max_num_segments?: ForceMergeReqPayload['maxNumSegments'];
}
const handler: RouterRouteHandler = async (request, callWithRequest, h) => {
const { maxNumSegments, indices = [] } = request.payload as ForceMergeReqPayload;
const params: Params = {
expandWildcards: 'none',
index: indices,
};
if (maxNumSegments) {
params.max_num_segments = maxNumSegments;
}
await callWithRequest('indices.forcemerge', params);
return h.response();
};
export function registerForcemergeRoute(router: Router) {
router.post('indices/forcemerge', handler);
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
const bodySchema = schema.object({
indices: schema.arrayOf(schema.string()),
maxNumSegments: schema.maybe(schema.number()),
});
export function registerForcemergeRoute({ router, license, lib }: RouteDependencies) {
router.post(
{
path: addBasePath('/indices/forcemerge'),
validate: {
body: bodySchema,
},
},
license.guardApiRoute(async (ctx, req, res) => {
const { maxNumSegments, indices = [] } = req.body as typeof bodySchema.type;
const params = {
expandWildcards: 'none',
index: indices,
};
if (maxNumSegments) {
(params as any).max_num_segments = maxNumSegments;
}
try {
await ctx.core.elasticsearch.dataClient.callAsCurrentUser('indices.forcemerge', params);
return res.ok();
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
return res.internalError({ body: e });
}
})
);
}

View file

@ -4,25 +4,43 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Router, RouterRouteHandler } from '../../../../../../server/lib/create_router';
import { schema } from '@kbn/config-schema';
interface ReqPayload {
indices: string[];
}
const handler: RouterRouteHandler = async (request, callWithRequest, h) => {
const payload = request.payload as ReqPayload;
const { indices = [] } = payload;
const params = {
path: `/${encodeURIComponent(indices.join(','))}/_freeze`,
method: 'POST',
};
await callWithRequest('transport.request', params);
return h.response();
};
export function registerFreezeRoute(router: Router) {
router.post('indices/freeze', handler);
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
const bodySchema = schema.object({
indices: schema.arrayOf(schema.string()),
});
export function registerFreezeRoute({ router, license, lib }: RouteDependencies) {
router.post(
{ path: addBasePath('/indices/freeze'), validate: { body: bodySchema } },
license.guardApiRoute(async (ctx, req, res) => {
const body = req.body as typeof bodySchema.type;
const { indices = [] } = body;
const params = {
path: `/${encodeURIComponent(indices.join(','))}/_freeze`,
method: 'POST',
};
try {
await await ctx.core.elasticsearch.dataClient.callAsCurrentUser(
'transport.request',
params
);
return res.ok();
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
return res.internalError({ body: e });
}
})
);
}

View file

@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Router } from '../../../../../../server/lib/create_router';
import { RouteDependencies } from '../../../types';
import { registerClearCacheRoute } from './register_clear_cache_route';
import { registerCloseRoute } from './register_close_route';
@ -17,16 +17,16 @@ import { registerDeleteRoute } from './register_delete_route';
import { registerFreezeRoute } from './register_freeze_route';
import { registerUnfreezeRoute } from './register_unfreeze_route';
export function registerIndicesRoutes(router: Router) {
registerClearCacheRoute(router);
registerCloseRoute(router);
registerFlushRoute(router);
registerForcemergeRoute(router);
registerListRoute(router);
registerOpenRoute(router);
registerRefreshRoute(router);
registerReloadRoute(router);
registerDeleteRoute(router);
registerFreezeRoute(router);
registerUnfreezeRoute(router);
export function registerIndicesRoutes(dependencies: RouteDependencies) {
registerClearCacheRoute(dependencies);
registerCloseRoute(dependencies);
registerFlushRoute(dependencies);
registerForcemergeRoute(dependencies);
registerListRoute(dependencies);
registerOpenRoute(dependencies);
registerRefreshRoute(dependencies);
registerReloadRoute(dependencies);
registerDeleteRoute(dependencies);
registerFreezeRoute(dependencies);
registerUnfreezeRoute(dependencies);
}

View file

@ -3,14 +3,31 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Router, RouterRouteHandler } from '../../../../../../server/lib/create_router';
import { fetchIndices } from '../../../lib/fetch_indices';
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
const handler: RouterRouteHandler = async (request, callWithRequest) => {
return fetchIndices(callWithRequest);
};
export function registerListRoute(router: Router) {
router.get('indices', handler);
export function registerListRoute({ router, license, indexDataEnricher, lib }: RouteDependencies) {
router.get(
{ path: addBasePath('/indices'), validate: false },
license.guardApiRoute(async (ctx, req, res) => {
try {
const indices = await fetchIndices(
ctx.core.elasticsearch.dataClient.callAsCurrentUser,
indexDataEnricher
);
return res.ok({ body: indices });
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
return res.internalError({ body: e });
}
})
);
}

View file

@ -3,25 +3,41 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Router, RouterRouteHandler } from '../../../../../../server/lib/create_router';
import { schema } from '@kbn/config-schema';
interface ReqPayload {
indices: string[];
}
const handler: RouterRouteHandler = async (request, callWithRequest, h) => {
const payload = request.payload as ReqPayload;
const { indices = [] } = payload;
const params = {
expandWildcards: 'none',
format: 'json',
index: indices,
};
await callWithRequest('indices.open', params);
return h.response();
};
export function registerOpenRoute(router: Router) {
router.post('indices/open', handler);
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
const bodySchema = schema.object({
indices: schema.arrayOf(schema.string()),
});
export function registerOpenRoute({ router, license, lib }: RouteDependencies) {
router.post(
{ path: addBasePath('/indices/open'), validate: { body: bodySchema } },
license.guardApiRoute(async (ctx, req, res) => {
const body = req.body as typeof bodySchema.type;
const { indices = [] } = body;
const params = {
expandWildcards: 'none',
format: 'json',
index: indices,
};
try {
await await ctx.core.elasticsearch.dataClient.callAsCurrentUser('indices.open', params);
return res.ok();
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
return res.internalError({ body: e });
}
})
);
}

View file

@ -4,25 +4,41 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Router, RouterRouteHandler } from '../../../../../../server/lib/create_router';
import { schema } from '@kbn/config-schema';
interface ReqPayload {
indices: string[];
}
const handler: RouterRouteHandler = async (request, callWithRequest, h) => {
const payload = request.payload as ReqPayload;
const { indices = [] } = payload;
const params = {
expandWildcards: 'none',
format: 'json',
index: indices,
};
await callWithRequest('indices.refresh', params);
return h.response();
};
export function registerRefreshRoute(router: Router) {
router.post('indices/refresh', handler);
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
const bodySchema = schema.object({
indices: schema.arrayOf(schema.string()),
});
export function registerRefreshRoute({ router, license, lib }: RouteDependencies) {
router.post(
{ path: addBasePath('/indices/refresh'), validate: { body: bodySchema } },
license.guardApiRoute(async (ctx, req, res) => {
const body = req.body as typeof bodySchema.type;
const { indices = [] } = body;
const params = {
expandWildcards: 'none',
format: 'json',
index: indices,
};
try {
await ctx.core.elasticsearch.dataClient.callAsCurrentUser('indices.refresh', params);
return res.ok();
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
return res.internalError({ body: e });
}
})
);
}

View file

@ -3,19 +3,46 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Router, RouterRouteHandler } from '../../../../../../server/lib/create_router';
import { schema } from '@kbn/config-schema';
import { RouteDependencies } from '../../../types';
import { fetchIndices } from '../../../lib/fetch_indices';
import { addBasePath } from '../index';
interface ReqPayload {
indexNames: string[];
}
const handler: RouterRouteHandler = async (request, callWithRequest) => {
const { indexNames = [] } = request.payload as ReqPayload;
return fetchIndices(callWithRequest, indexNames);
};
export function registerReloadRoute(router: Router) {
router.post('indices/reload', handler);
const bodySchema = schema.maybe(
schema.object({
indexNames: schema.maybe(schema.arrayOf(schema.string())),
})
);
export function registerReloadRoute({
router,
license,
indexDataEnricher,
lib,
}: RouteDependencies) {
router.post(
{ path: addBasePath('/indices/reload'), validate: { body: bodySchema } },
license.guardApiRoute(async (ctx, req, res) => {
const { indexNames = [] } = (req.body as typeof bodySchema.type) ?? {};
try {
const indices = await fetchIndices(
ctx.core.elasticsearch.dataClient.callAsCurrentUser,
indexDataEnricher,
indexNames
);
return res.ok({ body: indices });
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
return res.internalError({ body: e });
}
})
);
}

View file

@ -4,23 +4,38 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Router, RouterRouteHandler } from '../../../../../../server/lib/create_router';
import { schema } from '@kbn/config-schema';
interface ReqPayload {
indices: string[];
}
const handler: RouterRouteHandler = async (request, callWithRequest, h) => {
const { indices = [] } = request.payload as ReqPayload;
const params = {
path: `/${encodeURIComponent(indices.join(','))}/_unfreeze`,
method: 'POST',
};
await callWithRequest('transport.request', params);
return h.response();
};
export function registerUnfreezeRoute(router: Router) {
router.post('indices/unfreeze', handler);
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
const bodySchema = schema.object({
indices: schema.arrayOf(schema.string()),
});
export function registerUnfreezeRoute({ router, license, lib }: RouteDependencies) {
router.post(
{ path: addBasePath('/indices/unfreeze'), validate: { body: bodySchema } },
license.guardApiRoute(async (ctx, req, res) => {
const { indices = [] } = req.body as typeof bodySchema.type;
const params = {
path: `/${encodeURIComponent(indices.join(','))}/_unfreeze`,
method: 'POST',
};
try {
await ctx.core.elasticsearch.dataClient.callAsCurrentUser('transport.request', params);
return res.ok();
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
return res.internalError({ body: e });
}
})
);
}

View file

@ -3,7 +3,14 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Router, RouterRouteHandler } from '../../../../../../server/lib/create_router';
import { schema } from '@kbn/config-schema';
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
const paramsSchema = schema.object({
indexName: schema.string(),
});
function formatHit(hit: { [key: string]: { mappings: any } }, indexName: string) {
const mapping = hit[indexName].mappings;
@ -12,18 +19,33 @@ function formatHit(hit: { [key: string]: { mappings: any } }, indexName: string)
};
}
const handler: RouterRouteHandler = async (request, callWithRequest) => {
const { indexName } = request.params;
const params = {
expand_wildcards: 'none',
index: indexName,
};
export function registerMappingRoute({ router, license, lib }: RouteDependencies) {
router.get(
{ path: addBasePath('/mapping/{indexName}'), validate: { params: paramsSchema } },
license.guardApiRoute(async (ctx, req, res) => {
const { indexName } = req.params as typeof paramsSchema.type;
const params = {
expand_wildcards: 'none',
index: indexName,
};
const hit = await callWithRequest('indices.getMapping', params);
const response = formatHit(hit, indexName);
return response;
};
export function registerMappingRoute(router: Router) {
router.get('mapping/{indexName}', handler);
try {
const hit = await ctx.core.elasticsearch.dataClient.callAsCurrentUser(
'indices.getMapping',
params
);
const response = formatHit(hit, indexName);
return res.ok({ body: response });
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
return res.internalError({ body: e });
}
})
);
}

View file

@ -3,7 +3,14 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Router, RouterRouteHandler } from '../../../../../../server/lib/create_router';
import { schema } from '@kbn/config-schema';
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
const paramsSchema = schema.object({
indexName: schema.string(),
});
// response comes back as { [indexName]: { ... }}
// so plucking out the embedded object
@ -12,19 +19,35 @@ function formatHit(hit: { [key: string]: {} }) {
return hit[key];
}
const handler: RouterRouteHandler = async (request, callWithRequest) => {
const { indexName } = request.params;
const params = {
expandWildcards: 'none',
flatSettings: false,
local: false,
includeDefaults: true,
index: indexName,
};
export function registerLoadRoute({ router, license, lib }: RouteDependencies) {
router.get(
{ path: addBasePath('/settings/{indexName}'), validate: { params: paramsSchema } },
license.guardApiRoute(async (ctx, req, res) => {
const { indexName } = req.params as typeof paramsSchema.type;
const params = {
expandWildcards: 'none',
flatSettings: false,
local: false,
includeDefaults: true,
index: indexName,
};
const hit = await callWithRequest('indices.getSettings', params);
return formatHit(hit);
};
export function registerLoadRoute(router: Router) {
router.get('settings/{indexName}', handler);
try {
const hit = await ctx.core.elasticsearch.dataClient.callAsCurrentUser(
'indices.getSettings',
params
);
return res.ok({ body: formatHit(hit) });
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
return res.internalError({ body: e });
}
})
);
}

View file

@ -3,12 +3,12 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Router } from '../../../../../../server/lib/create_router';
import { RouteDependencies } from '../../../types';
import { registerLoadRoute } from './register_load_route';
import { registerUpdateRoute } from './register_update_route';
export function registerSettingsRoutes(router: Router) {
registerLoadRoute(router);
registerUpdateRoute(router);
export function registerSettingsRoutes(dependencies: RouteDependencies) {
registerLoadRoute(dependencies);
registerUpdateRoute(dependencies);
}

View file

@ -3,20 +3,49 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Router, RouterRouteHandler } from '../../../../../../server/lib/create_router';
import { schema } from '@kbn/config-schema';
const handler: RouterRouteHandler = async (request, callWithRequest) => {
const { indexName } = request.params;
const params = {
ignoreUnavailable: true,
allowNoIndices: false,
expandWildcards: 'none',
index: indexName,
body: request.payload,
};
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
return await callWithRequest('indices.putSettings', params);
};
export function registerUpdateRoute(router: Router) {
router.put('settings/{indexName}', handler);
const bodySchema = schema.any();
const paramsSchema = schema.object({
indexName: schema.string(),
});
export function registerUpdateRoute({ router, license, lib }: RouteDependencies) {
router.put(
{
path: addBasePath('/settings/{indexName}'),
validate: { body: bodySchema, params: paramsSchema },
},
license.guardApiRoute(async (ctx, req, res) => {
const { indexName } = req.params as typeof paramsSchema.type;
const params = {
ignoreUnavailable: true,
allowNoIndices: false,
expandWildcards: 'none',
index: indexName,
body: req.body,
};
try {
const response = await ctx.core.elasticsearch.dataClient.callAsCurrentUser(
'indices.putSettings',
params
);
return res.ok({ body: response });
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
return res.internalError({ body: e });
}
})
);
}

View file

@ -3,7 +3,14 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Router, RouterRouteHandler } from '../../../../../../server/lib/create_router';
import { schema } from '@kbn/config-schema';
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
const paramsSchema = schema.object({
indexName: schema.string(),
});
function formatHit(hit: { _shards: any; indices: { [key: string]: any } }, indexName: string) {
const { _shards, indices } = hit;
@ -14,17 +21,32 @@ function formatHit(hit: { _shards: any; indices: { [key: string]: any } }, index
};
}
const handler: RouterRouteHandler = async (request, callWithRequest) => {
const { indexName } = request.params;
const params = {
expand_wildcards: 'none',
index: indexName,
};
const hit = await callWithRequest('indices.stats', params);
const response = formatHit(hit, indexName);
export function registerStatsRoute({ router, license, lib }: RouteDependencies) {
router.get(
{ path: addBasePath('/stats/{indexName}'), validate: { params: paramsSchema } },
license.guardApiRoute(async (ctx, req, res) => {
const { indexName } = req.params as typeof paramsSchema.type;
const params = {
expand_wildcards: 'none',
index: indexName,
};
return response;
};
export function registerStatsRoute(router: Router) {
router.get('stats/{indexName}', handler);
try {
const hit = await ctx.core.elasticsearch.dataClient.callAsCurrentUser(
'indices.stats',
params
);
return res.ok({ body: formatHit(hit, indexName) });
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
return res.internalError({ body: e });
}
})
);
}

View file

@ -5,60 +5,74 @@
*/
import { i18n } from '@kbn/i18n';
import {
Router,
RouterRouteHandler,
wrapCustomError,
} from '../../../../../../server/lib/create_router';
import { Template, TemplateEs } from '../../../../common/types';
import { serializeTemplate } from '../../../../common/lib';
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
import { templateSchema } from './validate_schemas';
const handler: RouterRouteHandler = async (req, callWithRequest) => {
const template = req.payload as Template;
const serializedTemplate = serializeTemplate(template) as TemplateEs;
const bodySchema = templateSchema;
const { name, order, index_patterns, version, settings, mappings, aliases } = serializedTemplate;
export function registerCreateRoute({ router, license, lib }: RouteDependencies) {
router.put(
{ path: addBasePath('/templates'), validate: { body: bodySchema } },
license.guardApiRoute(async (ctx, req, res) => {
const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient;
const template = req.body as Template;
const serializedTemplate = serializeTemplate(template) as TemplateEs;
const conflictError = wrapCustomError(
new Error(
i18n.translate('xpack.idxMgmt.createRoute.duplicateTemplateIdErrorMessage', {
defaultMessage: "There is already a template with name '{name}'.",
values: {
const {
name,
order,
index_patterns,
version,
settings,
mappings,
aliases,
} = serializedTemplate;
// Check that template with the same name doesn't already exist
const templateExists = await callAsCurrentUser('indices.existsTemplate', { name });
if (templateExists) {
return res.conflict({
body: new Error(
i18n.translate('xpack.idxMgmt.createRoute.duplicateTemplateIdErrorMessage', {
defaultMessage: "There is already a template with name '{name}'.",
values: {
name,
},
})
),
});
}
try {
// Otherwise create new index template
const response = await callAsCurrentUser('indices.putTemplate', {
name,
},
})
),
409
order,
body: {
index_patterns,
version,
settings,
mappings,
aliases,
},
});
return res.ok({ body: response });
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
return res.internalError({ body: e });
}
})
);
// Check that template with the same name doesn't already exist
try {
const templateExists = await callWithRequest('indices.existsTemplate', { name });
if (templateExists) {
throw conflictError;
}
} catch (e) {
// Rethrow conflict error but silently swallow all others
if (e === conflictError) {
throw e;
}
}
// Otherwise create new index template
return await callWithRequest('indices.putTemplate', {
name,
order,
body: {
index_patterns,
version,
settings,
mappings,
aliases,
},
});
};
export function registerCreateRoute(router: Router) {
router.put('templates', handler);
}

View file

@ -4,38 +4,46 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
Router,
RouterRouteHandler,
wrapEsError,
} from '../../../../../../server/lib/create_router';
import { schema } from '@kbn/config-schema';
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
import { wrapEsError } from '../../helpers';
import { Template } from '../../../../common/types';
const handler: RouterRouteHandler = async (req, callWithRequest) => {
const { names } = req.params;
const templateNames = names.split(',');
const response: { templatesDeleted: Array<Template['name']>; errors: any[] } = {
templatesDeleted: [],
errors: [],
};
const paramsSchema = schema.object({
names: schema.string(),
});
await Promise.all(
templateNames.map(async name => {
try {
await callWithRequest('indices.deleteTemplate', { name });
return response.templatesDeleted.push(name);
} catch (e) {
return response.errors.push({
name,
error: wrapEsError(e),
});
}
export function registerDeleteRoute({ router, license }: RouteDependencies) {
router.delete(
{ path: addBasePath('/templates/{names}'), validate: { params: paramsSchema } },
license.guardApiRoute(async (ctx, req, res) => {
const { names } = req.params as typeof paramsSchema.type;
const templateNames = names.split(',');
const response: { templatesDeleted: Array<Template['name']>; errors: any[] } = {
templatesDeleted: [],
errors: [],
};
await Promise.all(
templateNames.map(async name => {
try {
await ctx.core.elasticsearch.dataClient.callAsCurrentUser('indices.deleteTemplate', {
name,
});
return response.templatesDeleted.push(name);
} catch (e) {
return response.errors.push({
name,
error: wrapEsError(e),
});
}
})
);
return res.ok({ body: response });
})
);
return response;
};
export function registerDeleteRoute(router: Router) {
router.delete('templates/{names}', handler);
}

View file

@ -3,37 +3,62 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { schema } from '@kbn/config-schema';
import { deserializeTemplate, deserializeTemplateList } from '../../../../common/lib';
import { Router, RouterRouteHandler } from '../../../../../../server/lib/create_router';
import { getManagedTemplatePrefix } from '../../../lib/get_managed_templates';
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
let callWithInternalUser: any;
export function registerGetAllRoute({ router, license }: RouteDependencies) {
router.get(
{ path: addBasePath('/templates'), validate: false },
license.guardApiRoute(async (ctx, req, res) => {
const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient;
const managedTemplatePrefix = await getManagedTemplatePrefix(callAsCurrentUser);
const allHandler: RouterRouteHandler = async (_req, callWithRequest) => {
const managedTemplatePrefix = await getManagedTemplatePrefix(callWithInternalUser);
const indexTemplatesByName = await callAsCurrentUser('indices.getTemplate');
const indexTemplatesByName = await callWithRequest('indices.getTemplate');
return deserializeTemplateList(indexTemplatesByName, managedTemplatePrefix);
};
const oneHandler: RouterRouteHandler = async (req, callWithRequest) => {
const { name } = req.params;
const managedTemplatePrefix = await getManagedTemplatePrefix(callWithInternalUser);
const indexTemplateByName = await callWithRequest('indices.getTemplate', { name });
if (indexTemplateByName[name]) {
return deserializeTemplate({ ...indexTemplateByName[name], name }, managedTemplatePrefix);
}
};
export function registerGetAllRoute(router: Router, server: any) {
callWithInternalUser = server.plugins.elasticsearch.getCluster('data').callWithInternalUser;
router.get('templates', allHandler);
return res.ok({ body: deserializeTemplateList(indexTemplatesByName, managedTemplatePrefix) });
})
);
}
export function registerGetOneRoute(router: Router, server: any) {
callWithInternalUser = server.plugins.elasticsearch.getCluster('data').callWithInternalUser;
router.get('templates/{name}', oneHandler);
const paramsSchema = schema.object({
name: schema.string(),
});
export function registerGetOneRoute({ router, license, lib }: RouteDependencies) {
router.get(
{ path: addBasePath('/templates/{name}'), validate: { params: paramsSchema } },
license.guardApiRoute(async (ctx, req, res) => {
const { name } = req.params as typeof paramsSchema.type;
const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient;
try {
const managedTemplatePrefix = await getManagedTemplatePrefix(callAsCurrentUser);
const indexTemplateByName = await callAsCurrentUser('indices.getTemplate', { name });
if (indexTemplateByName[name]) {
return res.ok({
body: deserializeTemplate(
{ ...indexTemplateByName[name], name },
managedTemplatePrefix
),
});
}
return res.notFound();
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
return res.internalError({ body: e });
}
})
);
}

View file

@ -4,16 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Router } from '../../../../../../server/lib/create_router';
import { RouteDependencies } from '../../../types';
import { registerGetAllRoute, registerGetOneRoute } from './register_get_routes';
import { registerDeleteRoute } from './register_delete_route';
import { registerCreateRoute } from './register_create_route';
import { registerUpdateRoute } from './register_update_route';
export function registerTemplateRoutes(router: Router, server: any) {
registerGetAllRoute(router, server);
registerGetOneRoute(router, server);
registerDeleteRoute(router);
registerCreateRoute(router);
registerUpdateRoute(router);
export function registerTemplateRoutes(dependencies: RouteDependencies) {
registerGetAllRoute(dependencies);
registerGetOneRoute(dependencies);
registerDeleteRoute(dependencies);
registerCreateRoute(dependencies);
registerUpdateRoute(dependencies);
}

View file

@ -3,35 +3,65 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { schema } from '@kbn/config-schema';
import { Router, RouterRouteHandler } from '../../../../../../server/lib/create_router';
import { Template, TemplateEs } from '../../../../common/types';
import { serializeTemplate } from '../../../../common/lib';
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
import { templateSchema } from './validate_schemas';
const handler: RouterRouteHandler = async (req, callWithRequest) => {
const { name } = req.params;
const template = req.payload as Template;
const serializedTemplate = serializeTemplate(template) as TemplateEs;
const bodySchema = templateSchema;
const paramsSchema = schema.object({
name: schema.string(),
});
const { order, index_patterns, version, settings, mappings, aliases } = serializedTemplate;
// Verify the template exists (ES will throw 404 if not)
await callWithRequest('indices.existsTemplate', { name });
// Next, update index template
return await callWithRequest('indices.putTemplate', {
name,
order,
body: {
index_patterns,
version,
settings,
mappings,
aliases,
export function registerUpdateRoute({ router, license, lib }: RouteDependencies) {
router.put(
{
path: addBasePath('/templates/{name}'),
validate: { body: bodySchema, params: paramsSchema },
},
});
};
license.guardApiRoute(async (ctx, req, res) => {
const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient;
const { name } = req.params as typeof paramsSchema.type;
const template = req.body as Template;
const serializedTemplate = serializeTemplate(template) as TemplateEs;
export function registerUpdateRoute(router: Router) {
router.put('templates/{name}', handler);
const { order, index_patterns, version, settings, mappings, aliases } = serializedTemplate;
// Verify the template exists (ES will throw 404 if not)
const doesExist = await callAsCurrentUser('indices.existsTemplate', { name });
if (!doesExist) {
return res.notFound();
}
try {
// Next, update index template
const response = await callAsCurrentUser('indices.putTemplate', {
name,
order,
body: {
index_patterns,
version,
settings,
mappings,
aliases,
},
});
return res.ok({ body: response });
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
return res.internalError({ body: e });
}
})
);
}

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { schema } from '@kbn/config-schema';
export const templateSchema = schema.object({
name: schema.string(),
indexPatterns: schema.arrayOf(schema.string()),
version: schema.maybe(schema.number()),
order: schema.maybe(schema.number()),
settings: schema.maybe(schema.object({}, { allowUnknowns: true })),
aliases: schema.maybe(schema.object({}, { allowUnknowns: true })),
mappings: schema.maybe(schema.object({}, { allowUnknowns: true })),
ilmPolicy: schema.maybe(
schema.object({
name: schema.maybe(schema.string()),
rollover_alias: schema.maybe(schema.string()),
})
),
isManaged: schema.maybe(schema.boolean()),
});

View file

@ -0,0 +1,58 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
const extractCausedByChain = (causedBy: any = {}, accumulator: any[] = []): any => {
const { reason, caused_by } = causedBy; // eslint-disable-line @typescript-eslint/camelcase
if (reason) {
accumulator.push(reason);
}
// eslint-disable-next-line @typescript-eslint/camelcase
if (caused_by) {
return extractCausedByChain(caused_by, accumulator);
}
return accumulator;
};
/**
* Wraps an error thrown by the ES JS client into a Boom error response and returns it
*
* @param err Object Error thrown by ES JS client
* @param statusCodeToMessageMap Object Optional map of HTTP status codes => error messages
* @return Object Boom error response
*/
export const wrapEsError = (err: any, statusCodeToMessageMap: any = {}) => {
const { statusCode, response } = err;
const {
error: {
root_cause = [], // eslint-disable-line @typescript-eslint/camelcase
caused_by = {}, // eslint-disable-line @typescript-eslint/camelcase
} = {},
} = JSON.parse(response);
// If no custom message if specified for the error's status code, just
// wrap the error as a Boom error response, include the additional information from ES, and return it
if (!statusCodeToMessageMap[statusCode]) {
// const boomError = Boom.boomify(err, { statusCode });
const error: any = { statusCode };
// The caused_by chain has the most information so use that if it's available. If not then
// settle for the root_cause.
const causedByChain = extractCausedByChain(caused_by);
const defaultCause = root_cause.length ? extractCausedByChain(root_cause[0]) : undefined;
error.cause = causedByChain.length ? causedByChain : defaultCause;
return error;
}
// Otherwise, use the custom message to create a Boom error response and
// return it
const message = statusCodeToMessageMap[statusCode];
return { message, statusCode };
};

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import { RouteDependencies } from '../types';
import { registerIndicesRoutes } from './api/indices';
import { registerTemplateRoutes } from './api/templates';
import { registerMappingRoute } from './api/mapping';
import { registerSettingsRoutes } from './api/settings';
import { registerStatsRoute } from './api/stats';
export class ApiRoutes {
setup(dependencies: RouteDependencies) {
registerIndicesRoutes(dependencies);
registerTemplateRoutes(dependencies);
registerSettingsRoutes(dependencies);
registerStatsRoute(dependencies);
registerMappingRoute(dependencies);
}
start() {}
stop() {}
}

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { License } from './license';
export { IndexDataEnricher, Enricher } from './index_data_enricher';

View file

@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Index, CallAsCurrentUser } from '../types';
export type Enricher = (indices: Index[], callAsCurrentUser: CallAsCurrentUser) => Promise<Index[]>;
export class IndexDataEnricher {
private readonly _enrichers: Enricher[] = [];
public add(enricher: Enricher) {
this._enrichers.push(enricher);
}
public enrichIndices = async (
indices: Index[],
callAsCurrentUser: CallAsCurrentUser
): Promise<Index[]> => {
let enrichedIndices = indices;
for (let i = 0; i < this.enrichers.length; i++) {
const dataEnricher = this.enrichers[i];
try {
const dataEnricherResponse = await dataEnricher(enrichedIndices, callAsCurrentUser);
enrichedIndices = dataEnricherResponse;
} catch (e) {
// silently swallow enricher response errors
}
}
return enrichedIndices;
};
public get enrichers() {
return this._enrichers;
}
}

View file

@ -0,0 +1,83 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Logger } from 'src/core/server';
import {
KibanaRequest,
KibanaResponseFactory,
RequestHandler,
RequestHandlerContext,
} from 'kibana/server';
import { LicensingPluginSetup } from '../../../../../plugins/licensing/server';
import { LicenseType } from '../../../../../plugins/licensing/common/types';
import { LICENSE_CHECK_STATE } from '../../../../../plugins/licensing/common/types';
export interface LicenseStatus {
isValid: boolean;
message?: string;
}
interface SetupSettings {
pluginId: string;
minimumLicenseType: LicenseType;
defaultErrorMessage: string;
}
export class License {
private licenseStatus: LicenseStatus = {
isValid: false,
message: 'Invalid License',
};
setup(
{ pluginId, minimumLicenseType, defaultErrorMessage }: SetupSettings,
{ licensing, logger }: { licensing: LicensingPluginSetup; logger: Logger }
) {
licensing.license$.subscribe(license => {
const { state, message } = license.check(pluginId, minimumLicenseType);
const hasRequiredLicense = state === LICENSE_CHECK_STATE.Valid;
if (hasRequiredLicense) {
this.licenseStatus = { isValid: true };
} else {
this.licenseStatus = {
isValid: false,
message: message || defaultErrorMessage,
};
if (message) {
logger.info(message);
}
}
});
}
guardApiRoute(handler: RequestHandler) {
const license = this;
return function licenseCheck(
ctx: RequestHandlerContext,
request: KibanaRequest,
response: KibanaResponseFactory
) {
const licenseStatus = license.getStatus();
if (!licenseStatus.isValid) {
return response.customError({
body: {
message: licenseStatus.message || '',
},
statusCode: 403,
});
}
return handler(ctx, request, response);
};
}
getStatus() {
return this.licenseStatus;
}
}

View file

@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { ScopedClusterClient, IRouter } from 'src/core/server';
import { LicensingPluginSetup } from '../../../../plugins/licensing/server';
import { License, IndexDataEnricher } from './services';
import { isEsError } from './lib/is_es_error';
export interface Dependencies {
licensing: LicensingPluginSetup;
}
export interface RouteDependencies {
router: IRouter;
license: License;
indexDataEnricher: IndexDataEnricher;
lib: {
isEsError: typeof isEsError;
};
}
export interface Index {
health: string;
status: string;
name: string;
uuid: string;
primary: string;
replica: string;
documents: any;
size: any;
isFrozen: boolean;
aliases: string | string[];
[key: string]: any;
}
export type CallAsCurrentUser = ScopedClusterClient['callAsCurrentUser'];

View file

@ -104,7 +104,7 @@ export default function({ getService }) {
it('should require index or indices to be provided', async () => {
const { body } = await deleteIndex().expect(400);
expect(body.message).to.contain('index / indices is missing');
expect(body.message).to.contain('expected value of type [string]');
});
});
@ -144,7 +144,7 @@ export default function({ getService }) {
it('should allow to define the number of segments', async () => {
const index = await createIndex();
await forceMerge(index, { max_num_segments: 1 }).expect(200);
await forceMerge(index, { maxNumSegments: 1 }).expect(200);
});
});

View file

@ -92,14 +92,16 @@ export default function({ getService }) {
await createTemplate(payload).expect(409);
});
it('should handle ES errors', async () => {
it('should validate the request payload', async () => {
const templateName = `template-${getRandomString()}`;
const payload = getTemplatePayload(templateName);
delete payload.indexPatterns; // index patterns are required
const { body } = await createTemplate(payload);
expect(body.message).to.contain('index patterns are missing');
expect(body.message).to.contain(
'[request body.indexPatterns]: expected value of type [array] '
);
});
});