diff --git a/package.json b/package.json index 38d7d3bd840ccb..ec8d9ab098a4b3 100644 --- a/package.json +++ b/package.json @@ -189,6 +189,7 @@ "rimraf": "2.4.3", "rison-node": "1.0.0", "rjs-repack-loader": "1.0.6", + "rxjs": "5.4.3", "script-loader": "0.6.1", "semver": "5.1.0", "style-loader": "0.12.3", diff --git a/src/cli/cluster/cluster_manager.js b/src/cli/cluster/cluster_manager.js index d41bcc59fcbe73..845cf5be6c6fa2 100644 --- a/src/cli/cluster/cluster_manager.js +++ b/src/cli/cluster/cluster_manager.js @@ -15,7 +15,6 @@ export default class ClusterManager { const serverArgv = []; const optimizerArgv = [ '--plugins.initialize=false', - '--uiSettings.enabled=false', '--server.autoListen=false', ]; diff --git a/src/cli/serve/__tests__/fixtures/reload_logging_config/kibana.test.yml b/src/cli/serve/__tests__/fixtures/reload_logging_config/kibana.test.yml index 22c5e93375c5f9..23f33940283c0d 100644 --- a/src/cli/serve/__tests__/fixtures/reload_logging_config/kibana.test.yml +++ b/src/cli/serve/__tests__/fixtures/reload_logging_config/kibana.test.yml @@ -4,3 +4,5 @@ logging: json: true optimize: enabled: false +plugins: + initialize: false diff --git a/src/cli/serve/__tests__/reload_logging_config.js b/src/cli/serve/__tests__/reload_logging_config.js index eb67ebf6b11f15..19ff760746e210 100644 --- a/src/cli/serve/__tests__/reload_logging_config.js +++ b/src/cli/serve/__tests__/reload_logging_config.js @@ -72,7 +72,7 @@ describe(`Server logging configuration`, function () { } function switchToPlainTextLog() { - json = 3; // ignore both "reloading" messages + ui settings status message + json = 2; // ignore both "reloading" messages setLoggingJson(false); child.kill(`SIGHUP`); // reload logging config } diff --git a/src/core_plugins/elasticsearch/index.js b/src/core_plugins/elasticsearch/index.js index 22b33531acbf40..411d6a711eab06 100644 --- a/src/core_plugins/elasticsearch/index.js +++ b/src/core_plugins/elasticsearch/index.js @@ -119,9 +119,14 @@ export default function (kibana) { createProxy(server, 'POST', '/{index}/_search'); createProxy(server, 'POST', '/_msearch'); + server.expose('waitUntilReady', () => { + return new Promise(resolve => { + this.status.once('green', resolve); + }); + }); + // Set up the health check service and start it. - const { start, waitUntilReady } = healthCheck(this, server); - server.expose('waitUntilReady', waitUntilReady); + const { start } = healthCheck(this, server); start(); } }); diff --git a/src/core_plugins/elasticsearch/lib/__tests__/create_kibana_index.js b/src/core_plugins/elasticsearch/lib/__tests__/create_kibana_index.js deleted file mode 100644 index 8b243ea962001a..00000000000000 --- a/src/core_plugins/elasticsearch/lib/__tests__/create_kibana_index.js +++ /dev/null @@ -1,120 +0,0 @@ -import _ from 'lodash'; -import sinon from 'sinon'; -import expect from 'expect.js'; -import Promise from 'bluebird'; -import mappings from './fixtures/mappings'; -import createKibanaIndex from '../create_kibana_index'; - -describe('plugins/elasticsearch', function () { - describe('lib/create_kibana_index', function () { - - let server; - let callWithInternalUser; - let cluster; - - beforeEach(function () { - server = {}; - - let config = { kibana: { index: '.my-kibana' } }; - const get = sinon.stub(); - - get.returns(config); - get.withArgs('kibana.index').returns(config.kibana.index); - config = function () { return { get: get }; }; - - _.set(server, 'config', config); - _.set(server, 'getKibanaIndexMappingsDsl', sinon.stub().returns(mappings)); - - callWithInternalUser = sinon.stub(); - cluster = { callWithInternalUser: callWithInternalUser }; - - _.set(server, 'plugins.elasticsearch.getCluster', sinon.stub().withArgs('admin').returns(cluster)); - }); - - describe('successful requests', function () { - beforeEach(function () { - callWithInternalUser.withArgs('indices.create', sinon.match.any).returns(Promise.resolve()); - callWithInternalUser.withArgs('cluster.health', sinon.match.any).returns(Promise.resolve()); - }); - - it('should check cluster.health upon successful index creation', function () { - const fn = createKibanaIndex(server); - return fn.then(function () { - sinon.assert.calledOnce(callWithInternalUser.withArgs('cluster.health', sinon.match.any)); - }); - }); - - it('should be created with mappings for config.buildNum', function () { - const fn = createKibanaIndex(server); - return fn.then(function () { - const params = callWithInternalUser.args[0][1]; - expect(params) - .to.have.property('body'); - expect(params.body) - .to.have.property('mappings'); - expect(params.body.mappings) - .to.have.property('config'); - expect(params.body.mappings.config) - .to.have.property('properties'); - expect(params.body.mappings.config.properties) - .to.have.property('buildNum'); - expect(params.body.mappings.config.properties.buildNum) - .to.have.property('type', 'keyword'); - }); - }); - - it('should be created with 1 shard and default replica', function () { - const fn = createKibanaIndex(server); - return fn.then(function () { - const params = callWithInternalUser.args[0][1]; - expect(params) - .to.have.property('body'); - expect(params.body) - .to.have.property('settings'); - expect(params.body.settings) - .to.have.property('number_of_shards', 1); - expect(params.body.settings) - .to.not.have.property('number_of_replicas'); - }); - }); - - it('should be created with index name set in the config', function () { - const fn = createKibanaIndex(server); - return fn.then(function () { - const params = callWithInternalUser.args[0][1]; - expect(params) - .to.have.property('index', '.my-kibana'); - }); - }); - }); - - describe('failure requests', function () { - it('should reject with an Error', function () { - const error = new Error('Oops!'); - callWithInternalUser.withArgs('indices.create', sinon.match.any).returns(Promise.reject(error)); - const fn = createKibanaIndex(server); - return fn.catch(function (err) { - expect(err).to.be.a(Error); - }); - }); - - it('should reject with an error if index creation fails', function () { - const error = new Error('Oops!'); - callWithInternalUser.withArgs('indices.create', sinon.match.any).returns(Promise.reject(error)); - const fn = createKibanaIndex(server); - return fn.catch(function (err) { - expect(err.message).to.be('Unable to create Kibana index ".my-kibana"'); - }); - }); - - it('should reject with an error if health check fails', function () { - callWithInternalUser.withArgs('indices.create', sinon.match.any).returns(Promise.resolve()); - callWithInternalUser.withArgs('cluster.health', sinon.match.any).returns(Promise.reject(new Error())); - const fn = createKibanaIndex(server); - return fn.catch(function (err) { - expect(err.message).to.be('Waiting for Kibana index ".my-kibana" to come online failed.'); - }); - }); - }); - }); -}); diff --git a/src/core_plugins/elasticsearch/lib/__tests__/fixtures/mappings.js b/src/core_plugins/elasticsearch/lib/__tests__/fixtures/mappings.js deleted file mode 100644 index 0c9cf770bef8c0..00000000000000 --- a/src/core_plugins/elasticsearch/lib/__tests__/fixtures/mappings.js +++ /dev/null @@ -1,13 +0,0 @@ -export default { - '_default_': { - 'dynamic': 'strict' - }, - config: { - dynamic: true, - properties: { - buildNum: { - type: 'keyword' - } - } - } -}; diff --git a/src/core_plugins/elasticsearch/lib/__tests__/health_check.js b/src/core_plugins/elasticsearch/lib/__tests__/health_check.js index af6d8df3777589..f3ff737352dc5d 100644 --- a/src/core_plugins/elasticsearch/lib/__tests__/health_check.js +++ b/src/core_plugins/elasticsearch/lib/__tests__/health_check.js @@ -4,12 +4,9 @@ import expect from 'expect.js'; const NoConnections = require('elasticsearch').errors.NoConnections; -import mappings from './fixtures/mappings'; import healthCheck from '../health_check'; import kibanaVersion from '../kibana_version'; import { esTestConfig } from '../../../../test_utils/es'; -import * as patchKibanaIndexNS from '../patch_kibana_index'; -import * as migrateConfigNS from '../migrate_config'; const esPort = esTestConfig.getPort(); const esUrl = esTestConfig.getUrl(); @@ -33,8 +30,6 @@ describe('plugins/elasticsearch', () => { // Stub the Kibana version instead of drawing from package.json. sandbox.stub(kibanaVersion, 'get').returns(COMPATIBLE_VERSION_NUMBER); - sandbox.stub(patchKibanaIndexNS, 'patchKibanaIndex'); - sandbox.stub(migrateConfigNS, 'migrateConfig'); // setup the plugin stub plugin = { @@ -80,9 +75,6 @@ describe('plugins/elasticsearch', () => { getCluster: sinon.stub().returns(cluster) } }, - getKibanaIndexMappingsDsl() { - return mappings; - }, ext: sinon.stub() }; @@ -126,7 +118,6 @@ describe('plugins/elasticsearch', () => { sinon.assert.calledOnce(cluster.callWithInternalUser.withArgs('ping')); sinon.assert.calledTwice(cluster.callWithInternalUser.withArgs('nodes.info', sinon.match.any)); - sinon.assert.calledOnce(cluster.callWithInternalUser.withArgs('cluster.health', sinon.match.any)); sinon.assert.notCalled(plugin.status.red); sinon.assert.calledOnce(plugin.status.green); @@ -155,68 +146,9 @@ describe('plugins/elasticsearch', () => { sinon.assert.calledTwice(ping); sinon.assert.calledTwice(cluster.callWithInternalUser.withArgs('nodes.info', sinon.match.any)); - sinon.assert.calledOnce(cluster.callWithInternalUser.withArgs('cluster.health', sinon.match.any)); - sinon.assert.calledOnce(plugin.status.green); - expect(plugin.status.green.args[0][0]).to.be('Kibana index ready'); - }); - }); - - it('should set the cluster red if the health check status is red, then to green', function () { - cluster.callWithInternalUser.withArgs('ping').returns(Promise.resolve()); - - const clusterHealth = cluster.callWithInternalUser.withArgs('cluster.health', sinon.match.any); - clusterHealth.onCall(0).returns(Promise.resolve({ timed_out: false, status: 'red' })); - clusterHealth.onCall(1).returns(Promise.resolve({ timed_out: false, status: 'green' })); - - return health.run() - .then(function () { - sinon.assert.calledOnce(plugin.status.yellow); - expect(plugin.status.yellow.args[0][0]).to.be('Waiting for Elasticsearch'); - sinon.assert.calledOnce(plugin.status.red); - expect(plugin.status.red.args[0][0]).to.be( - 'Elasticsearch is still initializing the kibana index.' - ); - sinon.assert.calledOnce(cluster.callWithInternalUser.withArgs('ping')); - sinon.assert.calledTwice(cluster.callWithInternalUser.withArgs('nodes.info', sinon.match.any)); - sinon.assert.calledTwice(cluster.callWithInternalUser.withArgs('cluster.health', sinon.match.any)); sinon.assert.calledOnce(plugin.status.green); expect(plugin.status.green.args[0][0]).to.be('Kibana index ready'); }); }); - - it('should set the cluster yellow if the health check timed_out and create index', function () { - cluster.callWithInternalUser.withArgs('ping').returns(Promise.resolve()); - - const clusterHealth = cluster.callWithInternalUser.withArgs('cluster.health', sinon.match.any); - clusterHealth.onCall(0).returns(Promise.resolve({ timed_out: true, status: 'red' })); - clusterHealth.onCall(1).returns(Promise.resolve({ timed_out: false, status: 'green' })); - - cluster.callWithInternalUser.withArgs('indices.create', sinon.match.any).returns(Promise.resolve()); - - return health.run() - .then(function () { - sinon.assert.calledTwice(plugin.status.yellow); - expect(plugin.status.yellow.args[0][0]).to.be('Waiting for Elasticsearch'); - expect(plugin.status.yellow.args[1][0]).to.be('No existing Kibana index found'); - - sinon.assert.calledOnce(cluster.callWithInternalUser.withArgs('ping')); - sinon.assert.calledOnce(cluster.callWithInternalUser.withArgs('indices.create', sinon.match.any)); - sinon.assert.calledTwice(cluster.callWithInternalUser.withArgs('nodes.info', sinon.match.any)); - sinon.assert.calledTwice(clusterHealth); - }); - }); - - describe('#waitUntilReady', function () { - it('polls health until index is ready', function () { - const clusterHealth = cluster.callWithInternalUser.withArgs('cluster.health', sinon.match.any); - clusterHealth.onCall(0).returns(Promise.resolve({ timed_out: true })); - clusterHealth.onCall(1).returns(Promise.resolve({ status: 'red' })); - clusterHealth.onCall(2).returns(Promise.resolve({ status: 'green' })); - - return health.waitUntilReady().then(function () { - sinon.assert.calledThrice(clusterHealth); - }); - }); - }); }); }); diff --git a/src/core_plugins/elasticsearch/lib/__tests__/is_upgradeable.js b/src/core_plugins/elasticsearch/lib/__tests__/is_upgradeable.js deleted file mode 100644 index 729ab19d58fadc..00000000000000 --- a/src/core_plugins/elasticsearch/lib/__tests__/is_upgradeable.js +++ /dev/null @@ -1,72 +0,0 @@ -import _ from 'lodash'; -import expect from 'expect.js'; - -import isUpgradeable from '../is_upgradeable'; -import { pkg } from '../../../../utils'; -let version = pkg.version; - -describe('plugins/elasticsearch', function () { - describe('lib/is_upgradeable', function () { - const server = { - config: _.constant({ - get: function (key) { - switch (key) { - case 'pkg.version': return version; - default: throw new Error(`no stub for config key ${key}`); - } - } - }) - }; - - function upgradeDoc(id, _version, bool) { - describe('', function () { - before(function () { version = _version; }); - - it(`should return ${bool} for ${id} <= ${version}`, function () { - expect(isUpgradeable(server, { id: id })).to.be(bool); - }); - - after(function () { version = pkg.version; }); - }); - } - - upgradeDoc('1.0.0-beta1', pkg.version, false); - upgradeDoc(pkg.version, pkg.version, false); - upgradeDoc('4.0.0-RC1', '4.0.0-RC2', true); - upgradeDoc('4.0.0-rc2', '4.0.0-rc1', false); - upgradeDoc('4.0.0-rc2', '4.0.0', true); - upgradeDoc('4.0.0-rc2', '4.0.2', true); - upgradeDoc('4.0.1', '4.1.0-rc', true); - upgradeDoc('4.0.0-rc1', '4.0.0', true); - upgradeDoc('4.0.0-rc1-SNAPSHOT', '4.0.0', false); - upgradeDoc('4.1.0-rc1-SNAPSHOT', '4.1.0-rc1', false); - upgradeDoc('5.0.0-alpha1', '5.0.0', false); - - it('should handle missing id field', function () { - const configSavedObject = { - 'type': 'config', - 'attributes': { - 'buildNum': 1.7976931348623157e+308, - 'defaultIndex': '[logstash-]YYYY.MM.DD' - } - }; - - expect(isUpgradeable(server, configSavedObject)).to.be(false); - }); - - it('should handle id of @@version', function () { - const configSavedObject = { - 'type': 'config', - 'id': '@@version', - 'attributes': { - 'buildNum': 1.7976931348623157e+308, - 'defaultIndex': '[logstash-]YYYY.MM.DD' - } - }; - expect(isUpgradeable(server, configSavedObject)).to.be(false); - }); - - }); - - -}); diff --git a/src/core_plugins/elasticsearch/lib/__tests__/upgrade_config.js b/src/core_plugins/elasticsearch/lib/__tests__/upgrade_config.js deleted file mode 100644 index a757bc1178d2a5..00000000000000 --- a/src/core_plugins/elasticsearch/lib/__tests__/upgrade_config.js +++ /dev/null @@ -1,157 +0,0 @@ -import Promise from 'bluebird'; -import sinon from 'sinon'; -import expect from 'expect.js'; - -import upgradeConfig from '../upgrade_config'; - -describe('plugins/elasticsearch', function () { - describe('lib/upgrade_config', function () { - let get; - let server; - let savedObjectsClient; - let upgrade; - - beforeEach(function () { - get = sinon.stub(); - get.withArgs('kibana.index').returns('.my-kibana'); - get.withArgs('pkg.version').returns('4.0.1'); - get.withArgs('pkg.buildNum').returns(Math.random()); - - savedObjectsClient = { - create: sinon.stub() - }; - - server = { - log: sinon.stub(), - config: function () { - return { - get: get - }; - }, - }; - upgrade = upgradeConfig(server, savedObjectsClient); - }); - - describe('nothing is found', function () { - const configSavedObjects = { hits: { hits:[] } }; - - beforeEach(function () { - savedObjectsClient.create.returns(Promise.resolve({ id: 1, version: 1 })); - }); - - describe('production', function () { - beforeEach(function () { - get.withArgs('env.name').returns('production'); - get.withArgs('env.prod').returns(true); - get.withArgs('env.dev').returns(false); - }); - - it('should resolve buildNum to pkg.buildNum config', function () { - return upgrade(configSavedObjects).then(function () { - sinon.assert.calledOnce(savedObjectsClient.create); - const attributes = savedObjectsClient.create.args[0][1]; - expect(attributes).to.have.property('buildNum', get('pkg.buildNum')); - }); - }); - - it('should resolve version to pkg.version config', function () { - return upgrade(configSavedObjects).then(function () { - const options = savedObjectsClient.create.args[0][2]; - expect(options).to.have.property('id', get('pkg.version')); - }); - }); - }); - - describe('development', function () { - beforeEach(function () { - get.withArgs('env.name').returns('development'); - get.withArgs('env.prod').returns(false); - get.withArgs('env.dev').returns(true); - }); - - it('should resolve buildNum to pkg.buildNum config', function () { - return upgrade(configSavedObjects).then(function () { - const attributes = savedObjectsClient.create.args[0][1]; - expect(attributes).to.have.property('buildNum', get('pkg.buildNum')); - }); - }); - - it('should resolve version to pkg.version config', function () { - return upgrade(configSavedObjects).then(function () { - const options = savedObjectsClient.create.args[0][2]; - expect(options).to.have.property('id', get('pkg.version')); - }); - }); - }); - }); - - it('should resolve with undefined if the current version is found', function () { - const configSavedObjects = [ { id: '4.0.1' } ]; - return upgrade(configSavedObjects).then(function (resp) { - expect(resp).to.be(undefined); - }); - }); - - it('should create new config if the nothing is upgradeable', function () { - get.withArgs('pkg.buildNum').returns(9833); - savedObjectsClient.create.returns(Promise.resolve({ id: 1, version: 1 })); - - const configSavedObjects = [ { id: '4.0.1-alpha3' }, { id: '4.0.1-beta1' }, { id: '4.0.0-SNAPSHOT1' } ]; - return upgrade(configSavedObjects).then(function () { - sinon.assert.calledOnce(savedObjectsClient.create); - const savedObjectType = savedObjectsClient.create.args[0][0]; - expect(savedObjectType).to.eql('config'); - const attributes = savedObjectsClient.create.args[0][1]; - expect(attributes).to.have.property('buildNum', 9833); - const options = savedObjectsClient.create.args[0][2]; - expect(options).to.have.property('id', '4.0.1'); - }); - }); - - it('should update the build number on the new config', function () { - get.withArgs('pkg.buildNum').returns(5801); - savedObjectsClient.create.returns(Promise.resolve({ id: 1, version: 1 })); - - const configSavedObjects = [ { id: '4.0.0', attributes: { buildNum: 1 } } ]; - - return upgrade(configSavedObjects).then(function () { - sinon.assert.calledOnce(savedObjectsClient.create); - const attributes = savedObjectsClient.create.args[0][1]; - expect(attributes).to.have.property('buildNum', 5801); - const savedObjectType = savedObjectsClient.create.args[0][0]; - expect(savedObjectType).to.eql('config'); - const options = savedObjectsClient.create.args[0][2]; - expect(options).to.have.property('id', '4.0.1'); - }); - }); - - it('should log a message for upgrades', function () { - get.withArgs('pkg.buildNum').returns(5801); - savedObjectsClient.create.returns(Promise.resolve({ id: 1, version: 1 })); - - const configSavedObjects = [ { id: '4.0.0', attributes: { buildNum: 1 } } ]; - - return upgrade(configSavedObjects).then(function () { - sinon.assert.calledOnce(server.log); - expect(server.log.args[0][0]).to.eql(['plugin', 'elasticsearch']); - const msg = server.log.args[0][1]; - expect(msg).to.have.property('prevVersion', '4.0.0'); - expect(msg).to.have.property('newVersion', '4.0.1'); - expect(msg.tmpl).to.contain('Upgrade'); - }); - }); - - it('should copy attributes from old config', function () { - get.withArgs('pkg.buildNum').returns(5801); - savedObjectsClient.create.returns(Promise.resolve({ id: 1, version: 1 })); - - const configSavedObjects = [ { id: '4.0.0', attributes: { buildNum: 1, defaultIndex: 'logstash-*' } } ]; - - return upgrade(configSavedObjects).then(function () { - sinon.assert.calledOnce(savedObjectsClient.create); - const attributes = savedObjectsClient.create.args[0][1]; - expect(attributes).to.have.property('defaultIndex', 'logstash-*'); - }); - }); - }); -}); diff --git a/src/core_plugins/elasticsearch/lib/create_kibana_index.js b/src/core_plugins/elasticsearch/lib/create_kibana_index.js deleted file mode 100644 index 53260dd170d726..00000000000000 --- a/src/core_plugins/elasticsearch/lib/create_kibana_index.js +++ /dev/null @@ -1,25 +0,0 @@ -export default function (server) { - const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); - const index = server.config().get('kibana.index'); - return callWithInternalUser('indices.create', { - index: index, - body: { - settings: { - number_of_shards: 1 - }, - mappings: server.getKibanaIndexMappingsDsl() - } - }) - .catch(() => { - throw new Error(`Unable to create Kibana index "${index}"`); - }) - .then(function () { - return callWithInternalUser('cluster.health', { - waitForStatus: 'yellow', - index: index - }) - .catch(() => { - throw new Error(`Waiting for Kibana index "${index}" to come online failed.`); - }); - }); -} diff --git a/src/core_plugins/elasticsearch/lib/health_check.js b/src/core_plugins/elasticsearch/lib/health_check.js index 2cc952e1b88309..d3617016247bb5 100644 --- a/src/core_plugins/elasticsearch/lib/health_check.js +++ b/src/core_plugins/elasticsearch/lib/health_check.js @@ -1,22 +1,14 @@ -import _ from 'lodash'; import Promise from 'bluebird'; import elasticsearch from 'elasticsearch'; -import { migrateConfig } from './migrate_config'; -import createKibanaIndex from './create_kibana_index'; import kibanaVersion from './kibana_version'; import { ensureEsVersion } from './ensure_es_version'; import { ensureNotTribe } from './ensure_not_tribe'; import { ensureAllowExplicitIndex } from './ensure_allow_explicit_index'; -import { patchKibanaIndex } from './patch_kibana_index'; const NoConnections = elasticsearch.errors.NoConnections; import util from 'util'; const format = util.format; -const NO_INDEX = 'no_index'; -const INITIALIZING = 'initializing'; -const READY = 'ready'; - export default function (plugin, server) { const config = server.config(); const callAdminAsKibanaUser = server.plugins.elasticsearch.getCluster('admin').callWithInternalUser; @@ -33,54 +25,6 @@ export default function (plugin, server) { }); } - // just figure out the current "health" of the es setup - function getHealth() { - return callAdminAsKibanaUser('cluster.health', { - timeout: '5s', // tells es to not sit around and wait forever - index: config.get('kibana.index'), - ignore: [408] - }) - .then(function (resp) { - // if "timed_out" === true then elasticsearch could not - // find any idices matching our filter within 5 seconds - if (!resp || resp.timed_out) { - return NO_INDEX; - } - - // If status === "red" that means that index(es) were found - // but the shards are not ready for queries - if (resp.status === 'red') { - return INITIALIZING; - } - - return READY; - }); - } - - function waitUntilReady() { - return getHealth() - .then(function (health) { - if (health !== READY) { - return Promise.delay(REQUEST_DELAY).then(waitUntilReady); - } - }); - } - - function waitForShards() { - return getHealth() - .then(function (health) { - if (health === NO_INDEX) { - plugin.status.yellow('No existing Kibana index found'); - return createKibanaIndex(server); - } - - if (health === INITIALIZING) { - plugin.status.red('Elasticsearch is still initializing the kibana index.'); - return Promise.delay(REQUEST_DELAY).then(waitForShards); - } - }); - } - function waitForEsVersion() { return ensureEsVersion(server, kibanaVersion.get()).catch(err => { plugin.status.red(err); @@ -98,14 +42,6 @@ export default function (plugin, server) { .then(waitForEsVersion) .then(() => ensureNotTribe(callAdminAsKibanaUser)) .then(() => ensureAllowExplicitIndex(callAdminAsKibanaUser, config)) - .then(waitForShards) - .then(() => patchKibanaIndex({ - callCluster: callAdminAsKibanaUser, - log: (...args) => server.log(...args), - indexName: config.get('kibana.index'), - kibanaIndexMappingsDsl: server.getKibanaIndexMappingsDsl() - })) - .then(_.partial(migrateConfig, server)) .then(() => { const tribeUrl = config.get('elasticsearch.tribe.url'); if (tribeUrl) { @@ -151,7 +87,6 @@ export default function (plugin, server) { }); return { - waitUntilReady: waitUntilReady, run: check, start: startorRestartChecking, stop: stopChecking, diff --git a/src/core_plugins/elasticsearch/lib/is_upgradeable.js b/src/core_plugins/elasticsearch/lib/is_upgradeable.js deleted file mode 100644 index 7b9de5632c02a8..00000000000000 --- a/src/core_plugins/elasticsearch/lib/is_upgradeable.js +++ /dev/null @@ -1,33 +0,0 @@ -import semver from 'semver'; -const rcVersionRegex = /(\d+\.\d+\.\d+)\-rc(\d+)/i; - -export default function (server, configSavedObject) { - const config = server.config(); - if (/alpha|beta|snapshot/i.test(configSavedObject.id)) return false; - if (!configSavedObject.id) return false; - if (configSavedObject.id === config.get('pkg.version')) return false; - - let packageRcRelease = Infinity; - let rcRelease = Infinity; - let packageVersion = config.get('pkg.version'); - let version = configSavedObject.id; - const matches = configSavedObject.id.match(rcVersionRegex); - const packageMatches = config.get('pkg.version').match(rcVersionRegex); - - if (matches) { - version = matches[1]; - rcRelease = parseInt(matches[2], 10); - } - - if (packageMatches) { - packageVersion = packageMatches[1]; - packageRcRelease = parseInt(packageMatches[2], 10); - } - - try { - if (semver.gte(version, packageVersion) && rcRelease >= packageRcRelease) return false; - } catch (e) { - return false; - } - return true; -} diff --git a/src/core_plugins/elasticsearch/lib/migrate_config.js b/src/core_plugins/elasticsearch/lib/migrate_config.js deleted file mode 100644 index a5e26fb0800afe..00000000000000 --- a/src/core_plugins/elasticsearch/lib/migrate_config.js +++ /dev/null @@ -1,17 +0,0 @@ -import upgrade from './upgrade_config'; - -export async function migrateConfig(server) { - const savedObjectsClient = server.savedObjectsClientFactory({ - callCluster: server.plugins.elasticsearch.getCluster('admin').callWithInternalUser - }); - - const { saved_objects: configSavedObjects } = await savedObjectsClient.find({ - type: 'config', - page: 1, - perPage: 1000, - sortField: 'buildNum', - sortOrder: 'desc' - }); - - return await upgrade(server, savedObjectsClient)(configSavedObjects); -} diff --git a/src/core_plugins/elasticsearch/lib/patch_kibana_index.js b/src/core_plugins/elasticsearch/lib/patch_kibana_index.js deleted file mode 100644 index b95d55e01dc764..00000000000000 --- a/src/core_plugins/elasticsearch/lib/patch_kibana_index.js +++ /dev/null @@ -1,102 +0,0 @@ -import { - getTypes, - getRootType, - getRootProperties -} from '../../../server/mappings'; - -/** - * Checks that the root type in the kibana index has all of the - * root properties specified by the kibanaIndexMappings. - * - * @param {Object} options - * @property {Function} options.log - * @property {string} options.indexName - * @property {Function} options.callCluster - * @property {EsMappingsDsl} options.kibanaIndexMappingsDsl - * @return {Promise} - */ -export async function patchKibanaIndex(options) { - const { - log, - indexName, - callCluster, - kibanaIndexMappingsDsl - } = options; - - const rootEsType = getRootType(kibanaIndexMappingsDsl); - const currentMappingsDsl = await getCurrentMappings(callCluster, indexName, rootEsType); - const missingProperties = await getMissingRootProperties(currentMappingsDsl, kibanaIndexMappingsDsl); - - const missingPropertyNames = Object.keys(missingProperties); - if (!missingPropertyNames.length) { - // all expected properties are in current mapping - return; - } - - // log about new properties - log(['info', 'elasticsearch'], { - tmpl: `Adding mappings to kibana index for SavedObject types "<%= names.join('", "') %>"`, - names: missingPropertyNames - }); - - // add the new properties to the index mapping - await callCluster('indices.putMapping', { - index: indexName, - type: rootEsType, - body: { - properties: missingProperties - }, - update_all_types: true - }); -} - -/** - * Get the mappings dsl for the current Kibana index - * @param {Function} callCluster - * @param {string} indexName - * @param {string} rootEsType - * @return {EsMappingsDsl} - */ -async function getCurrentMappings(callCluster, indexName, rootEsType) { - const index = await callCluster('indices.get', { - index: indexName, - feature: '_mappings' - }); - - // could be different if aliases were resolved by `indices.get` - const resolvedName = Object.keys(index)[0]; - const currentMappingsDsl = index[resolvedName].mappings; - const currentTypes = getTypes(currentMappingsDsl); - - const isV5Index = currentTypes.length > 1 || currentTypes[0] !== rootEsType; - if (isV5Index) { - throw new Error( - 'Your Kibana index is out of date, reset it or use the X-Pack upgrade assistant.' - ); - } - - return currentMappingsDsl; -} - -/** - * Get the properties that are in the expectedMappingsDsl but not the - * currentMappingsDsl. Properties will be an object of properties normally - * found at `[index]mappings[typeName].properties` is es mapping responses - * - * @param {EsMappingsDsl} currentMappingsDsl - * @param {EsMappingsDsl} expectedMappingsDsl - * @return {PropertyMappings} - */ -async function getMissingRootProperties(currentMappingsDsl, expectedMappingsDsl) { - const expectedProps = getRootProperties(expectedMappingsDsl); - const existingProps = getRootProperties(currentMappingsDsl); - - return Object.keys(expectedProps) - .reduce((acc, prop) => { - if (existingProps[prop]) { - return acc; - } else { - return { ...acc, [prop]: expectedProps[prop] }; - } - }, {}); -} diff --git a/src/core_plugins/elasticsearch/lib/upgrade_config.js b/src/core_plugins/elasticsearch/lib/upgrade_config.js deleted file mode 100644 index f66ee323319f0f..00000000000000 --- a/src/core_plugins/elasticsearch/lib/upgrade_config.js +++ /dev/null @@ -1,52 +0,0 @@ -import Promise from 'bluebird'; -import isUpgradeable from './is_upgradeable'; -import _ from 'lodash'; - -export default function (server, savedObjectsClient) { - const config = server.config(); - - function createNewConfig() { - return savedObjectsClient.create('config', { - buildNum: config.get('pkg.buildNum') - }, { - id: config.get('pkg.version') - }); - } - - return function (configSavedObjects) { - // Check to see if there are any doc. If not then we set the build number and id - if (configSavedObjects.length === 0) { - return createNewConfig(); - } - - // if we already have a the current version in the index then we need to stop - const devConfig = _.find(configSavedObjects, function currentVersion(configSavedObject) { - return configSavedObject.id !== '@@version' && configSavedObject.id === config.get('pkg.version'); - }); - - if (devConfig) { - return Promise.resolve(); - } - - // Look for upgradeable configs. If none of them are upgradeable - // then create a new one. - const configSavedObject = _.find(configSavedObjects, isUpgradeable.bind(null, server)); - if (!configSavedObject) { - return createNewConfig(); - } - - // if the build number is still the template string (which it wil be in development) - // then we need to set it to the max interger. Otherwise we will set it to the build num - configSavedObject.attributes.buildNum = config.get('pkg.buildNum'); - - server.log(['plugin', 'elasticsearch'], { - tmpl: 'Upgrade config from <%= prevVersion %> to <%= newVersion %>', - prevVersion: configSavedObject.id, - newVersion: config.get('pkg.version') - }); - - return savedObjectsClient.create('config', configSavedObject.attributes, { - id: config.get('pkg.version') - }); - }; -} diff --git a/src/server/kbn_server.js b/src/server/kbn_server.js index 8ed9760701c653..6d641338c8b15c 100644 --- a/src/server/kbn_server.js +++ b/src/server/kbn_server.js @@ -19,9 +19,8 @@ import uiMixin from '../ui'; import optimizeMixin from '../optimize'; import pluginsInitializeMixin from './plugins/initialize'; import { indexPatternsMixin } from './index_patterns'; -import { savedObjectsMixin } from './saved_objects'; +import { savedObjectMappingsMixin, savedObjectsMixin } from './saved_objects'; import { statsMixin } from './stats'; -import { kibanaIndexMappingsMixin } from './mappings'; import { serverExtensionsMixin } from './server_extensions'; const rootDir = fromRoot('.'); @@ -63,8 +62,8 @@ export default class KbnServer { // tell the config we are done loading plugins configCompleteMixin, - // setup kbnServer.mappings and server.getKibanaIndexMappingsDsl() - kibanaIndexMappingsMixin, + // setup kbnServer.savedObjectMappings + savedObjectMappingsMixin, // setup this.uiExports and this.bundles uiMixin, diff --git a/src/server/mappings/index.js b/src/server/mappings/index.js deleted file mode 100644 index 2c269e661ce786..00000000000000 --- a/src/server/mappings/index.js +++ /dev/null @@ -1,10 +0,0 @@ -export { - kibanaIndexMappingsMixin -} from './kibana_index_mappings_mixin'; - -export { - getTypes, - getRootType, - getProperty, - getRootProperties, -} from './lib'; diff --git a/src/server/mappings/index_mappings.js b/src/server/mappings/index_mappings.js deleted file mode 100644 index 26aaa9e7c5ad2b..00000000000000 --- a/src/server/mappings/index_mappings.js +++ /dev/null @@ -1,61 +0,0 @@ -import { cloneDeep, isPlainObject } from 'lodash'; - -import { formatListAsProse } from '../../utils'; -import { getRootProperties, getRootType } from './lib'; - -const DEFAULT_INITIAL_DSL = { - rootType: { - type: 'object', - properties: {}, - }, -}; - -export class IndexMappings { - constructor(initialDsl = DEFAULT_INITIAL_DSL) { - this._dsl = cloneDeep(initialDsl); - if (!isPlainObject(this._dsl)) { - throw new TypeError('initial mapping must be an object'); - } - - // ensure that we have a properties object in the dsl - // and that the dsl can be parsed with getRootProperties() and kin - this._setProperties(getRootProperties(this._dsl) || {}); - } - - getDsl() { - return cloneDeep(this._dsl); - } - - addRootProperties(newProperties, options = {}) { - const { plugin } = options; - const rootProperties = getRootProperties(this._dsl); - - - const conflicts = Object.keys(newProperties) - .filter(key => rootProperties.hasOwnProperty(key)); - - if (conflicts.length) { - const props = formatListAsProse(conflicts); - const owner = plugin ? `registered by plugin ${plugin} ` : ''; - throw new Error( - `Mappings for ${props} ${owner}have already been defined` - ); - } - - this._setProperties({ - ...rootProperties, - ...newProperties - }); - } - - _setProperties(newProperties) { - const rootType = getRootType(this._dsl); - this._dsl = { - ...this._dsl, - [rootType]: { - ...this._dsl[rootType], - properties: newProperties - } - }; - } -} diff --git a/src/server/mappings/kibana_index_mappings_mixin.js b/src/server/mappings/kibana_index_mappings_mixin.js deleted file mode 100644 index e1a169493c950a..00000000000000 --- a/src/server/mappings/kibana_index_mappings_mixin.js +++ /dev/null @@ -1,62 +0,0 @@ -import { IndexMappings } from './index_mappings'; - -/** - * The default mappings used for the kibana index. This is - * extended via uiExports type "mappings". See the kibana - * and timelion plugins for examples. - * @type {EsMappingDsl} - */ -const BASE_KIBANA_INDEX_MAPPINGS_DSL = { - doc: { - dynamic: 'strict', - properties: { - type: { - type: 'keyword' - }, - updated_at: { - type: 'date' - }, - config: { - dynamic: true, - properties: { - buildNum: { - type: 'keyword' - } - } - }, - } - } -}; - -export function kibanaIndexMappingsMixin(kbnServer, server) { - /** - * Stores the current mappings that we expect to find in the Kibana - * index. Using `kbnServer.mappings.addRootProperties()` the UiExports - * class extends these mappings based on `mappings` ui export specs. - * - * Application code should not access this object, and instead should - * use `server.getKibanaIndexMappingsDsl()` from below, mixed with the - * helpers exposed by this module, to interact with the mappings via - * their DSL. - * - * @type {IndexMappings} - */ - kbnServer.mappings = new IndexMappings(BASE_KIBANA_INDEX_MAPPINGS_DSL); - - /** - * Get the mappings dsl that we expect to see in the - * Kibana index. Used by the elasticsearch plugin to create - * and update the kibana index. Also used by the SavedObjectsClient - * to determine the properties defined in the mapping as well as - * things like the "rootType". - * - * See `src/server/mappings/lib/index.js` for helpers useful for reading - * the EsMappingDsl object. - * - * @method server.getKibanaIndexMappingsDsl - * @returns {EsMappingDsl} - */ - server.decorate('server', 'getKibanaIndexMappingsDsl', () => { - return kbnServer.mappings.getDsl(); - }); -} diff --git a/src/server/saved_objects/client/__tests__/saved_objects_client.js b/src/server/saved_objects/client/__tests__/saved_objects_client.js index 4e28a05e55b40b..9c430c32e7bb72 100644 --- a/src/server/saved_objects/client/__tests__/saved_objects_client.js +++ b/src/server/saved_objects/client/__tests__/saved_objects_client.js @@ -291,7 +291,9 @@ describe('SavedObjectsClient', () => { describe('#delete', () => { it('throws notFound when ES is unable to find the document', async () => { - callAdminCluster.returns(Promise.resolve({ found: false })); + callAdminCluster.returns(Promise.resolve({ + result: 'not_found' + })); try { await savedObjectsClient.delete('index-pattern', 'logstash-*'); @@ -303,7 +305,9 @@ describe('SavedObjectsClient', () => { }); it('passes the parameters to callAdminCluster', async () => { - callAdminCluster.returns({}); + callAdminCluster.returns({ + result: 'deleted' + }); await savedObjectsClient.delete('index-pattern', 'logstash-*'); expect(callAdminCluster.calledOnce).to.be(true); @@ -314,7 +318,8 @@ describe('SavedObjectsClient', () => { type: 'doc', id: 'index-pattern:logstash-*', refresh: 'wait_for', - index: '.kibana-test' + index: '.kibana-test', + ignore: [404], }); }); }); @@ -562,6 +567,7 @@ describe('SavedObjectsClient', () => { body: { doc: { updated_at: mockTimestamp, 'index-pattern': { title: 'Testing' } } }, + ignore: [404], refresh: 'wait_for', index: '.kibana-test' }); diff --git a/src/server/saved_objects/client/index.js b/src/server/saved_objects/client/index.js index 4b4ac9b5dcb174..00a2266b6e003c 100644 --- a/src/server/saved_objects/client/index.js +++ b/src/server/saved_objects/client/index.js @@ -1 +1,2 @@ export { SavedObjectsClient } from './saved_objects_client'; +export { errors } from './lib'; diff --git a/src/server/saved_objects/client/lib/decorate_es_error.js b/src/server/saved_objects/client/lib/decorate_es_error.js index 9f721e25eb6ba9..a6761ded75e687 100644 --- a/src/server/saved_objects/client/lib/decorate_es_error.js +++ b/src/server/saved_objects/client/lib/decorate_es_error.js @@ -21,6 +21,7 @@ import { decorateConflictError, decorateEsUnavailableError, decorateGeneralError, + isEsUnavailableError, } from './errors'; export function decorateEsError(error) { @@ -33,7 +34,8 @@ export function decorateEsError(error) { error instanceof ConnectionFault || error instanceof ServiceUnavailable || error instanceof NoConnections || - error instanceof RequestTimeout + error instanceof RequestTimeout || + isEsUnavailableError(error) ) { return decorateEsUnavailableError(error, reason); } diff --git a/src/server/saved_objects/client/lib/search_dsl/query_params.js b/src/server/saved_objects/client/lib/search_dsl/query_params.js index 3b29aa41b6d22b..dc71baee1a1b0d 100644 --- a/src/server/saved_objects/client/lib/search_dsl/query_params.js +++ b/src/server/saved_objects/client/lib/search_dsl/query_params.js @@ -1,4 +1,4 @@ -import { getRootProperties } from '../../../../mappings'; +import { getRootProperties } from '../../../mappings'; /** * Get the field params based on the types and searchFields diff --git a/src/server/saved_objects/client/lib/search_dsl/sorting_params.js b/src/server/saved_objects/client/lib/search_dsl/sorting_params.js index 0ccda5d230fa8d..18d66e324cbe32 100644 --- a/src/server/saved_objects/client/lib/search_dsl/sorting_params.js +++ b/src/server/saved_objects/client/lib/search_dsl/sorting_params.js @@ -1,6 +1,6 @@ import Boom from 'boom'; -import { getProperty } from '../../../../mappings'; +import { getProperty } from '../../../mappings'; export function getSortingParams(mappings, type, sortField, sortOrder) { if (!sortField) { diff --git a/src/server/saved_objects/client/saved_objects_client.js b/src/server/saved_objects/client/saved_objects_client.js index 2414954096fcc1..2a7c110f8eb56a 100644 --- a/src/server/saved_objects/client/saved_objects_client.js +++ b/src/server/saved_objects/client/saved_objects_client.js @@ -1,7 +1,7 @@ import Boom from 'boom'; import uuid from 'uuid'; -import { getRootType } from '../../mappings'; +import { getRootType } from '../mappings'; import { getSearchDsl, @@ -144,11 +144,25 @@ export class SavedObjectsClient { id: this._generateEsId(type, id), type: this._type, refresh: 'wait_for', + ignore: [404], }); - if (response.found === false) { + const deleted = response.result === 'deleted'; + if (deleted) { + return {}; + } + + // 404 might be because document is missing or index is missing, + // don't leak that implementation detail to the user + const docNotFound = response.result === 'not_found'; + const indexNotFound = response.error && response.error.type === 'index_not_found_exception'; + if (docNotFound || indexNotFound) { throw errors.decorateNotFoundError(Boom.notFound()); } + + throw new Error( + `Unexpected Elasticsearch DELETE response: ${JSON.stringify({ type, id, response, })}` + ); } /** @@ -188,6 +202,7 @@ export class SavedObjectsClient { size: perPage, from: perPage * (page - 1), _source: includedFields(type, fields), + ignore: [404], body: { version: true, ...getSearchDsl(this._mappings, { @@ -202,6 +217,17 @@ export class SavedObjectsClient { const response = await this._withKibanaIndex('search', esOptions); + if (response.status === 404) { + // 404 is only possible here if the index is missing, which + // is an implementation detail we don't want to leak + return { + page, + per_page: perPage, + total: 0, + saved_objects: [] + }; + } + return { page, per_page: perPage, @@ -249,13 +275,14 @@ export class SavedObjectsClient { saved_objects: response.docs.map((doc, i) => { const { id, type } = objects[i]; - if (doc.found === false) { + if (!doc.found) { return { id, type, error: { statusCode: 404, message: 'Not found' } }; } + const time = doc._source.updated_at; return { id, @@ -279,7 +306,16 @@ export class SavedObjectsClient { const response = await this._withKibanaIndex('get', { id: this._generateEsId(type, id), type: this._type, + ignore: [404] }); + + const docNotFound = response.found === false; + const indexNotFound = response.status === 404; + if (docNotFound || indexNotFound) { + // don't leak implementation details about why there was a 404 + throw errors.decorateNotFoundError(Boom.notFound()); + } + const { updated_at: updatedAt } = response._source; return { @@ -307,6 +343,7 @@ export class SavedObjectsClient { type: this._type, version: options.version, refresh: 'wait_for', + ignore: [404], body: { doc: { updated_at: time, @@ -315,6 +352,11 @@ export class SavedObjectsClient { }, }); + if (response.status === 404) { + // don't leak implementation details about why there was a 404 + throw errors.decorateNotFoundError(Boom.notFound()); + } + return { id, type, diff --git a/src/server/saved_objects/es_availability/es_availability.js b/src/server/saved_objects/es_availability/es_availability.js new file mode 100644 index 00000000000000..eaf3c3ab081151 --- /dev/null +++ b/src/server/saved_objects/es_availability/es_availability.js @@ -0,0 +1,184 @@ +import Boom from 'boom'; +import { delay } from 'bluebird'; +import { Subscription, BehaviorSubject, Subject, Observable } from 'rxjs/Rx'; + +import { mergeMapLatest } from './lib'; + +import { errors } from '../client'; + +export function createEsAvailability(kbnServer) { + const { server, config, savedObjectMappings } = kbnServer; + + /** + * Collective subscription, closed/torn-down when the server stops + * @type {Subscription} + */ + const sub = new Subscription(); + + /** + * Stores the esIsAvailable state. + * `undefined`: state is not known, fetch is probably in progress, wait for the next value + * `true`: es is available and we think it's in a good state + * `false`: es is not available, we were not able to get it into a good state + * @type {BehaviorSubject} + */ + const esIsAvailable$ = new BehaviorSubject(undefined); + + // complete esIsAvailable$ when the subscription is torn down so + // that pending requests will be released + sub.add(() => esIsAvailable$.complete()); + + /** + * Emits when a check is requested + * @type {Subject} + */ + const checkReq$ = new Subject(); + + /** + * Runs the check, which attempts to put the indexTemplate to elasticsearch + * and will eventually patch the index if it already exists + * @return {Promise} + */ + async function check() { + const esExports = server.plugins.elasticsearch; + + if (!esExports) { + // the `kbnServer.ready()` handler will prevent check from + // running after we know that elasticsearch is not enabled, but + // it's possible it will be triggered before that happens + return; + } + + const callCluster = esExports.getCluster('admin').callWithInternalUser; + const index = config.get('kibana.index'); + + // tell external parties that we don't know if es is available right now, + // they should wait to see what we discover + esIsAvailable$.next(undefined); + + try { + // try to add the index template to elasticsearch. if it already exists + // then a new version will be written + await callCluster('indices.putTemplate', { + name: `kibana_index_template:${index}`, + body: { + template: index, + settings: { + number_of_shards: 1 + }, + mappings: savedObjectMappings.getDsl() + } + }); + + // TODO: we need to check for existing index and make sure it + // has the types it needs + + // es seems to be available, let the listeners know + esIsAvailable$.next(true); + } catch (error) { + // we log every error + server.log(['error', 'savedObjects', 'putTemplate'], { + tmpl: 'Failed to put index template "<%= err.message %>"', + err: { + message: error.message, + stack: error.stack, + } + }); + + // and then notify listeners that es is not available + esIsAvailable$.next(false); + + // wait for 1 second before finishing this check so that we don't pound elasticsearch too hard + await delay(1000); + } + } + + sub.add( + checkReq$ + // mergeMapLatest will run check() for each request + // and buffer up to requests that arrives while it + // is processing. + .let(mergeMapLatest(check)) + + // this should only happen if there is a syntax error, + // undefined methods, or something like that, but since + // we are not "supervised" by a request or anything we + // need to log and resubscribe + .catch((error, resubscribe) => { + server.log(['error', 'savedObjects'], { + tmpl: 'Error in savedObjects/esAvailability check "<%= err.message %>"', + err: { + message: error.message, + stack: error.stack + } + }); + + return resubscribe; + }) + .subscribe() + ); + + + // when the kbnServer is ready plugins have loaded, so if + // elasticsearch is enabled it will be available on the server now + kbnServer.ready().then(() => { + const esStatus = kbnServer.status.getForPluginId('elasticsearch'); + + if (!esStatus) { + esIsAvailable$.complete(); + checkReq$.complete(); + return; + } + + sub.add( + Observable + // event the state of the es plugin whenever it changes + .fromEvent(esStatus, 'change', (prev, prevMsg, state) => state) + // start with the current state + .startWith(esStatus.switch) + // determine if the status is green or not + .map(state => state === 'green') + // toggle between green and not green + .distinctUntilChanged() + // request a check + .subscribe(() => checkReq$.next()) + ); + }); + + // when the server is stopping close the collective subscription + server.ext('onPreStop', (server, next) => { + sub.unsubscribe(); + next(); + }); + + return new class EsAvailability { + wrapCallClusterFunction(callCluster) { + return async (method, params) => { + // wait for the first non-undefined availablility + const esIsAvailable = await esIsAvailable$ + .filter(available => available !== undefined) + .take(1) + .toPromise(); + + // esIsAvailable will still be undefined in some + // scenarios (like es is disabled) so use falsy check + if (!esIsAvailable) { + checkReq$.next(); + throw errors.decorateEsUnavailableError( + Boom.serverUnavailable('Elasticsearch is unavailable') + ); + } + + try { + return await callCluster(method, params); + } catch (error) { + if (!error || !error.status || error.status >= 500) { + checkReq$.next(); + } + + throw error; + } + }; + } + }; +} diff --git a/src/server/saved_objects/es_availability/index.js b/src/server/saved_objects/es_availability/index.js new file mode 100644 index 00000000000000..6a8726cf6448dd --- /dev/null +++ b/src/server/saved_objects/es_availability/index.js @@ -0,0 +1 @@ +export { createEsAvailability } from './es_availability'; diff --git a/src/server/saved_objects/es_availability/lib/__tests__/merge_map_latest.js b/src/server/saved_objects/es_availability/lib/__tests__/merge_map_latest.js new file mode 100644 index 00000000000000..5151f6c54bd231 --- /dev/null +++ b/src/server/saved_objects/es_availability/lib/__tests__/merge_map_latest.js @@ -0,0 +1,47 @@ +import sinon from 'sinon'; +import expect from 'expect.js'; +import { delay } from 'bluebird'; +import { Observable } from 'rxjs/Rx'; + +import { mergeMapLatest } from '../merge_map_latest'; + +describe('$.let(mergeMapLatest(fn))', () => { + it('completes if it gets no events', async () => { + await Observable + .empty() + .let(mergeMapLatest(input => `foo(${input})`)) + .toPromise(); + }); + + it('waits for map to complete before processing next value', async () => { + const mapFn = sinon.spy(async () => { + await delay(100); + return Date.now(); + }); + + const result = await Observable + .of(1,2) + .let(mergeMapLatest(mapFn)) + .toArray() + .toPromise(); + + expect(result).to.be.an('array'); + expect(result).to.have.length(2); + expect(result[0] + 100 <= result[1]).to.be.ok(); + }); + + it('only sends the latest to the mergeMap', async () => { + const mapFn = sinon.spy(async (input) => { + await delay(100); + return input; + }); + + const result = await Observable + .of(1,2,3,4,5,6) + .let(mergeMapLatest(mapFn)) + .toArray() + .toPromise(); + + expect(result).to.eql([1,6]); + }); +}); diff --git a/src/server/saved_objects/es_availability/lib/index.js b/src/server/saved_objects/es_availability/lib/index.js new file mode 100644 index 00000000000000..656f4c372fa367 --- /dev/null +++ b/src/server/saved_objects/es_availability/lib/index.js @@ -0,0 +1 @@ +export { mergeMapLatest } from './merge_map_latest'; diff --git a/src/server/saved_objects/es_availability/lib/merge_map_latest.js b/src/server/saved_objects/es_availability/lib/merge_map_latest.js new file mode 100644 index 00000000000000..ad3fe49c5a57d4 --- /dev/null +++ b/src/server/saved_objects/es_availability/lib/merge_map_latest.js @@ -0,0 +1,60 @@ +import { Observable } from 'rxjs/Rx'; + +export const mergeMapLatest = (mapFn) => (source) => ( + source.lift({ + call(observer, source) { + let buffer = null; + + const sub = observer.add(source.subscribe({ + // either start or buffer each value + next(value) { + if (!buffer) { + mapValue(value); + return; + } + + buffer.unshift(value); + buffer.length = 1; // cap the buffer size to 1 + }, + error(error) { + observer.error(error); + }, + complete() { + // if we are buffering it's not our job to signal complete + if (!buffer) { + observer.complete(); + } + }, + })); + + function mapValue(value) { + // tells source sub to start buffering items, and that + // we will take care of completing when we are done + if (!buffer) { + buffer = []; + } + + observer.add(Observable.from(mapFn(value)).subscribe({ + next(mappedValue) { + observer.next(mappedValue); + }, + error(error) { + observer.error(error); + }, + complete() { + if (buffer.length) { + mapValue(buffer.shift()); + return; + } + + if (sub.closed) { + observer.complete(); + } + + buffer = null; + } + })); + } + } + }) +); diff --git a/src/server/saved_objects/index.js b/src/server/saved_objects/index.js index b920baa2c9c1aa..f903c9e39eedf2 100644 --- a/src/server/saved_objects/index.js +++ b/src/server/saved_objects/index.js @@ -1,2 +1,3 @@ export { savedObjectsMixin } from './saved_objects_mixin'; export { SavedObjectsClient } from './client'; +export { savedObjectMappingsMixin } from './mappings'; diff --git a/src/server/mappings/__tests__/index_mappings.js b/src/server/saved_objects/mappings/__tests__/mappings.js similarity index 68% rename from src/server/mappings/__tests__/index_mappings.js rename to src/server/saved_objects/mappings/__tests__/mappings.js index 999c0eb0d5b3c2..ff521a157933d4 100644 --- a/src/server/mappings/__tests__/index_mappings.js +++ b/src/server/saved_objects/mappings/__tests__/mappings.js @@ -1,30 +1,30 @@ import expect from 'expect.js'; import Chance from 'chance'; -import { IndexMappings } from '../index_mappings'; +import { Mappings } from '../mappings'; import { getRootType } from '../lib'; const chance = new Chance(); -describe('server/mapping/index_mapping', function () { +describe('server/mapping', function () { describe('constructor', () => { - it('initializes with a default mapping when no args', () => { - const mapping = new IndexMappings(); - const dsl = mapping.getDsl(); + it('initializes with default mappings when no args', () => { + const mappings = new Mappings(); + const dsl = mappings.getDsl(); expect(dsl).to.be.an('object'); expect(getRootType(dsl)).to.be.a('string'); expect(dsl[getRootType(dsl)]).to.be.an('object'); }); - it('accepts a default mapping dsl as the only argument', () => { - const mapping = new IndexMappings({ + it('accepts default mappings dsl as the only argument', () => { + const mappings = new Mappings({ foobar: { dynamic: false, properties: {} } }); - expect(mapping.getDsl()).to.eql({ + expect(mappings.getDsl()).to.eql({ foobar: { dynamic: false, properties: {} @@ -34,7 +34,7 @@ describe('server/mapping/index_mapping', function () { it('throws if root type is of type=anything-but-object', () => { expect(() => { - new IndexMappings({ + new Mappings({ root: { type: chance.pickone(['string', 'keyword', 'geo_point']) } @@ -44,20 +44,20 @@ describe('server/mapping/index_mapping', function () { it('throws if root type has no type and no properties', () => { expect(() => { - new IndexMappings({ + new Mappings({ root: {} }); }).to.throwException(/non-object/); }); it('initialized root type with properties object if not set', () => { - const mapping = new IndexMappings({ + const mappings = new Mappings({ root: { type: 'object' } }); - expect(mapping.getDsl()).to.eql({ + expect(mappings.getDsl()).to.eql({ root: { type: 'object', properties: {} @@ -68,19 +68,19 @@ describe('server/mapping/index_mapping', function () { describe('#getDsl()', () => { // tests are light because this method is used all over these tests - it('returns mapping as es dsl', function () { - const mapping = new IndexMappings(); - expect(mapping.getDsl()).to.be.an('object'); + it('returns mappings as es dsl', function () { + const mappings = new Mappings(); + expect(mappings.getDsl()).to.be.an('object'); }); }); describe('#addRootProperties()', () => { it('extends the properties of the root type', () => { - const mapping = new IndexMappings({ + const mappings = new Mappings({ x: { properties: {} } }); - mapping.addRootProperties({ + mappings.addRootProperties({ y: { properties: { z: { @@ -90,7 +90,7 @@ describe('server/mapping/index_mapping', function () { } }); - expect(mapping.getDsl()).to.eql({ + expect(mappings.getDsl()).to.eql({ x: { properties: { y: { @@ -107,21 +107,21 @@ describe('server/mapping/index_mapping', function () { it('throws if any property is conflicting', () => { const props = { foo: 'bar' }; - const mapping = new IndexMappings({ + const mappings = new Mappings({ root: { properties: props } }); expect(() => { - mapping.addRootProperties(props); + mappings.addRootProperties(props); }).to.throwException(/foo/); }); it('includes the plugin option in the error message when specified', () => { const props = { foo: 'bar' }; - const mapping = new IndexMappings({ root: { properties: props } }); + const mappings = new Mappings({ root: { properties: props } }); expect(() => { - mapping.addRootProperties(props, { plugin: 'abc123' }); + mappings.addRootProperties(props, { plugin: 'abc123' }); }).to.throwException(/plugin abc123/); }); }); diff --git a/src/core_plugins/elasticsearch/lib/__tests__/patch_kibana_index.js b/src/server/saved_objects/mappings/__tests__/patch_index.js similarity index 50% rename from src/core_plugins/elasticsearch/lib/__tests__/patch_kibana_index.js rename to src/server/saved_objects/mappings/__tests__/patch_index.js index ecddf49a62b17b..636dad4d32fc3d 100644 --- a/src/core_plugins/elasticsearch/lib/__tests__/patch_kibana_index.js +++ b/src/server/saved_objects/mappings/__tests__/patch_index.js @@ -3,12 +3,13 @@ import sinon from 'sinon'; import { times, cloneDeep, pick, partition } from 'lodash'; import Chance from 'chance'; -import { patchKibanaIndex } from '../patch_kibana_index'; -import { getRootProperties, getRootType } from '../../../../server/mappings'; +import { patchIndex } from '../patch_index'; +import { Mappings } from '../mappings'; +import { getRootProperties, getRootType } from '../lib'; const chance = new Chance(); -function createRandomMappings(n = chance.integer({ min: 10, max: 20 })) { +function createRandomMappingDsl(n = chance.integer({ min: 10, max: 20 })) { return { [chance.word()]: { properties: times(n, () => chance.word()) @@ -22,32 +23,32 @@ function createRandomMappings(n = chance.integer({ min: 10, max: 20 })) { }; } -function splitMappings(mappings) { - const type = getRootType(mappings); - const allProps = getRootProperties(mappings); +function splitMappingDsl(mappingsDsl) { + const type = getRootType(mappingsDsl); + const allProps = getRootProperties(mappingsDsl); const keyGroups = partition(Object.keys(allProps), (p, i) => i % 2); return keyGroups.map(keys => ({ [type]: { - ...mappings[type], + ...mappingsDsl[type], properties: pick(allProps, keys) } })); } -function createIndex(name, mappings = {}) { +function createIndexDsl(name, mappingsDsl = {}) { return { [name]: { - mappings + mappings: mappingsDsl } }; } -function createCallCluster(index) { +function createCallCluster(indexDsl) { return sinon.spy(async (method, params) => { switch (method) { case 'indices.get': - expect(params).to.have.property('index', Object.keys(index)[0]); - return cloneDeep(index); + expect(params).to.have.property('index', Object.keys(indexDsl)[0]); + return cloneDeep(indexDsl); case 'indices.putMapping': return { ok: true }; default: @@ -56,16 +57,16 @@ function createCallCluster(index) { }); } -describe('es/healthCheck/patchKibanaIndex()', () => { +describe('savedobjects/health_check/patchIndex()', () => { describe('general', () => { it('reads the _mappings feature of the indexName', async () => { - const indexName = chance.word(); - const mappings = createRandomMappings(); - const callCluster = createCallCluster(createIndex(indexName, mappings)); - await patchKibanaIndex({ + const index = chance.word(); + const mappingsDsl = createRandomMappingDsl(); + const callCluster = createCallCluster(createIndexDsl(index, mappingsDsl)); + await patchIndex({ callCluster, - indexName, - kibanaIndexMappingsDsl: mappings, + index, + mappings: new Mappings(mappingsDsl), log: sinon.stub() }); @@ -79,23 +80,22 @@ describe('es/healthCheck/patchKibanaIndex()', () => { describe('multi-type index', () => { it('rejects', async () => { try { - const mappings = createRandomMappings(); - const indexName = chance.word(); - const index = createIndex(indexName, { - ...mappings, - ...createRandomMappings(), - ...createRandomMappings(), - ...createRandomMappings(), - }); - const callCluster = createCallCluster(index); - - await patchKibanaIndex({ - indexName, + const mappingsDsl = createRandomMappingDsl(); + const index = chance.word(); + const callCluster = createCallCluster(createIndexDsl(index, { + ...mappingsDsl, + ...createRandomMappingDsl(), + ...createRandomMappingDsl(), + ...createRandomMappingDsl(), + })); + + await patchIndex({ + index, callCluster, - kibanaIndexMappingsDsl: mappings, + mappings: new Mappings(mappingsDsl), log: sinon.stub() }); - throw new Error('expected patchKibanaIndex() to throw an error'); + throw new Error('expected patchIndex() to throw an error'); } catch (error) { expect(error) .to.have.property('message') @@ -106,67 +106,69 @@ describe('es/healthCheck/patchKibanaIndex()', () => { describe('v6 index', () => { it('does nothing if mappings match elasticsearch', async () => { - const mappings = createRandomMappings(); - const indexName = chance.word(); - const callCluster = createCallCluster(createIndex(indexName, mappings)); - await patchKibanaIndex({ - indexName, + const mappingsDsl = createRandomMappingDsl(); + const index = chance.word(); + const callCluster = createCallCluster(createIndexDsl(index, mappingsDsl)); + await patchIndex({ + index, callCluster, - kibanaIndexMappingsDsl: mappings, + mappings: new Mappings(mappingsDsl), log: sinon.stub() }); sinon.assert.calledOnce(callCluster); - sinon.assert.calledWithExactly(callCluster, 'indices.get', sinon.match({ index: indexName })); + sinon.assert.calledWithExactly(callCluster, 'indices.get', sinon.match({ index })); }); it('adds properties that are not in index', async () => { - const [indexMappings, missingMappings] = splitMappings(createRandomMappings()); - const mappings = { - ...indexMappings, - ...missingMappings, + const [currentMappingsDsl, missingMappingsDsl] = splitMappingDsl(createRandomMappingDsl()); + const mappingsDsl = { + ...currentMappingsDsl, + ...missingMappingsDsl, }; - const indexName = chance.word(); - const callCluster = createCallCluster(createIndex(indexName, indexMappings)); - await patchKibanaIndex({ - indexName, + const index = chance.word(); + const callCluster = createCallCluster(createIndexDsl(index, currentMappingsDsl)); + await patchIndex({ + index, callCluster, - kibanaIndexMappingsDsl: mappings, + mappings: new Mappings(mappingsDsl), log: sinon.stub() }); sinon.assert.calledTwice(callCluster); - sinon.assert.calledWithExactly(callCluster, 'indices.get', sinon.match({ index: indexName })); + sinon.assert.calledWithExactly(callCluster, 'indices.get', sinon.match({ + index + })); sinon.assert.calledWithExactly(callCluster, 'indices.putMapping', sinon.match({ - index: indexName, - type: getRootType(mappings), + index: index, + type: getRootType(mappingsDsl), body: { - properties: getRootProperties(mappings) + properties: getRootProperties(mappingsDsl) } })); }); it('ignores extra properties in index', async () => { - const [indexMappings, mappings] = splitMappings(createRandomMappings()); - const indexName = chance.word(); - const callCluster = createCallCluster(createIndex(indexName, indexMappings)); - await patchKibanaIndex({ - indexName, + const [currentMappingsDsl, extraMappingsDsl] = splitMappingDsl(createRandomMappingDsl()); + const index = chance.word(); + const callCluster = createCallCluster(createIndexDsl(index, currentMappingsDsl)); + await patchIndex({ + index, callCluster, - kibanaIndexMappingsDsl: mappings, + mappings: new Mappings(extraMappingsDsl), log: sinon.stub() }); sinon.assert.calledTwice(callCluster); sinon.assert.calledWithExactly(callCluster, 'indices.get', sinon.match({ - index: indexName + index })); sinon.assert.calledWithExactly(callCluster, 'indices.putMapping', sinon.match({ - index: indexName, - type: getRootType(mappings), + index: index, + type: getRootType(extraMappingsDsl), body: { - properties: getRootProperties(mappings) + properties: getRootProperties(extraMappingsDsl) } })); }); diff --git a/src/server/saved_objects/mappings/index.js b/src/server/saved_objects/mappings/index.js new file mode 100644 index 00000000000000..917da3aa550ea3 --- /dev/null +++ b/src/server/saved_objects/mappings/index.js @@ -0,0 +1,9 @@ +export { savedObjectMappingsMixin } from './saved_object_mappings_mixin'; +export { patchIndex } from './patch_index'; + +export { + getTypes, + getRootType, + getProperty, + getRootProperties, +} from './lib'; diff --git a/src/server/mappings/lib/__tests__/get_property.js b/src/server/saved_objects/mappings/lib/__tests__/get_property.js similarity index 100% rename from src/server/mappings/lib/__tests__/get_property.js rename to src/server/saved_objects/mappings/lib/__tests__/get_property.js diff --git a/src/server/saved_objects/mappings/lib/get_missing_root_properties_from_es.js b/src/server/saved_objects/mappings/lib/get_missing_root_properties_from_es.js new file mode 100644 index 00000000000000..c7cdd428269339 --- /dev/null +++ b/src/server/saved_objects/mappings/lib/get_missing_root_properties_from_es.js @@ -0,0 +1,34 @@ +import { difference } from 'lodash'; + +import { getTypes } from './get_types'; +import { getRootProperties } from './get_root_properties'; + +export async function getMissingRootPropertiesFromEs({ callCluster, index, mappings }) { + const resp = await callCluster('indices.get', { + index, + feature: '_mappings' + }); + + // could be different if aliases were resolved by `indices.get` + const resolvedName = Object.keys(resp)[0]; + const currentDsl = resp[resolvedName].mappings; + const currentTypes = getTypes(currentDsl); + + const isV5Index = currentTypes.length > 1 || currentTypes[0] !== mappings.getRootType(); + if (isV5Index) { + throw new Error( + 'Your Kibana index is out of date, reset it or use the X-Pack upgrade assistant.' + ); + } + + const expectedRootProps = mappings.getRootProperties(); + const missingPropNames = difference( + Object.keys(expectedRootProps), + Object.keys(getRootProperties(currentDsl)) + ); + + return missingPropNames.reduce((acc, propName) => ({ + ...acc, + [propName]: expectedRootProps[propName] + }), []); +} diff --git a/src/server/mappings/lib/get_property.js b/src/server/saved_objects/mappings/lib/get_property.js similarity index 100% rename from src/server/mappings/lib/get_property.js rename to src/server/saved_objects/mappings/lib/get_property.js diff --git a/src/server/mappings/lib/get_root_properties.js b/src/server/saved_objects/mappings/lib/get_root_properties.js similarity index 100% rename from src/server/mappings/lib/get_root_properties.js rename to src/server/saved_objects/mappings/lib/get_root_properties.js diff --git a/src/server/mappings/lib/get_root_type.js b/src/server/saved_objects/mappings/lib/get_root_type.js similarity index 100% rename from src/server/mappings/lib/get_root_type.js rename to src/server/saved_objects/mappings/lib/get_root_type.js diff --git a/src/server/mappings/lib/get_types.js b/src/server/saved_objects/mappings/lib/get_types.js similarity index 100% rename from src/server/mappings/lib/get_types.js rename to src/server/saved_objects/mappings/lib/get_types.js diff --git a/src/server/mappings/lib/index.js b/src/server/saved_objects/mappings/lib/index.js similarity index 68% rename from src/server/mappings/lib/index.js rename to src/server/saved_objects/mappings/lib/index.js index 7998922732298d..7307e95ef23b4e 100644 --- a/src/server/mappings/lib/index.js +++ b/src/server/saved_objects/mappings/lib/index.js @@ -2,3 +2,4 @@ export { getProperty } from './get_property'; export { getTypes } from './get_types'; export { getRootType } from './get_root_type'; export { getRootProperties } from './get_root_properties'; +export { getMissingRootPropertiesFromEs } from './get_missing_root_properties_from_es'; diff --git a/src/server/saved_objects/mappings/mappings.js b/src/server/saved_objects/mappings/mappings.js new file mode 100644 index 00000000000000..4def64c075413e --- /dev/null +++ b/src/server/saved_objects/mappings/mappings.js @@ -0,0 +1,104 @@ +import { cloneDeep, isPlainObject } from 'lodash'; + +import { formatListAsProse } from '../../../utils'; +import { getRootProperties, getRootType } from './lib'; + +const DEFAULT_INITIAL_DSL = { + rootType: { + type: 'object', + properties: {}, + }, +}; + +/** + * Mappings objects wrap a mapping DSL and gives it methods for + * modification and formatting + * @class Mappings + */ +export class Mappings { + /** + * Create a Mappings object starting with a DSL + * @constructor + * @param {Object} [initialDsl=DEFAULT_INITIAL_DSL] + */ + constructor(initialDsl = DEFAULT_INITIAL_DSL) { + this._dsl = cloneDeep(initialDsl); + if (!isPlainObject(this._dsl)) { + throw new TypeError('initial mapping must be an object'); + } + + // ensure that we have a properties object in the dsl + // and that the dsl can be parsed with getRootProperties() and kin + this._setProperties(this.getRootProperties()); + } + + /** + * Get the elasticsearch DSL representation of the Mappings in its + * current state. + * @return {mappingDSL} + */ + getDsl() { + return cloneDeep(this._dsl); + } + + /** + * Get the name of the type at the root of the mapping. + * @return {string} + */ + getRootType() { + return getRootType(this._dsl); + } + + /** + * Get the property mappings for the root type. This same value + * can be found at `{indexName}.mappings.{typeName}.properties` + * in the es indices.get() response. + * @return {EsPropertyMappings} + */ + getRootProperties() { + return getRootProperties(this._dsl); + } + + /** + * Add some properties to the root type in the mapping. Since indices can + * only have one type this is how we simulate "types" in a single index + * @param {Object} newProperties + * @param {Object} [options={}] + * @property {string} options.plugin the plugin id that is adding this + * root property, used for error message + * if the property conflicts with existing + * properties + */ + addRootProperties(newProperties, options = {}) { + const { plugin } = options; + const rootProperties = this.getRootProperties(); + + + const conflicts = Object.keys(newProperties) + .filter(key => rootProperties.hasOwnProperty(key)); + + if (conflicts.length) { + const props = formatListAsProse(conflicts); + const owner = plugin ? `registered by plugin ${plugin} ` : ''; + throw new Error( + `Mappings for ${props} ${owner}have already been defined` + ); + } + + this._setProperties({ + ...rootProperties, + ...newProperties + }); + } + + _setProperties(newProperties) { + const rootType = getRootType(this._dsl); + this._dsl = { + ...this._dsl, + [rootType]: { + ...this._dsl[rootType], + properties: newProperties + } + }; + } +} diff --git a/src/server/saved_objects/mappings/patch_index.js b/src/server/saved_objects/mappings/patch_index.js new file mode 100644 index 00000000000000..a0373a28547ff8 --- /dev/null +++ b/src/server/saved_objects/mappings/patch_index.js @@ -0,0 +1,38 @@ +import { getMissingRootPropertiesFromEs } from './lib'; + +export async function patchIndex(options) { + const { + log, + index, + callCluster, + mappings + } = options; + + const missingProperties = await getMissingRootPropertiesFromEs({ + index, + callCluster, + mappings, + }); + + const missingPropertyNames = Object.keys(missingProperties); + if (!missingPropertyNames.length) { + // all expected properties are in current mapping + return; + } + + // log about new properties + log(['info', 'elasticsearch'], { + tmpl: `Adding mappings to kibana index for SavedObject types "<%= names.join('", "') %>"`, + names: missingPropertyNames + }); + + // add the new properties to the index mapping + await callCluster('indices.putMapping', { + index, + type: mappings.getRootType(), + body: { + properties: missingProperties + }, + update_all_types: true + }); +} diff --git a/src/server/saved_objects/mappings/saved_object_mappings_mixin.js b/src/server/saved_objects/mappings/saved_object_mappings_mixin.js new file mode 100644 index 00000000000000..8fda1e3f6be49c --- /dev/null +++ b/src/server/saved_objects/mappings/saved_object_mappings_mixin.js @@ -0,0 +1,25 @@ +import { Mappings } from './mappings'; + +export function savedObjectMappingsMixin(kbnServer) { + kbnServer.savedObjectMappings = new Mappings({ + doc: { + dynamic: 'strict', + properties: { + type: { + type: 'keyword' + }, + updated_at: { + type: 'date' + }, + config: { + dynamic: true, + properties: { + buildNum: { + type: 'keyword' + } + } + }, + } + } + }); +} diff --git a/src/server/saved_objects/saved_objects_mixin.js b/src/server/saved_objects/saved_objects_mixin.js index 0206be644a1203..5afed2f80cf438 100644 --- a/src/server/saved_objects/saved_objects_mixin.js +++ b/src/server/saved_objects/saved_objects_mixin.js @@ -1,4 +1,5 @@ import { SavedObjectsClient } from './client'; +import { createEsAvailability } from './es_availability'; import { createBulkGetRoute, @@ -26,11 +27,13 @@ export function savedObjectsMixin(kbnServer, server) { server.route(createGetRoute(prereqs)); server.route(createUpdateRoute(prereqs)); + const esAvailability = createEsAvailability(kbnServer); + server.decorate('server', 'savedObjectsClientFactory', ({ callCluster }) => { return new SavedObjectsClient( server.config().get('kibana.index'), - server.getKibanaIndexMappingsDsl(), - callCluster + kbnServer.savedObjectMappings.getDsl(), + esAvailability.wrapCallClusterFunction(callCluster), ); }); diff --git a/src/ui/__tests__/ui_exports_replace_injected_vars.js b/src/ui/__tests__/ui_exports_replace_injected_vars.js index 662236464c90f9..35c13c9bb9c256 100644 --- a/src/ui/__tests__/ui_exports_replace_injected_vars.js +++ b/src/ui/__tests__/ui_exports_replace_injected_vars.js @@ -39,7 +39,6 @@ describe('UiExports', function () { }); await kbnServer.ready(); - kbnServer.status.get('ui settings').state = 'green'; kbnServer.server.decorate('request', 'getUiSettingsService', () => { return { getDefaults: noop, getUserProvided: noop }; }); diff --git a/src/ui/index.js b/src/ui/index.js index 5f4bba02d723c1..be8fef976c0473 100644 --- a/src/ui/index.js +++ b/src/ui/index.js @@ -15,7 +15,7 @@ import { fieldFormatsMixin } from './field_formats_mixin'; export default async (kbnServer, server, config) => { const uiExports = kbnServer.uiExports = new UiExports({ urlBasePath: config.get('server.basePath'), - kibanaIndexMappings: kbnServer.mappings, + savedObjectMappings: kbnServer.savedObjectMappings, }); await kbnServer.mixin(uiSettingsMixin); diff --git a/src/ui/ui_exports.js b/src/ui/ui_exports.js index 38a87d992a827e..712df30dd825b9 100644 --- a/src/ui/ui_exports.js +++ b/src/ui/ui_exports.js @@ -5,7 +5,7 @@ import UiAppCollection from './ui_app_collection'; import UiNavLinkCollection from './ui_nav_link_collection'; export default class UiExports { - constructor({ urlBasePath, kibanaIndexMappings }) { + constructor({ urlBasePath, savedObjectMappings }) { this.navLinks = new UiNavLinkCollection(this); this.apps = new UiAppCollection(this); this.aliases = { @@ -33,7 +33,7 @@ export default class UiExports { this.bundleProviders = []; this.defaultInjectedVars = {}; this.injectedVarsReplacers = []; - this.kibanaIndexMappings = kibanaIndexMappings; + this.savedObjectMappings = savedObjectMappings; } consumePlugin(plugin) { @@ -151,7 +151,7 @@ export default class UiExports { case 'mappings': return (plugin, mappings) => { - this.kibanaIndexMappings.addRootProperties(mappings, { plugin: plugin.id }); + this.savedObjectMappings.addRootProperties(mappings, { plugin: plugin.id }); }; case 'replaceInjectedVars': diff --git a/src/ui/ui_settings/__tests__/ui_settings_mixin_integration.js b/src/ui/ui_settings/__tests__/ui_settings_mixin_integration.js index 67dcb9cfd57518..21dc8781f4bc1b 100644 --- a/src/ui/ui_settings/__tests__/ui_settings_mixin_integration.js +++ b/src/ui/ui_settings/__tests__/ui_settings_mixin_integration.js @@ -1,8 +1,6 @@ import sinon from 'sinon'; import expect from 'expect.js'; -import Chance from 'chance'; -import ServerStatus from '../../../server/status/server_status'; import Config from '../../../server/config/config'; /* eslint-disable import/no-duplicates */ @@ -14,8 +12,6 @@ import { getUiSettingsServiceForRequest } from '../ui_settings_service_for_reque import { uiSettingsMixin } from '../ui_settings_mixin'; -const chance = new Chance(); - describe('uiSettingsMixin()', () => { const sandbox = sinon.sandbox.create(); @@ -58,7 +54,6 @@ describe('uiSettingsMixin()', () => { server, config, uiExports: { addConsumer: sinon.stub() }, - status: new ServerStatus(server), ready: sinon.stub().returns(readyPromise), }; @@ -69,60 +64,11 @@ describe('uiSettingsMixin()', () => { server, decorations, readyPromise, - status: kbnServer.status.get('ui settings'), }; } afterEach(() => sandbox.restore()); - describe('status', () => { - it('creates a "ui settings" status', () => { - const { status } = setup(); - expect(status).to.have.property('state', 'uninitialized'); - }); - - describe('disabled', () => { - it('disables if uiSettings.enabled config is false', () => { - const { status } = setup({ enabled: false }); - expect(status).to.have.property('state', 'disabled'); - }); - - it('does not register a handler for kbnServer.ready()', () => { - const { readyPromise } = setup({ enabled: false }); - sinon.assert.notCalled(readyPromise.then); - }); - }); - - describe('enabled', () => { - it('registers a handler for kbnServer.ready()', () => { - const { readyPromise } = setup(); - sinon.assert.calledOnce(readyPromise.then); - }); - - it('mirrors the elasticsearch plugin status once kibanaServer.ready() resolves', () => { - const { kbnServer, readyPromise, status } = setup(); - const esStatus = kbnServer.status.createForPlugin({ - id: 'elasticsearch', - version: 'kibana', - }); - - esStatus.green(); - expect(status).to.have.property('state', 'uninitialized'); - const readyPromiseHandler = readyPromise.then.firstCall.args[0]; - readyPromiseHandler(); - expect(status).to.have.property('state', 'green'); - - - const states = chance.shuffle(['red', 'green', 'yellow']); - states.forEach((state) => { - esStatus[state](); - expect(esStatus).to.have.property('state', state); - expect(status).to.have.property('state', state); - }); - }); - }); - }); - describe('server.uiSettingsServiceFactory()', () => { it('decorates server with "uiSettingsServiceFactory"', () => { const { decorations } = setup(); @@ -172,32 +118,6 @@ describe('uiSettingsMixin()', () => { decorations.request.getUiSettingsService.call(request); sinon.assert.calledWith(getUiSettingsServiceForRequest, server, request); }); - - it('defines read interceptor that intercepts when status is not green', () => { - const { status, decorations } = setup(); - expect(decorations.request).to.have.property('getUiSettingsService').a('function'); - - sandbox.stub(getUiSettingsServiceForRequestNS, 'getUiSettingsServiceForRequest'); - decorations.request.getUiSettingsService(); - - const options = getUiSettingsServiceForRequest.firstCall.args[2]; - expect(options).to.have.property('readInterceptor'); - - const { readInterceptor } = options; - expect(readInterceptor).to.be.a('function'); - - status.green(); - expect(readInterceptor()).to.be(undefined); - - status.yellow(); - expect(readInterceptor()).to.eql({}); - - status.red(); - expect(readInterceptor()).to.eql({}); - - status.green(); - expect(readInterceptor()).to.eql(undefined); - }); }); describe('server.uiSettings()', () => { diff --git a/src/ui/ui_settings/__tests__/ui_settings_service.js b/src/ui/ui_settings/__tests__/ui_settings_service.js index c7ab164c5945e5..07ef3b252dde60 100644 --- a/src/ui/ui_settings/__tests__/ui_settings_service.js +++ b/src/ui/ui_settings/__tests__/ui_settings_service.js @@ -16,7 +16,6 @@ const chance = new Chance(); function setup(options = {}) { const { - readInterceptor, getDefaults, defaults = {}, esDocSource = {}, @@ -27,7 +26,6 @@ function setup(options = {}) { type: TYPE, id: ID, getDefaults: getDefaults || (() => defaults), - readInterceptor, savedObjectsClient, }); @@ -337,47 +335,4 @@ describe('ui settings', () => { expect(result).to.equal('YYYY-MM-DD'); }); }); - - describe('readInterceptor() argument', () => { - describe('#getUserProvided()', () => { - it('returns a promise when interceptValue doesn\'t', () => { - const { uiSettings } = setup({ readInterceptor: () => ({}) }); - expect(uiSettings.getUserProvided()).to.be.a(Promise); - }); - - it('returns intercept values', async () => { - const { uiSettings } = setup({ - readInterceptor: () => ({ - foo: 'bar' - }) - }); - - expect(await uiSettings.getUserProvided()).to.eql({ - foo: { - userValue: 'bar' - } - }); - }); - }); - - describe('#getAll()', () => { - it('merges intercept value with defaults', async () => { - const { uiSettings } = setup({ - defaults: { - foo: { value: 'foo' }, - bar: { value: 'bar' }, - }, - - readInterceptor: () => ({ - foo: 'not foo' - }), - }); - - expect(await uiSettings.getAll()).to.eql({ - foo: 'not foo', - bar: 'bar' - }); - }); - }); - }); }); diff --git a/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_integration.js b/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_integration.js new file mode 100644 index 00000000000000..3838aed792c3fe --- /dev/null +++ b/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_integration.js @@ -0,0 +1,200 @@ +import sinon from 'sinon'; +import expect from 'expect.js'; + +import { createEsTestCluster } from '../../../../test_utils/es'; +import { createServerWithCorePlugins } from '../../../../test_utils/kbn_server'; +import { createToolingLog } from '../../../../utils'; +import { createOrUpgradeSavedConfig } from '../create_or_upgrade_saved_config'; + +describe('createOrUpgradeSavedConfig()', () => { + let savedObjectsClient; + let kbnServer; + const cleanup = []; + + before(async function () { + const log = createToolingLog('debug'); + log.pipe(process.stdout); + log.indent(6); + + const es = createEsTestCluster({ + log: msg => log.debug(msg), + name: 'savedObjects/healthCheck/integration', + }); + + this.timeout(es.getStartTimeout()); + log.info('starting elasticsearch'); + log.indent(2); + await es.start(); + log.indent(-2); + cleanup.push(() => es.stop()); + + kbnServer = createServerWithCorePlugins(); + await kbnServer.ready(); + cleanup.push(async () => { + await kbnServer.close(); + kbnServer = null; + savedObjectsClient = null; + }); + + await kbnServer.server.plugins.elasticsearch.waitUntilReady(); + + savedObjectsClient = kbnServer.server.savedObjectsClientFactory({ + callCluster: es.getCallCluster(), + }); + + await savedObjectsClient.bulkCreate([ + { + id: '5.4.0-SNAPSHOT', + type: 'config', + attributes: { + buildNum: 54090, + '5.4.0-SNAPSHOT': true + }, + }, + { + id: '5.4.0-rc1', + type: 'config', + attributes: { + buildNum: 54010, + '5.4.0-rc1': true + }, + }, + { + id: '@@version', + type: 'config', + attributes: { + buildNum: 99999, + '@@version': true + }, + }, + ]); + }); + + after(async () => { + await Promise.all(cleanup.map(fn => fn())); + cleanup.length = 0; + }); + + it('upgrades the previous version on each increment', async function () { + this.timeout(30000); + + // ------------------------------------ + // upgrade to 5.4.0 + await createOrUpgradeSavedConfig({ + savedObjectsClient, + version: '5.4.0', + buildNum: 54099, + log: sinon.stub() + }); + + const config540 = await savedObjectsClient.get('config', '5.4.0'); + expect(config540).to.have.property('attributes').eql({ + // should have the new build number + buildNum: 54099, + + // 5.4.0-SNAPSHOT and @@version were ignored so we only have the + // attributes from 5.4.0-rc1, even though the other build nums are greater + '5.4.0-rc1': true, + }); + + // add the 5.4.0 flag to the 5.4.0 savedConfig + await savedObjectsClient.update('config', '5.4.0', { + '5.4.0': true, + }); + + // ------------------------------------ + // upgrade to 5.4.1 + await createOrUpgradeSavedConfig({ + savedObjectsClient, + version: '5.4.1', + buildNum: 54199, + log: sinon.stub() + }); + + const config541 = await savedObjectsClient.get('config', '5.4.1'); + expect(config541).to.have.property('attributes').eql({ + // should have the new build number + buildNum: 54199, + + // should also include properties from 5.4.0 and 5.4.0-rc1 + '5.4.0': true, + '5.4.0-rc1': true, + }); + + // add the 5.4.1 flag to the 5.4.1 savedConfig + await savedObjectsClient.update('config', '5.4.1', { + '5.4.1': true, + }); + + // ------------------------------------ + // upgrade to 7.0.0-rc1 + await createOrUpgradeSavedConfig({ + savedObjectsClient, + version: '7.0.0-rc1', + buildNum: 70010, + log: sinon.stub() + }); + + const config700rc1 = await savedObjectsClient.get('config', '7.0.0-rc1'); + expect(config700rc1).to.have.property('attributes').eql({ + // should have the new build number + buildNum: 70010, + + // should also include properties from 5.4.1, 5.4.0 and 5.4.0-rc1 + '5.4.1': true, + '5.4.0': true, + '5.4.0-rc1': true, + }); + + // tag the 7.0.0-rc1 doc + await savedObjectsClient.update('config', '7.0.0-rc1', { + '7.0.0-rc1': true, + }); + + // ------------------------------------ + // upgrade to 7.0.0 + await createOrUpgradeSavedConfig({ + savedObjectsClient, + version: '7.0.0', + buildNum: 70099, + log: sinon.stub() + }); + + const config700 = await savedObjectsClient.get('config', '7.0.0'); + expect(config700).to.have.property('attributes').eql({ + // should have the new build number + buildNum: 70099, + + // should also include properties from ancestors, including 7.0.0-rc1 + '7.0.0-rc1': true, + '5.4.1': true, + '5.4.0': true, + '5.4.0-rc1': true, + }); + + // tag the 7.0.0 doc + await savedObjectsClient.update('config', '7.0.0', { + '7.0.0': true, + }); + + // ------------------------------------ + // "downgrade" to 6.2.3-rc1 + await createOrUpgradeSavedConfig({ + savedObjectsClient, + version: '6.2.3-rc1', + buildNum: 62310, + log: sinon.stub() + }); + + const config623rc1 = await savedObjectsClient.get('config', '6.2.3-rc1'); + expect(config623rc1).to.have.property('attributes').eql({ + // should have the new build number + buildNum: 62310, + + // should also include properties from ancestors, but not 7.0.0-rc1 or 7.0.0 + '5.4.1': true, + '5.4.0': true, + '5.4.0-rc1': true, + }); + }); +}); 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 new file mode 100644 index 00000000000000..93645211cfe76d --- /dev/null +++ b/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_saved_config.js @@ -0,0 +1,120 @@ +import sinon from 'sinon'; +import Chance from 'chance'; + +import * as getUpgradeableConfigNS from '../get_upgradeable_config'; +import { createOrUpgradeSavedConfig } from '../create_or_upgrade_saved_config'; + +const chance = new Chance(); + +describe('uiSettings/createOrUpgradeSavedConfig', function () { + const sandbox = sinon.sandbox.create(); + afterEach(() => sandbox.restore()); + + const version = '4.0.1'; + const prevVersion = '4.0.0'; + const buildNum = chance.integer({ min: 1000, max: 5000 }); + + function setup() { + const log = sinon.stub(); + const getUpgradeableConfig = sandbox.stub(getUpgradeableConfigNS, 'getUpgradeableConfig'); + const savedObjectsClient = { + create: sinon.spy(async (type, attributes, options = {}) => ({ + type, + id: options.id, + version: 1, + })) + }; + + async function run() { + const resp = await createOrUpgradeSavedConfig({ + savedObjectsClient, + version, + buildNum, + log, + }); + + if (getUpgradeableConfig.callCount) { + sinon.assert.alwaysCalledWith(getUpgradeableConfig, { savedObjectsClient, version }); + } + + return resp; + } + + return { + buildNum, + log, + run, + version, + savedObjectsClient, + getUpgradeableConfig, + }; + } + + describe('nothing is upgradeable', function () { + it('should create config with current version and buildNum', async () => { + const { run, savedObjectsClient } = setup(); + + await run(); + + sinon.assert.calledOnce(savedObjectsClient.create); + sinon.assert.calledWithExactly(savedObjectsClient.create, 'config', { + buildNum, + }, { + id: version + }); + }); + }); + + describe('something is upgradeable', () => { + it('should merge upgraded attributes with current build number in new config', async () => { + const { + run, + getUpgradeableConfig, + savedObjectsClient + } = setup(); + + const savedAttributes = { + buildNum: buildNum - 100, + [chance.word()]: chance.sentence(), + [chance.word()]: chance.sentence(), + [chance.word()]: chance.sentence() + }; + + getUpgradeableConfig + .returns({ id: prevVersion, attributes: savedAttributes }); + + await run(); + + sinon.assert.calledOnce(getUpgradeableConfig); + sinon.assert.calledOnce(savedObjectsClient.create); + sinon.assert.calledWithExactly(savedObjectsClient.create, + 'config', + { + ...savedAttributes, + buildNum, + }, + { + id: version, + } + ); + }); + + it('should log a message for upgrades', async () => { + const { getUpgradeableConfig, log, run } = setup(); + + getUpgradeableConfig + .returns({ id: prevVersion, attributes: { buildNum: buildNum - 100 } }); + + await run(); + sinon.assert.calledOnce(log); + sinon.assert.calledWithExactly(log, + ['plugin', 'elasticsearch'], + sinon.match({ + tmpl: sinon.match('Upgrade'), + prevVersion, + newVersion: version, + }) + ); + }); + }); +}); diff --git a/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/is_config_version_upgradeable.js b/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/is_config_version_upgradeable.js new file mode 100644 index 00000000000000..69a86f87796b54 --- /dev/null +++ b/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/is_config_version_upgradeable.js @@ -0,0 +1,26 @@ +import expect from 'expect.js'; + +import { isConfigVersionUpgradeable } from '../is_config_version_upgradeable'; +import { pkg } from '../../../../utils'; + +describe('savedObjects/health_check/isConfigVersionUpgradeable', function () { + function runTest(savedVersion, kibanaVersion, expected) { + it(`should return ${expected} for config version ${savedVersion} and kibana version ${kibanaVersion}`, () => { + expect(isConfigVersionUpgradeable(savedVersion, kibanaVersion)).to.be(expected); + }); + } + + runTest('1.0.0-beta1', pkg.version, false); + runTest(pkg.version, pkg.version, false); + runTest('4.0.0-RC1', '4.0.0-RC2', true); + runTest('4.0.0-rc2', '4.0.0-rc1', false); + runTest('4.0.0-rc2', '4.0.0', true); + runTest('4.0.0-rc2', '4.0.2', true); + runTest('4.0.1', '4.1.0-rc', true); + runTest('4.0.0-rc1', '4.0.0', true); + runTest('4.0.0-rc1-SNAPSHOT', '4.0.0', false); + runTest('4.1.0-rc1-SNAPSHOT', '4.1.0-rc1', false); + runTest('5.0.0-alpha1', '5.0.0', false); + runTest(undefined, pkg.version, false); + runTest('@@version', pkg.version, false); +}); diff --git a/src/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.js b/src/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.js new file mode 100644 index 00000000000000..dd7fcf0a795b48 --- /dev/null +++ b/src/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.js @@ -0,0 +1,38 @@ +import { getUpgradeableConfig } from './get_upgradeable_config'; + +export async function createOrUpgradeSavedConfig(options) { + const { + savedObjectsClient, + version, + buildNum, + log, + } = options; + + // next, try to upgrade an older config + const upgradeable = await getUpgradeableConfig({ savedObjectsClient, version }); + if (upgradeable) { + log(['plugin', 'elasticsearch'], { + tmpl: 'Upgrade config from <%= prevVersion %> to <%= newVersion %>', + prevVersion: upgradeable.id, + newVersion: version + }); + + await savedObjectsClient.create( + 'config', + { + ...upgradeable.attributes, + buildNum + }, + { id: version } + ); + + return; + } + + // if all else fails, create a new SavedConfig + await savedObjectsClient.create( + 'config', + { buildNum }, + { id: version } + ); +} diff --git a/src/ui/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.js b/src/ui/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.js new file mode 100644 index 00000000000000..3336eb22925eaf --- /dev/null +++ b/src/ui/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.js @@ -0,0 +1,23 @@ +import { isConfigVersionUpgradeable } from './is_config_version_upgradeable'; + +/** + * Find the most recent SavedConfig that is upgradeable to the current version + * @param {SavedObjectsClient} savedObjectsClient + * @param {string} version + * @return {Promise} + */ +export async function getUpgradeableConfig({ savedObjectsClient, version }) { + // attempt to find a config we can upgrade + const { saved_objects: savedConfigs } = await savedObjectsClient.find({ + type: 'config', + page: 1, + perPage: 1000, + sortField: 'buildNum', + sortOrder: 'desc' + }); + + // try to find a config that we can upgrade + return savedConfigs.find(savedConfig => ( + isConfigVersionUpgradeable(savedConfig.id, version) + )); +} diff --git a/src/ui/ui_settings/create_or_upgrade_saved_config/index.js b/src/ui/ui_settings/create_or_upgrade_saved_config/index.js new file mode 100644 index 00000000000000..5cb1112e41e2f8 --- /dev/null +++ b/src/ui/ui_settings/create_or_upgrade_saved_config/index.js @@ -0,0 +1 @@ +export { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config'; diff --git a/src/ui/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.js b/src/ui/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.js new file mode 100644 index 00000000000000..7a55a88c5e1f5a --- /dev/null +++ b/src/ui/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.js @@ -0,0 +1,38 @@ +import semver from 'semver'; +const rcVersionRegex = /(\d+\.\d+\.\d+)\-rc(\d+)/i; + +function extractRcNumber(version) { + const match = version.match(rcVersionRegex); + return match + ? [match[1], parseInt(match[2], 10)] + : [version, Infinity]; +} + +export function isConfigVersionUpgradeable(savedVersion, kibanaVersion) { + if ( + !savedVersion || + savedVersion === kibanaVersion || + /alpha|beta|snapshot/i.test(savedVersion) + ) { + return false; + } + + const [savedReleaseVersion, savedRcNumber] = extractRcNumber(savedVersion); + const [kibanaReleaseVersion, kibanaRcNumber] = extractRcNumber(kibanaVersion); + + // ensure that both release versions are valid, if not then abort + if (!semver.valid(savedReleaseVersion) || !semver.valid(kibanaReleaseVersion)) { + return false; + } + + // ultimately if the saved config is from a previous kibana version + // or from an earlier rc of the same version, then we can upgrade + const savedIsLessThanKibana = semver.lt(savedReleaseVersion, kibanaReleaseVersion); + const savedIsSameAsKibana = semver.eq(savedReleaseVersion, kibanaReleaseVersion); + const savedRcIsLessThanKibana = savedRcNumber < kibanaRcNumber; + if (savedIsLessThanKibana || (savedIsSameAsKibana && savedRcIsLessThanKibana)) { + return true; + } + + return false; +} diff --git a/src/ui/ui_settings/mirror_status.js b/src/ui/ui_settings/mirror_status.js deleted file mode 100644 index 22f9255e2e595d..00000000000000 --- a/src/ui/ui_settings/mirror_status.js +++ /dev/null @@ -1,15 +0,0 @@ -export function mirrorStatus(status, esStatus) { - if (!esStatus) { - status.red('UI Settings requires the elasticsearch plugin'); - return; - } - - const copyEsStatus = () => { - const { state } = esStatus; - const statusMessage = state === 'green' ? 'Ready' : `Elasticsearch plugin is ${state}`; - status[state](statusMessage); - }; - - copyEsStatus(); - esStatus.on('change', copyEsStatus); -} diff --git a/src/ui/ui_settings/routes/__tests__/lib/assert.js b/src/ui/ui_settings/routes/__tests__/lib/assert.js index 36de9a0e1e090c..238d9d7bc58237 100644 --- a/src/ui/ui_settings/routes/__tests__/lib/assert.js +++ b/src/ui/ui_settings/routes/__tests__/lib/assert.js @@ -10,7 +10,6 @@ export function assertDocMissingResponse({ result }) { assertSinonMatch(result, { statusCode: 404, error: 'Not Found', - message: sinon.match('document_missing_exception') - .and(sinon.match('document missing')) + message: 'Not Found' }); } diff --git a/src/ui/ui_settings/ui_settings_mixin.js b/src/ui/ui_settings/ui_settings_mixin.js index 131219e7a421f6..7d2b7bf4397c94 100644 --- a/src/ui/ui_settings/ui_settings_mixin.js +++ b/src/ui/ui_settings/ui_settings_mixin.js @@ -1,6 +1,5 @@ import { uiSettingsServiceFactory } from './ui_settings_service_factory'; import { getUiSettingsServiceForRequest } from './ui_settings_service_for_request'; -import { mirrorStatus } from './mirror_status'; import { UiExportsConsumer } from './ui_exports_consumer'; import { deleteRoute, @@ -9,40 +8,15 @@ import { setRoute, } from './routes'; -export function uiSettingsMixin(kbnServer, server, config) { - const status = kbnServer.status.create('ui settings'); - +export function uiSettingsMixin(kbnServer, server) { // reads the "uiSettingDefaults" from uiExports const uiExportsConsumer = new UiExportsConsumer(); kbnServer.uiExports.addConsumer(uiExportsConsumer); - if (!config.get('uiSettings.enabled')) { - status.disabled('uiSettings.enabled config is set to `false`'); - return; - } - - // Passed to the UiSettingsService. - // UiSettingsService calls the function before trying to read data from - // elasticsearch, giving us a chance to prevent it from happening. - // - // If the ui settings status isn't green we shouldn't be attempting to get - // user settings, since we can't be sure that all the necessary conditions - // (e.g. elasticsearch being available) are met. - const readInterceptor = () => { - if (status.state !== 'green') { - return {}; - } - }; - const getDefaults = () => ( uiExportsConsumer.getUiSettingDefaults() ); - // don't return, just let it happen when the plugins are ready - kbnServer.ready().then(() => { - mirrorStatus(status, kbnServer.status.getForPluginId('elasticsearch')); - }); - server.decorate('server', 'uiSettingsServiceFactory', (options = {}) => { return uiSettingsServiceFactory(server, { getDefaults, @@ -53,7 +27,6 @@ export function uiSettingsMixin(kbnServer, server, config) { server.addMemoizedFactoryToRequest('getUiSettingsService', request => { return getUiSettingsServiceForRequest(server, request, { getDefaults, - readInterceptor, }); }); diff --git a/src/ui/ui_settings/ui_settings_service.js b/src/ui/ui_settings/ui_settings_service.js index 559134f90191a5..2485aea63f1e69 100644 --- a/src/ui/ui_settings/ui_settings_service.js +++ b/src/ui/ui_settings/ui_settings_service.js @@ -1,4 +1,5 @@ -import { defaultsDeep, noop } from 'lodash'; +import { defaultsDeep } from 'lodash'; +import { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config'; function hydrateUserSettings(userSettings) { return Object.keys(userSettings) @@ -17,17 +18,14 @@ function hydrateUserSettings(userSettings) { * @property {string} options.id id of ui settings Elasticsearch doc * @property {AsyncFunction} options.callCluster function that accepts a method name and * param object which causes a request via some elasticsearch client - * @property {AsyncFunction} [options.readInterceptor] async function that is called when the - * UiSettingsService does a read() an has an oportunity to intercept the - * request and return an alternate `_source` value to use. */ export class UiSettingsService { constructor(options) { const { type, id, + buildNum, savedObjectsClient, - readInterceptor = noop, // we use a function for getDefaults() so that defaults can be different in // different scenarios, and so they can change over time getDefaults = () => ({}), @@ -35,7 +33,7 @@ export class UiSettingsService { this._savedObjectsClient = savedObjectsClient; this._getDefaults = getDefaults; - this._readInterceptor = readInterceptor; + this._buildNum = buildNum; this._type = type; this._id = id; } @@ -92,15 +90,29 @@ export class UiSettingsService { } async _write(changes) { - await this._savedObjectsClient.update(this._type, this._id, changes); - } + try { + await this._savedObjectsClient.update(this._type, this._id, changes); + } catch (error) { + const { isNotFoundError } = this._savedObjectsClient.errors; + if (!isNotFoundError(error)) { + throw error; + } - async _read(options = {}) { - const interceptValue = await this._readInterceptor(options); - if (interceptValue != null) { - return interceptValue; + await createOrUpgradeSavedConfig({ + id: this._id, + type: this._type, + buildNum: this._buildNum, + savedObjectsClient: this._savedObjectsClient, + log(...args) { + console.log('createOrUpgradeSavedConfig()', ...args); + }, + }); + + await this._write(changes); } + } + async _read(options = {}) { const { ignore401Errors = false } = options; diff --git a/src/ui/ui_settings/ui_settings_service_factory.js b/src/ui/ui_settings/ui_settings_service_factory.js index eeb26e3ce9d4fe..0280304433dafe 100644 --- a/src/ui/ui_settings/ui_settings_service_factory.js +++ b/src/ui/ui_settings/ui_settings_service_factory.js @@ -10,9 +10,6 @@ import { UiSettingsService } from './ui_settings_service'; * param object which causes a request via some elasticsearch client * @property {AsyncFunction} [options.getDefaults] async function that returns defaults/details about * the uiSettings. - * @property {AsyncFunction} [options.readInterceptor] async function that is called when the - * UiSettingsService does a read() an has an oportunity to intercept the - * request and return an alternate `_source` value to use. * @return {UiSettingsService} */ export function uiSettingsServiceFactory(server, options) { @@ -20,7 +17,6 @@ export function uiSettingsServiceFactory(server, options) { const { savedObjectsClient, - readInterceptor, getDefaults, } = options; @@ -28,7 +24,6 @@ export function uiSettingsServiceFactory(server, options) { type: 'config', id: config.get('pkg.version'), savedObjectsClient, - readInterceptor, getDefaults, }); } diff --git a/src/ui/ui_settings/ui_settings_service_for_request.js b/src/ui/ui_settings/ui_settings_service_for_request.js index 9006d1fcdf0083..484a0aecf68707 100644 --- a/src/ui/ui_settings/ui_settings_service_for_request.js +++ b/src/ui/ui_settings/ui_settings_service_for_request.js @@ -11,19 +11,14 @@ import { uiSettingsServiceFactory } from './ui_settings_service_factory'; * @param {Object} [options={}] * @property {AsyncFunction} [options.getDefaults] async function that returns defaults/details about * the uiSettings. - * @property {AsyncFunction} [options.readInterceptor] async function that is called when the - * UiSettingsService does a read() and has an oportunity to intercept the - * request and return an alternate `_source` value to use. * @return {UiSettingsService} */ export function getUiSettingsServiceForRequest(server, request, options = {}) { const { - readInterceptor, getDefaults } = options; const uiSettingsService = uiSettingsServiceFactory(server, { - readInterceptor, getDefaults, savedObjectsClient: request.getSavedObjectsClient() }); diff --git a/tasks/config/run.js b/tasks/config/run.js index 04b56baed0f687..f7cd245993bd9f 100644 --- a/tasks/config/run.js +++ b/tasks/config/run.js @@ -55,6 +55,7 @@ module.exports = function (grunt) { ...stdDevArgs, '--optimize.enabled=false', '--elasticsearch.url=' + esTestConfig.getUrl(), + '--elasticsearch.healthCheck.delay=360000', '--server.port=' + kibanaTestServerUrlParts.port, '--server.xsrf.disableProtection=true', ...kbnServerFlags, diff --git a/test/api_integration/apis/index.js b/test/api_integration/apis/index.js index 8f533c898eabe4..cc2f5375eee61f 100644 --- a/test/api_integration/apis/index.js +++ b/test/api_integration/apis/index.js @@ -1,6 +1,7 @@ export default function ({ loadTestFile }) { describe('apis', () => { loadTestFile(require.resolve('./index_patterns')); + loadTestFile(require.resolve('./saved_objects')); loadTestFile(require.resolve('./scripts')); loadTestFile(require.resolve('./search')); loadTestFile(require.resolve('./suggestions')); diff --git a/test/api_integration/apis/saved_objects/bulk_get.js b/test/api_integration/apis/saved_objects/bulk_get.js new file mode 100644 index 00000000000000..a34f81a06abe94 --- /dev/null +++ b/test/api_integration/apis/saved_objects/bulk_get.js @@ -0,0 +1,122 @@ +import expect from 'expect.js'; + +export default function ({ getService }) { + const supertest = getService('supertest'); + const es = getService('es'); + const esArchiver = getService('esArchiver'); + + const BULK_REQUESTS = [ + { + type: 'visualization', + id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', + }, + { + type: 'dashboard', + id: 'does not exist', + }, + { + type: 'config', + id: '7.0.0-alpha1', + }, + ]; + + describe('bulk_get', () => { + describe('with kibana index', () => { + before(() => esArchiver.load('saved_objects/basic')); + after(() => esArchiver.unload('saved_objects/basic')); + + it('should return 200 with individual responses', async () => ( + await supertest + .post(`/api/saved_objects/bulk_get`) + .send(BULK_REQUESTS) + .expect(200) + .then(resp => { + expect(resp.body).to.eql({ + saved_objects: [ + { + id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', + type: 'visualization', + updated_at: '2017-09-21T18:51:23.794Z', + version: resp.body.saved_objects[0].version, + attributes: { + title: 'Count of requests', + description: '', + version: 1, + // cheat for some of the more complex attributes + visState: resp.body.saved_objects[0].attributes.visState, + uiStateJSON: resp.body.saved_objects[0].attributes.uiStateJSON, + kibanaSavedObjectMeta: resp.body.saved_objects[0].attributes.kibanaSavedObjectMeta + } + }, + { + id: 'does not exist', + type: 'dashboard', + error: { + statusCode: 404, + message: 'Not found' + } + }, + { + id: '7.0.0-alpha1', + type: 'config', + updated_at: '2017-09-21T18:49:16.302Z', + version: resp.body.saved_objects[2].version, + attributes: { + buildNum: 8467, + defaultIndex: '91200a00-9efd-11e7-acb3-3dab96693fab' + } + } + ] + }); + }) + )); + }); + + describe('without kibana index', () => { + before(async () => ( + // just in case the kibana server has recreated it + await es.indices.delete({ + index: '.kibana', + ignore: [404], + }) + )); + + it('should return 200 with individual responses', async () => ( + await supertest + .post('/api/saved_objects/bulk_get') + .send(BULK_REQUESTS) + .expect(200) + .then(resp => { + expect(resp.body).to.eql({ + saved_objects: [ + { + id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', + type: 'visualization', + error: { + statusCode: 404, + message: 'Not found' + } + }, + { + id: 'does not exist', + type: 'dashboard', + error: { + statusCode: 404, + message: 'Not found' + } + }, + { + id: '7.0.0-alpha1', + type: 'config', + error: { + statusCode: 404, + message: 'Not found' + } + } + ] + }); + }) + )); + }); + }); +} diff --git a/test/api_integration/apis/saved_objects/create.js b/test/api_integration/apis/saved_objects/create.js new file mode 100644 index 00000000000000..79f896abe1a0b2 --- /dev/null +++ b/test/api_integration/apis/saved_objects/create.js @@ -0,0 +1,86 @@ +import expect from 'expect.js'; + +export default function ({ getService }) { + const supertest = getService('supertest'); + const es = getService('es'); + const esArchiver = getService('esArchiver'); + + async function sendAndValidateCreate() { + await supertest + .post(`/api/saved_objects/visualization`) + .send({ + attributes: { + title: 'My favorite vis' + } + }) + .expect(200) + .then(resp => { + // loose uuid validation + expect(resp.body).to.have.property('id').match(/^[0-9a-f-]{36}$/); + + // loose ISO8601 UTC time with milliseconds validation + expect(resp.body).to.have.property('updated_at').match(/^[\d-]{10}T[\d:\.]{12}Z$/); + + expect(resp.body).to.eql({ + id: resp.body.id, + type: 'visualization', + updated_at: resp.body.updated_at, + version: 1, + attributes: { + title: 'My favorite vis' + } + }); + }); + } + + describe('create', () => { + describe('with kibana index', () => { + before(() => esArchiver.load('saved_objects/basic')); + after(() => esArchiver.unload('saved_objects/basic')); + it('should return 200', sendAndValidateCreate); + }); + + describe('without kibana index', () => { + before(async () => ( + // just in case the kibana server has recreated it + await es.indices.delete({ + index: '.kibana', + ignore: [404], + }) + )); + + after(async () => ( + // make sure to delete the invalid kibana index + await es.indices.delete({ index: '.kibana' }) + )); + + it('should return 200 and create invalid kibana index', async () => { + // just in case the kibana server has recreated it + await es.indices.delete({ + index: '.kibana', + ignore: [404], + }); + + await sendAndValidateCreate(); + + const index = await es.indices.get({ + index: '.kibana' + }); + + const { mappings } = index['.kibana']; + expect(mappings).to.have.keys('doc'); + expect(mappings.doc.properties).to.have.keys([ + 'type', + 'updated_at', + 'visualization' + ]); + expect(mappings.doc.properties).to.not.have.keys([ + 'config', + 'dashboard', + 'index-pattern', + 'search' + ]); + }); + }); + }); +} diff --git a/test/api_integration/apis/saved_objects/delete.js b/test/api_integration/apis/saved_objects/delete.js new file mode 100644 index 00000000000000..336c8d90c68fb6 --- /dev/null +++ b/test/api_integration/apis/saved_objects/delete.js @@ -0,0 +1,59 @@ +import expect from 'expect.js'; + +export default function ({ getService }) { + const supertest = getService('supertest'); + const es = getService('es'); + const esArchiver = getService('esArchiver'); + + describe('delete', () => { + describe('with kibana index', () => { + before(() => esArchiver.load('saved_objects/basic')); + after(() => esArchiver.unload('saved_objects/basic')); + + it('should return 200 when deleting a doc', async () => ( + await supertest + .delete(`/api/saved_objects/dashboard/be3733a0-9efe-11e7-acb3-3dab96693fab`) + .expect(200) + .then(resp => { + expect(resp.body).to.eql({}); + }) + )); + + it('should return generic 404 when deleting an unknown doc', async () => ( + await supertest + .delete(`/api/saved_objects/dashboard/not-a-real-id`) + .expect(404) + .then(resp => { + expect(resp.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found' + }); + }) + )); + }); + + describe('without kibana index', () => { + before(async () => ( + // just in case the kibana server has recreated it + await es.indices.delete({ + index: '.kibana', + ignore: [404], + }) + )); + + it('returns generic 404 when kibana index is missing', async () => ( + await supertest + .delete(`/api/saved_objects/dashboard/not-a-real-id`) + .expect(404) + .then(resp => { + expect(resp.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found' + }); + }) + )); + }); + }); +} diff --git a/test/api_integration/apis/saved_objects/find.js b/test/api_integration/apis/saved_objects/find.js new file mode 100644 index 00000000000000..b24a6a6fe69aff --- /dev/null +++ b/test/api_integration/apis/saved_objects/find.js @@ -0,0 +1,157 @@ +import expect from 'expect.js'; + +export default function ({ getService }) { + const supertest = getService('supertest'); + const es = getService('es'); + const esArchiver = getService('esArchiver'); + + describe('find', () => { + describe('with kibana index', () => { + before(() => esArchiver.load('saved_objects/basic')); + after(() => esArchiver.unload('saved_objects/basic')); + + it('should return 200 with individual responses', async () => ( + await supertest + .get('/api/saved_objects/visualization?fields=title') + .expect(200) + .then(resp => { + expect(resp.body).to.eql({ + page: 1, + per_page: 20, + total: 1, + saved_objects: [ + { + type: 'visualization', + id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', + version: 1, + attributes: { + 'title': 'Count of requests' + } + } + ] + }); + }) + )); + + describe('unknown type', () => { + it('should return 200 with empty response', async () => ( + await supertest + .get('/api/saved_objects/wigwags') + .expect(200) + .then(resp => { + expect(resp.body).to.eql({ + page: 1, + per_page: 20, + total: 0, + saved_objects: [] + }); + }) + )); + }); + + describe('page beyond total', () => { + it('should return 200 with empty response', async () => ( + await supertest + .get('/api/saved_objects/visualization?page=100&per_page=100') + .expect(200) + .then(resp => { + expect(resp.body).to.eql({ + page: 100, + per_page: 100, + total: 1, + saved_objects: [] + }); + }) + )); + }); + + describe('unknown search field', () => { + it('should return 200 with empty response', async () => ( + await supertest + .get('/api/saved_objects/wigwags?search_fields=a') + .expect(200) + .then(resp => { + expect(resp.body).to.eql({ + page: 1, + per_page: 20, + total: 0, + saved_objects: [] + }); + }) + )); + }); + }); + + describe('without kibana index', () => { + before(async () => ( + // just in case the kibana server has recreated it + await es.indices.delete({ + index: '.kibana', + ignore: [404], + }) + )); + + it('should return 200 with empty response', async () => ( + await supertest + .get('/api/saved_objects/visualization') + .expect(200) + .then(resp => { + expect(resp.body).to.eql({ + page: 1, + per_page: 20, + total: 0, + saved_objects: [] + }); + }) + )); + + describe('unknown type', () => { + it('should return 200 with empty response', async () => ( + await supertest + .get('/api/saved_objects/wigwags') + .expect(200) + .then(resp => { + expect(resp.body).to.eql({ + page: 1, + per_page: 20, + total: 0, + saved_objects: [] + }); + }) + )); + }); + + describe('page beyond total', () => { + it('should return 200 with empty response', async () => ( + await supertest + .get('/api/saved_objects/visualization?page=100&per_page=100') + .expect(200) + .then(resp => { + expect(resp.body).to.eql({ + page: 100, + per_page: 100, + total: 0, + saved_objects: [] + }); + }) + )); + }); + + describe('unknown search field', () => { + it('should return 200 with empty response', async () => ( + await supertest + .get('/api/saved_objects/wigwags?search_fields=a') + .expect(200) + .then(resp => { + expect(resp.body).to.eql({ + page: 1, + per_page: 20, + total: 0, + saved_objects: [] + }); + }) + )); + }); + }); + }); +} diff --git a/test/api_integration/apis/saved_objects/get.js b/test/api_integration/apis/saved_objects/get.js new file mode 100644 index 00000000000000..d44e76be86d23a --- /dev/null +++ b/test/api_integration/apis/saved_objects/get.js @@ -0,0 +1,75 @@ +import expect from 'expect.js'; + +export default function ({ getService }) { + const supertest = getService('supertest'); + const es = getService('es'); + const esArchiver = getService('esArchiver'); + + describe('get', () => { + describe('with kibana index', () => { + before(() => esArchiver.load('saved_objects/basic')); + after(() => esArchiver.unload('saved_objects/basic')); + + it('should return 200', async () => ( + await supertest + .get(`/api/saved_objects/visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab`) + .expect(200) + .then(resp => { + expect(resp.body).to.eql({ + id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', + type: 'visualization', + updated_at: '2017-09-21T18:51:23.794Z', + version: resp.body.version, + attributes: { + title: 'Count of requests', + description: '', + version: 1, + // cheat for some of the more complex attributes + visState: resp.body.attributes.visState, + uiStateJSON: resp.body.attributes.uiStateJSON, + kibanaSavedObjectMeta: resp.body.attributes.kibanaSavedObjectMeta + } + }); + }) + )); + + describe('doc does not exist', () => { + it('should return same generic error as when index does not exist', async () => ( + await supertest + .get(`/api/saved_objects/visualization/foobar`) + .expect(404) + .then(resp => { + expect(resp.body).to.eql({ + error: 'Not Found', + message: 'Not Found', + statusCode: 404, + }); + }) + )); + }); + }); + + describe('without kibana index', () => { + before(async () => ( + // just in case the kibana server has recreated it + await es.indices.delete({ + index: '.kibana', + ignore: [404], + }) + )); + + it('should return basic 404 without mentioning index', async () => ( + await supertest + .get('/api/saved_objects/visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab') + .expect(404) + .then(resp => { + expect(resp.body).to.eql({ + error: 'Not Found', + message: 'Not Found', + statusCode: 404, + }); + }) + )); + }); + }); +} diff --git a/test/api_integration/apis/saved_objects/index.js b/test/api_integration/apis/saved_objects/index.js new file mode 100644 index 00000000000000..62c9802da29ea5 --- /dev/null +++ b/test/api_integration/apis/saved_objects/index.js @@ -0,0 +1,10 @@ +export default function ({ loadTestFile }) { + describe('saved_objects', () => { + loadTestFile(require.resolve('./bulk_get')); + loadTestFile(require.resolve('./create')); + loadTestFile(require.resolve('./delete')); + loadTestFile(require.resolve('./find')); + loadTestFile(require.resolve('./get')); + loadTestFile(require.resolve('./update')); + }); +} diff --git a/test/api_integration/apis/saved_objects/update.js b/test/api_integration/apis/saved_objects/update.js new file mode 100644 index 00000000000000..edc26f035a6c74 --- /dev/null +++ b/test/api_integration/apis/saved_objects/update.js @@ -0,0 +1,89 @@ +import expect from 'expect.js'; + +export default function ({ getService }) { + const supertest = getService('supertest'); + const es = getService('es'); + const esArchiver = getService('esArchiver'); + + describe('update', () => { + describe('with kibana index', () => { + before(() => esArchiver.load('saved_objects/basic')); + after(() => esArchiver.unload('saved_objects/basic')); + it('should return 200', async () => { + await supertest + .put(`/api/saved_objects/visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab`) + .send({ + attributes: { + title: 'My second favorite vis' + } + }) + .expect(200) + .then(resp => { + // loose uuid validation + expect(resp.body).to.have.property('id').match(/^[0-9a-f-]{36}$/); + + // loose ISO8601 UTC time with milliseconds validation + expect(resp.body).to.have.property('updated_at').match(/^[\d-]{10}T[\d:\.]{12}Z$/); + + expect(resp.body).to.eql({ + id: resp.body.id, + type: 'visualization', + updated_at: resp.body.updated_at, + version: 2, + attributes: { + title: 'My second favorite vis' + } + }); + }); + }); + + describe('unkown id', () => { + it('should return a generic 404', async () => { + await supertest + .put(`/api/saved_objects/visualization/not an id`) + .send({ + attributes: { + title: 'My second favorite vis' + } + }) + .expect(404) + .then(resp => { + expect(resp.body).eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found' + }); + }); + }); + }); + }); + + describe('without kibana index', () => { + before(async () => ( + // just in case the kibana server has recreated it + await es.indices.delete({ + index: '.kibana', + ignore: [404], + }) + )); + + it('should return generic 404', async () => ( + await supertest + .put(`/api/saved_objects/visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab`) + .send({ + attributes: { + title: 'My second favorite vis' + } + }) + .expect(404) + .then(resp => { + expect(resp.body).eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found' + }); + }) + )); + }); + }); +} diff --git a/test/api_integration/fixtures/es_archiver/saved_objects/basic/data.json.gz b/test/api_integration/fixtures/es_archiver/saved_objects/basic/data.json.gz new file mode 100644 index 00000000000000..c07188439b0e07 Binary files /dev/null and b/test/api_integration/fixtures/es_archiver/saved_objects/basic/data.json.gz differ diff --git a/test/api_integration/fixtures/es_archiver/saved_objects/basic/mappings.json b/test/api_integration/fixtures/es_archiver/saved_objects/basic/mappings.json new file mode 100644 index 00000000000000..26c62bca335d94 --- /dev/null +++ b/test/api_integration/fixtures/es_archiver/saved_objects/basic/mappings.json @@ -0,0 +1,252 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "number_of_replicas": "1" + } + }, + "mappings": { + "doc": { + "dynamic": "strict", + "properties": { + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + }, + "defaultIndex": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + } + } + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + } + } + } +} \ No newline at end of file