diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 8adbd0a3d672e..5d34a004d8a99 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -29,6 +29,7 @@ export const IGNORE_FILE_GLOBS = [ 'x-pack/plugins/canvas/server/templates/assets/*.{png,jpg,svg}', 'x-pack/plugins/cases/docs/**/*', 'x-pack/plugins/monitoring/public/lib/jquery_flot/**/*', + 'x-pack/plugins/fleet/cypress/packages/*.zip', '**/.*', '**/__mocks__/**/*', 'x-pack/docs/**/*', diff --git a/x-pack/plugins/fleet/cypress/e2e/package_policy_real.cy.ts b/x-pack/plugins/fleet/cypress/e2e/package_policy_real.cy.ts new file mode 100644 index 0000000000000..00d9c4547966f --- /dev/null +++ b/x-pack/plugins/fleet/cypress/e2e/package_policy_real.cy.ts @@ -0,0 +1,98 @@ +/* + * 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 { + ADD_INTEGRATION_POLICY_BTN, + CREATE_PACKAGE_POLICY_SAVE_BTN, + INTEGRATION_NAME_LINK, + POLICY_EDITOR, +} from '../screens/integrations'; +import { EXISTING_HOSTS_TAB } from '../screens/fleet'; +import { CONFIRM_MODAL } from '../screens/navigation'; + +const TEST_PACKAGE = 'input_package-1.0.0'; +const agentPolicyId = 'test-input-package-policy'; +const agentPolicyName = 'Test input package policy'; +const inputPackagePolicyName = 'input-package-policy'; + +function editPackagePolicyandShowAdvanced() { + cy.visit(`/app/integrations/detail/${TEST_PACKAGE}/policies`); + + cy.getBySel(INTEGRATION_NAME_LINK).contains(inputPackagePolicyName).click(); + + cy.get('button').contains('Change defaults').click(); + cy.get('[data-test-subj^="advancedStreamOptionsToggle"]').click(); +} +describe('Input package create and edit package policy', () => { + before(() => { + cy.task('installTestPackage', TEST_PACKAGE); + + cy.request({ + method: 'POST', + url: `/api/fleet/agent_policies`, + body: { + id: agentPolicyId, + name: agentPolicyName, + description: 'desc', + namespace: 'default', + monitoring_enabled: [], + }, + headers: { 'kbn-xsrf': 'cypress' }, + }); + }); + after(() => { + // delete agent policy + cy.request({ + method: 'POST', + url: `/api/fleet/agent_policies/delete`, + headers: { 'kbn-xsrf': 'cypress' }, + body: JSON.stringify({ + agentPolicyId, + }), + }); + cy.task('uninstallTestPackage', TEST_PACKAGE); + }); + it('should successfully create a package policy', () => { + cy.visit(`/app/integrations/detail/${TEST_PACKAGE}/overview`); + cy.getBySel(ADD_INTEGRATION_POLICY_BTN).click(); + + cy.getBySel(POLICY_EDITOR.POLICY_NAME_INPUT).click().clear().type(inputPackagePolicyName); + cy.getBySel('multiTextInput-paths') + .find('[data-test-subj="multiTextInputRow-0"]') + .click() + .type('/var/log/test.log'); + + cy.getBySel('multiTextInput-tags') + .find('[data-test-subj="multiTextInputRow-0"]') + .click() + .type('tag1'); + + cy.getBySel(POLICY_EDITOR.DATASET_SELECT).click().type('testdataset'); + + cy.getBySel(EXISTING_HOSTS_TAB).click(); + + cy.getBySel(POLICY_EDITOR.AGENT_POLICY_SELECT).click().get(`#${agentPolicyId}`).click(); + cy.wait(500); // wait for policy id to be set + cy.getBySel(CREATE_PACKAGE_POLICY_SAVE_BTN).click(); + + cy.getBySel(CONFIRM_MODAL.CANCEL_BUTTON).click(); + }); + + it('should show pipelines editor with link to pipeline', () => { + editPackagePolicyandShowAdvanced(); + cy.getBySel(POLICY_EDITOR.INSPECT_PIPELINES_BTN).click(); + cy.getBySel(CONFIRM_MODAL.CONFIRM_BUTTON).click(); + cy.get('body').should('not.contain', 'Pipeline not found'); + cy.get('body').should('contain', '"managed_by": "fleet"'); + }); + it('should show mappings editor with link to create custom template', () => { + editPackagePolicyandShowAdvanced(); + cy.getBySel(POLICY_EDITOR.EDIT_MAPPINGS_BTN).click(); + cy.getBySel(CONFIRM_MODAL.CONFIRM_BUTTON).click(); + cy.get('body').should('contain', 'logs-testdataset@custom'); + }); +}); diff --git a/x-pack/plugins/fleet/cypress/packages/input_package-1.0.0.zip b/x-pack/plugins/fleet/cypress/packages/input_package-1.0.0.zip new file mode 100644 index 0000000000000..639072cab5ea7 Binary files /dev/null and b/x-pack/plugins/fleet/cypress/packages/input_package-1.0.0.zip differ diff --git a/x-pack/plugins/fleet/cypress/plugins/index.ts b/x-pack/plugins/fleet/cypress/plugins/index.ts index 17825ba12a2bb..ee01dd20c470c 100644 --- a/x-pack/plugins/fleet/cypress/plugins/index.ts +++ b/x-pack/plugins/fleet/cypress/plugins/index.ts @@ -5,12 +5,42 @@ * 2.0. */ +import { promisify } from 'util'; + +import fs from 'fs'; + +import fetch from 'node-fetch'; import { createEsClientForTesting } from '@kbn/test'; const plugin: Cypress.PluginConfig = (on, config) => { const client = createEsClientForTesting({ esUrl: config.env.ELASTICSEARCH_URL, }); + + async function kibanaFetch(opts: { + method: string; + path: string; + body?: any; + contentType?: string; + }) { + const { method, path, body, contentType } = opts; + const Authorization = `Basic ${Buffer.from( + `elastic:${config.env.ELASTICSEARCH_PASSWORD}` + ).toString('base64')}`; + + const url = `${config.env.KIBANA_URL}${path}`; + const res = await fetch(url, { + method, + headers: { + 'kbn-xsrf': 'cypress', + 'Content-Type': contentType || 'application/json', + Authorization, + }, + ...(body ? { body } : {}), + }); + + return res.json(); + } on('task', { async insertDoc({ index, doc, id }: { index: string; doc: any; id: string }) { return client.create({ id, document: doc, index, refresh: 'wait_for' }); @@ -37,6 +67,23 @@ const plugin: Cypress.PluginConfig = (on, config) => { conflicts: 'proceed', }); }, + async installTestPackage(packageName: string) { + const zipPath = require.resolve('../packages/' + packageName + '.zip'); + const zipContent = await promisify(fs.readFile)(zipPath, 'base64'); + return kibanaFetch({ + method: 'POST', + path: '/api/fleet/epm/packages', + body: Buffer.from(zipContent, 'base64'), + contentType: 'application/zip', + }); + }, + + async uninstallTestPackage(packageName: string) { + return kibanaFetch({ + method: 'DELETE', + path: `/api/fleet/epm/packages/${packageName}`, + }); + }, }); }; diff --git a/x-pack/plugins/fleet/cypress/screens/integrations.ts b/x-pack/plugins/fleet/cypress/screens/integrations.ts index 63b13d8ff7fd3..2faa0ade447a6 100644 --- a/x-pack/plugins/fleet/cypress/screens/integrations.ts +++ b/x-pack/plugins/fleet/cypress/screens/integrations.ts @@ -35,6 +35,14 @@ export const SETTINGS = { UNINSTALL_ASSETS_BTN: 'uninstallAssetsButton', }; +export const POLICY_EDITOR = { + POLICY_NAME_INPUT: 'packagePolicyNameInput', + DATASET_SELECT: 'datasetComboBox', + AGENT_POLICY_SELECT: 'agentPolicySelect', + INSPECT_PIPELINES_BTN: 'datastreamInspectPipelineBtn', + EDIT_MAPPINGS_BTN: 'datastreamEditMappingsBtn', +}; + export const INTEGRATION_POLICIES_UPGRADE_CHECKBOX = 'epmDetails.upgradePoliciesCheckbox'; export const getIntegrationCard = (integration: string) => `integration-card:epr:${integration}`; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/datastream_hooks.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/datastream_hooks.tsx index d8e21fce7c306..de740b99d97ce 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/datastream_hooks.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/datastream_hooks.tsx @@ -6,8 +6,9 @@ */ import { useRouteMatch } from 'react-router-dom'; +import { useQuery } from '@tanstack/react-query'; -import { useLink } from '../../../../hooks'; +import { sendRequestForRq, useLink } from '../../../../hooks'; export function usePackagePolicyEditorPageUrl(dataStreamId?: string) { const { @@ -27,3 +28,32 @@ export function usePackagePolicyEditorPageUrl(dataStreamId?: string) { return `${baseUrl}${dataStreamId ? `?datastreamId=${encodeURIComponent(dataStreamId)}` : ''}`; } + +export function useIndexTemplateExists( + templateName: string, + enabled: boolean +): { + exists?: boolean; + isLoading: boolean; +} { + const { data, isLoading } = useQuery( + ['indexTemplateExists', templateName], + () => + sendRequestForRq({ + path: `/api/index_management/index_templates/${templateName}`, + method: 'get', + }), + { enabled: enabled || !!templateName } + ); + + if (isLoading) { + return { + isLoading: true, + }; + } + + return { + exists: !!data, + isLoading: false, + }; +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/datastream_mappings.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/datastream_mappings.tsx index 37963d4abe1ac..86d1f3404d4f9 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/datastream_mappings.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/datastream_mappings.tsx @@ -18,7 +18,11 @@ import { usePackagePolicyEditorPageUrl } from './datastream_hooks'; export interface PackagePolicyEditorDatastreamMappingsProps { packageInfo: PackageInfo; - packageInputStream: { id?: string; data_stream: { dataset: string; type: string } }; + packageInputStream: { + id?: string; + data_stream: { dataset: string; type: string }; + }; + customDataset?: string; } function useComponentTemplates(dataStream: { dataset: string; type: string }) { @@ -35,8 +39,10 @@ function useComponentTemplates(dataStream: { dataset: string; type: string }) { export const PackagePolicyEditorDatastreamMappings: React.FunctionComponent< PackagePolicyEditorDatastreamMappingsProps -> = ({ packageInputStream, packageInfo }) => { - const dataStream = packageInputStream.data_stream; +> = ({ packageInputStream, packageInfo, customDataset }) => { + const dataStream = customDataset + ? { ...packageInputStream.data_stream, dataset: customDataset } + : packageInputStream.data_stream; const pageUrl = usePackagePolicyEditorPageUrl(packageInputStream.id); const { application, docLinks } = useStartServices(); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/datastream_pipelines.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/datastream_pipelines.tsx index 75e95a7975f61..b2325dcc15213 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/datastream_pipelines.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/datastream_pipelines.tsx @@ -31,6 +31,7 @@ import { usePackagePolicyEditorPageUrl } from './datastream_hooks'; export interface PackagePolicyEditorDatastreamPipelinesProps { packageInfo: PackageInfo; packageInputStream: { id?: string; data_stream: { dataset: string; type: string } }; + customDataset?: string; } interface PipelineItem { @@ -92,8 +93,10 @@ function useDatastreamIngestPipelines( export const PackagePolicyEditorDatastreamPipelines: React.FunctionComponent< PackagePolicyEditorDatastreamPipelinesProps -> = ({ packageInputStream, packageInfo }) => { - const dataStream = packageInputStream.data_stream; +> = ({ packageInputStream, packageInfo, customDataset }) => { + const dataStream = customDataset + ? { ...packageInputStream.data_stream, dataset: customDataset } + : packageInputStream.data_stream; const { application, share, docLinks } = useStartServices(); const ingestPipelineLocator = share.url.locators.get('INGEST_PIPELINES_APP_LOCATOR'); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/dataset_combo.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/dataset_combo.tsx index 680da0fd75012..31fcbee8d6821 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/dataset_combo.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/dataset_combo.tsx @@ -94,6 +94,7 @@ export const DatasetComboBox: React.FC<{ })} isClearable={false} isDisabled={isDisabled} + data-test-subj="datasetComboBox" /> {valueAsOption && valueAsOption.value.package !== pkgName && ( <> diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/multi_text_input.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/multi_text_input.tsx index 66ec097624c41..ebcf666d24710 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/multi_text_input.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/multi_text_input.tsx @@ -25,6 +25,7 @@ interface Props { errors?: Array<{ message: string; index?: number }>; isInvalid?: boolean; isDisabled?: boolean; + 'data-test-subj'?: string; } interface RowProps { @@ -69,6 +70,7 @@ const Row: FunctionComponent = ({ autoFocus={autoFocus} disabled={isDisabled} onBlur={onBlur} + data-test-subj={`multiTextInputRow-${index}`} /> {showDeleteButton && ( @@ -99,6 +101,7 @@ export const MultiTextInput: FunctionComponent = ({ isInvalid, isDisabled, errors, + 'data-test-subj': dataTestSubj, }) => { const [autoFocus, setAutoFocus] = useState(false); const [rows, setRows] = useState(() => defaultValue(value)); @@ -139,7 +142,7 @@ export const MultiTextInput: FunctionComponent = ({ return ( <> - + {rows.map((row, idx) => ( ( !!packagePolicyInputStream.id && packagePolicyInputStream.id === defaultDataStreamId; const isPackagePolicyEdit = !!packagePolicyId; - const isInputOnlyPackage = packageInfo.type === 'input'; - const hasDatasetVar = packageInputStream.vars?.some( - (varDef) => varDef.name === DATASET_VAR_NAME - ); - const showPipelinesAndMappings = !isInputOnlyPackage && !hasDatasetVar; + const customDatasetVar = packagePolicyInputStream.vars?.[DATASET_VAR_NAME]; + const customDatasetVarValue = customDatasetVar?.value?.dataset || customDatasetVar?.value; + + const { exists: indexTemplateExists, isLoading: isLoadingIndexTemplate } = + useIndexTemplateExists( + getRegistryDataStreamAssetBaseName({ + dataset: customDatasetVarValue, + type: packageInputStream.data_stream.type, + }), + isPackagePolicyEdit + ); + + // only show pipelines and mappings if the matching index template exists + // in the legacy case (e.g logs package pre 2.0.0) the index template will not exist + // because we allowed dataset to be customized but didnt create a matching index template + // for the new dataset. + const showPipelinesAndMappings = !isLoadingIndexTemplate && indexTemplateExists; + useEffect(() => { if (isDefaultDatastream && containerRef.current) { containerRef.current.scrollIntoView(); @@ -249,6 +267,7 @@ export const PackagePolicyInputStreamConfig = memo( iconType={isShowingAdvanced ? 'arrowDown' : 'arrowRight'} onClick={() => setIsShowingAdvanced(!isShowingAdvanced)} flush="left" + data-test-subj={`advancedStreamOptionsToggle-${packagePolicyInputStream.id}`} > ( ); })} - {/* Only show datastream pipelines and mappings on edit and not for input packages*/} {isPackagePolicyEdit && showPipelinesAndMappings && ( <> diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_var_field.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_var_field.tsx index 27457dedbc9b1..827d1fb2722c2 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_var_field.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_var_field.tsx @@ -63,7 +63,7 @@ export const PackagePolicyInputVarField: React.FunctionComponent<{ const isInvalid = (isDirty || forceShowErrors) && !!varErrors; const errors = isInvalid ? varErrors : null; const fieldLabel = title || name; - + const fieldTestSelector = fieldLabel.replace(/\s/g, '-').toLowerCase(); const field = useMemo(() => { if (multi) { return ( @@ -72,6 +72,7 @@ export const PackagePolicyInputVarField: React.FunctionComponent<{ onChange={onChange} onBlur={() => setIsDirty(true)} isDisabled={frozen} + data-test-subj={`multiTextInput-${fieldTestSelector}`} /> ); } @@ -96,6 +97,7 @@ export const PackagePolicyInputVarField: React.FunctionComponent<{ onBlur={() => setIsDirty(true)} disabled={frozen} resize="vertical" + data-test-subj={`textAreaInput-${fieldTestSelector}`} /> ); case 'yaml': @@ -142,6 +144,7 @@ export const PackagePolicyInputVarField: React.FunctionComponent<{ onChange={(e) => onChange(e.target.checked)} onBlur={() => setIsDirty(true)} disabled={frozen} + data-test-subj={`switch-${fieldTestSelector}`} /> ); case 'password': @@ -153,6 +156,7 @@ export const PackagePolicyInputVarField: React.FunctionComponent<{ onChange={(e) => onChange(e.target.value)} onBlur={() => setIsDirty(true)} disabled={frozen} + data-test-subj={`passwordInput-${fieldTestSelector}`} /> ); case 'select': @@ -177,6 +181,7 @@ export const PackagePolicyInputVarField: React.FunctionComponent<{ return onChange(newValue); }} onBlur={() => setIsDirty(true)} + data-test-subj={`select-${fieldTestSelector}`} /> ); default: @@ -187,6 +192,7 @@ export const PackagePolicyInputVarField: React.FunctionComponent<{ onChange={(e) => onChange(e.target.value)} onBlur={() => setIsDirty(true)} disabled={frozen} + data-test-subj={`textInput-${fieldTestSelector}`} /> ); } @@ -204,6 +210,7 @@ export const PackagePolicyInputVarField: React.FunctionComponent<{ isInvalid, fieldLabel, options, + fieldTestSelector, ]); // Boolean cannot be optional by default set to false