mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[8.14] [Fleet] add validation to dataset field in input packages to disallow special characters (#182925) (#183367)
# Backport This will backport the following commits from `main` to `8.14`: - [[Fleet] add validation to dataset field in input packages to disallow special characters (#182925)](https://github.com/elastic/kibana/pull/182925) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Julia Bardi","email":"90178898+juliaElastic@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-05-14T08:45:54Z","message":"[Fleet] add validation to dataset field in input packages to disallow special characters (#182925)\n\n## Summary\r\n\r\nCloses https://github.com/elastic/kibana/issues/181044\r\n\r\nAdded validation to allow only valid index names as dataset name in\r\ninput packages. Allowing lowercase letters, numbers, dot and underscore\r\n(except in the beginning).\r\n\r\nTo verify:\r\n- add Custom Logs integration\r\n- modfiy dataset to add invalid characters e.g. *\r\n- verify that the field shows a validation error\r\n- verify that the save button is disabled\r\n- verify that valid dataset names can be used\r\n\r\n<img width=\"968\" alt=\"image\"\r\nsrc=\"e9cd17f1
-6f5b-464c-bc8d-83d2ec42bada\">\r\n\r\n\r\n\r\n### Checklist\r\n\r\nDelete any items that are not applicable to this PR.\r\n\r\n- [x] Any text added follows [EUI's writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios","sha":"be9b46d911f8648203a4c1d5fe4ccea848020ef7","branchLabelMapping":{"^v8.15.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","Team:Fleet","backport:prev-minor","v8.15.0"],"title":"[Fleet] add validation to dataset field in input packages to disallow special characters","number":182925,"url":"https://github.com/elastic/kibana/pull/182925","mergeCommit":{"message":"[Fleet] add validation to dataset field in input packages to disallow special characters (#182925)\n\n## Summary\r\n\r\nCloses https://github.com/elastic/kibana/issues/181044\r\n\r\nAdded validation to allow only valid index names as dataset name in\r\ninput packages. Allowing lowercase letters, numbers, dot and underscore\r\n(except in the beginning).\r\n\r\nTo verify:\r\n- add Custom Logs integration\r\n- modfiy dataset to add invalid characters e.g. *\r\n- verify that the field shows a validation error\r\n- verify that the save button is disabled\r\n- verify that valid dataset names can be used\r\n\r\n<img width=\"968\" alt=\"image\"\r\nsrc=\"e9cd17f1
-6f5b-464c-bc8d-83d2ec42bada\">\r\n\r\n\r\n\r\n### Checklist\r\n\r\nDelete any items that are not applicable to this PR.\r\n\r\n- [x] Any text added follows [EUI's writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios","sha":"be9b46d911f8648203a4c1d5fe4ccea848020ef7"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v8.15.0","branchLabelMappingKey":"^v8.15.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/182925","number":182925,"mergeCommit":{"message":"[Fleet] add validation to dataset field in input packages to disallow special characters (#182925)\n\n## Summary\r\n\r\nCloses https://github.com/elastic/kibana/issues/181044\r\n\r\nAdded validation to allow only valid index names as dataset name in\r\ninput packages. Allowing lowercase letters, numbers, dot and underscore\r\n(except in the beginning).\r\n\r\nTo verify:\r\n- add Custom Logs integration\r\n- modfiy dataset to add invalid characters e.g. *\r\n- verify that the field shows a validation error\r\n- verify that the save button is disabled\r\n- verify that valid dataset names can be used\r\n\r\n<img width=\"968\" alt=\"image\"\r\nsrc=\"e9cd17f1
-6f5b-464c-bc8d-83d2ec42bada\">\r\n\r\n\r\n\r\n### Checklist\r\n\r\nDelete any items that are not applicable to this PR.\r\n\r\n- [x] Any text added follows [EUI's writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios","sha":"be9b46d911f8648203a4c1d5fe4ccea848020ef7"}}]}] BACKPORT--> Co-authored-by: Julia Bardi <90178898+juliaElastic@users.noreply.github.com>
This commit is contained in:
parent
b1f978ea67
commit
f956f9d1ca
13 changed files with 477 additions and 181 deletions
|
@ -73,6 +73,7 @@ export {
|
|||
fleetSetupRouteService,
|
||||
// Package policy helpers
|
||||
isValidNamespace,
|
||||
isValidDataset,
|
||||
INVALID_NAMESPACE_CHARACTERS,
|
||||
getFileMetadataIndexName,
|
||||
getFileDataIndexName,
|
||||
|
|
|
@ -16,7 +16,11 @@ export {
|
|||
} from './package_to_package_policy';
|
||||
export { fullAgentPolicyToYaml } from './full_agent_policy_to_yaml';
|
||||
export { isPackageLimited, doesAgentPolicyAlreadyIncludePackage } from './limited_package';
|
||||
export { isValidNamespace, INVALID_NAMESPACE_CHARACTERS } from './is_valid_namespace';
|
||||
export {
|
||||
isValidDataset,
|
||||
isValidNamespace,
|
||||
INVALID_NAMESPACE_CHARACTERS,
|
||||
} from './is_valid_namespace';
|
||||
export { isDiffPathProtocol } from './is_diff_path_protocol';
|
||||
export { LicenseService } from './license';
|
||||
export * from './is_agent_upgradeable';
|
||||
|
|
|
@ -14,37 +14,71 @@ export function isValidNamespace(
|
|||
namespace: string,
|
||||
allowBlankNamespace?: boolean
|
||||
): { valid: boolean; error?: string } {
|
||||
if (!namespace.trim() && !allowBlankNamespace) {
|
||||
return isValidEntity(namespace, 'Namespace', allowBlankNamespace);
|
||||
}
|
||||
|
||||
export function isValidDataset(
|
||||
dataset: string,
|
||||
allowBlank?: boolean
|
||||
): { valid: boolean; error?: string } {
|
||||
const { valid, error } = isValidEntity(dataset, 'Dataset', allowBlank);
|
||||
if (!valid) {
|
||||
return { valid, error };
|
||||
}
|
||||
if (dataset.startsWith('_') || dataset.startsWith('.')) {
|
||||
return {
|
||||
valid: false,
|
||||
error: i18n.translate(
|
||||
'xpack.fleet.datasetValidation.datasetStartsWithUnderscoreErrorMessage',
|
||||
{
|
||||
defaultMessage: 'Dataset cannot start with an underscore or dot',
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
return { valid, error };
|
||||
}
|
||||
|
||||
function isValidEntity(
|
||||
name: string,
|
||||
type: string,
|
||||
allowBlank?: boolean
|
||||
): { valid: boolean; error?: string } {
|
||||
if (!name.trim() && !allowBlank) {
|
||||
return {
|
||||
valid: false,
|
||||
error: i18n.translate('xpack.fleet.namespaceValidation.requiredErrorMessage', {
|
||||
defaultMessage: 'Namespace is required',
|
||||
defaultMessage: '{type} is required',
|
||||
values: { type },
|
||||
}),
|
||||
};
|
||||
} else if (namespace !== namespace.toLowerCase()) {
|
||||
} else if (name !== name.toLowerCase()) {
|
||||
return {
|
||||
valid: false,
|
||||
error: i18n.translate('xpack.fleet.namespaceValidation.lowercaseErrorMessage', {
|
||||
defaultMessage: 'Namespace must be lowercase',
|
||||
defaultMessage: '{type} must be lowercase',
|
||||
values: { type },
|
||||
}),
|
||||
};
|
||||
} else if (INVALID_NAMESPACE_CHARACTERS.test(namespace)) {
|
||||
} else if (INVALID_NAMESPACE_CHARACTERS.test(name)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: i18n.translate('xpack.fleet.namespaceValidation.invalidCharactersErrorMessage', {
|
||||
defaultMessage: 'Namespace contains invalid characters',
|
||||
defaultMessage: '{type} contains invalid characters',
|
||||
values: { type },
|
||||
}),
|
||||
};
|
||||
}
|
||||
// Node.js doesn't have Blob, and browser doesn't have Buffer :)
|
||||
else if (
|
||||
(typeof Blob === 'function' && new Blob([namespace]).size > 100) ||
|
||||
(typeof Buffer === 'function' && Buffer.from(namespace).length > 100)
|
||||
(typeof Blob === 'function' && new Blob([name]).size > 100) ||
|
||||
(typeof Buffer === 'function' && Buffer.from(name).length > 100)
|
||||
) {
|
||||
return {
|
||||
valid: false,
|
||||
error: i18n.translate('xpack.fleet.namespaceValidation.tooLongErrorMessage', {
|
||||
defaultMessage: 'Namespace cannot be more than 100 bytes',
|
||||
defaultMessage: '{type} cannot be more than 100 bytes',
|
||||
values: { type },
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1079,4 +1079,144 @@ describe('Fleet - validatePackagePolicyConfig', () => {
|
|||
expect(res).toEqual(['Secret reference is invalid, id must be a string']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dataset', () => {
|
||||
const datasetError = 'Dataset contains invalid characters';
|
||||
|
||||
const validateDataset = (dataset: string) => {
|
||||
return validatePackagePolicyConfig(
|
||||
{
|
||||
type: 'text',
|
||||
value: { dataset, package: 'log' },
|
||||
},
|
||||
{
|
||||
name: 'data_stream.dataset',
|
||||
type: 'text',
|
||||
},
|
||||
'data_stream.dataset',
|
||||
safeLoad,
|
||||
'input'
|
||||
);
|
||||
};
|
||||
|
||||
it('should return an error message if the value has *', () => {
|
||||
const res = validateDataset('test*');
|
||||
|
||||
expect(res).toEqual([datasetError]);
|
||||
});
|
||||
|
||||
it('should return an error message if the value has uppercase letter', () => {
|
||||
const res = validateDataset('Test');
|
||||
|
||||
expect(res).toEqual(['Dataset must be lowercase']);
|
||||
});
|
||||
|
||||
it('should return an error message if the value has _ in the beginning', () => {
|
||||
const res = validateDataset('_test');
|
||||
|
||||
expect(res).toEqual(['Dataset cannot start with an underscore or dot']);
|
||||
});
|
||||
|
||||
it('should return an error message if the value has . in the beginning', () => {
|
||||
const res = validateDataset('.test');
|
||||
|
||||
expect(res).toEqual(['Dataset cannot start with an underscore or dot']);
|
||||
});
|
||||
|
||||
it('should not return an error message if the value is valid', () => {
|
||||
const res = validateDataset('fleet_server.test_dataset');
|
||||
|
||||
expect(res).toEqual(null);
|
||||
});
|
||||
|
||||
it('should not return an error message if the value is undefined', () => {
|
||||
const res = validatePackagePolicyConfig(
|
||||
{
|
||||
type: 'text',
|
||||
value: undefined,
|
||||
},
|
||||
{
|
||||
name: 'data_stream.dataset',
|
||||
type: 'text',
|
||||
},
|
||||
'data_stream.dataset',
|
||||
safeLoad,
|
||||
'input'
|
||||
);
|
||||
|
||||
expect(res).toEqual(null);
|
||||
});
|
||||
|
||||
it('should not return an error message if the package is not input type', () => {
|
||||
const res = validatePackagePolicyConfig(
|
||||
{
|
||||
type: 'text',
|
||||
value: { dataset: 'Test', package: 'log' },
|
||||
},
|
||||
{
|
||||
name: 'data_stream.dataset',
|
||||
type: 'text',
|
||||
},
|
||||
'data_stream.dataset',
|
||||
safeLoad,
|
||||
'integration'
|
||||
);
|
||||
|
||||
expect(res).toEqual(null);
|
||||
});
|
||||
|
||||
it('should not return an error message if the var is not dataset', () => {
|
||||
const res = validatePackagePolicyConfig(
|
||||
{
|
||||
type: 'text',
|
||||
value: { dataset: 'Test', package: 'log' },
|
||||
},
|
||||
{
|
||||
name: 'test_field',
|
||||
type: 'text',
|
||||
},
|
||||
'test_field',
|
||||
safeLoad,
|
||||
'input'
|
||||
);
|
||||
|
||||
expect(res).toEqual(null);
|
||||
});
|
||||
|
||||
it('should return an error message if the string dataset value has special characters', () => {
|
||||
const res = validatePackagePolicyConfig(
|
||||
{
|
||||
type: 'text',
|
||||
value: 'test*',
|
||||
},
|
||||
{
|
||||
name: 'data_stream.dataset',
|
||||
type: 'text',
|
||||
},
|
||||
'data_stream.dataset',
|
||||
safeLoad,
|
||||
'input'
|
||||
);
|
||||
|
||||
expect(res).toEqual(['Dataset contains invalid characters']);
|
||||
});
|
||||
|
||||
it('should return an error message if the dataset value has special characters', () => {
|
||||
const res = validatePackagePolicyConfig(
|
||||
{
|
||||
type: 'text',
|
||||
value: { dataset: 'test*', package: 'log' },
|
||||
},
|
||||
{
|
||||
name: 'data_stream.dataset',
|
||||
type: 'text',
|
||||
},
|
||||
'data_stream.dataset',
|
||||
safeLoad,
|
||||
'input'
|
||||
);
|
||||
|
||||
expect(res).toEqual(['Dataset contains invalid characters']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,6 +19,8 @@ import type {
|
|||
RegistryVarsEntry,
|
||||
} from '../types';
|
||||
|
||||
import { DATASET_VAR_NAME } from '../constants';
|
||||
|
||||
import {
|
||||
isValidNamespace,
|
||||
doesPackageHaveIntegrations,
|
||||
|
@ -26,6 +28,7 @@ import {
|
|||
getNormalizedDataStreams,
|
||||
} from '.';
|
||||
import { packageHasNoPolicyTemplates } from './policy_template';
|
||||
import { isValidDataset } from './is_valid_namespace';
|
||||
|
||||
type Errors = string[] | null;
|
||||
|
||||
|
@ -173,7 +176,13 @@ export const validatePackagePolicy = (
|
|||
|
||||
results[name] =
|
||||
input.enabled && stream.enabled
|
||||
? validatePackagePolicyConfig(configEntry, streamVarDefs[name], name, safeLoadYaml)
|
||||
? validatePackagePolicyConfig(
|
||||
configEntry,
|
||||
streamVarDefs[name],
|
||||
name,
|
||||
safeLoadYaml,
|
||||
packageInfo.type
|
||||
)
|
||||
: null;
|
||||
|
||||
return results;
|
||||
|
@ -202,7 +211,8 @@ export const validatePackagePolicyConfig = (
|
|||
configEntry: PackagePolicyConfigRecordEntry | undefined,
|
||||
varDef: RegistryVarsEntry,
|
||||
varName: string,
|
||||
safeLoadYaml: (yaml: string) => any
|
||||
safeLoadYaml: (yaml: string) => any,
|
||||
packageType?: string
|
||||
): string[] | null => {
|
||||
const errors = [];
|
||||
|
||||
|
@ -357,6 +367,16 @@ export const validatePackagePolicyConfig = (
|
|||
}
|
||||
}
|
||||
|
||||
if (varName === DATASET_VAR_NAME && packageType === 'input' && parsedValue !== undefined) {
|
||||
const { valid, error } = isValidDataset(
|
||||
parsedValue.dataset ? parsedValue.dataset : parsedValue,
|
||||
false
|
||||
);
|
||||
if (!valid && error) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
|
||||
return errors.length ? errors : null;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,133 +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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { EuiComboBox, EuiIcon, EuiLink, EuiSpacer, EuiText, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import type { DataStream } from '../../../../../../../../../common/types';
|
||||
import { GENERIC_DATASET_NAME } from '../../../../../../../../../common/constants';
|
||||
|
||||
interface SelectedDataset {
|
||||
dataset: string;
|
||||
package: string;
|
||||
}
|
||||
|
||||
export const DatasetComboBox: React.FC<{
|
||||
value?: SelectedDataset | string;
|
||||
onChange: (newValue: SelectedDataset) => void;
|
||||
datastreams: DataStream[];
|
||||
pkgName?: string;
|
||||
isDisabled?: boolean;
|
||||
}> = ({ value, onChange, datastreams, isDisabled, pkgName = '' }) => {
|
||||
const datasetOptions =
|
||||
datastreams.map((datastream: DataStream) => ({
|
||||
label: datastream.dataset,
|
||||
value: datastream,
|
||||
})) ?? [];
|
||||
const existingGenericStream = datasetOptions.find((ds) => ds.label === GENERIC_DATASET_NAME);
|
||||
const valueAsOption = value
|
||||
? typeof value === 'string'
|
||||
? { label: value, value: { dataset: value, package: pkgName } }
|
||||
: { label: value.dataset, value: { dataset: value.dataset, package: value.package } }
|
||||
: undefined;
|
||||
const defaultOption = valueAsOption ||
|
||||
existingGenericStream || {
|
||||
label: GENERIC_DATASET_NAME,
|
||||
value: { dataset: GENERIC_DATASET_NAME, package: pkgName },
|
||||
};
|
||||
|
||||
const [selectedOptions, setSelectedOptions] = useState<Array<{ label: string }>>([defaultOption]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!value || typeof value === 'string') onChange(defaultOption.value as SelectedDataset);
|
||||
}, [value, defaultOption.value, onChange, pkgName]);
|
||||
|
||||
const onDatasetChange = (newSelectedOptions: Array<{ label: string; value?: DataStream }>) => {
|
||||
setSelectedOptions(newSelectedOptions);
|
||||
const dataStream = newSelectedOptions[0].value;
|
||||
onChange({
|
||||
dataset: newSelectedOptions[0].label,
|
||||
package: !dataStream || typeof dataStream === 'string' ? pkgName : dataStream.package,
|
||||
});
|
||||
};
|
||||
|
||||
const onCreateOption = (searchValue: string = '') => {
|
||||
const normalizedSearchValue = searchValue.trim().toLowerCase();
|
||||
if (!normalizedSearchValue) {
|
||||
return;
|
||||
}
|
||||
const newOption = {
|
||||
label: searchValue,
|
||||
value: { dataset: searchValue, package: pkgName },
|
||||
};
|
||||
setSelectedOptions([newOption]);
|
||||
onChange({
|
||||
dataset: searchValue,
|
||||
package: pkgName,
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<EuiComboBox
|
||||
aria-label={i18n.translate('xpack.fleet.datasetCombo.ariaLabel', {
|
||||
defaultMessage: 'Dataset combo box',
|
||||
})}
|
||||
placeholder={i18n.translate('xpack.fleet.datasetCombo.placeholder', {
|
||||
defaultMessage: 'Select a dataset',
|
||||
})}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
options={datasetOptions}
|
||||
selectedOptions={selectedOptions}
|
||||
onCreateOption={onCreateOption}
|
||||
onChange={onDatasetChange}
|
||||
customOptionText={i18n.translate('xpack.fleet.datasetCombo.customOptionText', {
|
||||
defaultMessage: 'Add {searchValue} as a custom option',
|
||||
values: { searchValue: '{searchValue}' },
|
||||
})}
|
||||
isClearable={false}
|
||||
isDisabled={isDisabled}
|
||||
data-test-subj="datasetComboBox"
|
||||
/>
|
||||
{valueAsOption && valueAsOption.value.package !== pkgName && (
|
||||
<>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiText size="xs" color="warning">
|
||||
<EuiIcon type="warning" />
|
||||
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.datasetCombo.warning"
|
||||
defaultMessage="This data stream is managed by the {package} integration, {learnMore}."
|
||||
values={{
|
||||
package: valueAsOption.value.package,
|
||||
learnMore: (
|
||||
<EuiToolTip
|
||||
position="bottom"
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.datasetCombo.warningTooltip"
|
||||
defaultMessage="The destination data stream may not be designed to receive data from this integration, check that the mappings and ingest pipelines are compatible before sending data."
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiLink target="_blank">
|
||||
{i18n.translate('xpack.fleet.datasetCombo.learnMoreLink', {
|
||||
defaultMessage: 'learn more',
|
||||
})}
|
||||
</EuiLink>
|
||||
</EuiToolTip>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
|
||||
import { createFleetTestRendererMock } from '../../../../../../../../mock';
|
||||
|
||||
import { DatasetComponent } from './dataset_component';
|
||||
|
||||
describe('DatasetComponent', () => {
|
||||
function render(value = 'generic', datastreams: any = []) {
|
||||
const renderer = createFleetTestRendererMock();
|
||||
const mockOnChange = jest.fn();
|
||||
const fieldLabel = 'Dataset name';
|
||||
|
||||
const utils = renderer.render(
|
||||
<DatasetComponent
|
||||
pkgName={'log'}
|
||||
datastreams={datastreams}
|
||||
value={{
|
||||
dataset: value,
|
||||
package: 'log',
|
||||
}}
|
||||
onChange={mockOnChange}
|
||||
isDisabled={false}
|
||||
fieldLabel={fieldLabel}
|
||||
/>
|
||||
);
|
||||
|
||||
return { utils, mockOnChange };
|
||||
}
|
||||
|
||||
it('should show validation error if dataset is invalid', () => {
|
||||
const { utils } = render();
|
||||
|
||||
const inputEl = utils.getByTestId('comboBoxSearchInput');
|
||||
fireEvent.change(inputEl, { target: { value: 'generic*' } });
|
||||
fireEvent.keyDown(inputEl, { key: 'Enter', code: 'Enter' });
|
||||
|
||||
utils.getByText('Dataset contains invalid characters');
|
||||
});
|
||||
|
||||
it('should not show validation error if dataset is valid', () => {
|
||||
const { utils } = render();
|
||||
|
||||
const inputEl = utils.getByTestId('comboBoxSearchInput');
|
||||
fireEvent.change(inputEl, { target: { value: 'test' } });
|
||||
fireEvent.keyDown(inputEl, { key: 'Enter', code: 'Enter' });
|
||||
|
||||
expect(utils.queryByText('Dataset contains invalid characters')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not show validation error if valid dataset selected from select', () => {
|
||||
const { utils, mockOnChange } = render(undefined, [
|
||||
{ dataset: 'fleet_server.test_ds', package: 'log' },
|
||||
]);
|
||||
|
||||
const inputEl = utils.getByTestId('comboBoxSearchInput');
|
||||
fireEvent.click(inputEl);
|
||||
const option = utils.getByText('fleet_server.test_ds');
|
||||
fireEvent.click(option);
|
||||
|
||||
expect(utils.queryByText('Dataset contains invalid characters')).toBeNull();
|
||||
expect(mockOnChange).toHaveBeenCalledWith({ dataset: 'fleet_server.test_ds', package: 'log' });
|
||||
});
|
||||
});
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* 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 React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
EuiComboBox,
|
||||
EuiFormRow,
|
||||
EuiIcon,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import styled from 'styled-components';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import type { DataStream } from '../../../../../../../../../common/types';
|
||||
import { GENERIC_DATASET_NAME } from '../../../../../../../../../common/constants';
|
||||
import { isValidDataset } from '../../../../../../../../../common';
|
||||
|
||||
const FormRow = styled(EuiFormRow)`
|
||||
.euiFormRow__label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.euiFormRow__fieldWrapper > .euiPanel {
|
||||
padding: ${(props) => props.theme.eui?.euiSizeXS};
|
||||
}
|
||||
`;
|
||||
|
||||
interface SelectedDataset {
|
||||
dataset: string;
|
||||
package: string;
|
||||
}
|
||||
|
||||
export const DatasetComponent: React.FC<{
|
||||
value?: SelectedDataset | string;
|
||||
onChange: (newValue: SelectedDataset) => void;
|
||||
datastreams: DataStream[];
|
||||
pkgName?: string;
|
||||
isDisabled?: boolean;
|
||||
fieldLabel: string;
|
||||
description?: string;
|
||||
}> = ({ value, onChange, datastreams, isDisabled, pkgName = '', fieldLabel, description }) => {
|
||||
const datasetOptions =
|
||||
datastreams.map((datastream: DataStream) => ({
|
||||
label: datastream.dataset,
|
||||
value: datastream,
|
||||
})) ?? [];
|
||||
const existingGenericStream = datasetOptions.find((ds) => ds.label === GENERIC_DATASET_NAME);
|
||||
const valueAsOption = value
|
||||
? typeof value === 'string'
|
||||
? { label: value, value: { dataset: value, package: pkgName } }
|
||||
: { label: value.dataset, value: { dataset: value.dataset, package: value.package } }
|
||||
: undefined;
|
||||
const defaultOption = valueAsOption ||
|
||||
existingGenericStream || {
|
||||
label: GENERIC_DATASET_NAME,
|
||||
value: { dataset: GENERIC_DATASET_NAME, package: pkgName },
|
||||
};
|
||||
|
||||
const [selectedOptions, setSelectedOptions] = useState<Array<{ label: string }>>([defaultOption]);
|
||||
const [isInvalid, setIsInvalid] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (!value || typeof value === 'string') onChange(defaultOption.value as SelectedDataset);
|
||||
}, [value, defaultOption.value, onChange, pkgName]);
|
||||
|
||||
const onDatasetChange = (newSelectedOptions: Array<{ label: string; value?: DataStream }>) => {
|
||||
setSelectedOptions(newSelectedOptions);
|
||||
const dataStream = newSelectedOptions[0].value;
|
||||
const { valid, error: dsError } = isValidDataset(newSelectedOptions[0].label, false);
|
||||
setIsInvalid(!valid);
|
||||
setError(dsError);
|
||||
onChange({
|
||||
dataset: newSelectedOptions[0].label,
|
||||
package: !dataStream || typeof dataStream === 'string' ? pkgName : dataStream.package,
|
||||
});
|
||||
};
|
||||
|
||||
const onCreateOption = (searchValue: string = '') => {
|
||||
const normalizedSearchValue = searchValue.trim().toLowerCase();
|
||||
if (!normalizedSearchValue) {
|
||||
return;
|
||||
}
|
||||
const newOption = {
|
||||
label: searchValue,
|
||||
value: { dataset: searchValue, package: pkgName },
|
||||
};
|
||||
setSelectedOptions([newOption]);
|
||||
const { valid, error: dsError } = isValidDataset(searchValue, false);
|
||||
setIsInvalid(!valid);
|
||||
setError(dsError);
|
||||
onChange({
|
||||
dataset: searchValue,
|
||||
package: pkgName,
|
||||
});
|
||||
};
|
||||
return (
|
||||
<FormRow
|
||||
isInvalid={isInvalid}
|
||||
error={error}
|
||||
hasChildLabel={true}
|
||||
label={fieldLabel}
|
||||
helpText={description && <ReactMarkdown children={description} />}
|
||||
fullWidth
|
||||
>
|
||||
<>
|
||||
<EuiComboBox
|
||||
aria-label={i18n.translate('xpack.fleet.datasetCombo.ariaLabel', {
|
||||
defaultMessage: 'Dataset combo box',
|
||||
})}
|
||||
placeholder={i18n.translate('xpack.fleet.datasetCombo.placeholder', {
|
||||
defaultMessage: 'Select a dataset',
|
||||
})}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
options={datasetOptions}
|
||||
selectedOptions={selectedOptions}
|
||||
onCreateOption={onCreateOption}
|
||||
onChange={onDatasetChange}
|
||||
customOptionText={i18n.translate('xpack.fleet.datasetCombo.customOptionText', {
|
||||
defaultMessage: 'Add {searchValue} as a custom option',
|
||||
values: { searchValue: '{searchValue}' },
|
||||
})}
|
||||
isClearable={false}
|
||||
isDisabled={isDisabled}
|
||||
data-test-subj="datasetComboBox"
|
||||
isInvalid={isInvalid}
|
||||
/>
|
||||
{valueAsOption && valueAsOption.value.package !== pkgName && (
|
||||
<>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiText size="xs" color="warning">
|
||||
<EuiIcon type="warning" />
|
||||
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.datasetCombo.warning"
|
||||
defaultMessage="This data stream is managed by the {package} integration, {learnMore}."
|
||||
values={{
|
||||
package: valueAsOption.value.package,
|
||||
learnMore: (
|
||||
<EuiToolTip
|
||||
position="bottom"
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.datasetCombo.warningTooltip"
|
||||
defaultMessage="The destination data stream may not be designed to receive data from this integration, check that the mappings and ingest pipelines are compatible before sending data."
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiLink target="_blank">
|
||||
{i18n.translate('xpack.fleet.datasetCombo.learnMoreLink', {
|
||||
defaultMessage: 'learn more',
|
||||
})}
|
||||
</EuiLink>
|
||||
</EuiToolTip>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</FormRow>
|
||||
);
|
||||
};
|
|
@ -36,7 +36,7 @@ import { DATASET_VAR_NAME } from '../../../../../../../../../common/constants';
|
|||
import type { DataStream, RegistryVarsEntry } from '../../../../../../types';
|
||||
|
||||
import { MultiTextInput } from './multi_text_input';
|
||||
import { DatasetComboBox } from './dataset_combo';
|
||||
import { DatasetComponent } from './dataset_component';
|
||||
|
||||
const FixedHeightDiv = styled.div`
|
||||
height: 300px;
|
||||
|
@ -88,7 +88,6 @@ export const PackagePolicyInputVarField: React.FunctionComponent<InputFieldProps
|
|||
isEditPage = false,
|
||||
}) => {
|
||||
const fleetStatus = useFleetStatus();
|
||||
|
||||
const [isDirty, setIsDirty] = useState<boolean>(false);
|
||||
const { required, type, title, name, description } = varDef;
|
||||
const isInvalid = Boolean((isDirty || forceShowErrors) && !!varErrors?.length);
|
||||
|
@ -101,6 +100,20 @@ export const PackagePolicyInputVarField: React.FunctionComponent<InputFieldProps
|
|||
const secretsStorageEnabled = fleetStatus.isReady && fleetStatus.isSecretsStorageEnabled;
|
||||
const useSecretsUi = secretsStorageEnabled && varDef.secret;
|
||||
|
||||
if (name === DATASET_VAR_NAME && packageType === 'input') {
|
||||
return (
|
||||
<DatasetComponent
|
||||
pkgName={packageName}
|
||||
datastreams={datastreams}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
isDisabled={isEditPage}
|
||||
fieldLabel={fieldLabel}
|
||||
description={description}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let field: JSX.Element;
|
||||
|
||||
if (useSecretsUi) {
|
||||
|
@ -127,10 +140,6 @@ export const PackagePolicyInputVarField: React.FunctionComponent<InputFieldProps
|
|||
value,
|
||||
onChange,
|
||||
frozen,
|
||||
packageName,
|
||||
packageType,
|
||||
datastreams,
|
||||
isEditPage,
|
||||
isInvalid,
|
||||
fieldLabel,
|
||||
fieldTestSelector,
|
||||
|
@ -171,16 +180,12 @@ function getInputComponent({
|
|||
value,
|
||||
onChange,
|
||||
frozen,
|
||||
packageName,
|
||||
packageType,
|
||||
datastreams = [],
|
||||
isEditPage,
|
||||
isInvalid,
|
||||
fieldLabel,
|
||||
fieldTestSelector,
|
||||
setIsDirty,
|
||||
}: InputComponentProps) {
|
||||
const { multi, type, name, options } = varDef;
|
||||
const { multi, type, options } = varDef;
|
||||
if (multi) {
|
||||
return (
|
||||
<MultiTextInput
|
||||
|
@ -193,17 +198,6 @@ function getInputComponent({
|
|||
/>
|
||||
);
|
||||
}
|
||||
if (name === DATASET_VAR_NAME && packageType === 'input') {
|
||||
return (
|
||||
<DatasetComboBox
|
||||
pkgName={packageName}
|
||||
datastreams={datastreams}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
isDisabled={isEditPage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
switch (type) {
|
||||
case 'textarea':
|
||||
return (
|
||||
|
|
|
@ -18714,10 +18714,6 @@
|
|||
"xpack.fleet.multiRowInput.addRow": "Ajouter une ligne",
|
||||
"xpack.fleet.multiRowInput.deleteButton": "Supprimer la ligne",
|
||||
"xpack.fleet.multiTextInput.addRow": "Ajouter une ligne",
|
||||
"xpack.fleet.namespaceValidation.invalidCharactersErrorMessage": "L'espace de nom contient des caractères non valides",
|
||||
"xpack.fleet.namespaceValidation.lowercaseErrorMessage": "L'espace de nom doit être en minuscules",
|
||||
"xpack.fleet.namespaceValidation.requiredErrorMessage": "L'espace de nom est obligatoire",
|
||||
"xpack.fleet.namespaceValidation.tooLongErrorMessage": "L'espace de nom ne peut pas dépasser 100 octets",
|
||||
"xpack.fleet.newEnrollmentKey.cancelButtonLabel": "Annuler",
|
||||
"xpack.fleet.newEnrollmentKey.helpText": "Un ID de jeton sera utilisé si ce champ est laissé vide.",
|
||||
"xpack.fleet.newEnrollmentKey.keyCreatedToasts": "Jeton d'enregistrement créé",
|
||||
|
|
|
@ -18691,10 +18691,6 @@
|
|||
"xpack.fleet.multiRowInput.addRow": "行の追加",
|
||||
"xpack.fleet.multiRowInput.deleteButton": "行の削除",
|
||||
"xpack.fleet.multiTextInput.addRow": "行の追加",
|
||||
"xpack.fleet.namespaceValidation.invalidCharactersErrorMessage": "名前空間に無効な文字が含まれています",
|
||||
"xpack.fleet.namespaceValidation.lowercaseErrorMessage": "名前空間は小文字で指定する必要があります",
|
||||
"xpack.fleet.namespaceValidation.requiredErrorMessage": "名前空間は必須です",
|
||||
"xpack.fleet.namespaceValidation.tooLongErrorMessage": "名前空間は100バイト以下でなければなりません",
|
||||
"xpack.fleet.newEnrollmentKey.cancelButtonLabel": "キャンセル",
|
||||
"xpack.fleet.newEnrollmentKey.helpText": "これを空にすると、トークンIDが使用されます。",
|
||||
"xpack.fleet.newEnrollmentKey.keyCreatedToasts": "登録トークンが作成されました",
|
||||
|
|
|
@ -18720,10 +18720,6 @@
|
|||
"xpack.fleet.multiRowInput.addRow": "添加行",
|
||||
"xpack.fleet.multiRowInput.deleteButton": "删除行",
|
||||
"xpack.fleet.multiTextInput.addRow": "添加行",
|
||||
"xpack.fleet.namespaceValidation.invalidCharactersErrorMessage": "命名空间包含无效字符",
|
||||
"xpack.fleet.namespaceValidation.lowercaseErrorMessage": "命名空间必须小写",
|
||||
"xpack.fleet.namespaceValidation.requiredErrorMessage": "“命名空间”必填",
|
||||
"xpack.fleet.namespaceValidation.tooLongErrorMessage": "命名空间不能超过 100 个字节",
|
||||
"xpack.fleet.newEnrollmentKey.cancelButtonLabel": "取消",
|
||||
"xpack.fleet.newEnrollmentKey.helpText": "此项留空时,将使用令牌 ID。",
|
||||
"xpack.fleet.newEnrollmentKey.keyCreatedToasts": "注册令牌已创建",
|
||||
|
|
|
@ -79,7 +79,7 @@ export default function (providerContext: FtrProviderContext) {
|
|||
.send(policy)
|
||||
.expect(expectStatusCode);
|
||||
|
||||
return res.body.item;
|
||||
return expectStatusCode === 200 ? res.body.item : res;
|
||||
};
|
||||
|
||||
const createAgentPolicy = async (name = 'Input Package Test 3') => {
|
||||
|
@ -117,7 +117,10 @@ export default function (providerContext: FtrProviderContext) {
|
|||
setupFleetAndAgents(providerContext);
|
||||
|
||||
it('should rollback package install on package policy create failure', async () => {
|
||||
await createPackagePolicyWithDataset(agentPolicyId, 'test*', 400);
|
||||
const res = await createPackagePolicyWithDataset(agentPolicyId, 'test*', 400);
|
||||
expect(res.body.message).to.eql(
|
||||
'Package policy is invalid: inputs.logfile.streams.input_package_upgrade.logs.vars.data_stream.dataset: Dataset contains invalid characters'
|
||||
);
|
||||
|
||||
const pkg = await getPackage(PACKAGE_NAME, START_VERSION);
|
||||
expect(pkg?.status).to.eql('not_installed');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue