Skip to content

Commit

Permalink
[licensing] Remove unnecessary refresh calls (elastic#194499)
Browse files Browse the repository at this point in the history
  • Loading branch information
afharo authored and tiansivive committed Oct 7, 2024
1 parent 9f11eaa commit 1848788
Show file tree
Hide file tree
Showing 16 changed files with 104 additions and 28 deletions.
2 changes: 1 addition & 1 deletion src/plugins/maps_ems/public/kibana_services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
34 changes: 16 additions & 18 deletions src/plugins/maps_ems/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@
* 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 {
config: MapConfig;
createEMSSettings: () => EMSSettings;
}

interface MapsEmsSetupServerDependencies {
licensing?: LicensingPluginSetup;
interface MapsEmsStartServerDependencies {
licensing?: LicensingPluginStart;
}

export class MapsEmsPlugin implements Plugin<MapsEmsPluginServerSetup> {
Expand All @@ -29,22 +29,20 @@ export class MapsEmsPlugin implements Plugin<MapsEmsPluginServerSetup> {
this._initializerContext = initializerContext;
}

public setup(core: CoreSetup, plugins: MapsEmsSetupServerDependencies) {
public setup(core: CoreSetup<MapsEmsStartServerDependencies>) {
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
};
2 changes: 1 addition & 1 deletion x-pack/plugins/cloud_defend/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export class CloudDefendPlugin implements Plugin<CloudDefendPluginSetup, CloudDe
plugins.fleet.registerExternalCallback(
'packagePolicyCreate',
async (packagePolicy: NewPackagePolicy): Promise<NewPackagePolicy> => {
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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
};
2 changes: 1 addition & 1 deletion x-pack/plugins/cloud_security_posture/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export class CspPlugin
plugins.fleet.registerExternalCallback(
'packagePolicyCreate',
async (packagePolicy: NewPackagePolicy): Promise<NewPackagePolicy> => {
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(
Expand Down
8 changes: 5 additions & 3 deletions x-pack/plugins/licensing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<ILicense>` 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<ILicense>` allows a plugin to enforce license retrieval.
- `license$: Observable<ILicense>` 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<ILicense>` returns the latest license data retrieved or waits for it to be resolved.
- `refresh: () => Promise<ILicense>` 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<ILicense>` 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<ILicense>` allows a plugin to enforce license retrieval.
- `getLicense(): Promise<ILicense>` returns the latest license data retrieved or waits for it to be resolved.
- `refresh: () => Promise<ILicense>` 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.
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/licensing/public/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const createStartMock = () => {
const license = licenseMock.createLicense();
const mock: jest.Mocked<LicensingPluginStart> = {
license$: new BehaviorSubject(license),
getLicense: jest.fn(),
refresh: jest.fn(),
featureUsage: featureUsageMock.createStart(),
};
Expand Down
30 changes: 30 additions & 0 deletions x-pack/plugins/licensing/public/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
3 changes: 2 additions & 1 deletion x-pack/plugins/licensing/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -134,6 +134,7 @@ export class LicensingPlugin implements Plugin<LicensingPluginSetup, LicensingPl
}
return {
refresh: this.refresh,
getLicense: async () => await firstValueFrom(this.license$!),
license$: this.license$,
featureUsage: this.featureUsage.start({ http: core.http }),
};
Expand Down
4 changes: 4 additions & 0 deletions x-pack/plugins/licensing/public/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export interface LicensingPluginStart {
* Steam of licensing information {@link ILicense}.
*/
license$: Observable<ILicense>;
/**
* Retrieves the {@link ILicense | licensing information}
*/
getLicense(): Promise<ILicense>;
/**
* Triggers licensing information re-fetch.
*/
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/licensing/server/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const createStartMock = (): jest.Mocked<LicensingPluginStart> => {
const license = licenseMock.createLicense();
const mock = {
license$: new BehaviorSubject(license),
getLicense: jest.fn(),
refresh: jest.fn(),
createLicensePoller: jest.fn(),
featureUsage: featureUsageMock.createStart(),
Expand Down
32 changes: 32 additions & 0 deletions x-pack/plugins/licensing/server/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/licensing/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
distinctUntilChanged,
ReplaySubject,
timer,
firstValueFrom,
} from 'rxjs';
import moment from 'moment';
import type { MaybePromise } from '@kbn/utility-types';
Expand Down Expand Up @@ -159,6 +160,7 @@ export class LicensingPlugin implements Plugin<LicensingPluginSetup, LicensingPl
}
return {
refresh: this.refresh,
getLicense: async () => await firstValueFrom(this.license$!),
license$: this.license$,
featureUsage: this.featureUsage.start(),
createLicensePoller: this.createLicensePoller.bind(this),
Expand Down
5 changes: 5 additions & 0 deletions x-pack/plugins/licensing/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ export interface LicensingPluginStart {
*/
license$: Observable<ILicense>;

/**
* Retrieves the {@link ILicense | licensing information}
*/
getLicense(): Promise<ILicense>;

/**
* Triggers licensing information re-fetch.
*/
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/maps/public/licensed_features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const whenLicenseInitialized = async (): Promise<void> => {
};

export async function setLicensingPluginStart(licensingPlugin: LicensingPluginStart) {
const license = await licensingPlugin.refresh();
const license = await licensingPlugin.getLicense();
updateLicenseState(license);

licensingPluginStart = licensingPlugin;
Expand Down

0 comments on commit 1848788

Please sign in to comment.