mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
This reverts commit 21c0b0b420
.
This commit is contained in:
parent
788d302396
commit
d7ab75ffc5
8 changed files with 211 additions and 387 deletions
|
@ -17,7 +17,6 @@ import {
|
|||
takeUntil,
|
||||
finalize,
|
||||
startWith,
|
||||
throttleTime,
|
||||
} from 'rxjs/operators';
|
||||
import { hasLicenseInfoChanged } from './has_license_info_changed';
|
||||
import type { ILicense } from './types';
|
||||
|
@ -30,15 +29,11 @@ export function createLicenseUpdate(
|
|||
) {
|
||||
const manuallyRefresh$ = new Subject<void>();
|
||||
|
||||
const fetched$ = merge(
|
||||
triggerRefresh$,
|
||||
manuallyRefresh$.pipe(
|
||||
throttleTime(1000, undefined, {
|
||||
leading: true,
|
||||
trailing: true,
|
||||
})
|
||||
)
|
||||
).pipe(takeUntil(stop$), exhaustMap(fetcher), share());
|
||||
const fetched$ = merge(triggerRefresh$, manuallyRefresh$).pipe(
|
||||
takeUntil(stop$),
|
||||
exhaustMap(fetcher),
|
||||
share()
|
||||
);
|
||||
|
||||
// provide a first, empty license, so that we can compare in the filter below
|
||||
const startWithArgs = initialValues ? [undefined, initialValues] : [undefined];
|
||||
|
|
|
@ -1,172 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { getLicenseFetcher } from './license_fetcher';
|
||||
import { loggerMock, type MockedLogger } from '@kbn/logging-mocks';
|
||||
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
|
||||
type EsLicense = estypes.XpackInfoMinimalLicenseInformation;
|
||||
|
||||
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
|
||||
|
||||
function buildRawLicense(options: Partial<EsLicense> = {}): EsLicense {
|
||||
return {
|
||||
uid: 'uid-000000001234',
|
||||
status: 'active',
|
||||
type: 'basic',
|
||||
mode: 'basic',
|
||||
expiry_date_in_millis: 1000,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
describe('LicenseFetcher', () => {
|
||||
let logger: MockedLogger;
|
||||
let clusterClient: ReturnType<typeof elasticsearchServiceMock.createClusterClient>;
|
||||
|
||||
beforeEach(() => {
|
||||
logger = loggerMock.create();
|
||||
clusterClient = elasticsearchServiceMock.createClusterClient();
|
||||
});
|
||||
|
||||
it('returns the license for successful calls', async () => {
|
||||
clusterClient.asInternalUser.xpack.info.mockResponse({
|
||||
license: buildRawLicense({
|
||||
uid: 'license-1',
|
||||
}),
|
||||
features: {},
|
||||
} as any);
|
||||
|
||||
const fetcher = getLicenseFetcher({
|
||||
logger,
|
||||
clusterClient,
|
||||
cacheDurationMs: 50_000,
|
||||
});
|
||||
|
||||
const license = await fetcher();
|
||||
expect(license.uid).toEqual('license-1');
|
||||
});
|
||||
|
||||
it('returns the latest license for successful calls', async () => {
|
||||
clusterClient.asInternalUser.xpack.info
|
||||
.mockResponseOnce({
|
||||
license: buildRawLicense({
|
||||
uid: 'license-1',
|
||||
}),
|
||||
features: {},
|
||||
} as any)
|
||||
.mockResponseOnce({
|
||||
license: buildRawLicense({
|
||||
uid: 'license-2',
|
||||
}),
|
||||
features: {},
|
||||
} as any);
|
||||
|
||||
const fetcher = getLicenseFetcher({
|
||||
logger,
|
||||
clusterClient,
|
||||
cacheDurationMs: 50_000,
|
||||
});
|
||||
|
||||
let license = await fetcher();
|
||||
expect(license.uid).toEqual('license-1');
|
||||
|
||||
license = await fetcher();
|
||||
expect(license.uid).toEqual('license-2');
|
||||
});
|
||||
|
||||
it('returns an error license in case of error', async () => {
|
||||
clusterClient.asInternalUser.xpack.info.mockResponseImplementation(() => {
|
||||
throw new Error('woups');
|
||||
});
|
||||
|
||||
const fetcher = getLicenseFetcher({
|
||||
logger,
|
||||
clusterClient,
|
||||
cacheDurationMs: 50_000,
|
||||
});
|
||||
|
||||
const license = await fetcher();
|
||||
expect(license.error).toEqual('woups');
|
||||
});
|
||||
|
||||
it('returns a license successfully fetched after an error', async () => {
|
||||
clusterClient.asInternalUser.xpack.info
|
||||
.mockResponseImplementationOnce(() => {
|
||||
throw new Error('woups');
|
||||
})
|
||||
.mockResponseOnce({
|
||||
license: buildRawLicense({
|
||||
uid: 'license-1',
|
||||
}),
|
||||
features: {},
|
||||
} as any);
|
||||
|
||||
const fetcher = getLicenseFetcher({
|
||||
logger,
|
||||
clusterClient,
|
||||
cacheDurationMs: 50_000,
|
||||
});
|
||||
|
||||
let license = await fetcher();
|
||||
expect(license.error).toEqual('woups');
|
||||
license = await fetcher();
|
||||
expect(license.uid).toEqual('license-1');
|
||||
});
|
||||
|
||||
it('returns the latest fetched license after an error within the cache duration period', async () => {
|
||||
clusterClient.asInternalUser.xpack.info
|
||||
.mockResponseOnce({
|
||||
license: buildRawLicense({
|
||||
uid: 'license-1',
|
||||
}),
|
||||
features: {},
|
||||
} as any)
|
||||
.mockResponseImplementationOnce(() => {
|
||||
throw new Error('woups');
|
||||
});
|
||||
|
||||
const fetcher = getLicenseFetcher({
|
||||
logger,
|
||||
clusterClient,
|
||||
cacheDurationMs: 50_000,
|
||||
});
|
||||
|
||||
let license = await fetcher();
|
||||
expect(license.uid).toEqual('license-1');
|
||||
license = await fetcher();
|
||||
expect(license.uid).toEqual('license-1');
|
||||
});
|
||||
|
||||
it('returns an error license after an error exceeding the cache duration period', async () => {
|
||||
clusterClient.asInternalUser.xpack.info
|
||||
.mockResponseOnce({
|
||||
license: buildRawLicense({
|
||||
uid: 'license-1',
|
||||
}),
|
||||
features: {},
|
||||
} as any)
|
||||
.mockResponseImplementationOnce(() => {
|
||||
throw new Error('woups');
|
||||
});
|
||||
|
||||
const fetcher = getLicenseFetcher({
|
||||
logger,
|
||||
clusterClient,
|
||||
cacheDurationMs: 1,
|
||||
});
|
||||
|
||||
let license = await fetcher();
|
||||
expect(license.uid).toEqual('license-1');
|
||||
|
||||
await delay(50);
|
||||
|
||||
license = await fetcher();
|
||||
expect(license.error).toEqual('woups');
|
||||
});
|
||||
});
|
|
@ -1,133 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { createHash } from 'crypto';
|
||||
import stringify from 'json-stable-stringify';
|
||||
import type { MaybePromise } from '@kbn/utility-types';
|
||||
import { isPromise } from '@kbn/std';
|
||||
import type { IClusterClient, Logger } from '@kbn/core/server';
|
||||
import type {
|
||||
ILicense,
|
||||
PublicLicense,
|
||||
PublicFeatures,
|
||||
LicenseType,
|
||||
LicenseStatus,
|
||||
} from '../common/types';
|
||||
import { License } from '../common/license';
|
||||
import type { ElasticsearchError, LicenseFetcher } from './types';
|
||||
|
||||
export const getLicenseFetcher = ({
|
||||
clusterClient,
|
||||
logger,
|
||||
cacheDurationMs,
|
||||
}: {
|
||||
clusterClient: MaybePromise<IClusterClient>;
|
||||
logger: Logger;
|
||||
cacheDurationMs: number;
|
||||
}): LicenseFetcher => {
|
||||
let currentLicense: ILicense | undefined;
|
||||
let lastSuccessfulFetchTime: number | undefined;
|
||||
|
||||
return async () => {
|
||||
const client = isPromise(clusterClient) ? await clusterClient : clusterClient;
|
||||
try {
|
||||
const response = await client.asInternalUser.xpack.info();
|
||||
const normalizedLicense =
|
||||
response.license && response.license.type !== 'missing'
|
||||
? normalizeServerLicense(response.license)
|
||||
: undefined;
|
||||
const normalizedFeatures = response.features
|
||||
? normalizeFeatures(response.features)
|
||||
: undefined;
|
||||
|
||||
const signature = sign({
|
||||
license: normalizedLicense,
|
||||
features: normalizedFeatures,
|
||||
error: '',
|
||||
});
|
||||
|
||||
currentLicense = new License({
|
||||
license: normalizedLicense,
|
||||
features: normalizedFeatures,
|
||||
signature,
|
||||
});
|
||||
lastSuccessfulFetchTime = Date.now();
|
||||
|
||||
return currentLicense;
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`License information could not be obtained from Elasticsearch due to ${error} error`
|
||||
);
|
||||
|
||||
if (lastSuccessfulFetchTime && lastSuccessfulFetchTime + cacheDurationMs > Date.now()) {
|
||||
return currentLicense!;
|
||||
} else {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
const signature = sign({ error: errorMessage });
|
||||
|
||||
return new License({
|
||||
error: getErrorMessage(error),
|
||||
signature,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
function normalizeServerLicense(
|
||||
license: estypes.XpackInfoMinimalLicenseInformation
|
||||
): PublicLicense {
|
||||
return {
|
||||
uid: license.uid,
|
||||
type: license.type as LicenseType,
|
||||
mode: license.mode as LicenseType,
|
||||
expiryDateInMillis:
|
||||
typeof license.expiry_date_in_millis === 'string'
|
||||
? parseInt(license.expiry_date_in_millis, 10)
|
||||
: license.expiry_date_in_millis,
|
||||
status: license.status as LicenseStatus,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeFeatures(rawFeatures: estypes.XpackInfoFeatures) {
|
||||
const features: PublicFeatures = {};
|
||||
for (const [name, feature] of Object.entries(rawFeatures)) {
|
||||
features[name] = {
|
||||
isAvailable: feature.available,
|
||||
isEnabled: feature.enabled,
|
||||
};
|
||||
}
|
||||
return features;
|
||||
}
|
||||
|
||||
function sign({
|
||||
license,
|
||||
features,
|
||||
error,
|
||||
}: {
|
||||
license?: PublicLicense;
|
||||
features?: PublicFeatures;
|
||||
error?: string;
|
||||
}) {
|
||||
return createHash('sha256')
|
||||
.update(
|
||||
stringify({
|
||||
license,
|
||||
features,
|
||||
error,
|
||||
})
|
||||
)
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
function getErrorMessage(error: ElasticsearchError): string {
|
||||
if (error.status === 400) {
|
||||
return 'X-Pack plugin is not installed on the Elasticsearch cluster.';
|
||||
}
|
||||
return error.message;
|
||||
}
|
|
@ -10,18 +10,12 @@ import { PluginConfigDescriptor } from '@kbn/core/server';
|
|||
|
||||
const configSchema = schema.object({
|
||||
api_polling_frequency: schema.duration({ defaultValue: '30s' }),
|
||||
license_cache_duration: schema.duration({
|
||||
defaultValue: '300s',
|
||||
validate: (value) => {
|
||||
if (value.asMinutes() > 15) {
|
||||
return 'license cache duration must be shorter than 15 minutes';
|
||||
}
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
export type LicenseConfigType = TypeOf<typeof configSchema>;
|
||||
|
||||
export const config: PluginConfigDescriptor<LicenseConfigType> = {
|
||||
schema: configSchema,
|
||||
schema: schema.object({
|
||||
api_polling_frequency: schema.duration({ defaultValue: '30s' }),
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -56,23 +56,22 @@ describe('licensing plugin', () => {
|
|||
return client;
|
||||
};
|
||||
|
||||
let plugin: LicensingPlugin;
|
||||
let pluginInitContextMock: ReturnType<typeof coreMock.createPluginInitializerContext>;
|
||||
|
||||
beforeEach(() => {
|
||||
pluginInitContextMock = coreMock.createPluginInitializerContext({
|
||||
api_polling_frequency: moment.duration(100),
|
||||
license_cache_duration: moment.duration(1000),
|
||||
});
|
||||
plugin = new LicensingPlugin(pluginInitContextMock);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await plugin?.stop();
|
||||
});
|
||||
|
||||
describe('#start', () => {
|
||||
describe('#license$', () => {
|
||||
let plugin: LicensingPlugin;
|
||||
let pluginInitContextMock: ReturnType<typeof coreMock.createPluginInitializerContext>;
|
||||
|
||||
beforeEach(() => {
|
||||
pluginInitContextMock = coreMock.createPluginInitializerContext({
|
||||
api_polling_frequency: moment.duration(100),
|
||||
});
|
||||
plugin = new LicensingPlugin(pluginInitContextMock);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await plugin.stop();
|
||||
});
|
||||
|
||||
it('returns license', async () => {
|
||||
const esClient = createEsClient({
|
||||
license: buildRawLicense(),
|
||||
|
@ -80,8 +79,8 @@ describe('licensing plugin', () => {
|
|||
});
|
||||
|
||||
const coreSetup = createCoreSetupWith(esClient);
|
||||
plugin.setup(coreSetup);
|
||||
const { license$ } = plugin.start();
|
||||
await plugin.setup(coreSetup);
|
||||
const { license$ } = await plugin.start();
|
||||
const license = await firstValueFrom(license$);
|
||||
expect(license.isAvailable).toBe(true);
|
||||
});
|
||||
|
@ -93,8 +92,8 @@ describe('licensing plugin', () => {
|
|||
});
|
||||
|
||||
const coreSetup = createCoreSetupWith(esClient);
|
||||
plugin.setup(coreSetup);
|
||||
const { license$ } = plugin.start();
|
||||
await plugin.setup(coreSetup);
|
||||
const { license$ } = await plugin.start();
|
||||
await firstValueFrom(license$);
|
||||
|
||||
expect(esClient.asInternalUser.xpack.info).toHaveBeenCalledTimes(1);
|
||||
|
@ -112,8 +111,8 @@ describe('licensing plugin', () => {
|
|||
});
|
||||
|
||||
const coreSetup = createCoreSetupWith(esClient);
|
||||
plugin.setup(coreSetup);
|
||||
const { license$ } = plugin.start();
|
||||
await plugin.setup(coreSetup);
|
||||
const { license$ } = await plugin.start();
|
||||
const [first, second, third] = await firstValueFrom(license$.pipe(take(3), toArray()));
|
||||
|
||||
expect(first.type).toBe('basic');
|
||||
|
@ -126,8 +125,8 @@ describe('licensing plugin', () => {
|
|||
esClient.asInternalUser.xpack.info.mockRejectedValue(new Error('test'));
|
||||
|
||||
const coreSetup = createCoreSetupWith(esClient);
|
||||
plugin.setup(coreSetup);
|
||||
const { license$ } = plugin.start();
|
||||
await plugin.setup(coreSetup);
|
||||
const { license$ } = await plugin.start();
|
||||
|
||||
const license = await firstValueFrom(license$);
|
||||
expect(license.isAvailable).toBe(false);
|
||||
|
@ -141,8 +140,8 @@ describe('licensing plugin', () => {
|
|||
esClient.asInternalUser.xpack.info.mockRejectedValue(error);
|
||||
|
||||
const coreSetup = createCoreSetupWith(esClient);
|
||||
plugin.setup(coreSetup);
|
||||
const { license$ } = plugin.start();
|
||||
await plugin.setup(coreSetup);
|
||||
const { license$ } = await plugin.start();
|
||||
|
||||
const license = await firstValueFrom(license$);
|
||||
expect(license.isAvailable).toBe(false);
|
||||
|
@ -170,8 +169,8 @@ describe('licensing plugin', () => {
|
|||
});
|
||||
|
||||
const coreSetup = createCoreSetupWith(esClient);
|
||||
plugin.setup(coreSetup);
|
||||
const { license$ } = plugin.start();
|
||||
await plugin.setup(coreSetup);
|
||||
const { license$ } = await plugin.start();
|
||||
|
||||
const [first, second, third] = await firstValueFrom(license$.pipe(take(3), toArray()));
|
||||
|
||||
|
@ -187,8 +186,8 @@ describe('licensing plugin', () => {
|
|||
});
|
||||
|
||||
const coreSetup = createCoreSetupWith(esClient);
|
||||
plugin.setup(coreSetup);
|
||||
plugin.start();
|
||||
await plugin.setup(coreSetup);
|
||||
await plugin.start();
|
||||
|
||||
await flushPromises();
|
||||
|
||||
|
@ -202,8 +201,8 @@ describe('licensing plugin', () => {
|
|||
});
|
||||
|
||||
const coreSetup = createCoreSetupWith(esClient);
|
||||
plugin.setup(coreSetup);
|
||||
plugin.start();
|
||||
await plugin.setup(coreSetup);
|
||||
await plugin.start();
|
||||
|
||||
await flushPromises();
|
||||
|
||||
|
@ -230,8 +229,8 @@ describe('licensing plugin', () => {
|
|||
});
|
||||
|
||||
const coreSetup = createCoreSetupWith(esClient);
|
||||
plugin.setup(coreSetup);
|
||||
const { license$ } = plugin.start();
|
||||
await plugin.setup(coreSetup);
|
||||
const { license$ } = await plugin.start();
|
||||
|
||||
const [first, second, third] = await firstValueFrom(license$.pipe(take(3), toArray()));
|
||||
expect(first.signature === third.signature).toBe(true);
|
||||
|
@ -240,12 +239,16 @@ describe('licensing plugin', () => {
|
|||
});
|
||||
|
||||
describe('#refresh', () => {
|
||||
let plugin: LicensingPlugin;
|
||||
afterEach(async () => {
|
||||
await plugin.stop();
|
||||
});
|
||||
|
||||
it('forces refresh immediately', async () => {
|
||||
plugin = new LicensingPlugin(
|
||||
coreMock.createPluginInitializerContext({
|
||||
// disable polling mechanism
|
||||
api_polling_frequency: moment.duration(50000),
|
||||
license_cache_duration: moment.duration(1000),
|
||||
})
|
||||
);
|
||||
const esClient = createEsClient({
|
||||
|
@ -254,26 +257,31 @@ describe('licensing plugin', () => {
|
|||
});
|
||||
|
||||
const coreSetup = createCoreSetupWith(esClient);
|
||||
plugin.setup(coreSetup);
|
||||
const { refresh, license$ } = plugin.start();
|
||||
await plugin.setup(coreSetup);
|
||||
const { refresh, license$ } = await plugin.start();
|
||||
|
||||
expect(esClient.asInternalUser.xpack.info).toHaveBeenCalledTimes(0);
|
||||
|
||||
await firstValueFrom(license$);
|
||||
await license$.pipe(take(1)).toPromise();
|
||||
expect(esClient.asInternalUser.xpack.info).toHaveBeenCalledTimes(1);
|
||||
|
||||
await refresh();
|
||||
refresh();
|
||||
await flushPromises();
|
||||
expect(esClient.asInternalUser.xpack.info).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
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),
|
||||
license_cache_duration: moment.duration(1000),
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -282,8 +290,8 @@ describe('licensing plugin', () => {
|
|||
features: {},
|
||||
});
|
||||
const coreSetup = createCoreSetupWith(esClient);
|
||||
plugin.setup(coreSetup);
|
||||
const { createLicensePoller, license$ } = plugin.start();
|
||||
await plugin.setup(coreSetup);
|
||||
const { createLicensePoller, license$ } = await plugin.start();
|
||||
|
||||
const customClient = createEsClient({
|
||||
license: buildRawLicense({ type: 'gold' }),
|
||||
|
@ -305,13 +313,19 @@ describe('licensing plugin', () => {
|
|||
expect(customLicense.isAvailable).toBe(true);
|
||||
expect(customLicense.type).toBe('gold');
|
||||
|
||||
expect(await firstValueFrom(license$)).not.toBe(customLicense);
|
||||
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();
|
||||
plugin.setup(coreSetup);
|
||||
const { createLicensePoller } = plugin.start();
|
||||
await plugin.setup(coreSetup);
|
||||
const { createLicensePoller } = await plugin.start();
|
||||
|
||||
const customClient = createEsClient({
|
||||
license: buildRawLicense({ type: 'gold' }),
|
||||
|
@ -330,10 +344,24 @@ describe('licensing plugin', () => {
|
|||
});
|
||||
|
||||
describe('extends core contexts', () => {
|
||||
let plugin: LicensingPlugin;
|
||||
|
||||
beforeEach(() => {
|
||||
plugin = new LicensingPlugin(
|
||||
coreMock.createPluginInitializerContext({
|
||||
api_polling_frequency: moment.duration(100),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await plugin.stop();
|
||||
});
|
||||
|
||||
it('provides a licensing context to http routes', async () => {
|
||||
const coreSetup = coreMock.createSetup();
|
||||
|
||||
plugin.setup(coreSetup);
|
||||
await plugin.setup(coreSetup);
|
||||
|
||||
expect(coreSetup.http.registerRouteHandlerContext.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -347,10 +375,22 @@ describe('licensing plugin', () => {
|
|||
});
|
||||
|
||||
describe('registers on pre-response interceptor', () => {
|
||||
let plugin: LicensingPlugin;
|
||||
|
||||
beforeEach(() => {
|
||||
plugin = new LicensingPlugin(
|
||||
coreMock.createPluginInitializerContext({ api_polling_frequency: moment.duration(100) })
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await plugin.stop();
|
||||
});
|
||||
|
||||
it('once', async () => {
|
||||
const coreSetup = coreMock.createSetup();
|
||||
|
||||
plugin.setup(coreSetup);
|
||||
await plugin.setup(coreSetup);
|
||||
|
||||
expect(coreSetup.http.registerOnPreResponse).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
@ -359,9 +399,14 @@ describe('licensing plugin', () => {
|
|||
|
||||
describe('#stop', () => {
|
||||
it('stops polling', async () => {
|
||||
const plugin = new LicensingPlugin(
|
||||
coreMock.createPluginInitializerContext({
|
||||
api_polling_frequency: moment.duration(100),
|
||||
})
|
||||
);
|
||||
const coreSetup = coreMock.createSetup();
|
||||
plugin.setup(coreSetup);
|
||||
const { license$ } = plugin.start();
|
||||
await plugin.setup(coreSetup);
|
||||
const { license$ } = await plugin.start();
|
||||
|
||||
let completed = false;
|
||||
license$.subscribe({ complete: () => (completed = true) });
|
||||
|
|
|
@ -8,7 +8,12 @@
|
|||
import type { Observable, Subject, Subscription } from 'rxjs';
|
||||
import { ReplaySubject, timer } from 'rxjs';
|
||||
import moment from 'moment';
|
||||
import { createHash } from 'crypto';
|
||||
import stringify from 'json-stable-stringify';
|
||||
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { MaybePromise } from '@kbn/utility-types';
|
||||
import { isPromise } from '@kbn/std';
|
||||
import type {
|
||||
CoreSetup,
|
||||
Logger,
|
||||
|
@ -16,17 +21,73 @@ import type {
|
|||
PluginInitializerContext,
|
||||
IClusterClient,
|
||||
} from '@kbn/core/server';
|
||||
|
||||
import { registerAnalyticsContextProvider } from '../common/register_analytics_context_provider';
|
||||
import type { ILicense } from '../common/types';
|
||||
import type {
|
||||
ILicense,
|
||||
PublicLicense,
|
||||
PublicFeatures,
|
||||
LicenseType,
|
||||
LicenseStatus,
|
||||
} from '../common/types';
|
||||
import type { LicensingPluginSetup, LicensingPluginStart } from './types';
|
||||
import { License } from '../common/license';
|
||||
import { createLicenseUpdate } from '../common/license_update';
|
||||
|
||||
import type { ElasticsearchError } from './types';
|
||||
import { registerRoutes } from './routes';
|
||||
import { FeatureUsageService } from './services';
|
||||
|
||||
import type { LicenseConfigType } from './licensing_config';
|
||||
import { createRouteHandlerContext } from './licensing_route_handler_context';
|
||||
import { createOnPreResponseHandler } from './on_pre_response_handler';
|
||||
import { getPluginStatus$ } from './plugin_status';
|
||||
import { getLicenseFetcher } from './license_fetcher';
|
||||
|
||||
function normalizeServerLicense(
|
||||
license: estypes.XpackInfoMinimalLicenseInformation
|
||||
): PublicLicense {
|
||||
return {
|
||||
uid: license.uid,
|
||||
type: license.type as LicenseType,
|
||||
mode: license.mode as LicenseType,
|
||||
expiryDateInMillis:
|
||||
typeof license.expiry_date_in_millis === 'string'
|
||||
? parseInt(license.expiry_date_in_millis, 10)
|
||||
: license.expiry_date_in_millis,
|
||||
status: license.status as LicenseStatus,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeFeatures(rawFeatures: estypes.XpackInfoFeatures) {
|
||||
const features: PublicFeatures = {};
|
||||
for (const [name, feature] of Object.entries(rawFeatures)) {
|
||||
features[name] = {
|
||||
isAvailable: feature.available,
|
||||
isEnabled: feature.enabled,
|
||||
};
|
||||
}
|
||||
return features;
|
||||
}
|
||||
|
||||
function sign({
|
||||
license,
|
||||
features,
|
||||
error,
|
||||
}: {
|
||||
license?: PublicLicense;
|
||||
features?: PublicFeatures;
|
||||
error?: string;
|
||||
}) {
|
||||
return createHash('sha256')
|
||||
.update(
|
||||
stringify({
|
||||
license,
|
||||
features,
|
||||
error,
|
||||
})
|
||||
)
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
|
@ -92,16 +153,9 @@ export class LicensingPlugin implements Plugin<LicensingPluginSetup, LicensingPl
|
|||
this.logger.debug(`Polling Elasticsearch License API with frequency ${pollingFrequency}ms.`);
|
||||
|
||||
const intervalRefresh$ = timer(0, pollingFrequency);
|
||||
const licenseFetcher = getLicenseFetcher({
|
||||
clusterClient,
|
||||
logger: this.logger,
|
||||
cacheDurationMs: this.config.license_cache_duration.asMilliseconds(),
|
||||
});
|
||||
|
||||
const { license$, refreshManually } = createLicenseUpdate(
|
||||
intervalRefresh$,
|
||||
this.stop$,
|
||||
licenseFetcher
|
||||
const { license$, refreshManually } = createLicenseUpdate(intervalRefresh$, this.stop$, () =>
|
||||
this.fetchLicense(clusterClient)
|
||||
);
|
||||
|
||||
this.loggingSubscription = license$.subscribe((license) =>
|
||||
|
@ -124,6 +178,50 @@ export class LicensingPlugin implements Plugin<LicensingPluginSetup, LicensingPl
|
|||
};
|
||||
}
|
||||
|
||||
private fetchLicense = async (clusterClient: MaybePromise<IClusterClient>): Promise<ILicense> => {
|
||||
const client = isPromise(clusterClient) ? await clusterClient : clusterClient;
|
||||
try {
|
||||
const response = await client.asInternalUser.xpack.info();
|
||||
const normalizedLicense =
|
||||
response.license && response.license.type !== 'missing'
|
||||
? normalizeServerLicense(response.license)
|
||||
: undefined;
|
||||
const normalizedFeatures = response.features
|
||||
? normalizeFeatures(response.features)
|
||||
: undefined;
|
||||
|
||||
const signature = sign({
|
||||
license: normalizedLicense,
|
||||
features: normalizedFeatures,
|
||||
error: '',
|
||||
});
|
||||
|
||||
return new License({
|
||||
license: normalizedLicense,
|
||||
features: normalizedFeatures,
|
||||
signature,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`License information could not be obtained from Elasticsearch due to ${error} error`
|
||||
);
|
||||
const errorMessage = this.getErrorMessage(error);
|
||||
const signature = sign({ error: errorMessage });
|
||||
|
||||
return new License({
|
||||
error: this.getErrorMessage(error),
|
||||
signature,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private getErrorMessage(error: ElasticsearchError): string {
|
||||
if (error.status === 400) {
|
||||
return 'X-Pack plugin is not installed on the Elasticsearch cluster.';
|
||||
}
|
||||
return error.message;
|
||||
}
|
||||
|
||||
public start() {
|
||||
if (!this.refresh || !this.license$) {
|
||||
throw new Error('Setup has not been completed');
|
||||
|
|
|
@ -14,8 +14,6 @@ export interface ElasticsearchError extends Error {
|
|||
status?: number;
|
||||
}
|
||||
|
||||
export type LicenseFetcher = () => Promise<ILicense>;
|
||||
|
||||
/**
|
||||
* Result from remote request fetching raw feature set.
|
||||
* @internal
|
||||
|
|
|
@ -15,8 +15,7 @@
|
|||
"@kbn/i18n",
|
||||
"@kbn/analytics-client",
|
||||
"@kbn/subscription-tracking",
|
||||
"@kbn/core-analytics-browser",
|
||||
"@kbn/logging-mocks"
|
||||
"@kbn/core-analytics-browser"
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue