[uiSettings] auto create/upgrade saved config (#14164)

* [uiSettings] auto upgrade savedConfig doc when missing

* naming tweaks

* fix comments

* ensure that rcVersions are not found within a version

* add some tests for non-single digit versions/rcs/betas

* return the condition, rather than using an if()

* assert that getUpgradeableConfig() is always called once

* [uiSettingsService] remove excess space

* [savedObjectsClient] only attempt to createOrUpgradeSavedConfig once

* [uiSettings/routes/tests] remove unused assert helper

* [functional/console] correct test title

* [ftr/kibanaServer/uiSettings] fix disableToastAutohide timeout
This commit is contained in:
Spencer 2017-10-05 15:34:09 -07:00 committed by GitHub
parent 330665b706
commit 6998f07454
38 changed files with 717 additions and 755 deletions

View file

@ -15,7 +15,6 @@ export default class ClusterManager {
const serverArgv = [];
const optimizerArgv = [
'--plugins.initialize=false',
'--uiSettings.enabled=false',
'--server.autoListen=false',
];

View file

@ -4,3 +4,5 @@ logging:
json: true
optimize:
enabled: false
plugins:
initialize: false

View file

@ -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
}

View file

@ -9,7 +9,6 @@ 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();
@ -34,7 +33,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 = {

View file

@ -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);
});
});
});

View file

@ -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-*');
});
});
});
});

View file

@ -1,7 +1,5 @@
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';
@ -107,7 +105,6 @@ export default function (plugin, server) {
indexName: config.get('kibana.index'),
kibanaIndexMappingsDsl: server.getKibanaIndexMappingsDsl()
}))
.then(_.partial(migrateConfig, server))
.then(() => {
const tribeUrl = config.get('elasticsearch.tribe.url');
if (tribeUrl) {

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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')
});
};
}

View file

@ -202,14 +202,6 @@ export default () => Joi.object({
indexCheckTimeout: Joi.number().default(5000)
}).default(),
uiSettings: Joi.object({
// this is used to prevent the uiSettings from initializing. Since they
// require the elasticsearch plugin in order to function we need to turn
// them off when we turn off the elasticsearch plugin (like we do in the
// optimizer half of the dev server)
enabled: Joi.boolean().default(true)
}).default(),
i18n: Joi.object({
defaultLocale: Joi.string().default('en'),
}).default(),

View file

@ -17,6 +17,7 @@ const deprecations = [
//server
rename('server.ssl.cert', 'server.ssl.certificate'),
unused('server.xsrf.token'),
unused('uiSettings.enabled'),
serverSslEnabled,
];

View file

@ -55,12 +55,13 @@ export default class ServerStatus {
}
overall() {
const state = _(this._created)
.map(function (status) {
return states.get(status.state);
})
.sortBy('severity')
.pop();
const state = Object
// take all created status objects
.values(this._created)
// get the state descriptor for each status
.map(status => states.get(status.state))
// reduce to the state with the highest severity, defaulting to green
.reduce((a, b) => a.severity > b.severity ? a : b, states.get('green'));
const statuses = _.where(this._created, { state: state.id });
const since = _.get(_.sortBy(statuses, 'since'), [0, 'since']);

View file

@ -32,17 +32,24 @@ describe('UiExports', function () {
server: { port: 0 }, // pick a random open port
logging: { silent: true }, // no logs
optimize: { enabled: false },
uiSettings: { enabled: false },
plugins: {
paths: [resolve(__dirname, './fixtures/test_app')] // inject an app so we can hit /app/{id}
},
});
await kbnServer.ready();
kbnServer.status.get('ui settings').state = 'green';
kbnServer.server.decorate('request', 'getUiSettingsService', () => {
return { getDefaults: noop, getUserProvided: noop };
});
// TODO: hopefully we can add better support for something
// like this in the new platform
kbnServer.server._requestor._decorations.getUiSettingsService = {
apply: undefined,
method() {
return {
getDefaults: noop,
getUserProvided: noop
};
}
};
});
afterEach(async () => {

View file

@ -6,8 +6,9 @@ export const savedObjectsClientErrors = SavedObjectsClient.errors;
export function createObjectsClientStub(type, id, esDocSource = {}) {
const savedObjectsClient = {
update: sinon.stub().returns(Promise.resolve()),
update: sinon.stub(),
get: sinon.stub().returns({ attributes: esDocSource }),
create: sinon.stub(),
errors: savedObjectsClientErrors
};

View file

@ -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()', () => {

View file

@ -2,9 +2,10 @@ import { isEqual } from 'lodash';
import expect from 'expect.js';
import { errors as esErrors } from 'elasticsearch';
import Chance from 'chance';
import sinon from 'sinon';
import { UiSettingsService } from '../ui_settings_service';
import * as createOrUpgradeSavedConfigNS from '../create_or_upgrade_saved_config/create_or_upgrade_saved_config';
import {
createObjectsClientStub,
savedObjectsClientErrors,
@ -12,33 +13,41 @@ import {
const TYPE = 'config';
const ID = 'kibana-version';
const BUILD_NUM = 1234;
const chance = new Chance();
function setup(options = {}) {
const {
readInterceptor,
getDefaults,
defaults = {},
esDocSource = {},
savedObjectsClient = createObjectsClientStub(TYPE, ID, esDocSource)
} = options;
const uiSettings = new UiSettingsService({
type: TYPE,
id: ID,
getDefaults: getDefaults || (() => defaults),
readInterceptor,
savedObjectsClient,
});
return {
uiSettings,
assertGetQuery: savedObjectsClient.assertGetQuery,
assertUpdateQuery: savedObjectsClient.assertUpdateQuery,
};
}
describe('ui settings', () => {
const sandbox = sinon.sandbox.create();
function setup(options = {}) {
const {
getDefaults,
defaults = {},
esDocSource = {},
savedObjectsClient = createObjectsClientStub(TYPE, ID, esDocSource)
} = options;
const uiSettings = new UiSettingsService({
type: TYPE,
id: ID,
buildNum: BUILD_NUM,
getDefaults: getDefaults || (() => defaults),
savedObjectsClient,
});
const createOrUpgradeSavedConfig = sandbox.stub(createOrUpgradeSavedConfigNS, 'createOrUpgradeSavedConfig');
return {
uiSettings,
savedObjectsClient,
createOrUpgradeSavedConfig,
assertGetQuery: savedObjectsClient.assertGetQuery,
assertUpdateQuery: savedObjectsClient.assertUpdateQuery,
};
}
afterEach(() => sandbox.restore());
describe('overview', () => {
it('has expected api surface', () => {
const { uiSettings } = setup();
@ -61,15 +70,43 @@ describe('ui settings', () => {
});
it('updates a single value in one operation', async () => {
const { uiSettings, assertUpdateQuery } = setup();
const { uiSettings, savedObjectsClient } = setup();
await uiSettings.setMany({ one: 'value' });
assertUpdateQuery({ one: 'value' });
savedObjectsClient.assertUpdateQuery({ one: 'value' });
});
it('updates several values in one operation', async () => {
const { uiSettings, assertUpdateQuery } = setup();
const { uiSettings, savedObjectsClient } = setup();
await uiSettings.setMany({ one: 'value', another: 'val' });
assertUpdateQuery({ one: 'value', another: 'val' });
savedObjectsClient.assertUpdateQuery({ one: 'value', another: 'val' });
});
it('automatically creates the savedConfig if it is missing', async () => {
const { uiSettings, savedObjectsClient, createOrUpgradeSavedConfig } = setup();
savedObjectsClient.update
.onFirstCall()
.throws(savedObjectsClientErrors.createGenericNotFoundError())
.onSecondCall()
.returns({});
await uiSettings.setMany({ foo: 'bar' });
sinon.assert.calledTwice(savedObjectsClient.update);
sinon.assert.calledOnce(createOrUpgradeSavedConfig);
});
it('only tried to auto create once and throws NotFound', async () => {
const { uiSettings, savedObjectsClient, createOrUpgradeSavedConfig } = setup();
savedObjectsClient.update.throws(savedObjectsClientErrors.createGenericNotFoundError());
try {
await uiSettings.setMany({ foo: 'bar' });
throw new Error('expected setMany to throw a NotFound error');
} catch (error) {
expect(savedObjectsClientErrors.isNotFoundError(error)).to.be(true);
}
sinon.assert.calledTwice(savedObjectsClient.update);
sinon.assert.calledOnce(createOrUpgradeSavedConfig);
});
});
@ -80,9 +117,9 @@ describe('ui settings', () => {
});
it('updates single values by (key, value)', async () => {
const { uiSettings, assertUpdateQuery } = setup();
const { uiSettings, savedObjectsClient } = setup();
await uiSettings.set('one', 'value');
assertUpdateQuery({ one: 'value' });
savedObjectsClient.assertUpdateQuery({ one: 'value' });
});
});
@ -93,9 +130,9 @@ describe('ui settings', () => {
});
it('removes single values by key', async () => {
const { uiSettings, assertUpdateQuery } = setup();
const { uiSettings, savedObjectsClient } = setup();
await uiSettings.remove('one');
assertUpdateQuery({ one: null });
savedObjectsClient.assertUpdateQuery({ one: null });
});
});
@ -106,15 +143,15 @@ describe('ui settings', () => {
});
it('removes a single value', async () => {
const { uiSettings, assertUpdateQuery } = setup();
const { uiSettings, savedObjectsClient } = setup();
await uiSettings.removeMany(['one']);
assertUpdateQuery({ one: null });
savedObjectsClient.assertUpdateQuery({ one: null });
});
it('updates several values in one operation', async () => {
const { uiSettings, assertUpdateQuery } = setup();
const { uiSettings, savedObjectsClient } = setup();
await uiSettings.removeMany(['one', 'two', 'three']);
assertUpdateQuery({ one: null, two: null, three: null });
savedObjectsClient.assertUpdateQuery({ one: null, two: null, three: null });
});
});
@ -145,9 +182,9 @@ describe('ui settings', () => {
describe('#getUserProvided()', () => {
it('pulls user configuration from ES', async () => {
const { uiSettings, assertGetQuery } = setup();
const { uiSettings, savedObjectsClient } = setup();
await uiSettings.getUserProvided();
assertGetQuery();
savedObjectsClient.assertGetQuery();
});
it('returns user configuration', async () => {
@ -240,9 +277,9 @@ describe('ui settings', () => {
describe('#getRaw()', () => {
it('pulls user configuration from ES', async () => {
const esDocSource = {};
const { uiSettings, assertGetQuery } = setup({ esDocSource });
const { uiSettings, savedObjectsClient } = setup({ esDocSource });
await uiSettings.getRaw();
assertGetQuery();
savedObjectsClient.assertGetQuery();
});
it(`without user configuration it's equal to the defaults`, async () => {
@ -273,9 +310,9 @@ describe('ui settings', () => {
describe('#getAll()', () => {
it('pulls user configuration from ES', async () => {
const esDocSource = {};
const { uiSettings, assertGetQuery } = setup({ esDocSource });
const { uiSettings, savedObjectsClient } = setup({ esDocSource });
await uiSettings.getAll();
assertGetQuery();
savedObjectsClient.assertGetQuery();
});
it(`returns defaults when es doc is empty`, async () => {
@ -310,9 +347,9 @@ describe('ui settings', () => {
describe('#get()', () => {
it('pulls user configuration from ES', async () => {
const esDocSource = {};
const { uiSettings, assertGetQuery } = setup({ esDocSource });
const { uiSettings, savedObjectsClient } = setup({ esDocSource });
await uiSettings.get();
assertGetQuery();
savedObjectsClient.assertGetQuery();
});
it(`returns the promised value for a key`, async () => {
@ -337,47 +374,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'
});
});
});
});
});

View file

@ -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,
});
});
});

View file

@ -0,0 +1,119 @@
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,
});
sinon.assert.calledOnce(getUpgradeableConfig);
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,
})
);
});
});
});

View file

@ -0,0 +1,33 @@
import expect from 'expect.js';
import { isConfigVersionUpgradeable } from '../is_config_version_upgradeable';
import { pkg } from '../../../../utils';
describe('savedObjects/health_check/isConfigVersionUpgradeable', function () {
function isUpgradableTest(savedVersion, kibanaVersion, expected) {
it(`should return ${expected} for config version ${savedVersion} and kibana version ${kibanaVersion}`, () => {
expect(isConfigVersionUpgradeable(savedVersion, kibanaVersion)).to.be(expected);
});
}
isUpgradableTest('1.0.0-beta1', pkg.version, false);
isUpgradableTest('1.0.0-beta256', pkg.version, false);
isUpgradableTest('10.100.1000-beta256', '10.100.1000-beta257', false);
isUpgradableTest(pkg.version, pkg.version, false);
isUpgradableTest('4.0.0-RC1', '4.0.0-RC2', true);
isUpgradableTest('10.100.1000-rc256', '10.100.1000-RC257', true);
isUpgradableTest('4.0.0-rc2', '4.0.0-rc1', false);
isUpgradableTest('4.0.0-rc2', '4.0.0', true);
isUpgradableTest('4.0.0-rc2', '4.0.2', true);
isUpgradableTest('4.0.1', '4.1.0-rc', true);
isUpgradableTest('4.0.0-rc1', '4.0.0', true);
isUpgradableTest('50.0.9-rc150', '50.0.9', true);
isUpgradableTest('50.0.9', '50.0.9-rc150', false);
isUpgradableTest('50.0.9', '50.0.10-rc150', true);
isUpgradableTest('4.0.0-rc1-SNAPSHOT', '4.0.0', false);
isUpgradableTest('4.1.0-rc1-SNAPSHOT', '4.1.0-rc1', false);
isUpgradableTest('5.0.0-alpha11', '5.0.0', false);
isUpgradableTest('50.0.10-rc150-SNAPSHOT', '50.0.9', false);
isUpgradableTest(undefined, pkg.version, false);
isUpgradableTest('@@version', pkg.version, false);
});

View file

@ -0,0 +1,39 @@
import { defaults } from 'lodash';
import { getUpgradeableConfig } from './get_upgradeable_config';
export async function createOrUpgradeSavedConfig(options) {
const {
savedObjectsClient,
version,
buildNum,
log,
} = options;
// try to find an older config we can upgrade
const upgradeableConfig = await getUpgradeableConfig({
savedObjectsClient,
version
});
if (upgradeableConfig) {
log(['plugin', 'elasticsearch'], {
tmpl: 'Upgrade config from <%= prevVersion %> to <%= newVersion %>',
prevVersion: upgradeableConfig.id,
newVersion: version
});
}
// default to the attributes of the upgradeableConfig if available
const attributes = defaults(
{ buildNum },
upgradeableConfig ? upgradeableConfig.attributes : {}
);
// create the new SavedConfig
await savedObjectsClient.create(
'config',
attributes,
{ id: version }
);
}

View file

@ -0,0 +1,24 @@
import { isConfigVersionUpgradeable } from './is_config_version_upgradeable';
/**
* Find the most recent SavedConfig that is upgradeable to the specified version
* @param {Object} options
* @property {SavedObjectsClient} savedObjectsClient
* @property {string} version
* @return {Promise<SavedConfig|undefined>}
*/
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)
));
}

View file

@ -0,0 +1 @@
export { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config';

View file

@ -0,0 +1,35 @@
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 (
typeof savedVersion !== 'string' ||
typeof kibanaVersion !== 'string' ||
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;
return savedIsLessThanKibana || (savedIsSameAsKibana && savedRcIsLessThanKibana);
}

View file

@ -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);
}

View file

@ -1,31 +1,32 @@
import expect from 'expect.js';
import sinon from 'sinon';
import {
getServices,
chance,
assertDocMissingResponse,
assertSinonMatch,
waitUntilNextHealthCheck,
} from './lib';
export function docMissingSuite() {
beforeEach(waitUntilNextHealthCheck);
// health check doesn't create config doc so we
// only have to wait once
before(waitUntilNextHealthCheck);
async function setup() {
const { kbnServer, savedObjectsClient } = getServices();
// delete all config docs
const { saved_objects: objs } = await savedObjectsClient.find({ type: 'config' });
for (const obj of objs) {
await savedObjectsClient.delete(obj.type, obj.id);
}
return { kbnServer };
}
// ensure the kibana index has no documents
beforeEach(async () => {
const { kbnServer, callCluster } = getServices();
await callCluster('deleteByQuery', {
index: kbnServer.config.get('kibana.index'),
body: {
query: { match_all: {} }
}
});
});
describe('get route', () => {
it('returns a 200 with empty values', async () => {
const { kbnServer } = await setup();
it('creates doc, returns a 200 with no settings', async () => {
const { kbnServer } = getServices();
const { statusCode, result } = await kbnServer.inject({
method: 'GET',
@ -33,48 +34,81 @@ export function docMissingSuite() {
});
expect(statusCode).to.be(200);
expect(result).to.eql({ settings: {} });
assertSinonMatch(result, {
settings: {}
});
});
});
describe('set route', () => {
it('returns a 404', async () => {
const { kbnServer } = await setup();
it('creates doc, returns a 200 with value set', async () => {
const { kbnServer } = getServices();
assertDocMissingResponse(await kbnServer.inject({
const defaultIndex = chance.word();
const { statusCode, result } = await kbnServer.inject({
method: 'POST',
url: '/api/kibana/settings/defaultIndex',
payload: {
value: chance.word()
payload: { value: defaultIndex }
});
expect(statusCode).to.be(200);
assertSinonMatch(result, {
settings: {
buildNum: {
userValue: sinon.match.number
},
defaultIndex: {
userValue: defaultIndex
}
}
}));
});
});
});
describe('setMany route', () => {
it('returns a 404', async () => {
const { kbnServer } = await setup();
it('creates doc, returns 200 with updated values', async () => {
const { kbnServer } = getServices();
assertDocMissingResponse(await kbnServer.inject({
const defaultIndex = chance.word();
const { statusCode, result } = await kbnServer.inject({
method: 'POST',
url: '/api/kibana/settings',
payload: {
changes: {
defaultIndex: chance.word()
changes: { defaultIndex }
}
});
expect(statusCode).to.be(200);
assertSinonMatch(result, {
settings: {
buildNum: {
userValue: sinon.match.number
},
defaultIndex: {
userValue: defaultIndex
}
}
}));
});
});
});
describe('delete route', () => {
it('returns a 404', async () => {
const { kbnServer } = await setup();
it('creates doc, returns a 200 with just buildNum', async () => {
const { kbnServer } = getServices();
assertDocMissingResponse(await kbnServer.inject({
const { statusCode, result } = await kbnServer.inject({
method: 'DELETE',
url: '/api/kibana/settings/defaultIndex'
}));
});
expect(statusCode).to.be(200);
assertSinonMatch(result, {
settings: {
buildNum: {
userValue: sinon.match.number
}
}
});
});
});
}

View file

@ -3,7 +3,7 @@ import expect from 'expect.js';
import {
getServices,
chance,
assertGeneric404Response,
assertServiceUnavailableResponse,
waitUntilNextHealthCheck,
} from './lib';
@ -58,10 +58,10 @@ export function indexMissingSuite() {
});
describe('set route', () => {
it('returns a generic 404 and does not create the kibana index', async () => {
it('returns a 503 and does not create the kibana index', async () => {
const { kbnServer, assertNoKibanaIndex } = await setup();
assertGeneric404Response(await kbnServer.inject({
assertServiceUnavailableResponse(await kbnServer.inject({
method: 'POST',
url: '/api/kibana/settings/defaultIndex',
payload: {
@ -74,10 +74,10 @@ export function indexMissingSuite() {
});
describe('setMany route', () => {
it('returns a generic 404 and does not create the kibana index', async () => {
it('returns a 503 and does not create the kibana index', async () => {
const { kbnServer, assertNoKibanaIndex } = await setup();
assertGeneric404Response(await kbnServer.inject({
assertServiceUnavailableResponse(await kbnServer.inject({
method: 'POST',
url: '/api/kibana/settings',
payload: {
@ -92,10 +92,10 @@ export function indexMissingSuite() {
});
describe('delete route', () => {
it('returns a generic 404 and does not create the kibana index', async () => {
it('returns a 503 and does not create the kibana index', async () => {
const { kbnServer, assertNoKibanaIndex } = await setup();
assertGeneric404Response(await kbnServer.inject({
assertServiceUnavailableResponse(await kbnServer.inject({
method: 'DELETE',
url: '/api/kibana/settings/defaultIndex'
}));

View file

@ -6,18 +6,10 @@ export function assertSinonMatch(value, match) {
sinon.assert.calledWithExactly(stub, match);
}
export function assertGeneric404Response({ result }) {
export function assertServiceUnavailableResponse({ result }) {
assertSinonMatch(result, {
statusCode: 404,
error: 'Not Found',
message: sinon.match.same('Not Found')
});
}
export function assertDocMissingResponse({ result }) {
assertSinonMatch(result, {
statusCode: 404,
error: 'Not Found',
message: 'Not Found'
statusCode: 503,
error: 'Service Unavailable',
message: 'Service Unavailable'
});
}

View file

@ -11,6 +11,5 @@ export {
export {
assertSinonMatch,
assertDocMissingResponse,
assertGeneric404Response,
assertServiceUnavailableResponse,
} from './assert';

View file

@ -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,
});
});

View file

@ -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)
@ -9,35 +10,38 @@ function hydrateUserSettings(userSettings) {
/**
* Service that provides access to the UiSettings stored in elasticsearch.
*
* @class UiSettingsService
* @param {Object} options
* @property {string} options.index Elasticsearch index name where settings are stored
* @property {string} options.type type of ui settings Elasticsearch doc
* @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
* @param {Object} options
* @property {string} options.type type of SavedConfig object
* @property {string} options.id id of SavedConfig object
* @property {number} options.buildNum
* @property {SavedObjectsClient} options.savedObjectsClient
* @property {Function} [options.getDefaults]
* @property {Function} [options.log]
*/
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 = () => ({}),
// function that accepts log messages in the same format as server.log
log = () => {},
} = options;
this._savedObjectsClient = savedObjectsClient;
this._getDefaults = getDefaults;
this._readInterceptor = readInterceptor;
this._type = type;
this._id = id;
this._buildNum = buildNum;
this._savedObjectsClient = savedObjectsClient;
this._getDefaults = getDefaults;
this._log = log;
}
async getDefaults() {
@ -72,7 +76,7 @@ export class UiSettingsService {
}
async setMany(changes) {
await this._write(changes);
await this._write({ changes });
}
async set(key, value) {
@ -91,16 +95,30 @@ export class UiSettingsService {
await this.setMany(changes);
}
async _write(changes) {
await this._savedObjectsClient.update(this._type, this._id, changes);
async _write({ changes, autoCreateOrUpgradeIfMissing = true }) {
try {
await this._savedObjectsClient.update(this._type, this._id, changes);
} catch (error) {
const { isNotFoundError } = this._savedObjectsClient.errors;
if (!isNotFoundError(error) || !autoCreateOrUpgradeIfMissing) {
throw error;
}
await createOrUpgradeSavedConfig({
savedObjectsClient: this._savedObjectsClient,
version: this._id,
buildNum: this._buildNum,
log: this._log,
});
await this._write({
changes,
autoCreateOrUpgradeIfMissing: false
});
}
}
async _read(options = {}) {
const interceptValue = await this._readInterceptor(options);
if (interceptValue != null) {
return interceptValue;
}
const {
ignore401Errors = false
} = options;

View file

@ -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,15 +17,15 @@ export function uiSettingsServiceFactory(server, options) {
const {
savedObjectsClient,
readInterceptor,
getDefaults,
} = options;
return new UiSettingsService({
type: 'config',
id: config.get('pkg.version'),
buildNum: config.get('pkg.buildNum'),
savedObjectsClient,
readInterceptor,
getDefaults,
log: (...args) => server.log(...args),
});
}

View file

@ -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()
});

View file

@ -1,7 +1,5 @@
import { format as formatUrl } from 'url';
import { delay } from 'bluebird';
import { KibanaServerStatus } from './status';
import { KibanaServerUiSettings } from './ui_settings';
import { KibanaServerVersion } from './version';
@ -9,7 +7,6 @@ import { KibanaServerVersion } from './version';
export async function KibanaServerProvider({ getService }) {
const log = getService('log');
const config = getService('config');
const lifecycle = getService('lifecycle');
const es = getService('es');
const kibanaIndex = await getService('kibanaIndex').init();
@ -18,51 +15,7 @@ export async function KibanaServerProvider({ getService }) {
const url = formatUrl(config.get('servers.kibana'));
this.status = new KibanaServerStatus(url);
this.version = new KibanaServerVersion(this.status);
this.uiSettings = new KibanaServerUiSettings(log, es, kibanaIndex, this.version);
lifecycle.on('beforeEachTest', async () => {
await this.waitForStabilization();
});
}
async waitForStabilization() {
const { status, uiSettings } = this;
let firstCheck = true;
const pingInterval = 500; // ping every 500 ms for an update
const startMs = Date.now();
const timeout = config.get('timeouts.kibanaStabilize');
let exists;
let state;
while (true) {
try {
exists = await uiSettings.existInEs();
state = await status.getOverallState();
if (exists && state === 'green') {
log.debug(`Kibana uiSettings are in elasticsearch and the server is reporting a green status`);
return;
}
} catch (err) {
log.warning(`Failed to check for kibana stabilization: ${err.stack}`);
}
if (Date.now() - startMs >= timeout) {
break;
}
if (firstCheck) {
// we only log once, and only if we failed the first check
firstCheck = false;
log.debug(`waiting up to ${timeout}ms for kibana to stabilize...`);
}
await delay(pingInterval);
}
const docState = exists ? 'exists' : `doesn't exist`;
throw new Error(`Kibana never stabilized: config doc ${docState} and status is ${state}`);
this.uiSettings = new KibanaServerUiSettings(url, log, es, kibanaIndex, this.version);
}
};
}

View file

@ -1,42 +1,27 @@
import { createCallCluster } from '../../../../src/test_utils/es';
import { SavedObjectsClient } from '../../../../src/server/saved_objects';
import Wreck from 'wreck';
import { get } from 'lodash';
const MINUTE = 60 * 1000;
const HOUR = 60 * MINUTE;
export class KibanaServerUiSettings {
constructor(log, es, kibanaIndex, kibanaVersion) {
constructor(url, log, es, kibanaIndex, kibanaVersion) {
this._log = log;
this._kibanaVersion = kibanaVersion;
this._savedObjectsClient = new SavedObjectsClient({
index: kibanaIndex.getName(),
mappings: kibanaIndex.getMappingsDsl(),
callCluster: createCallCluster(es),
async onBeforeWrite() {
await es.cluster.health({
timeout: '5s',
index: kibanaIndex.getName(),
waitForStatus: 'yellow',
});
}
this._wreck = Wreck.defaults({
headers: { 'kbn-xsrf': 'ftr/services/uiSettings' },
baseUrl: url,
json: true,
redirects: 3,
});
}
async _id() {
return await this._kibanaVersion.get();
}
async existInEs() {
return !!(await this._read());
}
/*
** Gets defaultIndex from the config doc.
*/
async getDefaultIndex() {
const doc = await this._read();
if (!doc) {
throw new TypeError('Failed to fetch kibana config doc');
}
const defaultIndex = doc.attributes.defaultIndex;
const { payload } = await this._wreck.get('/api/kibana/settings');
const defaultIndex = get(payload, 'settings.defaultIndex.userValue');
this._log.verbose('uiSettings.defaultIndex: %j', defaultIndex);
return defaultIndex;
}
@ -51,18 +36,26 @@ export class KibanaServerUiSettings {
*/
async disableToastAutohide() {
await this.update({
'notifications:lifetime:banner': 360000,
'notifications:lifetime:error': 360000,
'notifications:lifetime:warning': 360000,
'notifications:lifetime:info': 360000,
'notifications:lifetime:banner': HOUR,
'notifications:lifetime:error': HOUR,
'notifications:lifetime:warning': HOUR,
'notifications:lifetime:info': HOUR,
});
}
async replace(doc) {
const { payload } = await this._wreck.get('/api/kibana/settings');
for (const key of Object.keys(payload.settings)) {
await this._wreck.delete(`/api/kibana/settings/${key}`);
}
this._log.debug('replacing kibana config doc: %j', doc);
await this._savedObjectsClient.create('config', doc, {
id: await this._id(),
overwrite: true,
await this._wreck.post('/api/kibana/settings', {
payload: {
changes: doc
}
});
}
@ -72,17 +65,10 @@ export class KibanaServerUiSettings {
*/
async update(updates) {
this._log.debug('applying update to kibana config: %j', updates);
await this._savedObjectsClient.update('config', await this._id(), updates);
}
async _read() {
try {
const doc = await this._savedObjectsClient.get('config', await this._id());
this._log.verbose('Fetched kibana config doc', doc);
return doc;
} catch (err) {
this._log.debug('Failed to fetch kibana config doc', err.message);
return;
}
await this._wreck.post('/api/kibana/settings', {
payload: {
changes: updates
}
});
}
}

View file

@ -35,8 +35,8 @@ export default function ({ getService, getPageObjects }) {
});
});
it('default request response should contain .kibana' , function () {
const expectedResponseContains = '"_index": ".kibana",';
it('default request response should include `"timed_out": false`' , function () {
const expectedResponseContains = '"timed_out": false,';
return PageObjects.console.clickPlay()
.then(function () {

View file

@ -13,7 +13,6 @@ export default function ({ getService, getPageObjects }) {
await esArchiver.loadIfNeeded('logstash_functional');
await esArchiver.load('discover');
await kibanaServer.waitForStabilization();
// delete .kibana index and update configDoc
await kibanaServer.uiSettings.replace({
'dateFormat:tz': 'UTC',

View file

@ -10,7 +10,6 @@ export default function ({ getService, loadTestFile }) {
remote.setWindowSize(1280, 800);
await esArchiver.loadIfNeeded('logstash_functional');
await esArchiver.load('visualize');
await kibanaServer.waitForStabilization();
await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'UTC', 'defaultIndex': 'logstash-*' });
});