diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 738689fdaa2cdc..5c5a48642cba8e 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -178,6 +178,14 @@ export interface RegistryImage extends PackageSpecIcon { path: string; } +export interface DeploymentsModesEnablement { + enabled: boolean; +} +export interface DeploymentsModes { + agentless: DeploymentsModesEnablement; + default?: DeploymentsModesEnablement; +} + export enum RegistryPolicyTemplateKeys { categories = 'categories', data_streams = 'data_streams', @@ -193,6 +201,7 @@ export enum RegistryPolicyTemplateKeys { description = 'description', icons = 'icons', screenshots = 'screenshots', + deployment_modes = 'deployment_modes', } interface BaseTemplate { [RegistryPolicyTemplateKeys.name]: string; @@ -201,6 +210,7 @@ interface BaseTemplate { [RegistryPolicyTemplateKeys.icons]?: RegistryImage[]; [RegistryPolicyTemplateKeys.screenshots]?: RegistryImage[]; [RegistryPolicyTemplateKeys.multiple]?: boolean; + [RegistryPolicyTemplateKeys.deployment_modes]?: DeploymentsModes; } export interface RegistryPolicyIntegrationTemplate extends BaseTemplate { [RegistryPolicyTemplateKeys.categories]?: Array; @@ -424,6 +434,7 @@ export enum RegistryVarsEntryKeys { default = 'default', os = 'os', secret = 'secret', + hide_in_deployment_modes = 'hide_in_deployment_modes', } // EPR types this as `[]map[string]interface{}` @@ -445,6 +456,7 @@ export interface RegistryVarsEntry { default: string | string[]; }; }; + [RegistryVarsEntryKeys.hide_in_deployment_modes]?: string[]; } // Deprecated as part of the removing public references to saved object schemas diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.test.tsx index 9988ac1f7c22c4..a6bf030ad7fe04 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.test.tsx @@ -4,8 +4,29 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import React from 'react'; +import { waitFor } from '@testing-library/react'; -import { shouldShowStreamsByDefault } from './package_policy_input_panel'; +import { createFleetTestRendererMock } from '../../../../../../../../mock'; + +import type { TestRenderer } from '../../../../../../../../mock'; +import { useAgentless } from '../../../single_page_layout/hooks/setup_technology'; + +import type { + PackageInfo, + RegistryStreamWithDataStream, + RegistryInput, + NewPackagePolicyInput, +} from '../../../../../../types'; + +import { shouldShowStreamsByDefault, PackagePolicyInputPanel } from './package_policy_input_panel'; + +jest.mock('../../../single_page_layout/hooks/setup_technology', () => { + return { + useAgentless: jest.fn(), + }; +}); +const useAgentlessMock = useAgentless as jest.MockedFunction; describe('shouldShowStreamsByDefault', () => { it('should return true if a datastreamId is provided and contained in the input', () => { @@ -59,3 +80,447 @@ describe('shouldShowStreamsByDefault', () => { expect(res).toBeFalsy(); }); }); + +describe('PackagePolicyInputPanel', () => { + const mockPackageInfo = { + name: 'agentless_test_package', + version: '1.0.1-rc1', + description: 'Test package with new agentless deployment modes', + title: 'Agentless test package', + format_version: '3.2.0', + owner: { + github: 'elastic/integrations', + type: 'elastic', + }, + type: 'integration', + categories: ['custom'], + screenshots: [], + icons: [], + policy_templates: [ + { + name: 'sample', + title: 'Agentless sample logs', + description: 'Collect sample logs', + multiple: true, + inputs: [ + { + title: 'Collect sample logs from instances', + vars: [], + type: 'logfile', + description: 'Collecting sample logs', + }, + ], + deployment_modes: { + default: { + enabled: false, + }, + agentless: { + enabled: true, + }, + }, + }, + ], + data_streams: [ + { + title: 'Default data stream', + release: 'ga', + type: 'logs', + package: 'agentless_test_package', + dataset: 'agentless_test_package.default_data_stream', + path: 'default_data_stream', + elasticsearch: { + 'ingest_pipeline.name': 'default', + }, + ingest_pipeline: 'default', + streams: [ + { + input: 'logfile', + title: 'Sample logs hidden in agentless', + template_path: 'stream.yml.hbs', + vars: [ + { + name: 'paths', + type: 'text', + title: 'Paths', + multi: false, + default: ['/var/log/*.log'], + required: false, + show_user: true, + hide_in_deployment_modes: ['agentless'], + }, + ], + description: 'Collect sample logs - hidden in agentless', + }, + ], + }, + { + title: 'Logs Stream on Agentless', + release: 'ga', + type: 'logs', + package: 'agentless_test_package', + dataset: 'agentless_test_package.logs_stream', + path: 'logs_stream', + elasticsearch: { + 'ingest_pipeline.name': 'default', + }, + ingest_pipeline: 'default', + streams: [ + { + input: 'logfile', + title: 'Sample logs on Agentless', + template_path: 'stream.yml.hbs', + vars: [ + { + name: 'paths', + type: 'text', + title: 'Paths', + multi: true, + default: ['/var/log/*.log'], + required: false, + show_user: true, + hide_in_deployment_modes: ['default'], + }, + ], + description: 'Collect sample logs on Agentless only', + }, + ], + }, + ], + readme: '/package/agentless_test_package/1.0.1-rc1/docs/README.md', + release: 'beta', + latestVersion: '1.0.1-rc1', + assets: { kibana: {} }, + licensePath: '/package/agentless_test_package/1.0.1-rc1/LICENSE.txt', + keepPoliciesUpToDate: false, + status: 'installed', + installationInfo: {}, + } as unknown as PackageInfo; + + const mockPackageInput: RegistryInput = { + title: 'Collect sample logs from instances', + vars: [], + type: 'logfile', + description: 'Collecting sample logs', + }; + + const mockPackageInputStreams: RegistryStreamWithDataStream[] = [ + { + input: 'logfile', + title: 'Sample logs hidden in agentless', + template_path: 'stream.yml.hbs', + vars: [ + { + name: 'paths', + type: 'text', + title: 'Paths', + multi: false, + default: ['/var/log/*.log'], + required: false, + show_user: true, + hide_in_deployment_modes: ['agentless'], + }, + ], + description: 'Collect sample logs - hidden in agentless', + data_stream: { + title: 'Default data stream', + release: 'ga', + type: 'logs', + package: 'agentless_test_package', + dataset: 'agentless_test_package.default_data_stream', + path: 'default_data_stream', + elasticsearch: { + 'ingest_pipeline.name': 'default', + }, + ingest_pipeline: 'default', + streams: [ + { + input: 'logfile', + title: 'Sample logs hidden in agentless', + template_path: 'stream.yml.hbs', + vars: [ + { + name: 'paths', + type: 'text', + title: 'Paths', + multi: false, + default: ['/var/log/*.log'], + required: false, + show_user: true, + hide_in_deployment_modes: ['agentless'], + }, + ], + description: 'Collect sample logs - hidden in agentless', + }, + ], + }, + }, + { + input: 'logfile', + title: 'Sample logs on Agentless', + template_path: 'stream.yml.hbs', + vars: [ + { + name: 'paths', + type: 'text', + title: 'Paths', + multi: true, + default: ['/var/log/*.log'], + required: false, + show_user: true, + hide_in_deployment_modes: ['default'], + }, + ], + description: 'Collect sample logs on Agentless only', + data_stream: { + title: 'Logs Stream on Agentless', + release: 'ga', + type: 'logs', + package: 'agentless_test_package', + dataset: 'agentless_test_package.logs_stream', + path: 'logs_stream', + elasticsearch: { + 'ingest_pipeline.name': 'default', + }, + ingest_pipeline: 'default', + streams: [ + { + input: 'logfile', + title: 'Sample logs on Agentless', + template_path: 'stream.yml.hbs', + vars: [ + { + name: 'paths', + type: 'text', + title: 'Paths', + multi: true, + default: ['/var/log/*.log'], + required: false, + show_user: true, + hide_in_deployment_modes: ['default'], + }, + ], + description: 'Collect sample logs on Agentless only', + }, + ], + }, + }, + ]; + const mockUpdatePackagePolicyInput = jest.fn().mockImplementation((val: any) => { + return undefined; + }); + const inputValidationResults = { + streams: { + 'agentless_test_package.default_data_stream': { vars: { paths: null } }, + 'agentless_test_package.logs_stream': { vars: { paths: null } }, + }, + }; + + let testRenderer: TestRenderer; + let renderResult: ReturnType; + const packagePolicyInput = { + id: 'input-1', + type: 'logfile', + policy_template: 'sample', + enabled: true, + streams: [ + { + data_stream: { type: 'logs', dataset: 'agentless_test_package.default_data_stream' }, + enabled: true, + vars: { paths: [] }, + }, + { + data_stream: { type: 'logs', dataset: 'agentless_test_package.logs_stream' }, + enabled: true, + vars: { paths: [] }, + }, + ], + } as NewPackagePolicyInput; + const render = ( + packageInfo: PackageInfo, + packageInputStreams: RegistryStreamWithDataStream[] + ) => { + renderResult = testRenderer.render( + + ); + }; + beforeEach(() => { + testRenderer = createFleetTestRendererMock(); + }); + afterEach(() => { + jest.resetAllMocks(); + }); + describe('When agentless is enabled', () => { + beforeEach(() => { + useAgentlessMock.mockReturnValue({ + isAgentlessEnabled: true, + isAgentlessPackagePolicy: jest.fn(), + isAgentlessAgentPolicy: jest.fn(), + isAgentlessIntegration: jest.fn(), + }); + }); + + it('should render inputs specific to env', async () => { + render(mockPackageInfo, mockPackageInputStreams); + await waitFor(async () => { + expect( + await renderResult.findByTestId('PackagePolicy.InputStreamConfig.Switch') + ).toBeInTheDocument(); + }); + await waitFor(async () => { + expect( + await renderResult.findByText('Collect sample logs from instances') + ).toBeInTheDocument(); + }); + await waitFor(async () => { + expect(await renderResult.findByText('Sample logs on Agentless')).toBeInTheDocument(); + }); + await waitFor(async () => { + expect( + await renderResult.queryByText('Sample logs hidden in agentless') + ).not.toBeInTheDocument(); + }); + }); + }); + + describe('When agentless not enabled', () => { + beforeEach(() => { + useAgentlessMock.mockReturnValue({ + isAgentlessEnabled: false, + isAgentlessPackagePolicy: jest.fn(), + isAgentlessAgentPolicy: jest.fn(), + isAgentlessIntegration: jest.fn(), + }); + }); + + it('should render inputs specific to the env', async () => { + render(mockPackageInfo, mockPackageInputStreams); + await waitFor(async () => { + expect( + await renderResult.findByTestId('PackagePolicy.InputStreamConfig.Switch') + ).toBeInTheDocument(); + }); + await waitFor(async () => { + expect( + await renderResult.findByText('Collect sample logs from instances') + ).toBeInTheDocument(); + }); + await waitFor(async () => { + expect(await renderResult.queryByText('Sample logs on Agentless')).not.toBeInTheDocument(); + }); + await waitFor(async () => { + expect( + await renderResult.queryByText('Sample logs hidden in agentless') + ).toBeInTheDocument(); + }); + }); + + it('should render inputs when hide_in_deployment_modes is not present', async () => { + const packageInputStreams: RegistryStreamWithDataStream[] = [ + { + input: 'logfile', + title: 'Sample logs', + template_path: 'stream.yml.hbs', + vars: [ + { + name: 'paths', + type: 'text', + title: 'Paths', + multi: false, + default: ['/var/log/*.log'], + required: false, + show_user: true, + }, + ], + description: 'Collect sample logs', + data_stream: { + title: 'Default data stream', + release: 'ga', + type: 'logs', + package: 'agentless_test_package', + dataset: 'agentless_test_package.default_data_stream', + path: 'default_data_stream', + elasticsearch: { + 'ingest_pipeline.name': 'default', + }, + ingest_pipeline: 'default', + streams: [ + { + input: 'logfile', + title: 'Sample logs', + template_path: 'stream.yml.hbs', + vars: [ + { + name: 'paths', + type: 'text', + title: 'Paths', + multi: false, + default: ['/var/log/*.log'], + required: false, + show_user: true, + }, + ], + description: 'Collect sample log - hidden in agentless', + }, + ], + }, + }, + ]; + const packageInfo = { + ...mockPackageInfo, + data_streams: [ + { + title: 'Default data stream', + release: 'ga', + type: 'logs', + package: 'agentless_test_package', + dataset: 'agentless_test_package.default_data_stream', + path: 'default_data_stream', + elasticsearch: { + 'ingest_pipeline.name': 'default', + }, + ingest_pipeline: 'default', + streams: [ + { + input: 'logfile', + title: 'Sample logs', + template_path: 'stream.yml.hbs', + vars: [ + { + name: 'paths', + type: 'text', + title: 'Paths', + multi: false, + default: ['/var/log/*.log'], + required: false, + show_user: true, + }, + ], + description: 'Collect sample logs', + }, + ], + }, + ], + } as unknown as PackageInfo; + render(packageInfo, packageInputStreams); + + await waitFor(async () => { + expect( + await renderResult.findByText('Collect sample logs from instances') + ).toBeInTheDocument(); + }); + await waitFor(async () => { + expect(await renderResult.findByText('Sample logs')).toBeInTheDocument(); + }); + await waitFor(async () => { + expect(await renderResult.findByText('Collect sample logs')).toBeInTheDocument(); + }); + }); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.tsx index f3ef27474cb0f8..0661c2f36a9b37 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState, Fragment, memo, useMemo } from 'react'; +import React, { useState, Fragment, memo, useMemo, useCallback } from 'react'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n-react'; import { @@ -21,16 +21,17 @@ import { } from '@elastic/eui'; import type { - NewPackagePolicy, NewPackagePolicyInput, PackageInfo, PackagePolicyInputStream, RegistryInput, RegistryStream, RegistryStreamWithDataStream, + RegistryVarsEntry, } from '../../../../../../types'; import type { PackagePolicyInputValidationResults } from '../../../services'; import { hasInvalidButRequiredVar, countValidationErrors } from '../../../services'; +import { useAgentless } from '../../../single_page_layout/hooks/setup_technology'; import { PackagePolicyInputConfig } from './package_policy_input_config'; import { PackagePolicyInputStreamConfig } from './package_policy_input_stream'; @@ -74,10 +75,8 @@ export const shouldShowStreamsByDefault = ( export const PackagePolicyInputPanel: React.FunctionComponent<{ packageInput: RegistryInput; packageInfo: PackageInfo; - packagePolicy: NewPackagePolicy; packageInputStreams: RegistryStreamWithDataStream[]; packagePolicyInput: NewPackagePolicyInput; - updatePackagePolicy: (updatedPackagePolicy: Partial) => void; updatePackagePolicyInput: (updatedInput: Partial) => void; inputValidationResults: PackagePolicyInputValidationResults; forceShowErrors?: boolean; @@ -88,14 +87,14 @@ export const PackagePolicyInputPanel: React.FunctionComponent<{ packageInfo, packageInputStreams, packagePolicyInput, - packagePolicy, - updatePackagePolicy, updatePackagePolicyInput, inputValidationResults, forceShowErrors, isEditPage = false, }) => { const defaultDataStreamId = useDataStreamId(); + const { isAgentlessEnabled } = useAgentless(); + // Showing streams toggle state const [isShowingStreams, setIsShowingStreams] = useState(() => shouldShowStreamsByDefault( @@ -106,6 +105,33 @@ export const PackagePolicyInputPanel: React.FunctionComponent<{ ) ); + // Hide registry variables based on `hide_in_deployment_modes` value + const hideRegistryVars = useCallback( + (registryVar: RegistryVarsEntry) => { + if (!registryVar.hide_in_deployment_modes) return false; + return ( + (isAgentlessEnabled && + !!registryVar.hide_in_deployment_modes?.find((mode) => mode === 'agentless')) || + (!isAgentlessEnabled && + !!registryVar.hide_in_deployment_modes?.find((mode) => mode === 'default')) + ); + }, + [isAgentlessEnabled] + ); + + const packageInputStreamShouldBeVisible = useCallback( + (packageInputStream: RegistryStreamWithDataStream) => { + return ( + !!packageInputStream.vars && + packageInputStream.vars.length > 0 && + !!packageInputStream.vars.find( + (registryVar: RegistryVarsEntry) => !hideRegistryVars(registryVar) + ) + ); + }, + [hideRegistryVars] + ); + // Errors state const errorCount = inputValidationResults && countValidationErrors(inputValidationResults); const hasErrors = forceShowErrors && errorCount; @@ -114,9 +140,11 @@ export const PackagePolicyInputPanel: React.FunctionComponent<{ () => packageInputStreams.length > 0, [packageInputStreams.length] ); + const inputStreams = useMemo( () => packageInputStreams + .filter((packageInputStream) => packageInputStreamShouldBeVisible(packageInputStream)) .map((packageInputStream) => { return { packageInputStream, @@ -126,7 +154,7 @@ export const PackagePolicyInputPanel: React.FunctionComponent<{ }; }) .filter((stream) => Boolean(stream.packagePolicyInputStream)), - [packageInputStreams, packagePolicyInput.streams] + [packageInputStreamShouldBeVisible, packageInputStreams, packagePolicyInput.streams] ); const titleElementId = useMemo(() => htmlIdGenerator()(), []); @@ -137,11 +165,17 @@ export const PackagePolicyInputPanel: React.FunctionComponent<{ -

{packageInput.title || packageInput.type}

+

+ {packageInput.title || packageInput.type} +

@@ -203,6 +237,7 @@ export const PackagePolicyInputPanel: React.FunctionComponent<{ {isShowingStreams && packageInput.vars && packageInput.vars.length ? ( + {inputStreams.map(({ packageInputStream, packagePolicyInputStream }, index) => ( ) => { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_stream.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_stream.tsx index 6b1bfaaaa09619..8bb916f1d7c02c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_stream.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_stream.tsx @@ -33,7 +33,6 @@ import { } from '../../../../../../../../../common/services'; import type { - NewPackagePolicy, NewPackagePolicyInputStream, PackageInfo, RegistryStreamWithDataStream, @@ -57,11 +56,9 @@ const ScrollAnchor = styled.div` `; interface Props { - packagePolicy: NewPackagePolicy; packageInputStream: RegistryStreamWithDataStream; packageInfo: PackageInfo; packagePolicyInputStream: NewPackagePolicyInputStream; - updatePackagePolicy: (updatedPackagePolicy: Partial) => void; updatePackagePolicyInputStream: (updatedStream: Partial) => void; inputStreamValidationResults: PackagePolicyConfigValidationResults; forceShowErrors?: boolean; @@ -70,11 +67,9 @@ interface Props { export const PackagePolicyInputStreamConfig = memo( ({ - packagePolicy, packageInputStream, packageInfo, packagePolicyInputStream, - updatePackagePolicy, updatePackagePolicyInputStream, inputStreamValidationResults, forceShowErrors, @@ -136,9 +131,8 @@ export const PackagePolicyInputStreamConfig = memo( } }); } - return [_requiredVars, _advancedVars]; - }, [packageInputStream.vars]); + }, [packageInputStream]); const advancedVarsWithErrorsCount: number = useMemo( () => @@ -171,7 +165,7 @@ export const PackagePolicyInputStreamConfig = memo( return ( <> - + @@ -185,6 +179,7 @@ export const PackagePolicyInputStreamConfig = memo( {packageInfo.type !== 'input' && ( ( const varConfigEntry = packagePolicyInputStream.vars?.[varName]; const value = varConfigEntry?.value; const frozen = varConfigEntry?.frozen ?? false; - return ( ) => { const indexOfUpdatedInput = packagePolicy.inputs.findIndex( (input) => diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/layout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/layout.tsx index 535b129278f4f3..21d08d71eb5f7f 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/layout.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/layout.tsx @@ -19,7 +19,7 @@ import { EuiSpacer, } from '@elastic/eui'; -import { useAgentlessPolicy } from '../hooks/setup_technology'; +import { useAgentless } from '../hooks/setup_technology'; import { WithHeaderLayout } from '../../../../../layouts'; import type { AgentPolicy, PackageInfo, RegistryPolicyTemplate } from '../../../../../types'; @@ -233,8 +233,8 @@ export const CreatePackagePolicySinglePageLayout: React.FunctionComponent<{ ); - const { isAgentlessPolicyId } = useAgentlessPolicy(); - const hasAgentBasedPolicyId = !isAgentlessPolicyId(agentPolicy?.id); + const { isAgentlessAgentPolicy } = useAgentless(); + const hasAgentBasedPolicyId = !isAgentlessAgentPolicy(agentPolicy); const showAgentPolicyName = agentPolicy && (isAdd || isEdit) && hasAgentBasedPolicyId; const rightColumn = showAgentPolicyName ? ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx index f7b46d95f1e678..0ce292cd5cba6e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx @@ -48,7 +48,7 @@ import { getCloudShellUrlFromPackagePolicy, } from '../../../../../../../components/cloud_security_posture/services'; -import { useAgentlessPolicy } from './setup_technology'; +import { useAgentless } from './setup_technology'; async function createAgentPolicy({ packagePolicy, @@ -134,7 +134,8 @@ export function useOnSubmit({ const [hasAgentPolicyError, setHasAgentPolicyError] = useState(false); const hasErrors = validationResults ? validationHasErrors(validationResults) : false; - const { isAgentlessPolicyId } = useAgentlessPolicy(); + const { isAgentlessIntegration, isAgentlessAgentPolicy, isAgentlessPackagePolicy } = + useAgentless(); // Update agent policy method const updateAgentPolicy = useCallback( @@ -261,11 +262,7 @@ export function useOnSubmit({ setFormState('INVALID'); return; } - if ( - agentCount !== 0 && - !isAgentlessPolicyId(packagePolicy?.policy_id) && - formState !== 'CONFIRM' - ) { + if (agentCount !== 0 && !isAgentlessIntegration(packageInfo) && formState !== 'CONFIRM') { setFormState('CONFIRM'); return; } @@ -320,7 +317,12 @@ export function useOnSubmit({ } const agentPolicyIdToSave = createdPolicy?.id ?? packagePolicy.policy_id; - const shouldForceInstallOnAgentless = isAgentlessPolicyId(agentPolicyIdToSave); + + const shouldForceInstallOnAgentless = + isAgentlessAgentPolicy(createdPolicy) || + isAgentlessIntegration(packageInfo) || + isAgentlessPackagePolicy(packagePolicy); + const forceInstall = force || shouldForceInstallOnAgentless; setFormState('LOADING'); @@ -424,18 +426,20 @@ export function useOnSubmit({ formState, hasErrors, agentCount, - packagePolicy, + isAgentlessIntegration, + packageInfo, selectedPolicyTab, - isAgentlessPolicyId, + packagePolicy, + isAgentlessAgentPolicy, + isAgentlessPackagePolicy, + hasFleetAddAgentsPrivileges, withSysMonitoring, newAgentPolicy, updatePackagePolicy, - packageInfo, notifications.toasts, agentPolicy, onSaveNavigate, confirmForceInstall, - hasFleetAddAgentsPrivileges, ] ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts index 2eab867e2ae121..8642b84d7e1e49 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts @@ -8,24 +8,56 @@ import { useCallback, useEffect, useState } from 'react'; import { ExperimentalFeaturesService } from '../../../../../services'; -import type { AgentPolicy, NewAgentPolicy } from '../../../../../types'; +import type { + AgentPolicy, + NewAgentPolicy, + NewPackagePolicy, + PackageInfo, +} from '../../../../../types'; import { SetupTechnology } from '../../../../../types'; import { sendGetOneAgentPolicy, useStartServices } from '../../../../../hooks'; import { SelectedPolicyTab } from '../../components'; import { AGENTLESS_POLICY_ID } from '../../../../../../../../common/constants'; -export const useAgentlessPolicy = () => { +export const useAgentless = () => { const { agentless: agentlessExperimentalFeatureEnabled } = ExperimentalFeaturesService.get(); const { cloud } = useStartServices(); const isServerless = !!cloud?.isServerlessEnabled; const isAgentlessEnabled = agentlessExperimentalFeatureEnabled && isServerless; - const isAgentlessPolicyId = (id: string | undefined) => - isAgentlessEnabled && id === AGENTLESS_POLICY_ID; + const isAgentlessAgentPolicy = (agentPolicy: AgentPolicy | undefined) => { + if (!agentPolicy) return false; + return ( + isAgentlessEnabled && + (agentPolicy?.id === AGENTLESS_POLICY_ID || !!agentPolicy?.supports_agentless) + ); + }; + + // When an integration has at least a policy template enabled for agentless + const isAgentlessIntegration = (packageInfo: PackageInfo | undefined) => { + if ( + isAgentlessEnabled && + packageInfo?.policy_templates && + packageInfo?.policy_templates.length > 0 && + !!packageInfo?.policy_templates.find( + (policyTemplate) => policyTemplate?.deployment_modes?.agentless.enabled === true + ) + ) { + return true; + } + return false; + }; + + // TODO: remove this check when CSPM implements the above flag and rely only on `isAgentlessIntegration` + const isAgentlessPackagePolicy = (packagePolicy: NewPackagePolicy) => { + return isAgentlessEnabled && packagePolicy.policy_id === AGENTLESS_POLICY_ID; + }; return { isAgentlessEnabled, - isAgentlessPolicyId, + isAgentlessAgentPolicy, + isAgentlessIntegration, + isAgentlessPackagePolicy, }; }; @@ -34,18 +66,26 @@ export function useSetupTechnology({ newAgentPolicy, updateAgentPolicy, setSelectedPolicyTab, + packageInfo, }: { updateNewAgentPolicy: (policy: NewAgentPolicy) => void; newAgentPolicy: NewAgentPolicy; updateAgentPolicy: (policy: AgentPolicy | undefined) => void; setSelectedPolicyTab: (tab: SelectedPolicyTab) => void; + packageInfo?: PackageInfo; }) { - const { isAgentlessEnabled } = useAgentlessPolicy(); + const { isAgentlessEnabled, isAgentlessIntegration } = useAgentless(); const [selectedSetupTechnology, setSelectedSetupTechnology] = useState( SetupTechnology.AGENT_BASED ); const [agentlessPolicy, setAgentlessPolicy] = useState(); + useEffect(() => { + if (isAgentlessEnabled && packageInfo && isAgentlessIntegration(packageInfo)) { + setSelectedSetupTechnology(SetupTechnology.AGENTLESS); + } + }, [isAgentlessEnabled, isAgentlessIntegration, packageInfo]); + useEffect(() => { const fetchAgentlessPolicy = async () => { const { data, error } = await sendGetOneAgentPolicy(AGENTLESS_POLICY_ID); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx index c7518cff99f088..88d52d55549594 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx @@ -128,7 +128,7 @@ afterAll(() => { consoleDebugMock.mockRestore(); }); -describe('when on the package policy create page', () => { +describe('When on the package policy create page', () => { const createPageUrlPath = pagePathGetters.add_integration_to_policy({ pkgkey: 'nginx-1.3.0' })[1]; let testRenderer: TestRenderer; @@ -229,7 +229,7 @@ describe('when on the package policy create page', () => { (useGetPackageInfoByKeyQuery as jest.Mock).mockReturnValue(getMockPackageInfo()); }); - describe('and Route state is provided via Fleet HashRouter', () => { + describe('And Route state is provided via Fleet HashRouter', () => { let expectedRouteState: CreatePackagePolicyRouteState; beforeEach(() => { @@ -271,7 +271,7 @@ describe('when on the package policy create page', () => { }); }); - describe('submit page', () => { + describe('Submit page', () => { const newPackagePolicy = { description: '', enabled: true, @@ -453,7 +453,7 @@ describe('when on the package policy create page', () => { }); }); - describe('on save navigate', () => { + describe('On save navigate', () => { async function setupSaveNavigate(routeState: any, queryParamsPolicyId?: string) { (useIntraAppState as jest.MockedFunction).mockReturnValue(routeState); render(queryParamsPolicyId); @@ -563,7 +563,7 @@ describe('when on the package policy create page', () => { ); }); - describe('without query param', () => { + describe('Without query param', () => { beforeEach(async () => { await act(async () => { render(); @@ -736,7 +736,7 @@ describe('when on the package policy create page', () => { }); }); - describe('with agentless policy available', () => { + describe('With agentless policy available', () => { beforeEach(async () => { (sendGetOneAgentPolicy as jest.MockedFunction).mockResolvedValue({ data: { item: { id: AGENTLESS_POLICY_ID, name: 'Agentless CSPM', namespace: 'default' } }, @@ -756,6 +756,13 @@ describe('when on the package policy create page', () => { }); test('should not force create package policy when not in serverless', async () => { + (useStartServices as jest.MockedFunction).mockReturnValue({ + ...useStartServices(), + cloud: { + ...useStartServices().cloud, + isServerlessEnabled: false, + }, + }); await act(async () => { fireEvent.click(renderResult.getByText('Existing hosts')!); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx index 3d2446bc729ddd..1781e79c999bc3 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx @@ -329,6 +329,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ updateNewAgentPolicy, updateAgentPolicy, setSelectedPolicyTab, + packageInfo, }); const replaceStepConfigurePackagePolicy = diff --git a/x-pack/plugins/fleet/server/services/agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policy.test.ts index 1f6ae15606c462..9811445af6ee3e 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.test.ts @@ -278,7 +278,7 @@ describe('Agent policy', () => { ); }); - it('should not throw error if support_agentless is set if agentless feature flag is set in serverless', async () => { + it('should create a policy with is_managed true if agentless feature flag is set and in serverless env', async () => { jest .spyOn(appContextService, 'getExperimentalFeatures') .mockReturnValue({ agentless: true } as any); @@ -289,13 +289,31 @@ describe('Agent policy', () => { const soClient = getAgentPolicyCreateMock(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - await expect( - agentPolicyService.create(soClient, esClient, { - name: 'test', - namespace: 'default', - supports_agentless: true, - }) - ).resolves.not.toThrow(); + soClient.find.mockResolvedValueOnce({ + total: 0, + saved_objects: [], + per_page: 0, + page: 1, + }); + + const res = await agentPolicyService.create(soClient, esClient, { + name: 'test', + namespace: 'default', + supports_agentless: true, + }); + expect(res).toEqual({ + id: 'mocked', + name: 'test', + namespace: 'default', + supports_agentless: true, + status: 'active', + is_managed: true, + revision: 1, + updated_at: expect.anything(), + updated_by: 'system', + schema_version: '1.1.1', + is_protected: false, + }); }); }); diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 95de0d05e86b95..635948a5ca4e31 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -332,7 +332,7 @@ class AgentPolicyService { { ...agentPolicy, status: 'active', - is_managed: agentPolicy.is_managed ?? false, + is_managed: (agentPolicy.is_managed || agentPolicy?.supports_agentless) ?? false, revision: 1, updated_at: new Date().toISOString(), updated_by: options?.user?.username || 'system',