diff --git a/src/legacy/core_plugins/elasticsearch/lib/__tests__/health_check.js b/src/legacy/core_plugins/elasticsearch/lib/__tests__/health_check.js index 093a16bcb11579..9b82d3936b327a 100644 --- a/src/legacy/core_plugins/elasticsearch/lib/__tests__/health_check.js +++ b/src/legacy/core_plugins/elasticsearch/lib/__tests__/health_check.js @@ -61,7 +61,6 @@ describe('plugins/elasticsearch', () => { cluster = { callWithInternalUser: sinon.stub() }; cluster.callWithInternalUser.withArgs('index', sinon.match.any).returns(Promise.resolve()); - cluster.callWithInternalUser.withArgs('create', sinon.match.any).returns(Promise.resolve({ _id: '1', _version: 1 })); cluster.callWithInternalUser.withArgs('mget', sinon.match.any).returns(Promise.resolve({ ok: true })); cluster.callWithInternalUser.withArgs('get', sinon.match.any).returns(Promise.resolve({ found: false })); cluster.callWithInternalUser.withArgs('search', sinon.match.any).returns(Promise.resolve({ hits: { hits: [] } })); diff --git a/src/server/saved_objects/routes/bulk_create.js b/src/server/saved_objects/routes/bulk_create.js index 943b2520a331d7..83ba07049b79cb 100644 --- a/src/server/saved_objects/routes/bulk_create.js +++ b/src/server/saved_objects/routes/bulk_create.js @@ -35,7 +35,7 @@ export const createBulkCreateRoute = prereqs => ({ type: Joi.string().required(), id: Joi.string(), attributes: Joi.object().required(), - version: Joi.number(), + version: Joi.string(), migrationVersion: Joi.object().optional(), }).required() ), diff --git a/src/server/saved_objects/routes/bulk_get.test.js b/src/server/saved_objects/routes/bulk_get.test.js index e646629970fd43..86202b1e63d9b4 100644 --- a/src/server/saved_objects/routes/bulk_get.test.js +++ b/src/server/saved_objects/routes/bulk_get.test.js @@ -59,7 +59,7 @@ describe('POST /api/saved_objects/_bulk_get', () => { id: 'abc123', type: 'index-pattern', title: 'logstash-*', - version: 2 + version: 'foo', }] }; diff --git a/src/server/saved_objects/routes/update.js b/src/server/saved_objects/routes/update.js index cc7e2c0a4b4d16..a2006f30962069 100644 --- a/src/server/saved_objects/routes/update.js +++ b/src/server/saved_objects/routes/update.js @@ -32,7 +32,7 @@ export const createUpdateRoute = (prereqs) => { }).required(), payload: Joi.object({ attributes: Joi.object().required(), - version: Joi.number().min(1) + version: Joi.string(), }).required() }, handler(request) { diff --git a/src/server/saved_objects/routes/update.test.js b/src/server/saved_objects/routes/update.test.js index ac4d938794fda7..451bfbcc0c9971 100644 --- a/src/server/saved_objects/routes/update.test.js +++ b/src/server/saved_objects/routes/update.test.js @@ -66,7 +66,7 @@ describe('PUT /api/saved_objects/{type}/{id?}', () => { it('calls upon savedObjectClient.update', async () => { const attributes = { title: 'Testing' }; - const options = { version: 2 }; + const options = { version: 'foo' }; const request = { method: 'PUT', url: '/api/saved_objects/index-pattern/logstash-*', diff --git a/src/server/saved_objects/serialization/index.ts b/src/server/saved_objects/serialization/index.ts index 46b869cb28a850..40ba4255f04355 100644 --- a/src/server/saved_objects/serialization/index.ts +++ b/src/server/saved_objects/serialization/index.ts @@ -24,6 +24,7 @@ import uuid from 'uuid'; import { SavedObjectsSchema } from '../schema'; +import { decodeVersion, encodeVersion } from '../version'; /** * The root document type. In 7.0, this needs to change to '_doc'. @@ -37,7 +38,8 @@ export interface RawDoc { _id: string; _source: any; _type?: string; - _version?: number; + _seq_no?: number; + _primary_term?: number; } /** @@ -60,7 +62,7 @@ export interface SavedObjectDoc { type: string; namespace?: string; migrationVersion?: MigrationVersion; - version?: number; + version?: string; updated_at?: Date; [rootProp: string]: any; @@ -99,8 +101,14 @@ export class SavedObjectsSerializer { * * @param {RawDoc} rawDoc - The raw ES document to be converted to saved object format. */ - public rawToSavedObject({ _id, _source, _version }: RawDoc): SavedObjectDoc { + public rawToSavedObject({ _id, _source, _seq_no, _primary_term }: RawDoc): SavedObjectDoc { const { type, namespace } = _source; + + const version = + _seq_no != null || _primary_term != null + ? encodeVersion(_seq_no!, _primary_term!) + : undefined; + return { type, id: this.trimIdPrefix(namespace, type, _id), @@ -108,7 +116,7 @@ export class SavedObjectsSerializer { attributes: _source[type], ...(_source.migrationVersion && { migrationVersion: _source.migrationVersion }), ...(_source.updated_at && { updated_at: _source.updated_at }), - ...(_version != null && { version: _version }), + ...(version && { version }), }; } @@ -131,7 +139,7 @@ export class SavedObjectsSerializer { _id: this.generateRawId(namespace, type, id), _source: source, _type: ROOT_TYPE, - ...(version != null && { _version: version }), + ...(version != null && decodeVersion(version)), }; } diff --git a/src/server/saved_objects/serialization/serialization.test.ts b/src/server/saved_objects/serialization/serialization.test.ts index 686acf8521e364..b135af35093060 100644 --- a/src/server/saved_objects/serialization/serialization.test.ts +++ b/src/server/saved_objects/serialization/serialization.test.ts @@ -20,6 +20,7 @@ import _ from 'lodash'; import { ROOT_TYPE, SavedObjectsSerializer } from '.'; import { SavedObjectsSchema } from '../schema'; +import { encodeVersion } from '../version'; describe('saved object conversion', () => { describe('#rawToSavedObject', () => { @@ -69,7 +70,8 @@ describe('saved object conversion', () => { const actual = serializer.rawToSavedObject({ _id: 'hello:world', _type: ROOT_TYPE, - _version: 3, + _seq_no: 3, + _primary_term: 1, _source: { type: 'hello', hello: { @@ -86,7 +88,7 @@ describe('saved object conversion', () => { const expected = { id: 'world', type: 'hello', - version: 3, + version: encodeVersion(3, 1), attributes: { a: 'b', c: 'd', @@ -112,17 +114,46 @@ describe('saved object conversion', () => { expect(actual).not.toHaveProperty('version'); }); - test(`if specified it copies _version to version`, () => { + test(`if specified it encodes _seq_no and _primary_term to version`, () => { const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); const actual = serializer.rawToSavedObject({ _id: 'foo:bar', - _version: 4, + _seq_no: 4, + _primary_term: 1, _source: { type: 'foo', hello: {}, }, }); - expect(actual).toHaveProperty('version', 4); + expect(actual).toHaveProperty('version', encodeVersion(4, 1)); + }); + + test(`if only _seq_no is specified it throws`, () => { + const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + expect(() => + serializer.rawToSavedObject({ + _id: 'foo:bar', + _seq_no: 4, + _source: { + type: 'foo', + hello: {}, + }, + }) + ).toThrowErrorMatchingInlineSnapshot(`"_primary_term from elasticsearch must be an integer"`); + }); + + test(`if only _primary_term is throws`, () => { + const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + expect(() => + serializer.rawToSavedObject({ + _id: 'foo:bar', + _primary_term: 1, + _source: { + type: 'foo', + hello: {}, + }, + }) + ).toThrowErrorMatchingInlineSnapshot(`"_seq_no from elasticsearch must be an integer"`); }); test('if specified it copies the _source.updated_at property to updated_at', () => { @@ -206,7 +237,8 @@ describe('saved object conversion', () => { const raw = { _id: 'foo-namespace:foo:bar', _type: ROOT_TYPE, - _version: 24, + _primary_term: 24, + _seq_no: 42, _source: { type: 'foo', foo: { @@ -440,25 +472,38 @@ describe('saved object conversion', () => { expect(actual._source).not.toHaveProperty('migrationVersion'); }); - test('it copies the version property to _version', () => { + test('it decodes the version property to _seq_no and _primary_term', () => { const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); const actual = serializer.savedObjectToRaw({ type: '', attributes: {}, - version: 4, + version: encodeVersion(1, 2), } as any); - expect(actual).toHaveProperty('_version', 4); + expect(actual).toHaveProperty('_seq_no', 1); + expect(actual).toHaveProperty('_primary_term', 2); }); - test(`if unspecified it doesn't add _version property`, () => { + test(`if unspecified it doesn't add _seq_no or _primary_term properties`, () => { const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); const actual = serializer.savedObjectToRaw({ type: '', attributes: {}, } as any); - expect(actual).not.toHaveProperty('_version'); + expect(actual).not.toHaveProperty('_seq_no'); + expect(actual).not.toHaveProperty('_primary_term'); + }); + + test(`if version invalid it throws`, () => { + const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + expect(() => + serializer.savedObjectToRaw({ + type: '', + attributes: {}, + version: 'foo', + } as any) + ).toThrowErrorMatchingInlineSnapshot(`"Invalid version [foo]"`); }); test('it copies attributes to _source[type]', () => { diff --git a/src/server/saved_objects/service/lib/errors.d.ts b/src/server/saved_objects/service/lib/errors.d.ts index 5cc16993de9b84..cfeed417b98da7 100644 --- a/src/server/saved_objects/service/lib/errors.d.ts +++ b/src/server/saved_objects/service/lib/errors.d.ts @@ -25,3 +25,6 @@ export function isNotFoundError(maybeError: any): boolean; export function isConflictError(maybeError: any): boolean; export function isEsUnavailableError(maybeError: any): boolean; export function isEsAutoCreateIndexError(maybeError: any): boolean; + +export function createInvalidVersionError(version: any): Error; +export function isInvalidVersionError(maybeError: Error): boolean; diff --git a/src/server/saved_objects/service/lib/errors.js b/src/server/saved_objects/service/lib/errors.js index d0a4d6ecec796d..d54a812d9b3140 100644 --- a/src/server/saved_objects/service/lib/errors.js +++ b/src/server/saved_objects/service/lib/errors.js @@ -50,6 +50,15 @@ export function isBadRequestError(error) { return error && error[code] === CODE_BAD_REQUEST; } +// 400 - invalid version +const CODE_INVALID_VERSION = 'SavedObjectsClient/invalidVersion'; +export function createInvalidVersionError(versionInput) { + return decorate(Boom.badRequest(`Invalid version [${versionInput}]`), CODE_INVALID_VERSION, 400); +} +export function isInvalidVersionError(error) { + return error && error[code] === CODE_INVALID_VERSION; +} + // 401 - Not Authorized const CODE_NOT_AUTHORIZED = 'SavedObjectsClient/notAuthorized'; diff --git a/src/server/saved_objects/service/lib/repository.js b/src/server/saved_objects/service/lib/repository.js index 28d7a52db2b59c..b4f291f52aa1a0 100644 --- a/src/server/saved_objects/service/lib/repository.js +++ b/src/server/saved_objects/service/lib/repository.js @@ -23,6 +23,7 @@ import { getSearchDsl } from './search_dsl'; import { includedFields } from './included_fields'; import { decorateEsError } from './decorate_es_error'; import * as errors from './errors'; +import { decodeRequestVersion, encodeVersion, encodeHitVersion } from '../../version'; // BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository // so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient. @@ -169,7 +170,8 @@ export class SavedObjectsRepository { const { error, _id: responseId, - _version: version, + _seq_no: seqNo, + _primary_term: primaryTerm, } = Object.values(response)[0]; const { @@ -199,8 +201,8 @@ export class SavedObjectsRepository { id, type, updated_at: time, - version, - attributes + version: encodeVersion(seqNo, primaryTerm), + attributes, }; }) }; @@ -252,7 +254,6 @@ export class SavedObjectsRepository { * @returns {promise} - { took, timed_out, total, deleted, batches, version_conflicts, noops, retries, failures } */ async deleteByNamespace(namespace) { - if (!namespace || typeof namespace !== 'string') { throw new TypeError(`namespace is required, and must be a string`); } @@ -324,7 +325,7 @@ export class SavedObjectsRepository { ignore: [404], rest_total_hits_as_int: true, body: { - version: true, + seq_no_primary_term: true, ...getSearchDsl(this._mappings, this._schema, { search, searchFields, @@ -407,7 +408,7 @@ export class SavedObjectsRepository { id, type, ...time && { updated_at: time }, - version: doc._version, + version: encodeHitVersion(doc), attributes: doc._source[type], migrationVersion: doc._source.migrationVersion, }; @@ -449,7 +450,7 @@ export class SavedObjectsRepository { id, type, ...updatedAt && { updated_at: updatedAt }, - version: response._version, + version: encodeHitVersion(response), attributes: response._source[type], migrationVersion: response._source.migrationVersion, }; @@ -461,7 +462,7 @@ export class SavedObjectsRepository { * @param {string} type * @param {string} id * @param {object} [options={}] - * @property {integer} options.version - ensures version matches that of persisted object + * @property {string} options.version - ensures version matches that of persisted object * @property {string} [options.namespace] * @returns {promise} */ @@ -476,7 +477,7 @@ export class SavedObjectsRepository { id: this._serializer.generateRawId(namespace, type, id), type: this._type, index: this._index, - version, + ...(version && decodeRequestVersion(version)), refresh: 'wait_for', ignore: [404], body: { @@ -496,7 +497,7 @@ export class SavedObjectsRepository { id, type, updated_at: time, - version: response._version, + version: encodeHitVersion(response), attributes }; } @@ -570,7 +571,7 @@ export class SavedObjectsRepository { id, type, updated_at: time, - version: response._version, + version: encodeHitVersion(response), attributes: response.get._source[type], }; diff --git a/src/server/saved_objects/service/lib/repository.test.js b/src/server/saved_objects/service/lib/repository.test.js index 3771cdee44f7ab..04616c917d18bf 100644 --- a/src/server/saved_objects/service/lib/repository.test.js +++ b/src/server/saved_objects/service/lib/repository.test.js @@ -26,6 +26,7 @@ import * as errors from './errors'; import elasticsearch from 'elasticsearch'; import { SavedObjectsSchema } from '../../schema'; import { SavedObjectsSerializer } from '../../serialization'; +import { encodeHitVersion } from '../../version'; // BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository // so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient. @@ -39,6 +40,8 @@ describe('SavedObjectsRepository', () => { let migrator; const mockTimestamp = '2017-08-14T15:49:14.886Z'; const mockTimestampFields = { updated_at: mockTimestamp }; + const mockVersionProps = { _seq_no: 1, _primary_term: 1 }; + const mockVersion = encodeHitVersion(mockVersionProps); const noNamespaceSearchResults = { hits: { total: 4, @@ -47,6 +50,7 @@ describe('SavedObjectsRepository', () => { _type: 'doc', _id: 'index-pattern:logstash-*', _score: 1, + ...mockVersionProps, _source: { type: 'index-pattern', ...mockTimestampFields, @@ -61,6 +65,7 @@ describe('SavedObjectsRepository', () => { _type: 'doc', _id: 'config:6.0.0-alpha1', _score: 1, + ...mockVersionProps, _source: { type: 'config', ...mockTimestampFields, @@ -74,6 +79,7 @@ describe('SavedObjectsRepository', () => { _type: 'doc', _id: 'index-pattern:stocks-*', _score: 1, + ...mockVersionProps, _source: { type: 'index-pattern', ...mockTimestampFields, @@ -88,6 +94,7 @@ describe('SavedObjectsRepository', () => { _type: 'doc', _id: 'globaltype:something', _score: 1, + ...mockVersionProps, _source: { type: 'globaltype', ...mockTimestampFields, @@ -107,6 +114,7 @@ describe('SavedObjectsRepository', () => { _type: 'doc', _id: 'foo-namespace:index-pattern:logstash-*', _score: 1, + ...mockVersionProps, _source: { namespace: 'foo-namespace', type: 'index-pattern', @@ -122,6 +130,7 @@ describe('SavedObjectsRepository', () => { _type: 'doc', _id: 'foo-namespace:config:6.0.0-alpha1', _score: 1, + ...mockVersionProps, _source: { namespace: 'foo-namespace', type: 'config', @@ -136,6 +145,7 @@ describe('SavedObjectsRepository', () => { _type: 'doc', _id: 'foo-namespace:index-pattern:stocks-*', _score: 1, + ...mockVersionProps, _source: { namespace: 'foo-namespace', type: 'index-pattern', @@ -151,6 +161,7 @@ describe('SavedObjectsRepository', () => { _type: 'doc', _id: 'globaltype:something', _score: 1, + ...mockVersionProps, _source: { type: 'globaltype', ...mockTimestampFields, @@ -239,7 +250,7 @@ describe('SavedObjectsRepository', () => { callAdminCluster.callsFake((method, params) => ({ _type: 'doc', _id: params.id, - _version: 2 + ...mockVersionProps, })); }); @@ -267,7 +278,7 @@ describe('SavedObjectsRepository', () => { type: 'index-pattern', id: 'logstash-*', ...mockTimestampFields, - version: 2, + version: mockVersion, attributes: { title: 'Logstash', } @@ -518,7 +529,7 @@ describe('SavedObjectsRepository', () => { create: { _type: 'doc', _id: 'index-pattern:two', - _version: 2 + ...mockVersionProps, } }] })); @@ -537,7 +548,7 @@ describe('SavedObjectsRepository', () => { }, { id: 'two', type: 'index-pattern', - version: 2, + version: mockVersion, ...mockTimestampFields, attributes: { title: 'Test Two' }, } @@ -552,13 +563,13 @@ describe('SavedObjectsRepository', () => { create: { _type: 'doc', _id: 'config:one', - _version: 2 + ...mockVersionProps } }, { create: { _type: 'doc', _id: 'index-pattern:two', - _version: 2 + ...mockVersionProps } }] })); @@ -575,13 +586,13 @@ describe('SavedObjectsRepository', () => { { id: 'one', type: 'config', - version: 2, + version: mockVersion, ...mockTimestampFields, attributes: { title: 'Test One' }, }, { id: 'two', type: 'index-pattern', - version: 2, + version: mockVersion, ...mockTimestampFields, attributes: { title: 'Test Two' }, } @@ -858,8 +869,8 @@ describe('SavedObjectsRepository', () => { id: doc._id.replace(/(index-pattern|config|globaltype)\:/, ''), type: doc._source.type, ...mockTimestampFields, - version: doc._version, - attributes: doc._source[doc._source.type] + version: mockVersion, + attributes: doc._source[doc._source.type], }); }); }); @@ -881,8 +892,8 @@ describe('SavedObjectsRepository', () => { id: doc._id.replace(/(foo-namespace\:)?(index-pattern|config|globaltype)\:/, ''), type: doc._source.type, ...mockTimestampFields, - version: doc._version, - attributes: doc._source[doc._source.type] + version: mockVersion, + attributes: doc._source[doc._source.type], }); }); }); @@ -926,7 +937,7 @@ describe('SavedObjectsRepository', () => { const noNamespaceResult = { _id: 'index-pattern:logstash-*', _type: 'doc', - _version: 2, + ...mockVersionProps, _source: { type: 'index-pattern', specialProperty: 'specialValue', @@ -939,7 +950,7 @@ describe('SavedObjectsRepository', () => { const namespacedResult = { _id: 'foo-namespace:index-pattern:logstash-*', _type: 'doc', - _version: 2, + ...mockVersionProps, _source: { namespace: 'foo-namespace', type: 'index-pattern', @@ -968,7 +979,7 @@ describe('SavedObjectsRepository', () => { id: 'logstash-*', type: 'index-pattern', updated_at: mockTimestamp, - version: 2, + version: mockVersion, attributes: { title: 'Testing' } @@ -983,7 +994,7 @@ describe('SavedObjectsRepository', () => { id: 'logstash-*', type: 'index-pattern', updated_at: mockTimestamp, - version: 2, + version: mockVersion, attributes: { title: 'Testing' } @@ -1111,7 +1122,7 @@ describe('SavedObjectsRepository', () => { _type: 'doc', _id: 'config:good', found: true, - _version: 2, + ...mockVersionProps, _source: { ...mockTimestampFields, config: { title: 'Test' } } }, { _type: 'doc', @@ -1132,8 +1143,8 @@ describe('SavedObjectsRepository', () => { id: 'good', type: 'config', ...mockTimestampFields, - version: 2, - attributes: { title: 'Test' } + version: mockVersion, + attributes: { title: 'Test' }, }); expect(savedObjects[1]).toEqual({ id: 'bad', @@ -1146,14 +1157,13 @@ describe('SavedObjectsRepository', () => { describe('#update', () => { const id = 'logstash-*'; const type = 'index-pattern'; - const newVersion = 2; const attributes = { title: 'Testing' }; beforeEach(() => { callAdminCluster.returns(Promise.resolve({ _id: `${type}:${id}`, _type: 'doc', - _version: newVersion, + ...mockVersionProps, result: 'updated' })); }); @@ -1168,14 +1178,16 @@ describe('SavedObjectsRepository', () => { sinon.assert.calledOnce(migrator.awaitMigration); }); - it('returns current ES document version', async () => { - const response = await savedObjectsRepository.update('index-pattern', 'logstash-*', attributes, { namespace: 'foo-namespace' }); + it('returns current ES document _seq_no and _primary_term encoded as version', async () => { + const response = await savedObjectsRepository.update('index-pattern', 'logstash-*', attributes, { + namespace: 'foo-namespace', + }); expect(response).toEqual({ id, type, ...mockTimestampFields, - version: newVersion, - attributes + version: mockVersion, + attributes, }); }); @@ -1184,12 +1196,18 @@ describe('SavedObjectsRepository', () => { type, id, { title: 'Testing' }, - { version: newVersion - 1 } + { + version: encodeHitVersion({ + _seq_no: 100, + _primary_term: 200 + }) + } ); sinon.assert.calledOnce(callAdminCluster); sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({ - version: newVersion - 1 + if_seq_no: 100, + if_primary_term: 200, })); }); @@ -1204,7 +1222,6 @@ describe('SavedObjectsRepository', () => { sinon.assert.calledWithExactly(callAdminCluster, 'update', { type: 'doc', id: 'foo-namespace:index-pattern:logstash-*', - version: undefined, body: { doc: { updated_at: mockTimestamp, 'index-pattern': { title: 'Testing' } } }, @@ -1223,7 +1240,6 @@ describe('SavedObjectsRepository', () => { sinon.assert.calledWithExactly(callAdminCluster, 'update', { type: 'doc', id: 'index-pattern:logstash-*', - version: undefined, body: { doc: { updated_at: mockTimestamp, 'index-pattern': { title: 'Testing' } } }, @@ -1246,7 +1262,6 @@ describe('SavedObjectsRepository', () => { sinon.assert.calledWithExactly(callAdminCluster, 'update', { type: 'doc', id: 'globaltype:foo', - version: undefined, body: { doc: { updated_at: mockTimestamp, 'globaltype': { name: 'bar' } } }, @@ -1264,7 +1279,7 @@ describe('SavedObjectsRepository', () => { callAdminCluster.callsFake((method, params) => ({ _type: 'doc', _id: params.id, - _version: 2, + ...mockVersionProps, _index: '.kibana', get: { found: true, @@ -1284,7 +1299,7 @@ describe('SavedObjectsRepository', () => { callAdminCluster.callsFake((method, params) => ({ _type: 'doc', _id: params.id, - _version: 2, + ...mockVersionProps, _index: '.kibana', get: { found: true, @@ -1313,7 +1328,7 @@ describe('SavedObjectsRepository', () => { type: 'config', id: '6.0.0-alpha1', ...mockTimestampFields, - version: 2, + version: mockVersion, attributes: { buildNum: 8468, defaultIndex: 'logstash-*' @@ -1384,7 +1399,7 @@ describe('SavedObjectsRepository', () => { callAdminCluster.callsFake((method, params) => ({ _type: 'doc', _id: params.id, - _version: 2, + ...mockVersionProps, _index: '.kibana', get: { found: true, diff --git a/src/server/saved_objects/service/saved_objects_client.d.ts b/src/server/saved_objects/service/saved_objects_client.d.ts index a6e10aa1b85b10..36196c1ef250a2 100644 --- a/src/server/saved_objects/service/saved_objects_client.d.ts +++ b/src/server/saved_objects/service/saved_objects_client.d.ts @@ -58,7 +58,7 @@ export interface FindResponse { } export interface UpdateOptions extends BaseOptions { - version?: number; + version?: string; } export interface BulkGetObject { @@ -78,7 +78,7 @@ export interface SavedObjectAttributes { export interface SavedObject { id: string; type: string; - version?: number; + version?: string; updated_at?: string; error?: { message: string; diff --git a/src/server/saved_objects/version/base64.ts b/src/server/saved_objects/version/base64.ts new file mode 100644 index 00000000000000..b82b496a2fbb54 --- /dev/null +++ b/src/server/saved_objects/version/base64.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const decodeBase64 = (base64: string) => Buffer.from(base64, 'base64').toString('utf8'); +export const encodeBase64 = (utf8: string) => Buffer.from(utf8, 'utf8').toString('base64'); diff --git a/src/server/saved_objects/version/decode_request_version.test.ts b/src/server/saved_objects/version/decode_request_version.test.ts new file mode 100644 index 00000000000000..e2e9665be35cd7 --- /dev/null +++ b/src/server/saved_objects/version/decode_request_version.test.ts @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('./decode_version', () => ({ + decodeVersion: jest.fn().mockReturnValue({ _seq_no: 1, _primary_term: 2 }), +})); + +import { decodeRequestVersion } from './decode_request_version'; +import { decodeVersion } from './decode_version'; + +it('renames decodeVersion() return value to use if_seq_no and if_primary_term', () => { + expect(decodeRequestVersion('foobar')).toMatchInlineSnapshot(` +Object { + "if_primary_term": 2, + "if_seq_no": 1, +} +`); + expect(decodeVersion).toHaveBeenCalledWith('foobar'); +}); diff --git a/src/server/saved_objects/version/decode_request_version.ts b/src/server/saved_objects/version/decode_request_version.ts new file mode 100644 index 00000000000000..dc01262a664095 --- /dev/null +++ b/src/server/saved_objects/version/decode_request_version.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { decodeVersion } from './decode_version'; + +/** + * Helper for decoding version to request params that are driven + * by the version info + */ +export function decodeRequestVersion(version?: string) { + const decoded = decodeVersion(version); + return { + if_seq_no: decoded._seq_no, + if_primary_term: decoded._primary_term, + }; +} diff --git a/src/server/saved_objects/version/decode_version.test.ts b/src/server/saved_objects/version/decode_version.test.ts new file mode 100644 index 00000000000000..b157d97ae8a239 --- /dev/null +++ b/src/server/saved_objects/version/decode_version.test.ts @@ -0,0 +1,102 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Boom from 'boom'; + +import { decodeVersion } from './decode_version'; + +describe('decodeVersion', () => { + it('parses version back into {_seq_no,_primary_term} object', () => { + expect(decodeVersion('WzQsMV0=')).toMatchInlineSnapshot(` +Object { + "_primary_term": 1, + "_seq_no": 4, +} +`); + }); + + it('throws Boom error if not in base64', () => { + let error; + try { + decodeVersion('[1,4]'); + } catch (err) { + error = err; + } + + expect(error.message).toMatchInlineSnapshot(`"Invalid version [[1,4]]"`); + expect(Boom.isBoom(error)).toBe(true); + expect(error.output).toMatchInlineSnapshot(` +Object { + "headers": Object {}, + "payload": Object { + "error": "Bad Request", + "message": "Invalid version [[1,4]]", + "statusCode": 400, + }, + "statusCode": 400, +} +`); + }); + + it('throws if not JSON encoded', () => { + let error; + try { + decodeVersion('MSwy'); + } catch (err) { + error = err; + } + + expect(error.message).toMatchInlineSnapshot(`"Invalid version [MSwy]"`); + expect(Boom.isBoom(error)).toBe(true); + expect(error.output).toMatchInlineSnapshot(` +Object { + "headers": Object {}, + "payload": Object { + "error": "Bad Request", + "message": "Invalid version [MSwy]", + "statusCode": 400, + }, + "statusCode": 400, +} +`); + }); + + it('throws if either value is not an integer', () => { + let error; + try { + decodeVersion('WzEsMy41XQ=='); + } catch (err) { + error = err; + } + + expect(error.message).toMatchInlineSnapshot(`"Invalid version [WzEsMy41XQ==]"`); + expect(Boom.isBoom(error)).toBe(true); + expect(error.output).toMatchInlineSnapshot(` +Object { + "headers": Object {}, + "payload": Object { + "error": "Bad Request", + "message": "Invalid version [WzEsMy41XQ==]", + "statusCode": 400, + }, + "statusCode": 400, +} +`); + }); +}); diff --git a/src/server/saved_objects/version/decode_version.ts b/src/server/saved_objects/version/decode_version.ts new file mode 100644 index 00000000000000..92e06c080b087d --- /dev/null +++ b/src/server/saved_objects/version/decode_version.ts @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createInvalidVersionError } from '../service/lib/errors'; +import { decodeBase64 } from './base64'; + +/** + * Decode the "opaque" version string to the sequence params we + * can use to activate optimistic concurrency in Elasticsearch + */ +export function decodeVersion(version?: string) { + try { + if (typeof version !== 'string') { + throw new TypeError(); + } + + const seqParams = JSON.parse(decodeBase64(version)) as [number, number]; + + if ( + !Array.isArray(seqParams) || + seqParams.length !== 2 || + !Number.isInteger(seqParams[0]) || + !Number.isInteger(seqParams[1]) + ) { + throw new TypeError(); + } + + return { + _seq_no: seqParams[0], + _primary_term: seqParams[1], + }; + } catch (_) { + throw createInvalidVersionError(version); + } +} diff --git a/src/server/saved_objects/version/encode_hit_version.test.ts b/src/server/saved_objects/version/encode_hit_version.test.ts new file mode 100644 index 00000000000000..29c0ab59740cda --- /dev/null +++ b/src/server/saved_objects/version/encode_hit_version.test.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('./encode_version', () => ({ + encodeVersion: jest.fn().mockReturnValue('foo'), +})); + +import { encodeHitVersion } from './encode_hit_version'; +import { encodeVersion } from './encode_version'; + +it('renames decodeVersion() return value to use if_seq_no and if_primary_term', () => { + expect(encodeHitVersion({ _seq_no: 1, _primary_term: 2 })).toMatchInlineSnapshot(`"foo"`); + expect(encodeVersion).toHaveBeenCalledWith(1, 2); +}); diff --git a/src/server/saved_objects/version/encode_hit_version.ts b/src/server/saved_objects/version/encode_hit_version.ts new file mode 100644 index 00000000000000..bb0bd5e5c0d3ce --- /dev/null +++ b/src/server/saved_objects/version/encode_hit_version.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { encodeVersion } from './encode_version'; + +/** + * Helper for encoding a version from a "hit" (hits.hits[#] from _search) or + * "doc" (body from GET, update, etc) object + */ +export function encodeHitVersion(response: { _seq_no: number; _primary_term: number }) { + return encodeVersion(response._seq_no, response._primary_term); +} diff --git a/src/server/saved_objects/version/encode_version.test.ts b/src/server/saved_objects/version/encode_version.test.ts new file mode 100644 index 00000000000000..9f9d9140f939b1 --- /dev/null +++ b/src/server/saved_objects/version/encode_version.test.ts @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { encodeVersion } from './encode_version'; + +describe('encodeVersion', () => { + it('throws if primaryTerm is not an integer', () => { + expect(() => encodeVersion(1, undefined as any)).toThrowErrorMatchingInlineSnapshot( + `"_primary_term from elasticsearch must be an integer"` + ); + expect(() => encodeVersion(1, null as any)).toThrowErrorMatchingInlineSnapshot( + `"_primary_term from elasticsearch must be an integer"` + ); + expect(() => encodeVersion(1, {} as any)).toThrowErrorMatchingInlineSnapshot( + `"_primary_term from elasticsearch must be an integer"` + ); + expect(() => encodeVersion(1, [] as any)).toThrowErrorMatchingInlineSnapshot( + `"_primary_term from elasticsearch must be an integer"` + ); + expect(() => encodeVersion(1, 2.5 as any)).toThrowErrorMatchingInlineSnapshot( + `"_primary_term from elasticsearch must be an integer"` + ); + }); + + it('throws if seqNo is not an integer', () => { + expect(() => encodeVersion(undefined as any, 1)).toThrowErrorMatchingInlineSnapshot( + `"_seq_no from elasticsearch must be an integer"` + ); + expect(() => encodeVersion(null as any, 1)).toThrowErrorMatchingInlineSnapshot( + `"_seq_no from elasticsearch must be an integer"` + ); + expect(() => encodeVersion({} as any, 1)).toThrowErrorMatchingInlineSnapshot( + `"_seq_no from elasticsearch must be an integer"` + ); + expect(() => encodeVersion([] as any, 1)).toThrowErrorMatchingInlineSnapshot( + `"_seq_no from elasticsearch must be an integer"` + ); + expect(() => encodeVersion(2.5 as any, 1)).toThrowErrorMatchingInlineSnapshot( + `"_seq_no from elasticsearch must be an integer"` + ); + }); + + it('returns a base64 encoded, JSON string of seqNo and primaryTerm', () => { + expect(encodeVersion(123, 456)).toMatchInlineSnapshot(`"WzEyMyw0NTZd"`); + }); +}); diff --git a/src/server/saved_objects/version/encode_version.ts b/src/server/saved_objects/version/encode_version.ts new file mode 100644 index 00000000000000..9b0fcdfbab50c5 --- /dev/null +++ b/src/server/saved_objects/version/encode_version.ts @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { encodeBase64 } from './base64'; + +/** + * Encode the sequence params into an "opaque" version string + * that can be used in the saved object API in place of numeric + * version numbers + */ +export function encodeVersion(seqNo: number, primaryTerm: number) { + if (!Number.isInteger(primaryTerm)) { + throw new TypeError('_primary_term from elasticsearch must be an integer'); + } + + if (!Number.isInteger(seqNo)) { + throw new TypeError('_seq_no from elasticsearch must be an integer'); + } + + return encodeBase64(JSON.stringify([seqNo, primaryTerm])); +} diff --git a/src/server/saved_objects/version/index.ts b/src/server/saved_objects/version/index.ts new file mode 100644 index 00000000000000..73dce67462978f --- /dev/null +++ b/src/server/saved_objects/version/index.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './encode_version'; +export * from './encode_hit_version'; +export * from './decode_version'; +export * from './decode_request_version'; diff --git a/src/ui/public/courier/saved_object/__tests__/saved_object.js b/src/ui/public/courier/saved_object/__tests__/saved_object.js index 4d89ae5c19a665..a353e4f2b39e13 100644 --- a/src/ui/public/courier/saved_object/__tests__/saved_object.js +++ b/src/ui/public/courier/saved_object/__tests__/saved_object.js @@ -42,13 +42,13 @@ describe('Saved Object', function () { * that can be used to stub es calls. * @param indexPatternId * @param additionalOptions - object that will be assigned to the mocked doc response. - * @returns {{attributes: {}, type: string, id: *, _version: integer}} + * @returns {{attributes: {}, type: string, id: *, _version: string}} */ function getMockedDocResponse(indexPatternId, additionalOptions = {}) { return { type: 'dashboard', id: indexPatternId, - _version: 2, + _version: 'foo', attributes: {}, ...additionalOptions }; @@ -242,7 +242,11 @@ describe('Saved Object', function () { return createInitializedSavedObject({ type: 'dashboard' }).then(savedObject => { const mockDocResponse = getMockedDocResponse('myId'); sinon.stub(savedObjectsClientStub, 'create').callsFake(() => { - return BluebirdPromise.resolve({ type: 'dashboard', id: 'myId', _version: 2 }); + return BluebirdPromise.resolve({ + type: 'dashboard', + id: 'myId', + _version: 'foo' + }); }); stubESResponse(mockDocResponse); @@ -261,7 +265,9 @@ describe('Saved Object', function () { sinon.stub(savedObjectsClientStub, 'create').callsFake(() => { expect(savedObject.isSaving).to.be(true); return BluebirdPromise.resolve({ - type: 'dashboard', id, version: 2 + type: 'dashboard', + id, + version: 'foo' }); }); expect(savedObject.isSaving).to.be(false); @@ -451,7 +457,7 @@ describe('Saved Object', function () { attributes: { title: 'testIndexPattern' }, - _version: 2 + _version: 'foo' }); const savedObject = new SavedObject(config); diff --git a/src/ui/public/courier/saved_object/saved_object.js b/src/ui/public/courier/saved_object/saved_object.js index edbfa6b898f5c4..2139b257bc8a9f 100644 --- a/src/ui/public/courier/saved_object/saved_object.js +++ b/src/ui/public/courier/saved_object/saved_object.js @@ -208,7 +208,6 @@ export function SavedObjectProvider(Promise, Private, Notifier, confirmModalProm return savedObjectsClient.get(esType, this.id) .then(resp => { // temporary compatability for savedObjectsClient - return { _id: resp.id, _type: resp.type, diff --git a/src/ui/public/index_patterns/__tests__/_index_pattern.test.js b/src/ui/public/index_patterns/__tests__/_index_pattern.test.js index 5cfea76c4457a2..64f39634770b9f 100644 --- a/src/ui/public/index_patterns/__tests__/_index_pattern.test.js +++ b/src/ui/public/index_patterns/__tests__/_index_pattern.test.js @@ -86,7 +86,7 @@ jest.mock('../unsupported_time_patterns', () => ({ jest.mock('../../saved_objects', () => { const object = { - _version: 1, + _version: 'foo', _id: 'foo', attributes: { title: 'something' @@ -106,10 +106,11 @@ jest.mock('../../saved_objects', () => { } object.attributes.title = body.title; + object._version += 'a'; return { id: object._id, - _version: ++object._version, + _version: object._version, }; } }, @@ -137,13 +138,13 @@ describe('IndexPattern', () => { const pattern = new IndexPattern('foo'); await pattern.init(); - expect(pattern.version).toBe(2); + expect(pattern.version).toBe('fooa'); // Create the same one - we're going to handle concurrency const samePattern = new IndexPattern('foo'); await samePattern.init(); - expect(samePattern.version).toBe(3); + expect(samePattern.version).toBe('fooaa'); // This will conflict because samePattern did a save (from refreshFields) // but the resave should work fine diff --git a/src/ui/public/saved_objects/__tests__/saved_object.js b/src/ui/public/saved_objects/__tests__/saved_object.js index 8b2dd84d184f3a..4d8357a8bc76b0 100644 --- a/src/ui/public/saved_objects/__tests__/saved_object.js +++ b/src/ui/public/saved_objects/__tests__/saved_object.js @@ -47,7 +47,6 @@ describe('SavedObject', () => { const client = sinon.stub(); const savedObject = new SavedObject(client, { version }); - expect(savedObject._version).to.be(version); }); }); diff --git a/src/ui/public/saved_objects/__tests__/saved_objects_client.test.js b/src/ui/public/saved_objects/__tests__/saved_objects_client.test.js index 4a8de1d12b5e95..6819e9b7c4ad50 100644 --- a/src/ui/public/saved_objects/__tests__/saved_objects_client.test.js +++ b/src/ui/public/saved_objects/__tests__/saved_objects_client.test.js @@ -29,7 +29,7 @@ describe('SavedObjectsClient', () => { id: 'AVwSwFxtcMV38qjDZoQg', type: 'config', attributes: { title: 'Example title' }, - version: 2 + version: 'foo' }; let kfetchStub; @@ -228,8 +228,8 @@ describe('SavedObjectsClient', () => { test('makes HTTP call', () => { const attributes = { foo: 'Foo', bar: 'Bar' }; - const body = { attributes, version: 2 }; - const options = { version: 2 }; + const body = { attributes, version: 'foo' }; + const options = { version: 'foo' }; savedObjectsClient.update('index-pattern', 'logstash-*', attributes, options); sinon.assert.calledOnce(kfetchStub); diff --git a/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_saved_config.js b/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_saved_config.js index 9c36233d0d1f9f..8d865f81555efc 100644 --- a/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_saved_config.js +++ b/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_saved_config.js @@ -41,7 +41,7 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () { create: sinon.stub().callsFake(async (type, attributes, options = {}) => ({ type, id: options.id, - version: 1, + version: 'foo', })) }; diff --git a/test/api_integration/apis/saved_objects/bulk_create.js b/test/api_integration/apis/saved_objects/bulk_create.js index 153dda4691fa66..9a5ea63a621816 100644 --- a/test/api_integration/apis/saved_objects/bulk_create.js +++ b/test/api_integration/apis/saved_objects/bulk_create.js @@ -66,7 +66,7 @@ export default function ({ getService }) { type: 'dashboard', id: 'a01b2f57-fcfd-4864-b735-09e28f0d815e', updated_at: resp.body.saved_objects[1].updated_at, - version: 1, + version: 'WzgsMV0=', attributes: { title: 'A great new dashboard' } @@ -98,7 +98,7 @@ export default function ({ getService }) { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', updated_at: resp.body.saved_objects[0].updated_at, - version: 1, + version: 'WzAsMV0=', attributes: { title: 'An existing visualization' } @@ -107,7 +107,7 @@ export default function ({ getService }) { type: 'dashboard', id: 'a01b2f57-fcfd-4864-b735-09e28f0d815e', updated_at: resp.body.saved_objects[1].updated_at, - version: 1, + version: 'WzEsMV0=', attributes: { title: 'A great new dashboard' } diff --git a/test/api_integration/apis/saved_objects/create.js b/test/api_integration/apis/saved_objects/create.js index 516ca618da2ddb..3593578ba12939 100644 --- a/test/api_integration/apis/saved_objects/create.js +++ b/test/api_integration/apis/saved_objects/create.js @@ -48,7 +48,7 @@ export default function ({ getService }) { id: resp.body.id, type: 'visualization', updated_at: resp.body.updated_at, - version: 1, + version: 'WzgsMV0=', attributes: { title: 'My favorite vis' } @@ -86,7 +86,7 @@ export default function ({ getService }) { id: resp.body.id, type: 'visualization', updated_at: resp.body.updated_at, - version: 1, + version: 'WzAsMV0=', attributes: { title: 'My favorite vis' } diff --git a/test/api_integration/apis/saved_objects/find.js b/test/api_integration/apis/saved_objects/find.js index c9b1e9fc73f4a9..050a019f3d6b4c 100644 --- a/test/api_integration/apis/saved_objects/find.js +++ b/test/api_integration/apis/saved_objects/find.js @@ -42,7 +42,7 @@ export default function ({ getService }) { { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - version: 1, + version: 'WzIsMV0=', attributes: { 'title': 'Count of requests' } diff --git a/test/api_integration/apis/saved_objects/update.js b/test/api_integration/apis/saved_objects/update.js index e6ca3d0317bf30..f1fe77d45caac9 100644 --- a/test/api_integration/apis/saved_objects/update.js +++ b/test/api_integration/apis/saved_objects/update.js @@ -48,7 +48,7 @@ export default function ({ getService }) { id: resp.body.id, type: 'visualization', updated_at: resp.body.updated_at, - version: 2, + version: 'WzgsMV0=', attributes: { title: 'My second favorite vis' } diff --git a/x-pack/plugins/beats_management/server/lib/adapters/database/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/database/adapter_types.ts index ea86dc79bd6d08..c56bc6d72c7a7f 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/database/adapter_types.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/database/adapter_types.ts @@ -100,7 +100,8 @@ export interface DatabaseSearchResponse { _id: string; _score: number; _source: T; - _version?: number; + _seq_no?: number; + _primary_term?: number; _explanation?: DatabaseExplanation; fields?: any; highlight?: any; @@ -128,7 +129,8 @@ export interface DatabaseGetDocumentResponse { _index: string; _type: string; _id: string; - _version: number; + _seq_no: number; + _primary_term: number; found: boolean; _source: Source; } @@ -182,8 +184,8 @@ export interface DatabaseDeleteDocumentParams extends DatabaseGenericParams { refresh?: DatabaseRefresh; routing?: string; timeout?: string; - version?: number; - versionType?: DatabaseVersionType; + ifSeqNo?: number; + ifPrimaryTerm?: number; index: string; type: string; id: string; @@ -194,7 +196,8 @@ export interface DatabaseIndexDocumentResponse { _index: string; _type: string; _id: string; - _version: number; + _seq_no: number; + _primary_term: number; result: string; } @@ -203,7 +206,8 @@ export interface DatabaseUpdateDocumentResponse { _index: string; _type: string; _id: string; - _version: number; + _seq_no: number; + _primary_term: number; result: string; } @@ -212,7 +216,8 @@ export interface DatabaseDeleteDocumentResponse { _index: string; _type: string; _id: string; - _version: number; + _seq_no: number; + _primary_term: number; result: string; } @@ -225,8 +230,8 @@ export interface DatabaseIndexDocumentParams extends DatabaseGenericParams { timeout?: string; timestamp?: Date | number; ttl?: string; - version?: number; - versionType?: DatabaseVersionType; + ifSeqNo?: number; + ifPrimaryTerm?: number; pipeline?: string; id?: string; index: string; @@ -246,8 +251,8 @@ export interface DatabaseCreateDocumentParams extends DatabaseGenericParams { timeout?: string; timestamp?: Date | number; ttl?: string; - version?: number; - versionType?: DatabaseVersionType; + ifSeqNo?: number; + ifPrimaryTerm?: number; pipeline?: string; id?: string; index: string; @@ -265,8 +270,8 @@ export interface DatabaseDeleteDocumentParams extends DatabaseGenericParams { refresh?: DatabaseRefresh; routing?: string; timeout?: string; - version?: number; - versionType?: DatabaseVersionType; + ifSeqNo?: number; + ifPrimaryTerm?: number; index: string; type: string; id: string; @@ -282,8 +287,8 @@ export interface DatabaseGetParams extends DatabaseGenericParams { _source?: DatabaseNameList; _sourceExclude?: DatabaseNameList; _source_includes?: DatabaseNameList; - version?: number; - versionType?: DatabaseVersionType; + ifSeqNo?: number; + ifPrimaryTerm?: number; id: string; index: string; type: string; @@ -291,7 +296,6 @@ export interface DatabaseGetParams extends DatabaseGenericParams { export type DatabaseNameList = string | string[] | boolean; export type DatabaseRefresh = boolean | 'true' | 'false' | 'wait_for' | ''; -export type DatabaseVersionType = 'internal' | 'external' | 'external_gte' | 'force'; export type ExpandWildcards = 'open' | 'closed' | 'none' | 'all'; export type DefaultOperator = 'AND' | 'OR'; export type DatabaseConflicts = 'abort' | 'proceed'; @@ -310,6 +314,7 @@ export interface DatabaseDeleteDocumentResponse { _index: string; _type: string; _id: string; - _version: number; + _seq_no: number; + _primary_term: number; result: string; } diff --git a/x-pack/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts index f097994509ee4d..8bec4307d46eb5 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { get } from 'lodash'; import { INDEX_NAMES } from 'x-pack/plugins/beats_management/common/constants'; import { beatsIndexTemplate } from '../../../utils/index_templates'; import { FrameworkUser } from '../framework/adapter_types'; @@ -120,43 +119,6 @@ export class KibanaDatabaseAdapter implements DatabaseAdapter { return result; } - private async fetchAllFromScroll( - user: FrameworkUser, - response: DatabaseSearchResponse, - hits: DatabaseSearchResponse['hits']['hits'] = [] - ): Promise< - Array<{ - _index: string; - _type: string; - _id: string; - _score: number; - _source: Source; - _version?: number; - fields?: any; - highlight?: any; - inner_hits?: any; - sort?: string[]; - }> - > { - const newHits = get(response, 'hits.hits', []); - const scrollId = get(response, '_scroll_id'); - - if (newHits.length > 0) { - hits.push(...newHits); - - return this.callWithUser(user, 'scroll', { - body: { - scroll: '30s', - scroll_id: scrollId, - }, - }).then((innerResponse: DatabaseSearchResponse) => { - return this.fetchAllFromScroll(user, innerResponse, hits); - }); - } - - return Promise.resolve(hits); - } - private callWithUser(user: FrameworkUser, esMethod: string, options: any = {}): any { if (user.kind === 'authenticated') { return this.es.callWithRequest( diff --git a/x-pack/plugins/infra/public/graphql/introspection.json b/x-pack/plugins/infra/public/graphql/introspection.json index e64b567aa88efb..356bc03f19a8b3 100644 --- a/x-pack/plugins/infra/public/graphql/introspection.json +++ b/x-pack/plugins/infra/public/graphql/introspection.json @@ -89,7 +89,7 @@ "name": "version", "description": "The version number the source configuration was last persisted with", "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, @@ -430,6 +430,16 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "SCALAR", + "name": "String", + "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "SCALAR", "name": "Float", @@ -511,16 +521,6 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "SCALAR", - "name": "String", - "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, { "kind": "OBJECT", "name": "InfraSourceFields", diff --git a/x-pack/plugins/infra/public/graphql/types.ts b/x-pack/plugins/infra/public/graphql/types.ts index 0b311ec2b3e6d5..4044c2ab82590f 100644 --- a/x-pack/plugins/infra/public/graphql/types.ts +++ b/x-pack/plugins/infra/public/graphql/types.ts @@ -19,7 +19,7 @@ export interface InfraSource { /** The id of the source */ id: string; /** The version number the source configuration was last persisted with */ - version?: number | null; + version?: string | null; /** The timestamp the source configuration was last persisted at */ updatedAt?: number | null; /** The raw configuration of the source */ diff --git a/x-pack/plugins/infra/server/graphql/sources/schema.gql.ts b/x-pack/plugins/infra/server/graphql/sources/schema.gql.ts index b567ccd52dfc27..dd87e9ecb34cc4 100644 --- a/x-pack/plugins/infra/server/graphql/sources/schema.gql.ts +++ b/x-pack/plugins/infra/server/graphql/sources/schema.gql.ts @@ -12,7 +12,7 @@ export const sourcesSchema = gql` "The id of the source" id: ID! "The version number the source configuration was last persisted with" - version: Float + version: String "The timestamp the source configuration was last persisted at" updatedAt: Float "The raw configuration of the source" diff --git a/x-pack/plugins/infra/server/graphql/types.ts b/x-pack/plugins/infra/server/graphql/types.ts index 69332026338569..d1ffd4958b2607 100644 --- a/x-pack/plugins/infra/server/graphql/types.ts +++ b/x-pack/plugins/infra/server/graphql/types.ts @@ -47,7 +47,7 @@ export interface InfraSource { /** The id of the source */ id: string; /** The version number the source configuration was last persisted with */ - version?: number | null; + version?: string | null; /** The timestamp the source configuration was last persisted at */ updatedAt?: number | null; /** The raw configuration of the source */ @@ -606,7 +606,7 @@ export namespace InfraSourceResolvers { /** The id of the source */ id?: IdResolver; /** The version number the source configuration was last persisted with */ - version?: VersionResolver; + version?: VersionResolver; /** The timestamp the source configuration was last persisted at */ updatedAt?: UpdatedAtResolver; /** The raw configuration of the source */ @@ -635,7 +635,7 @@ export namespace InfraSourceResolvers { Context >; export type VersionResolver< - R = number | null, + R = string | null, Parent = InfraSource, Context = InfraContext > = Resolver; diff --git a/x-pack/plugins/infra/server/lib/sources/sources.test.ts b/x-pack/plugins/infra/server/lib/sources/sources.test.ts index b26f37eeefadc3..2374a83a642dfc 100644 --- a/x-pack/plugins/infra/server/lib/sources/sources.test.ts +++ b/x-pack/plugins/infra/server/lib/sources/sources.test.ts @@ -14,7 +14,7 @@ describe('the InfraSources lib', () => { configuration: createMockStaticConfiguration({}), savedObjects: createMockSavedObjectsService({ id: 'TEST_ID', - version: 1, + version: 'foo', updated_at: '2000-01-01T00:00:00.000Z', attributes: { metricAlias: 'METRIC_ALIAS', @@ -34,7 +34,7 @@ describe('the InfraSources lib', () => { expect(await sourcesLib.getSourceConfiguration(request, 'TEST_ID')).toMatchObject({ id: 'TEST_ID', - version: 1, + version: 'foo', updatedAt: 946684800000, configuration: { metricAlias: 'METRIC_ALIAS', @@ -66,7 +66,7 @@ describe('the InfraSources lib', () => { }), savedObjects: createMockSavedObjectsService({ id: 'TEST_ID', - version: 1, + version: 'foo', updated_at: '2000-01-01T00:00:00.000Z', attributes: { fields: { @@ -80,7 +80,7 @@ describe('the InfraSources lib', () => { expect(await sourcesLib.getSourceConfiguration(request, 'TEST_ID')).toMatchObject({ id: 'TEST_ID', - version: 1, + version: 'foo', updatedAt: 946684800000, configuration: { metricAlias: 'METRIC_ALIAS', @@ -101,7 +101,7 @@ describe('the InfraSources lib', () => { configuration: createMockStaticConfiguration({}), savedObjects: createMockSavedObjectsService({ id: 'TEST_ID', - version: 1, + version: 'foo', updated_at: '2000-01-01T00:00:00.000Z', attributes: {}, }), @@ -111,7 +111,7 @@ describe('the InfraSources lib', () => { expect(await sourcesLib.getSourceConfiguration(request, 'TEST_ID')).toMatchObject({ id: 'TEST_ID', - version: 1, + version: 'foo', updatedAt: 946684800000, configuration: { metricAlias: expect.any(String), diff --git a/x-pack/plugins/infra/server/lib/sources/types.ts b/x-pack/plugins/infra/server/lib/sources/types.ts index 3b1d3e4e49edbc..a70062fb3ddf56 100644 --- a/x-pack/plugins/infra/server/lib/sources/types.ts +++ b/x-pack/plugins/infra/server/lib/sources/types.ts @@ -51,7 +51,7 @@ export const InfraSavedSourceConfigurationRuntimeType = runtimeTypes.intersectio attributes: PartialInfraSourceConfigurationRuntimeType, }), runtimeTypes.partial({ - version: runtimeTypes.number, + version: runtimeTypes.string, updated_at: TimestampFromString, }), ]); diff --git a/x-pack/plugins/reporting/export_types/csv/server/lib/__tests__/field_format_map.js b/x-pack/plugins/reporting/export_types/csv/server/lib/__tests__/field_format_map.js index 4e545fb4c1a3aa..8035776166e9a7 100644 --- a/x-pack/plugins/reporting/export_types/csv/server/lib/__tests__/field_format_map.js +++ b/x-pack/plugins/reporting/export_types/csv/server/lib/__tests__/field_format_map.js @@ -17,7 +17,7 @@ describe('field format map', function () { const indexPatternSavedObject = { id: 'logstash-*', type: 'index-pattern', - version: 4, + version: 'abc', attributes: { title: 'logstash-*', timeFieldName: '@timestamp', diff --git a/x-pack/plugins/reporting/server/lib/esqueue/__tests__/fixtures/elasticsearch.js b/x-pack/plugins/reporting/server/lib/esqueue/__tests__/fixtures/elasticsearch.js index 4e64f426e90444..70cb912c8f721a 100644 --- a/x-pack/plugins/reporting/server/lib/esqueue/__tests__/fixtures/elasticsearch.js +++ b/x-pack/plugins/reporting/server/lib/esqueue/__tests__/fixtures/elasticsearch.js @@ -18,7 +18,8 @@ ClientMock.prototype.index = function (params = {}) { _index: params.index || 'index', _type: params.type || constants.DEFAULT_SETTING_DOCTYPE, _id: params.id || uniqueId('testDoc'), - _version: 1, + _seq_no: 1, + _primary_term: 1, _shards: { total: shardCount, successful: shardCount, failed: 0 }, created: true }); @@ -53,7 +54,8 @@ ClientMock.prototype.get = function (params = {}, source = {}) { _index: params.index || 'index', _type: params.type || constants.DEFAULT_SETTING_DOCTYPE, _id: params.id || 'AVRPRLnlp7Ur1SZXfT-T', - _version: params.version || 1, + _seq_no: params._seq_no || 1, + _primary_term: params._primary_term || 1, found: true, _source: _source }); @@ -65,7 +67,8 @@ ClientMock.prototype.search = function (params = {}, count = 5, source = {}) { _index: params.index || 'index', _type: params.type || constants.DEFAULT_SETTING_DOCTYPE, _id: uniqueId('documentId'), - _version: random(1, 5), + _seq_no: random(1, 5), + _primar_term: random(1, 5), _score: null, _source: { created_at: new Date().toString(), @@ -96,7 +99,8 @@ ClientMock.prototype.update = function (params = {}) { _index: params.index || 'index', _type: params.type || constants.DEFAULT_SETTING_DOCTYPE, _id: params.id || uniqueId('testDoc'), - _version: params.version + 1 || 2, + _seq_no: params.if_seq_no + 1 || 2, + _primary_term: params.if_primary_term + 1 || 2, _shards: { total: shardCount, successful: shardCount, failed: 0 }, created: true }); diff --git a/x-pack/plugins/reporting/server/lib/esqueue/__tests__/job.js b/x-pack/plugins/reporting/server/lib/esqueue/__tests__/job.js index 1bdc3589dbb3d6..7aefcf861f6876 100644 --- a/x-pack/plugins/reporting/server/lib/esqueue/__tests__/job.js +++ b/x-pack/plugins/reporting/server/lib/esqueue/__tests__/job.js @@ -135,7 +135,8 @@ describe('Job Class', function () { expect(jobDoc).to.have.property('id'); expect(jobDoc).to.have.property('index'); expect(jobDoc).to.have.property('type'); - expect(jobDoc).to.have.property('version'); + expect(jobDoc).to.have.property('_seq_no'); + expect(jobDoc).to.have.property('_primary_term'); done(); } catch (e) { done(e); @@ -383,7 +384,8 @@ describe('Job Class', function () { expect(doc).to.have.property('index', index); expect(doc).to.have.property('type', jobDoc.type); expect(doc).to.have.property('id', jobDoc.id); - expect(doc).to.have.property('version', jobDoc.version); + expect(doc).to.have.property('_seq_no', jobDoc._seq_no); + expect(doc).to.have.property('_primary_term', jobDoc._primary_term); expect(doc).to.have.property('created_by', defaultCreatedBy); expect(doc).to.have.property('payload'); diff --git a/x-pack/plugins/reporting/server/lib/esqueue/__tests__/worker.js b/x-pack/plugins/reporting/server/lib/esqueue/__tests__/worker.js index 15fa81954d7ddf..76098316b8d121 100644 --- a/x-pack/plugins/reporting/server/lib/esqueue/__tests__/worker.js +++ b/x-pack/plugins/reporting/server/lib/esqueue/__tests__/worker.js @@ -336,11 +336,6 @@ describe('Worker class', function () { searchStub = sinon.stub(mockQueue.client, 'search').callsFake(() => Promise.resolve({ hits: { hits: [] } })); }); - it('should query with version', function () { - const params = getSearchParams(); - expect(params).to.have.property('version', true); - }); - it('should query by default doctype', function () { const params = getSearchParams(); expect(params).to.have.property('type', constants.DEFAULT_SETTING_DOCTYPE); @@ -367,6 +362,11 @@ describe('Worker class', function () { clock.restore(); }); + it('should query with seq_no_primary_term', function () { + const { body } = getSearchParams(jobtype); + expect(body).to.have.property('seq_no_primary_term', true); + }); + it('should filter unwanted source data', function () { const excludedFields = [ 'output.content' ]; const { body } = getSearchParams(jobtype); @@ -432,7 +432,6 @@ describe('Worker class', function () { index: 'myIndex', type: 'test', id: 12345, - version: 3 }; return mockQueue.client.get(params) .then((jobDoc) => { @@ -446,13 +445,14 @@ describe('Worker class', function () { clock.restore(); }); - it('should use version on update', function () { + it('should use seqNo and primaryTerm on update', function () { worker._claimJob(job); const query = updateSpy.firstCall.args[0]; expect(query).to.have.property('index', job._index); expect(query).to.have.property('type', job._type); expect(query).to.have.property('id', job._id); - expect(query).to.have.property('version', job._version); + expect(query).to.have.property('if_seq_no', job._seq_no); + expect(query).to.have.property('if_primary_term', job._primary_term); }); it('should increment the job attempts', function () { @@ -500,7 +500,7 @@ describe('Worker class', function () { expect(msg).to.equal(false); }); - it('should reject the promise on version errors', function () { + it('should reject the promise on conflict errors', function () { mockQueue.client.update.restore(); sinon.stub(mockQueue.client, 'update').returns(Promise.reject({ statusCode: 409 })); return worker._claimJob(job) @@ -524,7 +524,8 @@ describe('Worker class', function () { _index: 'myIndex', _type: 'test', _id: 12345, - _version: 3, + _seq_no: 3, + _primary_term: 3, found: true, _source: { jobtype: 'jobtype', @@ -608,13 +609,14 @@ describe('Worker class', function () { clock.restore(); }); - it('should use version on update', function () { + it('should use _seq_no and _primary_term on update', function () { worker._failJob(job); const query = updateSpy.firstCall.args[0]; expect(query).to.have.property('index', job._index); expect(query).to.have.property('type', job._type); expect(query).to.have.property('id', job._id); - expect(query).to.have.property('version', job._version); + expect(query).to.have.property('if_seq_no', job._seq_no); + expect(query).to.have.property('if_primary_term', job._primary_term); }); it('should set status to failed', function () { @@ -631,7 +633,7 @@ describe('Worker class', function () { expect(doc.output).to.have.property('content', msg); }); - it('should return true on version mismatch errors', function () { + it('should return true on conflict errors', function () { mockQueue.client.update.restore(); sinon.stub(mockQueue.client, 'update').returns(Promise.reject({ statusCode: 409 })); return worker._failJob(job) @@ -735,7 +737,8 @@ describe('Worker class', function () { expect(query).to.have.property('index', job._index); expect(query).to.have.property('type', job._type); expect(query).to.have.property('id', job._id); - expect(query).to.have.property('version', job._version); + expect(query).to.have.property('if_seq_no', job._seq_no); + expect(query).to.have.property('if_primary_term', job._primary_term); expect(query.body.doc).to.have.property('output'); expect(query.body.doc.output).to.have.property('content_type', false); expect(query.body.doc.output).to.have.property('content', payload); diff --git a/x-pack/plugins/reporting/server/lib/esqueue/job.js b/x-pack/plugins/reporting/server/lib/esqueue/job.js index cf8eeacfbe92ed..097178a160626b 100644 --- a/x-pack/plugins/reporting/server/lib/esqueue/job.js +++ b/x-pack/plugins/reporting/server/lib/esqueue/job.js @@ -82,7 +82,8 @@ export class Job extends events.EventEmitter { id: doc._id, type: doc._type, index: doc._index, - version: doc._version, + _seq_no: doc._seq_no, + _primary_term: doc._primary_term, }; this.debug(`Job created in index ${this.index}`); @@ -118,7 +119,8 @@ export class Job extends events.EventEmitter { index: doc._index, id: doc._id, type: doc._type, - version: doc._version, + _seq_no: doc._seq_no, + _primary_term: doc._primary_term, }); }); } diff --git a/x-pack/plugins/reporting/server/lib/esqueue/worker.js b/x-pack/plugins/reporting/server/lib/esqueue/worker.js index c5fa020fb76c60..383f4cfb47e927 100644 --- a/x-pack/plugins/reporting/server/lib/esqueue/worker.js +++ b/x-pack/plugins/reporting/server/lib/esqueue/worker.js @@ -130,7 +130,8 @@ export class Worker extends events.EventEmitter { index: job._index, type: job._type, id: job._id, - version: job._version, + if_seq_no: job._seq_no, + if_primary_term: job._primary_term, body: { doc } }) .then((response) => { @@ -167,7 +168,8 @@ export class Worker extends events.EventEmitter { index: job._index, type: job._type, id: job._id, - version: job._version, + if_seq_no: job._seq_no, + if_primary_term: job._primary_term, body: { doc } }) .then(() => true) @@ -244,7 +246,8 @@ export class Worker extends events.EventEmitter { index: job._index, type: job._type, id: job._id, - version: job._version, + if_seq_no: job._seq_no, + if_primary_term: job._primary_term, body: { doc } }) .then(() => { @@ -351,6 +354,7 @@ export class Worker extends events.EventEmitter { _getPendingJobs() { const nowTime = moment().toISOString(); const query = { + seq_no_primary_term: true, _source: { excludes: [ 'output.content' ] }, @@ -385,7 +389,6 @@ export class Worker extends events.EventEmitter { return this.client.search({ index: `${this.queue.index}-*`, type: this.doctype, - version: true, body: query }) .then((results) => { diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.ts b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.ts index 2f69dbe34db1a5..4f5908eb335ad8 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.ts +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.ts @@ -215,7 +215,7 @@ export class SpacesSavedObjectsClient implements SavedObjectsClient { * @param {string} type * @param {string} id * @param {object} [options={}] - * @property {integer} options.version - ensures version matches that of persisted object + * @property {string} options.version - ensures version matches that of persisted object * @property {string} [options.namespace] * @returns {promise} */ diff --git a/x-pack/plugins/task_manager/lib/middleware.test.ts b/x-pack/plugins/task_manager/lib/middleware.test.ts index c249f86d159219..5d81420a319ffe 100644 --- a/x-pack/plugins/task_manager/lib/middleware.test.ts +++ b/x-pack/plugins/task_manager/lib/middleware.test.ts @@ -20,7 +20,8 @@ const getMockTaskInstance = () => ({ const getMockConcreteTaskInstance = () => { const concrete: { id: string; - version: number; + sequenceNumber: number; + primaryTerm: number; attempts: number; status: TaskStatus; runAt: Date; @@ -29,7 +30,8 @@ const getMockConcreteTaskInstance = () => { params: any; } = { id: 'hy8o99o83', - version: 1, + sequenceNumber: 1, + primaryTerm: 1, attempts: 0, status: 'idle', runAt: new Date(moment('2018-09-18T05:33:09.588Z').valueOf()), @@ -146,11 +148,12 @@ Object { "params": Object { "abc": "def", }, + "primaryTerm": 1, "runAt": 2018-09-18T05:33:09.588Z, + "sequenceNumber": 1, "state": Object {}, "status": "idle", "taskType": "nice_task", - "version": 1, }, } `); diff --git a/x-pack/plugins/task_manager/task.ts b/x-pack/plugins/task_manager/task.ts index 71f61ea0576b8e..01c2a24e0351d3 100644 --- a/x-pack/plugins/task_manager/task.ts +++ b/x-pack/plugins/task_manager/task.ts @@ -206,9 +206,14 @@ export interface ConcreteTaskInstance extends TaskInstance { id: string; /** - * The version of the Elaticsearch document. + * The sequence number from the Elaticsearch document. */ - version: number; + sequenceNumber: number; + + /** + * The primary term from the Elaticsearch document. + */ + primaryTerm: number; /** * The number of unsuccessful attempts since the last successful run. This diff --git a/x-pack/plugins/task_manager/task_runner.test.ts b/x-pack/plugins/task_manager/task_runner.test.ts index a3866c65f9d19e..10b65fbcd93d34 100644 --- a/x-pack/plugins/task_manager/task_runner.test.ts +++ b/x-pack/plugins/task_manager/task_runner.test.ts @@ -230,7 +230,8 @@ describe('TaskManagerRunner', () => { { id: 'foo', taskType: 'bar', - version: 32, + sequenceNumber: 32, + primaryTerm: 32, runAt: new Date(), attempts: 0, params: {}, diff --git a/x-pack/plugins/task_manager/task_store.test.ts b/x-pack/plugins/task_manager/task_store.test.ts index a76ba03de0faf4..48e3ff48ddca43 100644 --- a/x-pack/plugins/task_manager/task_store.test.ts +++ b/x-pack/plugins/task_manager/task_store.test.ts @@ -42,7 +42,8 @@ describe('TaskStore', () => { const callCluster = sinon.spy(() => Promise.resolve({ _id: 'testid', - _version: 3344, + _seq_no: 3344, + _primary_term: 3344, }) ); const store = new TaskStore({ @@ -90,7 +91,8 @@ describe('TaskStore', () => { expect(result).toMatchObject({ ...task, - version: 3344, + sequenceNumber: 3344, + primaryTerm: 3344, id: 'testid', }); }); @@ -257,7 +259,8 @@ describe('TaskStore', () => { status: 'idle', taskType: 'foo', user: 'jimbo', - version: undefined, + sequenceNumber: undefined, + primaryTerm: undefined, }, { attempts: 2, @@ -270,7 +273,8 @@ describe('TaskStore', () => { status: 'running', taskType: 'bar', user: 'dabo', - version: undefined, + sequenceNumber: undefined, + primaryTerm: undefined, }, ], searchAfter: ['b', 2], @@ -347,7 +351,7 @@ describe('TaskStore', () => { }, size: 10, sort: { 'task.runAt': { order: 'asc' } }, - version: true, + seq_no_primary_term: true, }, index, type: '_doc', @@ -408,7 +412,8 @@ describe('TaskStore', () => { status: 'idle', taskType: 'foo', user: 'jimbo', - version: undefined, + sequenceNumber: undefined, + primaryTerm: undefined, }, { attempts: 2, @@ -421,7 +426,8 @@ describe('TaskStore', () => { status: 'running', taskType: 'bar', user: 'dabo', - version: undefined, + sequenceNumber: undefined, + primaryTerm: undefined, }, ]); }); @@ -436,12 +442,17 @@ describe('TaskStore', () => { params: { hello: 'world' }, state: { foo: 'bar' }, taskType: 'report', - version: 2, + sequenceNumber: 2, + primaryTerm: 2, attempts: 3, status: 'idle' as TaskStatus, }; - const callCluster = sinon.spy(async () => ({ _version: task.version + 1 })); + const callCluster = sinon.spy(async () => ({ + _seq_no: task.sequenceNumber + 1, + _primary_term: task.primaryTerm + 1, + })); + const store = new TaskStore({ callCluster, index: 'tasky', @@ -458,12 +469,13 @@ describe('TaskStore', () => { id: task.id, index: 'tasky', type: '_doc', - version: 2, + if_seq_no: 2, + if_primary_term: 2, refresh: true, body: { doc: { task: { - ...['id', 'version'].reduce((acc, prop) => _.omit(acc, prop), task), + ..._.omit(task, ['id', 'sequenceNumber', 'primaryTerm']), params: JSON.stringify(task.params), state: JSON.stringify(task.state), }, @@ -471,7 +483,11 @@ describe('TaskStore', () => { }, }); - expect(result).toEqual({ ...task, version: 3 }); + expect(result).toEqual({ + ...task, + sequenceNumber: 3, + primaryTerm: 3, + }); }); }); @@ -482,7 +498,8 @@ describe('TaskStore', () => { Promise.resolve({ _index: 'myindex', _id: id, - _version: 32, + _seq_no: 32, + _primary_term: 32, result: 'deleted', }) ); @@ -500,7 +517,8 @@ describe('TaskStore', () => { expect(result).toEqual({ id, index: 'myindex', - version: 32, + sequenceNumber: 32, + primaryTerm: 32, result: 'deleted', }); diff --git a/x-pack/plugins/task_manager/task_store.ts b/x-pack/plugins/task_manager/task_store.ts index c3c1cd11a261d8..c6a48e139471c1 100644 --- a/x-pack/plugins/task_manager/task_store.ts +++ b/x-pack/plugins/task_manager/task_store.ts @@ -33,7 +33,8 @@ export interface FetchResult { export interface RemoveResult { index: string; id: string; - version: string; + sequenceNumber: number; + primaryTerm: number; result: string; } @@ -42,7 +43,8 @@ export interface RawTaskDoc { _id: string; _index: string; _type: string; - _version: number; + _seq_no: number; + _primary_term: number; _source: { type: string; task: { @@ -181,7 +183,8 @@ export class TaskStore { return { ...taskInstance, id: result._id, - version: result._version, + sequenceNumber: result._seq_no, + primaryTerm: result._primary_term, attempts: 0, status: task.status, runAt: task.runAt, @@ -227,7 +230,7 @@ export class TaskStore { }, size: 10, sort: { 'task.runAt': { order: 'asc' } }, - version: true, + seq_no_primary_term: true, }); return docs; @@ -243,14 +246,15 @@ export class TaskStore { public async update(doc: ConcreteTaskInstance): Promise { const rawDoc = taskDocToRaw(doc, this.index); - const { _version } = await this.callCluster('update', { + const result = await this.callCluster('update', { body: { doc: rawDoc._source, }, id: doc.id, index: this.index, type: DOC_TYPE, - version: doc.version, + if_seq_no: doc.sequenceNumber, + if_primary_term: doc.primaryTerm, // The refresh is important so that if we immediately look for work, // we don't pick up this task. refresh: true, @@ -258,7 +262,8 @@ export class TaskStore { return { ...doc, - version: _version, + sequenceNumber: result._seq_no, + primaryTerm: result._primary_term, }; } @@ -281,7 +286,8 @@ export class TaskStore { return { index: result._index, id: result._id, - version: result._version, + sequenceNumber: result._seq_no, + primaryTerm: result._primary_term, result: result.result, }; } @@ -338,7 +344,8 @@ function rawSource(doc: TaskInstance) { }; delete (source as any).id; - delete (source as any).version; + delete (source as any).sequenceNumber; + delete (source as any).primaryTerm; delete (source as any).type; return { @@ -356,7 +363,8 @@ function taskDocToRaw(doc: ConcreteTaskInstance, index: string): RawTaskDoc { _index: index, _source: { type, task }, _type: DOC_TYPE, - _version: doc.version, + _seq_no: doc.sequenceNumber, + _primary_term: doc.primaryTerm, }; } @@ -364,7 +372,8 @@ function rawToTaskDoc(doc: RawTaskDoc): ConcreteTaskInstance { return { ...doc._source.task, id: doc._id, - version: doc._version, + sequenceNumber: doc._seq_no, + primaryTerm: doc._primary_term, params: parseJSONField(doc._source.task.params, 'params', doc), state: parseJSONField(doc._source.task.state, 'state', doc), }; diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts index 6b706adc970d00..cbcf15e386118b 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts @@ -87,7 +87,7 @@ describe('ReindexActions', () => { type: REINDEX_OP_TYPE, id: '9', attributes: { indexName: 'hi', locked: moment().format() }, - version: 1, + version: 'foo', } as ReindexSavedObject, { newIndexName: 'test' } ); @@ -97,7 +97,7 @@ describe('ReindexActions', () => { expect(args[1]).toEqual('9'); expect(args[2].indexName).toEqual('hi'); expect(args[2].newIndexName).toEqual('test'); - expect(args[3]).toEqual({ version: 1 }); + expect(args[3]).toEqual({ version: 'foo' }); }); it('throws if the reindexOp is not locked', async () => { @@ -107,7 +107,7 @@ describe('ReindexActions', () => { type: REINDEX_OP_TYPE, id: '10', attributes: { indexName: 'hi', locked: null }, - version: 1, + version: 'foo', } as ReindexSavedObject, { newIndexName: 'test' } ) diff --git a/x-pack/test/api_integration/apis/infra/sources.ts b/x-pack/test/api_integration/apis/infra/sources.ts index 7decb45281dae2..d25a89d6f5b9cf 100644 --- a/x-pack/test/api_integration/apis/infra/sources.ts +++ b/x-pack/test/api_integration/apis/infra/sources.ts @@ -73,7 +73,7 @@ const sourcesTests: KbnTestProvider = ({ getService }) => { const { version, updatedAt, configuration, status } = response.data && response.data.createSource.source; - expect(version).to.be.greaterThan(0); + expect(version).to.be.a('string'); expect(updatedAt).to.be.greaterThan(0); expect(configuration.name).to.be('NAME'); expect(configuration.description).to.be('DESCRIPTION'); @@ -102,7 +102,7 @@ const sourcesTests: KbnTestProvider = ({ getService }) => { const { version, updatedAt, configuration, status } = response.data && response.data.createSource.source; - expect(version).to.be.greaterThan(0); + expect(version).to.be.a('string'); expect(updatedAt).to.be.greaterThan(0); expect(configuration.name).to.be('NAME'); expect(configuration.description).to.be(''); @@ -163,7 +163,7 @@ const sourcesTests: KbnTestProvider = ({ getService }) => { const { version } = creationResponse.data && creationResponse.data.createSource.source; - expect(version).to.be.greaterThan(0); + expect(version).to.be.a('string'); const deletionResponse = await client.mutate({ mutation: deleteSourceMutation, @@ -193,7 +193,7 @@ const sourcesTests: KbnTestProvider = ({ getService }) => { const { version: initialVersion, updatedAt: createdAt } = creationResponse.data && creationResponse.data.createSource.source; - expect(initialVersion).to.be.greaterThan(0); + expect(initialVersion).to.be.a('string'); expect(createdAt).to.be.greaterThan(0); const updateResponse = await client.mutate({ @@ -233,7 +233,8 @@ const sourcesTests: KbnTestProvider = ({ getService }) => { const { version, updatedAt, configuration, status } = updateResponse.data && updateResponse.data.updateSource.source; - expect(version).to.be.greaterThan(initialVersion); + expect(version).to.be.a('string'); + expect(version).to.not.be(initialVersion); expect(updatedAt).to.be.greaterThan(createdAt); expect(configuration.name).to.be('UPDATED_NAME'); expect(configuration.description).to.be('UPDATED_DESCRIPTION'); @@ -262,7 +263,7 @@ const sourcesTests: KbnTestProvider = ({ getService }) => { const { version: initialVersion, updatedAt: createdAt } = creationResponse.data && creationResponse.data.createSource.source; - expect(initialVersion).to.be.greaterThan(0); + expect(initialVersion).to.be.a('string'); expect(createdAt).to.be.greaterThan(0); const updateResponse = await client.mutate({ @@ -282,7 +283,8 @@ const sourcesTests: KbnTestProvider = ({ getService }) => { const { version, updatedAt, configuration, status } = updateResponse.data && updateResponse.data.updateSource.source; - expect(version).to.be.greaterThan(initialVersion); + expect(version).to.be.a('string'); + expect(version).to.not.be(initialVersion); expect(updatedAt).to.be.greaterThan(createdAt); expect(configuration.metricAlias).to.be('metricbeat-**'); expect(configuration.logAlias).to.be('filebeat-*'); @@ -304,7 +306,7 @@ const sourcesTests: KbnTestProvider = ({ getService }) => { const { version: initialVersion, updatedAt: createdAt } = creationResponse.data && creationResponse.data.createSource.source; - expect(initialVersion).to.be.greaterThan(0); + expect(initialVersion).to.be.a('string'); expect(createdAt).to.be.greaterThan(0); const updateResponse = await client.mutate({ @@ -324,7 +326,8 @@ const sourcesTests: KbnTestProvider = ({ getService }) => { const { version, updatedAt, configuration } = updateResponse.data && updateResponse.data.updateSource.source; - expect(version).to.be.greaterThan(initialVersion); + expect(version).to.be.a('string'); + expect(version).to.not.be(initialVersion); expect(updatedAt).to.be.greaterThan(createdAt); expect(configuration.fields.container).to.be('UPDATED_CONTAINER'); expect(configuration.fields.host).to.be('host.name'); diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts index cb93afedcb1d94..1bcff51844d8d6 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts @@ -93,7 +93,7 @@ export function bulkCreateTestSuiteFactory(es: any, esArchiver: any, supertest: type: 'dashboard', id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`, updated_at: resp.body.saved_objects[1].updated_at, - version: 1, + version: resp.body.saved_objects[1].version, attributes: { title: 'A great new dashboard', }, @@ -102,7 +102,7 @@ export function bulkCreateTestSuiteFactory(es: any, esArchiver: any, supertest: type: 'globaltype', id: `05976c65-1145-4858-bbf0-d225cc78a06e`, updated_at: resp.body.saved_objects[2].updated_at, - version: 1, + version: resp.body.saved_objects[2].version, attributes: { name: 'A new globaltype object', }, diff --git a/x-pack/test/saved_object_api_integration/common/suites/create.ts b/x-pack/test/saved_object_api_integration/common/suites/create.ts index 87c94c1d13b28f..8a210b6ca944d0 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/create.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/create.ts @@ -69,7 +69,7 @@ export function createTestSuiteFactory(es: any, esArchiver: any, supertest: Supe id: resp.body.id, type: spaceAwareType, updated_at: resp.body.updated_at, - version: 1, + version: resp.body.version, attributes: { title: 'My favorite vis', }, @@ -109,7 +109,7 @@ export function createTestSuiteFactory(es: any, esArchiver: any, supertest: Supe id: resp.body.id, type: notSpaceAwareType, updated_at: resp.body.updated_at, - version: 1, + version: resp.body.version, attributes: { name: `Can't be contained to a space`, }, diff --git a/x-pack/test/saved_object_api_integration/common/suites/find.ts b/x-pack/test/saved_object_api_integration/common/suites/find.ts index d7bc0180f8e2bf..54226dfde21284 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/find.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/find.ts @@ -72,7 +72,7 @@ export function findTestSuiteFactory(esArchiver: any, supertest: SuperTest) { type: 'globaltype', id: `8121a00-8efd-21e7-1cb3-34ab966434445`, - version: 1, + version: resp.body.saved_objects[0].version, attributes: { name: 'My favorite global object', }, @@ -104,7 +104,7 @@ export function findTestSuiteFactory(esArchiver: any, supertest: SuperTest) { type: 'visualization', id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, - version: 1, + version: resp.body.saved_objects[0].version, attributes: { title: 'Count of requests', }, diff --git a/x-pack/test/saved_object_api_integration/common/suites/update.ts b/x-pack/test/saved_object_api_integration/common/suites/update.ts index e45fa1928b809d..93304de357f21f 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/update.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/update.ts @@ -83,7 +83,7 @@ export function updateTestSuiteFactory(esArchiver: any, supertest: SuperTest