diff --git a/src/plugins/maps_ems/public/kibana_services.ts b/src/plugins/maps_ems/public/kibana_services.ts index 67534b6ae6fe5..45ae382cfa7bf 100644 --- a/src/plugins/maps_ems/public/kibana_services.ts +++ b/src/plugins/maps_ems/public/kibana_services.ts @@ -32,7 +32,7 @@ export function getIsEnterprisePlus() { } export async function setLicensingPluginStart(licensingPlugin: LicensingPluginStart) { - const license = await licensingPlugin.refresh(); + const license = await licensingPlugin.getLicense(); updateLicenseState(license); licensingPlugin.license$.subscribe(updateLicenseState); } diff --git a/src/plugins/maps_ems/server/plugin.ts b/src/plugins/maps_ems/server/plugin.ts index d70b639f3d70b..b30ee5fa9ec72 100644 --- a/src/plugins/maps_ems/server/plugin.ts +++ b/src/plugins/maps_ems/server/plugin.ts @@ -7,10 +7,10 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { ILicense, LicensingPluginSetup } from '@kbn/licensing-plugin/server'; -import { Plugin, PluginInitializerContext } from '@kbn/core-plugins-server'; -import { CoreSetup } from '@kbn/core-lifecycle-server'; -import { MapConfig } from './config'; +import type { ILicense, LicensingPluginStart } from '@kbn/licensing-plugin/server'; +import type { Plugin, PluginInitializerContext } from '@kbn/core-plugins-server'; +import type { CoreSetup } from '@kbn/core-lifecycle-server'; +import type { MapConfig } from './config'; import { LICENSE_CHECK_ID, EMSSettings } from '../common'; export interface MapsEmsPluginServerSetup { @@ -18,8 +18,8 @@ export interface MapsEmsPluginServerSetup { createEMSSettings: () => EMSSettings; } -interface MapsEmsSetupServerDependencies { - licensing?: LicensingPluginSetup; +interface MapsEmsStartServerDependencies { + licensing?: LicensingPluginStart; } export class MapsEmsPlugin implements Plugin { @@ -29,22 +29,20 @@ export class MapsEmsPlugin implements Plugin { this._initializerContext = initializerContext; } - public setup(core: CoreSetup, plugins: MapsEmsSetupServerDependencies) { + public setup(core: CoreSetup) { const mapConfig = this._initializerContext.config.get(); let isEnterprisePlus = false; - if (plugins.licensing) { - function updateLicenseState(license: ILicense) { - const enterprise = license.check(LICENSE_CHECK_ID, 'enterprise'); - isEnterprisePlus = enterprise.state === 'valid'; - } - - plugins.licensing - .refresh() - .then(updateLicenseState) - .catch(() => {}); - plugins.licensing.license$.subscribe(updateLicenseState); + function updateLicenseState(license: ILicense) { + const enterprise = license.check(LICENSE_CHECK_ID, 'enterprise'); + isEnterprisePlus = enterprise.state === 'valid'; } + core + .getStartServices() + .then(([_, { licensing }]) => { + licensing?.license$.subscribe(updateLicenseState); + }) + .catch(() => {}); return { config: mapConfig, diff --git a/x-pack/plugins/cloud_defend/public/common/hooks/use_subscription_status.ts b/x-pack/plugins/cloud_defend/public/common/hooks/use_subscription_status.ts index 26dd4caa33c4d..771da23d5ac05 100644 --- a/x-pack/plugins/cloud_defend/public/common/hooks/use_subscription_status.ts +++ b/x-pack/plugins/cloud_defend/public/common/hooks/use_subscription_status.ts @@ -16,7 +16,7 @@ export const useSubscriptionStatus = () => { const { licensing } = useKibana().services; const { isCloudEnabled } = useContext(SetupContext); return useQuery([SUBSCRIPTION_QUERY_KEY], async () => { - const license = await licensing.refresh(); + const license = await licensing.getLicense(); return isSubscriptionAllowed(isCloudEnabled, license); }); }; diff --git a/x-pack/plugins/cloud_defend/server/plugin.ts b/x-pack/plugins/cloud_defend/server/plugin.ts index b7c5016776816..5e889bc12e69b 100644 --- a/x-pack/plugins/cloud_defend/server/plugin.ts +++ b/x-pack/plugins/cloud_defend/server/plugin.ts @@ -61,7 +61,7 @@ export class CloudDefendPlugin implements Plugin => { - const license = await plugins.licensing.refresh(); + const license = await plugins.licensing.getLicense(); if (isCloudDefendPackage(packagePolicy.package?.name)) { if (!isSubscriptionAllowed(this.isCloudEnabled, license)) { throw new Error( diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_is_subscription_status_valid.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_is_subscription_status_valid.ts index 99ded40b04f63..e89495a1b4300 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_is_subscription_status_valid.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_is_subscription_status_valid.ts @@ -17,7 +17,7 @@ export const useIsSubscriptionStatusValid = () => { const { isCloudEnabled } = useContext(SetupContext); return useQuery([SUBSCRIPTION_QUERY_KEY], async () => { - const license = await licensing.refresh(); + const license = await licensing.getLicense(); return isSubscriptionAllowed(isCloudEnabled, license); }); }; diff --git a/x-pack/plugins/cloud_security_posture/server/plugin.ts b/x-pack/plugins/cloud_security_posture/server/plugin.ts index b373b6d80c5ee..0b6a58efdc67e 100755 --- a/x-pack/plugins/cloud_security_posture/server/plugin.ts +++ b/x-pack/plugins/cloud_security_posture/server/plugin.ts @@ -115,7 +115,7 @@ export class CspPlugin plugins.fleet.registerExternalCallback( 'packagePolicyCreate', async (packagePolicy: NewPackagePolicy): Promise => { - const license = await plugins.licensing.refresh(); + const license = await plugins.licensing.getLicense(); if (isCspPackage(packagePolicy.package?.name)) { if (!isSubscriptionAllowed(this.isCloudEnabled, license)) { throw new Error( diff --git a/x-pack/plugins/licensing/README.md b/x-pack/plugins/licensing/README.md index f368b894caaf0..a629124ccf627 100644 --- a/x-pack/plugins/licensing/README.md +++ b/x-pack/plugins/licensing/README.md @@ -8,13 +8,15 @@ Retrieves license data from Elasticsearch and becomes a source of license data f ## API: ### Server-side The licensing plugin retrieves license data from **Elasticsearch** at regular configurable intervals. -- `license$: Observable` Provides a steam of license data [ILicense](./common/types.ts). Plugin emits new value whenever it detects changes in license info. If the plugin cannot retrieve a license from **Elasticsearch**, it will emit `an empty license` object. -- `refresh: () => Promise` allows a plugin to enforce license retrieval. +- `license$: Observable` Provides a steam of license data [ILicense](./common/types.ts). Plugin emits new value whenever it detects changes in license info. If the plugin cannot retrieve a license from **Elasticsearch**, it will emit `an empty license` object. +- `getLicense(): Promise` returns the latest license data retrieved or waits for it to be resolved. +- `refresh: () => Promise` triggers the licensing information re-fetch. ### Client-side The licensing plugin retrieves license data from **licensing Kibana plugin** and does not communicate with Elasticsearch directly. - `license$: Observable` Provides a steam of license data [ILicense](./common/types.ts). Plugin emits new value whenever it detects changes in license info. If the plugin cannot retrieve a license from **Kibana**, it will emit `an empty license` object. -- `refresh: () => Promise` allows a plugin to enforce license retrieval. +- `getLicense(): Promise` returns the latest license data retrieved or waits for it to be resolved. +- `refresh: () => Promise` triggers the licensing information re-fetch. ## Migration example The new platform licensing plugin became stateless now. It means that instead of storing all your data from `checkLicense` within the plugin, you should react on license data change on both the client and server sides. diff --git a/x-pack/plugins/licensing/public/mocks.ts b/x-pack/plugins/licensing/public/mocks.ts index c95d0d7283bf2..9c258cee71c44 100644 --- a/x-pack/plugins/licensing/public/mocks.ts +++ b/x-pack/plugins/licensing/public/mocks.ts @@ -26,6 +26,7 @@ const createStartMock = () => { const license = licenseMock.createLicense(); const mock: jest.Mocked = { license$: new BehaviorSubject(license), + getLicense: jest.fn(), refresh: jest.fn(), featureUsage: featureUsageMock.createStart(), }; diff --git a/x-pack/plugins/licensing/public/plugin.test.ts b/x-pack/plugins/licensing/public/plugin.test.ts index 439be2410e9dd..f2468dacb0318 100644 --- a/x-pack/plugins/licensing/public/plugin.test.ts +++ b/x-pack/plugins/licensing/public/plugin.test.ts @@ -75,6 +75,36 @@ describe('licensing plugin', () => { }); }); + describe('#getLicense', () => { + it('awaits for the license and returns it', async () => { + const sessionStorage = coreMock.createStorage(); + plugin = new LicensingPlugin(coreMock.createPluginInitializerContext(), sessionStorage); + + const coreSetup = coreMock.createSetup(); + const firstLicense = licenseMock.createLicense({ + license: { uid: 'first', type: 'basic' }, + }); + coreSetup.http.get.mockResolvedValueOnce(firstLicense); + + await plugin.setup(coreSetup); + const { license$, getLicense, refresh } = await plugin.start(coreStart); + const getLicensePromise = getLicense(); + + let fromObservable; + license$.subscribe((license) => (fromObservable = license)); + await refresh(); // force the license fetch + + const licenseResult = await getLicensePromise; + expect(licenseResult.uid).toBe('first'); + expect(licenseResult).toBe(fromObservable); + + const secondResult = await getLicense(); // retrieves the same license without refreshing + expect(secondResult.uid).toBe('first'); + expect(secondResult).toBe(fromObservable); + expect(coreSetup.http.get).toHaveBeenCalledTimes(1); + }); + }); + describe('#license$', () => { it('starts with license saved in sessionStorage if available', async () => { const sessionStorage = coreMock.createStorage(); diff --git a/x-pack/plugins/licensing/public/plugin.ts b/x-pack/plugins/licensing/public/plugin.ts index 2a56bedf67b09..3a6c5c2d94841 100644 --- a/x-pack/plugins/licensing/public/plugin.ts +++ b/x-pack/plugins/licensing/public/plugin.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Observable, Subject, Subscription } from 'rxjs'; +import { firstValueFrom, Observable, Subject, Subscription } from 'rxjs'; import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public'; import { ILicense } from '../common/types'; @@ -134,6 +134,7 @@ export class LicensingPlugin implements Plugin await firstValueFrom(this.license$!), license$: this.license$, featureUsage: this.featureUsage.start({ http: core.http }), }; diff --git a/x-pack/plugins/licensing/public/types.ts b/x-pack/plugins/licensing/public/types.ts index 9cbd3a6731ec6..ca09f3f3e17e7 100644 --- a/x-pack/plugins/licensing/public/types.ts +++ b/x-pack/plugins/licensing/public/types.ts @@ -36,6 +36,10 @@ export interface LicensingPluginStart { * Steam of licensing information {@link ILicense}. */ license$: Observable; + /** + * Retrieves the {@link ILicense | licensing information} + */ + getLicense(): Promise; /** * Triggers licensing information re-fetch. */ diff --git a/x-pack/plugins/licensing/server/mocks.ts b/x-pack/plugins/licensing/server/mocks.ts index 724a5aa2d057e..cdca1f95a0849 100644 --- a/x-pack/plugins/licensing/server/mocks.ts +++ b/x-pack/plugins/licensing/server/mocks.ts @@ -30,6 +30,7 @@ const createStartMock = (): jest.Mocked => { const license = licenseMock.createLicense(); const mock = { license$: new BehaviorSubject(license), + getLicense: jest.fn(), refresh: jest.fn(), createLicensePoller: jest.fn(), featureUsage: featureUsageMock.createStart(), diff --git a/x-pack/plugins/licensing/server/plugin.test.ts b/x-pack/plugins/licensing/server/plugin.test.ts index cc67afcc37574..d46d9e675f6d3 100644 --- a/x-pack/plugins/licensing/server/plugin.test.ts +++ b/x-pack/plugins/licensing/server/plugin.test.ts @@ -249,6 +249,38 @@ describe('licensing plugin', () => { }); }); + describe('#getLicense', () => { + it('awaits for the license and returns it', async () => { + plugin = new LicensingPlugin( + coreMock.createPluginInitializerContext({ + // disable polling mechanism + api_polling_frequency: moment.duration(50000), + license_cache_duration: moment.duration(1000), + }) + ); + const esClient = createEsClient({ + license: buildRawLicense(), + features: {}, + }); + + const coreSetup = createCoreSetupWith(esClient); + plugin.setup(coreSetup); + const { license$, getLicense } = plugin.start(); + + expect(esClient.asInternalUser.xpack.info).toHaveBeenCalledTimes(0); + + const firstLicense = await getLicense(); + let fromObservable; + license$.subscribe((license) => (fromObservable = license)); + expect(firstLicense).toStrictEqual(fromObservable); + expect(esClient.asInternalUser.xpack.info).toHaveBeenCalledTimes(1); // the initial resolution + + const secondLicense = await getLicense(); + expect(secondLicense).toStrictEqual(fromObservable); + expect(esClient.asInternalUser.xpack.info).toHaveBeenCalledTimes(1); // still only one call + }); + }); + describe('#refresh', () => { it('forces refresh immediately', async () => { plugin = new LicensingPlugin( diff --git a/x-pack/plugins/licensing/server/plugin.ts b/x-pack/plugins/licensing/server/plugin.ts index 60bf0d7d1d210..1f9cfaf20af77 100644 --- a/x-pack/plugins/licensing/server/plugin.ts +++ b/x-pack/plugins/licensing/server/plugin.ts @@ -17,6 +17,7 @@ import { distinctUntilChanged, ReplaySubject, timer, + firstValueFrom, } from 'rxjs'; import moment from 'moment'; import type { MaybePromise } from '@kbn/utility-types'; @@ -159,6 +160,7 @@ export class LicensingPlugin implements Plugin await firstValueFrom(this.license$!), license$: this.license$, featureUsage: this.featureUsage.start(), createLicensePoller: this.createLicensePoller.bind(this), diff --git a/x-pack/plugins/licensing/server/types.ts b/x-pack/plugins/licensing/server/types.ts index fcccdecb66c00..63a33d5103732 100644 --- a/x-pack/plugins/licensing/server/types.ts +++ b/x-pack/plugins/licensing/server/types.ts @@ -75,6 +75,11 @@ export interface LicensingPluginStart { */ license$: Observable; + /** + * Retrieves the {@link ILicense | licensing information} + */ + getLicense(): Promise; + /** * Triggers licensing information re-fetch. */ diff --git a/x-pack/plugins/maps/public/licensed_features.ts b/x-pack/plugins/maps/public/licensed_features.ts index 9c95a389684b9..41e194dc7d0b7 100644 --- a/x-pack/plugins/maps/public/licensed_features.ts +++ b/x-pack/plugins/maps/public/licensed_features.ts @@ -50,7 +50,7 @@ export const whenLicenseInitialized = async (): Promise => { }; export async function setLicensingPluginStart(licensingPlugin: LicensingPluginStart) { - const license = await licensingPlugin.refresh(); + const license = await licensingPlugin.getLicense(); updateLicenseState(license); licensingPluginStart = licensingPlugin;