From 5dac38c6eb5f51cf7477f5e32740084d5edd3d62 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Tue, 26 May 2020 17:50:44 +0300 Subject: [PATCH] Licensing uses es from start contract. (#67291) * use es client provided from start * expose licengin API from start contract and deprecate setup counterparts * update tests * provide licensing API from start contract on the client-side * update tests * update mocks --- x-pack/plugins/licensing/public/index.ts | 2 +- .../plugins/licensing/public/plugin.test.ts | 46 +++--- x-pack/plugins/licensing/public/plugin.ts | 19 ++- x-pack/plugins/licensing/public/types.ts | 14 ++ x-pack/plugins/licensing/server/mocks.ts | 10 ++ .../plugins/licensing/server/plugin.test.ts | 140 ++++++++++-------- x-pack/plugins/licensing/server/plugin.ts | 42 +++++- x-pack/plugins/licensing/server/types.ts | 20 +++ 8 files changed, 207 insertions(+), 86 deletions(-) diff --git a/x-pack/plugins/licensing/public/index.ts b/x-pack/plugins/licensing/public/index.ts index e19ebe7a68418..687109f7aaf2a 100644 --- a/x-pack/plugins/licensing/public/index.ts +++ b/x-pack/plugins/licensing/public/index.ts @@ -8,5 +8,5 @@ import { PluginInitializerContext } from 'src/core/public'; import { LicensingPlugin } from './plugin'; export * from '../common/types'; -export * from './types'; +export { LicensingPluginSetup, LicensingPluginStart } from './types'; export const plugin = (context: PluginInitializerContext) => new LicensingPlugin(context); diff --git a/x-pack/plugins/licensing/public/plugin.test.ts b/x-pack/plugins/licensing/public/plugin.test.ts index a999b3993dc0c..564f2fdb21116 100644 --- a/x-pack/plugins/licensing/public/plugin.test.ts +++ b/x-pack/plugins/licensing/public/plugin.test.ts @@ -15,6 +15,7 @@ import { licenseMock } from '../common/licensing.mock'; import { coreMock } from '../../../../src/core/public/mocks'; import { HttpInterceptor } from 'src/core/public'; +const coreStart = coreMock.createStart(); describe('licensing plugin', () => { let plugin: LicensingPlugin; @@ -23,7 +24,7 @@ describe('licensing plugin', () => { await plugin.stop(); }); - describe('#setup', () => { + describe('#start', () => { describe('#refresh', () => { it('forces data re-fetch', async () => { const sessionStorage = coreMock.createStorage(); @@ -38,7 +39,8 @@ describe('licensing plugin', () => { }); coreSetup.http.get.mockResolvedValueOnce(firstLicense).mockResolvedValueOnce(secondLicense); - const { license$, refresh } = await plugin.setup(coreSetup); + await plugin.setup(coreSetup); + const { license$, refresh } = await plugin.start(coreStart); let fromObservable; license$.subscribe((license) => (fromObservable = license)); @@ -60,7 +62,8 @@ describe('licensing plugin', () => { const fetchedLicense = licenseMock.createLicense(); coreSetup.http.get.mockResolvedValue(fetchedLicense); - const { refresh } = await plugin.setup(coreSetup); + await plugin.setup(coreSetup); + const { refresh } = await plugin.start(coreStart); await refresh(); @@ -78,7 +81,8 @@ describe('licensing plugin', () => { plugin = new LicensingPlugin(coreMock.createPluginInitializerContext(), sessionStorage); const coreSetup = coreMock.createSetup(); - const { license$ } = await plugin.setup(coreSetup); + await plugin.setup(coreSetup); + const { license$ } = await plugin.start(coreStart); const license = await license$.pipe(take(1)).toPromise(); expect(license.isAvailable).toBe(true); @@ -99,7 +103,9 @@ describe('licensing plugin', () => { coreSetup.http.get.mockImplementation(() => Promise.resolve(licenseMock.createLicense({ license: { type: types.shift() } })) ); - const { license$, refresh } = await plugin.setup(coreSetup); + + await plugin.setup(coreSetup); + const { refresh, license$ } = await plugin.start(coreStart); let i = 0; license$.subscribe((value) => { @@ -128,7 +134,8 @@ describe('licensing plugin', () => { const fetchedLicense = licenseMock.createLicense({ license: { uid: 'fresh' } }); coreSetup.http.get.mockResolvedValue(fetchedLicense); - const { license$, refresh } = await plugin.setup(coreSetup); + await plugin.setup(coreSetup); + const { license$, refresh } = await plugin.start(coreStart); await refresh(); const license = await license$.pipe(take(1)).toPromise(); @@ -153,7 +160,8 @@ describe('licensing plugin', () => { const coreSetup = coreMock.createSetup(); coreSetup.http.get.mockRejectedValue(new Error('reason')); - const { license$, refresh } = await plugin.setup(coreSetup); + await plugin.setup(coreSetup); + const { license$, refresh } = await plugin.start(coreStart); await refresh(); const license = await license$.pipe(take(1)).toPromise(); @@ -169,7 +177,8 @@ describe('licensing plugin', () => { const coreSetup = coreMock.createSetup(); coreSetup.http.get.mockRejectedValue(new Error('sorry')); - const { license$, refresh } = await plugin.setup(coreSetup); + await plugin.setup(coreSetup); + const { license$, refresh } = await plugin.start(coreStart); expect(sessionStorage.removeItem).toHaveBeenCalledTimes(0); await refresh(); @@ -206,7 +215,8 @@ describe('licensing plugin', () => { return () => undefined; }); - const { license$ } = await plugin.setup(coreSetup); + await plugin.setup(coreSetup); + const { license$ } = await plugin.start(coreStart); expect(registeredInterceptor!.response).toBeDefined(); const httpResponse = { @@ -284,7 +294,8 @@ describe('licensing plugin', () => { return () => undefined; }); - const { license$ } = await plugin.setup(coreSetup); + await plugin.setup(coreSetup); + const { license$ } = await plugin.start(coreStart); let updated = false; license$.subscribe(() => (updated = true)); @@ -326,10 +337,8 @@ describe('licensing plugin', () => { licenseMock.createLicense({ license: { status: 'active', type: 'gold' } }) ); - const { refresh } = await plugin.setup(coreSetup); - - const coreStart = coreMock.createStart(); - await plugin.start(coreStart); + await plugin.setup(coreSetup); + const { refresh } = await plugin.start(coreStart); await refresh(); expect(coreStart.overlays.banners.add).toHaveBeenCalledTimes(0); @@ -352,10 +361,8 @@ describe('licensing plugin', () => { .mockResolvedValueOnce(activeLicense) .mockResolvedValueOnce(expiredLicense); - const { refresh } = await plugin.setup(coreSetup); - - const coreStart = coreMock.createStart(); - await plugin.start(coreStart); + await plugin.setup(coreSetup); + const { refresh } = await plugin.start(coreStart); await refresh(); expect(coreStart.overlays.banners.add).toHaveBeenCalledTimes(0); @@ -377,7 +384,8 @@ describe('licensing plugin', () => { const sessionStorage = coreMock.createStorage(); plugin = new LicensingPlugin(coreMock.createPluginInitializerContext(), sessionStorage); const coreSetup = coreMock.createSetup(); - const { license$ } = await plugin.setup(coreSetup); + await plugin.setup(coreSetup); + const { license$ } = await plugin.start(coreStart); let completed = false; license$.subscribe({ complete: () => (completed = true) }); diff --git a/x-pack/plugins/licensing/public/plugin.ts b/x-pack/plugins/licensing/public/plugin.ts index 6435f3b17313c..c39acb12b06e1 100644 --- a/x-pack/plugins/licensing/public/plugin.ts +++ b/x-pack/plugins/licensing/public/plugin.ts @@ -3,12 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Subject, Subscription } from 'rxjs'; +import { Observable, Subject, Subscription } from 'rxjs'; import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; import { ILicense } from '../common/types'; -import { LicensingPluginSetup } from './types'; +import { LicensingPluginSetup, LicensingPluginStart } from './types'; import { createLicenseUpdate } from '../common/license_update'; import { License } from '../common/license'; import { mountExpiredBanner } from './expired_banner'; @@ -20,7 +20,7 @@ export const licensingSessionStorageKey = 'xpack.licensing'; * A plugin for fetching, refreshing, and receiving information about the license for the * current Kibana instance. */ -export class LicensingPlugin implements Plugin { +export class LicensingPlugin implements Plugin { /** * Used as a flag to halt all other plugin observables. */ @@ -37,6 +37,9 @@ export class LicensingPlugin implements Plugin { private coreStart?: CoreStart; private prevSignature?: string; + private refresh?: () => Promise; + private license$?: Observable; + constructor( context: PluginInitializerContext, private readonly storage: Storage = sessionStorage @@ -107,6 +110,9 @@ export class LicensingPlugin implements Plugin { }, }); + this.refresh = refreshManually; + this.license$ = license$; + return { refresh: refreshManually, license$, @@ -115,6 +121,13 @@ export class LicensingPlugin implements Plugin { public async start(core: CoreStart) { this.coreStart = core; + if (!this.refresh || !this.license$) { + throw new Error('Setup has not been completed'); + } + return { + refresh: this.refresh, + license$: this.license$, + }; } public stop() { diff --git a/x-pack/plugins/licensing/public/types.ts b/x-pack/plugins/licensing/public/types.ts index df8e50be5d150..71a4a452d163d 100644 --- a/x-pack/plugins/licensing/public/types.ts +++ b/x-pack/plugins/licensing/public/types.ts @@ -9,6 +9,20 @@ import { ILicense } from '../common/types'; /** @public */ export interface LicensingPluginSetup { + /** + * Steam of licensing information {@link ILicense}. + * @deprecated in favour of the counterpart provided from start contract + */ + license$: Observable; + /** + * Triggers licensing information re-fetch. + * @deprecated in favour of the counterpart provided from start contract + */ + refresh(): Promise; +} + +/** @public */ +export interface LicensingPluginStart { /** * Steam of licensing information {@link ILicense}. */ diff --git a/x-pack/plugins/licensing/server/mocks.ts b/x-pack/plugins/licensing/server/mocks.ts index 154692a2fd197..0d154f76d5134 100644 --- a/x-pack/plugins/licensing/server/mocks.ts +++ b/x-pack/plugins/licensing/server/mocks.ts @@ -26,10 +26,20 @@ const createSetupMock = (): jest.Mocked => { }; const createStartMock = (): jest.Mocked => { + const license = licenseMock.createLicense(); const mock = { + license$: new BehaviorSubject(license), + refresh: jest.fn(), + createLicensePoller: jest.fn(), featureUsage: featureUsageMock.createStart(), }; + mock.refresh.mockResolvedValue(license); + mock.createLicensePoller.mockReturnValue({ + license$: mock.license$, + refresh: mock.refresh, + }); + return mock; }; diff --git a/x-pack/plugins/licensing/server/plugin.test.ts b/x-pack/plugins/licensing/server/plugin.test.ts index 1dacc9406004a..3e31eae945383 100644 --- a/x-pack/plugins/licensing/server/plugin.test.ts +++ b/x-pack/plugins/licensing/server/plugin.test.ts @@ -14,6 +14,7 @@ import { elasticsearchServiceMock, loggingServiceMock, } from '../../../../src/core/server/mocks'; +import { IClusterClient } from '../../../../src/core/server/'; function buildRawLicense(options: Partial = {}): RawLicense { const defaultRawLicense: RawLicense = { @@ -28,8 +29,22 @@ function buildRawLicense(options: Partial = {}): RawLicense { const flushPromises = (ms = 50) => new Promise((res) => setTimeout(res, ms)); +function createCoreSetupWith(esClient: IClusterClient) { + const coreSetup = coreMock.createSetup(); + + coreSetup.getStartServices.mockResolvedValue([ + { + ...coreMock.createStart(), + elasticsearch: { legacy: { client: esClient, createClient: jest.fn() } }, + }, + {}, + {}, + ]); + return coreSetup; +} + describe('licensing plugin', () => { - describe('#setup', () => { + describe('#start', () => { describe('#license$', () => { let plugin: LicensingPlugin; let pluginInitContextMock: ReturnType; @@ -46,15 +61,15 @@ describe('licensing plugin', () => { }); it('returns license', async () => { - const dataClient = elasticsearchServiceMock.createClusterClient(); - dataClient.callAsInternalUser.mockResolvedValue({ + const esClient = elasticsearchServiceMock.createClusterClient(); + esClient.callAsInternalUser.mockResolvedValue({ license: buildRawLicense(), features: {}, }); - const coreSetup = coreMock.createSetup(); - coreSetup.elasticsearch.dataClient = dataClient; - const { license$ } = await plugin.setup(coreSetup); + const coreSetup = createCoreSetupWith(esClient); + await plugin.setup(coreSetup); + const { license$ } = await plugin.start(); const license = await license$.pipe(take(1)).toPromise(); expect(license.isAvailable).toBe(true); }); @@ -62,17 +77,17 @@ describe('licensing plugin', () => { it('observable receives updated licenses', async () => { const types: LicenseType[] = ['basic', 'gold', 'platinum']; - const dataClient = elasticsearchServiceMock.createClusterClient(); - dataClient.callAsInternalUser.mockImplementation(() => + const esClient = elasticsearchServiceMock.createClusterClient(); + esClient.callAsInternalUser.mockImplementation(() => Promise.resolve({ license: buildRawLicense({ type: types.shift() }), features: {}, }) ); - const coreSetup = coreMock.createSetup(); - coreSetup.elasticsearch.dataClient = dataClient; - const { license$ } = await plugin.setup(coreSetup); + const coreSetup = createCoreSetupWith(esClient); + await plugin.setup(coreSetup); + const { license$ } = await plugin.start(); const [first, second, third] = await license$.pipe(take(3), toArray()).toPromise(); expect(first.type).toBe('basic'); @@ -81,26 +96,28 @@ describe('licensing plugin', () => { }); it('returns a license with error when request fails', async () => { - const dataClient = elasticsearchServiceMock.createClusterClient(); - dataClient.callAsInternalUser.mockRejectedValue(new Error('test')); - const coreSetup = coreMock.createSetup(); - coreSetup.elasticsearch.dataClient = dataClient; + const esClient = elasticsearchServiceMock.createClusterClient(); + esClient.callAsInternalUser.mockRejectedValue(new Error('test')); + + const coreSetup = createCoreSetupWith(esClient); + await plugin.setup(coreSetup); + const { license$ } = await plugin.start(); - const { license$ } = await plugin.setup(coreSetup); const license = await license$.pipe(take(1)).toPromise(); expect(license.isAvailable).toBe(false); expect(license.error).toBeDefined(); }); it('generate error message when x-pack plugin was not installed', async () => { - const dataClient = elasticsearchServiceMock.createClusterClient(); + const esClient = elasticsearchServiceMock.createClusterClient(); const error: ElasticsearchError = new Error('reason'); error.status = 400; - dataClient.callAsInternalUser.mockRejectedValue(error); - const coreSetup = coreMock.createSetup(); - coreSetup.elasticsearch.dataClient = dataClient; + esClient.callAsInternalUser.mockRejectedValue(error); + + const coreSetup = createCoreSetupWith(esClient); + await plugin.setup(coreSetup); + const { license$ } = await plugin.start(); - const { license$ } = await plugin.setup(coreSetup); const license = await license$.pipe(take(1)).toPromise(); expect(license.isAvailable).toBe(false); expect(license.error).toBe('X-Pack plugin is not installed on the Elasticsearch cluster.'); @@ -110,50 +127,50 @@ describe('licensing plugin', () => { const error1 = new Error('reason-1'); const error2 = new Error('reason-2'); - const dataClient = elasticsearchServiceMock.createClusterClient(); + const esClient = elasticsearchServiceMock.createClusterClient(); - dataClient.callAsInternalUser + esClient.callAsInternalUser .mockRejectedValueOnce(error1) .mockRejectedValueOnce(error2) .mockResolvedValue({ license: buildRawLicense(), features: {} }); - const coreSetup = coreMock.createSetup(); - coreSetup.elasticsearch.dataClient = dataClient; + const coreSetup = createCoreSetupWith(esClient); + await plugin.setup(coreSetup); + const { license$ } = await plugin.start(); - const { license$ } = await plugin.setup(coreSetup); const [first, second, third] = await license$.pipe(take(3), toArray()).toPromise(); - expect(first.error).toBe(error1.message); expect(second.error).toBe(error2.message); expect(third.type).toBe('basic'); }); it('fetch license immediately without subscriptions', async () => { - const dataClient = elasticsearchServiceMock.createClusterClient(); - dataClient.callAsInternalUser.mockResolvedValue({ + const esClient = elasticsearchServiceMock.createClusterClient(); + esClient.callAsInternalUser.mockResolvedValue({ license: buildRawLicense(), features: {}, }); - const coreSetup = coreMock.createSetup(); - coreSetup.elasticsearch.dataClient = dataClient; - + const coreSetup = createCoreSetupWith(esClient); await plugin.setup(coreSetup); + await plugin.start(); + await flushPromises(); - expect(dataClient.callAsInternalUser).toHaveBeenCalledTimes(1); + + expect(esClient.callAsInternalUser).toHaveBeenCalledTimes(1); }); it('logs license details without subscriptions', async () => { - const dataClient = elasticsearchServiceMock.createClusterClient(); - dataClient.callAsInternalUser.mockResolvedValue({ + const esClient = elasticsearchServiceMock.createClusterClient(); + esClient.callAsInternalUser.mockResolvedValue({ license: buildRawLicense(), features: {}, }); - const coreSetup = coreMock.createSetup(); - coreSetup.elasticsearch.dataClient = dataClient; - + const coreSetup = createCoreSetupWith(esClient); await plugin.setup(coreSetup); + await plugin.start(); + await flushPromises(); const loggedMessages = loggingServiceMock.collect(pluginInitContextMock.logger).debug; @@ -170,20 +187,19 @@ describe('licensing plugin', () => { it('generates signature based on fetched license content', async () => { const types: LicenseType[] = ['basic', 'gold', 'basic']; - const dataClient = elasticsearchServiceMock.createClusterClient(); - dataClient.callAsInternalUser.mockImplementation(() => + const esClient = elasticsearchServiceMock.createClusterClient(); + esClient.callAsInternalUser.mockImplementation(() => Promise.resolve({ license: buildRawLicense({ type: types.shift() }), features: {}, }) ); - const coreSetup = coreMock.createSetup(); - coreSetup.elasticsearch.dataClient = dataClient; + const coreSetup = createCoreSetupWith(esClient); + await plugin.setup(coreSetup); + const { license$ } = await plugin.start(); - const { license$ } = await plugin.setup(coreSetup); const [first, second, third] = await license$.pipe(take(3), toArray()).toPromise(); - expect(first.signature === third.signature).toBe(true); expect(first.signature === second.signature).toBe(false); }); @@ -202,22 +218,24 @@ describe('licensing plugin', () => { api_polling_frequency: moment.duration(50000), }) ); - const dataClient = elasticsearchServiceMock.createClusterClient(); - dataClient.callAsInternalUser.mockResolvedValue({ + const esClient = elasticsearchServiceMock.createClusterClient(); + esClient.callAsInternalUser.mockResolvedValue({ license: buildRawLicense(), features: {}, }); - const coreSetup = coreMock.createSetup(); - coreSetup.elasticsearch.dataClient = dataClient; - const { refresh } = await plugin.setup(coreSetup); - expect(dataClient.callAsInternalUser).toHaveBeenCalledTimes(0); + const coreSetup = createCoreSetupWith(esClient); + await plugin.setup(coreSetup); + const { refresh, license$ } = await plugin.start(); + + expect(esClient.callAsInternalUser).toHaveBeenCalledTimes(0); - refresh(); - expect(dataClient.callAsInternalUser).toHaveBeenCalledTimes(1); + await license$.pipe(take(1)).toPromise(); + expect(esClient.callAsInternalUser).toHaveBeenCalledTimes(1); refresh(); - expect(dataClient.callAsInternalUser).toHaveBeenCalledTimes(2); + await flushPromises(); + expect(esClient.callAsInternalUser).toHaveBeenCalledTimes(2); }); }); @@ -235,15 +253,15 @@ describe('licensing plugin', () => { }) ); - const dataClient = elasticsearchServiceMock.createClusterClient(); - dataClient.callAsInternalUser.mockResolvedValue({ + const esClient = elasticsearchServiceMock.createClusterClient(); + esClient.callAsInternalUser.mockResolvedValue({ license: buildRawLicense(), features: {}, }); - const coreSetup = coreMock.createSetup(); - coreSetup.elasticsearch.dataClient = dataClient; + const coreSetup = createCoreSetupWith(esClient); + await plugin.setup(coreSetup); + const { createLicensePoller, license$ } = await plugin.start(); - const { createLicensePoller, license$ } = await plugin.setup(coreSetup); const customClient = elasticsearchServiceMock.createClusterClient(); customClient.callAsInternalUser.mockResolvedValue({ license: buildRawLicense({ type: 'gold' }), @@ -276,7 +294,8 @@ describe('licensing plugin', () => { ); const coreSetup = coreMock.createSetup(); - const { createLicensePoller } = await plugin.setup(coreSetup); + await plugin.setup(coreSetup); + const { createLicensePoller } = await plugin.start(); const customClient = elasticsearchServiceMock.createClusterClient(); customClient.callAsInternalUser.mockResolvedValue({ @@ -357,7 +376,8 @@ describe('licensing plugin', () => { }) ); const coreSetup = coreMock.createSetup(); - const { license$ } = await plugin.setup(coreSetup); + await plugin.setup(coreSetup); + const { license$ } = await plugin.start(); let completed = false; license$.subscribe({ complete: () => (completed = true) }); diff --git a/x-pack/plugins/licensing/server/plugin.ts b/x-pack/plugins/licensing/server/plugin.ts index 51e778ba0aa46..33f70c549914d 100644 --- a/x-pack/plugins/licensing/server/plugin.ts +++ b/x-pack/plugins/licensing/server/plugin.ts @@ -12,11 +12,12 @@ import stringify from 'json-stable-stringify'; import { CoreSetup, - CoreStart, Logger, Plugin, PluginInitializerContext, IClusterClient, + IScopedClusterClient, + ScopeableRequest, } from 'src/core/server'; import { ILicense, PublicLicense, PublicFeatures } from '../common/types'; @@ -85,6 +86,9 @@ export class LicensingPlugin implements Plugin Promise; + private license$?: Observable; + constructor(private readonly context: PluginInitializerContext) { this.logger = this.context.logger.get(); this.config$ = this.context.config.create(); @@ -94,7 +98,30 @@ export class LicensingPlugin implements Plugin + ): ReturnType { + const [coreStart] = await core.getStartServices(); + const client = coreStart.elasticsearch.legacy.client; + return await client.callAsInternalUser(...args); + } + + const dataClient: IClusterClient = { + callAsInternalUser, + asScoped(request?: ScopeableRequest): IScopedClusterClient { + return { + async callAsCurrentUser( + ...args: Parameters + ): ReturnType { + const [coreStart] = await core.getStartServices(); + const client = coreStart.elasticsearch.legacy.client; + return await client.asScoped(request).callAsCurrentUser(...args); + }, + callAsInternalUser, + }; + }, + }; const { refresh, license$ } = this.createLicensePoller( dataClient, @@ -106,6 +133,9 @@ export class LicensingPlugin implements Plugin; /** * Triggers licensing information re-fetch. + * @deprecated in favour of the counterpart provided from start contract */ refresh(): Promise; /** * 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. + * @deprecated in favour of the counterpart provided from start contract */ createLicensePoller: ( clusterClient: IClusterClient, @@ -75,6 +78,23 @@ export interface LicensingPluginSetup { /** @public */ export interface LicensingPluginStart { + /** + * Steam of licensing information {@link ILicense}. + */ + license$: Observable; + /** + * Triggers licensing information re-fetch. + */ + refresh(): Promise; + /** + * 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; refresh(): Promise }; /** * APIs to manage licensed feature usage. */