mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
* Add x-pack plugin for new platform server licensing information * Update x-pack translations * Implement core plugin interface for licensing plugin: * Rename references to service to plugin * Use CoreStart in licensing start method * Fix outstanding reference error * Fix type check errors * Address outstanding review comments * Fix type error in tests * Address review comments, move polling logic to standalone * Split up test files * Fix bad reference in test * Use relative reference to poller util * Add more plugin tests to address review comments * Fix different manners of config generation in licensing plugin * Update test fixtures * Fix path to test fixtures
This commit is contained in:
parent
d8392c4771
commit
c05b383b63
17 changed files with 1098 additions and 6 deletions
70
src/core/utils/poller.test.ts
Normal file
70
src/core/utils/poller.test.ts
Normal file
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { Poller } from './poller';
|
||||
|
||||
const delay = (duration: number) => new Promise(r => setTimeout(r, duration));
|
||||
|
||||
describe('Poller', () => {
|
||||
let handler: jest.Mock<any, any>;
|
||||
let poller: Poller<string>;
|
||||
|
||||
beforeEach(() => {
|
||||
handler = jest.fn().mockImplementation((iteration: number) => `polling-${iteration}`);
|
||||
poller = new Poller<string>(100, 'polling', handler);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
poller.unsubscribe();
|
||||
});
|
||||
|
||||
it('returns an observable of subject', async () => {
|
||||
await delay(300);
|
||||
expect(poller.subject$.getValue()).toBe('polling-2');
|
||||
});
|
||||
|
||||
it('executes a function on an interval', async () => {
|
||||
await delay(300);
|
||||
expect(handler).toBeCalledTimes(3);
|
||||
});
|
||||
|
||||
it('no longer polls after unsubscribing', async () => {
|
||||
await delay(300);
|
||||
poller.unsubscribe();
|
||||
await delay(300);
|
||||
expect(handler).toBeCalledTimes(3);
|
||||
});
|
||||
|
||||
it('does not add next value if returns undefined', async () => {
|
||||
const values: any[] = [];
|
||||
const polling = new Poller<string>(100, 'polling', iteration => {
|
||||
if (iteration % 2 === 0) {
|
||||
return `polling-${iteration}`;
|
||||
}
|
||||
});
|
||||
|
||||
polling.subject$.subscribe(value => {
|
||||
values.push(value);
|
||||
});
|
||||
await delay(300);
|
||||
polling.unsubscribe();
|
||||
|
||||
expect(values).toEqual(['polling', 'polling-0', 'polling-2']);
|
||||
});
|
||||
});
|
55
src/core/utils/poller.ts
Normal file
55
src/core/utils/poller.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { BehaviorSubject, timer } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Create an Observable BehaviorSubject to invoke a function on an interval
|
||||
* which returns the next value for the observable.
|
||||
* @public
|
||||
*/
|
||||
export class Poller<T> {
|
||||
/**
|
||||
* The observable to observe for changes to the poller value.
|
||||
*/
|
||||
public readonly subject$ = new BehaviorSubject<T>(this.initialValue);
|
||||
private poller$ = timer(0, this.frequency);
|
||||
private subscription = this.poller$.subscribe(async iteration => {
|
||||
const next = await this.handler(iteration);
|
||||
|
||||
if (next !== undefined) {
|
||||
this.subject$.next(next);
|
||||
}
|
||||
|
||||
return iteration;
|
||||
});
|
||||
|
||||
constructor(
|
||||
private frequency: number,
|
||||
private initialValue: T,
|
||||
private handler: (iteration: number) => Promise<T | undefined> | T | undefined
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Permanently end the polling operation.
|
||||
*/
|
||||
unsubscribe() {
|
||||
return this.subscription.unsubscribe();
|
||||
}
|
||||
}
|
8
x-pack/plugins/licensing/kibana.json
Normal file
8
x-pack/plugins/licensing/kibana.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"id": "licensing",
|
||||
"version": "0.0.1",
|
||||
"kibanaVersion": "kibana",
|
||||
"configPath": ["x-pack", "licensing"],
|
||||
"server": true,
|
||||
"ui": false
|
||||
}
|
115
x-pack/plugins/licensing/server/__fixtures__/setup.ts
Normal file
115
x-pack/plugins/licensing/server/__fixtures__/setup.ts
Normal file
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* 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 { take, skip } from 'rxjs/operators';
|
||||
import { merge } from 'lodash';
|
||||
import { ClusterClient } from 'src/core/server';
|
||||
import { coreMock } from '../../../../../src/core/server/mocks';
|
||||
import { Plugin } from '../plugin';
|
||||
import { schema } from '../schema';
|
||||
|
||||
export async function licenseMerge(xpackInfo = {}) {
|
||||
return merge(
|
||||
{
|
||||
license: {
|
||||
uid: '00000000-0000-0000-0000-000000000000',
|
||||
type: 'basic',
|
||||
mode: 'basic',
|
||||
status: 'active',
|
||||
},
|
||||
features: {
|
||||
ccr: {
|
||||
available: false,
|
||||
enabled: true,
|
||||
},
|
||||
data_frame: {
|
||||
available: true,
|
||||
enabled: true,
|
||||
},
|
||||
graph: {
|
||||
available: false,
|
||||
enabled: true,
|
||||
},
|
||||
ilm: {
|
||||
available: true,
|
||||
enabled: true,
|
||||
},
|
||||
logstash: {
|
||||
available: false,
|
||||
enabled: true,
|
||||
},
|
||||
ml: {
|
||||
available: false,
|
||||
enabled: true,
|
||||
},
|
||||
monitoring: {
|
||||
available: true,
|
||||
enabled: true,
|
||||
},
|
||||
rollup: {
|
||||
available: true,
|
||||
enabled: true,
|
||||
},
|
||||
security: {
|
||||
available: true,
|
||||
enabled: true,
|
||||
},
|
||||
sql: {
|
||||
available: true,
|
||||
enabled: true,
|
||||
},
|
||||
vectors: {
|
||||
available: true,
|
||||
enabled: true,
|
||||
},
|
||||
voting_only: {
|
||||
available: true,
|
||||
enabled: true,
|
||||
},
|
||||
watcher: {
|
||||
available: false,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
xpackInfo
|
||||
);
|
||||
}
|
||||
|
||||
export async function setupOnly(pluginInitializerContext: any = {}) {
|
||||
const coreSetup = coreMock.createSetup();
|
||||
const clusterClient = ((await coreSetup.elasticsearch.dataClient$
|
||||
.pipe(take(1))
|
||||
.toPromise()) as unknown) as jest.Mocked<PublicMethodsOf<ClusterClient>>;
|
||||
const plugin = new Plugin(
|
||||
coreMock.createPluginInitializerContext({
|
||||
config: schema.validate(pluginInitializerContext.config || {}),
|
||||
})
|
||||
);
|
||||
|
||||
return { coreSetup, plugin, clusterClient };
|
||||
}
|
||||
|
||||
export async function setup(xpackInfo = {}, pluginInitializerContext: any = {}) {
|
||||
const { coreSetup, clusterClient, plugin } = await setupOnly(pluginInitializerContext);
|
||||
|
||||
clusterClient.callAsInternalUser.mockResolvedValueOnce(licenseMerge(xpackInfo));
|
||||
|
||||
const { license$ } = await plugin.setup(coreSetup);
|
||||
const license = await license$
|
||||
.pipe(
|
||||
skip(1),
|
||||
take(1)
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
return {
|
||||
plugin,
|
||||
license$,
|
||||
license,
|
||||
clusterClient,
|
||||
};
|
||||
}
|
21
x-pack/plugins/licensing/server/constants.ts
Normal file
21
x-pack/plugins/licensing/server/constants.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const SERVICE_NAME = 'licensing';
|
||||
export const DEFAULT_POLLING_FREQUENCY = 30001; // 30 seconds
|
||||
export enum LICENSE_STATUS {
|
||||
Unavailable = 'UNAVAILABLE',
|
||||
Invalid = 'INVALID',
|
||||
Expired = 'EXPIRED',
|
||||
Valid = 'VALID',
|
||||
}
|
||||
export enum LICENSE_TYPE {
|
||||
basic = 10,
|
||||
standard = 20,
|
||||
gold = 30,
|
||||
platinum = 40,
|
||||
trial = 50,
|
||||
}
|
13
x-pack/plugins/licensing/server/index.ts
Normal file
13
x-pack/plugins/licensing/server/index.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 { PluginInitializerContext } from 'src/core/server';
|
||||
import { schema } from './schema';
|
||||
import { Plugin } from './plugin';
|
||||
|
||||
export * from './types';
|
||||
export const config = { schema };
|
||||
export const plugin = (context: PluginInitializerContext) => new Plugin(context);
|
180
x-pack/plugins/licensing/server/license.test.ts
Normal file
180
x-pack/plugins/licensing/server/license.test.ts
Normal file
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
* 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 { ILicense } from './types';
|
||||
import { Plugin } from './plugin';
|
||||
import { LICENSE_STATUS } from './constants';
|
||||
import { LicenseFeature } from './license_feature';
|
||||
import { setup } from './__fixtures__/setup';
|
||||
|
||||
describe('license', () => {
|
||||
let plugin: Plugin;
|
||||
let license: ILicense;
|
||||
|
||||
afterEach(async () => {
|
||||
await plugin.stop();
|
||||
});
|
||||
|
||||
test('uid returns a UID field', async () => {
|
||||
({ plugin, license } = await setup());
|
||||
|
||||
expect(license.uid).toBe('00000000-0000-0000-0000-000000000000');
|
||||
});
|
||||
|
||||
test('isActive returns true if status is active', async () => {
|
||||
({ plugin, license } = await setup());
|
||||
|
||||
expect(license.isActive).toBe(true);
|
||||
});
|
||||
|
||||
test('isActive returns false if status is not active', async () => {
|
||||
({ plugin, license } = await setup({
|
||||
license: {
|
||||
status: 'aCtIvE', // needs to match exactly
|
||||
},
|
||||
}));
|
||||
|
||||
expect(license.isActive).toBe(false);
|
||||
});
|
||||
|
||||
test('expiryDateInMillis returns expiry_date_in_millis', async () => {
|
||||
const expiry = Date.now();
|
||||
|
||||
({ plugin, license } = await setup({
|
||||
license: {
|
||||
expiry_date_in_millis: expiry,
|
||||
},
|
||||
}));
|
||||
|
||||
expect(license.expiryDateInMillis).toBe(expiry);
|
||||
});
|
||||
|
||||
test('isOneOf returns true if the type includes one of the license types', async () => {
|
||||
({ plugin, license } = await setup({
|
||||
license: {
|
||||
type: 'platinum',
|
||||
},
|
||||
}));
|
||||
|
||||
expect(license.isOneOf('platinum')).toBe(true);
|
||||
expect(license.isOneOf(['platinum'])).toBe(true);
|
||||
expect(license.isOneOf(['gold', 'platinum'])).toBe(true);
|
||||
expect(license.isOneOf(['platinum', 'gold'])).toBe(true);
|
||||
expect(license.isOneOf(['basic', 'gold'])).toBe(false);
|
||||
expect(license.isOneOf(['basic'])).toBe(false);
|
||||
});
|
||||
|
||||
test('type returns the license type', async () => {
|
||||
({ plugin, license } = await setup());
|
||||
|
||||
expect(license.type).toBe('basic');
|
||||
});
|
||||
|
||||
test('returns feature API with getFeature', async () => {
|
||||
({ plugin, license } = await setup());
|
||||
|
||||
const security = license.getFeature('security');
|
||||
const fake = license.getFeature('fake');
|
||||
|
||||
expect(security).toBeInstanceOf(LicenseFeature);
|
||||
expect(fake).toBeInstanceOf(LicenseFeature);
|
||||
});
|
||||
|
||||
describe('isActive', () => {
|
||||
test('should return Valid if active and check matches', async () => {
|
||||
({ plugin, license } = await setup({
|
||||
license: {
|
||||
type: 'gold',
|
||||
},
|
||||
}));
|
||||
|
||||
expect(license.check('test', 'basic').check).toBe(LICENSE_STATUS.Valid);
|
||||
expect(license.check('test', 'gold').check).toBe(LICENSE_STATUS.Valid);
|
||||
});
|
||||
|
||||
test('should return Invalid if active and check does not match', async () => {
|
||||
({ plugin, license } = await setup());
|
||||
|
||||
const { check } = license.check('test', 'gold');
|
||||
|
||||
expect(check).toBe(LICENSE_STATUS.Invalid);
|
||||
});
|
||||
|
||||
test('should return Unavailable if missing license', async () => {
|
||||
({ plugin, license } = await setup({ license: null }));
|
||||
|
||||
const { check } = license.check('test', 'gold');
|
||||
|
||||
expect(check).toBe(LICENSE_STATUS.Unavailable);
|
||||
});
|
||||
|
||||
test('should return Expired if not active', async () => {
|
||||
({ plugin, license } = await setup({
|
||||
license: {
|
||||
status: 'not-active',
|
||||
},
|
||||
}));
|
||||
|
||||
const { check } = license.check('test', 'basic');
|
||||
|
||||
expect(check).toBe(LICENSE_STATUS.Expired);
|
||||
});
|
||||
});
|
||||
|
||||
describe('basic', () => {
|
||||
test('isBasic is true if active and basic', async () => {
|
||||
({ plugin, license } = await setup());
|
||||
|
||||
expect(license.isBasic).toBe(true);
|
||||
});
|
||||
|
||||
test('isBasic is false if active and not basic', async () => {
|
||||
({ plugin, license } = await setup({
|
||||
license: {
|
||||
type: 'gold',
|
||||
},
|
||||
}));
|
||||
|
||||
expect(license.isBasic).toBe(false);
|
||||
});
|
||||
|
||||
test('isBasic is false if not active and basic', async () => {
|
||||
({ plugin, license } = await setup({
|
||||
license: {
|
||||
status: 'not-active',
|
||||
},
|
||||
}));
|
||||
|
||||
expect(license.isBasic).toBe(false);
|
||||
});
|
||||
|
||||
test('isNotBasic is false if not active', async () => {
|
||||
({ plugin, license } = await setup({
|
||||
license: {
|
||||
status: 'not-active',
|
||||
},
|
||||
}));
|
||||
|
||||
expect(license.isNotBasic).toBe(false);
|
||||
});
|
||||
|
||||
test('isNotBasic is true if active and not basic', async () => {
|
||||
({ plugin, license } = await setup({
|
||||
license: {
|
||||
type: 'gold',
|
||||
},
|
||||
}));
|
||||
|
||||
expect(license.isNotBasic).toBe(true);
|
||||
});
|
||||
|
||||
test('isNotBasic is false if active and basic', async () => {
|
||||
({ plugin, license } = await setup());
|
||||
|
||||
expect(license.isNotBasic).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
178
x-pack/plugins/licensing/server/license.ts
Normal file
178
x-pack/plugins/licensing/server/license.ts
Normal file
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { createHash } from 'crypto';
|
||||
import { LicenseFeature } from './license_feature';
|
||||
import { LICENSE_STATUS, LICENSE_TYPE } from './constants';
|
||||
import { LicenseType, ILicense } from './types';
|
||||
|
||||
function toLicenseType(minimumLicenseRequired: LICENSE_TYPE | string) {
|
||||
if (typeof minimumLicenseRequired !== 'string') {
|
||||
return minimumLicenseRequired;
|
||||
}
|
||||
|
||||
if (!(minimumLicenseRequired in LICENSE_TYPE)) {
|
||||
throw new Error(`${minimumLicenseRequired} is not a valid license type`);
|
||||
}
|
||||
|
||||
return LICENSE_TYPE[minimumLicenseRequired as LicenseType];
|
||||
}
|
||||
|
||||
export class License implements ILicense {
|
||||
private readonly hasLicense: boolean;
|
||||
private readonly license: any;
|
||||
private readonly features: any;
|
||||
private _signature!: string;
|
||||
private objectified!: any;
|
||||
private readonly featuresMap: Map<string, LicenseFeature>;
|
||||
|
||||
constructor(
|
||||
license: any,
|
||||
features: any,
|
||||
private error: Error | null,
|
||||
private clusterSource: string
|
||||
) {
|
||||
this.hasLicense = Boolean(license);
|
||||
this.license = license || {};
|
||||
this.features = features;
|
||||
this.featuresMap = new Map<string, LicenseFeature>();
|
||||
}
|
||||
|
||||
public get uid() {
|
||||
return this.license.uid;
|
||||
}
|
||||
|
||||
public get status() {
|
||||
return this.license.status;
|
||||
}
|
||||
|
||||
public get isActive() {
|
||||
return this.status === 'active';
|
||||
}
|
||||
|
||||
public get expiryDateInMillis() {
|
||||
return this.license.expiry_date_in_millis;
|
||||
}
|
||||
|
||||
public get type() {
|
||||
return this.license.type;
|
||||
}
|
||||
|
||||
public get isAvailable() {
|
||||
return this.hasLicense;
|
||||
}
|
||||
|
||||
public get isBasic() {
|
||||
return this.isActive && this.type === 'basic';
|
||||
}
|
||||
|
||||
public get isNotBasic() {
|
||||
return this.isActive && this.type !== 'basic';
|
||||
}
|
||||
|
||||
public get reasonUnavailable() {
|
||||
if (!this.isAvailable) {
|
||||
return `[${this.clusterSource}] Elasticsearch cluster did not respond with license information.`;
|
||||
}
|
||||
|
||||
if (this.error instanceof Error && (this.error as any).status === 400) {
|
||||
return `X-Pack plugin is not installed on the [${this.clusterSource}] Elasticsearch cluster.`;
|
||||
}
|
||||
|
||||
return this.error;
|
||||
}
|
||||
|
||||
public get signature() {
|
||||
if (this._signature !== undefined) {
|
||||
return this._signature;
|
||||
}
|
||||
|
||||
this._signature = createHash('md5')
|
||||
.update(JSON.stringify(this.toObject()))
|
||||
.digest('hex');
|
||||
|
||||
return this._signature;
|
||||
}
|
||||
|
||||
isOneOf(candidateLicenses: string | string[]) {
|
||||
if (!Array.isArray(candidateLicenses)) {
|
||||
candidateLicenses = [candidateLicenses];
|
||||
}
|
||||
|
||||
return candidateLicenses.includes(this.type);
|
||||
}
|
||||
|
||||
meetsMinimumOf(minimum: LICENSE_TYPE) {
|
||||
return LICENSE_TYPE[this.type as LicenseType] >= minimum;
|
||||
}
|
||||
|
||||
check(pluginName: string, minimumLicenseRequired: LICENSE_TYPE | string) {
|
||||
const minimum = toLicenseType(minimumLicenseRequired);
|
||||
|
||||
if (!this.isAvailable) {
|
||||
return {
|
||||
check: LICENSE_STATUS.Unavailable,
|
||||
message: i18n.translate('xpack.licensing.check.errorUnavailableMessage', {
|
||||
defaultMessage:
|
||||
'You cannot use {pluginName} because license information is not available at this time.',
|
||||
values: { pluginName },
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const { type: licenseType } = this.license;
|
||||
|
||||
if (!this.meetsMinimumOf(minimum)) {
|
||||
return {
|
||||
check: LICENSE_STATUS.Invalid,
|
||||
message: i18n.translate('xpack.licensing.check.errorUnsupportedMessage', {
|
||||
defaultMessage:
|
||||
'Your {licenseType} license does not support {pluginName}. Please upgrade your license.',
|
||||
values: { licenseType, pluginName },
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.isActive) {
|
||||
return {
|
||||
check: LICENSE_STATUS.Expired,
|
||||
message: i18n.translate('xpack.licensing.check.errorExpiredMessage', {
|
||||
defaultMessage:
|
||||
'You cannot use {pluginName} because your {licenseType} license has expired.',
|
||||
values: { licenseType, pluginName },
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return { check: LICENSE_STATUS.Valid };
|
||||
}
|
||||
|
||||
toObject() {
|
||||
if (this.objectified) {
|
||||
return this.objectified;
|
||||
}
|
||||
|
||||
this.objectified = {
|
||||
license: {
|
||||
type: this.type,
|
||||
isActive: this.isActive,
|
||||
expiryDateInMillis: this.expiryDateInMillis,
|
||||
},
|
||||
features: [...this.featuresMap].map(([, feature]) => feature.toObject()),
|
||||
};
|
||||
|
||||
return this.objectified;
|
||||
}
|
||||
|
||||
getFeature(name: string) {
|
||||
if (!this.featuresMap.has(name)) {
|
||||
this.featuresMap.set(name, new LicenseFeature(name, this.features[name], this));
|
||||
}
|
||||
|
||||
return this.featuresMap.get(name);
|
||||
}
|
||||
}
|
42
x-pack/plugins/licensing/server/license_feature.test.ts
Normal file
42
x-pack/plugins/licensing/server/license_feature.test.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { ILicense } from './types';
|
||||
import { Plugin } from './plugin';
|
||||
import { setup } from './__fixtures__/setup';
|
||||
|
||||
describe('licensing feature', () => {
|
||||
let plugin: Plugin;
|
||||
let license: ILicense;
|
||||
|
||||
afterEach(async () => {
|
||||
await plugin.stop();
|
||||
});
|
||||
|
||||
test('isAvailable', async () => {
|
||||
({ plugin, license } = await setup());
|
||||
|
||||
const security = license.getFeature('security');
|
||||
|
||||
expect(security!.isAvailable).toBe(true);
|
||||
});
|
||||
|
||||
test('isEnabled', async () => {
|
||||
({ plugin, license } = await setup());
|
||||
|
||||
const security = license.getFeature('security');
|
||||
|
||||
expect(security!.isEnabled).toBe(true);
|
||||
});
|
||||
|
||||
test('name', async () => {
|
||||
({ plugin, license } = await setup());
|
||||
|
||||
const security = license.getFeature('security');
|
||||
|
||||
expect(security!.name).toBe('security');
|
||||
});
|
||||
});
|
34
x-pack/plugins/licensing/server/license_feature.ts
Normal file
34
x-pack/plugins/licensing/server/license_feature.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { License } from './license';
|
||||
import { LicenseFeatureSerializer } from './types';
|
||||
|
||||
export class LicenseFeature {
|
||||
private serializable: LicenseFeatureSerializer = license => ({
|
||||
name: this.name,
|
||||
isAvailable: this.isAvailable,
|
||||
isEnabled: this.isEnabled,
|
||||
});
|
||||
|
||||
constructor(public name: string, private feature: any = {}, private license: License) {}
|
||||
|
||||
public get isAvailable() {
|
||||
return !!this.feature.available;
|
||||
}
|
||||
|
||||
public get isEnabled() {
|
||||
return !!this.feature.enabled;
|
||||
}
|
||||
|
||||
public onObject(serializable: LicenseFeatureSerializer) {
|
||||
this.serializable = serializable;
|
||||
}
|
||||
|
||||
public toObject() {
|
||||
return this.serializable(this.license);
|
||||
}
|
||||
}
|
23
x-pack/plugins/licensing/server/licensing_config.ts
Normal file
23
x-pack/plugins/licensing/server/licensing_config.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { PluginInitializerContext } from 'src/core/server';
|
||||
import { LicensingConfigType } from './types';
|
||||
|
||||
export class LicensingConfig {
|
||||
public isEnabled: boolean;
|
||||
public clusterSource: string;
|
||||
public pollingFrequency: number;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(rawConfig: LicensingConfigType, env: PluginInitializerContext['env']) {
|
||||
this.isEnabled = rawConfig.isEnabled;
|
||||
this.clusterSource = rawConfig.clusterSource;
|
||||
this.pollingFrequency = rawConfig.pollingFrequency;
|
||||
}
|
||||
}
|
83
x-pack/plugins/licensing/server/plugin.test.ts
Normal file
83
x-pack/plugins/licensing/server/plugin.test.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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 { take, skip } from 'rxjs/operators';
|
||||
import { ILicense } from './types';
|
||||
import { Plugin } from './plugin';
|
||||
import { License } from './license';
|
||||
import { setup, setupOnly, licenseMerge } from './__fixtures__/setup';
|
||||
|
||||
describe('licensing plugin', () => {
|
||||
let plugin: Plugin;
|
||||
let license: ILicense;
|
||||
|
||||
afterEach(async () => {
|
||||
await plugin.stop();
|
||||
});
|
||||
|
||||
test('returns instance of licensing setup', async () => {
|
||||
({ plugin, license } = await setup());
|
||||
expect(license).toBeInstanceOf(License);
|
||||
});
|
||||
|
||||
test('still returns instance of licensing setup when request fails', async () => {
|
||||
const { clusterClient, coreSetup, plugin: _plugin } = await setupOnly();
|
||||
|
||||
plugin = _plugin;
|
||||
clusterClient.callAsInternalUser.mockRejectedValue(new Error('test'));
|
||||
|
||||
const { license$ } = await plugin.setup(coreSetup);
|
||||
const finalLicense = await license$
|
||||
.pipe(
|
||||
skip(1),
|
||||
take(1)
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
expect(finalLicense).toBeInstanceOf(License);
|
||||
});
|
||||
|
||||
test('observable receives updated licenses', async () => {
|
||||
const { clusterClient, coreSetup, plugin: _plugin } = await setupOnly({
|
||||
config: {
|
||||
pollingFrequency: 100,
|
||||
},
|
||||
});
|
||||
const types = ['basic', 'gold', 'platinum'];
|
||||
let iterations = 0;
|
||||
|
||||
plugin = _plugin;
|
||||
clusterClient.callAsInternalUser.mockImplementation(() => {
|
||||
return Promise.resolve(
|
||||
licenseMerge({
|
||||
license: {
|
||||
type: types[iterations++],
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
const { license$ } = await plugin.setup(coreSetup);
|
||||
const licenseTypes: any[] = [];
|
||||
|
||||
await new Promise(resolve => {
|
||||
const subscription = license$.subscribe(next => {
|
||||
if (!next.type) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (iterations > 3) {
|
||||
subscription.unsubscribe();
|
||||
resolve();
|
||||
} else {
|
||||
licenseTypes.push(next.type);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
expect(licenseTypes).toEqual(['basic', 'gold', 'platinum']);
|
||||
});
|
||||
});
|
136
x-pack/plugins/licensing/server/plugin.ts
Normal file
136
x-pack/plugins/licensing/server/plugin.ts
Normal file
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* 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 { first, map } from 'rxjs/operators';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
CoreSetup,
|
||||
CoreStart,
|
||||
Logger,
|
||||
Plugin as CorePlugin,
|
||||
PluginInitializerContext,
|
||||
} from 'src/core/server';
|
||||
import { Poller } from '../../../../src/core/utils/poller';
|
||||
import { LicensingConfigType, LicensingPluginSetup, ILicense } from './types';
|
||||
import { LicensingConfig } from './licensing_config';
|
||||
import { License } from './license';
|
||||
|
||||
export class Plugin implements CorePlugin<LicensingPluginSetup> {
|
||||
private readonly logger: Logger;
|
||||
private readonly config$: Observable<LicensingConfig>;
|
||||
private poller!: Poller<ILicense>;
|
||||
|
||||
constructor(private readonly context: PluginInitializerContext) {
|
||||
this.logger = this.context.logger.get();
|
||||
this.config$ = this.context.config
|
||||
.create<LicensingConfigType | { config: LicensingConfigType }>()
|
||||
.pipe(
|
||||
map(config =>
|
||||
'config' in config
|
||||
? new LicensingConfig(config.config, this.context.env)
|
||||
: new LicensingConfig(config, this.context.env)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private hasLicenseInfoChanged(newLicense: any) {
|
||||
const currentLicense = this.poller.subject$.getValue();
|
||||
|
||||
if ((currentLicense && !newLicense) || (newLicense && !currentLicense)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
newLicense.type !== currentLicense.type ||
|
||||
newLicense.status !== currentLicense.status ||
|
||||
newLicense.expiry_date_in_millis !== currentLicense.expiryDateInMillis
|
||||
);
|
||||
}
|
||||
|
||||
private async fetchInfo(core: CoreSetup, clusterSource: string, pollingFrequency: number) {
|
||||
this.logger.debug(
|
||||
`Calling [${clusterSource}] Elasticsearch _xpack API. Polling frequency: ${pollingFrequency}`
|
||||
);
|
||||
|
||||
const cluster = await core.elasticsearch.dataClient$.pipe(first()).toPromise();
|
||||
|
||||
try {
|
||||
const response = await cluster.callAsInternalUser('transport.request', {
|
||||
method: 'GET',
|
||||
path: '/_xpack',
|
||||
});
|
||||
const rawLicense = response && response.license;
|
||||
const features = (response && response.features) || {};
|
||||
const licenseInfoChanged = this.hasLicenseInfoChanged(rawLicense);
|
||||
|
||||
if (!licenseInfoChanged) {
|
||||
return { license: false, error: null, features: null };
|
||||
}
|
||||
|
||||
const currentLicense = this.poller.subject$.getValue();
|
||||
const licenseInfo = [
|
||||
'type' in rawLicense && `type: ${rawLicense.type}`,
|
||||
'status' in rawLicense && `status: ${rawLicense.status}`,
|
||||
'expiry_date_in_millis' in rawLicense &&
|
||||
`expiry date: ${moment(rawLicense.expiry_date_in_millis, 'x').format()}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' | ');
|
||||
|
||||
this.logger.info(
|
||||
`Imported ${currentLicense ? 'changed ' : ''}license information` +
|
||||
` from Elasticsearch for the [${clusterSource}] cluster: ${licenseInfo}`
|
||||
);
|
||||
|
||||
return { license: rawLicense, error: null, features };
|
||||
} catch (err) {
|
||||
this.logger.warn(
|
||||
`License information could not be obtained from Elasticsearch` +
|
||||
` for the [${clusterSource}] cluster. ${err}`
|
||||
);
|
||||
|
||||
return { license: null, error: err, features: {} };
|
||||
}
|
||||
}
|
||||
|
||||
private create({ clusterSource, pollingFrequency }: LicensingConfig, core: CoreSetup) {
|
||||
this.poller = new Poller<ILicense>(
|
||||
pollingFrequency,
|
||||
new License(null, {}, null, clusterSource),
|
||||
async () => {
|
||||
const { license, features, error } = await this.fetchInfo(
|
||||
core,
|
||||
clusterSource,
|
||||
pollingFrequency
|
||||
);
|
||||
|
||||
if (license !== false) {
|
||||
return new License(license, features, error, clusterSource);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return this.poller;
|
||||
}
|
||||
|
||||
public async setup(core: CoreSetup) {
|
||||
const config = await this.config$.pipe(first()).toPromise();
|
||||
const poller = this.create(config, core);
|
||||
|
||||
return {
|
||||
license$: poller.subject$.asObservable(),
|
||||
};
|
||||
}
|
||||
|
||||
public async start(core: CoreStart) {}
|
||||
|
||||
public stop() {
|
||||
if (this.poller) {
|
||||
this.poller.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
14
x-pack/plugins/licensing/server/schema.ts
Normal file
14
x-pack/plugins/licensing/server/schema.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 { schema as Schema } from '@kbn/config-schema';
|
||||
import { DEFAULT_POLLING_FREQUENCY } from './constants';
|
||||
|
||||
export const schema = Schema.object({
|
||||
isEnabled: Schema.boolean({ defaultValue: true }),
|
||||
clusterSource: Schema.string({ defaultValue: 'data' }),
|
||||
pollingFrequency: Schema.number({ defaultValue: DEFAULT_POLLING_FREQUENCY }),
|
||||
});
|
120
x-pack/plugins/licensing/server/types.ts
Normal file
120
x-pack/plugins/licensing/server/types.ts
Normal file
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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 { TypeOf } from '@kbn/config-schema';
|
||||
import { schema } from './schema';
|
||||
import { LICENSE_TYPE, LICENSE_STATUS } from './constants';
|
||||
import { LicenseFeature } from './license_feature';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Results from checking if a particular license type meets the minimum
|
||||
* requirements of the license type.
|
||||
*/
|
||||
export interface ILicenseCheck {
|
||||
/**
|
||||
* The status of checking the results of a license type meeting the license minimum.
|
||||
*/
|
||||
check: LICENSE_STATUS;
|
||||
/**
|
||||
* A message containing the reason for a license type not being valid.
|
||||
*/
|
||||
message?: string;
|
||||
}
|
||||
/** @public */
|
||||
export interface ILicense {
|
||||
/**
|
||||
* UID for license.
|
||||
*/
|
||||
uid?: string;
|
||||
|
||||
/**
|
||||
* The validity status of the license.
|
||||
*/
|
||||
status?: string;
|
||||
|
||||
/**
|
||||
* Determine if the status of the license is active.
|
||||
*/
|
||||
isActive: boolean;
|
||||
|
||||
/**
|
||||
* Unix epoch of the expiration date of the license.
|
||||
*/
|
||||
expiryDateInMillis?: number;
|
||||
|
||||
/**
|
||||
* The license type, being usually one of basic, standard, gold, platinum, or trial.
|
||||
*/
|
||||
type?: string;
|
||||
|
||||
/**
|
||||
* Determine if the license container has information.
|
||||
*/
|
||||
isAvailable: boolean;
|
||||
|
||||
/**
|
||||
* Determine if the type of the license is basic, and also active.
|
||||
*/
|
||||
isBasic: boolean;
|
||||
|
||||
/**
|
||||
* Determine if the type of the license is not basic, and also active.
|
||||
*/
|
||||
isNotBasic: boolean;
|
||||
|
||||
/**
|
||||
* If the license is not available, provides a string or Error containing the reason.
|
||||
*/
|
||||
reasonUnavailable: string | Error | null;
|
||||
|
||||
/**
|
||||
* The MD5 hash of the serialized license.
|
||||
*/
|
||||
signature: string;
|
||||
|
||||
/**
|
||||
* Determine if the provided license types match against the license type.
|
||||
* @param candidateLicenses license types to intersect against the license.
|
||||
*/
|
||||
isOneOf(candidateLicenses: string | string[]): boolean;
|
||||
|
||||
/**
|
||||
* Determine if the provided license type is sufficient for the current license.
|
||||
* @param minimum a license type to determine for sufficiency
|
||||
*/
|
||||
meetsMinimumOf(minimum: LICENSE_TYPE): boolean;
|
||||
|
||||
/**
|
||||
* For a given plugin and license type, receive information about the status of the license.
|
||||
* @param pluginName the name of the plugin
|
||||
* @param minimumLicenseRequired the minimum valid license for operating the given plugin
|
||||
*/
|
||||
check(pluginName: string, minimumLicenseRequired: LICENSE_TYPE | string): ILicenseCheck;
|
||||
|
||||
/**
|
||||
* Receive a serialized plain object of the license.
|
||||
*/
|
||||
toObject(): any;
|
||||
|
||||
/**
|
||||
* A specific API for interacting with the specific features of the license.
|
||||
* @param name the name of the feature to interact with
|
||||
*/
|
||||
getFeature(name: string): LicenseFeature | undefined;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface LicensingPluginSetup {
|
||||
license$: Observable<ILicense>;
|
||||
}
|
||||
/** @public */
|
||||
export type LicensingConfigType = TypeOf<typeof schema>;
|
||||
/** @public */
|
||||
export type LicenseType = keyof typeof LICENSE_TYPE;
|
||||
/** @public */
|
||||
export type LicenseFeatureSerializer = (licensing: ILicense) => any;
|
|
@ -5491,6 +5491,9 @@
|
|||
"xpack.licenseMgmt.uploadLicense.uploadButtonLabel": "アップロード",
|
||||
"xpack.licenseMgmt.uploadLicense.uploadingButtonLabel": "アップロード中…",
|
||||
"xpack.licenseMgmt.uploadLicense.uploadLicenseTitle": "ライセンスのアップロード",
|
||||
"xpack.licensing.check.errorExpiredMessage": "{licenseType} ライセンスが期限切れのため {pluginName} を使用できません",
|
||||
"xpack.licensing.check.errorUnavailableMessage": "現在ライセンス情報が利用できないため {pluginName} を使用できません。",
|
||||
"xpack.licensing.check.errorUnsupportedMessage": "ご使用の {licenseType} ライセンスは {pluginName} をサポートしていません。ライセンスをアップグレードしてください。",
|
||||
"xpack.logstash.addRoleAlert.grantAdditionalPrivilegesDescription": "Kibana の管理で、Kibana ユーザーに {role} ロールを割り当ててください。",
|
||||
"xpack.logstash.addRoleAlert.grantAdditionalPrivilegesTitle": "追加権限の授与。",
|
||||
"xpack.logstash.alertCallOut.howToSeeAdditionalPipelinesDescription": "追加パイプラインを表示させる方法",
|
||||
|
@ -9779,9 +9782,6 @@
|
|||
"xpack.security.users.breadcrumb": "ユーザー",
|
||||
"xpack.security.users.createBreadcrumb": "作成",
|
||||
"xpack.security.management.editRole.featureTable.excludedFromBasePrivilegsTooltip": "アクセスを許可するには、「カスタム」特権を使用します。{featureName} は基本権限の一部ではありません。",
|
||||
"xpack.server.checkLicense.errorExpiredMessage": "{licenseType} ライセンスが期限切れのため {pluginName} を使用できません",
|
||||
"xpack.server.checkLicense.errorUnavailableMessage": "現在ライセンス情報が利用できないため {pluginName} を使用できません。",
|
||||
"xpack.server.checkLicense.errorUnsupportedMessage": "ご使用の {licenseType} ライセンスは {pluginName} をサポートしていません。ライセンスをアップグレードしてください。",
|
||||
"xpack.siem.add_to_kql.filterForValueHoverAction": "値でフィルターします",
|
||||
"xpack.siem.andOrBadge.and": "AND",
|
||||
"xpack.siem.andOrBadge.or": "OR",
|
||||
|
|
|
@ -5494,6 +5494,9 @@
|
|||
"xpack.licenseMgmt.uploadLicense.uploadButtonLabel": "上传",
|
||||
"xpack.licenseMgmt.uploadLicense.uploadingButtonLabel": "正在上传……",
|
||||
"xpack.licenseMgmt.uploadLicense.uploadLicenseTitle": "上传您的许可",
|
||||
"xpack.licensing.check.errorExpiredMessage": "您不能使用 {pluginName},因为您的 {licenseType} 许可证已过期",
|
||||
"xpack.licensing.check.errorUnavailableMessage": "您不能使用 {pluginName},因为许可证信息当前不可用。",
|
||||
"xpack.licensing.check.errorUnsupportedMessage": "您的 {licenseType} 许可证不支持 {pluginName}。请升级您的许可。",
|
||||
"xpack.logstash.addRoleAlert.grantAdditionalPrivilegesDescription": "在 Kibana“管理”中,将 {role} 角色分配给您的 Kibana 用户。",
|
||||
"xpack.logstash.addRoleAlert.grantAdditionalPrivilegesTitle": "授予其他权限。",
|
||||
"xpack.logstash.alertCallOut.howToSeeAdditionalPipelinesDescription": "我如何可以看到其他管道?",
|
||||
|
@ -9781,9 +9784,6 @@
|
|||
"xpack.security.users.breadcrumb": "用户",
|
||||
"xpack.security.users.createBreadcrumb": "创建",
|
||||
"xpack.security.management.editRole.featureTable.excludedFromBasePrivilegsTooltip": "使用“定制”权限来授予权限。{featureName} 不属于基础权限。",
|
||||
"xpack.server.checkLicense.errorExpiredMessage": "您不能使用 {pluginName},因为您的 {licenseType} 许可证已过期",
|
||||
"xpack.server.checkLicense.errorUnavailableMessage": "您不能使用 {pluginName},因为许可证信息当前不可用。",
|
||||
"xpack.server.checkLicense.errorUnsupportedMessage": "您的 {licenseType} 许可证不支持 {pluginName}。请升级您的许可。",
|
||||
"xpack.siem.add_to_kql.filterForValueHoverAction": "筛留值",
|
||||
"xpack.siem.andOrBadge.and": "AND",
|
||||
"xpack.siem.andOrBadge.or": "OR",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue