Co-authored-by: Rudolf Meijering <skaapgif@gmail.com>
This commit is contained in:
parent
f2a4168dca
commit
863ade4670
|
@ -18,7 +18,7 @@ export interface SavedObjectMigrationMap
|
|||
|
||||
|
||||
```typescript
|
||||
const migrations: SavedObjectMigrationMap = {
|
||||
const migrationsMap: SavedObjectMigrationMap = {
|
||||
'1.0.0': migrateToV1,
|
||||
'2.1.0': migrateToV21
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ export const myType: SavedObjectsType = {
|
|||
},
|
||||
migrations: {
|
||||
'2.0.0': migrations.migrateToV2,
|
||||
'2.1.0': migrations.migrateToV2_1
|
||||
'2.1.0': migrations.migrateToV2_1,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ This is only internal for now, and will only be public when we expose the regist
|
|||
| [indexPattern](./kibana-plugin-core-server.savedobjectstype.indexpattern.md) | <code>string</code> | If defined, the type instances will be stored in the given index instead of the default one. |
|
||||
| [management](./kibana-plugin-core-server.savedobjectstype.management.md) | <code>SavedObjectsTypeManagementDefinition</code> | An optional [saved objects management section](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.md) definition for the type. |
|
||||
| [mappings](./kibana-plugin-core-server.savedobjectstype.mappings.md) | <code>SavedObjectsTypeMappingDefinition</code> | The [mapping definition](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) for the type. |
|
||||
| [migrations](./kibana-plugin-core-server.savedobjectstype.migrations.md) | <code>SavedObjectMigrationMap</code> | An optional map of [migrations](./kibana-plugin-core-server.savedobjectmigrationfn.md) to be used to migrate the type. |
|
||||
| [migrations](./kibana-plugin-core-server.savedobjectstype.migrations.md) | <code>SavedObjectMigrationMap | (() => SavedObjectMigrationMap)</code> | An optional map of [migrations](./kibana-plugin-core-server.savedobjectmigrationfn.md) or a function returning a map of [migrations](./kibana-plugin-core-server.savedobjectmigrationfn.md) to be used to migrate the type. |
|
||||
| [name](./kibana-plugin-core-server.savedobjectstype.name.md) | <code>string</code> | The name of the type, which is also used as the internal id. |
|
||||
| [namespaceType](./kibana-plugin-core-server.savedobjectstype.namespacetype.md) | <code>SavedObjectsNamespaceType</code> | The [namespace type](./kibana-plugin-core-server.savedobjectsnamespacetype.md) for the type. |
|
||||
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
|
||||
## SavedObjectsType.migrations property
|
||||
|
||||
An optional map of [migrations](./kibana-plugin-core-server.savedobjectmigrationfn.md) to be used to migrate the type.
|
||||
An optional map of [migrations](./kibana-plugin-core-server.savedobjectmigrationfn.md) or a function returning a map of [migrations](./kibana-plugin-core-server.savedobjectmigrationfn.md) to be used to migrate the type.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
migrations?: SavedObjectMigrationMap;
|
||||
migrations?: SavedObjectMigrationMap | (() => SavedObjectMigrationMap);
|
||||
```
|
||||
|
|
|
@ -52,50 +52,84 @@ describe('DocumentMigrator', () => {
|
|||
};
|
||||
}
|
||||
|
||||
it('validates individual migration definitions', () => {
|
||||
const invalidDefinition = {
|
||||
kibanaVersion: '3.2.3',
|
||||
typeRegistry: createRegistry({
|
||||
name: 'foo',
|
||||
migrations: _.noop as any,
|
||||
}),
|
||||
log: mockLogger,
|
||||
};
|
||||
expect(() => new DocumentMigrator(invalidDefinition)).toThrow(
|
||||
/Migration for type foo should be an object/i
|
||||
const createDefinition = (migrations: any) => ({
|
||||
kibanaVersion: '3.2.3',
|
||||
typeRegistry: createRegistry({
|
||||
name: 'foo',
|
||||
migrations: migrations as any,
|
||||
}),
|
||||
log: mockLogger,
|
||||
});
|
||||
|
||||
it('validates migration definition', () => {
|
||||
expect(() => new DocumentMigrator(createDefinition(() => {}))).not.toThrow();
|
||||
expect(() => new DocumentMigrator(createDefinition({}))).not.toThrow();
|
||||
expect(() => new DocumentMigrator(createDefinition(123))).toThrow(
|
||||
/Migration for type foo should be an object or a function/i
|
||||
);
|
||||
});
|
||||
|
||||
it('validates individual migration semvers', () => {
|
||||
const invalidDefinition = {
|
||||
kibanaVersion: '3.2.3',
|
||||
typeRegistry: createRegistry({
|
||||
name: 'foo',
|
||||
migrations: {
|
||||
bar: (doc) => doc,
|
||||
},
|
||||
}),
|
||||
log: mockLogger,
|
||||
};
|
||||
expect(() => new DocumentMigrator(invalidDefinition)).toThrow(
|
||||
/Expected all properties to be semvers/i
|
||||
);
|
||||
describe('#prepareMigrations', () => {
|
||||
it('validates individual migration definitions', () => {
|
||||
const invalidMigrator = new DocumentMigrator(createDefinition(() => 123));
|
||||
const voidMigrator = new DocumentMigrator(createDefinition(() => {}));
|
||||
const emptyObjectMigrator = new DocumentMigrator(createDefinition(() => ({})));
|
||||
|
||||
expect(invalidMigrator.prepareMigrations).toThrow(
|
||||
/Migrations map for type foo should be an object/i
|
||||
);
|
||||
expect(voidMigrator.prepareMigrations).not.toThrow();
|
||||
expect(emptyObjectMigrator.prepareMigrations).not.toThrow();
|
||||
});
|
||||
|
||||
it('validates individual migration semvers', () => {
|
||||
const withInvalidVersion = {
|
||||
bar: (doc: any) => doc,
|
||||
'1.2.3': (doc: any) => doc,
|
||||
};
|
||||
const migrationFn = new DocumentMigrator(createDefinition(() => withInvalidVersion));
|
||||
const migrationObj = new DocumentMigrator(createDefinition(withInvalidVersion));
|
||||
|
||||
expect(migrationFn.prepareMigrations).toThrow(/Expected all properties to be semvers/i);
|
||||
expect(migrationObj.prepareMigrations).toThrow(/Expected all properties to be semvers/i);
|
||||
});
|
||||
|
||||
it('validates the migration function', () => {
|
||||
const invalidVersionFunction = { '1.2.3': 23 as any };
|
||||
const migrationFn = new DocumentMigrator(createDefinition(() => invalidVersionFunction));
|
||||
const migrationObj = new DocumentMigrator(createDefinition(invalidVersionFunction));
|
||||
|
||||
expect(migrationFn.prepareMigrations).toThrow(/expected a function, but got 23/i);
|
||||
expect(migrationObj.prepareMigrations).toThrow(/expected a function, but got 23/i);
|
||||
});
|
||||
it('validates definitions with migrations: Function | Objects', () => {
|
||||
const validMigrationMap = { '1.2.3': () => {} };
|
||||
const migrationFn = new DocumentMigrator(createDefinition(() => validMigrationMap));
|
||||
const migrationObj = new DocumentMigrator(createDefinition(validMigrationMap));
|
||||
expect(migrationFn.prepareMigrations).not.toThrow();
|
||||
expect(migrationObj.prepareMigrations).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
it('validates the migration function', () => {
|
||||
const invalidDefinition = {
|
||||
kibanaVersion: '3.2.3',
|
||||
it('throws if #prepareMigrations is not called before #migrate is called', () => {
|
||||
const migrator = new DocumentMigrator({
|
||||
...testOpts(),
|
||||
typeRegistry: createRegistry({
|
||||
name: 'foo',
|
||||
name: 'user',
|
||||
migrations: {
|
||||
'1.2.3': 23 as any,
|
||||
'1.2.3': setAttr('attributes.name', 'Chris'),
|
||||
},
|
||||
}),
|
||||
log: mockLogger,
|
||||
};
|
||||
expect(() => new DocumentMigrator(invalidDefinition)).toThrow(
|
||||
/expected a function, but got 23/i
|
||||
);
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
migrator.migrate({
|
||||
id: 'me',
|
||||
type: 'user',
|
||||
attributes: { name: 'Christopher' },
|
||||
migrationVersion: {},
|
||||
})
|
||||
).toThrow(/Migrations are not ready. Make sure prepareMigrations is called first./i);
|
||||
});
|
||||
|
||||
it('migrates type and attributes', () => {
|
||||
|
@ -108,6 +142,8 @@ describe('DocumentMigrator', () => {
|
|||
},
|
||||
}),
|
||||
});
|
||||
migrator.prepareMigrations();
|
||||
|
||||
const actual = migrator.migrate({
|
||||
id: 'me',
|
||||
type: 'user',
|
||||
|
@ -141,6 +177,7 @@ describe('DocumentMigrator', () => {
|
|||
attributes: {},
|
||||
migrationVersion: {},
|
||||
};
|
||||
migrator.prepareMigrations();
|
||||
const migratedDoc = migrator.migrate(originalDoc);
|
||||
expect(_.get(originalDoc, 'attributes.name')).toBeUndefined();
|
||||
expect(_.get(migratedDoc, 'attributes.name')).toBe('Mike');
|
||||
|
@ -156,6 +193,7 @@ describe('DocumentMigrator', () => {
|
|||
},
|
||||
}),
|
||||
});
|
||||
migrator.prepareMigrations();
|
||||
const actual = migrator.migrate({
|
||||
id: 'me',
|
||||
type: 'user',
|
||||
|
@ -196,6 +234,7 @@ describe('DocumentMigrator', () => {
|
|||
}
|
||||
),
|
||||
});
|
||||
migrator.prepareMigrations();
|
||||
const actual = migrator.migrate({
|
||||
id: 'me',
|
||||
type: 'user',
|
||||
|
@ -233,6 +272,7 @@ describe('DocumentMigrator', () => {
|
|||
}
|
||||
),
|
||||
});
|
||||
migrator.prepareMigrations();
|
||||
const actual = migrator.migrate({
|
||||
id: 'me',
|
||||
type: 'user',
|
||||
|
@ -263,6 +303,7 @@ describe('DocumentMigrator', () => {
|
|||
},
|
||||
}),
|
||||
});
|
||||
migrator.prepareMigrations();
|
||||
const actual = migrator.migrate({
|
||||
id: 'smelly',
|
||||
type: 'dog',
|
||||
|
@ -282,6 +323,7 @@ describe('DocumentMigrator', () => {
|
|||
...testOpts(),
|
||||
kibanaVersion: '8.0.1',
|
||||
});
|
||||
migrator.prepareMigrations();
|
||||
expect(() =>
|
||||
migrator.migrate({
|
||||
id: 'smelly',
|
||||
|
@ -304,6 +346,7 @@ describe('DocumentMigrator', () => {
|
|||
},
|
||||
}),
|
||||
});
|
||||
migrator.prepareMigrations();
|
||||
expect(() =>
|
||||
migrator.migrate({
|
||||
id: 'fleabag',
|
||||
|
@ -329,6 +372,7 @@ describe('DocumentMigrator', () => {
|
|||
},
|
||||
}),
|
||||
});
|
||||
migrator.prepareMigrations();
|
||||
const actual = migrator.migrate({
|
||||
id: 'smelly',
|
||||
type: 'dog',
|
||||
|
@ -361,6 +405,7 @@ describe('DocumentMigrator', () => {
|
|||
}
|
||||
),
|
||||
});
|
||||
migrator.prepareMigrations();
|
||||
const actual = migrator.migrate({
|
||||
id: 'smelly',
|
||||
type: 'dog',
|
||||
|
@ -396,6 +441,7 @@ describe('DocumentMigrator', () => {
|
|||
}
|
||||
),
|
||||
});
|
||||
migrator.prepareMigrations();
|
||||
const actual = migrator.migrate({
|
||||
id: 'smelly',
|
||||
type: 'foo',
|
||||
|
@ -430,6 +476,7 @@ describe('DocumentMigrator', () => {
|
|||
}
|
||||
),
|
||||
});
|
||||
migrator.prepareMigrations();
|
||||
const actual = migrator.migrate({
|
||||
id: 'smelly',
|
||||
type: 'dog',
|
||||
|
@ -454,6 +501,7 @@ describe('DocumentMigrator', () => {
|
|||
},
|
||||
}),
|
||||
});
|
||||
migrator.prepareMigrations();
|
||||
|
||||
expect(() =>
|
||||
migrator.migrate({
|
||||
|
@ -477,6 +525,7 @@ describe('DocumentMigrator', () => {
|
|||
},
|
||||
}),
|
||||
});
|
||||
migrator.prepareMigrations();
|
||||
expect(() =>
|
||||
migrator.migrate({
|
||||
id: 'smelly',
|
||||
|
@ -506,6 +555,7 @@ describe('DocumentMigrator', () => {
|
|||
},
|
||||
}),
|
||||
});
|
||||
migrator.prepareMigrations();
|
||||
const actual = migrator.migrate({
|
||||
id: 'smelly',
|
||||
type: 'cat',
|
||||
|
@ -530,6 +580,7 @@ describe('DocumentMigrator', () => {
|
|||
},
|
||||
}),
|
||||
});
|
||||
migrator.prepareMigrations();
|
||||
const actual = migrator.migrate({
|
||||
id: 'smelly',
|
||||
type: 'cat',
|
||||
|
@ -565,6 +616,7 @@ describe('DocumentMigrator', () => {
|
|||
migrationVersion: {},
|
||||
};
|
||||
try {
|
||||
migrator.prepareMigrations();
|
||||
migrator.migrate(_.cloneDeep(failedDoc));
|
||||
expect('Did not throw').toEqual('But it should have!');
|
||||
} catch (error) {
|
||||
|
@ -597,13 +649,14 @@ describe('DocumentMigrator', () => {
|
|||
attributes: {},
|
||||
migrationVersion: {},
|
||||
};
|
||||
migrator.prepareMigrations();
|
||||
migrator.migrate(doc);
|
||||
expect(loggingSystemMock.collect(mockLoggerFactory).info[0][0]).toEqual(logTestMsg);
|
||||
expect(loggingSystemMock.collect(mockLoggerFactory).warn[1][0]).toEqual(logTestMsg);
|
||||
});
|
||||
|
||||
test('extracts the latest migration version info', () => {
|
||||
const { migrationVersion } = new DocumentMigrator({
|
||||
const migrator = new DocumentMigrator({
|
||||
...testOpts(),
|
||||
typeRegistry: createRegistry(
|
||||
{
|
||||
|
@ -624,7 +677,8 @@ describe('DocumentMigrator', () => {
|
|||
),
|
||||
});
|
||||
|
||||
expect(migrationVersion).toEqual({
|
||||
migrator.prepareMigrations();
|
||||
expect(migrator.migrationVersion).toEqual({
|
||||
aaa: '10.4.0',
|
||||
bbb: '3.2.3',
|
||||
});
|
||||
|
|
|
@ -69,7 +69,7 @@ import { SavedObjectUnsanitizedDoc } from '../../serialization';
|
|||
import { SavedObjectsMigrationVersion } from '../../types';
|
||||
import { MigrationLogger } from './migration_logger';
|
||||
import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry';
|
||||
import { SavedObjectMigrationFn } from '../types';
|
||||
import { SavedObjectMigrationFn, SavedObjectMigrationMap } from '../types';
|
||||
|
||||
export type TransformFn = (doc: SavedObjectUnsanitizedDoc) => SavedObjectUnsanitizedDoc;
|
||||
|
||||
|
@ -94,6 +94,7 @@ interface ActiveMigrations {
|
|||
*/
|
||||
export interface VersionedTransformer {
|
||||
migrationVersion: SavedObjectsMigrationVersion;
|
||||
prepareMigrations: () => void;
|
||||
migrate: TransformFn;
|
||||
}
|
||||
|
||||
|
@ -101,8 +102,9 @@ export interface VersionedTransformer {
|
|||
* A concrete implementation of the VersionedTransformer interface.
|
||||
*/
|
||||
export class DocumentMigrator implements VersionedTransformer {
|
||||
private migrations: ActiveMigrations;
|
||||
private transformDoc: TransformFn;
|
||||
private documentMigratorOptions: DocumentMigratorOptions;
|
||||
private migrations?: ActiveMigrations;
|
||||
private transformDoc?: TransformFn;
|
||||
|
||||
/**
|
||||
* Creates an instance of DocumentMigrator.
|
||||
|
@ -115,12 +117,7 @@ export class DocumentMigrator implements VersionedTransformer {
|
|||
*/
|
||||
constructor({ typeRegistry, kibanaVersion, log }: DocumentMigratorOptions) {
|
||||
validateMigrationDefinition(typeRegistry);
|
||||
|
||||
this.migrations = buildActiveMigrations(typeRegistry, log);
|
||||
this.transformDoc = buildDocumentTransform({
|
||||
kibanaVersion,
|
||||
migrations: this.migrations,
|
||||
});
|
||||
this.documentMigratorOptions = { typeRegistry, kibanaVersion, log };
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -131,9 +128,28 @@ export class DocumentMigrator implements VersionedTransformer {
|
|||
* @memberof DocumentMigrator
|
||||
*/
|
||||
public get migrationVersion(): SavedObjectsMigrationVersion {
|
||||
if (!this.migrations) {
|
||||
throw new Error('Migrations are not ready. Make sure prepareMigrations is called first.');
|
||||
}
|
||||
return _.mapValues(this.migrations, ({ latestVersion }) => latestVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares active migrations and document transformer function.
|
||||
*
|
||||
* @returns {void}
|
||||
* @memberof DocumentMigrator
|
||||
*/
|
||||
|
||||
public prepareMigrations = () => {
|
||||
const { typeRegistry, kibanaVersion, log } = this.documentMigratorOptions;
|
||||
this.migrations = buildActiveMigrations(typeRegistry, log);
|
||||
this.transformDoc = buildDocumentTransform({
|
||||
kibanaVersion,
|
||||
migrations: this.migrations,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Migrates a document to the latest version.
|
||||
*
|
||||
|
@ -142,6 +158,10 @@ export class DocumentMigrator implements VersionedTransformer {
|
|||
* @memberof DocumentMigrator
|
||||
*/
|
||||
public migrate = (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc => {
|
||||
if (!this.migrations || !this.transformDoc) {
|
||||
throw new Error('Migrations are not ready. Make sure prepareMigrations is called first.');
|
||||
}
|
||||
|
||||
// Clone the document to prevent accidental mutations on the original data
|
||||
// Ex: Importing sample data that is cached at import level, migrations would
|
||||
// execute on mutated data the second time.
|
||||
|
@ -150,13 +170,7 @@ export class DocumentMigrator implements VersionedTransformer {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic validation that the migraiton definition matches our expectations. We can't
|
||||
* rely on TypeScript here, as the caller may be JavaScript / ClojureScript / any compile-to-js
|
||||
* language. So, this is just to provide a little developer-friendly error messaging. Joi was
|
||||
* giving weird errors, so we're just doing manual validation.
|
||||
*/
|
||||
function validateMigrationDefinition(registry: ISavedObjectTypeRegistry) {
|
||||
function validateMigrationsMapObject(name: string, migrationsMap?: SavedObjectMigrationMap) {
|
||||
function assertObject(obj: any, prefix: string) {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
throw new Error(`${prefix} Got ${obj}.`);
|
||||
|
@ -177,16 +191,38 @@ function validateMigrationDefinition(registry: ISavedObjectTypeRegistry) {
|
|||
}
|
||||
}
|
||||
|
||||
if (migrationsMap) {
|
||||
assertObject(
|
||||
migrationsMap,
|
||||
`Migrations map for type ${name} should be an object like { '2.0.0': (doc) => doc }.`
|
||||
);
|
||||
|
||||
Object.entries(migrationsMap).forEach(([version, fn]) => {
|
||||
assertValidSemver(version, name);
|
||||
assertValidTransform(fn, version, name);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic validation that the migraiton definition matches our expectations. We can't
|
||||
* rely on TypeScript here, as the caller may be JavaScript / ClojureScript / any compile-to-js
|
||||
* language. So, this is just to provide a little developer-friendly error messaging. Joi was
|
||||
* giving weird errors, so we're just doing manual validation.
|
||||
*/
|
||||
function validateMigrationDefinition(registry: ISavedObjectTypeRegistry) {
|
||||
function assertObjectOrFunction(entity: any, prefix: string) {
|
||||
if (!entity || (typeof entity !== 'function' && typeof entity !== 'object')) {
|
||||
throw new Error(`${prefix} Got! ${typeof entity}, ${JSON.stringify(entity)}.`);
|
||||
}
|
||||
}
|
||||
|
||||
registry.getAllTypes().forEach((type) => {
|
||||
if (type.migrations) {
|
||||
assertObject(
|
||||
assertObjectOrFunction(
|
||||
type.migrations,
|
||||
`Migration for type ${type.name} should be an object like { '2.0.0': (doc) => doc }.`
|
||||
`Migration for type ${type.name} should be an object or a function returning an object like { '2.0.0': (doc) => doc }.`
|
||||
);
|
||||
Object.entries(type.migrations).forEach(([version, fn]) => {
|
||||
assertValidSemver(version, type.name);
|
||||
assertValidTransform(fn, version, type.name);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -201,11 +237,22 @@ function buildActiveMigrations(
|
|||
typeRegistry: ISavedObjectTypeRegistry,
|
||||
log: Logger
|
||||
): ActiveMigrations {
|
||||
return typeRegistry
|
||||
const typesWithMigrationMaps = typeRegistry
|
||||
.getAllTypes()
|
||||
.filter((type) => type.migrations && Object.keys(type.migrations).length > 0)
|
||||
.map((type) => ({
|
||||
...type,
|
||||
migrationsMap: typeof type.migrations === 'function' ? type.migrations() : type.migrations,
|
||||
}))
|
||||
.filter((type) => typeof type.migrationsMap !== 'undefined');
|
||||
|
||||
typesWithMigrationMaps.forEach((type) =>
|
||||
validateMigrationsMapObject(type.name, type.migrationsMap)
|
||||
);
|
||||
|
||||
return typesWithMigrationMaps
|
||||
.filter((type) => type.migrationsMap && Object.keys(type.migrationsMap).length > 0)
|
||||
.reduce((migrations, type) => {
|
||||
const transforms = Object.entries(type.migrations!)
|
||||
const transforms = Object.entries(type.migrationsMap!)
|
||||
.map(([version, transform]) => ({
|
||||
version,
|
||||
transform: wrapWithTry(version, type.name, transform, log),
|
||||
|
@ -220,7 +267,6 @@ function buildActiveMigrations(
|
|||
};
|
||||
}, {} as ActiveMigrations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a function which migrates and validates any document that is passed to it.
|
||||
*/
|
||||
|
|
|
@ -42,6 +42,7 @@ describe('IndexMigrator', () => {
|
|||
documentMigrator: {
|
||||
migrationVersion: {},
|
||||
migrate: _.identity,
|
||||
prepareMigrations: jest.fn(),
|
||||
},
|
||||
serializer: new SavedObjectsSerializer(new SavedObjectTypeRegistry()),
|
||||
};
|
||||
|
@ -326,6 +327,7 @@ describe('IndexMigrator', () => {
|
|||
|
||||
testOpts.documentMigrator = {
|
||||
migrationVersion: { foo: '1.2.3' },
|
||||
prepareMigrations: jest.fn(),
|
||||
migrate: migrateDoc,
|
||||
};
|
||||
|
||||
|
@ -378,6 +380,7 @@ describe('IndexMigrator', () => {
|
|||
|
||||
testOpts.documentMigrator = {
|
||||
migrationVersion: { foo: '1.2.3' },
|
||||
prepareMigrations: jest.fn(),
|
||||
migrate: migrateDoc,
|
||||
};
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ const defaultSavedObjectTypes: SavedObjectsType[] = [
|
|||
name: { type: 'keyword' },
|
||||
},
|
||||
},
|
||||
migrations: {},
|
||||
migrations: () => ({}),
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -56,6 +56,7 @@ const createMigrator = (
|
|||
runMigrations: jest.fn(),
|
||||
getActiveMappings: jest.fn(),
|
||||
migrateDocument: jest.fn(),
|
||||
prepareMigrations: jest.fn(),
|
||||
getStatus$: jest.fn(
|
||||
() =>
|
||||
new BehaviorSubject<KibanaMigratorStatus>({
|
||||
|
|
|
@ -65,7 +65,53 @@ describe('KibanaMigrator', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('migrateDocument', () => {
|
||||
it('throws an error if documentMigrator.prepareMigrations is not called previously', () => {
|
||||
const options = mockOptions();
|
||||
const kibanaMigrator = new KibanaMigrator(options);
|
||||
const doc = {} as any;
|
||||
expect(() => kibanaMigrator.migrateDocument(doc)).toThrowError(
|
||||
/Migrations are not ready. Make sure prepareMigrations is called first./i
|
||||
);
|
||||
});
|
||||
|
||||
it('calls documentMigrator.migrate', () => {
|
||||
const options = mockOptions();
|
||||
const kibanaMigrator = new KibanaMigrator(options);
|
||||
const mockDocumentMigrator = { migrate: jest.fn() };
|
||||
// @ts-expect-error `documentMigrator` is readonly.
|
||||
kibanaMigrator.documentMigrator = mockDocumentMigrator;
|
||||
const doc = {} as any;
|
||||
|
||||
expect(() => kibanaMigrator.migrateDocument(doc)).not.toThrowError();
|
||||
expect(mockDocumentMigrator.migrate).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('runMigrations', () => {
|
||||
it('throws if prepareMigrations is not called first', async () => {
|
||||
const options = mockOptions();
|
||||
|
||||
options.client.cat.templates.mockReturnValue(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise(
|
||||
{ templates: [] },
|
||||
{ statusCode: 404 }
|
||||
)
|
||||
);
|
||||
options.client.indices.get.mockReturnValue(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 })
|
||||
);
|
||||
options.client.indices.getAlias.mockReturnValue(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 })
|
||||
);
|
||||
|
||||
const migrator = new KibanaMigrator(options);
|
||||
|
||||
expect(() => migrator.runMigrations()).rejects.toThrow(
|
||||
/Migrations are not ready. Make sure prepareMigrations is called first./i
|
||||
);
|
||||
});
|
||||
|
||||
it('only runs migrations once if called multiple times', async () => {
|
||||
const options = mockOptions();
|
||||
|
||||
|
@ -84,6 +130,7 @@ describe('KibanaMigrator', () => {
|
|||
|
||||
const migrator = new KibanaMigrator(options);
|
||||
|
||||
migrator.prepareMigrations();
|
||||
await migrator.runMigrations();
|
||||
await migrator.runMigrations();
|
||||
|
||||
|
@ -120,6 +167,8 @@ describe('KibanaMigrator', () => {
|
|||
|
||||
const migrator = new KibanaMigrator(options);
|
||||
const migratorStatus = migrator.getStatus$().pipe(take(3)).toPromise();
|
||||
|
||||
migrator.prepareMigrations();
|
||||
await migrator.runMigrations();
|
||||
|
||||
expect(options.client.indices.create).toHaveBeenCalledTimes(3);
|
||||
|
@ -145,6 +194,7 @@ describe('KibanaMigrator', () => {
|
|||
|
||||
const migrator = new KibanaMigrator(options);
|
||||
const migratorStatus = migrator.getStatus$().pipe(take(3)).toPromise();
|
||||
migrator.prepareMigrations();
|
||||
await migrator.runMigrations();
|
||||
const { status, result } = await migratorStatus;
|
||||
expect(status).toEqual('completed');
|
||||
|
@ -171,6 +221,7 @@ describe('KibanaMigrator', () => {
|
|||
const options = mockV2MigrationOptions();
|
||||
const migrator = new KibanaMigrator(options);
|
||||
const migratorStatus = migrator.getStatus$().pipe(take(3)).toPromise();
|
||||
migrator.prepareMigrations();
|
||||
await migrator.runMigrations();
|
||||
|
||||
// Basic assertions that we're creating and reindexing the expected indices
|
||||
|
@ -212,6 +263,7 @@ describe('KibanaMigrator', () => {
|
|||
const options = mockV2MigrationOptions();
|
||||
const migrator = new KibanaMigrator(options);
|
||||
const migratorStatus = migrator.getStatus$().pipe(take(3)).toPromise();
|
||||
migrator.prepareMigrations();
|
||||
await migrator.runMigrations();
|
||||
|
||||
const { status, result } = await migratorStatus;
|
||||
|
@ -247,6 +299,7 @@ describe('KibanaMigrator', () => {
|
|||
);
|
||||
|
||||
const migrator = new KibanaMigrator(options);
|
||||
migrator.prepareMigrations();
|
||||
return expect(migrator.runMigrations()).rejects.toMatchInlineSnapshot(
|
||||
`[Error: Unable to complete saved object migrations for the [.my-index] index: The .my-index alias is pointing to a newer version of Kibana: v8.2.4]`
|
||||
);
|
||||
|
@ -263,7 +316,7 @@ describe('KibanaMigrator', () => {
|
|||
);
|
||||
|
||||
const migrator = new KibanaMigrator(options);
|
||||
|
||||
migrator.prepareMigrations();
|
||||
await expect(migrator.runMigrations()).rejects.toMatchInlineSnapshot(`
|
||||
[Error: Unable to complete saved object migrations for the [.my-index] index. Please check the health of your Elasticsearch cluster and try again. Error: Reindex failed with the following error:
|
||||
{"_tag":"Some","value":{"type":"elatsicsearch_exception","reason":"task failed with an error"}}]
|
||||
|
|
|
@ -158,6 +158,10 @@ export class KibanaMigrator {
|
|||
return this.migrationResult;
|
||||
}
|
||||
|
||||
public prepareMigrations() {
|
||||
this.documentMigrator.prepareMigrations();
|
||||
}
|
||||
|
||||
public getStatus$() {
|
||||
return this.status$.asObservable();
|
||||
}
|
||||
|
|
|
@ -79,7 +79,7 @@ export interface SavedObjectMigrationContext {
|
|||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const migrations: SavedObjectMigrationMap = {
|
||||
* const migrationsMap: SavedObjectMigrationMap = {
|
||||
* '1.0.0': migrateToV1,
|
||||
* '2.1.0': migrateToV21
|
||||
* }
|
||||
|
|
|
@ -388,6 +388,12 @@ export class SavedObjectsService
|
|||
*/
|
||||
const skipMigrations = this.config.migration.skip || !pluginsInitialized;
|
||||
|
||||
/**
|
||||
* Note: Prepares all migrations maps. If a saved object type was registered with property `migrations`
|
||||
* of type function; this function will be called to get the type's SavedObjectMigrationMap.
|
||||
*/
|
||||
migrator.prepareMigrations();
|
||||
|
||||
if (skipMigrations) {
|
||||
this.logger.warn(
|
||||
'Skipping Saved Object migrations on startup. Note: Individual documents will still be migrated when read or written.'
|
||||
|
|
|
@ -217,6 +217,7 @@ describe('SavedObjectsRepository', () => {
|
|||
beforeEach(() => {
|
||||
client = elasticsearchClientMock.createElasticsearchClient();
|
||||
migrator = mockKibanaMigrator.create();
|
||||
documentMigrator.prepareMigrations();
|
||||
migrator.migrateDocument = jest.fn().mockImplementation(documentMigrator.migrate);
|
||||
migrator.runMigrations = async () => ({ status: 'skipped' });
|
||||
|
||||
|
|
|
@ -245,9 +245,9 @@ export interface SavedObjectsType {
|
|||
*/
|
||||
mappings: SavedObjectsTypeMappingDefinition;
|
||||
/**
|
||||
* An optional map of {@link SavedObjectMigrationFn | migrations} to be used to migrate the type.
|
||||
* An optional map of {@link SavedObjectMigrationFn | migrations} or a function returning a map of {@link SavedObjectMigrationFn | migrations} to be used to migrate the type.
|
||||
*/
|
||||
migrations?: SavedObjectMigrationMap;
|
||||
migrations?: SavedObjectMigrationMap | (() => SavedObjectMigrationMap);
|
||||
/**
|
||||
* An optional {@link SavedObjectsTypeManagementDefinition | saved objects management section} definition for the type.
|
||||
*/
|
||||
|
|
|
@ -2774,7 +2774,7 @@ export interface SavedObjectsType {
|
|||
indexPattern?: string;
|
||||
management?: SavedObjectsTypeManagementDefinition;
|
||||
mappings: SavedObjectsTypeMappingDefinition;
|
||||
migrations?: SavedObjectMigrationMap;
|
||||
migrations?: SavedObjectMigrationMap | (() => SavedObjectMigrationMap);
|
||||
name: string;
|
||||
namespaceType: SavedObjectsNamespaceType;
|
||||
}
|
||||
|
|
|
@ -400,6 +400,8 @@ async function migrateIndex({
|
|||
log: getLogMock(),
|
||||
});
|
||||
|
||||
documentMigrator.prepareMigrations();
|
||||
|
||||
const migrator = new IndexMigrator({
|
||||
client: createMigrationEsClient(esClient, getLogMock()),
|
||||
documentMigrator,
|
||||
|
|
Loading…
Reference in a new issue