mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
* Fix wrong impor (#52994) * Licensing plugin and XPackInfo uses the same license data (#52507) * convert xpackinfo to TS * use NP Licensing plugin in XPackInfo * update mocks * put license regresh hack back. otherwise new license won't be re-fetched when signature changed. was deleted by mistake * add functional test for legacy xpackmain * declare setup types on client & server explicitly * rename mock license --> licensing to match plugin name * add tests for createLicensePoller * fix type error * adopt tests for xpack_info * createXPackInfo uses new platform API * put back error mute * address comments * fix renamed import * address comment * update tests to reduce delays * deprecate xpack.xpack_main.xpack_api_polling_frequency_millis * use snake_case in config * fix wrong import * prevent eslint error with renaming mock file
This commit is contained in:
parent
e0f682afa6
commit
3e952be85e
40 changed files with 736 additions and 774 deletions
|
@ -292,7 +292,7 @@ module.exports = {
|
|||
allowSameFolder: true,
|
||||
},
|
||||
{
|
||||
target: ['src/core/**/*'],
|
||||
target: ['src/**/*'],
|
||||
from: ['x-pack/**/*'],
|
||||
errorMessage: 'OSS cannot import x-pack files.',
|
||||
},
|
||||
|
|
|
@ -32,7 +32,6 @@ import { DevToolsSetup, DevToolsStart } from '../../../../plugins/dev_tools/publ
|
|||
import { KibanaLegacySetup, KibanaLegacyStart } from '../../../../plugins/kibana_legacy/public';
|
||||
import { HomePublicPluginSetup, HomePublicPluginStart } from '../../../../plugins/home/public';
|
||||
import { SharePluginSetup, SharePluginStart } from '../../../../plugins/share/public';
|
||||
import { LicensingPluginSetup } from '../../../../../x-pack/plugins/licensing/common/types';
|
||||
import { BfetchPublicSetup, BfetchPublicStart } from '../../../../plugins/bfetch/public';
|
||||
import { UsageCollectionSetup } from '../../../../plugins/usage_collection/public';
|
||||
|
||||
|
@ -47,7 +46,6 @@ export interface PluginsSetup {
|
|||
dev_tools: DevToolsSetup;
|
||||
kibana_legacy: KibanaLegacySetup;
|
||||
share: SharePluginSetup;
|
||||
licensing: LicensingPluginSetup;
|
||||
usageCollection: UsageCollectionSetup;
|
||||
}
|
||||
|
||||
|
|
3
x-pack/legacy/common/poller.d.ts
vendored
3
x-pack/legacy/common/poller.d.ts
vendored
|
@ -8,4 +8,7 @@ export declare class Poller {
|
|||
constructor(options: any);
|
||||
|
||||
public start(): void;
|
||||
public stop(): void;
|
||||
public isRunning(): boolean;
|
||||
public getPollFrequency(): number;
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_regis
|
|||
import { npSetup, npStart } from 'ui/new_platform';
|
||||
import { Storage } from '../../../../../src/plugins/kibana_utils/public';
|
||||
import { start as navigation } from '../../../../../src/legacy/core_plugins/navigation/public/legacy';
|
||||
import { LicensingPluginSetup } from '../../../../plugins/licensing/public';
|
||||
import { GraphPlugin } from './plugin';
|
||||
|
||||
// @ts-ignore
|
||||
|
@ -39,13 +40,17 @@ async function getAngularInjectedDependencies(): Promise<LegacyAngularInjectedDe
|
|||
};
|
||||
}
|
||||
|
||||
type XpackNpSetupDeps = typeof npSetup.plugins & {
|
||||
licensing: LicensingPluginSetup;
|
||||
};
|
||||
|
||||
(async () => {
|
||||
const instance = new GraphPlugin();
|
||||
instance.setup(npSetup.core, {
|
||||
__LEGACY: {
|
||||
Storage,
|
||||
},
|
||||
...npSetup.plugins,
|
||||
...(npSetup.plugins as XpackNpSetupDeps),
|
||||
});
|
||||
instance.start(npStart.core, {
|
||||
npData: npStart.plugins.data,
|
||||
|
|
|
@ -9,7 +9,7 @@ import { CoreSetup, CoreStart, Plugin, SavedObjectsClientContract } from 'src/co
|
|||
import { Plugin as DataPlugin } from 'src/plugins/data/public';
|
||||
import { LegacyAngularInjectedDependencies } from './render_app';
|
||||
import { NavigationStart } from '../../../../../src/legacy/core_plugins/navigation/public';
|
||||
import { LicensingPluginSetup } from '../../../../plugins/licensing/common/types';
|
||||
import { LicensingPluginSetup } from '../../../../plugins/licensing/public';
|
||||
|
||||
export interface GraphPluginStartDependencies {
|
||||
npData: ReturnType<DataPlugin['start']>;
|
||||
|
|
|
@ -38,7 +38,7 @@ import {
|
|||
IndexPatternsContract,
|
||||
} from '../../../../../src/plugins/data/public';
|
||||
import { NavigationStart } from '../../../../../src/legacy/core_plugins/navigation/public';
|
||||
import { LicensingPluginSetup } from '../../../../plugins/licensing/common/types';
|
||||
import { LicensingPluginSetup } from '../../../../plugins/licensing/public';
|
||||
import { checkLicense } from '../../../../plugins/graph/common/check_license';
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,10 +6,7 @@
|
|||
|
||||
import { resolve } from 'path';
|
||||
import dedent from 'dedent';
|
||||
import {
|
||||
XPACK_DEFAULT_ADMIN_EMAIL_UI_SETTING,
|
||||
XPACK_INFO_API_DEFAULT_POLL_FREQUENCY_IN_MILLIS,
|
||||
} from '../../server/lib/constants';
|
||||
import { XPACK_DEFAULT_ADMIN_EMAIL_UI_SETTING } from '../../server/lib/constants';
|
||||
import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status';
|
||||
import { replaceInjectedVars } from './server/lib/replace_injected_vars';
|
||||
import { setupXPackMain } from './server/lib/setup_xpack_main';
|
||||
|
@ -36,9 +33,6 @@ export const xpackMain = kibana => {
|
|||
enabled: Joi.boolean().default(),
|
||||
url: Joi.string().default(),
|
||||
}).default(), // deprecated
|
||||
xpack_api_polling_frequency_millis: Joi.number().default(
|
||||
XPACK_INFO_API_DEFAULT_POLL_FREQUENCY_IN_MILLIS
|
||||
),
|
||||
}).default();
|
||||
},
|
||||
|
||||
|
@ -65,6 +59,7 @@ export const xpackMain = kibana => {
|
|||
value: null,
|
||||
},
|
||||
},
|
||||
hacks: ['plugins/xpack_main/hacks/check_xpack_info_change'],
|
||||
replaceInjectedVars,
|
||||
injectDefaultVars(server) {
|
||||
const config = server.config();
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { identity } from 'lodash';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { Path } from 'plugins/xpack_main/services/path';
|
||||
import { xpackInfo } from 'plugins/xpack_main/services/xpack_info';
|
||||
import { xpackInfoSignature } from 'plugins/xpack_main/services/xpack_info_signature';
|
||||
|
||||
const module = uiModules.get('xpack_main', []);
|
||||
|
||||
module.factory('checkXPackInfoChange', ($q, Private, $injector) => {
|
||||
/**
|
||||
* Intercept each network response to look for the kbn-xpack-sig header.
|
||||
* When that header is detected, compare its value with the value cached
|
||||
* in the browser storage. When the value is new, call `xpackInfo.refresh()`
|
||||
* so that it will pull down the latest x-pack info
|
||||
*
|
||||
* @param {object} response - the angular $http response object
|
||||
* @param {function} handleResponse - callback, expects to receive the response
|
||||
* @return
|
||||
*/
|
||||
function interceptor(response, handleResponse) {
|
||||
if (Path.isUnauthenticated()) {
|
||||
return handleResponse(response);
|
||||
}
|
||||
|
||||
const currentSignature = response.headers('kbn-xpack-sig');
|
||||
const cachedSignature = xpackInfoSignature.get();
|
||||
|
||||
if (currentSignature && cachedSignature !== currentSignature) {
|
||||
// Signature from the server differ from the signature of our
|
||||
// cached info, so we need to refresh it.
|
||||
// Intentionally swallowing this error
|
||||
// because nothing catches it and it's an ugly console error.
|
||||
xpackInfo.refresh($injector).catch(() => {});
|
||||
}
|
||||
|
||||
return handleResponse(response);
|
||||
}
|
||||
|
||||
return {
|
||||
response: response => interceptor(response, identity),
|
||||
responseError: response => interceptor(response, $q.reject),
|
||||
};
|
||||
});
|
||||
|
||||
module.config($httpProvider => {
|
||||
$httpProvider.interceptors.push('checkXPackInfoChange');
|
||||
});
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import sinon from 'sinon';
|
||||
import { XPackInfo } from '../xpack_info';
|
||||
import { setupXPackMain } from '../setup_xpack_main';
|
||||
|
@ -39,7 +40,9 @@ describe('setupXPackMain()', () => {
|
|||
elasticsearch: mockElasticsearchPlugin,
|
||||
xpack_main: mockXPackMainPlugin,
|
||||
},
|
||||
newPlatform: { setup: { plugins: { features: {} } } },
|
||||
newPlatform: {
|
||||
setup: { plugins: { features: {}, licensing: { license$: new BehaviorSubject() } } },
|
||||
},
|
||||
events: { on() {} },
|
||||
log() {},
|
||||
config() {},
|
||||
|
@ -47,11 +50,10 @@ describe('setupXPackMain()', () => {
|
|||
ext() {},
|
||||
});
|
||||
|
||||
// Make sure we don't misspell config key.
|
||||
// Make sure plugins doesn't consume config
|
||||
const configGetStub = sinon
|
||||
.stub()
|
||||
.throws(new Error('`config.get` is called with unexpected key.'));
|
||||
configGetStub.withArgs('xpack.xpack_main.xpack_api_polling_frequency_millis').returns(1234);
|
||||
mockServer.config.returns({ get: configGetStub });
|
||||
});
|
||||
|
||||
|
|
|
@ -5,36 +5,32 @@
|
|||
*/
|
||||
|
||||
import { createHash } from 'crypto';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import expect from '@kbn/expect';
|
||||
import sinon from 'sinon';
|
||||
import { XPackInfo } from '../xpack_info';
|
||||
import { licensingMock } from '../../../../../../plugins/licensing/server/licensing.mocks';
|
||||
|
||||
const nowDate = new Date(2010, 10, 10);
|
||||
|
||||
function getMockXPackInfoAPIResponse(license = {}, features = {}) {
|
||||
return Promise.resolve({
|
||||
build: {
|
||||
hash: '5927d85',
|
||||
date: '2010-10-10T00:00:00.000Z',
|
||||
},
|
||||
function createLicense(license = {}, features = {}) {
|
||||
return licensingMock.createLicense({
|
||||
license: {
|
||||
uid: 'custom-uid',
|
||||
type: 'gold',
|
||||
mode: 'gold',
|
||||
status: 'active',
|
||||
expiry_date_in_millis: 1286575200000,
|
||||
expiryDateInMillis: 1286575200000,
|
||||
...license,
|
||||
},
|
||||
features: {
|
||||
security: {
|
||||
description: 'Security for the Elastic Stack',
|
||||
available: true,
|
||||
enabled: true,
|
||||
isAvailable: true,
|
||||
isEnabled: true,
|
||||
},
|
||||
watcher: {
|
||||
description: 'Alerting, Notification and Automation for the Elastic Stack',
|
||||
available: true,
|
||||
enabled: false,
|
||||
isAvailable: true,
|
||||
isEnabled: false,
|
||||
},
|
||||
...features,
|
||||
},
|
||||
|
@ -48,251 +44,61 @@ function getSignature(object) {
|
|||
}
|
||||
|
||||
describe('XPackInfo', () => {
|
||||
const sandbox = sinon.createSandbox();
|
||||
|
||||
let mockServer;
|
||||
let mockElasticsearchCluster;
|
||||
let mockElasticsearchPlugin;
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox.useFakeTimers(nowDate.getTime());
|
||||
|
||||
mockElasticsearchCluster = {
|
||||
callWithInternalUser: sinon.stub(),
|
||||
};
|
||||
|
||||
mockElasticsearchPlugin = {
|
||||
getCluster: sinon.stub().returns(mockElasticsearchCluster),
|
||||
};
|
||||
|
||||
mockServer = sinon.stub({
|
||||
plugins: { elasticsearch: mockElasticsearchPlugin },
|
||||
events: { on() {} },
|
||||
log() {},
|
||||
newPlatform: {
|
||||
setup: {
|
||||
plugins: {
|
||||
licensing: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => sandbox.restore());
|
||||
|
||||
it('correctly initializes its own properties with defaults.', () => {
|
||||
mockElasticsearchPlugin.getCluster.throws(
|
||||
new Error('`getCluster` is called with unexpected source.')
|
||||
);
|
||||
mockElasticsearchPlugin.getCluster.withArgs('data').returns(mockElasticsearchCluster);
|
||||
|
||||
const xPackInfo = new XPackInfo(mockServer, { pollFrequencyInMillis: 1500 });
|
||||
|
||||
expect(xPackInfo.isAvailable()).to.be(false);
|
||||
expect(xPackInfo.license.isActive()).to.be(false);
|
||||
expect(xPackInfo.unavailableReason()).to.be(undefined);
|
||||
|
||||
// Poller is not started.
|
||||
sandbox.clock.tick(10000);
|
||||
sinon.assert.notCalled(mockElasticsearchCluster.callWithInternalUser);
|
||||
});
|
||||
|
||||
it('correctly initializes its own properties with custom cluster type.', () => {
|
||||
mockElasticsearchPlugin.getCluster.throws(
|
||||
new Error('`getCluster` is called with unexpected source.')
|
||||
);
|
||||
mockElasticsearchPlugin.getCluster.withArgs('monitoring').returns(mockElasticsearchCluster);
|
||||
|
||||
const xPackInfo = new XPackInfo(mockServer, {
|
||||
clusterSource: 'monitoring',
|
||||
pollFrequencyInMillis: 1234,
|
||||
});
|
||||
|
||||
expect(xPackInfo.isAvailable()).to.be(false);
|
||||
expect(xPackInfo.license.isActive()).to.be(false);
|
||||
expect(xPackInfo.unavailableReason()).to.be(undefined);
|
||||
|
||||
// Poller is not started.
|
||||
sandbox.clock.tick(9999);
|
||||
sinon.assert.notCalled(mockElasticsearchCluster.callWithInternalUser);
|
||||
});
|
||||
|
||||
describe('refreshNow()', () => {
|
||||
let xPackInfo;
|
||||
beforeEach(async () => {
|
||||
mockElasticsearchCluster.callWithInternalUser.returns(getMockXPackInfoAPIResponse());
|
||||
it('delegates to the new platform licensing plugin', async () => {
|
||||
const refresh = sinon.spy();
|
||||
|
||||
xPackInfo = new XPackInfo(mockServer, { pollFrequencyInMillis: 1500 });
|
||||
await xPackInfo.refreshNow();
|
||||
});
|
||||
|
||||
it('forces xpack info to be immediately updated with the data returned from Elasticsearch API.', async () => {
|
||||
sinon.assert.calledOnce(mockElasticsearchCluster.callWithInternalUser);
|
||||
sinon.assert.calledWithExactly(
|
||||
mockElasticsearchCluster.callWithInternalUser,
|
||||
'transport.request',
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/_xpack',
|
||||
}
|
||||
);
|
||||
|
||||
expect(xPackInfo.isAvailable()).to.be(true);
|
||||
expect(xPackInfo.license.isActive()).to.be(true);
|
||||
});
|
||||
|
||||
it('communicates X-Pack being unavailable', async () => {
|
||||
const badRequestError = new Error('Bad request');
|
||||
badRequestError.status = 400;
|
||||
|
||||
mockElasticsearchCluster.callWithInternalUser.returns(Promise.reject(badRequestError));
|
||||
await xPackInfo.refreshNow();
|
||||
|
||||
expect(xPackInfo.isAvailable()).to.be(false);
|
||||
expect(xPackInfo.isXpackUnavailable()).to.be(true);
|
||||
expect(xPackInfo.license.isActive()).to.be(false);
|
||||
expect(xPackInfo.unavailableReason()).to.be(
|
||||
'X-Pack plugin is not installed on the [data] Elasticsearch cluster.'
|
||||
);
|
||||
});
|
||||
|
||||
it('correctly updates xpack info if Elasticsearch API fails.', async () => {
|
||||
expect(xPackInfo.isAvailable()).to.be(true);
|
||||
expect(xPackInfo.license.isActive()).to.be(true);
|
||||
|
||||
mockElasticsearchCluster.callWithInternalUser.returns(Promise.reject(new Error('Uh oh')));
|
||||
await xPackInfo.refreshNow();
|
||||
|
||||
expect(xPackInfo.isAvailable()).to.be(false);
|
||||
expect(xPackInfo.license.isActive()).to.be(false);
|
||||
});
|
||||
|
||||
it('correctly updates xpack info when Elasticsearch API recovers after failure.', async () => {
|
||||
expect(xPackInfo.isAvailable()).to.be(true);
|
||||
expect(xPackInfo.license.isActive()).to.be(true);
|
||||
expect(xPackInfo.unavailableReason()).to.be(undefined);
|
||||
|
||||
const randomError = new Error('Uh oh');
|
||||
mockElasticsearchCluster.callWithInternalUser.returns(Promise.reject(randomError));
|
||||
await xPackInfo.refreshNow();
|
||||
|
||||
expect(xPackInfo.isAvailable()).to.be(false);
|
||||
expect(xPackInfo.license.isActive()).to.be(false);
|
||||
expect(xPackInfo.unavailableReason()).to.be(randomError);
|
||||
sinon.assert.calledWithExactly(
|
||||
mockServer.log,
|
||||
['license', 'warning', 'xpack'],
|
||||
`License information from the X-Pack plugin could not be obtained from Elasticsearch` +
|
||||
` for the [data] cluster. ${randomError}`
|
||||
);
|
||||
|
||||
const badRequestError = new Error('Bad request');
|
||||
badRequestError.status = 400;
|
||||
mockElasticsearchCluster.callWithInternalUser.returns(Promise.reject(badRequestError));
|
||||
await xPackInfo.refreshNow();
|
||||
|
||||
expect(xPackInfo.isAvailable()).to.be(false);
|
||||
expect(xPackInfo.license.isActive()).to.be(false);
|
||||
expect(xPackInfo.unavailableReason()).to.be(
|
||||
'X-Pack plugin is not installed on the [data] Elasticsearch cluster.'
|
||||
);
|
||||
sinon.assert.calledWithExactly(
|
||||
mockServer.log,
|
||||
['license', 'warning', 'xpack'],
|
||||
`License information from the X-Pack plugin could not be obtained from Elasticsearch` +
|
||||
` for the [data] cluster. ${badRequestError}`
|
||||
);
|
||||
|
||||
mockElasticsearchCluster.callWithInternalUser.returns(getMockXPackInfoAPIResponse());
|
||||
await xPackInfo.refreshNow();
|
||||
|
||||
expect(xPackInfo.isAvailable()).to.be(true);
|
||||
expect(xPackInfo.license.isActive()).to.be(true);
|
||||
});
|
||||
|
||||
it('logs license status changes.', async () => {
|
||||
sinon.assert.calledWithExactly(
|
||||
mockServer.log,
|
||||
['license', 'info', 'xpack'],
|
||||
sinon.match(
|
||||
'Imported license information from Elasticsearch for the [data] cluster: ' +
|
||||
'mode: gold | status: active | expiry date: '
|
||||
)
|
||||
);
|
||||
mockServer.log.resetHistory();
|
||||
const xPackInfo = new XPackInfo(mockServer, {
|
||||
licensing: {
|
||||
license$: new BehaviorSubject(createLicense()),
|
||||
refresh: refresh,
|
||||
},
|
||||
});
|
||||
|
||||
await xPackInfo.refreshNow();
|
||||
|
||||
// Response is still the same, so nothing should be logged.
|
||||
sinon.assert.neverCalledWith(mockServer.log, ['license', 'info', 'xpack']);
|
||||
|
||||
// Change mode/status of the license and the change should be logged.
|
||||
mockElasticsearchCluster.callWithInternalUser.returns(
|
||||
getMockXPackInfoAPIResponse({ status: 'expired', mode: 'platinum' })
|
||||
);
|
||||
|
||||
await xPackInfo.refreshNow();
|
||||
|
||||
sinon.assert.calledWithExactly(
|
||||
mockServer.log,
|
||||
['license', 'info', 'xpack'],
|
||||
sinon.match(
|
||||
'Imported changed license information from Elasticsearch for the [data] cluster: ' +
|
||||
'mode: platinum | status: expired | expiry date: '
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('restarts the poller.', async () => {
|
||||
mockElasticsearchCluster.callWithInternalUser.resetHistory();
|
||||
|
||||
sandbox.clock.tick(1499);
|
||||
sinon.assert.notCalled(mockElasticsearchCluster.callWithInternalUser);
|
||||
|
||||
sandbox.clock.tick(1);
|
||||
sinon.assert.calledOnce(mockElasticsearchCluster.callWithInternalUser);
|
||||
// Exhaust micro-task queue, to make sure that `callWithInternalUser` is completed and
|
||||
// new poller iteration is rescheduled.
|
||||
await Promise.resolve();
|
||||
|
||||
sandbox.clock.tick(1500);
|
||||
sinon.assert.calledTwice(mockElasticsearchCluster.callWithInternalUser);
|
||||
// Exhaust micro-task queue, to make sure that `callWithInternalUser` is completed and
|
||||
// new poller iteration is rescheduled.
|
||||
await Promise.resolve();
|
||||
|
||||
sandbox.clock.tick(1499);
|
||||
await xPackInfo.refreshNow();
|
||||
mockElasticsearchCluster.callWithInternalUser.resetHistory();
|
||||
|
||||
// Since poller has been restarted, it should not be called now.
|
||||
sandbox.clock.tick(1);
|
||||
sinon.assert.notCalled(mockElasticsearchCluster.callWithInternalUser);
|
||||
|
||||
// Here it still shouldn't be called.
|
||||
sandbox.clock.tick(1498);
|
||||
sinon.assert.notCalled(mockElasticsearchCluster.callWithInternalUser);
|
||||
|
||||
sandbox.clock.tick(1);
|
||||
sinon.assert.calledOnce(mockElasticsearchCluster.callWithInternalUser);
|
||||
sinon.assert.calledOnce(refresh);
|
||||
});
|
||||
});
|
||||
|
||||
describe('license', () => {
|
||||
let xPackInfo;
|
||||
let license$;
|
||||
beforeEach(async () => {
|
||||
mockElasticsearchCluster.callWithInternalUser.returns(getMockXPackInfoAPIResponse());
|
||||
|
||||
xPackInfo = new XPackInfo(mockServer, { pollFrequencyInMillis: 1500 });
|
||||
await xPackInfo.refreshNow();
|
||||
license$ = new BehaviorSubject(createLicense());
|
||||
xPackInfo = new XPackInfo(mockServer, {
|
||||
licensing: {
|
||||
license$,
|
||||
refresh: () => null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('getUid() shows license uid returned from the backend.', async () => {
|
||||
it('getUid() shows license uid returned from the license$.', async () => {
|
||||
expect(xPackInfo.license.getUid()).to.be('custom-uid');
|
||||
|
||||
mockElasticsearchCluster.callWithInternalUser.returns(
|
||||
getMockXPackInfoAPIResponse({ uid: 'new-custom-uid' })
|
||||
);
|
||||
await xPackInfo.refreshNow();
|
||||
license$.next(createLicense({ uid: 'new-custom-uid' }));
|
||||
|
||||
expect(xPackInfo.license.getUid()).to.be('new-custom-uid');
|
||||
|
||||
mockElasticsearchCluster.callWithInternalUser.returns(Promise.reject(new Error('Uh oh')));
|
||||
await xPackInfo.refreshNow();
|
||||
license$.next(createLicense({ uid: undefined, error: 'error-reason' }));
|
||||
|
||||
expect(xPackInfo.license.getUid()).to.be(undefined);
|
||||
});
|
||||
|
@ -300,80 +106,46 @@ describe('XPackInfo', () => {
|
|||
it('isActive() is based on the status returned from the backend.', async () => {
|
||||
expect(xPackInfo.license.isActive()).to.be(true);
|
||||
|
||||
mockElasticsearchCluster.callWithInternalUser.returns(
|
||||
getMockXPackInfoAPIResponse({ status: 'expired' })
|
||||
);
|
||||
await xPackInfo.refreshNow();
|
||||
|
||||
license$.next(createLicense({ status: 'expired' }));
|
||||
expect(xPackInfo.license.isActive()).to.be(false);
|
||||
|
||||
mockElasticsearchCluster.callWithInternalUser.returns(
|
||||
getMockXPackInfoAPIResponse({ status: 'some other value' })
|
||||
);
|
||||
await xPackInfo.refreshNow();
|
||||
|
||||
license$.next(createLicense({ status: 'some other value' }));
|
||||
expect(xPackInfo.license.isActive()).to.be(false);
|
||||
|
||||
mockElasticsearchCluster.callWithInternalUser.returns(
|
||||
getMockXPackInfoAPIResponse({ status: 'active' })
|
||||
);
|
||||
await xPackInfo.refreshNow();
|
||||
|
||||
license$.next(createLicense({ status: 'active' }));
|
||||
expect(xPackInfo.license.isActive()).to.be(true);
|
||||
|
||||
mockElasticsearchCluster.callWithInternalUser.returns(Promise.reject(new Error('Uh oh')));
|
||||
await xPackInfo.refreshNow();
|
||||
|
||||
license$.next(createLicense({ status: undefined, error: 'error-reason' }));
|
||||
expect(xPackInfo.license.isActive()).to.be(false);
|
||||
});
|
||||
|
||||
it('getExpiryDateInMillis() is based on the value returned from the backend.', async () => {
|
||||
expect(xPackInfo.license.getExpiryDateInMillis()).to.be(1286575200000);
|
||||
|
||||
mockElasticsearchCluster.callWithInternalUser.returns(
|
||||
getMockXPackInfoAPIResponse({ expiry_date_in_millis: 10203040 })
|
||||
);
|
||||
await xPackInfo.refreshNow();
|
||||
|
||||
license$.next(createLicense({ expiryDateInMillis: 10203040 }));
|
||||
expect(xPackInfo.license.getExpiryDateInMillis()).to.be(10203040);
|
||||
|
||||
mockElasticsearchCluster.callWithInternalUser.returns(Promise.reject(new Error('Uh oh')));
|
||||
await xPackInfo.refreshNow();
|
||||
|
||||
license$.next(createLicense({ expiryDateInMillis: undefined, error: 'error-reason' }));
|
||||
expect(xPackInfo.license.getExpiryDateInMillis()).to.be(undefined);
|
||||
});
|
||||
|
||||
it('getType() is based on the value returned from the backend.', async () => {
|
||||
expect(xPackInfo.license.getType()).to.be('gold');
|
||||
|
||||
mockElasticsearchCluster.callWithInternalUser.returns(
|
||||
getMockXPackInfoAPIResponse({ type: 'basic' })
|
||||
);
|
||||
await xPackInfo.refreshNow();
|
||||
|
||||
license$.next(createLicense({ type: 'basic' }));
|
||||
expect(xPackInfo.license.getType()).to.be('basic');
|
||||
|
||||
mockElasticsearchCluster.callWithInternalUser.returns(Promise.reject(new Error('Uh oh')));
|
||||
await xPackInfo.refreshNow();
|
||||
|
||||
license$.next(createLicense({ type: undefined, error: 'error-reason' }));
|
||||
expect(xPackInfo.license.getType()).to.be(undefined);
|
||||
});
|
||||
|
||||
it('isOneOf() correctly determines if current license is presented in the specified list.', async () => {
|
||||
mockElasticsearchCluster.callWithInternalUser.returns(
|
||||
getMockXPackInfoAPIResponse({ mode: 'gold' })
|
||||
);
|
||||
await xPackInfo.refreshNow();
|
||||
|
||||
expect(xPackInfo.license.isOneOf('gold')).to.be(true);
|
||||
expect(xPackInfo.license.isOneOf(['gold', 'basic'])).to.be(true);
|
||||
expect(xPackInfo.license.isOneOf(['platinum', 'basic'])).to.be(false);
|
||||
expect(xPackInfo.license.isOneOf('standard')).to.be(false);
|
||||
|
||||
mockElasticsearchCluster.callWithInternalUser.returns(
|
||||
getMockXPackInfoAPIResponse({ mode: 'basic' })
|
||||
);
|
||||
await xPackInfo.refreshNow();
|
||||
license$.next(createLicense({ mode: 'basic' }));
|
||||
|
||||
expect(xPackInfo.license.isOneOf('basic')).to.be(true);
|
||||
expect(xPackInfo.license.isOneOf(['gold', 'basic'])).to.be(true);
|
||||
|
@ -384,21 +156,25 @@ describe('XPackInfo', () => {
|
|||
|
||||
describe('feature', () => {
|
||||
let xPackInfo;
|
||||
let license$;
|
||||
beforeEach(async () => {
|
||||
mockElasticsearchCluster.callWithInternalUser.returns(
|
||||
getMockXPackInfoAPIResponse(
|
||||
license$ = new BehaviorSubject(
|
||||
createLicense(
|
||||
{},
|
||||
{
|
||||
feature: {
|
||||
available: false,
|
||||
enabled: true,
|
||||
isAvailable: false,
|
||||
isEnabled: true,
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
xPackInfo = new XPackInfo(mockServer, { pollFrequencyInMillis: 1500 });
|
||||
await xPackInfo.refreshNow();
|
||||
xPackInfo = new XPackInfo(mockServer, {
|
||||
licensing: {
|
||||
license$,
|
||||
refresh: () => null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('isAvailable() checks whether particular feature is available.', async () => {
|
||||
|
@ -466,10 +242,7 @@ describe('XPackInfo', () => {
|
|||
someAnotherCustomValue: 500100,
|
||||
});
|
||||
|
||||
mockElasticsearchCluster.callWithInternalUser.returns(
|
||||
getMockXPackInfoAPIResponse({ type: 'platinum' })
|
||||
);
|
||||
await xPackInfo.refreshNow();
|
||||
license$.next(createLicense({ type: 'platinum' }));
|
||||
|
||||
expect(xPackInfo.toJSON().features.security).to.eql({
|
||||
isXPackInfo: true,
|
||||
|
@ -524,10 +297,7 @@ describe('XPackInfo', () => {
|
|||
someAnotherCustomValue: 500100,
|
||||
});
|
||||
|
||||
mockElasticsearchCluster.callWithInternalUser.returns(
|
||||
getMockXPackInfoAPIResponse({ type: 'platinum' })
|
||||
);
|
||||
await xPackInfo.refreshNow();
|
||||
license$.next(createLicense({ type: 'platinum' }));
|
||||
|
||||
expect(securityFeature.getLicenseCheckResults()).to.eql({
|
||||
isXPackInfo: true,
|
||||
|
@ -543,9 +313,13 @@ describe('XPackInfo', () => {
|
|||
});
|
||||
|
||||
it('getSignature() returns correct signature.', async () => {
|
||||
mockElasticsearchCluster.callWithInternalUser.returns(getMockXPackInfoAPIResponse());
|
||||
const xPackInfo = new XPackInfo(mockServer, { pollFrequencyInMillis: 1500 });
|
||||
await xPackInfo.refreshNow();
|
||||
const license$ = new BehaviorSubject(createLicense());
|
||||
const xPackInfo = new XPackInfo(mockServer, {
|
||||
licensing: {
|
||||
license$,
|
||||
refresh: () => null,
|
||||
},
|
||||
});
|
||||
|
||||
expect(xPackInfo.getSignature()).to.be(
|
||||
getSignature({
|
||||
|
@ -558,24 +332,21 @@ describe('XPackInfo', () => {
|
|||
})
|
||||
);
|
||||
|
||||
mockElasticsearchCluster.callWithInternalUser.returns(
|
||||
getMockXPackInfoAPIResponse({ type: 'platinum', expiry_date_in_millis: nowDate.getTime() })
|
||||
);
|
||||
|
||||
await xPackInfo.refreshNow();
|
||||
license$.next(createLicense({ type: 'platinum', expiryDateInMillis: 20304050 }));
|
||||
|
||||
const expectedSignature = getSignature({
|
||||
license: {
|
||||
type: 'platinum',
|
||||
isActive: true,
|
||||
expiryDateInMillis: nowDate.getTime(),
|
||||
expiryDateInMillis: 20304050,
|
||||
},
|
||||
features: {},
|
||||
});
|
||||
expect(xPackInfo.getSignature()).to.be(expectedSignature);
|
||||
|
||||
// Should stay the same after refresh if nothing changed.
|
||||
await xPackInfo.refreshNow();
|
||||
license$.next(createLicense({ type: 'platinum', expiryDateInMillis: 20304050 }));
|
||||
|
||||
expect(xPackInfo.getSignature()).to.be(expectedSignature);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,14 +16,19 @@ import { XPackInfo } from './xpack_info';
|
|||
* @param server {Object} The Kibana server object.
|
||||
*/
|
||||
export function setupXPackMain(server) {
|
||||
const info = new XPackInfo(server, {
|
||||
pollFrequencyInMillis: server
|
||||
.config()
|
||||
.get('xpack.xpack_main.xpack_api_polling_frequency_millis'),
|
||||
});
|
||||
const info = new XPackInfo(server, { licensing: server.newPlatform.setup.plugins.licensing });
|
||||
|
||||
server.expose('info', info);
|
||||
server.expose('createXPackInfo', options => new XPackInfo(server, options));
|
||||
server.expose('createXPackInfo', options => {
|
||||
const client = server.newPlatform.setup.core.elasticsearch.createClient(options.clusterSource);
|
||||
const monitoringLicensing = server.newPlatform.setup.plugins.licensing.createLicensePoller(
|
||||
client,
|
||||
options.pollFrequencyInMillis
|
||||
);
|
||||
|
||||
return new XPackInfo(server, { licensing: monitoringLicensing });
|
||||
});
|
||||
|
||||
server.ext('onPreResponse', (request, h) => injectXPackInfoSignature(info, request, h));
|
||||
|
||||
const { registerFeature, getFeatures } = server.newPlatform.setup.plugins.features;
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Server } from 'hapi';
|
||||
import { XPackInfoLicense } from './xpack_info_license';
|
||||
|
||||
interface XPackFeature {
|
||||
isAvailable(): boolean;
|
||||
isEnabled(): boolean;
|
||||
registerLicenseCheckResultsGenerator(generator: (xpackInfo: XPackInfo) => void): void;
|
||||
getLicenseCheckResults(): any;
|
||||
}
|
||||
|
||||
export interface XPackInfoOptions {
|
||||
clusterSource?: string;
|
||||
pollFrequencyInMillis: number;
|
||||
}
|
||||
|
||||
export declare class XPackInfo {
|
||||
public license: XPackInfoLicense;
|
||||
|
||||
constructor(server: Server, options: XPackInfoOptions);
|
||||
|
||||
public isAvailable(): boolean;
|
||||
public isXpackUnavailable(): boolean;
|
||||
public unavailableReason(): string | Error;
|
||||
public onLicenseInfoChange(handler: () => void): void;
|
||||
public refreshNow(): Promise<this>;
|
||||
|
||||
public feature(name: string): XPackFeature;
|
||||
|
||||
public getSignature(): string;
|
||||
public toJSON(): any;
|
||||
}
|
|
@ -1,308 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { createHash } from 'crypto';
|
||||
import moment from 'moment';
|
||||
import { get, has } from 'lodash';
|
||||
import { Poller } from '../../../../common/poller';
|
||||
import { XPackInfoLicense } from './xpack_info_license';
|
||||
|
||||
/**
|
||||
* A helper that provides a convenient way to access XPack Info returned by Elasticsearch.
|
||||
*/
|
||||
export class XPackInfo {
|
||||
/**
|
||||
* XPack License object.
|
||||
* @type {XPackInfoLicense}
|
||||
* @private
|
||||
*/
|
||||
_license;
|
||||
|
||||
/**
|
||||
* Feature name <-> feature license check generator function mapping.
|
||||
* @type {Map<string, Function>}
|
||||
* @private
|
||||
*/
|
||||
_featureLicenseCheckResultsGenerators = new Map();
|
||||
|
||||
/**
|
||||
* Set of listener functions that will be called whenever the license
|
||||
* info changes
|
||||
* @type {Set<Function>}
|
||||
*/
|
||||
_licenseInfoChangedListeners = new Set();
|
||||
|
||||
/**
|
||||
* Cache that may contain last xpack info API response or error, json representation
|
||||
* of xpack info and xpack info signature.
|
||||
* @type {{response: Object|undefined, error: Object|undefined, json: Object|undefined, signature: string|undefined}}
|
||||
* @private
|
||||
*/
|
||||
_cache = {};
|
||||
|
||||
/**
|
||||
* XPack info poller.
|
||||
* @type {Poller}
|
||||
* @private
|
||||
*/
|
||||
_poller;
|
||||
|
||||
/**
|
||||
* XPack License instance.
|
||||
* @returns {XPackInfoLicense}
|
||||
*/
|
||||
get license() {
|
||||
return this._license;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs XPack info object.
|
||||
* @param {Hapi.Server} server HapiJS server instance.
|
||||
* @param {Object} options
|
||||
* @property {string} [options.clusterSource] Type of the cluster that should be used
|
||||
* to fetch XPack info (data, monitoring etc.). If not provided, `data` is used.
|
||||
* @property {number} options.pollFrequencyInMillis Polling interval used to automatically
|
||||
* refresh XPack Info by the internal poller.
|
||||
*/
|
||||
constructor(server, { clusterSource = 'data', pollFrequencyInMillis }) {
|
||||
this._log = server.log.bind(server);
|
||||
this._cluster = server.plugins.elasticsearch.getCluster(clusterSource);
|
||||
this._clusterSource = clusterSource;
|
||||
|
||||
// Create a poller that will be (re)started inside of the `refreshNow` call.
|
||||
this._poller = new Poller({
|
||||
functionToPoll: () => this.refreshNow(),
|
||||
trailing: true,
|
||||
pollFrequencyInMillis,
|
||||
continuePollingOnError: true,
|
||||
});
|
||||
|
||||
server.events.on('stop', () => {
|
||||
this._poller.stop();
|
||||
});
|
||||
|
||||
this._license = new XPackInfoLicense(
|
||||
() => this._cache.response && this._cache.response.license
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether XPack info is available.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isAvailable() {
|
||||
return !!this._cache.response && !!this._cache.response.license;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether ES was available
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isXpackUnavailable() {
|
||||
return this._cache.error instanceof Error && this._cache.error.status === 400;
|
||||
}
|
||||
|
||||
/**
|
||||
* If present, describes the reason why XPack info is not available.
|
||||
* @returns {Error|string}
|
||||
*/
|
||||
unavailableReason() {
|
||||
if (!this._cache.error && this._cache.response && !this._cache.response.license) {
|
||||
return `[${this._clusterSource}] Elasticsearch cluster did not respond with license information.`;
|
||||
}
|
||||
|
||||
if (this.isXpackUnavailable()) {
|
||||
return `X-Pack plugin is not installed on the [${this._clusterSource}] Elasticsearch cluster.`;
|
||||
}
|
||||
|
||||
return this._cache.error;
|
||||
}
|
||||
|
||||
onLicenseInfoChange(handler) {
|
||||
this._licenseInfoChangedListeners.add(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries server to get the updated XPack info.
|
||||
* @returns {Promise.<XPackInfo>}
|
||||
*/
|
||||
async refreshNow() {
|
||||
this._log(
|
||||
['license', 'debug', 'xpack'],
|
||||
`Calling [${
|
||||
this._clusterSource
|
||||
}] Elasticsearch _xpack API. Polling frequency: ${this._poller.getPollFrequency()}`
|
||||
);
|
||||
|
||||
// We can reset polling timer since we force refresh here.
|
||||
this._poller.stop();
|
||||
|
||||
try {
|
||||
const response = await this._cluster.callWithInternalUser('transport.request', {
|
||||
method: 'GET',
|
||||
path: '/_xpack',
|
||||
});
|
||||
|
||||
const licenseInfoChanged = this._hasLicenseInfoChanged(response);
|
||||
|
||||
if (licenseInfoChanged) {
|
||||
const licenseInfoParts = [
|
||||
`mode: ${get(response, 'license.mode')}`,
|
||||
`status: ${get(response, 'license.status')}`,
|
||||
];
|
||||
|
||||
if (has(response, 'license.expiry_date_in_millis')) {
|
||||
const expiryDate = moment(response.license.expiry_date_in_millis, 'x').format();
|
||||
licenseInfoParts.push(`expiry date: ${expiryDate}`);
|
||||
}
|
||||
|
||||
const licenseInfo = licenseInfoParts.join(' | ');
|
||||
|
||||
this._log(
|
||||
['license', 'info', 'xpack'],
|
||||
`Imported ${this._cache.response ? 'changed ' : ''}license information` +
|
||||
` from Elasticsearch for the [${this._clusterSource}] cluster: ${licenseInfo}`
|
||||
);
|
||||
}
|
||||
|
||||
this._cache = { response };
|
||||
|
||||
if (licenseInfoChanged) {
|
||||
// call license info changed listeners
|
||||
for (const listener of this._licenseInfoChangedListeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this._log(
|
||||
['license', 'warning', 'xpack'],
|
||||
`License information from the X-Pack plugin could not be obtained from Elasticsearch` +
|
||||
` for the [${this._clusterSource}] cluster. ${error}`
|
||||
);
|
||||
|
||||
this._cache = { error };
|
||||
}
|
||||
|
||||
this._poller.start();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a wrapper around XPack info that gives an access to the properties of
|
||||
* the specific feature.
|
||||
* @param {string} name Name of the feature to get a wrapper for.
|
||||
* @returns {Object}
|
||||
*/
|
||||
feature(name) {
|
||||
return {
|
||||
/**
|
||||
* Checks whether feature is available (permitted by the current license).
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isAvailable: () => {
|
||||
return !!get(this._cache.response, `features.${name}.available`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks whether feature is enabled (not disabled by the configuration specifically).
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isEnabled: () => {
|
||||
return !!get(this._cache.response, `features.${name}.enabled`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Registers a `generator` function that will be called with XPackInfo instance as
|
||||
* argument whenever XPack info changes. Whatever `generator` returns will be stored
|
||||
* in XPackInfo JSON representation and can be accessed with `getLicenseCheckResults`.
|
||||
* @param {Function} generator Function to call whenever XPackInfo changes.
|
||||
*/
|
||||
registerLicenseCheckResultsGenerator: generator => {
|
||||
this._featureLicenseCheckResultsGenerators.set(name, generator);
|
||||
|
||||
// Since JSON representation and signature are cached we should invalidate them to
|
||||
// include results from newly registered generator when they are requested.
|
||||
this._cache.json = undefined;
|
||||
this._cache.signature = undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns license check results that were previously produced by the `generator` function.
|
||||
* @returns {Object}
|
||||
*/
|
||||
getLicenseCheckResults: () => this.toJSON().features[name],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts string md5 hash from the stringified version of license JSON representation.
|
||||
* @returns {string}
|
||||
*/
|
||||
getSignature() {
|
||||
if (this._cache.signature) {
|
||||
return this._cache.signature;
|
||||
}
|
||||
|
||||
this._cache.signature = createHash('md5')
|
||||
.update(JSON.stringify(this.toJSON()))
|
||||
.digest('hex');
|
||||
|
||||
return this._cache.signature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns JSON representation of the license object that is suitable for serialization.
|
||||
* @returns {Object}
|
||||
*/
|
||||
toJSON() {
|
||||
if (this._cache.json) {
|
||||
return this._cache.json;
|
||||
}
|
||||
|
||||
this._cache.json = {
|
||||
license: {
|
||||
type: this.license.getType(),
|
||||
isActive: this.license.isActive(),
|
||||
expiryDateInMillis: this.license.getExpiryDateInMillis(),
|
||||
},
|
||||
features: {},
|
||||
};
|
||||
|
||||
// Set response elements specific to each feature. To do this,
|
||||
// call the license check results generator for each feature, passing them
|
||||
// the xpack info object
|
||||
for (const [feature, licenseChecker] of this._featureLicenseCheckResultsGenerators) {
|
||||
// return value expected to be a dictionary object.
|
||||
this._cache.json.features[feature] = licenseChecker(this);
|
||||
}
|
||||
|
||||
return this._cache.json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether license within specified response differs from the current license.
|
||||
* Comparison is based on license mode, status and expiration date.
|
||||
* @param {Object} response xPack info response object returned from the backend.
|
||||
* @returns {boolean} True if license within specified response object differs from
|
||||
* the one we already have.
|
||||
* @private
|
||||
*/
|
||||
_hasLicenseInfoChanged(response) {
|
||||
const newLicense = get(response, 'license') || {};
|
||||
const cachedLicense = get(this._cache.response, 'license') || {};
|
||||
|
||||
if (newLicense.mode !== cachedLicense.mode) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (newLicense.status !== cachedLicense.status) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return newLicense.expiry_date_in_millis !== cachedLicense.expiry_date_in_millis;
|
||||
}
|
||||
}
|
240
x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.ts
Normal file
240
x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.ts
Normal file
|
@ -0,0 +1,240 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { createHash } from 'crypto';
|
||||
import { Legacy } from 'kibana';
|
||||
|
||||
import { XPackInfoLicense } from './xpack_info_license';
|
||||
|
||||
import { LicensingPluginSetup, ILicense } from '../../../../../plugins/licensing/server';
|
||||
|
||||
export interface XPackInfoOptions {
|
||||
clusterSource?: string;
|
||||
pollFrequencyInMillis: number;
|
||||
}
|
||||
|
||||
type LicenseGeneratorCheck = (xpackInfo: XPackInfo) => any;
|
||||
|
||||
export interface XPackFeature {
|
||||
isAvailable(): boolean;
|
||||
isEnabled(): boolean;
|
||||
registerLicenseCheckResultsGenerator(generator: LicenseGeneratorCheck): void;
|
||||
getLicenseCheckResults(): any;
|
||||
}
|
||||
|
||||
interface Deps {
|
||||
licensing: LicensingPluginSetup;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper that provides a convenient way to access XPack Info returned by Elasticsearch.
|
||||
*/
|
||||
export class XPackInfo {
|
||||
/**
|
||||
* XPack License object.
|
||||
* @type {XPackInfoLicense}
|
||||
* @private
|
||||
*/
|
||||
_license: XPackInfoLicense;
|
||||
|
||||
/**
|
||||
* Feature name <-> feature license check generator function mapping.
|
||||
* @type {Map<string, Function>}
|
||||
* @private
|
||||
*/
|
||||
_featureLicenseCheckResultsGenerators = new Map<string, LicenseGeneratorCheck>();
|
||||
|
||||
/**
|
||||
* Set of listener functions that will be called whenever the license
|
||||
* info changes
|
||||
* @type {Set<Function>}
|
||||
*/
|
||||
_licenseInfoChangedListeners = new Set<() => void>();
|
||||
|
||||
/**
|
||||
* Cache that may contain last xpack info API response or error, json representation
|
||||
* of xpack info and xpack info signature.
|
||||
* @type {{response: Object|undefined, error: Object|undefined, json: Object|undefined, signature: string|undefined}}
|
||||
* @private
|
||||
*/
|
||||
private _cache: {
|
||||
license?: ILicense;
|
||||
error?: string;
|
||||
json?: Record<string, any>;
|
||||
signature?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* XPack License instance.
|
||||
* @returns {XPackInfoLicense}
|
||||
*/
|
||||
public get license() {
|
||||
return this._license;
|
||||
}
|
||||
|
||||
private readonly licensingPlugin: LicensingPluginSetup;
|
||||
|
||||
/**
|
||||
* Constructs XPack info object.
|
||||
* @param {Hapi.Server} server HapiJS server instance.
|
||||
*/
|
||||
constructor(server: Legacy.Server, deps: Deps) {
|
||||
if (!deps.licensing) {
|
||||
throw new Error('XPackInfo requires enabled Licensing plugin');
|
||||
}
|
||||
this.licensingPlugin = deps.licensing;
|
||||
|
||||
this._cache = {};
|
||||
|
||||
this.licensingPlugin.license$.subscribe((license: ILicense) => {
|
||||
if (license.isActive) {
|
||||
this._cache = {
|
||||
license,
|
||||
error: undefined,
|
||||
};
|
||||
} else {
|
||||
this._cache = {
|
||||
license,
|
||||
error: license.error,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
this._license = new XPackInfoLicense(() => this._cache.license);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether XPack info is available.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isAvailable() {
|
||||
return Boolean(this._cache.license?.isAvailable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether ES was available
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isXpackUnavailable() {
|
||||
return (
|
||||
this._cache.error &&
|
||||
this._cache.error === 'X-Pack plugin is not installed on the Elasticsearch cluster.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* If present, describes the reason why XPack info is not available.
|
||||
* @returns {Error|string}
|
||||
*/
|
||||
unavailableReason() {
|
||||
return this._cache.license?.getUnavailableReason();
|
||||
}
|
||||
|
||||
onLicenseInfoChange(handler: () => void) {
|
||||
this._licenseInfoChangedListeners.add(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries server to get the updated XPack info.
|
||||
* @returns {Promise.<XPackInfo>}
|
||||
*/
|
||||
async refreshNow() {
|
||||
await this.licensingPlugin.refresh();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a wrapper around XPack info that gives an access to the properties of
|
||||
* the specific feature.
|
||||
* @param {string} name Name of the feature to get a wrapper for.
|
||||
* @returns {Object}
|
||||
*/
|
||||
feature(name: string): XPackFeature {
|
||||
return {
|
||||
/**
|
||||
* Checks whether feature is available (permitted by the current license).
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isAvailable: () => {
|
||||
return Boolean(this._cache.license?.getFeature(name).isAvailable);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks whether feature is enabled (not disabled by the configuration specifically).
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isEnabled: () => {
|
||||
return Boolean(this._cache.license?.getFeature(name).isEnabled);
|
||||
},
|
||||
|
||||
/**
|
||||
* Registers a `generator` function that will be called with XPackInfo instance as
|
||||
* argument whenever XPack info changes. Whatever `generator` returns will be stored
|
||||
* in XPackInfo JSON representation and can be accessed with `getLicenseCheckResults`.
|
||||
* @param {Function} generator Function to call whenever XPackInfo changes.
|
||||
*/
|
||||
registerLicenseCheckResultsGenerator: (generator: LicenseGeneratorCheck) => {
|
||||
this._featureLicenseCheckResultsGenerators.set(name, generator);
|
||||
|
||||
// Since JSON representation and signature are cached we should invalidate them to
|
||||
// include results from newly registered generator when they are requested.
|
||||
this._cache.json = undefined;
|
||||
this._cache.signature = undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns license check results that were previously produced by the `generator` function.
|
||||
* @returns {Object}
|
||||
*/
|
||||
getLicenseCheckResults: () => this.toJSON().features[name],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts string md5 hash from the stringified version of license JSON representation.
|
||||
* @returns {string}
|
||||
*/
|
||||
getSignature() {
|
||||
if (this._cache.signature) {
|
||||
return this._cache.signature;
|
||||
}
|
||||
|
||||
this._cache.signature = createHash('md5')
|
||||
.update(JSON.stringify(this.toJSON()))
|
||||
.digest('hex');
|
||||
|
||||
return this._cache.signature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns JSON representation of the license object that is suitable for serialization.
|
||||
* @returns {Object}
|
||||
*/
|
||||
toJSON() {
|
||||
if (this._cache.json) {
|
||||
return this._cache.json;
|
||||
}
|
||||
|
||||
this._cache.json = {
|
||||
license: {
|
||||
type: this.license.getType(),
|
||||
isActive: this.license.isActive(),
|
||||
expiryDateInMillis: this.license.getExpiryDateInMillis(),
|
||||
},
|
||||
features: {},
|
||||
};
|
||||
|
||||
// Set response elements specific to each feature. To do this,
|
||||
// call the license check results generator for each feature, passing them
|
||||
// the xpack info object
|
||||
for (const [feature, licenseChecker] of this._featureLicenseCheckResultsGenerators) {
|
||||
// return value expected to be a dictionary object.
|
||||
this._cache.json.features[feature] = licenseChecker(this);
|
||||
}
|
||||
|
||||
return this._cache.json;
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
type LicenseType = 'oss' | 'basic' | 'trial' | 'standard' | 'basic' | 'gold' | 'platinum';
|
||||
|
||||
export declare class XPackInfoLicense {
|
||||
constructor(getRawLicense: () => any);
|
||||
|
||||
public getUid(): string | undefined;
|
||||
public isActive(): boolean;
|
||||
public getExpiryDateInMillis(): number | undefined;
|
||||
public isOneOf(candidateLicenses: string[]): boolean;
|
||||
public getType(): LicenseType | undefined;
|
||||
public getMode(): string | undefined;
|
||||
public isActiveLicense(typeChecker: (mode: string) => boolean): boolean;
|
||||
public isBasic(): boolean;
|
||||
public isNotBasic(): boolean;
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { licensingMock } from '../../../../../plugins/licensing/server/licensing.mocks';
|
||||
import { XPackInfoLicense } from './xpack_info_license';
|
||||
|
||||
function getXPackInfoLicense(getRawLicense) {
|
||||
|
@ -23,7 +24,7 @@ describe('XPackInfoLicense', () => {
|
|||
test('getUid returns uid field', () => {
|
||||
const uid = 'abc123';
|
||||
|
||||
getRawLicense.mockReturnValue({ uid });
|
||||
getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { uid } }));
|
||||
|
||||
expect(xpackInfoLicense.getUid()).toBe(uid);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(1);
|
||||
|
@ -32,14 +33,14 @@ describe('XPackInfoLicense', () => {
|
|||
});
|
||||
|
||||
test('isActive returns true if status is active', () => {
|
||||
getRawLicense.mockReturnValue({ status: 'active' });
|
||||
getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'active' } }));
|
||||
|
||||
expect(xpackInfoLicense.isActive()).toBe(true);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('isActive returns false if status is not active', () => {
|
||||
getRawLicense.mockReturnValue({ status: 'aCtIvE' }); // needs to match exactly
|
||||
getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'aCtIvE' } })); // needs to match exactly
|
||||
|
||||
expect(xpackInfoLicense.isActive()).toBe(false);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(1);
|
||||
|
@ -48,7 +49,9 @@ describe('XPackInfoLicense', () => {
|
|||
});
|
||||
|
||||
test('getExpiryDateInMillis returns expiry_date_in_millis', () => {
|
||||
getRawLicense.mockReturnValue({ expiry_date_in_millis: 123 });
|
||||
getRawLicense.mockReturnValue(
|
||||
licensingMock.createLicense({ license: { expiryDateInMillis: 123 } })
|
||||
);
|
||||
|
||||
expect(xpackInfoLicense.getExpiryDateInMillis()).toBe(123);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(1);
|
||||
|
@ -57,7 +60,7 @@ describe('XPackInfoLicense', () => {
|
|||
});
|
||||
|
||||
test('isOneOf returns true of the mode includes one of the types', () => {
|
||||
getRawLicense.mockReturnValue({ mode: 'platinum' });
|
||||
getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { mode: 'platinum' } }));
|
||||
|
||||
expect(xpackInfoLicense.isOneOf('platinum')).toBe(true);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(1);
|
||||
|
@ -77,12 +80,12 @@ describe('XPackInfoLicense', () => {
|
|||
});
|
||||
|
||||
test('getType returns the type', () => {
|
||||
getRawLicense.mockReturnValue({ type: 'basic' });
|
||||
getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { type: 'basic' } }));
|
||||
|
||||
expect(xpackInfoLicense.getType()).toBe('basic');
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(1);
|
||||
|
||||
getRawLicense.mockReturnValue({ type: 'gold' });
|
||||
getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { type: 'gold' } }));
|
||||
|
||||
expect(xpackInfoLicense.getType()).toBe('gold');
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(2);
|
||||
|
@ -91,12 +94,12 @@ describe('XPackInfoLicense', () => {
|
|||
});
|
||||
|
||||
test('getMode returns the mode', () => {
|
||||
getRawLicense.mockReturnValue({ mode: 'basic' });
|
||||
getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { mode: 'basic' } }));
|
||||
|
||||
expect(xpackInfoLicense.getMode()).toBe('basic');
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(1);
|
||||
|
||||
getRawLicense.mockReturnValue({ mode: 'gold' });
|
||||
getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { mode: 'gold' } }));
|
||||
|
||||
expect(xpackInfoLicense.getMode()).toBe('gold');
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(2);
|
||||
|
@ -107,22 +110,30 @@ describe('XPackInfoLicense', () => {
|
|||
test('isActiveLicense returns the true if active and typeChecker matches', () => {
|
||||
const expectAbc123 = type => type === 'abc123';
|
||||
|
||||
getRawLicense.mockReturnValue({ status: 'active', mode: 'abc123' });
|
||||
getRawLicense.mockReturnValue(
|
||||
licensingMock.createLicense({ license: { status: 'active', mode: 'abc123' } })
|
||||
);
|
||||
|
||||
expect(xpackInfoLicense.isActiveLicense(expectAbc123)).toBe(true);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(1);
|
||||
|
||||
getRawLicense.mockReturnValue({ status: 'NOTactive', mode: 'abc123' });
|
||||
getRawLicense.mockReturnValue(
|
||||
licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'abc123' } })
|
||||
);
|
||||
|
||||
expect(xpackInfoLicense.isActiveLicense(expectAbc123)).toBe(false);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(2);
|
||||
|
||||
getRawLicense.mockReturnValue({ status: 'NOTactive', mode: 'NOTabc123' });
|
||||
getRawLicense.mockReturnValue(
|
||||
licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'NOTabc123' } })
|
||||
);
|
||||
|
||||
expect(xpackInfoLicense.isActiveLicense(expectAbc123)).toBe(false);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(3);
|
||||
|
||||
getRawLicense.mockReturnValue({ status: 'active', mode: 'NOTabc123' });
|
||||
getRawLicense.mockReturnValue(
|
||||
licensingMock.createLicense({ license: { status: 'active', mode: 'NOTabc123' } })
|
||||
);
|
||||
|
||||
expect(xpackInfoLicense.isActiveLicense(expectAbc123)).toBe(false);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(4);
|
||||
|
@ -131,22 +142,30 @@ describe('XPackInfoLicense', () => {
|
|||
});
|
||||
|
||||
test('isBasic returns the true if active and basic', () => {
|
||||
getRawLicense.mockReturnValue({ status: 'active', mode: 'basic' });
|
||||
getRawLicense.mockReturnValue(
|
||||
licensingMock.createLicense({ license: { status: 'active', mode: 'basic' } })
|
||||
);
|
||||
|
||||
expect(xpackInfoLicense.isBasic()).toBe(true);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(1);
|
||||
|
||||
getRawLicense.mockReturnValue({ status: 'NOTactive', mode: 'gold' });
|
||||
getRawLicense.mockReturnValue(
|
||||
licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'gold' } })
|
||||
);
|
||||
|
||||
expect(xpackInfoLicense.isBasic()).toBe(false);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(2);
|
||||
|
||||
getRawLicense.mockReturnValue({ status: 'NOTactive', mode: 'trial' });
|
||||
getRawLicense.mockReturnValue(
|
||||
licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'trial' } })
|
||||
);
|
||||
|
||||
expect(xpackInfoLicense.isBasic()).toBe(false);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(3);
|
||||
|
||||
getRawLicense.mockReturnValue({ status: 'active', mode: 'platinum' });
|
||||
getRawLicense.mockReturnValue(
|
||||
licensingMock.createLicense({ license: { status: 'active', mode: 'platinum' } })
|
||||
);
|
||||
|
||||
expect(xpackInfoLicense.isBasic()).toBe(false);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(4);
|
||||
|
@ -155,22 +174,30 @@ describe('XPackInfoLicense', () => {
|
|||
});
|
||||
|
||||
test('isNotBasic returns the true if active and not basic', () => {
|
||||
getRawLicense.mockReturnValue({ status: 'active', mode: 'platinum' });
|
||||
getRawLicense.mockReturnValue(
|
||||
licensingMock.createLicense({ license: { status: 'active', mode: 'platinum' } })
|
||||
);
|
||||
|
||||
expect(xpackInfoLicense.isNotBasic()).toBe(true);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(1);
|
||||
|
||||
getRawLicense.mockReturnValue({ status: 'NOTactive', mode: 'gold' });
|
||||
getRawLicense.mockReturnValue(
|
||||
licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'gold' } })
|
||||
);
|
||||
|
||||
expect(xpackInfoLicense.isNotBasic()).toBe(false);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(2);
|
||||
|
||||
getRawLicense.mockReturnValue({ status: 'NOTactive', mode: 'trial' });
|
||||
getRawLicense.mockReturnValue(
|
||||
licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'trial' } })
|
||||
);
|
||||
|
||||
expect(xpackInfoLicense.isNotBasic()).toBe(false);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(3);
|
||||
|
||||
getRawLicense.mockReturnValue({ status: 'active', mode: 'basic' });
|
||||
getRawLicense.mockReturnValue(
|
||||
licensingMock.createLicense({ license: { status: 'active', mode: 'basic' } })
|
||||
);
|
||||
|
||||
expect(xpackInfoLicense.isNotBasic()).toBe(false);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(4);
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { get } from 'lodash';
|
||||
import { ILicense } from '../../../../../plugins/licensing/server';
|
||||
|
||||
/**
|
||||
* "View" for XPack Info license information.
|
||||
|
@ -15,9 +15,9 @@ export class XPackInfoLicense {
|
|||
* @type {Function}
|
||||
* @private
|
||||
*/
|
||||
_getRawLicense = null;
|
||||
_getRawLicense: () => ILicense | undefined;
|
||||
|
||||
constructor(getRawLicense) {
|
||||
constructor(getRawLicense: () => ILicense | undefined) {
|
||||
this._getRawLicense = getRawLicense;
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@ export class XPackInfoLicense {
|
|||
* @returns {string|undefined}
|
||||
*/
|
||||
getUid() {
|
||||
return get(this._getRawLicense(), 'uid');
|
||||
return this._getRawLicense()?.uid;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -34,7 +34,7 @@ export class XPackInfoLicense {
|
|||
* @returns {boolean}
|
||||
*/
|
||||
isActive() {
|
||||
return get(this._getRawLicense(), 'status') === 'active';
|
||||
return Boolean(this._getRawLicense()?.isActive);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -45,7 +45,7 @@ export class XPackInfoLicense {
|
|||
* @returns {number|undefined}
|
||||
*/
|
||||
getExpiryDateInMillis() {
|
||||
return get(this._getRawLicense(), 'expiry_date_in_millis');
|
||||
return this._getRawLicense()?.expiryDateInMillis;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -53,12 +53,10 @@ export class XPackInfoLicense {
|
|||
* @param {String} candidateLicenses List of the licenses to check against.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isOneOf(candidateLicenses) {
|
||||
if (!Array.isArray(candidateLicenses)) {
|
||||
candidateLicenses = [candidateLicenses];
|
||||
}
|
||||
|
||||
return candidateLicenses.includes(get(this._getRawLicense(), 'mode'));
|
||||
isOneOf(candidateLicenses: string | string[]) {
|
||||
const candidates = Array.isArray(candidateLicenses) ? candidateLicenses : [candidateLicenses];
|
||||
const mode = this._getRawLicense()?.mode;
|
||||
return Boolean(mode && candidates.includes(mode));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -66,7 +64,7 @@ export class XPackInfoLicense {
|
|||
* @returns {string|undefined}
|
||||
*/
|
||||
getType() {
|
||||
return get(this._getRawLicense(), 'type');
|
||||
return this._getRawLicense()?.type;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -74,7 +72,7 @@ export class XPackInfoLicense {
|
|||
* @returns {string|undefined}
|
||||
*/
|
||||
getMode() {
|
||||
return get(this._getRawLicense(), 'mode');
|
||||
return this._getRawLicense()?.mode;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -83,10 +81,10 @@ export class XPackInfoLicense {
|
|||
* @param {Function} typeChecker The license type checker.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isActiveLicense(typeChecker) {
|
||||
isActiveLicense(typeChecker: (mode: string) => boolean) {
|
||||
const license = this._getRawLicense();
|
||||
|
||||
return get(license, 'status') === 'active' && typeChecker(get(license, 'mode'));
|
||||
return Boolean(license?.isActive && typeChecker(license.mode as any));
|
||||
}
|
||||
|
||||
/**
|
|
@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { CoreSetup, CoreStart } from 'kibana/public';
|
||||
import { Plugin } from 'src/core/public';
|
||||
import { toggleNavLink } from './services/toggle_nav_link';
|
||||
import { LicensingPluginSetup } from '../../licensing/common/types';
|
||||
import { LicensingPluginSetup } from '../../licensing/public';
|
||||
import { checkLicense } from '../common/check_license';
|
||||
import {
|
||||
FeatureCatalogueCategory,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { Plugin, CoreSetup } from 'src/core/server';
|
||||
import { LicensingPluginSetup } from '../../licensing/common/types';
|
||||
import { LicensingPluginSetup } from '../../licensing/server';
|
||||
import { LicenseState } from './lib/license_state';
|
||||
import { registerSearchRoute } from './routes/search';
|
||||
import { registerExploreRoute } from './routes/explore';
|
||||
|
|
|
@ -13,6 +13,7 @@ function license({ error, ...customLicense }: { error?: string; [key: string]: a
|
|||
uid: 'uid-000000001234',
|
||||
status: 'active',
|
||||
type: 'basic',
|
||||
mode: 'basic',
|
||||
expiryDateInMillis: 1000,
|
||||
};
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { License } from './license';
|
||||
import { LICENSE_CHECK_STATE } from './types';
|
||||
import { licenseMock } from './license.mock';
|
||||
import { licenseMock } from './licensing.mocks';
|
||||
|
||||
describe('License', () => {
|
||||
const basicLicense = licenseMock.create();
|
||||
|
|
|
@ -33,6 +33,7 @@ export class License implements ILicense {
|
|||
public readonly status?: LicenseStatus;
|
||||
public readonly expiryDateInMillis?: number;
|
||||
public readonly type?: LicenseType;
|
||||
public readonly mode?: LicenseType;
|
||||
public readonly signature: string;
|
||||
|
||||
/**
|
||||
|
@ -65,6 +66,7 @@ export class License implements ILicense {
|
|||
this.status = license.status;
|
||||
this.expiryDateInMillis = license.expiryDateInMillis;
|
||||
this.type = license.type;
|
||||
this.mode = license.mode;
|
||||
}
|
||||
|
||||
this.isActive = this.status === 'active';
|
||||
|
|
|
@ -9,7 +9,7 @@ import { take, toArray } from 'rxjs/operators';
|
|||
|
||||
import { ILicense, LicenseType } from './types';
|
||||
import { createLicenseUpdate } from './license_update';
|
||||
import { licenseMock } from './license.mock';
|
||||
import { licenseMock } from './licensing.mocks';
|
||||
|
||||
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
const stop$ = new Subject();
|
||||
|
|
|
@ -19,6 +19,7 @@ function createLicense({
|
|||
uid: 'uid-000000001234',
|
||||
status: 'active',
|
||||
type: 'basic',
|
||||
mode: 'basic',
|
||||
expiryDateInMillis: 5000,
|
||||
};
|
||||
|
|
@ -3,7 +3,6 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export enum LICENSE_CHECK_STATE {
|
||||
Unavailable = 'UNAVAILABLE',
|
||||
|
@ -57,6 +56,11 @@ export interface PublicLicense {
|
|||
* The license type, being usually one of basic, standard, gold, platinum, or trial.
|
||||
*/
|
||||
type: LicenseType;
|
||||
/**
|
||||
* The license type, being usually one of basic, standard, gold, platinum, or trial.
|
||||
* @deprecated use 'type' instead
|
||||
*/
|
||||
mode: LicenseType;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -119,6 +123,12 @@ export interface ILicense {
|
|||
*/
|
||||
type?: LicenseType;
|
||||
|
||||
/**
|
||||
* The license type, being usually one of basic, standard, gold, platinum, or trial.
|
||||
* @deprecated use 'type' instead.
|
||||
*/
|
||||
mode?: LicenseType;
|
||||
|
||||
/**
|
||||
* Signature of the license content.
|
||||
*/
|
||||
|
@ -173,15 +183,3 @@ export interface ILicense {
|
|||
*/
|
||||
getFeature(name: string): LicenseFeature;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface LicensingPluginSetup {
|
||||
/**
|
||||
* Steam of licensing information {@link ILicense}.
|
||||
*/
|
||||
license$: Observable<ILicense>;
|
||||
/**
|
||||
* Triggers licensing information re-fetch.
|
||||
*/
|
||||
refresh(): Promise<ILicense>;
|
||||
}
|
||||
|
|
|
@ -8,4 +8,5 @@ import { PluginInitializerContext } from 'src/core/public';
|
|||
import { LicensingPlugin } from './plugin';
|
||||
|
||||
export * from '../common/types';
|
||||
export * from './types';
|
||||
export const plugin = (context: PluginInitializerContext) => new LicensingPlugin(context);
|
||||
|
|
24
x-pack/plugins/licensing/public/licensing.mocks.ts
Normal file
24
x-pack/plugins/licensing/public/licensing.mocks.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { LicensingPluginSetup } from './types';
|
||||
import { licenseMock } from '../common/licensing.mocks';
|
||||
|
||||
const createSetupMock = () => {
|
||||
const license = licenseMock.create();
|
||||
const mock: jest.Mocked<LicensingPluginSetup> = {
|
||||
license$: new BehaviorSubject(license),
|
||||
refresh: jest.fn(),
|
||||
};
|
||||
mock.refresh.mockResolvedValue(license);
|
||||
|
||||
return mock;
|
||||
};
|
||||
|
||||
export const licensingMock = {
|
||||
createSetup: createSetupMock,
|
||||
createLicense: licenseMock.create,
|
||||
};
|
|
@ -11,12 +11,10 @@ import { LicenseType } from '../common/types';
|
|||
import { LicensingPlugin, licensingSessionStorageKey } from './plugin';
|
||||
|
||||
import { License } from '../common/license';
|
||||
import { licenseMock } from '../common/license.mock';
|
||||
import { licenseMock } from '../common/licensing.mocks';
|
||||
import { coreMock } from '../../../../src/core/public/mocks';
|
||||
import { HttpInterceptor } from 'src/core/public';
|
||||
|
||||
const delay = (ms: number) => new Promise(res => setTimeout(res, ms));
|
||||
|
||||
describe('licensing plugin', () => {
|
||||
let plugin: LicensingPlugin;
|
||||
|
||||
|
@ -34,15 +32,7 @@ describe('licensing plugin', () => {
|
|||
const coreSetup = coreMock.createSetup();
|
||||
const firstLicense = licenseMock.create({ license: { uid: 'first', type: 'basic' } });
|
||||
const secondLicense = licenseMock.create({ license: { uid: 'second', type: 'gold' } });
|
||||
coreSetup.http.get
|
||||
.mockImplementationOnce(async () => {
|
||||
await delay(100);
|
||||
return firstLicense;
|
||||
})
|
||||
.mockImplementationOnce(async () => {
|
||||
await delay(100);
|
||||
return secondLicense;
|
||||
});
|
||||
coreSetup.http.get.mockResolvedValueOnce(firstLicense).mockResolvedValueOnce(secondLicense);
|
||||
|
||||
const { license$, refresh } = await plugin.setup(coreSetup);
|
||||
|
||||
|
@ -147,7 +137,7 @@ describe('licensing plugin', () => {
|
|||
|
||||
expect(sessionStorage.setItem.mock.calls[0][0]).toBe(licensingSessionStorageKey);
|
||||
expect(sessionStorage.setItem.mock.calls[0][1]).toMatchInlineSnapshot(
|
||||
`"{\\"license\\":{\\"uid\\":\\"fresh\\",\\"status\\":\\"active\\",\\"type\\":\\"basic\\",\\"expiryDateInMillis\\":5000},\\"features\\":{\\"ccr\\":{\\"isEnabled\\":true,\\"isAvailable\\":true},\\"ml\\":{\\"isEnabled\\":false,\\"isAvailable\\":true}},\\"signature\\":\\"xxxxxxxxx\\"}"`
|
||||
`"{\\"license\\":{\\"uid\\":\\"fresh\\",\\"status\\":\\"active\\",\\"type\\":\\"basic\\",\\"mode\\":\\"basic\\",\\"expiryDateInMillis\\":5000},\\"features\\":{\\"ccr\\":{\\"isEnabled\\":true,\\"isAvailable\\":true},\\"ml\\":{\\"isEnabled\\":false,\\"isAvailable\\":true}},\\"signature\\":\\"xxxxxxxxx\\"}"`
|
||||
);
|
||||
|
||||
const saved = JSON.parse(sessionStorage.setItem.mock.calls[0][1]);
|
||||
|
|
|
@ -7,7 +7,8 @@ import { Subject, Subscription } from 'rxjs';
|
|||
|
||||
import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public';
|
||||
|
||||
import { ILicense, LicensingPluginSetup } from '../common/types';
|
||||
import { ILicense } from '../common/types';
|
||||
import { LicensingPluginSetup } from './types';
|
||||
import { createLicenseUpdate } from '../common/license_update';
|
||||
import { License } from '../common/license';
|
||||
import { mountExpiredBanner } from './expired_banner';
|
||||
|
|
20
x-pack/plugins/licensing/public/types.ts
Normal file
20
x-pack/plugins/licensing/public/types.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { ILicense } from '../common/types';
|
||||
|
||||
/** @public */
|
||||
export interface LicensingPluginSetup {
|
||||
/**
|
||||
* Steam of licensing information {@link ILicense}.
|
||||
*/
|
||||
license$: Observable<ILicense>;
|
||||
/**
|
||||
* Triggers licensing information re-fetch.
|
||||
*/
|
||||
refresh(): Promise<ILicense>;
|
||||
}
|
|
@ -10,4 +10,5 @@ import { LicensingPlugin } from './plugin';
|
|||
export const plugin = (context: PluginInitializerContext) => new LicensingPlugin(context);
|
||||
|
||||
export * from '../common/types';
|
||||
export * from './types';
|
||||
export { config } from './licensing_config';
|
||||
|
|
29
x-pack/plugins/licensing/server/licensing.mocks.ts
Normal file
29
x-pack/plugins/licensing/server/licensing.mocks.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { LicensingPluginSetup } from './types';
|
||||
import { licenseMock } from '../common/licensing.mocks';
|
||||
|
||||
const createSetupMock = () => {
|
||||
const license = licenseMock.create();
|
||||
const mock: jest.Mocked<LicensingPluginSetup> = {
|
||||
license$: new BehaviorSubject(license),
|
||||
refresh: jest.fn(),
|
||||
createLicensePoller: jest.fn(),
|
||||
};
|
||||
mock.refresh.mockResolvedValue(license);
|
||||
mock.createLicensePoller.mockReturnValue({
|
||||
license$: mock.license$,
|
||||
refresh: mock.refresh,
|
||||
});
|
||||
|
||||
return mock;
|
||||
};
|
||||
|
||||
export const licensingMock = {
|
||||
createSetup: createSetupMock,
|
||||
createLicense: licenseMock.create,
|
||||
};
|
|
@ -5,11 +5,22 @@
|
|||
*/
|
||||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { PluginConfigDescriptor } from 'kibana/server';
|
||||
|
||||
export const config = {
|
||||
const configSchema = schema.object({
|
||||
api_polling_frequency: schema.duration({ defaultValue: '30s' }),
|
||||
});
|
||||
|
||||
export type LicenseConfigType = TypeOf<typeof configSchema>;
|
||||
|
||||
export const config: PluginConfigDescriptor<LicenseConfigType> = {
|
||||
schema: schema.object({
|
||||
pollingFrequency: schema.duration({ defaultValue: '30s' }),
|
||||
api_polling_frequency: schema.duration({ defaultValue: '30s' }),
|
||||
}),
|
||||
deprecations: ({ renameFromRoot }) => [
|
||||
renameFromRoot(
|
||||
'xpack.xpack_main.xpack_api_polling_frequency_millis',
|
||||
'xpack.licensing.api_polling_frequency'
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export type LicenseConfigType = TypeOf<typeof config.schema>;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { licenseMock } from '../common/license.mock';
|
||||
import { licenseMock } from '../common/licensing.mocks';
|
||||
|
||||
import { createRouteHandlerContext } from './licensing_route_handler_context';
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import { BehaviorSubject } from 'rxjs';
|
||||
import { createOnPreResponseHandler } from './on_pre_response_handler';
|
||||
import { httpServiceMock, httpServerMock } from '../../../../src/core/server/mocks';
|
||||
import { licenseMock } from '../common/license.mock';
|
||||
import { licenseMock } from '../common/licensing.mocks';
|
||||
|
||||
describe('createOnPreResponseHandler', () => {
|
||||
it('sets license.signature header immediately for non-error responses', async () => {
|
||||
|
|
|
@ -21,11 +21,11 @@ function buildRawLicense(options: Partial<RawLicense> = {}): RawLicense {
|
|||
uid: 'uid-000000001234',
|
||||
status: 'active',
|
||||
type: 'basic',
|
||||
mode: 'basic',
|
||||
expiry_date_in_millis: 1000,
|
||||
};
|
||||
return Object.assign(defaultRawLicense, options);
|
||||
}
|
||||
const pollingFrequency = moment.duration(100);
|
||||
|
||||
const flushPromises = (ms = 50) => new Promise(res => setTimeout(res, ms));
|
||||
|
||||
|
@ -37,7 +37,7 @@ describe('licensing plugin', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
pluginInitContextMock = coreMock.createPluginInitializerContext({
|
||||
pollingFrequency,
|
||||
api_polling_frequency: moment.duration(100),
|
||||
});
|
||||
plugin = new LicensingPlugin(pluginInitContextMock);
|
||||
});
|
||||
|
@ -200,7 +200,7 @@ describe('licensing plugin', () => {
|
|||
plugin = new LicensingPlugin(
|
||||
coreMock.createPluginInitializerContext({
|
||||
// disable polling mechanism
|
||||
pollingFrequency: moment.duration(50000),
|
||||
api_polling_frequency: moment.duration(50000),
|
||||
})
|
||||
);
|
||||
const dataClient = elasticsearchServiceMock.createClusterClient();
|
||||
|
@ -222,13 +222,88 @@ describe('licensing plugin', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#createLicensePoller', () => {
|
||||
let plugin: LicensingPlugin;
|
||||
|
||||
afterEach(async () => {
|
||||
await plugin.stop();
|
||||
});
|
||||
|
||||
it(`creates a poller fetching license from passed 'clusterClient' every 'api_polling_frequency' ms`, async () => {
|
||||
plugin = new LicensingPlugin(
|
||||
coreMock.createPluginInitializerContext({
|
||||
api_polling_frequency: moment.duration(50000),
|
||||
})
|
||||
);
|
||||
|
||||
const dataClient = elasticsearchServiceMock.createClusterClient();
|
||||
dataClient.callAsInternalUser.mockResolvedValue({
|
||||
license: buildRawLicense(),
|
||||
features: {},
|
||||
});
|
||||
const coreSetup = coreMock.createSetup();
|
||||
coreSetup.elasticsearch.dataClient$ = new BehaviorSubject(dataClient);
|
||||
|
||||
const { createLicensePoller, license$ } = await plugin.setup(coreSetup);
|
||||
const customClient = elasticsearchServiceMock.createClusterClient();
|
||||
customClient.callAsInternalUser.mockResolvedValue({
|
||||
license: buildRawLicense({ type: 'gold' }),
|
||||
features: {},
|
||||
});
|
||||
|
||||
const customPollingFrequency = 100;
|
||||
const { license$: customLicense$ } = createLicensePoller(
|
||||
customClient,
|
||||
customPollingFrequency
|
||||
);
|
||||
expect(customClient.callAsInternalUser).toHaveBeenCalledTimes(0);
|
||||
|
||||
const customLicense = await customLicense$.pipe(take(1)).toPromise();
|
||||
expect(customClient.callAsInternalUser).toHaveBeenCalledTimes(1);
|
||||
|
||||
await flushPromises(customPollingFrequency * 1.5);
|
||||
expect(customClient.callAsInternalUser).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(customLicense.isAvailable).toBe(true);
|
||||
expect(customLicense.type).toBe('gold');
|
||||
|
||||
expect(await license$.pipe(take(1)).toPromise()).not.toBe(customLicense);
|
||||
});
|
||||
|
||||
it('creates a poller with a manual refresh control', async () => {
|
||||
plugin = new LicensingPlugin(
|
||||
coreMock.createPluginInitializerContext({
|
||||
api_polling_frequency: moment.duration(100),
|
||||
})
|
||||
);
|
||||
|
||||
const coreSetup = coreMock.createSetup();
|
||||
const { createLicensePoller } = await plugin.setup(coreSetup);
|
||||
|
||||
const customClient = elasticsearchServiceMock.createClusterClient();
|
||||
customClient.callAsInternalUser.mockResolvedValue({
|
||||
license: buildRawLicense({ type: 'gold' }),
|
||||
features: {},
|
||||
});
|
||||
|
||||
const { license$, refresh } = createLicensePoller(customClient, 10000);
|
||||
expect(customClient.callAsInternalUser).toHaveBeenCalledTimes(0);
|
||||
|
||||
await refresh();
|
||||
|
||||
expect(customClient.callAsInternalUser).toHaveBeenCalledTimes(1);
|
||||
const license = await license$.pipe(take(1)).toPromise();
|
||||
expect(license.type).toBe('gold');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extends core contexts', () => {
|
||||
let plugin: LicensingPlugin;
|
||||
|
||||
beforeEach(() => {
|
||||
plugin = new LicensingPlugin(
|
||||
coreMock.createPluginInitializerContext({
|
||||
pollingFrequency,
|
||||
api_polling_frequency: moment.duration(100),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@ -257,7 +332,9 @@ describe('licensing plugin', () => {
|
|||
let plugin: LicensingPlugin;
|
||||
|
||||
beforeEach(() => {
|
||||
plugin = new LicensingPlugin(coreMock.createPluginInitializerContext({ pollingFrequency }));
|
||||
plugin = new LicensingPlugin(
|
||||
coreMock.createPluginInitializerContext({ api_polling_frequency: moment.duration(100) })
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
@ -278,7 +355,7 @@ describe('licensing plugin', () => {
|
|||
it('stops polling', async () => {
|
||||
const plugin = new LicensingPlugin(
|
||||
coreMock.createPluginInitializerContext({
|
||||
pollingFrequency,
|
||||
api_polling_frequency: moment.duration(100),
|
||||
})
|
||||
);
|
||||
const coreSetup = coreMock.createSetup();
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { Observable, Subject, Subscription, timer } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
import moment, { Duration } from 'moment';
|
||||
import moment from 'moment';
|
||||
import { createHash } from 'crypto';
|
||||
import stringify from 'json-stable-stringify';
|
||||
|
||||
|
@ -19,7 +19,8 @@ import {
|
|||
IClusterClient,
|
||||
} from 'src/core/server';
|
||||
|
||||
import { ILicense, LicensingPluginSetup, PublicLicense, PublicFeatures } from '../common/types';
|
||||
import { ILicense, PublicLicense, PublicFeatures } from '../common/types';
|
||||
import { LicensingPluginSetup } from './types';
|
||||
import { License } from '../common/license';
|
||||
import { createLicenseUpdate } from '../common/license_update';
|
||||
|
||||
|
@ -34,6 +35,7 @@ function normalizeServerLicense(license: RawLicense): PublicLicense {
|
|||
return {
|
||||
uid: license.uid,
|
||||
type: license.type,
|
||||
mode: license.mode,
|
||||
expiryDateInMillis: license.expiry_date_in_millis,
|
||||
status: license.status,
|
||||
};
|
||||
|
@ -89,9 +91,13 @@ export class LicensingPlugin implements Plugin<LicensingPluginSetup> {
|
|||
public async setup(core: CoreSetup) {
|
||||
this.logger.debug('Setting up Licensing plugin');
|
||||
const config = await this.config$.pipe(take(1)).toPromise();
|
||||
const pollingFrequency = config.api_polling_frequency;
|
||||
const dataClient = await core.elasticsearch.dataClient$.pipe(take(1)).toPromise();
|
||||
|
||||
const { refresh, license$ } = this.createLicensePoller(dataClient, config.pollingFrequency);
|
||||
const { refresh, license$ } = this.createLicensePoller(
|
||||
dataClient,
|
||||
pollingFrequency.asMilliseconds()
|
||||
);
|
||||
|
||||
core.http.registerRouteHandlerContext('licensing', createRouteHandlerContext(license$));
|
||||
|
||||
|
@ -101,11 +107,14 @@ export class LicensingPlugin implements Plugin<LicensingPluginSetup> {
|
|||
return {
|
||||
refresh,
|
||||
license$,
|
||||
createLicensePoller: this.createLicensePoller.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
private createLicensePoller(clusterClient: IClusterClient, pollingFrequency: Duration) {
|
||||
const intervalRefresh$ = timer(0, pollingFrequency.asMilliseconds());
|
||||
private createLicensePoller(clusterClient: IClusterClient, pollingFrequency: number) {
|
||||
this.logger.debug(`Polling Elasticsearch License API with frequency ${pollingFrequency}ms.`);
|
||||
|
||||
const intervalRefresh$ = timer(0, pollingFrequency);
|
||||
|
||||
const { license$, refreshManually } = createLicenseUpdate(intervalRefresh$, this.stop$, () =>
|
||||
this.fetchLicense(clusterClient)
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { Observable } from 'rxjs';
|
||||
import { IClusterClient } from 'src/core/server';
|
||||
import { ILicense, LicenseStatus, LicenseType } from '../common/types';
|
||||
|
||||
export interface ElasticsearchError extends Error {
|
||||
|
@ -34,6 +36,7 @@ export interface RawLicense {
|
|||
status: LicenseStatus;
|
||||
expiry_date_in_millis: number;
|
||||
type: LicenseType;
|
||||
mode: LicenseType;
|
||||
}
|
||||
|
||||
declare module 'src/core/server' {
|
||||
|
@ -43,3 +46,25 @@ declare module 'src/core/server' {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface LicensingPluginSetup {
|
||||
/**
|
||||
* Steam of licensing information {@link ILicense}.
|
||||
*/
|
||||
license$: Observable<ILicense>;
|
||||
/**
|
||||
* Triggers licensing information re-fetch.
|
||||
*/
|
||||
refresh(): Promise<ILicense>;
|
||||
|
||||
/**
|
||||
* Creates a license poller to retrieve a license data with.
|
||||
* Allows a plugin to configure a cluster to retrieve data from at
|
||||
* given polling frequency.
|
||||
*/
|
||||
createLicensePoller: (
|
||||
clusterClient: IClusterClient,
|
||||
pollingFrequency: number
|
||||
) => { license$: Observable<ILicense>; refresh(): Promise<ILicense> };
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) {
|
|||
},
|
||||
|
||||
async getLicense(): Promise<PublicLicenseJSON> {
|
||||
// > --xpack.licensing.pollingFrequency set in test config
|
||||
// > --xpack.licensing.api_polling_frequency set in test config
|
||||
// to wait for Kibana server to re-fetch the license from Elasticsearch
|
||||
await delay(1000);
|
||||
|
||||
|
@ -97,30 +97,71 @@ export default function({ getService, getPageObjects }: FtrProviderContext) {
|
|||
isEnabled: true,
|
||||
});
|
||||
|
||||
const {
|
||||
body: legacyInitialLicense,
|
||||
headers: legacyInitialLicenseHeaders,
|
||||
} = await supertest.get('/api/xpack/v1/info').expect(200);
|
||||
|
||||
expect(legacyInitialLicense.license?.type).to.be('basic');
|
||||
expect(legacyInitialLicense.features).to.have.property('security');
|
||||
expect(legacyInitialLicenseHeaders['kbn-xpack-sig']).to.be.a('string');
|
||||
|
||||
// license hasn't changed
|
||||
const refetchedLicense = await scenario.getLicense();
|
||||
expect(refetchedLicense.license?.type).to.be('basic');
|
||||
expect(refetchedLicense.signature).to.be(initialLicense.signature);
|
||||
|
||||
const {
|
||||
body: legacyRefetchedLicense,
|
||||
headers: legacyRefetchedLicenseHeaders,
|
||||
} = await supertest.get('/api/xpack/v1/info').expect(200);
|
||||
|
||||
expect(legacyRefetchedLicense.license?.type).to.be('basic');
|
||||
expect(legacyRefetchedLicenseHeaders['kbn-xpack-sig']).to.be(
|
||||
legacyInitialLicenseHeaders['kbn-xpack-sig']
|
||||
);
|
||||
|
||||
// server allows to request trial only once.
|
||||
// other attempts will throw 403
|
||||
await scenario.startTrial();
|
||||
const trialLicense = await scenario.getLicense();
|
||||
expect(trialLicense.license?.type).to.be('trial');
|
||||
expect(trialLicense.signature).to.not.be(initialLicense.signature);
|
||||
|
||||
expect(trialLicense.features?.security).to.eql({
|
||||
isAvailable: true,
|
||||
isEnabled: true,
|
||||
});
|
||||
|
||||
const { body: legacyTrialLicense, headers: legacyTrialLicenseHeaders } = await supertest
|
||||
.get('/api/xpack/v1/info')
|
||||
.expect(200);
|
||||
|
||||
expect(legacyTrialLicense.license?.type).to.be('trial');
|
||||
expect(legacyTrialLicense.features).to.have.property('security');
|
||||
expect(legacyTrialLicenseHeaders['kbn-xpack-sig']).to.not.be(
|
||||
legacyInitialLicenseHeaders['kbn-xpack-sig']
|
||||
);
|
||||
|
||||
await scenario.startBasic();
|
||||
const basicLicense = await scenario.getLicense();
|
||||
expect(basicLicense.license?.type).to.be('basic');
|
||||
expect(basicLicense.signature).not.to.be(initialLicense.signature);
|
||||
expect(trialLicense.features?.security).to.eql({
|
||||
|
||||
expect(basicLicense.features?.security).to.eql({
|
||||
isAvailable: true,
|
||||
isEnabled: true,
|
||||
});
|
||||
|
||||
const { body: legacyBasicLicense, headers: legacyBasicLicenseHeaders } = await supertest
|
||||
.get('/api/xpack/v1/info')
|
||||
.expect(200);
|
||||
expect(legacyBasicLicense.license?.type).to.be('basic');
|
||||
expect(legacyBasicLicense.features).to.have.property('security');
|
||||
expect(legacyBasicLicenseHeaders['kbn-xpack-sig']).to.not.be(
|
||||
legacyInitialLicenseHeaders['kbn-xpack-sig']
|
||||
);
|
||||
|
||||
await scenario.deleteLicense();
|
||||
const inactiveLicense = await scenario.getLicense();
|
||||
expect(inactiveLicense.signature).to.not.be(initialLicense.signature);
|
||||
|
|
|
@ -43,7 +43,7 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) {
|
|||
...functionalTestsConfig.get('kbnTestServer'),
|
||||
serverArgs: [
|
||||
...functionalTestsConfig.get('kbnTestServer.serverArgs'),
|
||||
'--xpack.licensing.pollingFrequency=300',
|
||||
'--xpack.licensing.api_polling_frequency=300',
|
||||
],
|
||||
},
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue