Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SavedObjects bulkCreate API should return migrationVersion and strip the type & namespace from the id #65150

Merged
merged 12 commits into from
May 8, 2020
Merged
61 changes: 55 additions & 6 deletions src/core/server/saved_objects/service/lib/repository.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { SavedObjectsErrorHelpers } from './errors';
import { SavedObjectsSerializer } from '../../serialization';
import { encodeHitVersion } from '../../version';
import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry';
import { DocumentMigrator } from '../../migrations/core/document_migrator';

jest.mock('./search_dsl/search_dsl', () => ({ getSearchDsl: jest.fn() }));

Expand Down Expand Up @@ -115,6 +116,7 @@ describe('SavedObjectsRepository', () => {
const createType = type => ({
name: type,
mappings: { properties: mappings.properties[type].properties },
migrations: { '1.1.1': doc => doc },
});

const registry = new SavedObjectTypeRegistry();
Expand Down Expand Up @@ -144,6 +146,13 @@ describe('SavedObjectsRepository', () => {
namespaceType: 'agnostic',
});

const documentMigrator = new DocumentMigrator({
typeRegistry: registry,
kibanaVersion: '2.0.0',
log: {},
validateDoc: jest.fn(),
});

const getMockGetResponse = ({ type, id, references, namespace }) => ({
// NOTE: Elasticsearch returns more fields (_index, _type) but the SavedObjectsRepository method ignores these
found: true,
Expand Down Expand Up @@ -207,7 +216,7 @@ describe('SavedObjectsRepository', () => {
beforeEach(() => {
callAdminCluster = jest.fn();
migrator = {
migrateDocument: jest.fn(doc => doc),
migrateDocument: jest.fn().mockImplementation(documentMigrator.migrate),
runMigrations: async () => ({ status: 'skipped' }),
};

Expand Down Expand Up @@ -424,9 +433,17 @@ describe('SavedObjectsRepository', () => {

const getMockBulkCreateResponse = (objects, namespace) => {
return {
items: objects.map(({ type, id }) => ({
items: objects.map(({ type, id, attributes, references, migrationVersion }) => ({
create: {
_id: `${namespace ? `${namespace}:` : ''}${type}:${id}`,
_source: {
[type]: attributes,
type,
namespace,
references,
rudolf marked this conversation as resolved.
Show resolved Hide resolved
...mockTimestampFields,
migrationVersion: migrationVersion || { [type]: '1.1.1' },
},
...mockVersionProps,
},
})),
Expand Down Expand Up @@ -474,7 +491,7 @@ describe('SavedObjectsRepository', () => {

const expectSuccessResult = obj => ({
...obj,
migrationVersion: undefined,
migrationVersion: { [obj.type]: '1.1.1' },
version: mockVersion,
...mockTimestampFields,
});
Expand Down Expand Up @@ -619,13 +636,16 @@ describe('SavedObjectsRepository', () => {
};

const bulkCreateError = async (obj, esError, expectedError) => {
const objects = [obj1, obj, obj2];
const response = getMockBulkCreateResponse(objects);
let response;
if (esError) {
response = getMockBulkCreateResponse([obj1, obj, obj2]);
response.items[1].create = { error: esError };
} else {
response = getMockBulkCreateResponse([obj1, obj2]);
}
callAdminCluster.mockResolvedValue(response); // this._writeToCluster('bulk', ...)

const objects = [obj1, obj, obj2];
const result = await savedObjectsRepository.bulkCreate(objects);
expectClusterCalls('bulk');
const objCall = esError ? expectObjArgs(obj) : [];
Expand Down Expand Up @@ -781,14 +801,42 @@ describe('SavedObjectsRepository', () => {
id: 'three',
};
const objects = [obj1, obj, obj2];
const response = getMockBulkCreateResponse(objects);
const response = getMockBulkCreateResponse([obj1, obj2]);
callAdminCluster.mockResolvedValue(response); // this._writeToCluster('bulk', ...)
const result = await savedObjectsRepository.bulkCreate(objects);
expect(callAdminCluster).toHaveBeenCalledTimes(1);
expect(result).toEqual({
saved_objects: [expectSuccessResult(obj1), expectError(obj), expectSuccessResult(obj2)],
});
});

it(`a deserialized saved object`, async () => {
// Test for fix to https://github.com/elastic/kibana/issues/65088 where
// we returned raw ID's when an object without an id was created.
const namespace = 'myspace';
const response = getMockBulkCreateResponse([obj1, obj2], namespace);
callAdminCluster.mockResolvedValueOnce(response); // this._writeToCluster('bulk', ...)

// Bulk create one object with id unspecified, and one with id specified
const result = await savedObjectsRepository.bulkCreate([{ ...obj1, id: undefined }, obj2], {
namespace,
});

// Assert that both raw docs from the ES response are deserialized
expect(serializer.rawToSavedObject).toHaveBeenNthCalledWith(1, {
...response.items[0].create,
_id: expect.stringMatching(/myspace:config:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/),
});
expect(serializer.rawToSavedObject).toHaveBeenNthCalledWith(2, response.items[1].create);

// Assert that ID's are deserialized to remove the type and namespace
expect(result.saved_objects[0].id).toEqual(
expect.not.stringMatching(/config|index-pattern|myspace/)
);
expect(result.saved_objects[1].id).toEqual(
expect.not.stringMatching(/config|index-pattern|myspace/)
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we use the uuid regexp ([0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}) as done L828 instead of asserting against the names of the types/spaces used in the test suite? I think that's a little more explicit?

Copy link
Contributor Author

@rudolf rudolf May 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

obj2's id is logstash-* so I initially thought I'll try to keep these assertions the same. But I agree, a uuid regexp would be better, so I'll change the first match to use a regexp and the second match to match obj2.id.

});
});
});

Expand Down Expand Up @@ -1604,6 +1652,7 @@ describe('SavedObjectsRepository', () => {
version: mockVersion,
attributes,
references,
migrationVersion: { [type]: '1.1.1' },
});
});
});
Expand Down
43 changes: 18 additions & 25 deletions src/core/server/saved_objects/service/lib/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/

import { omit } from 'lodash';
import uuid from 'uuid';
import { retryCallCluster } from '../../../elasticsearch/retry_call_cluster';
import { APICaller } from '../../../elasticsearch/';

Expand Down Expand Up @@ -299,6 +300,8 @@ export class SavedObjectsRepository {
const requiresNamespacesCheck =
method === 'index' && this._registry.isMultiNamespace(object.type);

if (object.id == null) object.id = uuid.v1();
pgayvallet marked this conversation as resolved.
Show resolved Hide resolved

return {
tag: 'Right' as 'Right',
value: {
Expand Down Expand Up @@ -404,35 +407,25 @@ export class SavedObjectsRepository {
}

const { requestedId, rawMigratedDoc, esRequestIndex } = expectedResult.value;
const response = bulkResponse.items[esRequestIndex];
const {
error,
_id: responseId,
_seq_no: seqNo,
_primary_term: primaryTerm,
} = Object.values(response)[0] as any;

const {
_source: { type, [type]: attributes, references = [], namespaces },
} = rawMigratedDoc;

const id = requestedId || responseId;
const { error, ...rawResponse } = Object.values(
bulkResponse.items[esRequestIndex]
)[0] as any;

if (error) {
return {
id,
type,
error: getBulkOperationError(error, type, id),
id: requestedId,
type: rawMigratedDoc._source.type,
error: getBulkOperationError(error, rawMigratedDoc._source.type, requestedId),
};
}
return {
id,
type,
...(namespaces && { namespaces }),
updated_at: time,
version: encodeVersion(seqNo, primaryTerm),
attributes,
references,
};

// When method == 'index' the bulkResponse doesn't include the indexed
// _source so we return rawMigratedDoc but have to spread the latest
// _seq_no and _primary_term values from the rawResponse.
return this._serializer.rawToSavedObject({
...rawMigratedDoc,
...{ _seq_no: rawResponse._seq_no, _primary_term: rawResponse._primary_term },
});
}),
};
}
Expand Down
21 changes: 21 additions & 0 deletions test/api_integration/apis/saved_objects/bulk_create.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,26 @@ export default function({ getService }) {
attributes: {
title: 'A great new dashboard',
},
migrationVersion: {
dashboard: resp.body.saved_objects[1].migrationVersion.dashboard,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This value will change anytime a new dashboard migration is added. This seems like the best workaround when using kbn-expect to prevent tests from failing anytime a new dashboard migration is added.

We should probably "modernize" these integration tests, but it felt like it's not enough of a priority to attempt this right now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, having jest toolbox for FTR tests would we really great. kbn-expect feels very old when compared to jest.

},
references: [],
},
],
});
}));

it('should not return raw id when object id is unspecified', async () =>
await supertest
.post(`/api/saved_objects/_bulk_create`)
// eslint-disable-next-line no-unused-vars
.send(BULK_REQUESTS.map(({ id, ...rest }) => rest))
.expect(200)
.then(resp => {
resp.body.saved_objects.map(({ id }) =>
expect(id).not.match(/visualization|dashboard/)
);
}));
});

describe('without kibana index', () => {
Expand Down Expand Up @@ -106,6 +121,9 @@ export default function({ getService }) {
title: 'An existing visualization',
},
references: [],
migrationVersion: {
visualization: resp.body.saved_objects[0].migrationVersion.visualization,
},
rudolf marked this conversation as resolved.
Show resolved Hide resolved
},
{
type: 'dashboard',
Expand All @@ -116,6 +134,9 @@ export default function({ getService }) {
title: 'A great new dashboard',
},
references: [],
migrationVersion: {
dashboard: resp.body.saved_objects[1].migrationVersion.dashboard,
},
},
],
});
Expand Down