Migrations v2: don't auto-create indices + FTR/esArchiver support (#85778)

* Migrations V2 on by default

* esArchiver delete migrations v2 indices

* Fix saved_objects_management api_integration tests

* Try to fix v2 migrations for pre-release builds

* esArchiver delete auto-created v2 migration indices like .kibana_8.0.0

* Try to fix v2 migrations for pre-release builds

* Use require_alias to prevent auto-created saved objects index

* Wrap SO routes until core logs all internal errors

* Fix api_integration tests requiring an empty kibana index

* Delete corrupt saved object from lens archives

* Update docs

* Fix ui_settings tests

* Fix core jest tests

* Fix type errors

* Fix accessibility tests

* Fix plugin functional tests

* Fix api_integration tests after merging in master

* Fix plugin functional tests #2

* EsArchiver: Don't reset ui settings after the .kibana index was deleted

* Fix functional management/visualize tests

* Fix oss security functional tests

* EsArchiver clean task manager indices to fix alerting api integration tests

* migrationsv2 correctly handle unknown saved object type mappings

* Revert "Try to fix v2 migrations for pre-release builds"

This reverts commit a1a1567501.

* Revert "Try to fix v2 migrations for pre-release builds"

This reverts commit a9a935558c.

* Re-enable v2 migrations in tests after merging in master

* Try to fix async dashboard functional test

* Restore UiSettings defaults after emptyKibanaIndex()

* Review feedback: rename test to match behaviour
This commit is contained in:
Rudolf Meijering 2021-02-01 15:46:16 +01:00 committed by GitHub
parent c66124e170
commit 03636a07fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
71 changed files with 645 additions and 238 deletions

View file

@ -0,0 +1,22 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) &gt; [createIndexAliasNotFoundError](./kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md)
## SavedObjectsErrorHelpers.createIndexAliasNotFoundError() method
<b>Signature:</b>
```typescript
static createIndexAliasNotFoundError(alias: string): DecoratedError;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| alias | <code>string</code> | |
<b>Returns:</b>
`DecoratedError`

View file

@ -0,0 +1,23 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) &gt; [decorateIndexAliasNotFoundError](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateindexaliasnotfounderror.md)
## SavedObjectsErrorHelpers.decorateIndexAliasNotFoundError() method
<b>Signature:</b>
```typescript
static decorateIndexAliasNotFoundError(error: Error, alias: string): DecoratedError;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| error | <code>Error</code> | |
| alias | <code>string</code> | |
<b>Returns:</b>
`DecoratedError`

View file

@ -0,0 +1,22 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) &gt; [isGeneralError](./kibana-plugin-core-server.savedobjectserrorhelpers.isgeneralerror.md)
## SavedObjectsErrorHelpers.isGeneralError() method
<b>Signature:</b>
```typescript
static isGeneralError(error: Error | DecoratedError): boolean;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| error | <code>Error &#124; DecoratedError</code> | |
<b>Returns:</b>
`boolean`

View file

@ -18,6 +18,7 @@ export declare class SavedObjectsErrorHelpers
| [createBadRequestError(reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.createbadrequesterror.md) | <code>static</code> | |
| [createConflictError(type, id, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md) | <code>static</code> | |
| [createGenericNotFoundError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfounderror.md) | <code>static</code> | |
| [createIndexAliasNotFoundError(alias)](./kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md) | <code>static</code> | |
| [createInvalidVersionError(versionInput)](./kibana-plugin-core-server.savedobjectserrorhelpers.createinvalidversionerror.md) | <code>static</code> | |
| [createTooManyRequestsError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.createtoomanyrequestserror.md) | <code>static</code> | |
| [createUnsupportedTypeError(type)](./kibana-plugin-core-server.savedobjectserrorhelpers.createunsupportedtypeerror.md) | <code>static</code> | |
@ -27,6 +28,7 @@ export declare class SavedObjectsErrorHelpers
| [decorateEsUnavailableError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateesunavailableerror.md) | <code>static</code> | |
| [decorateForbiddenError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateforbiddenerror.md) | <code>static</code> | |
| [decorateGeneralError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorategeneralerror.md) | <code>static</code> | |
| [decorateIndexAliasNotFoundError(error, alias)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateindexaliasnotfounderror.md) | <code>static</code> | |
| [decorateNotAuthorizedError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decoratenotauthorizederror.md) | <code>static</code> | |
| [decorateRequestEntityTooLargeError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decoraterequestentitytoolargeerror.md) | <code>static</code> | |
| [decorateTooManyRequestsError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decoratetoomanyrequestserror.md) | <code>static</code> | |
@ -35,6 +37,7 @@ export declare class SavedObjectsErrorHelpers
| [isEsCannotExecuteScriptError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isescannotexecutescripterror.md) | <code>static</code> | |
| [isEsUnavailableError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isesunavailableerror.md) | <code>static</code> | |
| [isForbiddenError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isforbiddenerror.md) | <code>static</code> | |
| [isGeneralError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isgeneralerror.md) | <code>static</code> | |
| [isInvalidVersionError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isinvalidversionerror.md) | <code>static</code> | |
| [isNotAuthorizedError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isnotauthorizederror.md) | <code>static</code> | |
| [isNotFoundError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isnotfounderror.md) | <code>static</code> | |

View file

@ -25,5 +25,6 @@ export async function emptyKibanaIndexAction({
await cleanKibanaIndices({ client, stats, log, kibanaPluginIds });
await migrateKibanaIndex({ client, kbnClient });
return stats;
stats.createdIndex('.kibana');
return stats.toJSON();
}

View file

@ -155,7 +155,7 @@ export class EsArchiver {
* @return Promise
*/
async emptyKibanaIndex() {
await emptyKibanaIndexAction({
return await emptyKibanaIndexAction({
client: this.client,
log: this.log,
kbnClient: this.kbnClient,

View file

@ -76,7 +76,9 @@ export async function migrateKibanaIndex({
*/
async function fetchKibanaIndices(client: Client) {
const kibanaIndices = await client.cat.indices({ index: '.kibana*', format: 'json' });
const isKibanaIndex = (index: string) => /^\.kibana(:?_\d*)?$/.test(index);
const isKibanaIndex = (index: string) =>
/^\.kibana(:?_\d*)?$/.test(index) ||
/^\.kibana(_task_manager)?_(pre)?\d+\.\d+\.\d+/.test(index);
return kibanaIndices.map((x: { index: string }) => x.index).filter(isKibanaIndex);
}
@ -103,7 +105,7 @@ export async function cleanKibanaIndices({
while (true) {
const resp = await client.deleteByQuery({
index: `.kibana`,
index: `.kibana,.kibana_task_manager`,
body: {
query: {
bool: {
@ -115,7 +117,7 @@ export async function cleanKibanaIndices({
},
},
},
ignore: [409],
ignore: [404, 409],
});
if (resp.total !== resp.deleted) {

View file

@ -182,6 +182,21 @@ describe('migrations v2 model', () => {
versionAlias: '.kibana_7.11.0',
versionIndex: '.kibana_7.11.0_001',
};
const mappingsWithUnknownType = {
properties: {
disabled_saved_object_type: {
properties: {
value: { type: 'keyword' },
},
},
},
_meta: {
migrationMappingPropertyHashes: {
disabled_saved_object_type: '7997cf5a56cc02bdc9c93361bde732b0',
},
},
};
test('INIT -> OUTDATED_DOCUMENTS_SEARCH if .kibana is already pointing to the target index', () => {
const res: ResponseType<'INIT'> = Either.right({
'.kibana_7.11.0_001': {
@ -189,38 +204,27 @@ describe('migrations v2 model', () => {
'.kibana': {},
'.kibana_7.11.0': {},
},
mappings: {
properties: {
disabled_saved_object_type: {
properties: {
value: { type: 'keyword' },
},
},
},
_meta: {
migrationMappingPropertyHashes: {
disabled_saved_object_type: '7997cf5a56cc02bdc9c93361bde732b0',
},
},
},
mappings: mappingsWithUnknownType,
settings: {},
},
});
const newState = model(initState, res);
expect(newState.controlState).toEqual('OUTDATED_DOCUMENTS_SEARCH');
// This snapshot asserts that we merge the
// migrationMappingPropertyHashes of the existing index, but we leave
// the mappings for the disabled_saved_object_type untouched. There
// might be another Kibana instance that knows about this type and
// needs these mappings in place.
expect(newState.targetIndexMappings).toMatchInlineSnapshot(`
Object {
"_meta": Object {
"migrationMappingPropertyHashes": Object {
"disabled_saved_object_type": "7997cf5a56cc02bdc9c93361bde732b0",
"new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0",
},
},
"properties": Object {
"disabled_saved_object_type": Object {
"dynamic": false,
"properties": Object {},
},
"new_saved_object_type": Object {
"properties": Object {
"value": Object {
@ -271,7 +275,7 @@ describe('migrations v2 model', () => {
'.kibana': {},
'.kibana_7.12.0': {},
},
mappings: { properties: {}, _meta: { migrationMappingPropertyHashes: {} } },
mappings: mappingsWithUnknownType,
settings: {},
},
'.kibana_7.11.0_001': {
@ -288,12 +292,37 @@ describe('migrations v2 model', () => {
sourceIndex: Option.some('.kibana_7.invalid.0_001'),
targetIndex: '.kibana_7.11.0_001',
});
// This snapshot asserts that we disable the unknown saved object
// type. Because it's mappings are disabled, we also don't copy the
// `_meta.migrationMappingPropertyHashes` for the disabled type.
expect(newState.targetIndexMappings).toMatchInlineSnapshot(`
Object {
"_meta": Object {
"migrationMappingPropertyHashes": Object {
"new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0",
},
},
"properties": Object {
"disabled_saved_object_type": Object {
"dynamic": false,
"properties": Object {},
},
"new_saved_object_type": Object {
"properties": Object {
"value": Object {
"type": "text",
},
},
},
},
}
`);
});
test('INIT -> SET_SOURCE_WRITE_BLOCK when migrating from a v2 migrations index (>= 7.11.0)', () => {
const res: ResponseType<'INIT'> = Either.right({
'.kibana_7.11.0_001': {
aliases: { '.kibana': {}, '.kibana_7.11.0': {} },
mappings: { properties: {}, _meta: { migrationMappingPropertyHashes: {} } },
mappings: mappingsWithUnknownType,
settings: {},
},
'.kibana_3': {
@ -319,6 +348,31 @@ describe('migrations v2 model', () => {
sourceIndex: Option.some('.kibana_7.11.0_001'),
targetIndex: '.kibana_7.12.0_001',
});
// This snapshot asserts that we disable the unknown saved object
// type. Because it's mappings are disabled, we also don't copy the
// `_meta.migrationMappingPropertyHashes` for the disabled type.
expect(newState.targetIndexMappings).toMatchInlineSnapshot(`
Object {
"_meta": Object {
"migrationMappingPropertyHashes": Object {
"new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0",
},
},
"properties": Object {
"disabled_saved_object_type": Object {
"dynamic": false,
"properties": Object {},
},
"new_saved_object_type": Object {
"properties": Object {
"value": Object {
"type": "text",
},
},
},
},
}
`);
expect(newState.retryCount).toEqual(0);
expect(newState.retryDelay).toEqual(0);
});
@ -328,7 +382,7 @@ describe('migrations v2 model', () => {
aliases: {
'.kibana': {},
},
mappings: { properties: {}, _meta: { migrationMappingPropertyHashes: {} } },
mappings: mappingsWithUnknownType,
settings: {},
},
});
@ -339,6 +393,31 @@ describe('migrations v2 model', () => {
sourceIndex: Option.some('.kibana_3'),
targetIndex: '.kibana_7.11.0_001',
});
// This snapshot asserts that we disable the unknown saved object
// type. Because it's mappings are disabled, we also don't copy the
// `_meta.migrationMappingPropertyHashes` for the disabled type.
expect(newState.targetIndexMappings).toMatchInlineSnapshot(`
Object {
"_meta": Object {
"migrationMappingPropertyHashes": Object {
"new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0",
},
},
"properties": Object {
"disabled_saved_object_type": Object {
"dynamic": false,
"properties": Object {},
},
"new_saved_object_type": Object {
"properties": Object {
"value": Object {
"type": "text",
},
},
},
},
}
`);
expect(newState.retryCount).toEqual(0);
expect(newState.retryDelay).toEqual(0);
});
@ -346,7 +425,7 @@ describe('migrations v2 model', () => {
const res: ResponseType<'INIT'> = Either.right({
'.kibana': {
aliases: {},
mappings: { properties: {}, _meta: {} },
mappings: mappingsWithUnknownType,
settings: {},
},
});
@ -357,6 +436,31 @@ describe('migrations v2 model', () => {
sourceIndex: Option.some('.kibana_pre6.5.0_001'),
targetIndex: '.kibana_7.11.0_001',
});
// This snapshot asserts that we disable the unknown saved object
// type. Because it's mappings are disabled, we also don't copy the
// `_meta.migrationMappingPropertyHashes` for the disabled type.
expect(newState.targetIndexMappings).toMatchInlineSnapshot(`
Object {
"_meta": Object {
"migrationMappingPropertyHashes": Object {
"new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0",
},
},
"properties": Object {
"disabled_saved_object_type": Object {
"dynamic": false,
"properties": Object {},
},
"new_saved_object_type": Object {
"properties": Object {
"value": Object {
"type": "text",
},
},
},
},
}
`);
expect(newState.retryCount).toEqual(0);
expect(newState.retryDelay).toEqual(0);
});
@ -366,7 +470,7 @@ describe('migrations v2 model', () => {
aliases: {
'my-saved-objects': {},
},
mappings: { properties: {}, _meta: { migrationMappingPropertyHashes: {} } },
mappings: mappingsWithUnknownType,
settings: {},
},
});
@ -386,6 +490,31 @@ describe('migrations v2 model', () => {
sourceIndex: Option.some('my-saved-objects_3'),
targetIndex: 'my-saved-objects_7.11.0_001',
});
// This snapshot asserts that we disable the unknown saved object
// type. Because it's mappings are disabled, we also don't copy the
// `_meta.migrationMappingPropertyHashes` for the disabled type.
expect(newState.targetIndexMappings).toMatchInlineSnapshot(`
Object {
"_meta": Object {
"migrationMappingPropertyHashes": Object {
"new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0",
},
},
"properties": Object {
"disabled_saved_object_type": Object {
"dynamic": false,
"properties": Object {},
},
"new_saved_object_type": Object {
"properties": Object {
"value": Object {
"type": "text",
},
},
},
},
}
`);
expect(newState.retryCount).toEqual(0);
expect(newState.retryDelay).toEqual(0);
});
@ -395,7 +524,7 @@ describe('migrations v2 model', () => {
aliases: {
'my-saved-objects': {},
},
mappings: { properties: {}, _meta: { migrationMappingPropertyHashes: {} } },
mappings: mappingsWithUnknownType,
settings: {},
},
});
@ -416,6 +545,31 @@ describe('migrations v2 model', () => {
sourceIndex: Option.some('my-saved-objects_7.11.0'),
targetIndex: 'my-saved-objects_7.12.0_001',
});
// This snapshot asserts that we disable the unknown saved object
// type. Because it's mappings are disabled, we also don't copy the
// `_meta.migrationMappingPropertyHashes` for the disabled type.
expect(newState.targetIndexMappings).toMatchInlineSnapshot(`
Object {
"_meta": Object {
"migrationMappingPropertyHashes": Object {
"new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0",
},
},
"properties": Object {
"disabled_saved_object_type": Object {
"dynamic": false,
"properties": Object {},
},
"new_saved_object_type": Object {
"properties": Object {
"value": Object {
"type": "text",
},
},
},
},
}
`);
expect(newState.retryCount).toEqual(0);
expect(newState.retryDelay).toEqual(0);
});

View file

@ -60,13 +60,13 @@ function throwBadResponse(state: State, res: any): never {
* Merge the _meta.migrationMappingPropertyHashes mappings of an index with
* the given target mappings.
*
* @remarks Mapping updates are commutative (deeply merged) by Elasticsearch,
* except for the _meta key. The source index we're migrating from might
* contain documents created by a plugin that is disabled in the Kibana
* instance performing this migration. We merge the
* _meta.migrationMappingPropertyHashes mappings from the source index into
* the targetMappings to ensure that any `migrationPropertyHashes` for
* disabled plugins aren't lost.
* @remarks When another instance already completed a migration, the existing
* target index might contain documents and mappings created by a plugin that
* is disabled in the current Kibana instance performing this migration.
* Mapping updates are commutative (deeply merged) by Elasticsearch, except
* for the `_meta` key. By merging the `_meta.migrationMappingPropertyHashes`
* mappings from the existing target index index into the targetMappings we
* ensure that any `migrationPropertyHashes` for disabled plugins aren't lost.
*
* Right now we don't use these `migrationPropertyHashes` but it could be used
* in the future to detect if mappings were changed. If mappings weren't
@ -209,7 +209,7 @@ export const model = (currentState: State, resW: ResponseType<AllActionStates>):
// index
sourceIndex: Option.none,
targetIndex: `${stateP.indexPrefix}_${stateP.kibanaVersion}_001`,
targetIndexMappings: disableUnknownTypeMappingFields(
targetIndexMappings: mergeMigrationMappingPropertyHashes(
stateP.targetIndexMappings,
indices[aliases[stateP.currentAlias]].mappings
),
@ -242,7 +242,7 @@ export const model = (currentState: State, resW: ResponseType<AllActionStates>):
controlState: 'SET_SOURCE_WRITE_BLOCK',
sourceIndex: Option.some(source) as Option.Some<string>,
targetIndex: target,
targetIndexMappings: mergeMigrationMappingPropertyHashes(
targetIndexMappings: disableUnknownTypeMappingFields(
stateP.targetIndexMappings,
indices[source].mappings
),
@ -279,7 +279,7 @@ export const model = (currentState: State, resW: ResponseType<AllActionStates>):
controlState: 'LEGACY_SET_WRITE_BLOCK',
sourceIndex: Option.some(legacyReindexTarget) as Option.Some<string>,
targetIndex: target,
targetIndexMappings: mergeMigrationMappingPropertyHashes(
targetIndexMappings: disableUnknownTypeMappingFields(
stateP.targetIndexMappings,
indices[stateP.legacyIndex].mappings
),

View file

@ -9,6 +9,7 @@
import { schema } from '@kbn/config-schema';
import { IRouter } from '../../http';
import { CoreUsageDataSetup } from '../../core_usage_data';
import { catchAndReturnBoomErrors } from './utils';
interface RouteDependencies {
coreUsageData: CoreUsageDataSetup;
@ -44,7 +45,7 @@ export const registerBulkCreateRoute = (router: IRouter, { coreUsageData }: Rout
),
},
},
router.handleLegacyErrors(async (context, req, res) => {
catchAndReturnBoomErrors(async (context, req, res) => {
const { overwrite } = req.query;
const usageStatsClient = coreUsageData.getClient();

View file

@ -9,6 +9,7 @@
import { schema } from '@kbn/config-schema';
import { IRouter } from '../../http';
import { CoreUsageDataSetup } from '../../core_usage_data';
import { catchAndReturnBoomErrors } from './utils';
interface RouteDependencies {
coreUsageData: CoreUsageDataSetup;
@ -28,7 +29,7 @@ export const registerBulkGetRoute = (router: IRouter, { coreUsageData }: RouteDe
),
},
},
router.handleLegacyErrors(async (context, req, res) => {
catchAndReturnBoomErrors(async (context, req, res) => {
const usageStatsClient = coreUsageData.getClient();
usageStatsClient.incrementSavedObjectsBulkGet({ request: req }).catch(() => {});

View file

@ -9,6 +9,7 @@
import { schema } from '@kbn/config-schema';
import { IRouter } from '../../http';
import { CoreUsageDataSetup } from '../../core_usage_data';
import { catchAndReturnBoomErrors } from './utils';
interface RouteDependencies {
coreUsageData: CoreUsageDataSetup;
@ -39,7 +40,7 @@ export const registerBulkUpdateRoute = (router: IRouter, { coreUsageData }: Rout
),
},
},
router.handleLegacyErrors(async (context, req, res) => {
catchAndReturnBoomErrors(async (context, req, res) => {
const usageStatsClient = coreUsageData.getClient();
usageStatsClient.incrementSavedObjectsBulkUpdate({ request: req }).catch(() => {});

View file

@ -9,6 +9,7 @@
import { schema } from '@kbn/config-schema';
import { IRouter } from '../../http';
import { CoreUsageDataSetup } from '../../core_usage_data';
import { catchAndReturnBoomErrors } from './utils';
interface RouteDependencies {
coreUsageData: CoreUsageDataSetup;
@ -43,7 +44,7 @@ export const registerCreateRoute = (router: IRouter, { coreUsageData }: RouteDep
}),
},
},
router.handleLegacyErrors(async (context, req, res) => {
catchAndReturnBoomErrors(async (context, req, res) => {
const { type, id } = req.params;
const { overwrite } = req.query;
const {

View file

@ -9,6 +9,7 @@
import { schema } from '@kbn/config-schema';
import { IRouter } from '../../http';
import { CoreUsageDataSetup } from '../../core_usage_data';
import { catchAndReturnBoomErrors } from './utils';
interface RouteDependencies {
coreUsageData: CoreUsageDataSetup;
@ -28,7 +29,7 @@ export const registerDeleteRoute = (router: IRouter, { coreUsageData }: RouteDep
}),
},
},
router.handleLegacyErrors(async (context, req, res) => {
catchAndReturnBoomErrors(async (context, req, res) => {
const { type, id } = req.params;
const { force } = req.query;

View file

@ -18,7 +18,7 @@ import {
SavedObjectsExportByObjectOptions,
SavedObjectsExportError,
} from '../export';
import { validateTypes, validateObjects } from './utils';
import { validateTypes, validateObjects, catchAndReturnBoomErrors } from './utils';
interface RouteDependencies {
config: SavedObjectConfig;
@ -163,7 +163,7 @@ export const registerExportRoute = (
}),
},
},
router.handleLegacyErrors(async (context, req, res) => {
catchAndReturnBoomErrors(async (context, req, res) => {
const cleaned = cleanOptions(req.body);
const supportedTypes = context.core.savedObjects.typeRegistry
.getImportableAndExportableTypes()

View file

@ -9,6 +9,7 @@
import { schema } from '@kbn/config-schema';
import { IRouter } from '../../http';
import { CoreUsageDataSetup } from '../../core_usage_data';
import { catchAndReturnBoomErrors } from './utils';
interface RouteDependencies {
coreUsageData: CoreUsageDataSetup;
@ -49,7 +50,7 @@ export const registerFindRoute = (router: IRouter, { coreUsageData }: RouteDepen
}),
},
},
router.handleLegacyErrors(async (context, req, res) => {
catchAndReturnBoomErrors(async (context, req, res) => {
const query = req.query;
const namespaces =

View file

@ -9,6 +9,7 @@
import { schema } from '@kbn/config-schema';
import { IRouter } from '../../http';
import { CoreUsageDataSetup } from '../../core_usage_data';
import { catchAndReturnBoomErrors } from './utils';
interface RouteDependencies {
coreUsageData: CoreUsageDataSetup;
@ -25,7 +26,7 @@ export const registerGetRoute = (router: IRouter, { coreUsageData }: RouteDepend
}),
},
},
router.handleLegacyErrors(async (context, req, res) => {
catchAndReturnBoomErrors(async (context, req, res) => {
const { type, id } = req.params;
const usageStatsClient = coreUsageData.getClient();

View file

@ -13,7 +13,7 @@ import { IRouter } from '../../http';
import { CoreUsageDataSetup } from '../../core_usage_data';
import { SavedObjectConfig } from '../saved_objects_config';
import { SavedObjectsImportError } from '../import';
import { createSavedObjectsStreamFromNdJson } from './utils';
import { catchAndReturnBoomErrors, createSavedObjectsStreamFromNdJson } from './utils';
interface RouteDependencies {
config: SavedObjectConfig;
@ -61,7 +61,7 @@ export const registerImportRoute = (
}),
},
},
router.handleLegacyErrors(async (context, req, res) => {
catchAndReturnBoomErrors(async (context, req, res) => {
const { overwrite, createNewCopies } = req.query;
const usageStatsClient = coreUsageData.getClient();

View file

@ -8,6 +8,7 @@
import { IRouter } from '../../http';
import { IKibanaMigrator } from '../migrations';
import { catchAndReturnBoomErrors } from './utils';
export const registerMigrateRoute = (
router: IRouter,
@ -21,7 +22,7 @@ export const registerMigrateRoute = (
tags: ['access:migrateSavedObjects'],
},
},
router.handleLegacyErrors(async (context, req, res) => {
catchAndReturnBoomErrors(async (context, req, res) => {
const migrator = await migratorPromise;
await migrator.runMigrations({ rerun: true });
return res.ok({

View file

@ -13,8 +13,7 @@ import { IRouter } from '../../http';
import { CoreUsageDataSetup } from '../../core_usage_data';
import { SavedObjectConfig } from '../saved_objects_config';
import { SavedObjectsImportError } from '../import';
import { createSavedObjectsStreamFromNdJson } from './utils';
import { catchAndReturnBoomErrors, createSavedObjectsStreamFromNdJson } from './utils';
interface RouteDependencies {
config: SavedObjectConfig;
coreUsageData: CoreUsageDataSetup;
@ -69,7 +68,7 @@ export const registerResolveImportErrorsRoute = (
}),
},
},
router.handleLegacyErrors(async (context, req, res) => {
catchAndReturnBoomErrors(async (context, req, res) => {
const { createNewCopies } = req.query;
const usageStatsClient = coreUsageData.getClient();

View file

@ -9,6 +9,7 @@
import { schema } from '@kbn/config-schema';
import { IRouter } from '../../http';
import { CoreUsageDataSetup } from '../../core_usage_data';
import { catchAndReturnBoomErrors } from './utils';
interface RouteDependencies {
coreUsageData: CoreUsageDataSetup;
@ -38,7 +39,7 @@ export const registerUpdateRoute = (router: IRouter, { coreUsageData }: RouteDep
}),
},
},
router.handleLegacyErrors(async (context, req, res) => {
catchAndReturnBoomErrors(async (context, req, res) => {
const { type, id } = req.params;
const { attributes, version, references } = req.body;
const options = { version, references };

View file

@ -9,6 +9,15 @@
import { createSavedObjectsStreamFromNdJson, validateTypes, validateObjects } from './utils';
import { Readable } from 'stream';
import { createPromiseFromStreams, createConcatStream } from '@kbn/utils';
import { catchAndReturnBoomErrors } from './utils';
import Boom from '@hapi/boom';
import {
KibanaRequest,
RequestHandler,
RequestHandlerContext,
KibanaResponseFactory,
kibanaResponseFactory,
} from '../../';
async function readStreamToCompletion(stream: Readable) {
return createPromiseFromStreams([stream, createConcatStream([])]);
@ -143,3 +152,69 @@ describe('validateObjects', () => {
).toBeUndefined();
});
});
describe('catchAndReturnBoomErrors', () => {
let context: RequestHandlerContext;
let request: KibanaRequest<any, any, any>;
let response: KibanaResponseFactory;
const createHandler = (handler: () => any): RequestHandler<any, any, any> => () => {
return handler();
};
beforeEach(() => {
context = {} as any;
request = {} as any;
response = kibanaResponseFactory;
});
it('should pass-though call parameters to the handler', async () => {
const handler = jest.fn();
const wrapped = catchAndReturnBoomErrors(handler);
await wrapped(context, request, response);
expect(handler).toHaveBeenCalledWith(context, request, response);
});
it('should pass-though result from the handler', async () => {
const handler = createHandler(() => {
return 'handler-response';
});
const wrapped = catchAndReturnBoomErrors(handler);
const result = await wrapped(context, request, response);
expect(result).toBe('handler-response');
});
it('should intercept and convert thrown Boom errors', async () => {
const handler = createHandler(() => {
throw Boom.notFound('not there');
});
const wrapped = catchAndReturnBoomErrors(handler);
const result = await wrapped(context, request, response);
expect(result.status).toBe(404);
expect(result.payload).toEqual({
error: 'Not Found',
message: 'not there',
statusCode: 404,
});
});
it('should re-throw non-Boom errors', async () => {
const handler = createHandler(() => {
throw new Error('something went bad');
});
const wrapped = catchAndReturnBoomErrors(handler);
await expect(wrapped(context, request, response)).rejects.toMatchInlineSnapshot(
`[Error: something went bad]`
);
});
it('should re-throw Boom internal/500 errors', async () => {
const handler = createHandler(() => {
throw Boom.internal();
});
const wrapped = catchAndReturnBoomErrors(handler);
await expect(wrapped(context, request, response)).rejects.toMatchInlineSnapshot(
`[Error: Internal Server Error]`
);
});
});

View file

@ -7,7 +7,11 @@
*/
import { Readable } from 'stream';
import { SavedObject, SavedObjectsExportResultDetails } from 'src/core/server';
import {
RequestHandlerWrapper,
SavedObject,
SavedObjectsExportResultDetails,
} from 'src/core/server';
import {
createSplitStream,
createMapStream,
@ -16,6 +20,7 @@ import {
createListStream,
createConcatStream,
} from '@kbn/utils';
import Boom from '@hapi/boom';
export async function createSavedObjectsStreamFromNdJson(ndJsonStream: Readable) {
const savedObjects = await createPromiseFromStreams([
@ -52,3 +57,30 @@ export function validateObjects(
.join(', ')}`;
}
}
/**
* Catches errors thrown by saved object route handlers and returns an error
* with the payload and statusCode of the boom error.
*
* This is very close to the core `router.handleLegacyErrors` except that it
* throws internal errors (statusCode: 500) so that the internal error's
* message get logged by Core.
*
* TODO: Remove once https://github.com/elastic/kibana/issues/65291 is fixed.
*/
export const catchAndReturnBoomErrors: RequestHandlerWrapper = (handler) => {
return async (context, request, response) => {
try {
return await handler(context, request, response);
} catch (e) {
if (Boom.isBoom(e) && e.output.statusCode !== 500) {
return response.customError({
body: e.output.payload,
statusCode: e.output.statusCode,
headers: e.output.headers as { [key: string]: string },
});
}
throw e;
}
};
};

View file

@ -109,6 +109,27 @@ describe('savedObjectsClient/decorateEsError', () => {
expect(SavedObjectsErrorHelpers.isNotFoundError(genericError)).toBe(true);
});
it('if saved objects index does not exist makes NotFound a SavedObjectsClient/generalError', () => {
const error = new esErrors.ResponseError(
elasticsearchClientMock.createApiResponse({
statusCode: 404,
body: {
error: {
reason:
'no such index [.kibana_8.0.0] and [require_alias] request flag is [true] and [.kibana_8.0.0] is not an alias',
},
},
})
);
expect(SavedObjectsErrorHelpers.isGeneralError(error)).toBe(false);
const genericError = decorateEsError(error);
expect(genericError.message).toEqual(
`Saved object index alias [.kibana_8.0.0] not found: Response Error`
);
expect(genericError.output.statusCode).toBe(500);
expect(SavedObjectsErrorHelpers.isGeneralError(error)).toBe(true);
});
it('makes BadRequest a SavedObjectsClient/BadRequest error', () => {
const error = new esErrors.ResponseError(
elasticsearchClientMock.createApiResponse({ statusCode: 400 })

View file

@ -63,6 +63,12 @@ export function decorateEsError(error: EsErrors) {
}
if (responseErrors.isNotFound(error.statusCode)) {
const match = error?.meta?.body?.error?.reason?.match(
/no such index \[(.+)\] and \[require_alias\] request flag is \[true\] and \[.+\] is not an alias/
);
if (match?.length > 0) {
return SavedObjectsErrorHelpers.decorateIndexAliasNotFoundError(error, match[1]);
}
return SavedObjectsErrorHelpers.createGenericNotFoundError();
}

View file

@ -135,6 +135,19 @@ export class SavedObjectsErrorHelpers {
return decorate(Boom.notFound(), CODE_NOT_FOUND, 404);
}
public static createIndexAliasNotFoundError(alias: string) {
return SavedObjectsErrorHelpers.decorateIndexAliasNotFoundError(Boom.internal(), alias);
}
public static decorateIndexAliasNotFoundError(error: Error, alias: string) {
return decorate(
error,
CODE_GENERAL_ERROR,
500,
`Saved object index alias [${alias}] not found`
);
}
public static isNotFoundError(error: Error | DecoratedError) {
return isSavedObjectsClientError(error) && error[code] === CODE_NOT_FOUND;
}
@ -185,4 +198,8 @@ export class SavedObjectsErrorHelpers {
public static decorateGeneralError(error: Error, reason?: string) {
return decorate(error, CODE_GENERAL_ERROR, 500, reason);
}
public static isGeneralError(error: Error | DecoratedError) {
return isSavedObjectsClientError(error) && error[code] === CODE_GENERAL_ERROR;
}
}

View file

@ -18,6 +18,7 @@ import { DocumentMigrator } from '../../migrations/core/document_migrator';
import { mockKibanaMigrator } from '../../migrations/kibana/kibana_migrator.mock';
import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks';
import { esKuery } from '../../es_query';
import { errors as EsErrors } from '@elastic/elasticsearch';
const { nodeTypes } = esKuery;
jest.mock('./search_dsl/search_dsl', () => ({ getSearchDsl: jest.fn() }));
@ -4341,8 +4342,14 @@ describe('SavedObjectsRepository', () => {
});
it(`throws when ES is unable to find the document during update`, async () => {
const notFoundError = new EsErrors.ResponseError(
elasticsearchClientMock.createApiResponse({
statusCode: 404,
body: { error: { type: 'es_type', reason: 'es_reason' } },
})
);
client.update.mockResolvedValueOnce(
elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 })
elasticsearchClientMock.createErrorTransportRequestPromise(notFoundError)
);
await expectNotFoundError(type, id);
expect(client.update).toHaveBeenCalledTimes(1);

View file

@ -299,6 +299,7 @@ export class SavedObjectsRepository {
refresh,
body: raw._source,
...(overwrite && version ? decodeRequestVersion(version) : {}),
require_alias: true,
};
const { body } =
@ -469,6 +470,7 @@ export class SavedObjectsRepository {
const bulkResponse = bulkCreateParams.length
? await this.client.bulk({
refresh,
require_alias: true,
body: bulkCreateParams,
})
: undefined;
@ -1117,8 +1119,8 @@ export class SavedObjectsRepository {
...(Array.isArray(references) && { references }),
};
const { body, statusCode } = await this.client.update(
{
const { body } = await this.client
.update({
id: this._serializer.generateRawId(namespace, type, id),
index: this.getIndexForType(type),
...getExpectedVersionProperties(version, preflightResult),
@ -1128,14 +1130,15 @@ export class SavedObjectsRepository {
doc,
},
_source_includes: ['namespace', 'namespaces', 'originId'],
},
{ ignore: [404] }
);
if (statusCode === 404) {
// see "404s from missing index" above
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}
require_alias: true,
})
.catch((err) => {
if (SavedObjectsErrorHelpers.isNotFoundError(err)) {
// see "404s from missing index" above
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}
throw err;
});
const { originId } = body.get._source;
let namespaces = [];
@ -1496,6 +1499,7 @@ export class SavedObjectsRepository {
refresh,
body: bulkUpdateParams,
_source_includes: ['originId'],
require_alias: true,
})
: undefined;
@ -1712,6 +1716,7 @@ export class SavedObjectsRepository {
id: raw._id,
index: this.getIndexForType(type),
refresh,
require_alias: true,
_source: 'true',
body: {
script: {
@ -1933,12 +1938,18 @@ export class SavedObjectsRepository {
}
}
function getBulkOperationError(error: { type: string; reason?: string }, type: string, id: string) {
function getBulkOperationError(
error: { type: string; reason?: string; index?: string },
type: string,
id: string
) {
switch (error.type) {
case 'version_conflict_engine_exception':
return errorContent(SavedObjectsErrorHelpers.createConflictError(type, id));
case 'document_missing_exception':
return errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id));
case 'index_not_found_exception':
return errorContent(SavedObjectsErrorHelpers.createIndexAliasNotFoundError(error.index!));
default:
return {
message: error.reason || JSON.stringify(error),

View file

@ -2336,6 +2336,8 @@ export class SavedObjectsErrorHelpers {
// (undocumented)
static createGenericNotFoundError(type?: string | null, id?: string | null): DecoratedError;
// (undocumented)
static createIndexAliasNotFoundError(alias: string): DecoratedError;
// (undocumented)
static createInvalidVersionError(versionInput?: string): DecoratedError;
// (undocumented)
static createTooManyRequestsError(type: string, id: string): DecoratedError;
@ -2354,6 +2356,8 @@ export class SavedObjectsErrorHelpers {
// (undocumented)
static decorateGeneralError(error: Error, reason?: string): DecoratedError;
// (undocumented)
static decorateIndexAliasNotFoundError(error: Error, alias: string): DecoratedError;
// (undocumented)
static decorateNotAuthorizedError(error: Error, reason?: string): DecoratedError;
// (undocumented)
static decorateRequestEntityTooLargeError(error: Error, reason?: string): DecoratedError;
@ -2370,6 +2374,8 @@ export class SavedObjectsErrorHelpers {
// (undocumented)
static isForbiddenError(error: Error | DecoratedError): boolean;
// (undocumented)
static isGeneralError(error: Error | DecoratedError): boolean;
// (undocumented)
static isInvalidVersionError(error: Error | DecoratedError): boolean;
// (undocumented)
static isNotAuthorizedError(error: Error | DecoratedError): boolean;

View file

@ -8,7 +8,7 @@
import { getServices, chance } from './lib';
export function docExistsSuite() {
export const docExistsSuite = (savedObjectsIndex: string) => () => {
async function setup(options: any = {}) {
const { initialSettings } = options;
@ -16,7 +16,7 @@ export function docExistsSuite() {
// delete the kibana index to ensure we start fresh
await callCluster('deleteByQuery', {
index: kbnServer.config.get('kibana.index'),
index: savedObjectsIndex,
body: {
conflicts: 'proceed',
query: { match_all: {} },
@ -212,4 +212,4 @@ export function docExistsSuite() {
});
});
});
}
};

View file

@ -8,7 +8,7 @@
import { getServices, chance } from './lib';
export function docMissingSuite() {
export const docMissingSuite = (savedObjectsIndex: string) => () => {
// ensure the kibana index has no documents
beforeEach(async () => {
const { kbnServer, callCluster } = getServices();
@ -22,7 +22,7 @@ export function docMissingSuite() {
// delete all docs from kibana index to ensure savedConfig is not found
await callCluster('deleteByQuery', {
index: kbnServer.config.get('kibana.index'),
index: savedObjectsIndex,
body: {
query: { match_all: {} },
},
@ -136,4 +136,4 @@ export function docMissingSuite() {
});
});
});
}
};

View file

@ -8,7 +8,7 @@
import { getServices, chance } from './lib';
export function docMissingAndIndexReadOnlySuite() {
export const docMissingAndIndexReadOnlySuite = (savedObjectsIndex: string) => () => {
// ensure the kibana index has no documents
beforeEach(async () => {
const { kbnServer, callCluster } = getServices();
@ -22,7 +22,7 @@ export function docMissingAndIndexReadOnlySuite() {
// delete all docs from kibana index to ensure savedConfig is not found
await callCluster('deleteByQuery', {
index: kbnServer.config.get('kibana.index'),
index: savedObjectsIndex,
body: {
query: { match_all: {} },
},
@ -30,7 +30,7 @@ export function docMissingAndIndexReadOnlySuite() {
// set the index to read only
await callCluster('indices.putSettings', {
index: kbnServer.config.get('kibana.index'),
index: savedObjectsIndex,
body: {
index: {
blocks: {
@ -42,11 +42,11 @@ export function docMissingAndIndexReadOnlySuite() {
});
afterEach(async () => {
const { kbnServer, callCluster } = getServices();
const { callCluster } = getServices();
// disable the read only block
await callCluster('indices.putSettings', {
index: kbnServer.config.get('kibana.index'),
index: savedObjectsIndex,
body: {
index: {
blocks: {
@ -142,4 +142,4 @@ export function docMissingAndIndexReadOnlySuite() {
});
});
});
}
};

View file

@ -6,20 +6,25 @@
* Public License, v 1.
*/
import { Env } from '@kbn/config';
import { REPO_ROOT } from '@kbn/dev-utils';
import { getEnvOptions } from '@kbn/config/target/mocks';
import { startServers, stopServers } from './lib';
import { docExistsSuite } from './doc_exists';
import { docMissingSuite } from './doc_missing';
import { docMissingAndIndexReadOnlySuite } from './doc_missing_and_index_read_only';
const kibanaVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version;
const savedObjectIndex = `.kibana_${kibanaVersion}_001`;
describe('uiSettings/routes', function () {
jest.setTimeout(10000);
beforeAll(startServers);
/* eslint-disable jest/valid-describe */
describe('doc missing', docMissingSuite);
describe('doc missing and index readonly', docMissingAndIndexReadOnlySuite);
describe('doc exists', docExistsSuite);
describe('doc missing', docMissingSuite(savedObjectIndex));
describe('doc missing and index readonly', docMissingAndIndexReadOnlySuite(savedObjectIndex));
describe('doc exists', docExistsSuite(savedObjectIndex));
/* eslint-enable jest/valid-describe */
afterAll(stopServers);
});

View file

@ -37,9 +37,6 @@ export async function startServers() {
adjustTimeout: (t) => jest.setTimeout(t),
settings: {
kbn: {
migrations: {
enableV2: false,
},
uiSettings: {
overrides: {
foo: 'bar',

View file

@ -40,7 +40,7 @@ const DEFAULTS_SETTINGS = {
},
logging: { silent: true },
plugins: {},
migrations: { skip: true },
migrations: { skip: false },
};
const DEFAULT_SETTINGS_WITH_CORE_PLUGINS = {

View file

@ -16,7 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
before(async () => {
await esArchiver.load('empty_kibana');
await esArchiver.emptyKibanaIndex();
await PageObjects.common.navigateToApp('kibanaOverview');
});
@ -25,7 +25,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
useActualUrl: true,
});
await PageObjects.home.removeSampleDataSet('flights');
await esArchiver.unload('empty_kibana');
});
it('Getting started view', async () => {

View file

@ -11,11 +11,15 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
const es = getService('es');
const MILLISECOND_IN_WEEK = 1000 * 60 * 60 * 24 * 7;
describe('sample data apis', () => {
before(async () => {
await esArchiver.emptyKibanaIndex();
});
describe('list', () => {
it('should return list of sample data sets with installed status', async () => {
const resp = await supertest.get(`/api/sample_data`).set('kbn-xsrf', 'kibana').expect(200);

View file

@ -97,10 +97,11 @@ export default function ({ getService }: FtrProviderContext) {
before(
async () =>
// just in case the kibana server has recreated it
await es.indices.delete({ index: '.kibana' }, { ignore: [404] })
await es.indices.delete({ index: '.kibana*' }, { ignore: [404] })
);
it('should return 200 with individual responses', async () =>
it('should return 200 with errors', async () => {
await new Promise((resolve) => setTimeout(resolve, 2000));
await supertest
.post('/api/saved_objects/_bulk_create')
.send(BULK_REQUESTS)
@ -109,38 +110,27 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.body).to.eql({
saved_objects: [
{
type: 'visualization',
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
updated_at: resp.body.saved_objects[0].updated_at,
version: resp.body.saved_objects[0].version,
attributes: {
title: 'An existing visualization',
id: BULK_REQUESTS[0].id,
type: BULK_REQUESTS[0].type,
error: {
error: 'Internal Server Error',
message: 'An internal server error occurred',
statusCode: 500,
},
references: [],
namespaces: ['default'],
migrationVersion: {
visualization: resp.body.saved_objects[0].migrationVersion.visualization,
},
coreMigrationVersion: KIBANA_VERSION, // updated from 1.2.3 to the latest kibana version
},
{
type: 'dashboard',
id: 'a01b2f57-fcfd-4864-b735-09e28f0d815e',
updated_at: resp.body.saved_objects[1].updated_at,
version: resp.body.saved_objects[1].version,
attributes: {
title: 'A great new dashboard',
id: BULK_REQUESTS[1].id,
type: BULK_REQUESTS[1].type,
error: {
error: 'Internal Server Error',
message: 'An internal server error occurred',
statusCode: 500,
},
references: [],
namespaces: ['default'],
migrationVersion: {
dashboard: resp.body.saved_objects[1].migrationVersion.dashboard,
},
coreMigrationVersion: KIBANA_VERSION,
},
],
});
}));
});
});
});
});
}

View file

@ -108,7 +108,7 @@ export default function ({ getService }: FtrProviderContext) {
before(
async () =>
// just in case the kibana server has recreated it
await es.indices.delete({ index: '.kibana' }, { ignore: [404] })
await es.indices.delete({ index: '.kibana*' }, { ignore: [404] })
);
it('should return 200 with individual responses', async () =>

View file

@ -235,10 +235,10 @@ export default function ({ getService }: FtrProviderContext) {
before(
async () =>
// just in case the kibana server has recreated it
await es.indices.delete({ index: '.kibana' }, { ignore: [404] })
await es.indices.delete({ index: '.kibana*' }, { ignore: [404] })
);
it('should return generic 404', async () => {
it('should return 200 with errors', async () => {
const response = await supertest
.put(`/api/saved_objects/_bulk_update`)
.send([
@ -267,9 +267,9 @@ export default function ({ getService }: FtrProviderContext) {
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
type: 'visualization',
error: {
statusCode: 404,
error: 'Not Found',
message: 'Saved object [visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab] not found',
statusCode: 500,
error: 'Internal Server Error',
message: 'An internal server error occurred',
},
});
@ -277,9 +277,9 @@ export default function ({ getService }: FtrProviderContext) {
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
type: 'dashboard',
error: {
statusCode: 404,
error: 'Not Found',
message: 'Saved object [dashboard/be3733a0-9efe-11e7-acb3-3dab96693fab] not found',
statusCode: 500,
error: 'Internal Server Error',
message: 'An internal server error occurred',
},
});
});

View file

@ -82,10 +82,10 @@ export default function ({ getService }: FtrProviderContext) {
before(
async () =>
// just in case the kibana server has recreated it
await es.indices.delete({ index: '.kibana' }, { ignore: [404] })
await es.indices.delete({ index: '.kibana*' }, { ignore: [404] })
);
it('should return 200 and create kibana index', async () => {
it('should return 500 and not auto-create saved objects index', async () => {
await supertest
.post(`/api/saved_objects/visualization`)
.send({
@ -93,50 +93,16 @@ export default function ({ getService }: FtrProviderContext) {
title: 'My favorite vis',
},
})
.expect(200)
.expect(500)
.then((resp) => {
// loose uuid validation
expect(resp.body)
.to.have.property('id')
.match(/^[0-9a-f-]{36}$/);
// loose ISO8601 UTC time with milliseconds validation
expect(resp.body)
.to.have.property('updated_at')
.match(/^[\d-]{10}T[\d:\.]{12}Z$/);
expect(resp.body).to.eql({
id: resp.body.id,
type: 'visualization',
migrationVersion: resp.body.migrationVersion,
coreMigrationVersion: KIBANA_VERSION,
updated_at: resp.body.updated_at,
version: resp.body.version,
attributes: {
title: 'My favorite vis',
},
references: [],
namespaces: ['default'],
error: 'Internal Server Error',
message: 'An internal server error occurred.',
statusCode: 500,
});
expect(resp.body.migrationVersion).to.be.ok();
});
expect((await es.indices.exists({ index: '.kibana' })).body).to.be(true);
});
it('result should have the latest coreMigrationVersion', async () => {
await supertest
.post(`/api/saved_objects/visualization`)
.send({
attributes: {
title: 'My favorite vis',
},
coreMigrationVersion: '1.2.3',
})
.expect(200)
.then((resp) => {
expect(resp.body.coreMigrationVersion).to.eql(KIBANA_VERSION);
});
expect((await es.indices.exists({ index: '.kibana' })).body).to.be(false);
});
});
});

View file

@ -44,7 +44,7 @@ export default function ({ getService }: FtrProviderContext) {
before(
async () =>
// just in case the kibana server has recreated it
await es.indices.delete({ index: '.kibana' }, { ignore: [404] })
await es.indices.delete({ index: '.kibana*' }, { ignore: [404] })
);
it('returns generic 404 when kibana index is missing', async () =>

View file

@ -534,7 +534,7 @@ export default function ({ getService }: FtrProviderContext) {
before(
async () =>
// just in case the kibana server has recreated it
await es.indices.delete({ index: '.kibana' }, { ignore: [404] })
await es.indices.delete({ index: '.kibana*' }, { ignore: [404] })
);
it('should return empty response', async () => {

View file

@ -40,7 +40,7 @@ export default function ({ getService }: FtrProviderContext) {
{
type: 'visualization',
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
version: 'WzIsMV0=',
version: 'WzE4LDJd',
attributes: {
title: 'Count of requests',
},
@ -137,7 +137,7 @@ export default function ({ getService }: FtrProviderContext) {
{
type: 'visualization',
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
version: 'WzIsMV0=',
version: 'WzE4LDJd',
attributes: {
title: 'Count of requests',
},
@ -174,7 +174,7 @@ export default function ({ getService }: FtrProviderContext) {
{
type: 'visualization',
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
version: 'WzIsMV0=',
version: 'WzE4LDJd',
attributes: {
title: 'Count of requests',
},
@ -209,7 +209,7 @@ export default function ({ getService }: FtrProviderContext) {
score: 0,
type: 'visualization',
updated_at: '2017-09-21T18:51:23.794Z',
version: 'WzYsMV0=',
version: 'WzIyLDJd',
},
],
});
@ -256,7 +256,7 @@ export default function ({ getService }: FtrProviderContext) {
migrationVersion: resp.body.saved_objects[0].migrationVersion,
coreMigrationVersion: KIBANA_VERSION,
updated_at: '2017-09-21T18:51:23.794Z',
version: 'WzIsMV0=',
version: 'WzE4LDJd',
},
],
});
@ -426,11 +426,11 @@ export default function ({ getService }: FtrProviderContext) {
}));
});
describe.skip('without kibana index', () => {
describe('without kibana index', () => {
before(
async () =>
// just in case the kibana server has recreated it
await es.indices.delete({ index: '.kibana' }, { ignore: [404] })
await es.indices.delete({ index: '.kibana*' }, { ignore: [404] })
);
it('should return 200 with empty response', async () =>

View file

@ -78,7 +78,7 @@ export default function ({ getService }: FtrProviderContext) {
before(
async () =>
// just in case the kibana server has recreated it
await es.indices.delete({ index: '.kibana' }, { ignore: [404] })
await es.indices.delete({ index: '.kibana*' }, { ignore: [404] })
);
it('should return basic 404 without mentioning index', async () =>

View file

@ -13,6 +13,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
const es = getService('legacyEs');
describe('resolve_import_errors', () => {
// mock success results including metadata
@ -34,7 +35,14 @@ export default function ({ getService }: FtrProviderContext) {
describe('without kibana index', () => {
// Cleanup data that got created in import
after(() => esArchiver.unload('saved_objects/basic'));
before(
async () =>
// just in case the kibana server has recreated it
await es.indices.delete({
index: '.kibana*',
ignore: [404],
})
);
it('should return 200 and import nothing when empty parameters are passed in', async () => {
await supertest
@ -51,7 +59,7 @@ export default function ({ getService }: FtrProviderContext) {
});
});
it('should return 200 and import everything when overwrite parameters contains all objects', async () => {
it('should return 200 with internal server errors', async () => {
await supertest
.post('/api/saved_objects/_resolve_import_errors')
.field(
@ -78,12 +86,42 @@ export default function ({ getService }: FtrProviderContext) {
.expect(200)
.then((resp) => {
expect(resp.body).to.eql({
success: true,
successCount: 3,
successResults: [
{ ...indexPattern, overwrite: true },
{ ...visualization, overwrite: true },
{ ...dashboard, overwrite: true },
successCount: 0,
success: false,
errors: [
{
...indexPattern,
...{ title: indexPattern.meta.title },
overwrite: true,
error: {
statusCode: 500,
error: 'Internal Server Error',
message: 'An internal server error occurred',
type: 'unknown',
},
},
{
...visualization,
...{ title: visualization.meta.title },
overwrite: true,
error: {
statusCode: 500,
error: 'Internal Server Error',
message: 'An internal server error occurred',
type: 'unknown',
},
},
{
...dashboard,
...{ title: dashboard.meta.title },
overwrite: true,
error: {
statusCode: 500,
error: 'Internal Server Error',
message: 'An internal server error occurred',
type: 'unknown',
},
},
],
warnings: [],
});

View file

@ -121,10 +121,10 @@ export default function ({ getService }: FtrProviderContext) {
before(
async () =>
// just in case the kibana server has recreated it
await es.indices.delete({ index: '.kibana' }, { ignore: [404] })
await es.indices.delete({ index: '.kibana*' }, { ignore: [404] })
);
it('should return generic 404', async () =>
it('should return 500', async () =>
await supertest
.put(`/api/saved_objects/visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab`)
.send({
@ -132,13 +132,12 @@ export default function ({ getService }: FtrProviderContext) {
title: 'My second favorite vis',
},
})
.expect(404)
.expect(500)
.then((resp) => {
expect(resp.body).eql({
statusCode: 404,
error: 'Not Found',
message:
'Saved object [visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab] not found',
statusCode: 500,
error: 'Internal Server Error',
message: 'An internal server error occurred.',
});
}));
});

View file

@ -42,7 +42,7 @@ export default function ({ getService }: FtrProviderContext) {
{
type: 'visualization',
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
version: 'WzIsMV0=',
version: 'WzE4LDJd',
attributes: {
title: 'Count of requests',
},
@ -184,7 +184,7 @@ export default function ({ getService }: FtrProviderContext) {
before(
async () =>
// just in case the kibana server has recreated it
await es.indices.delete({ index: '.kibana' }, { ignore: [404] })
await es.indices.delete({ index: '.kibana*' }, { ignore: [404] })
);
it('should return 200 with empty response', async () =>

View file

@ -45,7 +45,7 @@ export default function ({ getService }: FtrProviderContext) {
before(
async () =>
// just in case the kibana server has recreated it
await es.indices.delete({ index: '.kibana' }, { ignore: [404] })
await es.indices.delete({ index: '.kibana*' }, { ignore: [404] })
);
it('should return 404 for object that no longer exists', async () =>

View file

@ -17,6 +17,7 @@ export default function ({ getService }: FtrProviderContext) {
describe('search', () => {
before(async () => {
await esArchiver.emptyKibanaIndex();
await esArchiver.loadIfNeeded('../../../functional/fixtures/es_archiver/logstash_functional');
});

View file

@ -14,10 +14,13 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default function optInTest({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const kibanaServer = getService('kibanaServer');
const esArchiver = getService('esArchiver');
describe('/api/telemetry/v2/optIn API', () => {
let defaultAttributes: TelemetrySavedObjectAttributes;
let kibanaVersion: any;
before(async () => {
await esArchiver.emptyKibanaIndex();
const kibanaVersionAccessor = kibanaServer.version;
kibanaVersion = await kibanaVersionAccessor.get();
defaultAttributes =

View file

@ -177,6 +177,7 @@ export default function ({ getService }: FtrProviderContext) {
describe('basic behaviour', () => {
let savedObjectIds: string[] = [];
before('create application usage entries', async () => {
await esArchiver.emptyKibanaIndex();
savedObjectIds = await Promise.all([
createSavedObject(),
createSavedObject('appView1'),

View file

@ -13,6 +13,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
const es = getService('es');
const createUiCounterEvent = (eventName: string, type: UiCounterMetricType, count = 1) => ({
@ -23,6 +24,10 @@ export default function ({ getService }: FtrProviderContext) {
});
describe('UI Counters API', () => {
before(async () => {
await esArchiver.emptyKibanaIndex();
});
const dayDate = moment().format('DDMMYYYY');
it('stores ui counter events in savedObjects', async () => {

View file

@ -13,6 +13,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
const es = getService('es');
const createStatsMetric = (
@ -34,6 +35,10 @@ export default function ({ getService }: FtrProviderContext) {
});
describe('ui_metric savedObject data', () => {
before(async () => {
await esArchiver.emptyKibanaIndex();
});
it('increments the count field in the document defined by the {app}/{action_type} path', async () => {
const reportManager = new ReportManager();
const uiStatsMetric = createStatsMetric('myEvent');

View file

@ -61,8 +61,6 @@ export default function () {
...(!!process.env.CODE_COVERAGE
? [`--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'coverage')}`]
: []),
// Disable v2 migrations in tests for now
'--migrations.enableV2=false',
],
},
services,

View file

@ -6,7 +6,7 @@
* Public License, v 1.
*/
const ES_ARCHIVER_LOAD_METHODS = ['load', 'loadIfNeeded', 'unload'];
const ES_ARCHIVER_LOAD_METHODS = ['load', 'loadIfNeeded', 'unload', 'emptyKibanaIndex'];
const KIBANA_INDEX = '.kibana';
export function extendEsArchiver({ esArchiver, kibanaServer, retry, defaults }) {
@ -25,7 +25,7 @@ export function extendEsArchiver({ esArchiver, kibanaServer, retry, defaults })
const statsKeys = Object.keys(stats);
const kibanaKeys = statsKeys.filter(
// this also matches stats keys like '.kibana_1' and '.kibana_2,.kibana_1'
(key) => key.includes(KIBANA_INDEX) && (stats[key].created || stats[key].deleted)
(key) => key.includes(KIBANA_INDEX) && stats[key].created
);
// if the kibana index was created by the esArchiver then update the uiSettings

View file

@ -27,9 +27,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe.skip('import objects', function describeIndexTests() {
describe('.ndjson file', () => {
beforeEach(async function () {
await esArchiver.load('management');
await kibanaServer.uiSettings.replace({});
await PageObjects.settings.navigateTo();
await esArchiver.load('management');
await PageObjects.settings.clickKibanaSavedObjects();
});
@ -213,10 +213,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('.json file', () => {
beforeEach(async function () {
// delete .kibana index and then wait for Kibana to re-create it
await esArchiver.load('saved_objects_imports');
await kibanaServer.uiSettings.replace({});
await PageObjects.settings.navigateTo();
await esArchiver.load('saved_objects_imports');
await PageObjects.settings.clickKibanaSavedObjects();
});

View file

@ -12,10 +12,11 @@ export default function ({ getService, getPageObjects }) {
const kibanaServer = getService('kibanaServer');
const retry = getService('retry');
const PageObjects = getPageObjects(['settings']);
const esArchiver = getService('esArchiver');
describe('index pattern filter', function describeIndexTests() {
before(async function () {
// delete .kibana index and then wait for Kibana to re-create it
await esArchiver.emptyKibanaIndex();
await kibanaServer.uiSettings.replace({});
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaIndexPatterns();

View file

@ -19,7 +19,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
describe('index pattern empty view', () => {
before(async () => {
await esArchiver.load('empty_kibana');
await esArchiver.emptyKibanaIndex();
await esArchiver.unload('logstash_functional');
await esArchiver.unload('makelogs');
await kibanaServer.uiSettings.replace({});
@ -27,7 +27,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
after(async () => {
await esArchiver.unload('empty_kibana');
await esArchiver.loadIfNeeded('makelogs');
// @ts-expect-error
await es.transport.request({

View file

@ -18,14 +18,13 @@ export default function ({ getService, getPageObjects }) {
describe('mgmt saved objects', function describeIndexTests() {
beforeEach(async function () {
await esArchiver.load('empty_kibana');
await esArchiver.emptyKibanaIndex();
await esArchiver.load('discover');
await PageObjects.settings.navigateTo();
});
afterEach(async function () {
await esArchiver.unload('discover');
await esArchiver.load('empty_kibana');
});
it('should import saved objects mgmt', async function () {

View file

@ -20,6 +20,7 @@ export default function ({ getService, getPageObjects }) {
const EXPECTED_FIELD_COUNT = '10006';
before(async function () {
await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader']);
await esArchiver.emptyKibanaIndex();
await esArchiver.loadIfNeeded('large_fields');
await PageObjects.settings.createIndexPattern('testhuge', 'date');
});

View file

@ -14,13 +14,11 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
describe('management', function () {
before(async () => {
await esArchiver.unload('logstash_functional');
await esArchiver.load('empty_kibana');
await esArchiver.loadIfNeeded('makelogs');
});
after(async () => {
await esArchiver.unload('makelogs');
await esArchiver.unload('empty_kibana');
});
describe('', function () {

View file

@ -12,7 +12,6 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const find = getService('find');
const security = getService('security');
const { visualize, visEditor } = getPageObjects(['visualize', 'visEditor']);
@ -53,7 +52,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await esArchiver.loadIfNeeded('logstash_functional');
await esArchiver.loadIfNeeded('long_window_logstash');
await esArchiver.load('visualize');
await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' });
await security.testUser.restoreDefaults();
});
});

View file

@ -19,6 +19,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
const find = getService('find');
const retry = getService('retry');
const deployment = getService('deployment');
const esArchiver = getService('esArchiver');
const loadingScreenNotShown = async () =>
expect(await testSubjects.exists('kbnLoadingMessage')).to.be(false);
@ -50,6 +51,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
describe('ui applications', function describeIndexTests() {
before(async () => {
await esArchiver.emptyKibanaIndex();
await PageObjects.common.navigateToApp('foo');
});

View file

@ -12,8 +12,12 @@ import '../../plugins/core_provider_plugin/types';
export default function ({ getService }: PluginFunctionalProviderContext) {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
describe('index patterns', function () {
before(async () => {
await esArchiver.emptyKibanaIndex();
});
let indexPatternId = '';
it('can create an index pattern', async () => {

View file

@ -10,10 +10,15 @@ import path from 'path';
import expect from '@kbn/expect';
import { PluginFunctionalProviderContext } from '../../services';
export default function ({ getPageObjects }: PluginFunctionalProviderContext) {
export default function ({ getPageObjects, getService }: PluginFunctionalProviderContext) {
const PageObjects = getPageObjects(['common', 'settings', 'header', 'savedObjects']);
const esArchiver = getService('esArchiver');
describe('import warnings', () => {
before(async () => {
await esArchiver.emptyKibanaIndex();
});
beforeEach(async () => {
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaSavedObjects();

View file

@ -31,6 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
before(async () => {
await browser.setLocalStorageItem('insecureClusterWarningVisibility', '');
await esArchiver.unload('hamlet');
await esArchiver.emptyKibanaIndex();
});
it('should not warn when the cluster contains no user data', async () => {

View file

@ -11,10 +11,6 @@ import { setupSpacesAndUsers, tearDown } from '..';
export default function alertingTests({ loadTestFile, getService }: FtrProviderContext) {
describe('Alerts', () => {
describe('legacy alerts', () => {
before(async () => {
await setupSpacesAndUsers(getService);
});
after(async () => {
await tearDown(getService);
});

View file

@ -12,6 +12,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const browser = getService('browser');
const kibanaServer = getService('kibanaServer');
const esArchiver = getService('esArchiver');
const log = getService('log');
const pieChart = getService('pieChart');
const find = getService('find');
@ -29,6 +30,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('sample data dashboard', function describeIndexTests() {
before(async () => {
await esArchiver.emptyKibanaIndex();
await PageObjects.common.sleep(5000);
await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', {
useActualUrl: true,

File diff suppressed because one or more lines are too long

View file

@ -32,7 +32,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
kbnTestServer: {
...apiConfig.get('kbnTestServer'),
serverArgs: [
`--migrations.enableV2=false`,
`--elasticsearch.hosts=${formatUrl(esTestConfig.getUrlParts())}`,
`--logging.json=false`,
`--server.maxPayloadBytes=1679958`,