diff --git a/x-pack/plugins/licensing/server/mocks.ts b/x-pack/plugins/licensing/server/mocks.ts index 186d35ec47dd34..724a5aa2d057e1 100644 --- a/x-pack/plugins/licensing/server/mocks.ts +++ b/x-pack/plugins/licensing/server/mocks.ts @@ -19,14 +19,9 @@ const createSetupMock = (): jest.Mocked => { const mock = { license$: new BehaviorSubject(license), refresh: jest.fn(), - createLicensePoller: jest.fn(), featureUsage: featureUsageMock.createSetup(), }; 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 446cd2baa1cc0a..1fe4bbf238e197 100644 --- a/x-pack/plugins/licensing/server/plugin.test.ts +++ b/x-pack/plugins/licensing/server/plugin.test.ts @@ -6,31 +6,34 @@ */ import { take, toArray } from 'rxjs/operators'; +import { estypes } from '@elastic/elasticsearch'; import moment from 'moment'; import { LicenseType } from '../common/types'; -import { ElasticsearchError, RawLicense } from './types'; +import { ElasticsearchError } from './types'; import { LicensingPlugin } from './plugin'; import { coreMock, elasticsearchServiceMock, loggingSystemMock, } from '../../../../src/core/server/mocks'; -import { ILegacyClusterClient } from '../../../../src/core/server/'; +import { IClusterClient } from '../../../../src/core/server'; -function buildRawLicense(options: Partial = {}): RawLicense { - const defaultRawLicense: RawLicense = { +function buildRawLicense( + options: Partial = {} +): estypes.XpackInfoMinimalLicenseInformation { + return { uid: 'uid-000000001234', status: 'active', type: 'basic', mode: 'basic', expiry_date_in_millis: 1000, + ...options, }; - return Object.assign(defaultRawLicense, options); } const flushPromises = (ms = 50) => new Promise((res) => setTimeout(res, ms)); -function createCoreSetupWith(esClient: ILegacyClusterClient) { +function createCoreSetupWith(esClient: IClusterClient) { const coreSetup = coreMock.createSetup(); const coreStart = coreMock.createStart(); coreSetup.getStartServices.mockResolvedValue([ @@ -38,11 +41,7 @@ function createCoreSetupWith(esClient: ILegacyClusterClient) { ...coreStart, elasticsearch: { ...coreStart.elasticsearch, - legacy: { - ...coreStart.elasticsearch.legacy, - client: esClient, - createClient: jest.fn(), - }, + client: esClient, }, }, {}, @@ -52,6 +51,16 @@ function createCoreSetupWith(esClient: ILegacyClusterClient) { } describe('licensing plugin', () => { + const createEsClient = (response?: Record) => { + const client = elasticsearchServiceMock.createClusterClient(); + if (response) { + client.asInternalUser.xpack.info.mockReturnValue( + elasticsearchServiceMock.createSuccessTransportRequestPromise(response as any) + ); + } + return client; + }; + describe('#start', () => { describe('#license$', () => { let plugin: LicensingPlugin; @@ -69,8 +78,7 @@ describe('licensing plugin', () => { }); it('returns license', async () => { - const esClient = elasticsearchServiceMock.createLegacyClusterClient(); - esClient.callAsInternalUser.mockResolvedValue({ + const esClient = createEsClient({ license: buildRawLicense(), features: {}, }); @@ -83,8 +91,7 @@ describe('licensing plugin', () => { }); it('calls `callAsInternalUser` with the correct parameters', async () => { - const esClient = elasticsearchServiceMock.createLegacyClusterClient(); - esClient.callAsInternalUser.mockResolvedValue({ + const esClient = createEsClient({ license: buildRawLicense(), features: {}, }); @@ -94,23 +101,22 @@ describe('licensing plugin', () => { const { license$ } = await plugin.start(); await license$.pipe(take(1)).toPromise(); - expect(esClient.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(esClient.callAsInternalUser).toHaveBeenCalledWith('transport.request', { - method: 'GET', - path: '/_xpack?accept_enterprise=true', + expect(esClient.asInternalUser.xpack.info).toHaveBeenCalledTimes(1); + expect(esClient.asInternalUser.xpack.info).toHaveBeenCalledWith({ + accept_enterprise: true, }); }); it('observable receives updated licenses', async () => { const types: LicenseType[] = ['basic', 'gold', 'platinum']; - const esClient = elasticsearchServiceMock.createLegacyClusterClient(); - esClient.callAsInternalUser.mockImplementation(() => - Promise.resolve({ + const esClient = createEsClient(); + esClient.asInternalUser.xpack.info.mockImplementation(() => { + return elasticsearchServiceMock.createSuccessTransportRequestPromise({ license: buildRawLicense({ type: types.shift() }), features: {}, - }) - ); + } as estypes.XpackInfoResponse); + }); const coreSetup = createCoreSetupWith(esClient); await plugin.setup(coreSetup); @@ -123,8 +129,8 @@ describe('licensing plugin', () => { }); it('returns a license with error when request fails', async () => { - const esClient = elasticsearchServiceMock.createLegacyClusterClient(); - esClient.callAsInternalUser.mockRejectedValue(new Error('test')); + const esClient = createEsClient(); + esClient.asInternalUser.xpack.info.mockRejectedValue(new Error('test')); const coreSetup = createCoreSetupWith(esClient); await plugin.setup(coreSetup); @@ -136,10 +142,10 @@ describe('licensing plugin', () => { }); it('generate error message when x-pack plugin was not installed', async () => { - const esClient = elasticsearchServiceMock.createLegacyClusterClient(); + const esClient = createEsClient(); const error: ElasticsearchError = new Error('reason'); error.status = 400; - esClient.callAsInternalUser.mockRejectedValue(error); + esClient.asInternalUser.xpack.info.mockRejectedValue(error); const coreSetup = createCoreSetupWith(esClient); await plugin.setup(coreSetup); @@ -154,26 +160,35 @@ describe('licensing plugin', () => { const error1 = new Error('reason-1'); const error2 = new Error('reason-2'); - const esClient = elasticsearchServiceMock.createLegacyClusterClient(); - - esClient.callAsInternalUser - .mockRejectedValueOnce(error1) - .mockRejectedValueOnce(error2) - .mockResolvedValue({ license: buildRawLicense(), features: {} }); + const esClient = createEsClient(); + let i = 0; + esClient.asInternalUser.xpack.info.mockImplementation(() => { + i++; + if (i === 1) { + return elasticsearchServiceMock.createErrorTransportRequestPromise(error1); + } + if (i === 2) { + return elasticsearchServiceMock.createErrorTransportRequestPromise(error2); + } + return elasticsearchServiceMock.createSuccessTransportRequestPromise({ + license: buildRawLicense(), + features: {}, + } as estypes.XpackInfoResponse); + }); 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.error).toBe(error1.message); expect(second.error).toBe(error2.message); expect(third.type).toBe('basic'); }); it('fetch license immediately without subscriptions', async () => { - const esClient = elasticsearchServiceMock.createLegacyClusterClient(); - esClient.callAsInternalUser.mockResolvedValue({ + const esClient = createEsClient({ license: buildRawLicense(), features: {}, }); @@ -184,12 +199,11 @@ describe('licensing plugin', () => { await flushPromises(); - expect(esClient.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(esClient.asInternalUser.xpack.info).toHaveBeenCalledTimes(1); }); it('logs license details without subscriptions', async () => { - const esClient = elasticsearchServiceMock.createLegacyClusterClient(); - esClient.callAsInternalUser.mockResolvedValue({ + const esClient = createEsClient({ license: buildRawLicense(), features: {}, }); @@ -214,13 +228,13 @@ describe('licensing plugin', () => { it('generates signature based on fetched license content', async () => { const types: LicenseType[] = ['basic', 'gold', 'basic']; - const esClient = elasticsearchServiceMock.createLegacyClusterClient(); - esClient.callAsInternalUser.mockImplementation(() => - Promise.resolve({ + const esClient = createEsClient(); + esClient.asInternalUser.xpack.info.mockImplementation(() => { + return elasticsearchServiceMock.createSuccessTransportRequestPromise({ license: buildRawLicense({ type: types.shift() }), features: {}, - }) - ); + } as estypes.XpackInfoResponse); + }); const coreSetup = createCoreSetupWith(esClient); await plugin.setup(coreSetup); @@ -245,8 +259,7 @@ describe('licensing plugin', () => { api_polling_frequency: moment.duration(50000), }) ); - const esClient = elasticsearchServiceMock.createLegacyClusterClient(); - esClient.callAsInternalUser.mockResolvedValue({ + const esClient = createEsClient({ license: buildRawLicense(), features: {}, }); @@ -255,14 +268,14 @@ describe('licensing plugin', () => { await plugin.setup(coreSetup); const { refresh, license$ } = await plugin.start(); - expect(esClient.callAsInternalUser).toHaveBeenCalledTimes(0); + expect(esClient.asInternalUser.xpack.info).toHaveBeenCalledTimes(0); await license$.pipe(take(1)).toPromise(); - expect(esClient.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(esClient.asInternalUser.xpack.info).toHaveBeenCalledTimes(1); refresh(); await flushPromises(); - expect(esClient.callAsInternalUser).toHaveBeenCalledTimes(2); + expect(esClient.asInternalUser.xpack.info).toHaveBeenCalledTimes(2); }); }); @@ -280,8 +293,7 @@ describe('licensing plugin', () => { }) ); - const esClient = elasticsearchServiceMock.createLegacyClusterClient(); - esClient.callAsInternalUser.mockResolvedValue({ + const esClient = createEsClient({ license: buildRawLicense(), features: {}, }); @@ -289,8 +301,7 @@ describe('licensing plugin', () => { await plugin.setup(coreSetup); const { createLicensePoller, license$ } = await plugin.start(); - const customClient = elasticsearchServiceMock.createLegacyClusterClient(); - customClient.callAsInternalUser.mockResolvedValue({ + const customClient = createEsClient({ license: buildRawLicense({ type: 'gold' }), features: {}, }); @@ -300,10 +311,10 @@ describe('licensing plugin', () => { customClient, customPollingFrequency ); - expect(customClient.callAsInternalUser).toHaveBeenCalledTimes(0); + expect(customClient.asInternalUser.xpack.info).toHaveBeenCalledTimes(0); const customLicense = await customLicense$.pipe(take(1)).toPromise(); - expect(customClient.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(customClient.asInternalUser.xpack.info).toHaveBeenCalledTimes(1); await flushPromises(customPollingFrequency * 1.5); @@ -324,18 +335,17 @@ describe('licensing plugin', () => { await plugin.setup(coreSetup); const { createLicensePoller } = await plugin.start(); - const customClient = elasticsearchServiceMock.createLegacyClusterClient(); - customClient.callAsInternalUser.mockResolvedValue({ + const customClient = createEsClient({ license: buildRawLicense({ type: 'gold' }), features: {}, }); const { license$, refresh } = createLicensePoller(customClient, 10000); - expect(customClient.callAsInternalUser).toHaveBeenCalledTimes(0); + expect(customClient.asInternalUser.xpack.info).toHaveBeenCalledTimes(0); await refresh(); - expect(customClient.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(customClient.asInternalUser.xpack.info).toHaveBeenCalledTimes(1); const license = await license$.pipe(take(1)).toPromise(); expect(license.type).toBe('gold'); }); diff --git a/x-pack/plugins/licensing/server/plugin.ts b/x-pack/plugins/licensing/server/plugin.ts index 7c0f0a119d2590..00d2ae602fcae5 100644 --- a/x-pack/plugins/licensing/server/plugin.ts +++ b/x-pack/plugins/licensing/server/plugin.ts @@ -10,22 +10,29 @@ import moment from 'moment'; import { createHash } from 'crypto'; import stringify from 'json-stable-stringify'; +import { estypes } from '@elastic/elasticsearch'; +import { MaybePromise } from '@kbn/utility-types'; +import { isPromise } from '@kbn/std'; import { CoreSetup, Logger, Plugin, PluginInitializerContext, - ILegacyClusterClient, - ILegacyScopedClusterClient, - ScopeableRequest, + IClusterClient, } from 'src/core/server'; -import { ILicense, PublicLicense, PublicFeatures } from '../common/types'; +import { + ILicense, + PublicLicense, + PublicFeatures, + LicenseType, + LicenseStatus, +} from '../common/types'; import { LicensingPluginSetup, LicensingPluginStart } from './types'; import { License } from '../common/license'; import { createLicenseUpdate } from '../common/license_update'; -import { ElasticsearchError, RawLicense, RawFeatures } from './types'; +import { ElasticsearchError } from './types'; import { registerRoutes } from './routes'; import { FeatureUsageService } from './services'; @@ -34,17 +41,22 @@ import { createRouteHandlerContext } from './licensing_route_handler_context'; import { createOnPreResponseHandler } from './on_pre_response_handler'; import { getPluginStatus$ } from './plugin_status'; -function normalizeServerLicense(license: RawLicense): PublicLicense { +function normalizeServerLicense( + license: estypes.XpackInfoMinimalLicenseInformation +): PublicLicense { return { uid: license.uid, - type: license.type, - mode: license.mode, - expiryDateInMillis: license.expiry_date_in_millis, - status: license.status, + type: license.type as LicenseType, + mode: license.mode as LicenseType, + expiryDateInMillis: + typeof license.expiry_date_in_millis === 'string' + ? parseInt(license.expiry_date_in_millis, 10) + : license.expiry_date_in_millis, + status: license.status as LicenseStatus, }; } -function normalizeFeatures(rawFeatures: RawFeatures) { +function normalizeFeatures(rawFeatures: estypes.XpackInfoFeatures) { const features: PublicFeatures = {}; for (const [name, feature] of Object.entries(rawFeatures)) { features[name] = { @@ -99,32 +111,12 @@ export class LicensingPlugin implements Plugin - ): ReturnType { - const [coreStart] = await core.getStartServices(); - const client = coreStart.elasticsearch.legacy.client; - return await client.callAsInternalUser(...args); - } - - const client: ILegacyClusterClient = { - callAsInternalUser, - asScoped(request?: ScopeableRequest): ILegacyScopedClusterClient { - 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 clientPromise = core.getStartServices().then(([{ elasticsearch }]) => { + return elasticsearch.client; + }); const { refresh, license$ } = this.createLicensePoller( - client, + clientPromise, pollingFrequency.asMilliseconds() ); @@ -146,12 +138,14 @@ export class LicensingPlugin implements Plugin, + pollingFrequency: number + ) { this.logger.debug(`Polling Elasticsearch License API with frequency ${pollingFrequency}ms.`); const intervalRefresh$ = timer(0, pollingFrequency); @@ -180,16 +174,17 @@ export class LicensingPlugin implements Plugin => { + private fetchLicense = async (clusterClient: MaybePromise): Promise => { + const client = isPromise(clusterClient) ? await clusterClient : clusterClient; try { - const response = await clusterClient.callAsInternalUser('transport.request', { - method: 'GET', - path: '/_xpack?accept_enterprise=true', + const { body: response } = await client.asInternalUser.xpack.info({ + // @ts-expect-error `accept_enterprise` is not present in the client definition + accept_enterprise: true, }); - - const normalizedLicense = response.license - ? normalizeServerLicense(response.license) - : undefined; + const normalizedLicense = + response.license && response.license.type !== 'missing' + ? normalizeServerLicense(response.license) + : undefined; const normalizedFeatures = response.features ? normalizeFeatures(response.features) : undefined; diff --git a/x-pack/plugins/licensing/server/types.ts b/x-pack/plugins/licensing/server/types.ts index 8b2d9599e0ffe2..ff13ee882be605 100644 --- a/x-pack/plugins/licensing/server/types.ts +++ b/x-pack/plugins/licensing/server/types.ts @@ -6,13 +6,14 @@ */ import { Observable } from 'rxjs'; -import type { ILegacyClusterClient, IRouter, RequestHandlerContext } from 'src/core/server'; -import { ILicense, LicenseStatus, LicenseType } from '../common/types'; +import type { IClusterClient, IRouter, RequestHandlerContext } from 'src/core/server'; +import { ILicense } from '../common/types'; import { FeatureUsageServiceSetup, FeatureUsageServiceStart } from './services'; export interface ElasticsearchError extends Error { status?: number; } + /** * Result from remote request fetching raw feature set. * @internal @@ -22,26 +23,6 @@ export interface RawFeature { enabled: boolean; } -/** - * Results from remote request fetching raw feature sets. - * @internal - */ -export interface RawFeatures { - [key: string]: RawFeature; -} - -/** - * Results from remote request fetching a raw license. - * @internal - */ -export interface RawLicense { - uid: string; - status: LicenseStatus; - expiry_date_in_millis: number; - type: LicenseType; - mode: LicenseType; -} - /** * The APIs exposed on the `licensing` key of {@link RequestHandlerContext} for plugins that depend on licensing. * @public @@ -70,21 +51,13 @@ export interface LicensingPluginSetup { * @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; - /** - * 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: ILegacyClusterClient, - pollingFrequency: number - ) => { license$: Observable; refresh(): Promise }; + /** * APIs to register licensed feature usage. */ @@ -97,17 +70,19 @@ 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: ILegacyClusterClient, + clusterClient: IClusterClient, pollingFrequency: number ) => { license$: Observable; refresh(): Promise }; /** diff --git a/x-pack/plugins/monitoring/server/license_service.ts b/x-pack/plugins/monitoring/server/license_service.ts index ab10193fc93cbd..0ac1c9d3498b17 100644 --- a/x-pack/plugins/monitoring/server/license_service.ts +++ b/x-pack/plugins/monitoring/server/license_service.ts @@ -6,17 +6,16 @@ */ import { Subscription } from 'rxjs'; -import { IClusterClient, ILegacyClusterClient } from 'kibana/server'; +import { ICustomClusterClient } from 'kibana/server'; import { ILicense, LicenseFeature } from '../../licensing/common/types'; import { LicensingPluginStart } from '../../licensing/server'; import { MonitoringConfig } from './config'; import { Logger } from '../../../../src/core/server'; import { MonitoringLicenseService } from './types'; -import { EndpointTypes, Globals, ClientParams } from './static_globals'; interface SetupDeps { licensing: LicensingPluginStart; - monitoringClient: IClusterClient; + monitoringClient: ICustomClusterClient; config: MonitoringConfig; log: Logger; } @@ -28,15 +27,8 @@ const defaultLicenseFeature: LicenseFeature = { export class LicenseService { public setup({ licensing, monitoringClient, config, log }: SetupDeps): MonitoringLicenseService { - // TODO: This needs to be changed to an IClusterClient as when the Licensing server - // is upgraded to the new client. - const fakeLegacyClusterClient = { - callAsInternalUser: (endpoint: EndpointTypes, options: ClientParams) => - Globals.app.getLegacyClusterShim(monitoringClient.asInternalUser, endpoint, options), - } as ILegacyClusterClient; - const { refresh, license$ } = licensing.createLicensePoller( - fakeLegacyClusterClient, + monitoringClient, config.licensing.api_polling_frequency.asMilliseconds() ); diff --git a/x-pack/plugins/monitoring/server/plugin.test.ts b/x-pack/plugins/monitoring/server/plugin.test.ts index 1bdef23dd0c369..68453a849a15b2 100644 --- a/x-pack/plugins/monitoring/server/plugin.test.ts +++ b/x-pack/plugins/monitoring/server/plugin.test.ts @@ -13,6 +13,9 @@ jest.mock('./es_client/instantiate_client', () => ({ instantiateClient: jest.fn().mockImplementation(() => ({ cluster: {}, })), + instantiateLegacyClient: jest.fn().mockImplementation(() => ({ + cluster: {}, + })), })); jest.mock('./license_service', () => ({