diff --git a/packages/kbn-custom-integrations/src/components/create/form.tsx b/packages/kbn-custom-integrations/src/components/create/form.tsx index 2273db68a479a..a05fa97b78fdf 100644 --- a/packages/kbn-custom-integrations/src/components/create/form.tsx +++ b/packages/kbn-custom-integrations/src/components/create/form.tsx @@ -28,6 +28,11 @@ import { } from '../../state_machines/create/types'; import { Dataset, IntegrationError } from '../../types'; import { hasFailedSelector } from '../../state_machines/create/selectors'; +import { + datasetNameWillBePrefixed, + getDatasetNamePrefix, + prefixDatasetName, +} from '../../state_machines/create/pipelines/fields'; // NOTE: Hardcoded for now. We will likely extend the functionality here to allow the selection of the type. // And also to allow adding multiple datasets. @@ -50,6 +55,7 @@ export const ConnectedCreateCustomIntegrationForm = ({ testSubjects?: CreateTestSubjects; }) => { const [state, send] = useActor(machineRef); + const updateIntegrationName = useCallback( (integrationName: string) => { send({ type: 'UPDATE_FIELDS', fields: { integrationName } }); @@ -181,17 +187,32 @@ export const CreateCustomIntegrationForm = ({ } - helpText={i18n.translate('customIntegrationsPackage.create.dataset.helper', { - defaultMessage: - "All lowercase, max 100 chars, special characters will be replaced with '_'.", - })} + helpText={[ + i18n.translate('customIntegrationsPackage.create.dataset.helper', { + defaultMessage: + "All lowercase, max 100 chars, special characters will be replaced with '_'.", + }), + datasetNameWillBePrefixed(datasetName, integrationName) + ? i18n.translate( + 'customIntegrationsPackage.create.dataset.name.tooltipPrefixMessage', + { + defaultMessage: + 'This name will be prefixed with {prefixValue}, e.g. {prefixedDatasetName}', + values: { + prefixValue: getDatasetNamePrefix(integrationName), + prefixedDatasetName: prefixDatasetName(datasetName, integrationName), + }, + } + ) + : '', + ].join(' ')} isInvalid={hasErrors(errors?.fields?.datasets?.[0]?.name) && touchedFields.datasets} error={errorsList(errors?.fields?.datasets?.[0]?.name)} > diff --git a/packages/kbn-custom-integrations/src/components/create/utils.ts b/packages/kbn-custom-integrations/src/components/create/utils.ts index 3af857be37236..688bea61128ed 100644 --- a/packages/kbn-custom-integrations/src/components/create/utils.ts +++ b/packages/kbn-custom-integrations/src/components/create/utils.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -export const replaceSpecialChars = (filename: string) => { +export const replaceSpecialChars = (value: string) => { // Replace special characters with _ - const replacedSpecialCharacters = filename.replaceAll(/[^a-zA-Z0-9_]/g, '_'); + const replacedSpecialCharacters = value.replaceAll(/[^a-zA-Z0-9_]/g, '_'); // Allow only one _ in a row const noRepetitions = replacedSpecialCharacters.replaceAll(/[\_]{2,}/g, '_'); return noRepetitions; diff --git a/packages/kbn-custom-integrations/src/state_machines/create/notifications.ts b/packages/kbn-custom-integrations/src/state_machines/create/notifications.ts index 38ef7fb993906..2ca7e09ae8f6e 100644 --- a/packages/kbn-custom-integrations/src/state_machines/create/notifications.ts +++ b/packages/kbn-custom-integrations/src/state_machines/create/notifications.ts @@ -27,11 +27,20 @@ export type CreateCustomIntegrationNotificationEvent = }; export const CreateIntegrationNotificationEventSelectors = { - integrationCreated: (context: CreateCustomIntegrationContext) => - ({ - type: 'INTEGRATION_CREATED', - fields: context.fields, - } as CreateCustomIntegrationNotificationEvent), + integrationCreated: ( + context: CreateCustomIntegrationContext, + event: CreateCustomIntegrationEvent + ) => { + return 'data' in event && 'integrationName' in event.data && 'datasets' in event.data + ? ({ + type: 'INTEGRATION_CREATED', + fields: { + integrationName: event.data.integrationName, + datasets: event.data.datasets, + }, + } as CreateCustomIntegrationNotificationEvent) + : null; + }, integrationCleanup: ( context: CreateCustomIntegrationContext, event: CreateCustomIntegrationEvent diff --git a/packages/kbn-custom-integrations/src/state_machines/create/pipelines/fields.ts b/packages/kbn-custom-integrations/src/state_machines/create/pipelines/fields.ts new file mode 100644 index 0000000000000..3a7fef9ce98a7 --- /dev/null +++ b/packages/kbn-custom-integrations/src/state_machines/create/pipelines/fields.ts @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { replaceSpecialChars } from '../../../components/create/utils'; +import { CreateCustomIntegrationContext, UpdateFieldsEvent, WithTouchedFields } from '../types'; + +type ValuesTuple = [CreateCustomIntegrationContext, UpdateFieldsEvent]; + +// Pipeline for updating the fields and touchedFields properties within context +export const executeFieldsPipeline = ( + context: CreateCustomIntegrationContext, + event: UpdateFieldsEvent +) => { + return pipe( + [context, event] as ValuesTuple, + updateFields(context), + updateTouchedFields(context), + maybeMatchDatasetNameToIntegrationName(context), + replaceSpecialCharacters(context) + ); +}; + +const updateFields = + (originalContext: CreateCustomIntegrationContext) => + (values: ValuesTuple): ValuesTuple => { + const [context, event] = values; + + const mergedContext = { + ...context, + fields: { + ...context.fields, + ...event.fields, + }, + }; + return [mergedContext, event]; + }; + +const updateTouchedFields = + (originalContext: CreateCustomIntegrationContext) => + (values: ValuesTuple): ValuesTuple => { + const [context, event] = values; + + const mergedContext = { + ...context, + touchedFields: { + ...context.touchedFields, + ...Object.keys(event.fields).reduce( + (acc, field) => ({ ...acc, [field]: true }), + {} as WithTouchedFields['touchedFields'] + ), + }, + }; + return [mergedContext, event]; + }; + +const maybeMatchDatasetNameToIntegrationName = + (originalContext: CreateCustomIntegrationContext) => + (values: ValuesTuple): ValuesTuple => { + const [context, event] = values; + if (context.touchedFields.integrationName && !context.touchedFields.datasets) { + return [ + { + ...context, + fields: { + ...context.fields, + datasets: context.fields.datasets.map((dataset, index) => ({ + ...dataset, + name: index === 0 ? context.fields.integrationName : dataset.name, + })), + }, + }, + event, + ]; + } else { + return [context, event]; + } + }; + +const replaceSpecialCharacters = + (originalContext: CreateCustomIntegrationContext) => + (values: ValuesTuple): ValuesTuple => { + const [context, event] = values; + + const mergedContext = { + ...context, + fields: { + ...context.fields, + integrationName: replaceSpecialChars(context.fields.integrationName), + datasets: context.fields.datasets.map((dataset) => ({ + ...dataset, + name: replaceSpecialChars(dataset.name), + })), + }, + }; + + return [mergedContext, event]; + }; + +export const getDatasetNamePrefix = (integrationName: string) => `${integrationName}.`; +export const datasetNameIsPrefixed = (datasetName: string, integrationName: string) => + datasetName.startsWith(getDatasetNamePrefix(integrationName)); +export const datasetNameWillBePrefixed = (datasetName: string, integrationName: string) => + datasetName !== integrationName; +export const prefixDatasetName = (datasetName: string, integrationName: string) => + `${getDatasetNamePrefix(integrationName)}${datasetName}`; + +// The content after the integration name prefix. +export const getDatasetNameWithoutPrefix = (datasetName: string, integrationName: string) => + datasetNameIsPrefixed(datasetName, integrationName) + ? datasetName.split(getDatasetNamePrefix(integrationName))[1] + : datasetName; + +// The machine holds unprefixed names internally to dramatically reduce complexity and improve performance for input changes in the UI. +// Prefixed names are used at the outermost edges e.g. when providing initial state and submitting to the API. +export const normalizeDatasetNames = (fields: UpdateFieldsEvent['fields']) => { + const value = { + ...fields, + ...(fields.datasets !== undefined && fields.integrationName !== undefined + ? { + datasets: fields.datasets.map((dataset) => ({ + ...dataset, + name: getDatasetNameWithoutPrefix(dataset.name, fields.integrationName!), + })), + } + : {}), + }; + return value; +}; diff --git a/packages/kbn-custom-integrations/src/state_machines/create/state_machine.ts b/packages/kbn-custom-integrations/src/state_machines/create/state_machine.ts index ad93d3eb8cb05..2ae0e51bad3d8 100644 --- a/packages/kbn-custom-integrations/src/state_machines/create/state_machine.ts +++ b/packages/kbn-custom-integrations/src/state_machines/create/state_machine.ts @@ -28,10 +28,12 @@ import { DefaultCreateCustomIntegrationContext, WithErrors, WithPreviouslyCreatedIntegration, - WithTouchedFields, - WithFields, } from './types'; -import { replaceSpecialChars } from '../../components/create/utils'; +import { + datasetNameWillBePrefixed, + executeFieldsPipeline, + prefixDatasetName, +} from './pipelines/fields'; export const createPureCreateCustomIntegrationStateMachine = ( initialContext: DefaultCreateCustomIntegrationContext = DEFAULT_CONTEXT @@ -211,32 +213,12 @@ export const createPureCreateCustomIntegrationStateMachine = ( : {}; }), storeFields: actions.assign((context, event) => { - return event.type === 'UPDATE_FIELDS' - ? ({ - fields: { - ...context.fields, - ...event.fields, - integrationName: - event.fields.integrationName !== undefined - ? replaceSpecialChars(event.fields.integrationName) - : context.fields.integrationName, - datasets: - event.fields.datasets !== undefined - ? event.fields.datasets.map((dataset) => ({ - ...dataset, - name: replaceSpecialChars(dataset.name), - })) - : context.fields.datasets, - }, - touchedFields: { - ...context.touchedFields, - ...Object.keys(event.fields).reduce( - (acc, field) => ({ ...acc, [field]: true }), - {} as WithTouchedFields['touchedFields'] - ), - }, - } as WithFields & WithTouchedFields) - : {}; + if (event.type === 'UPDATE_FIELDS') { + const [contextResult] = executeFieldsPipeline(context, event); + return contextResult; + } else { + return {}; + } }), resetValues: actions.assign((context, event) => { return { @@ -315,7 +297,15 @@ export const createCreateCustomIntegrationStateMachine = ({ }), }), save: (context) => { - return integrationsClient.createCustomIntegration(context.fields); + return integrationsClient.createCustomIntegration({ + ...context.fields, + datasets: context.fields.datasets.map((dataset) => ({ + ...dataset, + name: datasetNameWillBePrefixed(dataset.name, context.fields.integrationName) + ? prefixDatasetName(dataset.name, context.fields.integrationName) + : dataset.name, + })), + }); }, cleanup: (context) => { return integrationsClient.deleteCustomIntegration({ diff --git a/packages/kbn-custom-integrations/src/state_machines/create/types.ts b/packages/kbn-custom-integrations/src/state_machines/create/types.ts index e7cfad728d78e..ec0e61f2309d9 100644 --- a/packages/kbn-custom-integrations/src/state_machines/create/types.ts +++ b/packages/kbn-custom-integrations/src/state_machines/create/types.ts @@ -19,7 +19,9 @@ export interface WithTouchedFields { touchedFields: Record; } -export type CreateInitialState = WithOptions & WithFields & WithPreviouslyCreatedIntegration; +export type CreateInitialState = Partial & + Partial & + WithPreviouslyCreatedIntegration; export interface WithOptions { options: { @@ -91,11 +93,13 @@ export type CreateCustomIntegrationTypestate = export type CreateCustomIntegrationContext = CreateCustomIntegrationTypestate['context']; +export interface UpdateFieldsEvent { + type: 'UPDATE_FIELDS'; + fields: Partial; +} + export type CreateCustomIntegrationEvent = - | { - type: 'UPDATE_FIELDS'; - fields: Partial; - } + | UpdateFieldsEvent | { type: 'INITIALIZE'; } diff --git a/packages/kbn-custom-integrations/src/state_machines/custom_integrations/state_machine.ts b/packages/kbn-custom-integrations/src/state_machines/custom_integrations/state_machine.ts index 4bd71313bf43a..3baa44d76a7e6 100644 --- a/packages/kbn-custom-integrations/src/state_machines/custom_integrations/state_machine.ts +++ b/packages/kbn-custom-integrations/src/state_machines/custom_integrations/state_machine.ts @@ -20,6 +20,8 @@ import { import { createCreateCustomIntegrationStateMachine } from '../create/state_machine'; import { IIntegrationsClient } from '../services/integrations_client'; import { CustomIntegrationsNotificationChannel } from './notifications'; +import { executeFieldsPipeline, normalizeDatasetNames } from '../create/pipelines/fields'; +import { CreateInitialState } from '../create/types'; export const createPureCustomIntegrationsStateMachine = ( initialContext: DefaultCustomIntegrationsContext = DEFAULT_CONTEXT @@ -95,22 +97,32 @@ export const createCustomIntegrationsStateMachine = ({ return createPureCustomIntegrationsStateMachine(initialContext).withConfig({ services: { createCustomIntegration: (context) => { + const getInitialContextForCreate = (initialCreateState: CreateInitialState) => { + const baseAndOptions = { + ...DEFAULT_CREATE_CONTEXT, + ...(initialCreateState ? initialCreateState : {}), + options: { + ...DEFAULT_CREATE_CONTEXT.options, + ...(initialCreateState?.options ? initialCreateState.options : {}), + }, + }; + const fields = initialCreateState.fields + ? executeFieldsPipeline(baseAndOptions, { + type: 'UPDATE_FIELDS', + fields: normalizeDatasetNames(initialCreateState.fields), + })[0] + : {}; + return { + ...baseAndOptions, + ...fields, + }; + }; + return createCreateCustomIntegrationStateMachine({ integrationsClient, initialContext: - initialState.mode === 'create' - ? { - ...DEFAULT_CREATE_CONTEXT, - ...(initialState?.context ? initialState?.context : {}), - options: { - ...DEFAULT_CREATE_CONTEXT.options, - ...(initialState?.context?.options ? initialState.context.options : {}), - }, - fields: { - ...DEFAULT_CREATE_CONTEXT.fields, - ...(initialState?.context?.fields ? initialState.context.fields : {}), - }, - } + initialState.mode === 'create' && initialState.context + ? getInitialContextForCreate(initialState.context) : DEFAULT_CREATE_CONTEXT, }); }, diff --git a/packages/kbn-custom-integrations/src/state_machines/services/integrations_client.ts b/packages/kbn-custom-integrations/src/state_machines/services/integrations_client.ts index c5606a7e2e154..047bd4a7644d0 100644 --- a/packages/kbn-custom-integrations/src/state_machines/services/integrations_client.ts +++ b/packages/kbn-custom-integrations/src/state_machines/services/integrations_client.ts @@ -19,6 +19,8 @@ import { IntegrationNotInstalledError, NamingCollisionError, UnknownError, + IntegrationName, + Dataset, } from '../../types'; const GENERIC_CREATE_ERROR_MESSAGE = i18n.translate( @@ -70,6 +72,7 @@ export class IntegrationsClient implements IIntegrationsClient { return { integrationName: params.integrationName, + datasets: params.datasets, installedAssets: data.items, }; } catch (error) { @@ -128,7 +131,8 @@ export const createCustomIntegrationResponseRT = rt.exact( ); export interface CreateCustomIntegrationValue { - integrationName: string; + integrationName: IntegrationName; + datasets: Dataset[]; installedAssets: AssetList; } diff --git a/packages/kbn-custom-integrations/src/types.ts b/packages/kbn-custom-integrations/src/types.ts index 062601239137a..84ecda1eb4df3 100644 --- a/packages/kbn-custom-integrations/src/types.ts +++ b/packages/kbn-custom-integrations/src/types.ts @@ -9,25 +9,26 @@ import * as rt from 'io-ts'; export const integrationNameRT = rt.string; +export type IntegrationName = rt.TypeOf; -const datasetTypes = rt.keyof({ +const datasetTypesRT = rt.keyof({ logs: null, metrics: null, }); -const dataset = rt.exact( +export const datasetRT = rt.exact( rt.type({ name: rt.string, - type: datasetTypes, + type: datasetTypesRT, }) ); -export type Dataset = rt.TypeOf; +export type Dataset = rt.TypeOf; export const customIntegrationOptionsRT = rt.exact( rt.type({ integrationName: integrationNameRT, - datasets: rt.array(dataset), + datasets: rt.array(datasetRT), }) ); diff --git a/x-pack/plugins/fleet/server/routes/epm/handlers.ts b/x-pack/plugins/fleet/server/routes/epm/handlers.ts index c507d9e6732a7..ffc0742706063 100644 --- a/x-pack/plugins/fleet/server/routes/epm/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/epm/handlers.ts @@ -85,6 +85,7 @@ import type { } from '../../types'; import { getDataStreams } from '../../services/epm/data_streams'; import { NamingCollisionError } from '../../services/epm/packages/custom_integrations/validation/check_naming_collision'; +import { DatasetNamePrefixError } from '../../services/epm/packages/custom_integrations/validation/check_dataset_name_format'; const CACHE_CONTROL_10_MINUTES_HEADER: HttpResponseOptions['headers'] = { 'cache-control': 'max-age=600', @@ -452,6 +453,13 @@ export const createCustomIntegrationHandler: FleetRequestHandler< message: error.message, }, }); + } else if (error instanceof DatasetNamePrefixError) { + return response.customError({ + statusCode: 422, + body: { + message: error.message, + }, + }); } return await defaultFleetErrorHandler({ error, response }); } diff --git a/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/dataset/utils.ts b/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/dataset/utils.ts index b984b8cddbbd3..c3512443c8828 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/dataset/utils.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/dataset/utils.ts @@ -15,7 +15,7 @@ export const generateDatastreamEntries = ( const { name, type } = dataset; return { type, - dataset: `${packageName}.${name}`, + dataset: `${name}`, title: `Data stream for the ${packageName} custom integration, and ${name} dataset.`, package: packageName, path: name, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/validation/check_dataset_name_format.ts b/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/validation/check_dataset_name_format.ts new file mode 100644 index 0000000000000..daa90f892034e --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/validation/check_dataset_name_format.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { CustomPackageDatasetConfiguration } from '../../install'; + +// Dataset name must either match integration name exactly OR be prefixed with integration name and a dot. +export const checkDatasetsNameFormat = ( + datasets: CustomPackageDatasetConfiguration[], + integrationName: string +) => { + const invalidNames = datasets + .filter((dataset) => { + const { name } = dataset; + return name !== integrationName && !name.startsWith(`${integrationName}.`); + }) + .map((dataset) => dataset.name); + + if (invalidNames.length > 0) { + throw new DatasetNamePrefixError( + `Dataset names '${invalidNames.join( + ', ' + )}' must either match integration name '${integrationName}' exactly or be prefixed with integration name and a dot (e.g. '${integrationName}.').` + ); + } +}; + +export class DatasetNamePrefixError extends Error { + constructor(message?: string) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + this.name = 'DatasetNamePrefixError'; + } +} diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index 4bb77d03996c7..da31b26d275e9 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -103,6 +103,7 @@ import { createAssets } from './custom_integrations'; import { cacheAssets } from './custom_integrations/assets/cache'; import { generateDatastreamEntries } from './custom_integrations/assets/dataset/utils'; import { checkForNamingCollision } from './custom_integrations/validation/check_naming_collision'; +import { checkDatasetsNameFormat } from './custom_integrations/validation/check_dataset_name_format'; export async function isPackageInstalled(options: { savedObjectsClient: SavedObjectsClientContract; @@ -784,6 +785,7 @@ export async function installCustomPackage( // Validate that we can create this package, validations will throw if they don't pass await checkForNamingCollision(savedObjectsClient, pkgName); + checkDatasetsNameFormat(datasets, pkgName); // Compose a packageInfo const packageInfo = { diff --git a/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/logs/custom_logs/install_elastic_agent.cy.ts b/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/logs/custom_logs/install_elastic_agent.cy.ts index 0843423f10b1b..19e84284b671b 100644 --- a/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/logs/custom_logs/install_elastic_agent.cy.ts +++ b/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/logs/custom_logs/install_elastic_agent.cy.ts @@ -623,7 +623,7 @@ describe('[Logs onboarding] Custom logs - install elastic agent', () => { cy.getByTestSubj('obltOnboardingExploreLogs').should('exist').click(); cy.url().should('include', '/app/observability-log-explorer'); - cy.get('button').contains('[mylogs] mylogs').should('exist'); + cy.get('button').contains('[Mylogs] mylogs').should('exist'); }); }); }); diff --git a/x-pack/plugins/observability_onboarding/e2e/cypress/support/commands.ts b/x-pack/plugins/observability_onboarding/e2e/cypress/support/commands.ts index f4a30e896c8db..e989089f491eb 100644 --- a/x-pack/plugins/observability_onboarding/e2e/cypress/support/commands.ts +++ b/x-pack/plugins/observability_onboarding/e2e/cypress/support/commands.ts @@ -107,9 +107,9 @@ Cypress.Commands.add('installCustomIntegration', (integrationName: string) => { force: true, integrationName, datasets: [ - { name: 'access', type: 'logs' }, - { name: 'error', type: 'metrics' }, - { name: 'warning', type: 'logs' }, + { name: `${integrationName}.access`, type: 'logs' }, + { name: `${integrationName}.error`, type: 'metrics' }, + { name: `${integrationName}.warning`, type: 'logs' }, ], }, headers: { diff --git a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/configure_logs.tsx b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/configure_logs.tsx index b5edcf75b1214..3bfe683d48c83 100644 --- a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/configure_logs.tsx +++ b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/configure_logs.tsx @@ -92,10 +92,14 @@ export function ConfigureLogs() { resetOnCreation: false, errorOnFailedCleanup: false, }, - fields: { - integrationName, - datasets: [{ name: datasetName, type: 'logs' as const }], - }, + ...(integrationName !== undefined && datasetName !== undefined + ? { + fields: { + integrationName, + datasets: [{ name: datasetName, type: 'logs' as const }], + }, + } + : {}), previouslyCreatedIntegration: lastCreatedIntegrationOptions, }, }} diff --git a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/index.tsx b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/index.tsx index 81d7faf448a48..afe575c08018c 100644 --- a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/index.tsx +++ b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/index.tsx @@ -17,9 +17,9 @@ import { InstallElasticAgent } from './install_elastic_agent'; import { SelectLogs } from './select_logs'; interface WizardState { - integrationName: string; + integrationName?: string; lastCreatedIntegrationOptions?: CustomIntegrationOptions; - datasetName: string; + datasetName?: string; serviceName: string; logFilePaths: string[]; namespace: string; @@ -40,8 +40,8 @@ interface WizardState { } const initialState: WizardState = { - integrationName: '', - datasetName: '', + integrationName: undefined, + datasetName: undefined, serviceName: '', logFilePaths: [''], namespace: 'default', diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_custom.ts b/x-pack/test/fleet_api_integration/apis/epm/install_custom.ts index 88d71edb74d47..63dfd7690e887 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_custom.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_custom.ts @@ -39,9 +39,9 @@ export default function (providerContext: FtrProviderContext) { force: true, integrationName: INTEGRATION_NAME, datasets: [ - { name: 'access', type: 'logs' }, - { name: 'error', type: 'metrics' }, - { name: 'warning', type: 'logs' }, + { name: `${INTEGRATION_NAME}.access`, type: 'logs' }, + { name: `${INTEGRATION_NAME}.error`, type: 'metrics' }, + { name: `${INTEGRATION_NAME}.warning`, type: 'logs' }, ], }) .expect(200); @@ -108,9 +108,9 @@ export default function (providerContext: FtrProviderContext) { force: true, integrationName: INTEGRATION_NAME, datasets: [ - { name: 'access', type: 'logs' }, - { name: 'error', type: 'metrics' }, - { name: 'warning', type: 'logs' }, + { name: `${INTEGRATION_NAME}.access`, type: 'logs' }, + { name: `${INTEGRATION_NAME}.error`, type: 'metrics' }, + { name: `${INTEGRATION_NAME}.warning`, type: 'logs' }, ], }) .expect(200); @@ -123,9 +123,9 @@ export default function (providerContext: FtrProviderContext) { force: true, integrationName: INTEGRATION_NAME, datasets: [ - { name: 'access', type: 'logs' }, - { name: 'error', type: 'metrics' }, - { name: 'warning', type: 'logs' }, + { name: `${INTEGRATION_NAME}.access`, type: 'logs' }, + { name: `${INTEGRATION_NAME}.error`, type: 'metrics' }, + { name: `${INTEGRATION_NAME}.warning`, type: 'logs' }, ], }) .expect(409); @@ -145,7 +145,7 @@ export default function (providerContext: FtrProviderContext) { .send({ force: true, integrationName: pkgName, - datasets: [{ name: 'error', type: 'logs' }], + datasets: [{ name: `${INTEGRATION_NAME}.error`, type: 'logs' }], }) .expect(409); @@ -153,5 +153,48 @@ export default function (providerContext: FtrProviderContext) { `Failed to create the integration as an integration with the name ${pkgName} already exists in the package registry or as a bundled package.` ); }); + + it('Throws an error when dataset names are not prefixed correctly', async () => { + const response = await supertest + .post(`/api/fleet/epm/custom_integrations`) + .set('kbn-xsrf', 'xxxx') + .type('application/json') + .send({ + force: true, + integrationName: INTEGRATION_NAME, + datasets: [{ name: 'error', type: 'logs' }], + }) + .expect(422); + + expect(response.body.message).to.be( + `Dataset names 'error' must either match integration name '${INTEGRATION_NAME}' exactly or be prefixed with integration name and a dot (e.g. '${INTEGRATION_NAME}.').` + ); + + await uninstallPackage(); + + await supertest + .post(`/api/fleet/epm/custom_integrations`) + .set('kbn-xsrf', 'xxxx') + .type('application/json') + .send({ + force: true, + integrationName: INTEGRATION_NAME, + datasets: [{ name: INTEGRATION_NAME, type: 'logs' }], + }) + .expect(200); + + await uninstallPackage(); + + await supertest + .post(`/api/fleet/epm/custom_integrations`) + .set('kbn-xsrf', 'xxxx') + .type('application/json') + .send({ + force: true, + integrationName: INTEGRATION_NAME, + datasets: [{ name: `${INTEGRATION_NAME}.error`, type: 'logs' }], + }) + .expect(200); + }); }); }