[Index Management] Disable legacy index templates (#163518)

This commit is contained in:
Alison Goryachev 2023-08-14 10:40:16 -04:00 committed by GitHub
parent e12ad9785c
commit 97f44c1e50
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 259 additions and 26 deletions

View file

@ -32,8 +32,11 @@ xpack.remote_clusters.enabled: false
xpack.snapshot_restore.enabled: false
xpack.license_management.enabled: false
# Disable index management actions from the UI
# Management team UI configurations
# Disable index actions from the Index Management UI
xpack.index_management.enableIndexActions: false
# Disable legacy index templates from Index Management UI
xpack.index_management.enableLegacyTemplates: false
# Keep deeplinks visible so that they are shown in the sidenav
dev_tools.deeplinks.navLinkStatus: visible

View file

@ -240,6 +240,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
'xpack.ilm.ui.enabled (boolean)',
'xpack.index_management.ui.enabled (boolean)',
'xpack.index_management.enableIndexActions (any)',
'xpack.index_management.enableLegacyTemplates (any)',
'xpack.infra.sources.default.fields.message (array)',
/**
* xpack.infra.logs is conditional and will resolve to an object of properties

View file

@ -53,7 +53,7 @@ POST %25%7B%5B%40metadata%5D%5Bbeat%5D%7D-%25%7B%5B%40metadata%5D%5Bversion%5D%7
### Quick steps for testing
By default, **legacy index templates** are not shown in the UI. Make them appear by creating one in Console:
**Legacy index templates** are only shown in the UI on stateful *and* if a user has existing legacy index templates. You can test this functionality by creating one in Console:
```
PUT _template/template_1
@ -62,6 +62,8 @@ PUT _template/template_1
}
```
On serverless, Elasticsearch does not support legacy index templates and therefore this functionality is disabled in Kibana via the config `xpack.index_management.enableLegacyTemplates`. For more details, see [#163518](https://github.com/elastic/kibana/pull/163518).
To test **Cloud-managed templates**:
1. Add `cluster.metadata.managed_index_templates` setting via Dev Tools:

View file

@ -56,6 +56,11 @@ const appDependencies = {
executionContext: executionContextServiceMock.createStartContract(),
},
plugins: {},
// Default stateful configuration
config: {
enableLegacyTemplates: true,
enableIndexActions: true,
},
} as any;
export const kibanaVersion = new SemVer(MAJOR_VERSION);
@ -82,7 +87,6 @@ export const WithAppDependencies =
(props: any) => {
httpService.setup(httpSetup);
const mergedDependencies = merge({}, appDependencies, overridingDependencies);
return (
<KibanaReactContextProvider>
<AppContextProvider value={mergedDependencies}>

View file

@ -225,11 +225,11 @@ describe('<IndexManagementHome />', () => {
]);
httpRequestsMockHelpers.setReloadIndicesResponse({ indexNames: [indexNameA, indexNameB] });
testBed = await setup(httpSetup, {
enableIndexActions: true,
await act(async () => {
testBed = await setup(httpSetup);
});
const { component, find } = testBed;
const { component, find } = testBed;
component.update();
find('indexTableIndexNameLink').at(0).simulate('click');
@ -270,8 +270,8 @@ describe('<IndexManagementHome />', () => {
});
test('should be able to open a closed index', async () => {
testBed = await setup(httpSetup, {
enableIndexActions: true,
await act(async () => {
testBed = await setup(httpSetup);
});
const { component, find, actions } = testBed;

View file

@ -168,7 +168,11 @@ describe('index table', () => {
},
plugins: {},
url: urlServiceMock,
enableIndexActions: true,
// Default stateful configuration
config: {
enableLegacyTemplates: true,
enableIndexActions: true,
},
};
component = (
@ -515,8 +519,8 @@ describe('index table', () => {
describe('Common index actions', () => {
beforeEach(() => {
// Mock initialization of services
setupMockComponent({ enableIndexActions: false });
// Mock initialization of services; set enableIndexActions=false to verify config behavior
setupMockComponent({ config: { enableIndexActions: false, enableLegacyTemplates: true } });
});
test('Common index actions should be hidden when feature is turned off', async () => {

View file

@ -44,6 +44,10 @@ export interface AppDependencies {
httpService: HttpService;
notificationService: NotificationService;
};
config: {
enableIndexActions: boolean;
enableLegacyTemplates: boolean;
};
history: ScopedHistory;
setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs'];
uiSettings: IUiSettingsClient;
@ -52,7 +56,6 @@ export interface AppDependencies {
docLinks: DocLinksStart;
kibanaVersion: SemVer;
theme$: Observable<CoreTheme>;
enableIndexActions: boolean;
}
export const AppContextProvider = ({

View file

@ -53,7 +53,8 @@ export async function mountManagementSection(
extensionsService: ExtensionsService,
isFleetEnabled: boolean,
kibanaVersion: SemVer,
enableIndexActions: boolean = true
enableIndexActions: boolean = true,
enableLegacyTemplates: boolean = true
) {
const { element, setBreadcrumbs, history, theme$ } = params;
const [core, startDependencies] = await coreSetup.getStartServices();
@ -95,7 +96,10 @@ export async function mountManagementSection(
uiMetricService,
extensionsService,
},
enableIndexActions,
config: {
enableIndexActions,
enableLegacyTemplates,
},
history,
setBreadcrumbs,
uiSettings,

View file

@ -49,7 +49,9 @@ export class IndexActionsContextMenu extends Component {
this.setState({ isActionConfirmed });
};
panels({ services: { extensionsService }, core: { getUrlForApp } }) {
const { enableIndexActions } = this.context;
const {
config: { enableIndexActions },
} = this.context;
const {
closeIndices,

View file

@ -19,6 +19,7 @@ import { getTemplateDetailsLink } from '../../services/routing';
import { saveTemplate, useLoadIndexTemplate } from '../../services/api';
import { getIsLegacyFromQueryParams } from '../../lib/index_templates';
import { attemptToURIDecode } from '../../../shared_imports';
import { useAppContext } from '../../app_context';
interface MatchParams {
name: string;
@ -32,7 +33,11 @@ export const TemplateClone: React.FunctionComponent<RouteComponentProps<MatchPar
history,
}) => {
const decodedTemplateName = attemptToURIDecode(name)!;
const isLegacy = getIsLegacyFromQueryParams(location);
const {
config: { enableLegacyTemplates },
} = useAppContext();
// We don't expect the `legacy` query to be used when legacy templates are disabled, however, we add the `enableLegacyTemplates` check as a safeguard
const isLegacy = enableLegacyTemplates && getIsLegacyFromQueryParams(location);
const [isSaving, setIsSaving] = useState<boolean>(false);
const [saveError, setSaveError] = useState<any>(null);

View file

@ -18,12 +18,17 @@ import { TemplateForm } from '../../components';
import { breadcrumbService } from '../../services/breadcrumbs';
import { saveTemplate } from '../../services/api';
import { getTemplateDetailsLink } from '../../services/routing';
import { useAppContext } from '../../app_context';
export const TemplateCreate: React.FunctionComponent<RouteComponentProps> = ({ history }) => {
const [isSaving, setIsSaving] = useState<boolean>(false);
const [saveError, setSaveError] = useState<any>(null);
const {
config: { enableLegacyTemplates },
} = useAppContext();
const search = parse(useLocation().search.substring(1));
const isLegacy = Boolean(search.legacy);
// We don't expect the `legacy` query to be used when legacy templates are disabled, however, we add the `enableLegacyTemplates` check as a safeguard
const isLegacy = enableLegacyTemplates && Boolean(search.legacy);
const onSave = async (template: TemplateDeserialized) => {
const { name } = template;

View file

@ -23,6 +23,7 @@ import { useLoadIndexTemplate, updateTemplate } from '../../services/api';
import { getTemplateDetailsLink } from '../../services/routing';
import { TemplateForm } from '../../components';
import { getIsLegacyFromQueryParams } from '../../lib/index_templates';
import { useAppContext } from '../../app_context';
interface MatchParams {
name: string;
@ -36,7 +37,12 @@ export const TemplateEdit: React.FunctionComponent<RouteComponentProps<MatchPara
history,
}) => {
const decodedTemplateName = attemptToURIDecode(name)!;
const isLegacy = getIsLegacyFromQueryParams(location);
const {
config: { enableLegacyTemplates },
} = useAppContext();
// We don't expect the `legacy` query to be used when legacy templates are disabled, however, we add the enableLegacyTemplates check as a safeguard
const isLegacy = enableLegacyTemplates && getIsLegacyFromQueryParams(location);
const [isSaving, setIsSaving] = useState<boolean>(false);
const [saveError, setSaveError] = useState<any>(null);

View file

@ -39,6 +39,7 @@ export class IndexMgmtUIPlugin {
const {
ui: { enabled: isIndexManagementUiEnabled },
enableIndexActions,
enableLegacyTemplates,
} = this.ctx.config.get<ClientConfigType>();
if (isIndexManagementUiEnabled) {
@ -57,7 +58,8 @@ export class IndexMgmtUIPlugin {
this.extensionsService,
Boolean(fleet),
kibanaVersion,
enableIndexActions
enableIndexActions,
enableLegacyTemplates
);
},
});

View file

@ -29,4 +29,5 @@ export interface ClientConfigType {
enabled: boolean;
};
enableIndexActions?: boolean;
enableLegacyTemplates?: boolean;
}

View file

@ -30,6 +30,14 @@ const schemaLatest = schema.object(
schema.boolean({ defaultValue: true }),
schema.never()
),
enableLegacyTemplates: schema.conditional(
schema.contextRef('serverless'),
true,
// Legacy templates functionality is disabled in serverless; refer to the serverless.yml file as the source of truth
// We take this approach in order to have a central place (serverless.yml) for serverless config across Kibana
schema.boolean({ defaultValue: true }),
schema.never()
),
},
{ defaultValue: undefined }
);
@ -38,6 +46,7 @@ const configLatest: PluginConfigDescriptor<IndexManagementConfig> = {
exposeToBrowser: {
ui: true,
enableIndexActions: true,
enableLegacyTemplates: true,
},
schema: schemaLatest,
deprecations: () => [],

View file

@ -12,6 +12,7 @@ import { Dependencies } from './types';
import { ApiRoutes } from './routes';
import { IndexDataEnricher } from './services';
import { handleEsError } from './shared_imports';
import { IndexManagementConfig } from './config';
export interface IndexManagementPluginSetup {
indexDataEnricher: {
@ -22,10 +23,12 @@ export interface IndexManagementPluginSetup {
export class IndexMgmtServerPlugin implements Plugin<IndexManagementPluginSetup, void, any, any> {
private readonly apiRoutes: ApiRoutes;
private readonly indexDataEnricher: IndexDataEnricher;
private readonly config: IndexManagementConfig;
constructor(initContext: PluginInitializerContext) {
this.apiRoutes = new ApiRoutes();
this.indexDataEnricher = new IndexDataEnricher();
this.config = initContext.config.get();
}
setup(
@ -51,6 +54,7 @@ export class IndexMgmtServerPlugin implements Plugin<IndexManagementPluginSetup,
router: http.createRouter(),
config: {
isSecurityEnabled: () => security !== undefined && security.license.isEnabled(),
isLegacyTemplatesEnabled: this.config.enableLegacyTemplates,
},
indexDataEnricher: this.indexDataEnricher,
lib: {

View file

@ -46,6 +46,7 @@ describe('GET privileges', () => {
router,
config: {
isSecurityEnabled: () => true,
isLegacyTemplatesEnabled: true,
},
indexDataEnricher: mockedIndexDataEnricher,
lib: {
@ -112,6 +113,7 @@ describe('GET privileges', () => {
router,
config: {
isSecurityEnabled: () => false,
isLegacyTemplatesEnabled: true,
},
indexDataEnricher: mockedIndexDataEnricher,
lib: {

View file

@ -17,7 +17,7 @@ import { getCloudManagedTemplatePrefix } from '../../../lib/get_managed_template
import { RouteDependencies } from '../../../types';
import { addBasePath } from '..';
export function registerGetAllRoute({ router, lib: { handleEsError } }: RouteDependencies) {
export function registerGetAllRoute({ router, config, lib: { handleEsError } }: RouteDependencies) {
router.get(
{ path: addBasePath('/index_templates'), validate: false },
async (context, request, response) => {
@ -25,17 +25,24 @@ export function registerGetAllRoute({ router, lib: { handleEsError } }: RouteDep
try {
const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(client);
const legacyTemplatesEs = await client.asCurrentUser.indices.getTemplate();
const { index_templates: templatesEs } =
await client.asCurrentUser.indices.getIndexTemplate();
// @ts-expect-error TemplateSerialized.index_patterns not compatible with IndicesIndexTemplate.index_patterns
const templates = deserializeTemplateList(templatesEs, cloudManagedTemplatePrefix);
if (config.isLegacyTemplatesEnabled === false) {
// If isLegacyTemplatesEnabled=false, we do not want to fetch legacy templates and return an empty array;
// we retain the same response format to limit changes required on the client
return response.ok({ body: { templates, legacyTemplates: [] } });
}
const legacyTemplatesEs = await client.asCurrentUser.indices.getTemplate();
const legacyTemplates = deserializeLegacyTemplateList(
legacyTemplatesEs,
cloudManagedTemplatePrefix
);
// @ts-expect-error TemplateSerialized.index_patterns not compatible with IndicesIndexTemplate.index_patterns
const templates = deserializeTemplateList(templatesEs, cloudManagedTemplatePrefix);
const body = {
templates,
@ -59,7 +66,7 @@ const querySchema = schema.object({
legacy: schema.maybe(schema.oneOf([schema.literal('true'), schema.literal('false')])),
});
export function registerGetOneRoute({ router, lib: { handleEsError } }: RouteDependencies) {
export function registerGetOneRoute({ router, config, lib: { handleEsError } }: RouteDependencies) {
router.get(
{
path: addBasePath('/index_templates/{name}'),
@ -68,7 +75,10 @@ export function registerGetOneRoute({ router, lib: { handleEsError } }: RouteDep
async (context, request, response) => {
const { client } = (await context.core).elasticsearch;
const { name } = request.params as TypeOf<typeof paramsSchema>;
const isLegacy = (request.query as TypeOf<typeof querySchema>).legacy === 'true';
// We don't expect the `legacy` query to be used when legacy templates are disabled, however, we add the `enableLegacyTemplates` check as a safeguard
const isLegacy =
config.isLegacyTemplatesEnabled !== false &&
(request.query as TypeOf<typeof querySchema>).legacy === 'true';
try {
const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(client);

View file

@ -12,6 +12,7 @@ import type { RouteDependencies } from '../../types';
export const routeDependencies: Omit<RouteDependencies, 'router'> = {
config: {
isSecurityEnabled: jest.fn().mockReturnValue(true),
isLegacyTemplatesEnabled: true,
},
indexDataEnricher: new IndexDataEnricher(),
lib: {

View file

@ -23,6 +23,7 @@ export interface RouteDependencies {
router: IRouter;
config: {
isSecurityEnabled: () => boolean;
isLegacyTemplatesEnabled: boolean;
};
indexDataEnricher: IndexDataEnricher;
lib: {

View file

@ -13,5 +13,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./spaces'));
loadTestFile(require.resolve('./security_response_headers'));
loadTestFile(require.resolve('./rollups'));
loadTestFile(require.resolve('./index_management'));
});
}

View file

@ -0,0 +1,14 @@
/*
* 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 { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('Index Management APIs', function () {
loadTestFile(require.resolve('./index_templates'));
});
}

View file

@ -0,0 +1,94 @@
/*
* 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 expect from 'expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
const API_BASE_PATH = '/api/index_management';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const es = getService('es');
const log = getService('log');
describe('Index templates', function () {
const templateName = `template-${Math.random()}`;
const indexTemplate = {
name: templateName,
body: {
index_patterns: ['test*'],
},
};
before(async () => {
// Create a new index template to test against
try {
await es.indices.putIndexTemplate(indexTemplate);
} catch (err) {
log.debug('[Setup error] Error creating index template');
throw err;
}
});
after(async () => {
// Cleanup template created for testing purposes
try {
await es.indices.deleteIndexTemplate({
name: templateName,
});
} catch (err) {
log.debug('[Cleanup error] Error deleting index template');
throw err;
}
});
describe('get all', () => {
it('should list all the index templates with the expected parameters', async () => {
const { body: allTemplates } = await supertest
.get(`${API_BASE_PATH}/index_templates`)
.set('kbn-xsrf', 'xxx')
.set('x-elastic-internal-origin', 'xxx')
.expect(200);
// Legacy templates are not applicable on serverless
expect(allTemplates.legacyTemplates.length).toEqual(0);
const indexTemplateFound = allTemplates.templates.find(
(template: { name: string }) => template.name === indexTemplate.name
);
expect(indexTemplateFound).toBeTruthy();
const expectedKeys = [
'name',
'indexPatterns',
'hasSettings',
'hasAliases',
'hasMappings',
'_kbnMeta',
].sort();
expect(Object.keys(indexTemplateFound).sort()).toEqual(expectedKeys);
});
});
describe('get one', () => {
it('should return an index template with the expected parameters', async () => {
const { body } = await supertest
.get(`${API_BASE_PATH}/index_templates/${templateName}`)
.set('kbn-xsrf', 'xxx')
.set('x-elastic-internal-origin', 'xxx')
.expect(200);
const expectedKeys = ['name', 'indexPatterns', 'template', '_kbnMeta'].sort();
expect(body.name).toEqual(templateName);
expect(Object.keys(body).sort()).toEqual(expectedKeys);
});
});
});
}

View file

@ -55,6 +55,9 @@ export function createTestConfig(options: CreateTestConfigOptions) {
management: {
pathname: '/app/management',
},
indexManagement: {
pathname: '/app/management/data/index_management',
},
},
// choose where screenshots should be saved
screenshots: {

View file

@ -14,5 +14,8 @@ export default function ({ loadTestFile }: FtrProviderContext) {
// platform security
loadTestFile(require.resolve('./security/navigation/avatar_menu'));
// Management
loadTestFile(require.resolve('./index_management'));
});
}

View file

@ -0,0 +1,14 @@
/*
* 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 { FtrProviderContext } from '../../../ftr_provider_context';
export default ({ loadTestFile }: FtrProviderContext) => {
describe('Index Management', function () {
loadTestFile(require.resolve('./index_templates'));
});
};

View file

@ -0,0 +1,35 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default ({ getPageObjects, getService }: FtrProviderContext) => {
const testSubjects = getService('testSubjects');
const pageObjects = getPageObjects(['common', 'indexManagement', 'header']);
const browser = getService('browser');
const security = getService('security');
const retry = getService('retry');
describe('Index Templates', function () {
before(async () => {
await security.testUser.setRoles(['index_management_user']);
await pageObjects.common.navigateToApp('indexManagement');
// Navigate to the index templates tab
await pageObjects.indexManagement.changeTabs('templatesTab');
});
it('renders the index templates tab', async () => {
await retry.waitFor('index templates list to be visible', async () => {
return await testSubjects.exists('templateList');
});
const url = await browser.getCurrentUrl();
expect(url).to.contain(`/templates`);
});
});
};