Skip to content

Commit

Permalink
[Index Management] Add serverless tests for indices routes (elastic#1…
Browse files Browse the repository at this point in the history
…71773)

## Summary

This PR adds api integration tests for indices routes that were missing
for serverless ("reload" and "delete index"). To avoid copy-pasting the
code, this PR also adds an index management service to re-use in
serverless tests. Also some refactoring of js code into ts.


### Checklist

Delete any items that are not applicable to this PR.

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)


### Risk Matrix

Delete this section if it is not applicable to this PR.

Before closing this PR, invite QA, stakeholders, and other developers to
identify risks that should be tested prior to the change/feature
release.

When forming the risk matrix, consider some of the following examples
and how they may potentially impact the change:

| Risk | Probability | Severity | Mitigation/Notes |

|---------------------------|-------------|----------|-------------------------|
| Multiple Spaces—unexpected behavior in non-default Kibana Space.
| Low | High | Integration tests will verify that all features are still
supported in non-default Kibana Space and when user switches between
spaces. |
| Multiple nodes—Elasticsearch polling might have race conditions
when multiple Kibana nodes are polling for the same tasks. | High | Low
| Tasks are idempotent, so executing them multiple times will not result
in logical error, but will degrade performance. To test for this case we
add plenty of unit tests around this logic and document manual testing
procedure. |
| Code should gracefully handle cases when feature X or plugin Y are
disabled. | Medium | High | Unit tests will verify that any feature flag
or plugin combination still results in our service operational. |
| [See more potential risk
examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) |


### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
yuliacech authored Nov 24, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 1a4ac01 commit 52d0e38
Showing 14 changed files with 233 additions and 158 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -7,19 +7,13 @@

import expect from '@kbn/expect';

import { initElasticsearchHelpers } from './lib';
import { registerHelpers } from './indices.helpers';
import { sortedExpectedIndexKeys } from './constants';
import { indicesApi } from './lib/indices.api';
import { indicesHelpers } from './lib/indices.helpers';
import { FtrProviderContext } from '../../../ftr_provider_context';

export default function ({ getService }) {
const supertest = getService('supertest');

const {
createIndex,
catIndex,
indexStats,
cleanUp: cleanUpEsResources,
} = initElasticsearchHelpers(getService);
export default function ({ getService }: FtrProviderContext) {
const { createIndex, deleteAllIndices, catIndex, indexStats } = indicesHelpers(getService);

const {
closeIndex,
@@ -32,10 +26,10 @@ export default function ({ getService }) {
list,
reload,
clearCache,
} = registerHelpers({ supertest });
} = indicesApi(getService);

describe('indices', () => {
after(() => Promise.all([cleanUpEsResources()]));
after(async () => await deleteAllIndices());

describe('clear cache', () => {
it('should clear the cache on a single index', async () => {
@@ -45,9 +39,6 @@ export default function ({ getService }) {
});

describe('close', function () {
// The Cloud backend disallows users from closing indices.
this.tags(['skipCloud']);

it('should close an index', async () => {
const index = await createIndex();

@@ -68,10 +59,6 @@ export default function ({ getService }) {
});

describe('open', function () {
// The Cloud backend disallows users from closing indices, so there's no point testing
// the open behavior.
this.tags(['skipCloud']);

it('should open an index', async () => {
const index = await createIndex();

@@ -98,12 +85,12 @@ export default function ({ getService }) {
const index = await createIndex();

const { body: indices1 } = await catIndex(undefined, 'i');
expect(indices1.map((index) => index.i)).to.contain(index);
expect(indices1.map((indexItem) => indexItem.i)).to.contain(index);

await deleteIndex([index]).expect(200);
await deleteIndex(index).expect(200);

const { body: indices2 } = await catIndex(undefined, 'i');
expect(indices2.map((index) => index.i)).not.to.contain(index);
expect(indices2.map((indexItem) => indexItem.i)).not.to.contain(index);
});

it('should require index or indices to be provided', async () => {
@@ -119,13 +106,15 @@ export default function ({ getService }) {
const {
body: { indices: indices1 },
} = await indexStats(index, 'flush');
// @ts-ignore
expect(indices1[index].total.flush.total).to.be(0);

await flushIndex(index).expect(200);

const {
body: { indices: indices2 },
} = await indexStats(index, 'flush');
// @ts-ignore
expect(indices2[index].total.flush.total).to.be(1);
});
});
@@ -137,13 +126,15 @@ export default function ({ getService }) {
const {
body: { indices: indices1 },
} = await indexStats(index, 'refresh');
// @ts-ignore
const previousRefreshes = indices1[index].total.refresh.total;

await refreshIndex(index).expect(200);

const {
body: { indices: indices2 },
} = await indexStats(index, 'refresh');
// @ts-ignore
expect(indices2[index].total.refresh.total).to.be(previousRefreshes + 1);
});
});
@@ -175,8 +166,6 @@ export default function ({ getService }) {
});

describe('list', function () {
this.tags(['skipCloud']);

it('should list all the indices with the expected properties and data enrichers', async function () {
// Create an index that we can assert against
await createIndex('test_index');
@@ -185,7 +174,7 @@ export default function ({ getService }) {
const { body: indices } = await list().expect(200);

// Find the "test_index" created to verify expected keys
const indexCreated = indices.find((index) => index.name === 'test_index');
const indexCreated = indices.find((index: { name: string }) => index.name === 'test_index');

const sortedReceivedKeys = Object.keys(indexCreated).sort();

@@ -194,19 +183,17 @@ export default function ({ getService }) {
});

describe('reload', function () {
describe('(not on Cloud)', function () {
this.tags(['skipCloud']);

it('should list all the indices with the expected properties and data enrichers', async function () {
// create an index to assert against, otherwise the test is flaky
await createIndex('reload-test-index');
const { body } = await reload().expect(200);

const indexCreated = body.find((index) => index.name === 'reload-test-index');
const sortedReceivedKeys = Object.keys(indexCreated).sort();
expect(sortedReceivedKeys).to.eql(sortedExpectedIndexKeys);
expect(body.length > 1).to.be(true); // to contrast it with the next test
});
it('should list all the indices with the expected properties and data enrichers', async function () {
// create an index to assert against, otherwise the test is flaky
await createIndex('reload-test-index');
const { body } = await reload().expect(200);

const indexCreated = body.find(
(index: { name: string }) => index.name === 'reload-test-index'
);
const sortedReceivedKeys = Object.keys(indexCreated).sort();
expect(sortedReceivedKeys).to.eql(sortedExpectedIndexKeys);
expect(body.length > 1).to.be(true); // to contrast it with the next test
});

it('should allow reloading only certain indices', async () => {
Original file line number Diff line number Diff line change
@@ -5,18 +5,14 @@
* 2.0.
*/

import { getRandomString } from './random';

/**
* Helpers to create and delete indices on the Elasticsearch instance
* during our tests.
* @param {ElasticsearchClient} es The Elasticsearch client instance
*/
export const initElasticsearchHelpers = (getService) => {
const es = getService('es');
const esDeleteAllIndices = getService('esDeleteAllIndices');

let indicesCreated = [];
let datastreamCreated = [];
let indexTemplatesCreated = [];
let componentTemplatesCreated = [];
@@ -40,22 +36,6 @@ export const initElasticsearchHelpers = (getService) => {
console.log(`[Cleanup error] Error deleting ES resources: ${err.message}`);
});

const createIndex = (index = getRandomString(), body) => {
indicesCreated.push(index);
return es.indices.create({ index, body }).then(() => index);
};

const deleteAllIndices = async () => {
await esDeleteAllIndices(indicesCreated);
indicesCreated = [];
};

const catIndex = (index, h) => es.cat.indices({ index, format: 'json', h }, { meta: true });

const indexStats = (index, metric) => es.indices.stats({ index, metric }, { meta: true });

const cleanUp = () => deleteAllIndices();

const catTemplate = (name) => es.cat.templates({ name, format: 'json' }, { meta: true });

const createIndexTemplate = (indexTemplate, shouldCacheTemplate) => {
@@ -103,14 +83,9 @@ export const initElasticsearchHelpers = (getService) => {
});

return {
createIndex,
deleteAllIndices,
catIndex,
indexStats,
createDatastream,
deleteDatastream,
cleanupDatastreams,
cleanUp,
catTemplate,
createIndexTemplate,
deleteIndexTemplate,
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* 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 { API_BASE_PATH } from '../constants';
import { FtrProviderContext } from '../../../../ftr_provider_context';

export function indicesApi(getService: FtrProviderContext['getService']) {
const supertest = getService('supertest');
const executeActionOnIndices = ({
index,
urlParam,
args,
}: {
index?: string | string[];
urlParam: string;
args?: any;
}) => {
const indices = Array.isArray(index) ? index : [index];

return supertest
.post(`${API_BASE_PATH}/indices/${urlParam}`)
.set('kbn-xsrf', 'xxx')
.send({ indices, ...args });
};

const closeIndex = (index: string) => executeActionOnIndices({ index, urlParam: 'close' });

const openIndex = (index: string) => executeActionOnIndices({ index, urlParam: 'open' });

const deleteIndex = (index?: string) => executeActionOnIndices({ index, urlParam: 'delete' });

const flushIndex = (index: string) => executeActionOnIndices({ index, urlParam: 'flush' });

const refreshIndex = (index: string) => executeActionOnIndices({ index, urlParam: 'refresh' });

const forceMerge = (index: string, args?: { maxNumSegments: number }) =>
executeActionOnIndices({ index, urlParam: 'forcemerge', args });

const unfreeze = (index: string) => executeActionOnIndices({ index, urlParam: 'unfreeze' });

const clearCache = (index: string) => executeActionOnIndices({ index, urlParam: 'clear_cache' });

const list = () => supertest.get(`${API_BASE_PATH}/indices`);

const reload = (indexNames?: string[]) =>
supertest.post(`${API_BASE_PATH}/indices/reload`).set('kbn-xsrf', 'xxx').send({ indexNames });

return {
closeIndex,
openIndex,
deleteIndex,
flushIndex,
refreshIndex,
forceMerge,
unfreeze,
list,
reload,
clearCache,
};
}
Original file line number Diff line number Diff line change
@@ -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 { getRandomString } from './random';
import { FtrProviderContext } from '../../../../ftr_provider_context';

export function indicesHelpers(getService: FtrProviderContext['getService']) {
const es = getService('es');
const esDeleteAllIndices = getService('esDeleteAllIndices');

let indicesCreated: string[] = [];

const createIndex = async (index: string = getRandomString(), mappings?: any) => {
indicesCreated.push(index);
await es.indices.create({ index, mappings });
return index;
};

const deleteAllIndices = async () => {
await esDeleteAllIndices(indicesCreated);
indicesCreated = [];
};

const catIndex = (index?: string, h?: any) =>
es.cat.indices({ index, format: 'json', h }, { meta: true });

const indexStats = (index: string, metric: string) =>
es.indices.stats({ index, metric }, { meta: true });

return { createIndex, deleteAllIndices, catIndex, indexStats };
}
Original file line number Diff line number Diff line change
@@ -7,18 +7,18 @@

import expect from '@kbn/expect';

import { initElasticsearchHelpers } from './lib';
import { indicesHelpers } from './lib/indices.helpers';
import { registerHelpers } from './mapping.helpers';

export default function ({ getService }) {
const supertest = getService('supertest');

const { createIndex, cleanUp: cleanUpEsResources } = initElasticsearchHelpers(getService);
const { createIndex, deleteAllIndices } = indicesHelpers(getService);

const { getIndexMapping } = registerHelpers({ supertest });

describe('mapping', () => {
after(() => Promise.all([cleanUpEsResources()]));
after(async () => await deleteAllIndices());

it('should fetch the index mapping', async () => {
const mappings = {
@@ -28,7 +28,7 @@ export default function ({ getService }) {
createdAt: { type: 'date' },
},
};
const index = await createIndex(undefined, { mappings });
const index = await createIndex(undefined, mappings);

const { body } = await getIndexMapping(index).expect(200);

Original file line number Diff line number Diff line change
@@ -7,18 +7,18 @@

import expect from '@kbn/expect';

import { initElasticsearchHelpers } from './lib';
import { indicesHelpers } from './lib/indices.helpers';
import { registerHelpers } from './settings.helpers';

export default function ({ getService }) {
const supertest = getService('supertest');

const { createIndex, cleanUp: cleanUpEsResources } = initElasticsearchHelpers(getService);
const { createIndex, deleteAllIndices } = indicesHelpers(getService);

const { getIndexSettings, updateIndexSettings } = registerHelpers({ supertest });

describe('settings', () => {
after(() => Promise.all([cleanUpEsResources()]));
after(async () => await deleteAllIndices());

it('should fetch an index settings', async () => {
const index = await createIndex();
Original file line number Diff line number Diff line change
@@ -7,18 +7,18 @@

import expect from '@kbn/expect';

import { initElasticsearchHelpers } from './lib';
import { indicesHelpers } from './lib/indices.helpers';
import { registerHelpers } from './stats.helpers';

export default function ({ getService }) {
const supertest = getService('supertest');

const { createIndex, cleanUp: cleanUpEsResources } = initElasticsearchHelpers(getService);
const { createIndex, deleteAllIndices } = indicesHelpers(getService);

const { getIndexStats } = registerHelpers({ supertest });

describe('stats', () => {
after(() => Promise.all([cleanUpEsResources()]));
after(async () => await deleteAllIndices());

it('should fetch the index stats', async () => {
const index = await createIndex();
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@ import { registerHelpers } from './templates.helpers';
export default function ({ getService }) {
const supertest = getService('supertest');

const { cleanUp: cleanUpEsResources, catTemplate } = initElasticsearchHelpers(getService);
const { catTemplate } = initElasticsearchHelpers(getService);

const {
getAllTemplates,
@@ -27,7 +27,7 @@ export default function ({ getService }) {

// FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/170980
describe.skip('index templates', () => {
after(() => Promise.all([cleanUpEsResources(), cleanUpTemplates()]));
after(async () => await cleanUpTemplates());

describe('get all', () => {
const indexTemplate = getTemplatePayload(`template-${getRandomString()}`, [
2 changes: 2 additions & 0 deletions x-pack/test/api_integration/services/index.ts
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@ import { MachineLearningProvider } from './ml';
import { IngestManagerProvider } from '../../common/services/ingest_manager';
import { TransformProvider } from './transform';
import { IngestPipelinesProvider } from './ingest_pipelines';
import { IndexManagementProvider } from './index_management';

export const services = {
...commonServices,
@@ -37,4 +38,5 @@ export const services = {
ingestManager: IngestManagerProvider,
transform: TransformProvider,
ingestPipelines: IngestPipelinesProvider,
indexManagement: IndexManagementProvider,
};
19 changes: 19 additions & 0 deletions x-pack/test/api_integration/services/index_management.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* 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';
import { indicesApi } from '../apis/management/index_management/lib/indices.api';
import { indicesHelpers } from '../apis/management/index_management/lib/indices.helpers';

export function IndexManagementProvider({ getService }: FtrProviderContext) {
return {
indices: {
api: indicesApi(getService),
helpers: indicesHelpers(getService),
},
};
}
Original file line number Diff line number Diff line change
@@ -5,33 +5,37 @@
* 2.0.
*/

import expect from 'expect';
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';

const API_BASE_PATH = '/api/index_management';
const INTERNAL_API_BASE_PATH = '/internal/index_management';
const expectedKeys = ['aliases', 'hidden', 'isFrozen', 'primary', 'replica', 'name'].sort();

export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const es = getService('es');
const log = getService('log');
const indexManagementService = getService('indexManagement');

describe('Indices', function () {
const indexName = `index-${Math.random()}`;
let indexName: string;
let reload: typeof indexManagementService['indices']['api']['reload'];
let list: typeof indexManagementService['indices']['api']['list'];
let deleteIndex: typeof indexManagementService['indices']['api']['deleteIndex'];
let createIndex: typeof indexManagementService['indices']['helpers']['createIndex'];
let deleteAllIndices: typeof indexManagementService['indices']['helpers']['deleteAllIndices'];
let catIndex: typeof indexManagementService['indices']['helpers']['catIndex'];

before(async () => {
// Create a new index to test against
const indexExists = await es.indices.exists({ index: indexName });

// Index should not exist, but in the case that it already does, we bypass the create request
if (indexExists) {
return;
}

({
indices: {
api: { reload, list, deleteIndex },
helpers: { createIndex, deleteAllIndices, catIndex },
},
} = indexManagementService);
log.debug(`Creating index: '${indexName}'`);
try {
await es.indices.create({ index: indexName });
indexName = await createIndex();
} catch (err) {
log.debug('[Setup error] Error creating index');
throw err;
@@ -41,28 +45,27 @@ export default function ({ getService }: FtrProviderContext) {
after(async () => {
// Cleanup index created for testing purposes
try {
await es.indices.delete({
index: indexName,
});
await deleteAllIndices();
} catch (err) {
log.debug('[Cleanup error] Error deleting index');
throw err;
}
});

describe('get all', () => {
it('should list indices with the expected parameters', async () => {
const { body: indices } = await supertest
.get(`${API_BASE_PATH}/indices`)
.set('kbn-xsrf', 'xxx')
.set('x-elastic-internal-origin', 'xxx')
.expect(200);
describe('list', () => {
it('should list all the indices with the expected properties', async function () {
// Create an index that we can assert against
await createIndex('test_index');

const indexFound = indices.find((index: { name: string }) => index.name === indexName);
// Verify indices request
const { body: indices } = await list().set('x-elastic-internal-origin', 'xxx').expect(200);

expect(indexFound).toBeTruthy();
// Find the "test_index" created to verify expected keys
const indexCreated = indices.find((index: { name: string }) => index.name === 'test_index');

expect(Object.keys(indexFound).sort()).toEqual(expectedKeys);
const sortedReceivedKeys = Object.keys(indexCreated).sort();

expect(sortedReceivedKeys).to.eql(expectedKeys);
});
});

@@ -74,9 +77,9 @@ export default function ({ getService }: FtrProviderContext) {
.set('x-elastic-internal-origin', 'xxx')
.expect(200);

expect(index).toBeTruthy();
expect(index).to.be.ok();

expect(Object.keys(index).sort()).toEqual(expectedKeys);
expect(Object.keys(index).sort()).to.eql(expectedKeys);
});

it('throws 404 for a non-existent index', async () => {
@@ -118,9 +121,9 @@ export default function ({ getService }: FtrProviderContext) {
.set('x-elastic-internal-origin', 'xxx')
.expect(200);

expect(index).toBeTruthy();
expect(index).to.be.ok();

expect(Object.keys(index).sort()).toEqual(expectedKeys);
expect(Object.keys(index).sort()).to.eql(expectedKeys);
});

it('fails to re-create the same index', async () => {
@@ -134,5 +137,47 @@ export default function ({ getService }: FtrProviderContext) {
.expect(400);
});
});

describe('reload', function () {
it('should list all the indices with the expected properties', async function () {
// create an index to assert against, otherwise the test is flaky
await createIndex('reload-test-index');
const { body } = await reload().set('x-elastic-internal-origin', 'xxx').expect(200);

const indexCreated = body.find(
(index: { name: string }) => index.name === 'reload-test-index'
);
const sortedReceivedKeys = Object.keys(indexCreated).sort();
expect(sortedReceivedKeys).to.eql(expectedKeys);
expect(body.length > 1).to.be(true); // to contrast it with the next test
});

it('should allow reloading only certain indices', async () => {
const index = await createIndex();
const { body } = await reload([index]).set('x-elastic-internal-origin', 'xxx');

expect(body.length === 1).to.be(true);
expect(body[0].name).to.be(index);
});
});

describe('delete indices', () => {
it('should delete an index', async () => {
const index = await createIndex();

const { body: indices1 } = await catIndex(undefined, 'i');
expect(indices1.map((indexItem) => indexItem.i)).to.contain(index);

await deleteIndex(index).set('x-elastic-internal-origin', 'xxx').expect(200);

const { body: indices2 } = await catIndex(undefined, 'i');
expect(indices2.map((indexItem) => indexItem.i)).not.to.contain(index);
});

it('should require index or indices to be provided', async () => {
const { body } = await deleteIndex().set('x-elastic-internal-origin', 'xxx').expect(400);
expect(body.message).to.contain('expected value of type [string]');
});
});
});
}
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@ const deploymentAgnosticApiIntegrationServices = _.pick(apiIntegrationServices,
'esSupertest',
'indexPatterns',
'ingestPipelines',
'indexManagement',
'kibanaServer',
'ml',
'randomness',

0 comments on commit 52d0e38

Please sign in to comment.