[8.x] [licensing] Remove unnecessary refresh calls (#194499) (#194855)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[licensing] Remove unnecessary refresh calls
(#194499)](https://github.com/elastic/kibana/pull/194499)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Alejandro Fernández
Haro","email":"alejandro.haro@elastic.co"},"sourceCommit":{"committedDate":"2024-10-03T15:42:39Z","message":"[licensing]
Remove unnecessary refresh calls
(#194499)","sha":"f3f53e054237087aab8590084cb7c8c10972427c","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Core","Team:Presentation","release_note:skip","v9.0.0","Team:Cloud
Security","backport:prev-minor"],"title":"[licensing] Remove unnecessary
refresh
calls","number":194499,"url":"https://github.com/elastic/kibana/pull/194499","mergeCommit":{"message":"[licensing]
Remove unnecessary refresh calls
(#194499)","sha":"f3f53e054237087aab8590084cb7c8c10972427c"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/194499","number":194499,"mergeCommit":{"message":"[licensing]
Remove unnecessary refresh calls
(#194499)","sha":"f3f53e054237087aab8590084cb7c8c10972427c"}}]}]
BACKPORT-->

Co-authored-by: Alejandro Fernández Haro <alejandro.haro@elastic.co>
This commit is contained in:
Kibana Machine 2024-10-04 03:30:09 +10:00 committed by GitHub
parent 71b6f0b5e7
commit 026a8e43c8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 104 additions and 28 deletions

View file

@ -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);
}

View file

@ -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<MapsEmsPluginServerSetup> {
@ -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,

View file

@ -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);
});
};

View file

@ -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(

View file

@ -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);
});
};

View file

@ -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(

View file

@ -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.

View file

@ -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(),
};

View file

@ -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();

View file

@ -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<LicensingPluginSetup, LicensingPl
}
return {
refresh: this.refresh,
getLicense: async () => await firstValueFrom(this.license$!),
license$: this.license$,
featureUsage: this.featureUsage.start({ http: core.http }),
};

View file

@ -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.
*/

View file

@ -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(),

View file

@ -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(

View file

@ -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<LicensingPluginSetup, LicensingPl
}
return {
refresh: this.refresh,
getLicense: async () => await firstValueFrom(this.license$!),
license$: this.license$,
featureUsage: this.featureUsage.start(),
createLicensePoller: this.createLicensePoller.bind(this),

View file

@ -75,6 +75,11 @@ export interface LicensingPluginStart {
*/
license$: Observable<ILicense>;
/**
* Retrieves the {@link ILicense | licensing information}
*/
getLicense(): Promise<ILicense>;
/**
* Triggers licensing information re-fetch.
*/

View file

@ -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;