From 9af2fd6f18f1d265799ba0de4a50a39309357109 Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 4 Feb 2019 21:13:34 -0800 Subject: [PATCH] Remove dependency on doc versions (#29906) See https://github.com/elastic/elasticsearch/pull/38254 Using the `version` parameter to implement optimistic concurrency is not going to be supported in 7.0, so we need to replace our usage of document version with the new `_seq_no` and `_primary_term` parameters. These fields are returned in the same way that `_version` was returned on all read/write requests except for search, where it needs to be requested by sending `seq_no_primary_term: true` in the body of the search request. These parameters are sent back to Elasticsearch on write requests with the `if_seq_no` and `if_primary_term` parameters, and are functionally equivalent to sending a `version` in a write request before elastic/elasticsearch#38254. To make these updates I searched the code base for uses of a `version` and `_version`, then triaged each usage, so I'm fairly confident that I got everything but it's possible something slipped through the cracks, so if you know of any usage of the document version field please help me out by double checking that I converted it. - [x] **Saved Objects**: @elastic/kibana-platform, @elastic/es-security - for BWC and ergonomics the `version` provided by the Saved Objects client/API was not removed, it was converted from a number to a string whose value is `base64(json([_seq_no, _primary_term]))`. This allows the Saved Objects API and its consumers to remain mostly unmodified, as long as the underlying value in the version field is irrelevant. This was the case for all usages in Kibana, only thing that needed updating was tests and TS types. - [x] **Reporting/esqueue**: @joelgriffith, @tsullivan - the version parameter was used here specifically for implementing optimistic concurrency, and since its usage was contained within the esqueue module I just updated it to use the new `_seq_no` and `_primary_term` fields. - [x] **Task Manager**: @tsullivan @njd5475 - Like esqueue this module uses version for optimistic concurrency but the usage is contained with the module so I just updated it to use, store, and request the `_seq_no` and `_primary_term` fields. - [ ] **ML**: @elastic/ml-ui - Best I could tell the only "version" in the ML code refers to the stack version, https://github.com/elastic/kibana/commit/077245fed8eb83f03cb01efe58440e9ed6e45ee2 - [ ] **Beats CM**: @elastic/beats - Looks like the references to `_version` in the code is only in the types but not in the code itself. I updated the types to use `_seq_no` and `_primary_term`, and their camelCase equivalents where appropriate. I did find a method that used one of the types referencing version but when investigating its usage it seemed the only consumer of that method was itself so i removed it. https://github.com/elastic/kibana/pull/29906/commits/52d890fed7126eb2a154b0b9e9de13bd619d9c60 - [x] **Spaces (tests)**: @elastic/kibana-security - The spaces test helpers use saved objects with versions in a number of places, so I updated them to use the new string versions where the version was predictable, and removed the assertion on version where it wasn't. We test the version in the saved objects code so this should be fine. --- .../lib/__tests__/health_check.js | 1 - .../saved_objects/routes/bulk_create.js | 2 +- .../saved_objects/routes/bulk_get.test.js | 2 +- src/server/saved_objects/routes/update.js | 2 +- .../saved_objects/routes/update.test.js | 2 +- .../saved_objects/serialization/index.ts | 18 +++- .../serialization/serialization.test.ts | 67 ++++++++++-- .../saved_objects/service/lib/errors.d.ts | 3 + .../saved_objects/service/lib/errors.js | 9 ++ .../saved_objects/service/lib/repository.js | 23 ++-- .../service/lib/repository.test.js | 83 ++++++++------ .../service/saved_objects_client.d.ts | 4 +- src/server/saved_objects/version/base64.ts | 21 ++++ .../version/decode_request_version.test.ts | 35 ++++++ .../version/decode_request_version.ts | 32 ++++++ .../version/decode_version.test.ts | 102 ++++++++++++++++++ .../saved_objects/version/decode_version.ts | 51 +++++++++ .../version/encode_hit_version.test.ts | 30 ++++++ .../version/encode_hit_version.ts | 28 +++++ .../version/encode_version.test.ts | 62 +++++++++++ .../saved_objects/version/encode_version.ts | 37 +++++++ src/server/saved_objects/version/index.ts | 23 ++++ .../saved_object/__tests__/saved_object.js | 16 ++- .../courier/saved_object/saved_object.js | 1 - .../__tests__/_index_pattern.test.js | 9 +- .../saved_objects/__tests__/saved_object.js | 1 - .../__tests__/saved_objects_client.test.js | 6 +- .../create_or_upgrade_saved_config.js | 2 +- .../apis/saved_objects/bulk_create.js | 6 +- .../apis/saved_objects/create.js | 4 +- .../apis/saved_objects/find.js | 2 +- .../apis/saved_objects/update.js | 2 +- .../lib/adapters/database/adapter_types.ts | 39 ++++--- .../database/kibana_database_adapter.ts | 38 ------- .../infra/public/graphql/introspection.json | 22 ++-- x-pack/plugins/infra/public/graphql/types.ts | 2 +- .../server/graphql/sources/schema.gql.ts | 2 +- x-pack/plugins/infra/server/graphql/types.ts | 6 +- .../infra/server/lib/sources/sources.test.ts | 12 +-- .../plugins/infra/server/lib/sources/types.ts | 2 +- .../server/lib/__tests__/field_format_map.js | 2 +- .../__tests__/fixtures/elasticsearch.js | 12 ++- .../server/lib/esqueue/__tests__/job.js | 6 +- .../server/lib/esqueue/__tests__/worker.js | 31 +++--- .../reporting/server/lib/esqueue/job.js | 6 +- .../reporting/server/lib/esqueue/worker.js | 11 +- .../spaces_saved_objects_client.ts | 2 +- .../task_manager/lib/middleware.test.ts | 9 +- x-pack/plugins/task_manager/task.ts | 9 +- .../plugins/task_manager/task_runner.test.ts | 3 +- .../plugins/task_manager/task_store.test.ts | 46 +++++--- x-pack/plugins/task_manager/task_store.ts | 31 ++++-- .../lib/reindexing/reindex_actions.test.ts | 6 +- .../api_integration/apis/infra/sources.ts | 21 ++-- .../common/suites/bulk_create.ts | 4 +- .../common/suites/create.ts | 4 +- .../common/suites/find.ts | 4 +- .../common/suites/update.ts | 4 +- 58 files changed, 773 insertions(+), 247 deletions(-) create mode 100644 src/server/saved_objects/version/base64.ts create mode 100644 src/server/saved_objects/version/decode_request_version.test.ts create mode 100644 src/server/saved_objects/version/decode_request_version.ts create mode 100644 src/server/saved_objects/version/decode_version.test.ts create mode 100644 src/server/saved_objects/version/decode_version.ts create mode 100644 src/server/saved_objects/version/encode_hit_version.test.ts create mode 100644 src/server/saved_objects/version/encode_hit_version.ts create mode 100644 src/server/saved_objects/version/encode_version.test.ts create mode 100644 src/server/saved_objects/version/encode_version.ts create mode 100644 src/server/saved_objects/version/index.ts 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