[Ingest Pipelines] Add license checks for processors (#154525)

This commit is contained in:
Ignacio Rivas 2023-04-12 14:28:51 +02:00 committed by GitHub
parent dd083a351a
commit aefc949911
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 89 additions and 16 deletions

View file

@ -11,6 +11,7 @@
"ingest_pipelines" "ingest_pipelines"
], ],
"requiredPlugins": [ "requiredPlugins": [
"licensing",
"management", "management",
"features", "features",
"share", "share",

View file

@ -6,8 +6,14 @@
*/ */
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { licensingMock } from '@kbn/licensing-plugin/server/mocks';
import { setup, SetupResult } from './pipeline_processors_editor.helpers'; import { setup, SetupResult } from './pipeline_processors_editor.helpers';
import { Pipeline } from '../../../../../common/types'; import { Pipeline } from '../../../../../common/types';
import {
extractProcessorDetails,
getProcessorTypesAndLabels,
} from '../components/processor_form/processors/common_fields/processor_type_field';
import { mapProcessorTypeToDescriptor } from '../components/shared/map_processor_type_to_form';
const testProcessors: Pick<Pipeline, 'processors'> = { const testProcessors: Pick<Pipeline, 'processors'> = {
processors: [ processors: [
@ -96,6 +102,28 @@ describe('Pipeline Editor', () => {
expect(d).toEqual({ test: { if: '1 == 1' } }); expect(d).toEqual({ test: { if: '1 == 1' } });
}); });
it('Shows inference and redact processors for licenses > platinum', async () => {
const basicLicense = licensingMock.createLicense({
license: { status: 'active', type: 'basic' },
});
const platinumLicense = licensingMock.createLicense({
license: { status: 'active', type: 'platinum' },
});
// Get the list of processors that are only available for platinum licenses
const processorsForPlatinumLicense = extractProcessorDetails(mapProcessorTypeToDescriptor)
.filter((processor) => processor.forLicenseAtLeast === 'platinum')
.map(({ value, label }) => ({ label, value }));
// Check that the list of processors for platinum licenses is not included in the list of processors for basic licenses
expect(getProcessorTypesAndLabels(basicLicense)).toEqual(
expect.not.arrayContaining(processorsForPlatinumLicense)
);
expect(getProcessorTypesAndLabels(platinumLicense)).toEqual(
expect.arrayContaining(processorsForPlatinumLicense)
);
});
it('edits a processor without removing unknown processor.options', async () => { it('edits a processor without removing unknown processor.options', async () => {
const { actions, exists, form } = testBed; const { actions, exists, form } = testBed;
// Open the edit processor form for the set processor // Open the edit processor form for the set processor

View file

@ -7,7 +7,7 @@
import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import React, { FunctionComponent, ReactNode } from 'react'; import React, { FunctionComponent, ReactNode, useMemo } from 'react';
import { flow } from 'fp-ts/lib/function'; import { flow } from 'fp-ts/lib/function';
import { map } from 'fp-ts/lib/Array'; import { map } from 'fp-ts/lib/Array';
@ -15,6 +15,7 @@ import {
FieldValidateResponse, FieldValidateResponse,
VALIDATION_TYPES, VALIDATION_TYPES,
} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import { ILicense } from '../../../../../../../types';
import { import {
FIELD_TYPES, FIELD_TYPES,
FieldConfig, FieldConfig,
@ -25,11 +26,12 @@ import {
import { getProcessorDescriptor, mapProcessorTypeToDescriptor } from '../../../shared'; import { getProcessorDescriptor, mapProcessorTypeToDescriptor } from '../../../shared';
const extractProcessorTypesAndLabels = flow( export const extractProcessorDetails = flow(
Object.entries, Object.entries,
map(([type, { label }]) => ({ map(([type, { label, forLicenseAtLeast }]) => ({
label, label,
value: type, value: type,
...(forLicenseAtLeast ? { forLicenseAtLeast } : {}),
})), })),
(arr) => arr.sort((a, b) => a.label.localeCompare(b.label)) (arr) => arr.sort((a, b) => a.label.localeCompare(b.label))
); );
@ -39,9 +41,17 @@ interface ProcessorTypeAndLabel {
label: string; label: string;
} }
const processorTypesAndLabels: ProcessorTypeAndLabel[] = extractProcessorTypesAndLabels( export const getProcessorTypesAndLabels = (license: ILicense | null) => {
mapProcessorTypeToDescriptor return (
); extractProcessorDetails(mapProcessorTypeToDescriptor)
// Filter out any processors that are not available for the current license type
.filter((option) => {
return option.forLicenseAtLeast ? license?.hasAtLeast(option.forLicenseAtLeast) : true;
})
// Convert to EuiComboBox options
.map(({ value, label }) => ({ label, value }))
);
};
interface Props { interface Props {
initialType?: string; initialType?: string;
@ -68,9 +78,12 @@ const typeConfig: FieldConfig<string> = {
export const ProcessorTypeField: FunctionComponent<Props> = ({ initialType }) => { export const ProcessorTypeField: FunctionComponent<Props> = ({ initialType }) => {
const { const {
services: { documentation }, services: { documentation, license },
} = useKibana(); } = useKibana();
const esDocUrl = documentation.getEsDocsBasePath(); const esDocUrl = documentation.getEsDocsBasePath();
// Some processors are only available for certain license types
const processorOptions = useMemo(() => getProcessorTypesAndLabels(license), [license]);
return ( return (
<UseField<string> config={typeConfig} defaultValue={initialType} path="type"> <UseField<string> config={typeConfig} defaultValue={initialType} path="type">
{(typeField) => { {(typeField) => {
@ -128,7 +141,7 @@ export const ProcessorTypeField: FunctionComponent<Props> = ({ initialType }) =>
defaultMessage: 'Type and then hit "ENTER"', defaultMessage: 'Type and then hit "ENTER"',
} }
)} )}
options={processorTypesAndLabels} options={processorOptions}
selectedOptions={selectedOptions} selectedOptions={selectedOptions}
onCreateOption={onCreateComboOption} onCreateOption={onCreateComboOption}
onChange={(options: Array<EuiComboBoxOptionOption<string>>) => { onChange={(options: Array<EuiComboBoxOptionOption<string>>) => {

View file

@ -10,6 +10,8 @@ import React, { ReactNode } from 'react';
import { FormattedMessage } from '@kbn/i18n-react'; import { FormattedMessage } from '@kbn/i18n-react';
import { EuiCode, EuiLink } from '@elastic/eui'; import { EuiCode, EuiLink } from '@elastic/eui';
import { LicenseType } from '../../../../../types';
import { import {
Append, Append,
Bytes, Bytes,
@ -69,6 +71,10 @@ interface FieldDescriptor {
* Default * Default
*/ */
getDefaultDescription: (processorOptions: Record<string, any>) => string | undefined; getDefaultDescription: (processorOptions: Record<string, any>) => string | undefined;
/**
* Some processors are only available for certain license types
*/
forLicenseAtLeast?: LicenseType;
} }
type MapProcessorTypeToDescriptor = Record<string, FieldDescriptor>; type MapProcessorTypeToDescriptor = Record<string, FieldDescriptor>;
@ -453,6 +459,7 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
}, },
inference: { inference: {
FieldsComponent: Inference, FieldsComponent: Inference,
forLicenseAtLeast: 'platinum',
docLinkPath: '/inference-processor.html', docLinkPath: '/inference-processor.html',
label: i18n.translate('xpack.ingestPipelines.processors.label.inference', { label: i18n.translate('xpack.ingestPipelines.processors.label.inference', {
defaultMessage: 'Inference', defaultMessage: 'Inference',
@ -580,6 +587,7 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
}, },
redact: { redact: {
FieldsComponent: Redact, FieldsComponent: Redact,
forLicenseAtLeast: 'platinum',
docLinkPath: '/redact-processor.html', docLinkPath: '/redact-processor.html',
label: i18n.translate('xpack.ingestPipelines.processors.label.redact', { label: i18n.translate('xpack.ingestPipelines.processors.label.redact', {
defaultMessage: 'Redact', defaultMessage: 'Redact',

View file

@ -16,6 +16,7 @@ import { ManagementAppMountParams } from '@kbn/management-plugin/public';
import type { SharePluginStart } from '@kbn/share-plugin/public'; import type { SharePluginStart } from '@kbn/share-plugin/public';
import type { FileUploadPluginStart } from '@kbn/file-upload-plugin/public'; import type { FileUploadPluginStart } from '@kbn/file-upload-plugin/public';
import { KibanaContextProvider, KibanaThemeProvider } from '../shared_imports'; import { KibanaContextProvider, KibanaThemeProvider } from '../shared_imports';
import { ILicense } from '../types';
import { API_BASE_PATH } from '../../common/constants'; import { API_BASE_PATH } from '../../common/constants';
@ -42,6 +43,7 @@ export interface AppServices {
share: SharePluginStart; share: SharePluginStart;
fileUpload: FileUploadPluginStart; fileUpload: FileUploadPluginStart;
application: ApplicationStart; application: ApplicationStart;
license: ILicense | null;
} }
export interface CoreServices { export interface CoreServices {

View file

@ -8,7 +8,7 @@
import { CoreSetup } from '@kbn/core/public'; import { CoreSetup } from '@kbn/core/public';
import { ManagementAppMountParams } from '@kbn/management-plugin/public'; import { ManagementAppMountParams } from '@kbn/management-plugin/public';
import { StartDependencies } from '../types'; import { StartDependencies, ILicense } from '../types';
import { import {
documentationService, documentationService,
uiMetricService, uiMetricService,
@ -18,11 +18,15 @@ import {
} from './services'; } from './services';
import { renderApp } from '.'; import { renderApp } from '.';
export interface AppParams extends ManagementAppMountParams {
license: ILicense | null;
}
export async function mountManagementSection( export async function mountManagementSection(
{ http, getStartServices, notifications }: CoreSetup<StartDependencies>, { http, getStartServices, notifications }: CoreSetup<StartDependencies>,
params: ManagementAppMountParams params: AppParams
) { ) {
const { element, setBreadcrumbs, history, theme$ } = params; const { element, setBreadcrumbs, history, theme$, license } = params;
const [coreStart, depsStart] = await getStartServices(); const [coreStart, depsStart] = await getStartServices();
const { const {
docLinks, docLinks,
@ -47,6 +51,7 @@ export async function mountManagementSection(
fileUpload: depsStart.fileUpload, fileUpload: depsStart.fileUpload,
application, application,
executionContext, executionContext,
license,
}; };
return renderApp(element, I18nContext, services, { http }, { theme$ }); return renderApp(element, I18nContext, services, { http }, { theme$ });

View file

@ -6,16 +6,20 @@
*/ */
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { CoreSetup, Plugin } from '@kbn/core/public'; import { Subscription } from 'rxjs';
import { CoreStart, CoreSetup, Plugin } from '@kbn/core/public';
import { PLUGIN_ID } from '../common/constants'; import { PLUGIN_ID } from '../common/constants';
import { uiMetricService, apiService } from './application/services'; import { uiMetricService, apiService } from './application/services';
import { SetupDependencies, StartDependencies } from './types'; import { SetupDependencies, StartDependencies, ILicense } from './types';
import { IngestPipelinesLocatorDefinition } from './locator'; import { IngestPipelinesLocatorDefinition } from './locator';
export class IngestPipelinesPlugin export class IngestPipelinesPlugin
implements Plugin<void, void, SetupDependencies, StartDependencies> implements Plugin<void, void, SetupDependencies, StartDependencies>
{ {
private license: ILicense | null = null;
private licensingSubscription?: Subscription;
public setup(coreSetup: CoreSetup<StartDependencies>, plugins: SetupDependencies): void { public setup(coreSetup: CoreSetup<StartDependencies>, plugins: SetupDependencies): void {
const { management, usageCollection, share } = plugins; const { management, usageCollection, share } = plugins;
const { http, getStartServices } = coreSetup; const { http, getStartServices } = coreSetup;
@ -42,7 +46,10 @@ export class IngestPipelinesPlugin
docTitle.change(pluginName); docTitle.change(pluginName);
const { mountManagementSection } = await import('./application/mount_management_section'); const { mountManagementSection } = await import('./application/mount_management_section');
const unmountAppCallback = await mountManagementSection(coreSetup, params); const unmountAppCallback = await mountManagementSection(coreSetup, {
...params,
license: this.license,
});
return () => { return () => {
docTitle.reset(); docTitle.reset();
@ -58,7 +65,13 @@ export class IngestPipelinesPlugin
); );
} }
public start() {} public start(core: CoreStart, { licensing }: StartDependencies) {
this.licensingSubscription = licensing?.license$.subscribe((license) => {
this.license = license;
});
}
public stop() {} public stop() {
this.licensingSubscription?.unsubscribe();
}
} }

View file

@ -9,6 +9,8 @@ import { ManagementSetup } from '@kbn/management-plugin/public';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import { SharePluginStart, SharePluginSetup } from '@kbn/share-plugin/public'; import { SharePluginStart, SharePluginSetup } from '@kbn/share-plugin/public';
import type { FileUploadPluginStart } from '@kbn/file-upload-plugin/public'; import type { FileUploadPluginStart } from '@kbn/file-upload-plugin/public';
import { LicensingPluginStart } from '@kbn/licensing-plugin/public';
export type { LicenseType, ILicense } from '@kbn/licensing-plugin/public';
export interface SetupDependencies { export interface SetupDependencies {
management: ManagementSetup; management: ManagementSetup;
@ -19,4 +21,5 @@ export interface SetupDependencies {
export interface StartDependencies { export interface StartDependencies {
share: SharePluginStart; share: SharePluginStart;
fileUpload: FileUploadPluginStart; fileUpload: FileUploadPluginStart;
licensing?: LicensingPluginStart;
} }