diff --git a/src/plugins/maps_ems/public/kibana_services.ts b/src/plugins/maps_ems/public/kibana_services.ts index 4027501ae2c56..45ae382cfa7bf 100644 --- a/src/plugins/maps_ems/public/kibana_services.ts +++ b/src/plugins/maps_ems/public/kibana_services.ts @@ -9,7 +9,6 @@ import { LicensingPluginStart } from '@kbn/licensing-plugin/public'; import { ILicense } from '@kbn/licensing-plugin/common/types'; -import { firstValueFrom, take } from 'rxjs'; import type { MapConfig } from '../server/config'; import { LICENSE_CHECK_ID } from '../common'; @@ -33,7 +32,7 @@ export function getIsEnterprisePlus() { } export async function setLicensingPluginStart(licensingPlugin: LicensingPluginStart) { - const license = await firstValueFrom(licensingPlugin.license$.pipe(take(1))); + const license = await licensingPlugin.getLicense(); updateLicenseState(license); licensingPlugin.license$.subscribe(updateLicenseState); } 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 239432279cc94..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 @@ -6,7 +6,6 @@ */ import { useContext } from 'react'; import { useQuery } from '@tanstack/react-query'; -import { firstValueFrom, take } from 'rxjs'; import { SetupContext } from '../../application/setup_context'; import { isSubscriptionAllowed } from '../../../common/utils/subscription'; import { useKibana } from './use_kibana'; @@ -17,7 +16,7 @@ export const useSubscriptionStatus = () => { const { licensing } = useKibana().services; const { isCloudEnabled } = useContext(SetupContext); return useQuery([SUBSCRIPTION_QUERY_KEY], async () => { - const license = await firstValueFrom(licensing.license$.pipe(take(1))); + 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 824e46df2dca5..5e889bc12e69b 100644 --- a/x-pack/plugins/cloud_defend/server/plugin.ts +++ b/x-pack/plugins/cloud_defend/server/plugin.ts @@ -13,7 +13,6 @@ import { SavedObjectsClientContract, } from '@kbn/core/server'; import type { PackagePolicy, NewPackagePolicy } from '@kbn/fleet-plugin/common'; -import { firstValueFrom, take } from 'rxjs'; import { CloudDefendPluginSetup, CloudDefendPluginStart, @@ -62,7 +61,7 @@ export class CloudDefendPlugin implements Plugin => { - const license = await firstValueFrom(plugins.licensing.license$.pipe(take(1))); + 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 c7bd2a1a644a8..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 @@ -6,7 +6,6 @@ */ import { useContext } from 'react'; import { useQuery } from '@tanstack/react-query'; -import { firstValueFrom, take } from 'rxjs'; import { SetupContext } from '../../application/setup_context'; import { isSubscriptionAllowed } from '../../../common/utils/subscription'; import { useKibana } from './use_kibana'; @@ -18,7 +17,7 @@ export const useIsSubscriptionStatusValid = () => { const { isCloudEnabled } = useContext(SetupContext); return useQuery([SUBSCRIPTION_QUERY_KEY], async () => { - const license = await firstValueFrom(licensing.license$.pipe(take(1))); + 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 cce907a7d70c2..0b6a58efdc67e 100755 --- a/x-pack/plugins/cloud_security_posture/server/plugin.ts +++ b/x-pack/plugins/cloud_security_posture/server/plugin.ts @@ -28,7 +28,6 @@ import type { CspBenchmarkRule, CspSettings, } from '@kbn/cloud-security-posture-common/schema/rules/latest'; -import { firstValueFrom, take } from 'rxjs'; import { isCspPackage } from '../common/utils/helpers'; import { isSubscriptionAllowed } from '../common/utils/subscription'; import { cleanupCredentials } from '../common/utils/helpers'; @@ -116,7 +115,7 @@ export class CspPlugin plugins.fleet.registerExternalCallback( 'packagePolicyCreate', async (packagePolicy: NewPackagePolicy): Promise => { - const license = await firstValueFrom(plugins.licensing.license$.pipe(take(1))); + 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/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..5c7701a2ce7bc 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, take } 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$!.pipe(take(1))), 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..c1b16b08c3435 100644 --- a/x-pack/plugins/licensing/server/plugin.ts +++ b/x-pack/plugins/licensing/server/plugin.ts @@ -17,6 +17,8 @@ import { distinctUntilChanged, ReplaySubject, timer, + firstValueFrom, + take, } from 'rxjs'; import moment from 'moment'; import type { MaybePromise } from '@kbn/utility-types'; @@ -159,6 +161,7 @@ export class LicensingPlugin implements Plugin await firstValueFrom(this.license$!.pipe(take(1))), 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 11b46c7f58395..41e194dc7d0b7 100644 --- a/x-pack/plugins/maps/public/licensed_features.ts +++ b/x-pack/plugins/maps/public/licensed_features.ts @@ -7,7 +7,6 @@ import { ILicense, LicenseType } from '@kbn/licensing-plugin/common/types'; import { LicensingPluginSetup, LicensingPluginStart } from '@kbn/licensing-plugin/public'; -import { firstValueFrom, take } from 'rxjs'; import { APP_ID } from '../common/constants'; export enum LICENSED_FEATURES { @@ -51,7 +50,7 @@ export const whenLicenseInitialized = async (): Promise => { }; export async function setLicensingPluginStart(licensingPlugin: LicensingPluginStart) { - const license = await firstValueFrom(licensingPlugin.license$.pipe(take(1))); + const license = await licensingPlugin.getLicense(); updateLicenseState(license); licensingPluginStart = licensingPlugin;