From 48e146f2f1f94addee0f5aea9e28ae6529590bf4 Mon Sep 17 00:00:00 2001
From: Kerry Gallagher <471693+Kerry350@users.noreply.github.com>
Date: Thu, 14 Sep 2023 19:02:17 +0100
Subject: [PATCH 1/5] Enforce dataset name validation (client and server side)
---
.../src/components/create/form.tsx | 34 ++++-
.../src/components/create/utils.ts | 4 +-
.../state_machines/create/notifications.ts | 19 ++-
.../state_machines/create/pipelines/fields.ts | 134 ++++++++++++++++++
.../state_machines/create/state_machine.ts | 50 +++----
.../src/state_machines/create/types.ts | 14 +-
.../custom_integrations/state_machine.ts | 38 +++--
.../services/integrations_client.ts | 6 +-
packages/kbn-custom-integrations/src/types.ts | 11 +-
.../fleet/server/routes/epm/handlers.ts | 8 ++
.../assets/dataset/utils.ts | 2 +-
.../validation/check_dataset_name_format.ts | 37 +++++
.../server/services/epm/packages/install.ts | 2 +
.../e2e/cypress/support/commands.ts | 6 +-
.../app/custom_logs/configure_logs.tsx | 12 +-
.../components/app/custom_logs/index.tsx | 8 +-
.../apis/epm/install_custom.ts | 63 ++++++--
17 files changed, 359 insertions(+), 89 deletions(-)
create mode 100644 packages/kbn-custom-integrations/src/state_machines/create/pipelines/fields.ts
create mode 100644 x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/validation/check_dataset_name_format.ts
diff --git a/packages/kbn-custom-integrations/src/components/create/form.tsx b/packages/kbn-custom-integrations/src/components/create/form.tsx
index 2273db68a479a..22fc0b26633e2 100644
--- a/packages/kbn-custom-integrations/src/components/create/form.tsx
+++ b/packages/kbn-custom-integrations/src/components/create/form.tsx
@@ -28,6 +28,12 @@ import {
} from '../../state_machines/create/types';
import { Dataset, IntegrationError } from '../../types';
import { hasFailedSelector } from '../../state_machines/create/selectors';
+import {
+ datasetNameWillBePrefixed,
+ getDatasetNamePrefix,
+ getDatasetNameWithoutPrefix,
+ 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 +56,7 @@ export const ConnectedCreateCustomIntegrationForm = ({
testSubjects?: CreateTestSubjects;
}) => {
const [state, send] = useActor(machineRef);
+
const updateIntegrationName = useCallback(
(integrationName: string) => {
send({ type: 'UPDATE_FIELDS', fields: { integrationName } });
@@ -181,17 +188,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)}
>
@@ -199,7 +221,7 @@ export const CreateCustomIntegrationForm = ({
placeholder={i18n.translate('customIntegrationsPackage.create.dataset.placeholder', {
defaultMessage: "Give your integration's dataset a name",
})}
- value={datasetName}
+ value={getDatasetNameWithoutPrefix(datasetName, integrationName)}
onChange={(event) => updateDatasetName(event.target.value)}
isInvalid={hasErrors(errors?.fields?.datasets?.[0].name) && touchedFields.datasets}
max={100}
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/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 1e438e1dd0e66..6cf64b5a32fd0 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);
+ });
});
}
From 105b6bbcbbedcab691a01e1feeb59656f003deaa Mon Sep 17 00:00:00 2001
From: Kerry Gallagher <471693+Kerry350@users.noreply.github.com>
Date: Wed, 20 Sep 2023 13:30:45 +0100
Subject: [PATCH 2/5] Remove formatting from previous approach
---
.../kbn-custom-integrations/src/components/create/form.tsx | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/packages/kbn-custom-integrations/src/components/create/form.tsx b/packages/kbn-custom-integrations/src/components/create/form.tsx
index 22fc0b26633e2..a05fa97b78fdf 100644
--- a/packages/kbn-custom-integrations/src/components/create/form.tsx
+++ b/packages/kbn-custom-integrations/src/components/create/form.tsx
@@ -31,7 +31,6 @@ import { hasFailedSelector } from '../../state_machines/create/selectors';
import {
datasetNameWillBePrefixed,
getDatasetNamePrefix,
- getDatasetNameWithoutPrefix,
prefixDatasetName,
} from '../../state_machines/create/pipelines/fields';
@@ -221,7 +220,7 @@ export const CreateCustomIntegrationForm = ({
placeholder={i18n.translate('customIntegrationsPackage.create.dataset.placeholder', {
defaultMessage: "Give your integration's dataset a name",
})}
- value={getDatasetNameWithoutPrefix(datasetName, integrationName)}
+ value={datasetName}
onChange={(event) => updateDatasetName(event.target.value)}
isInvalid={hasErrors(errors?.fields?.datasets?.[0].name) && touchedFields.datasets}
max={100}
From f70e1c99d8f19d6a8b110953bdbce6cec4761a24 Mon Sep 17 00:00:00 2001
From: Kerry Gallagher <471693+Kerry350@users.noreply.github.com>
Date: Thu, 21 Sep 2023 14:55:24 +0100
Subject: [PATCH 3/5] Amend Cypress test
---
.../create/state_machine.test.ts | 424 ++++++++++++++++++
1 file changed, 424 insertions(+)
create mode 100644 packages/kbn-custom-integrations/src/state_machines/create/state_machine.test.ts
diff --git a/packages/kbn-custom-integrations/src/state_machines/create/state_machine.test.ts b/packages/kbn-custom-integrations/src/state_machines/create/state_machine.test.ts
new file mode 100644
index 0000000000000..75bf08ae3fe09
--- /dev/null
+++ b/packages/kbn-custom-integrations/src/state_machines/create/state_machine.test.ts
@@ -0,0 +1,424 @@
+/*
+ * 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 { IntegrationError } from '../../types';
+import { DEFAULT_CONTEXT } from './defaults';
+import { createPureCreateCustomIntegrationStateMachine } from './state_machine';
+
+// Generated by CodiumAI
+
+describe('createPureCreateCustomIntegrationStateMachine', () => {
+ // State machine initializes with default context
+ it('should initialize state machine with default context', () => {
+ const machine = createPureCreateCustomIntegrationStateMachine();
+ expect(machine.initialState.context).toEqual(DEFAULT_CONTEXT);
+ });
+
+ // State machine transitions from 'uninitialized' to 'untouched' if context is default
+ it('should transition from "uninitialized" to "untouched" if context is default', () => {
+ const machine = createPureCreateCustomIntegrationStateMachine();
+ const nextState = machine.transition('uninitialized', { type: 'INITIALIZE' });
+ expect(nextState.value).toBe('untouched');
+ });
+
+ // State machine transitions from 'uninitialized' to 'validating' if context is not default
+ it('should transition from "uninitialized" to "validating" if context is not default', () => {
+ const initialContext = {
+ options: {
+ deletePrevious: true,
+ resetOnCreation: false,
+ errorOnFailedCleanup: true,
+ },
+ fields: {
+ integrationName: 'test',
+ datasets: [
+ {
+ type: 'logs',
+ name: 'test',
+ },
+ ],
+ },
+ touchedFields: {
+ integrationName: true,
+ datasets: true,
+ },
+ errors: null,
+ };
+ const machine = createPureCreateCustomIntegrationStateMachine(initialContext);
+ const nextState = machine.transition('uninitialized', { type: 'INITIALIZE' });
+ expect(nextState.value).toBe('validating');
+ });
+
+ // State machine transitions from 'uninitialized' to 'validating' if context is not default and shouldValidateInitialContext guard returns true
+ it('should transition from "uninitialized" to "validating" if context is not default and shouldValidateInitialContext guard returns true', () => {
+ const initialContext = {
+ options: {
+ deletePrevious: true,
+ resetOnCreation: false,
+ errorOnFailedCleanup: true,
+ },
+ fields: {
+ integrationName: 'test',
+ datasets: [
+ {
+ type: 'logs',
+ name: 'test',
+ },
+ ],
+ },
+ touchedFields: {
+ integrationName: true,
+ datasets: true,
+ },
+ errors: null,
+ };
+ const machine = createPureCreateCustomIntegrationStateMachine(initialContext);
+ const nextState = machine.transition('uninitialized', { type: 'INITIALIZE' });
+ expect(nextState.value).toBe('validating');
+ });
+
+ // State machine transitions from 'valid' to 'deletingPrevious' if SAVE event is triggered and deletePrevious option is true and previously created integration does not exist
+ it('should transition from "valid" to "deletingPrevious" if SAVE event is triggered and deletePrevious option is true and previously created integration does not exist', () => {
+ const initialContext = {
+ options: {
+ deletePrevious: true,
+ resetOnCreation: false,
+ errorOnFailedCleanup: true,
+ },
+ fields: {
+ integrationName: 'test',
+ datasets: [
+ {
+ type: 'logs',
+ name: 'test',
+ },
+ ],
+ },
+ touchedFields: {
+ integrationName: true,
+ datasets: true,
+ },
+ errors: null,
+ };
+ const machine = createPureCreateCustomIntegrationStateMachine(initialContext);
+ const nextState = machine.transition('valid', { type: 'SAVE' });
+ expect(nextState.value).toBe('deletingPrevious');
+ });
+
+ // State machine transitions from 'validating' to 'valid' if validation succeeds
+ it('should transition to "valid" when validation succeeds', () => {
+ const machine = createPureCreateCustomIntegrationStateMachine();
+ const nextState = machine.transition('validating', { type: 'VALIDATION_SUCCESS' });
+ expect(nextState.value).toBe('valid');
+ });
+
+ // State machine transitions from 'valid' to 'success' if SAVE event is triggered and fields match previously created integration
+ it('should transition to "success" when SAVE event is triggered and fields match previously created integration', () => {
+ const machine = createPureCreateCustomIntegrationStateMachine();
+ const context = {
+ ...DEFAULT_CONTEXT,
+ previouslyCreatedIntegration: {
+ integrationName: 'integration1',
+ datasets: [
+ {
+ type: 'logs',
+ name: 'dataset1',
+ },
+ ],
+ },
+ };
+ const nextState = machine.transition('valid', { type: 'SAVE' }, context);
+ expect(nextState.value).toBe('success');
+ });
+
+ // State machine transitions from 'valid' to 'deletingPrevious' if SAVE event is triggered and deletePrevious option is true and previously created integration exists
+ it("should transition from 'valid' to 'deletingPrevious' when SAVE event is triggered and deletePrevious option is true and previously created integration exists", () => {
+ const initialContext = {
+ options: {
+ deletePrevious: true,
+ resetOnCreation: true,
+ errorOnFailedCleanup: false,
+ },
+ fields: {
+ integrationName: 'Integration 1',
+ datasets: [
+ {
+ type: 'logs',
+ name: 'Dataset 1',
+ },
+ ],
+ },
+ touchedFields: {
+ integrationName: true,
+ datasets: true,
+ },
+ errors: null,
+ previouslyCreatedIntegration: {
+ integrationName: 'Integration 1',
+ datasets: [
+ {
+ type: 'logs',
+ name: 'Dataset 1',
+ },
+ ],
+ },
+ };
+
+ const machine = createPureCreateCustomIntegrationStateMachine(initialContext);
+ const nextState = machine.transition('valid', { type: 'SAVE' });
+
+ expect(nextState.value).toBe('deletingPrevious');
+ });
+
+ // State machine transitions from 'valid' to 'submitting' if SAVE event is triggered and fields do not match previously created integration and deletePrevious option is false
+ it("should transition from 'valid' to 'submitting' when SAVE event is triggered and fields do not match previously created integration and deletePrevious option is false", () => {
+ const initialContext = {
+ options: {
+ deletePrevious: false,
+ resetOnCreation: true,
+ errorOnFailedCleanup: false,
+ },
+ fields: {
+ integrationName: 'Integration 1',
+ datasets: [
+ {
+ type: 'logs',
+ name: 'Dataset 1',
+ },
+ ],
+ },
+ touchedFields: {
+ integrationName: true,
+ datasets: true,
+ },
+ errors: null,
+ previouslyCreatedIntegration: {
+ integrationName: 'Integration 2',
+ datasets: [
+ {
+ type: 'logs',
+ name: 'Dataset 2',
+ },
+ ],
+ },
+ };
+
+ const machine = createPureCreateCustomIntegrationStateMachine(initialContext);
+ const nextState = machine.transition('valid', { type: 'SAVE' });
+
+ expect(nextState.value).toBe('submitting');
+ });
+
+ // State machine transitions from 'deletingPrevious' to 'submitting' if cleanup succeeds
+ it("should transition from 'deletingPrevious' to 'submitting' when cleanup succeeds", () => {
+ const initialContext = {
+ options: {
+ deletePrevious: true,
+ resetOnCreation: true,
+ errorOnFailedCleanup: false,
+ },
+ fields: {
+ integrationName: 'Integration 1',
+ datasets: [
+ {
+ type: 'logs',
+ name: 'Dataset 1',
+ },
+ ],
+ },
+ touchedFields: {
+ integrationName: true,
+ datasets: true,
+ },
+ errors: null,
+ previouslyCreatedIntegration: {
+ integrationName: 'Integration 1',
+ datasets: [
+ {
+ type: 'logs',
+ name: 'Dataset 1',
+ },
+ ],
+ },
+ };
+
+ const machine = createPureCreateCustomIntegrationStateMachine(initialContext);
+ const nextState = machine.transition('deletingPrevious', {
+ type: 'done.invoke.submitting:invocation[0]',
+ data: {},
+ });
+
+ expect(nextState.value).toBe('submitting');
+ });
+
+ // State machine transitions from 'submitting' to 'success' if save succeeds
+ it("should transition from 'submitting' to 'success' when save succeeds", () => {
+ const machine = createPureCreateCustomIntegrationStateMachine();
+ const nextState = machine.transition('submitting', {
+ type: 'done.invoke.submitting:invocation[0]',
+ data: {},
+ });
+ expect(nextState.value).toBe('success');
+ });
+
+ // State machine transitions from 'failure' to 'deletingPrevious' if RETRY event is triggered and deletePrevious option is true and previously created integration exists
+ it("should transition from 'failure' to 'deletingPrevious' when RETRY event is triggered and deletePrevious option is true and previously created integration exists", () => {
+ const machine = createPureCreateCustomIntegrationStateMachine({
+ initialContext: {
+ options: {
+ deletePrevious: true,
+ },
+ previouslyCreatedIntegration: {},
+ },
+ });
+ const nextState = machine.transition('failure', { type: 'RETRY' });
+ expect(nextState.value).toBe('deletingPrevious');
+ });
+
+ // State machine transitions from 'failure' to 'submitting' if RETRY event is triggered and deletePrevious option is false
+ it("should transition from 'failure' to 'submitting' when RETRY event is triggered and deletePrevious option is false", () => {
+ const machine = createPureCreateCustomIntegrationStateMachine({
+ initialContext: {
+ options: {
+ deletePrevious: false,
+ },
+ },
+ });
+ const nextState = machine.transition('failure', { type: 'RETRY' });
+ expect(nextState.value).toBe('submitting');
+ });
+
+ // State machine transitions from 'deletingPrevious' to 'failure' if cleanup fails and shouldErrorOnFailedCleanup guard returns true
+ it("should transition from 'deletingPrevious' to 'failure' when cleanup fails and shouldErrorOnFailedCleanup is true", () => {
+ const initialContext = {
+ ...DEFAULT_CONTEXT,
+ options: {
+ ...DEFAULT_CONTEXT.options,
+ deletePrevious: true,
+ errorOnFailedCleanup: true,
+ },
+ previouslyCreatedIntegration: {
+ fields: {
+ integrationName: 'test',
+ datasets: [
+ {
+ type: 'logs',
+ name: 'test',
+ },
+ ],
+ },
+ },
+ };
+ const machine = createPureCreateCustomIntegrationStateMachine(initialContext);
+ const nextState = machine.transition('deletingPrevious', {
+ type: 'error.platform.CreateCustomIntegration.deletingPrevious:invocation[0]',
+ data: new IntegrationError('Cleanup failed'),
+ });
+ expect(nextState.value).toBe('failure');
+ });
+
+ // State machine transitions from 'failure' to 'success' if RETRY event is triggered and save succeeds and fields match previously created integration
+ it("should transition from 'failure' to 'success' when RETRY event is triggered and save succeeds and fields match previously created integration", () => {
+ const initialContext = {
+ ...DEFAULT_CONTEXT,
+ options: {
+ ...DEFAULT_CONTEXT.options,
+ deletePrevious: true,
+ errorOnFailedCleanup: false,
+ },
+ previouslyCreatedIntegration: {
+ fields: {
+ integrationName: 'test',
+ datasets: [
+ {
+ type: 'logs',
+ name: 'test',
+ },
+ ],
+ },
+ },
+ };
+ const machine = createPureCreateCustomIntegrationStateMachine(initialContext);
+ const nextState = machine.transition('failure', {
+ type: 'RETRY',
+ data: {
+ integrationName: 'test',
+ datasets: [
+ {
+ type: 'logs',
+ name: 'test',
+ },
+ ],
+ },
+ });
+ expect(nextState.value).toBe('success');
+ });
+
+ // State machine transitions from 'valid' to 'validationFailed' if validation fails
+ it("should transition from 'valid' to 'validationFailed' when validation fails", () => {
+ const initialContext = {
+ ...DEFAULT_CONTEXT,
+ options: {
+ ...DEFAULT_CONTEXT.options,
+ deletePrevious: false,
+ errorOnFailedCleanup: false,
+ },
+ fields: {
+ integrationName: 'test',
+ datasets: [
+ {
+ type: 'logs',
+ name: '',
+ },
+ ],
+ },
+ };
+ const machine = createPureCreateCustomIntegrationStateMachine(initialContext);
+ const nextState = machine.transition('valid', {
+ type: 'error.platform.validating:invocation[0]',
+ data: {
+ errors: {
+ fields: {
+ integrationName: [new IntegrationError('Invalid integration name')],
+ datasets: {
+ 0: [new IntegrationError('Invalid dataset name')],
+ },
+ },
+ general: null,
+ },
+ },
+ });
+ expect(nextState.value).toBe('validationFailed');
+ });
+
+ // State machine transitions from 'validationFailed' to 'validating' if UPDATE_FIELDS event is triggered
+ it("should transition from 'validationFailed' to 'validating' when UPDATE_FIELDS event is triggered", () => {
+ const machine = createPureCreateCustomIntegrationStateMachine();
+ const initialState = machine.initialState;
+ const nextState = machine.transition(initialState, { type: 'UPDATE_FIELDS' });
+ expect(nextState.value).toBe('validating');
+ });
+
+ // State machine transitions from 'validationFailed' to 'valid' if UPDATE_FIELDS event is triggered and validation succeeds
+ it("should transition from 'validationFailed' to 'valid' when UPDATE_FIELDS event is triggered and validation succeeds", () => {
+ const machine = createPureCreateCustomIntegrationStateMachine();
+ const initialState = machine.initialState;
+ const nextState = machine.transition(initialState, { type: 'UPDATE_FIELDS' });
+ const validState = machine.transition(nextState, { type: 'VALIDATE_SUCCESS' });
+ expect(validState.value).toBe('valid');
+ });
+
+ // State machine transitions from 'submitting' to 'failure' if save fails
+ it("should transition from 'submitting' to 'failure' when save fails", () => {
+ const machine = createPureCreateCustomIntegrationStateMachine();
+ const initialState = machine.initialState;
+ const nextState = machine.transition(initialState, { type: 'SAVE' });
+ const failureState = machine.transition(nextState, { type: 'SAVE_FAILURE' });
+ expect(failureState.value).toBe('failure');
+ });
+});
From 565d70b409430e9b26cc295da1db27c5a39044ec Mon Sep 17 00:00:00 2001
From: Kerry Gallagher <471693+Kerry350@users.noreply.github.com>
Date: Thu, 21 Sep 2023 14:56:57 +0100
Subject: [PATCH 4/5] Revert "Amend Cypress test"
This reverts commit f70e1c99d8f19d6a8b110953bdbce6cec4761a24.
---
.../create/state_machine.test.ts | 424 ------------------
1 file changed, 424 deletions(-)
delete mode 100644 packages/kbn-custom-integrations/src/state_machines/create/state_machine.test.ts
diff --git a/packages/kbn-custom-integrations/src/state_machines/create/state_machine.test.ts b/packages/kbn-custom-integrations/src/state_machines/create/state_machine.test.ts
deleted file mode 100644
index 75bf08ae3fe09..0000000000000
--- a/packages/kbn-custom-integrations/src/state_machines/create/state_machine.test.ts
+++ /dev/null
@@ -1,424 +0,0 @@
-/*
- * 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 { IntegrationError } from '../../types';
-import { DEFAULT_CONTEXT } from './defaults';
-import { createPureCreateCustomIntegrationStateMachine } from './state_machine';
-
-// Generated by CodiumAI
-
-describe('createPureCreateCustomIntegrationStateMachine', () => {
- // State machine initializes with default context
- it('should initialize state machine with default context', () => {
- const machine = createPureCreateCustomIntegrationStateMachine();
- expect(machine.initialState.context).toEqual(DEFAULT_CONTEXT);
- });
-
- // State machine transitions from 'uninitialized' to 'untouched' if context is default
- it('should transition from "uninitialized" to "untouched" if context is default', () => {
- const machine = createPureCreateCustomIntegrationStateMachine();
- const nextState = machine.transition('uninitialized', { type: 'INITIALIZE' });
- expect(nextState.value).toBe('untouched');
- });
-
- // State machine transitions from 'uninitialized' to 'validating' if context is not default
- it('should transition from "uninitialized" to "validating" if context is not default', () => {
- const initialContext = {
- options: {
- deletePrevious: true,
- resetOnCreation: false,
- errorOnFailedCleanup: true,
- },
- fields: {
- integrationName: 'test',
- datasets: [
- {
- type: 'logs',
- name: 'test',
- },
- ],
- },
- touchedFields: {
- integrationName: true,
- datasets: true,
- },
- errors: null,
- };
- const machine = createPureCreateCustomIntegrationStateMachine(initialContext);
- const nextState = machine.transition('uninitialized', { type: 'INITIALIZE' });
- expect(nextState.value).toBe('validating');
- });
-
- // State machine transitions from 'uninitialized' to 'validating' if context is not default and shouldValidateInitialContext guard returns true
- it('should transition from "uninitialized" to "validating" if context is not default and shouldValidateInitialContext guard returns true', () => {
- const initialContext = {
- options: {
- deletePrevious: true,
- resetOnCreation: false,
- errorOnFailedCleanup: true,
- },
- fields: {
- integrationName: 'test',
- datasets: [
- {
- type: 'logs',
- name: 'test',
- },
- ],
- },
- touchedFields: {
- integrationName: true,
- datasets: true,
- },
- errors: null,
- };
- const machine = createPureCreateCustomIntegrationStateMachine(initialContext);
- const nextState = machine.transition('uninitialized', { type: 'INITIALIZE' });
- expect(nextState.value).toBe('validating');
- });
-
- // State machine transitions from 'valid' to 'deletingPrevious' if SAVE event is triggered and deletePrevious option is true and previously created integration does not exist
- it('should transition from "valid" to "deletingPrevious" if SAVE event is triggered and deletePrevious option is true and previously created integration does not exist', () => {
- const initialContext = {
- options: {
- deletePrevious: true,
- resetOnCreation: false,
- errorOnFailedCleanup: true,
- },
- fields: {
- integrationName: 'test',
- datasets: [
- {
- type: 'logs',
- name: 'test',
- },
- ],
- },
- touchedFields: {
- integrationName: true,
- datasets: true,
- },
- errors: null,
- };
- const machine = createPureCreateCustomIntegrationStateMachine(initialContext);
- const nextState = machine.transition('valid', { type: 'SAVE' });
- expect(nextState.value).toBe('deletingPrevious');
- });
-
- // State machine transitions from 'validating' to 'valid' if validation succeeds
- it('should transition to "valid" when validation succeeds', () => {
- const machine = createPureCreateCustomIntegrationStateMachine();
- const nextState = machine.transition('validating', { type: 'VALIDATION_SUCCESS' });
- expect(nextState.value).toBe('valid');
- });
-
- // State machine transitions from 'valid' to 'success' if SAVE event is triggered and fields match previously created integration
- it('should transition to "success" when SAVE event is triggered and fields match previously created integration', () => {
- const machine = createPureCreateCustomIntegrationStateMachine();
- const context = {
- ...DEFAULT_CONTEXT,
- previouslyCreatedIntegration: {
- integrationName: 'integration1',
- datasets: [
- {
- type: 'logs',
- name: 'dataset1',
- },
- ],
- },
- };
- const nextState = machine.transition('valid', { type: 'SAVE' }, context);
- expect(nextState.value).toBe('success');
- });
-
- // State machine transitions from 'valid' to 'deletingPrevious' if SAVE event is triggered and deletePrevious option is true and previously created integration exists
- it("should transition from 'valid' to 'deletingPrevious' when SAVE event is triggered and deletePrevious option is true and previously created integration exists", () => {
- const initialContext = {
- options: {
- deletePrevious: true,
- resetOnCreation: true,
- errorOnFailedCleanup: false,
- },
- fields: {
- integrationName: 'Integration 1',
- datasets: [
- {
- type: 'logs',
- name: 'Dataset 1',
- },
- ],
- },
- touchedFields: {
- integrationName: true,
- datasets: true,
- },
- errors: null,
- previouslyCreatedIntegration: {
- integrationName: 'Integration 1',
- datasets: [
- {
- type: 'logs',
- name: 'Dataset 1',
- },
- ],
- },
- };
-
- const machine = createPureCreateCustomIntegrationStateMachine(initialContext);
- const nextState = machine.transition('valid', { type: 'SAVE' });
-
- expect(nextState.value).toBe('deletingPrevious');
- });
-
- // State machine transitions from 'valid' to 'submitting' if SAVE event is triggered and fields do not match previously created integration and deletePrevious option is false
- it("should transition from 'valid' to 'submitting' when SAVE event is triggered and fields do not match previously created integration and deletePrevious option is false", () => {
- const initialContext = {
- options: {
- deletePrevious: false,
- resetOnCreation: true,
- errorOnFailedCleanup: false,
- },
- fields: {
- integrationName: 'Integration 1',
- datasets: [
- {
- type: 'logs',
- name: 'Dataset 1',
- },
- ],
- },
- touchedFields: {
- integrationName: true,
- datasets: true,
- },
- errors: null,
- previouslyCreatedIntegration: {
- integrationName: 'Integration 2',
- datasets: [
- {
- type: 'logs',
- name: 'Dataset 2',
- },
- ],
- },
- };
-
- const machine = createPureCreateCustomIntegrationStateMachine(initialContext);
- const nextState = machine.transition('valid', { type: 'SAVE' });
-
- expect(nextState.value).toBe('submitting');
- });
-
- // State machine transitions from 'deletingPrevious' to 'submitting' if cleanup succeeds
- it("should transition from 'deletingPrevious' to 'submitting' when cleanup succeeds", () => {
- const initialContext = {
- options: {
- deletePrevious: true,
- resetOnCreation: true,
- errorOnFailedCleanup: false,
- },
- fields: {
- integrationName: 'Integration 1',
- datasets: [
- {
- type: 'logs',
- name: 'Dataset 1',
- },
- ],
- },
- touchedFields: {
- integrationName: true,
- datasets: true,
- },
- errors: null,
- previouslyCreatedIntegration: {
- integrationName: 'Integration 1',
- datasets: [
- {
- type: 'logs',
- name: 'Dataset 1',
- },
- ],
- },
- };
-
- const machine = createPureCreateCustomIntegrationStateMachine(initialContext);
- const nextState = machine.transition('deletingPrevious', {
- type: 'done.invoke.submitting:invocation[0]',
- data: {},
- });
-
- expect(nextState.value).toBe('submitting');
- });
-
- // State machine transitions from 'submitting' to 'success' if save succeeds
- it("should transition from 'submitting' to 'success' when save succeeds", () => {
- const machine = createPureCreateCustomIntegrationStateMachine();
- const nextState = machine.transition('submitting', {
- type: 'done.invoke.submitting:invocation[0]',
- data: {},
- });
- expect(nextState.value).toBe('success');
- });
-
- // State machine transitions from 'failure' to 'deletingPrevious' if RETRY event is triggered and deletePrevious option is true and previously created integration exists
- it("should transition from 'failure' to 'deletingPrevious' when RETRY event is triggered and deletePrevious option is true and previously created integration exists", () => {
- const machine = createPureCreateCustomIntegrationStateMachine({
- initialContext: {
- options: {
- deletePrevious: true,
- },
- previouslyCreatedIntegration: {},
- },
- });
- const nextState = machine.transition('failure', { type: 'RETRY' });
- expect(nextState.value).toBe('deletingPrevious');
- });
-
- // State machine transitions from 'failure' to 'submitting' if RETRY event is triggered and deletePrevious option is false
- it("should transition from 'failure' to 'submitting' when RETRY event is triggered and deletePrevious option is false", () => {
- const machine = createPureCreateCustomIntegrationStateMachine({
- initialContext: {
- options: {
- deletePrevious: false,
- },
- },
- });
- const nextState = machine.transition('failure', { type: 'RETRY' });
- expect(nextState.value).toBe('submitting');
- });
-
- // State machine transitions from 'deletingPrevious' to 'failure' if cleanup fails and shouldErrorOnFailedCleanup guard returns true
- it("should transition from 'deletingPrevious' to 'failure' when cleanup fails and shouldErrorOnFailedCleanup is true", () => {
- const initialContext = {
- ...DEFAULT_CONTEXT,
- options: {
- ...DEFAULT_CONTEXT.options,
- deletePrevious: true,
- errorOnFailedCleanup: true,
- },
- previouslyCreatedIntegration: {
- fields: {
- integrationName: 'test',
- datasets: [
- {
- type: 'logs',
- name: 'test',
- },
- ],
- },
- },
- };
- const machine = createPureCreateCustomIntegrationStateMachine(initialContext);
- const nextState = machine.transition('deletingPrevious', {
- type: 'error.platform.CreateCustomIntegration.deletingPrevious:invocation[0]',
- data: new IntegrationError('Cleanup failed'),
- });
- expect(nextState.value).toBe('failure');
- });
-
- // State machine transitions from 'failure' to 'success' if RETRY event is triggered and save succeeds and fields match previously created integration
- it("should transition from 'failure' to 'success' when RETRY event is triggered and save succeeds and fields match previously created integration", () => {
- const initialContext = {
- ...DEFAULT_CONTEXT,
- options: {
- ...DEFAULT_CONTEXT.options,
- deletePrevious: true,
- errorOnFailedCleanup: false,
- },
- previouslyCreatedIntegration: {
- fields: {
- integrationName: 'test',
- datasets: [
- {
- type: 'logs',
- name: 'test',
- },
- ],
- },
- },
- };
- const machine = createPureCreateCustomIntegrationStateMachine(initialContext);
- const nextState = machine.transition('failure', {
- type: 'RETRY',
- data: {
- integrationName: 'test',
- datasets: [
- {
- type: 'logs',
- name: 'test',
- },
- ],
- },
- });
- expect(nextState.value).toBe('success');
- });
-
- // State machine transitions from 'valid' to 'validationFailed' if validation fails
- it("should transition from 'valid' to 'validationFailed' when validation fails", () => {
- const initialContext = {
- ...DEFAULT_CONTEXT,
- options: {
- ...DEFAULT_CONTEXT.options,
- deletePrevious: false,
- errorOnFailedCleanup: false,
- },
- fields: {
- integrationName: 'test',
- datasets: [
- {
- type: 'logs',
- name: '',
- },
- ],
- },
- };
- const machine = createPureCreateCustomIntegrationStateMachine(initialContext);
- const nextState = machine.transition('valid', {
- type: 'error.platform.validating:invocation[0]',
- data: {
- errors: {
- fields: {
- integrationName: [new IntegrationError('Invalid integration name')],
- datasets: {
- 0: [new IntegrationError('Invalid dataset name')],
- },
- },
- general: null,
- },
- },
- });
- expect(nextState.value).toBe('validationFailed');
- });
-
- // State machine transitions from 'validationFailed' to 'validating' if UPDATE_FIELDS event is triggered
- it("should transition from 'validationFailed' to 'validating' when UPDATE_FIELDS event is triggered", () => {
- const machine = createPureCreateCustomIntegrationStateMachine();
- const initialState = machine.initialState;
- const nextState = machine.transition(initialState, { type: 'UPDATE_FIELDS' });
- expect(nextState.value).toBe('validating');
- });
-
- // State machine transitions from 'validationFailed' to 'valid' if UPDATE_FIELDS event is triggered and validation succeeds
- it("should transition from 'validationFailed' to 'valid' when UPDATE_FIELDS event is triggered and validation succeeds", () => {
- const machine = createPureCreateCustomIntegrationStateMachine();
- const initialState = machine.initialState;
- const nextState = machine.transition(initialState, { type: 'UPDATE_FIELDS' });
- const validState = machine.transition(nextState, { type: 'VALIDATE_SUCCESS' });
- expect(validState.value).toBe('valid');
- });
-
- // State machine transitions from 'submitting' to 'failure' if save fails
- it("should transition from 'submitting' to 'failure' when save fails", () => {
- const machine = createPureCreateCustomIntegrationStateMachine();
- const initialState = machine.initialState;
- const nextState = machine.transition(initialState, { type: 'SAVE' });
- const failureState = machine.transition(nextState, { type: 'SAVE_FAILURE' });
- expect(failureState.value).toBe('failure');
- });
-});
From 7a1b2d433a937c58ace905312715f349b39d311a Mon Sep 17 00:00:00 2001
From: Kerry Gallagher <471693+Kerry350@users.noreply.github.com>
Date: Thu, 21 Sep 2023 14:57:29 +0100
Subject: [PATCH 5/5] Amend Cypress test
---
.../cypress/e2e/logs/custom_logs/install_elastic_agent.cy.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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');
});
});
});