diff --git a/x-pack/platform/plugins/shared/fleet/common/constants/fleet_server_policy_config.ts b/x-pack/platform/plugins/shared/fleet/common/constants/fleet_server_policy_config.ts index 11582a2631dfa..d27b3dda541fb 100644 --- a/x-pack/platform/plugins/shared/fleet/common/constants/fleet_server_policy_config.ts +++ b/x-pack/platform/plugins/shared/fleet/common/constants/fleet_server_policy_config.ts @@ -14,3 +14,12 @@ export const FLEET_PROXY_SAVED_OBJECT_TYPE = 'fleet-proxy'; export const PROXY_URL_REGEX = /^(http[s]?|socks5):\/\/[^\s$.?#].[^\s]*$/gm; export const SERVERLESS_DEFAULT_FLEET_SERVER_HOST_ID = 'default-fleet-server'; + +// Fleet Server IDs used for agentless policies: +// - For ECH, this is created by Fleet, see `createCloudFleetServerHostsIfNeeded` in: +// `x-pack/platform/plugins/shared/fleet/server/services/preconfiguration/fleet_server_host.ts` +// - For Serverless, this is the `default-fleet-server` host that is created from +// preconfiguration via project controller +// - Both are uneditable by users due to having `is_preconfigured: true` set +export const ECH_AGENTLESS_FLEET_SERVER_HOST_ID = 'internal-agentless-fleet-server'; +export const SERVERLESS_AGENTLESS_FLEET_SERVER_HOST_ID = SERVERLESS_DEFAULT_FLEET_SERVER_HOST_ID; diff --git a/x-pack/platform/plugins/shared/fleet/common/constants/output.ts b/x-pack/platform/plugins/shared/fleet/common/constants/output.ts index 47395be66d16b..9e789057f813b 100644 --- a/x-pack/platform/plugins/shared/fleet/common/constants/output.ts +++ b/x-pack/platform/plugins/shared/fleet/common/constants/output.ts @@ -28,6 +28,15 @@ export const DEFAULT_OUTPUT: NewOutput = { export const SERVERLESS_DEFAULT_OUTPUT_ID = 'es-default-output'; +// Output IDs used for agentless policies: +// - For ECH, this is an output created by Fleet, see `ensureDefaultOutputs()` in +// `x-pack/plugins/fleet/server/services/output.ts` +// - For Serverless, this is the `es-default-output` output that is created from +// preconfiguration via project controller +// - Both are uneditable by users due to having `is_preconfigured: true` set +export const ECH_AGENTLESS_OUTPUT_ID = 'es-agentless-output'; +export const SERVERLESS_AGENTLESS_OUTPUT_ID = SERVERLESS_DEFAULT_OUTPUT_ID; + export const LICENCE_FOR_PER_POLICY_OUTPUT = 'platinum'; export const LICENCE_FOR_OUTPUT_PER_INTEGRATION = 'enterprise'; diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts index 010bb05fc23d9..37a5cadfdf64d 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts @@ -21,10 +21,10 @@ import { SelectedPolicyTab } from '../../components'; import { AGENTLESS_AGENT_POLICY_INACTIVITY_TIMEOUT, AGENTLESS_AGENT_POLICY_MONITORING, - SERVERLESS_DEFAULT_OUTPUT_ID, - DEFAULT_OUTPUT_ID, - SERVERLESS_DEFAULT_FLEET_SERVER_HOST_ID, - DEFAULT_FLEET_SERVER_HOST_ID, + ECH_AGENTLESS_OUTPUT_ID, + SERVERLESS_AGENTLESS_OUTPUT_ID, + ECH_AGENTLESS_FLEET_SERVER_HOST_ID, + SERVERLESS_AGENTLESS_FLEET_SERVER_HOST_ID, } from '../../../../../../../../common/constants'; import { isAgentlessIntegration as isAgentlessIntegrationFn, @@ -153,9 +153,9 @@ export function useSetupTechnology({ useEffect(() => { const fetchOutputId = async () => { const outputId = isServerless - ? SERVERLESS_DEFAULT_OUTPUT_ID + ? SERVERLESS_AGENTLESS_OUTPUT_ID : isCloud - ? DEFAULT_OUTPUT_ID + ? ECH_AGENTLESS_OUTPUT_ID : undefined; if (outputId) { const outputData = await sendGetOneOutput(outputId); @@ -166,9 +166,9 @@ export function useSetupTechnology({ }; const fetchFleetServerHostId = async () => { const hostId = isServerless - ? SERVERLESS_DEFAULT_FLEET_SERVER_HOST_ID + ? SERVERLESS_AGENTLESS_FLEET_SERVER_HOST_ID : isCloud - ? DEFAULT_FLEET_SERVER_HOST_ID + ? ECH_AGENTLESS_FLEET_SERVER_HOST_ID : undefined; if (hostId) { @@ -214,7 +214,12 @@ export function useSetupTechnology({ inactivity_timeout: AGENTLESS_AGENT_POLICY_INACTIVITY_TIMEOUT, supports_agentless: true, monitoring_enabled: AGENTLESS_AGENT_POLICY_MONITORING, - ...(agentlessPolicyOutputId ? { data_output_id: agentlessPolicyOutputId } : {}), + ...(agentlessPolicyOutputId + ? { + data_output_id: agentlessPolicyOutputId, + monitoring_output_id: agentlessPolicyOutputId, + } + : {}), ...(agentlessPolicyFleetServerHostId ? { fleet_server_host_id: agentlessPolicyFleetServerHostId } : {}), diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx index 6fb2045a2094a..f293506a02f22 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx @@ -63,20 +63,20 @@ import { OutputFormLogstashSection } from './output_form_logstash'; import { OutputFormElasticsearchSection } from './output_form_elasticsearch'; export interface EditOutputFlyoutProps { - defaultOuput?: Output; + defaultOutput?: Output; output?: Output; onClose: () => void; proxies: FleetProxy[]; } export const EditOutputFlyout: React.FunctionComponent = ({ - defaultOuput, + defaultOutput, onClose, output, proxies, }) => { useBreadcrumbs('settings'); - const form = useOutputForm(onClose, output, defaultOuput); + const form = useOutputForm(onClose, output, defaultOutput); const inputs = form.inputs; const { docLinks, cloud } = useStartServices(); const fleetStatus = useFleetStatus(); diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/use_output_form.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/use_output_form.tsx index 57d72ac36dd4a..e92a39bdeff58 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/use_output_form.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/use_output_form.tsx @@ -193,7 +193,7 @@ export function extractDefaultDynamicKafkaTopics( ]; } -export function useOutputForm(onSucess: () => void, output?: Output, defaultOuput?: Output) { +export function useOutputForm(onSucess: () => void, output?: Output, defaultOutput?: Output) { const fleetStatus = useFleetStatus(); const authz = useAuthz(); @@ -254,7 +254,7 @@ export function useOutputForm(onSucess: () => void, output?: Output, defaultOupu const isServerless = cloud?.isServerlessEnabled; // Set the hosts to default for new ES output in serverless. const elasticsearchUrlDefaultValue = - isServerless && !output?.hosts ? defaultOuput?.hosts || [] : output?.hosts || []; + isServerless && !output?.hosts ? defaultOutput?.hosts || [] : output?.hosts || []; const elasticsearchUrlDisabled = isServerless || isDisabled('hosts'); const elasticsearchUrlInput = useComboInput( 'esHostsComboxBox', diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/settings/index.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/settings/index.tsx index 27e874349b932..de2c8992d0983 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/settings/index.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/settings/index.tsx @@ -84,6 +84,7 @@ export const SettingsApp = withConfirmModalProvider(() => { ]); const { cloud } = useStartServices(); + const isServerlessEnabled = cloud?.isServerlessEnabled; if ( (outputs.isLoading && outputs.isInitialRequest) || @@ -127,7 +128,7 @@ export const SettingsApp = withConfirmModalProvider(() => { - {cloud?.isServerlessEnabled ? ( + {isServerlessEnabled ? ( { o.id === SERVERLESS_DEFAULT_OUTPUT_ID)} + defaultOutput={ + isServerlessEnabled + ? outputs.data?.items.find((o) => o.id === SERVERLESS_DEFAULT_OUTPUT_ID) + : undefined + } /> @@ -182,9 +187,11 @@ export const SettingsApp = withConfirmModalProvider(() => { proxies={proxies.data?.items ?? []} onClose={onCloseCallback} output={output} - defaultOuput={outputs.data?.items.find( - (o) => o.id === SERVERLESS_DEFAULT_OUTPUT_ID - )} + defaultOutput={ + isServerlessEnabled + ? outputs.data?.items.find((o) => o.id === SERVERLESS_DEFAULT_OUTPUT_ID) + : undefined + } /> ); diff --git a/x-pack/platform/plugins/shared/fleet/server/constants/index.ts b/x-pack/platform/plugins/shared/fleet/server/constants/index.ts index f9e93bc785bcf..b103d94095118 100644 --- a/x-pack/platform/plugins/shared/fleet/server/constants/index.ts +++ b/x-pack/platform/plugins/shared/fleet/server/constants/index.ts @@ -63,6 +63,8 @@ export { DEFAULT_OUTPUT, DEFAULT_OUTPUT_ID, SERVERLESS_DEFAULT_OUTPUT_ID, + ECH_AGENTLESS_OUTPUT_ID, + SERVERLESS_AGENTLESS_OUTPUT_ID, PACKAGE_POLICY_DEFAULT_INDEX_PRIVILEGES, AGENT_POLICY_DEFAULT_MONITORING_DATASETS, // Fleet Server index @@ -84,6 +86,8 @@ export { DEFAULT_FLEET_SERVER_HOST_ID, FLEET_SERVER_HOST_SAVED_OBJECT_TYPE, SERVERLESS_DEFAULT_FLEET_SERVER_HOST_ID, + ECH_AGENTLESS_FLEET_SERVER_HOST_ID, + SERVERLESS_AGENTLESS_FLEET_SERVER_HOST_ID, FLEET_SERVER_PACKAGE, // Proxy FLEET_PROXY_SAVED_OBJECT_TYPE, diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agent_policy.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/agent_policy.test.ts index 8ca7af0a29edc..50ec43a692f92 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agent_policy.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agent_policy.test.ts @@ -442,7 +442,7 @@ describe('Agent policy', () => { updated_by: 'system', schema_version: '1.1.1', is_protected: false, - fleet_server_host_id: 'fleet-default-fleet-server-host', + fleet_server_host_id: 'internal-agentless-fleet-server', }); }); diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agentless_settings_ids.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/agentless_settings_ids.test.ts index e6b52a2843827..51d94e57326bd 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agentless_settings_ids.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agentless_settings_ids.test.ts @@ -70,6 +70,7 @@ describe('correct agentless policy settings', () => { 'agent_policy_1', { data_output_id: 'es-default-output', + monitoring_output_id: 'es-default-output', fleet_server_host_id: 'default-fleet-server', }, { @@ -82,6 +83,7 @@ describe('correct agentless policy settings', () => { 'agent_policy_2', { data_output_id: 'es-default-output', + monitoring_output_id: 'es-default-output', fleet_server_host_id: 'default-fleet-server', }, { diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agentless_settings_ids.ts b/x-pack/platform/plugins/shared/fleet/server/services/agentless_settings_ids.ts index e13005f18f11f..b57b425213bb8 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agentless_settings_ids.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agentless_settings_ids.ts @@ -35,7 +35,7 @@ export async function ensureCorrectAgentlessSettingsIds(esClient: ElasticsearchC const internalSoClientWithoutSpaceExtension = appContextService.getInternalUserSOClientWithoutSpaceExtension(); - const agentlessOutputIdsToFix = correctOutputId + const agentlessDataOutputIdsToFix = correctOutputId ? ( await internalSoClientWithoutSpaceExtension.find({ type: agentPolicySavedObjectType, @@ -48,6 +48,19 @@ export async function ensureCorrectAgentlessSettingsIds(esClient: ElasticsearchC )?.saved_objects.map((so) => so.id) : []; + const agentlessMonitoringOutputIdsToFix = correctOutputId + ? ( + await internalSoClientWithoutSpaceExtension.find({ + type: agentPolicySavedObjectType, + page: 1, + perPage: SO_SEARCH_LIMIT, + filter: `${agentPolicySavedObjectType}.attributes.supports_agentless:true AND NOT ${agentPolicySavedObjectType}.attributes.monitoring_output_id:${correctOutputId}`, + fields: [`id`], + namespaces: ['*'], + }) + )?.saved_objects.map((so) => so.id) + : []; + const agentlessFleetServerIdsToFix = correctFleetServerId ? ( await internalSoClientWithoutSpaceExtension.find({ @@ -63,7 +76,10 @@ export async function ensureCorrectAgentlessSettingsIds(esClient: ElasticsearchC try { // Check that the output ID exists - if (correctOutputId && agentlessOutputIdsToFix?.length > 0) { + if ( + correctOutputId && + (agentlessDataOutputIdsToFix?.length > 0 || agentlessMonitoringOutputIdsToFix?.length > 0) + ) { const output = await outputService.get( internalSoClientWithoutSpaceExtension, correctOutputId @@ -71,7 +87,7 @@ export async function ensureCorrectAgentlessSettingsIds(esClient: ElasticsearchC fixOutput = output != null; } } catch (e) { - // Silently swallow + // Silently swallow so that output will not be fixed if the correct output ID does not exist } try { @@ -84,12 +100,13 @@ export async function ensureCorrectAgentlessSettingsIds(esClient: ElasticsearchC fixFleetServer = fleetServerHost != null; } } catch (e) { - // Silently swallow + // Silently swallow so that fleet server host will not be fixed if the correct fleet server host ID does not exist } const allIdsToFix = Array.from( new Set([ - ...(fixOutput ? agentlessOutputIdsToFix : []), + ...(fixOutput ? agentlessDataOutputIdsToFix : []), + ...(fixOutput ? agentlessMonitoringOutputIdsToFix : []), ...(fixFleetServer ? agentlessFleetServerIdsToFix : []), ]) ); @@ -100,9 +117,7 @@ export async function ensureCorrectAgentlessSettingsIds(esClient: ElasticsearchC appContextService .getLogger() - .debug( - `Fixing output and/or fleet server host IDs on agent policies: ${agentlessOutputIdsToFix}` - ); + .debug(`Fixing output and/or fleet server host IDs on agent policies: ${allIdsToFix}`); await pMap( allIdsToFix, @@ -113,6 +128,7 @@ export async function ensureCorrectAgentlessSettingsIds(esClient: ElasticsearchC agentPolicyId, { data_output_id: correctOutputId, + monitoring_output_id: correctOutputId, fleet_server_host_id: correctFleetServerId, }, { diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agents/agentless_agent.ts b/x-pack/platform/plugins/shared/fleet/server/services/agents/agentless_agent.ts index 2dcc7f0a292e0..b8226f0fb32a2 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agents/agentless_agent.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agents/agentless_agent.ts @@ -33,9 +33,9 @@ import { AGENTLESS_GLOBAL_TAG_NAME_ORGANIZATION, AGENTLESS_GLOBAL_TAG_NAME_DIVISION, AGENTLESS_GLOBAL_TAG_NAME_TEAM, - DEFAULT_OUTPUT_ID, + ECH_AGENTLESS_OUTPUT_ID, + ECH_AGENTLESS_FLEET_SERVER_HOST_ID, SERVERLESS_DEFAULT_OUTPUT_ID, - DEFAULT_FLEET_SERVER_HOST_ID, SERVERLESS_DEFAULT_FLEET_SERVER_HOST_ID, } from '../../constants'; @@ -70,12 +70,12 @@ class AgentlessAgentService { const outputId = isServerless ? SERVERLESS_DEFAULT_OUTPUT_ID : isCloud - ? DEFAULT_OUTPUT_ID + ? ECH_AGENTLESS_OUTPUT_ID : undefined; const fleetServerId = isServerless ? SERVERLESS_DEFAULT_FLEET_SERVER_HOST_ID : isCloud - ? DEFAULT_FLEET_SERVER_HOST_ID + ? ECH_AGENTLESS_FLEET_SERVER_HOST_ID : undefined; return { diff --git a/x-pack/platform/plugins/shared/fleet/server/services/output.ts b/x-pack/platform/plugins/shared/fleet/server/services/output.ts index cb96d563544d9..e1b31cbc3971b 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/output.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/output.ts @@ -83,6 +83,7 @@ import { } from './secrets'; import { findAgentlessPolicies } from './outputs/helpers'; import { patchUpdateDataWithRequireEncryptedAADFields } from './outputs/so_helpers'; + import { canEnableSyncIntegrations, createOrUpdateFleetSyncedIntegrationsIndex, diff --git a/x-pack/platform/plugins/shared/fleet/server/services/preconfiguration/fleet_server_host.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/preconfiguration/fleet_server_host.test.ts index 899a393dc6536..d36a8e4bb69df 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/preconfiguration/fleet_server_host.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/preconfiguration/fleet_server_host.test.ts @@ -34,6 +34,11 @@ const mockedFleetServerHostService = fleetServerHostService as jest.Mocked< >; describe('getPreconfiguredFleetServerHostFromConfig', () => { + afterEach(() => { + mockedAppContextService.getCloud.mockReset(); + mockedAppContextService.getConfig.mockReset(); + }); + it('should work with preconfigured fleetServerHosts', () => { const config = { fleetServerHosts: [ @@ -51,6 +56,100 @@ describe('getPreconfiguredFleetServerHostFromConfig', () => { expect(res).toEqual(config.fleetServerHosts); }); + it('should include ECH agentless Fleet Server host when agentless is enabled in cloud', () => { + mockedAppContextService.getCloud.mockReturnValue({ + isCloudEnabled: true, + isServerlessEnabled: false, + deploymentId: 'test-deployment', + cloudHost: 'test.co', + } as any); + mockedAppContextService.getConfig.mockReturnValue({ + agentless: { enabled: true }, + } as any); + + const config = { + fleetServerHosts: [ + { + id: 'fleet-123', + name: 'TEST', + is_default: true, + host_urls: ['http://test.fr'], + }, + ], + }; + + const res = getPreconfiguredFleetServerHostFromConfig(config); + + expect(res).toHaveLength(2); + expect(res).toEqual([ + { + id: 'fleet-123', + name: 'TEST', + is_default: true, + host_urls: ['http://test.fr'], + }, + { + id: 'internal-agentless-fleet-server', + name: 'Internal Fleet Server for agentless', + host_urls: ['https://test-deployment.fleet.test.co'], + is_default: false, + is_preconfigured: true, + }, + ]); + }); + + it('should not include ECH agentless Fleet Server host when agentless is disabled', () => { + mockedAppContextService.getCloud.mockReturnValue({ + isCloudEnabled: true, + isServerlessEnabled: false, + } as any); + mockedAppContextService.getConfig.mockReturnValue({ + agentless: { enabled: false }, + } as any); + + const config = { + fleetServerHosts: [ + { + id: 'fleet-123', + name: 'TEST', + is_default: true, + host_urls: ['http://test.fr'], + }, + ], + }; + + const res = getPreconfiguredFleetServerHostFromConfig(config); + + expect(res).toHaveLength(1); + expect(res).toEqual(config.fleetServerHosts); + }); + + it('should not include ECH agentless Fleet Server host in serverless environment', () => { + mockedAppContextService.getCloud.mockReturnValue({ + isCloudEnabled: true, + isServerlessEnabled: true, + } as any); + mockedAppContextService.getConfig.mockReturnValue({ + agentless: { enabled: true }, + } as any); + + const config = { + fleetServerHosts: [ + { + id: 'fleet-123', + name: 'TEST', + is_default: true, + host_urls: ['http://test.fr'], + }, + ], + }; + + const res = getPreconfiguredFleetServerHostFromConfig(config); + + expect(res).toHaveLength(1); + expect(res).toEqual(config.fleetServerHosts); + }); + it('should work with preconfigured fleetServerHosts that have SSL options', () => { const config = { fleetServerHosts: [ @@ -226,10 +325,11 @@ describe('getCloudFleetServersHosts', () => { }); }); -describe('createCloudFleetServerHostIfNeeded', () => { +describe('createCloudFleetServerHostsIfNeeded', () => { afterEach(() => { mockedFleetServerHostService.create.mockReset(); mockedAppContextService.getCloud.mockReset(); + mockedAppContextService.getConfig.mockReset(); }); it('should do nothing if there is no cloud fleet server hosts', async () => { const soClient = savedObjectsClientMock.create(); @@ -240,7 +340,7 @@ describe('createCloudFleetServerHostIfNeeded', () => { expect(mockedFleetServerHostService.create).not.toBeCalled(); }); - it('should do nothing if there is already an host configured', async () => { + it('should create only default fleet server host if agentless already exists', async () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; @@ -249,6 +349,7 @@ describe('createCloudFleetServerHostIfNeeded', () => { 'dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRjZWM2ZjI2MWE3NGJmMjRjZTMzYmI4ODExYjg0Mjk0ZiRjNmMyY2E2ZDA0MjI0OWFmMGNjN2Q3YTllOTYyNTc0Mw==', isCloudEnabled: true, deploymentId: 'deployment-id-1', + cloudHost: 'us-east-1.aws.found.io', apm: {}, onboarding: {}, isServerlessEnabled: false, @@ -256,16 +357,36 @@ describe('createCloudFleetServerHostIfNeeded', () => { projectId: undefined, }, }); + mockedAppContextService.getConfig.mockReturnValue({ + agentless: { enabled: true }, + } as any); + // Default doesn't exist but agentless does + mockedFleetServerHostService.getDefaultFleetServerHost = jest + .fn() + .mockResolvedValue(null as any); mockedFleetServerHostService.get.mockResolvedValue({ - id: 'test', + id: 'existing-agentless', } as any); await createCloudFleetServerHostIfNeeded(soClient, esClient); - expect(mockedFleetServerHostService.create).not.toBeCalled(); + expect(mockedFleetServerHostService.create).toBeCalledTimes(1); + + // Verify only default Fleet Server host creation + expect(mockedFleetServerHostService.create).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.objectContaining({ + name: 'Default', + host_urls: ['https://deployment-id-1.fleet.us-east-1.aws.found.io'], + is_default: true, + is_preconfigured: false, + }), + { id: 'fleet-default-fleet-server-host', overwrite: true, fromPreconfiguration: true } + ); }); - it('should create a new fleet server hosts if there is no host configured', async () => { + it('should create only default fleet server host if agentless is disabled', async () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; @@ -282,6 +403,13 @@ describe('createCloudFleetServerHostIfNeeded', () => { projectId: undefined, }, }); + mockedAppContextService.getConfig.mockReturnValue({ + agentless: { enabled: false }, + } as any); + // Mock both getDefaultFleetServerHost and get calls to return null + mockedFleetServerHostService.getDefaultFleetServerHost = jest + .fn() + .mockResolvedValue(null as any); mockedFleetServerHostService.get.mockResolvedValue(null as any); soClient.create.mockResolvedValue({ id: 'test-id', @@ -291,16 +419,145 @@ describe('createCloudFleetServerHostIfNeeded', () => { await createCloudFleetServerHostIfNeeded(soClient, esClient); expect(mockedFleetServerHostService.create).toBeCalledTimes(1); - expect(mockedFleetServerHostService.create).toBeCalledWith( + + // Verify only default Fleet Server host creation + expect(mockedFleetServerHostService.create).toHaveBeenCalledWith( expect.anything(), expect.anything(), expect.objectContaining({ + name: 'Default', host_urls: ['https://deployment-id-1.fleet.us-east-1.aws.found.io'], is_default: true, + is_preconfigured: false, }), { id: 'fleet-default-fleet-server-host', overwrite: true, fromPreconfiguration: true } ); }); + + it('should not create agentless fleet server host if default already exists (ECH agentless is now handled via preconfiguration)', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + mockedAppContextService.getCloud.mockReturnValue({ + cloudId: + 'dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRjZWM2ZjI2MWE3NGJmMjRjZTMzYmI4ODExYjg0Mjk0ZiRjNmMyY2E2ZDA0MjI0OWFmMGNjN2Q3YTllOTYyNTc0Mw==', + isCloudEnabled: true, + deploymentId: 'deployment-id-1', + cloudHost: 'us-east-1.aws.found.io', + apm: {}, + onboarding: {}, + isServerlessEnabled: false, + serverless: { + projectId: undefined, + }, + }); + mockedAppContextService.getConfig.mockReturnValue({ + agentless: { enabled: true }, + } as any); + // Default exists but agentless doesn't + mockedFleetServerHostService.getDefaultFleetServerHost = jest + .fn() + .mockResolvedValue({ id: 'existing-default' } as any); + mockedFleetServerHostService.get.mockResolvedValue(null as any); + + await createCloudFleetServerHostIfNeeded(soClient, esClient); + + // Should not create any Fleet Server hosts since default exists and ECH agentless is now handled via preconfiguration + expect(mockedFleetServerHostService.create).toBeCalledTimes(0); + }); + + it('should not create any fleet server hosts if both already exist', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + mockedAppContextService.getCloud.mockReturnValue({ + cloudId: + 'dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRjZWM2ZjI2MWE3NGJmMjRjZTMzYmI4ODExYjg0Mjk0ZiRjNmMyY2E2ZDA0MjI0OWFmMGNjN2Q3YTllOTYyNTc0Mw==', + isCloudEnabled: true, + deploymentId: 'deployment-id-1', + cloudHost: 'us-east-1.aws.found.io', + apm: {}, + onboarding: {}, + isServerlessEnabled: false, + serverless: { + projectId: undefined, + }, + }); + mockedAppContextService.getConfig.mockReturnValue({ + agentless: { enabled: true }, + } as any); + // Both exist + mockedFleetServerHostService.getDefaultFleetServerHost = jest + .fn() + .mockResolvedValue({ id: 'existing-default' } as any); + mockedFleetServerHostService.get.mockResolvedValue({ id: 'existing-agentless' } as any); + + await createCloudFleetServerHostIfNeeded(soClient, esClient); + + expect(mockedFleetServerHostService.create).not.toBeCalled(); + }); + + it('should not create agentless fleet server host if default already exists but agentless is disabled', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + mockedAppContextService.getCloud.mockReturnValue({ + cloudId: + 'dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRjZWM2ZjI2MWE3NGJmMjRjZTMzYmI4ODExYjg0Mjk0ZiRjNmMyY2E2ZDA0MjI0OWFmMGNjN2Q3YTllOTYyNTc0Mw==', + isCloudEnabled: true, + deploymentId: 'deployment-id-1', + cloudHost: 'us-east-1.aws.found.io', + apm: {}, + onboarding: {}, + isServerlessEnabled: false, + serverless: { + projectId: undefined, + }, + }); + mockedAppContextService.getConfig.mockReturnValue({ + agentless: { enabled: false }, + } as any); + // Default exists but agentless doesn't and agentless is disabled + mockedFleetServerHostService.getDefaultFleetServerHost = jest + .fn() + .mockResolvedValue({ id: 'existing-default' } as any); + mockedFleetServerHostService.get.mockResolvedValue(null as any); + + await createCloudFleetServerHostIfNeeded(soClient, esClient); + + expect(mockedFleetServerHostService.create).not.toBeCalled(); + }); + + it('should not create agentless fleet server host in serverless environment', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + mockedAppContextService.getCloud.mockReturnValue({ + cloudId: + 'dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRjZWM2ZjI2MWE3NGJmMjRjZTMzYmI4ODExYjg0Mjk0ZiRjNmMyY2E2ZDA0MjI0OWFmMGNjN2Q3YTllOTYyNTc0Mw==', + isCloudEnabled: true, + deploymentId: 'deployment-id-1', + cloudHost: 'us-east-1.aws.found.io', + apm: {}, + onboarding: {}, + isServerlessEnabled: true, + serverless: { + projectId: 'project-123', + }, + }); + mockedAppContextService.getConfig.mockReturnValue({ + agentless: { enabled: true }, + } as any); + // Default exists but we're in serverless + mockedFleetServerHostService.getDefaultFleetServerHost = jest + .fn() + .mockResolvedValue({ id: 'existing-default' } as any); + mockedFleetServerHostService.get.mockResolvedValue(null as any); + + await createCloudFleetServerHostIfNeeded(soClient, esClient); + + expect(mockedFleetServerHostService.create).not.toBeCalled(); + }); }); describe('createOrUpdatePreconfiguredFleetServerHosts', () => { diff --git a/x-pack/platform/plugins/shared/fleet/server/services/preconfiguration/fleet_server_host.ts b/x-pack/platform/plugins/shared/fleet/server/services/preconfiguration/fleet_server_host.ts index 8245a0fce532c..2186775d9261e 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/preconfiguration/fleet_server_host.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/preconfiguration/fleet_server_host.ts @@ -9,13 +9,14 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/ import { normalizeHostsForAgents } from '../../../common/services'; import type { FleetConfigType } from '../../config'; -import { DEFAULT_FLEET_SERVER_HOST_ID } from '../../constants'; +import { DEFAULT_FLEET_SERVER_HOST_ID, ECH_AGENTLESS_FLEET_SERVER_HOST_ID } from '../../constants'; import { FleetError } from '../../errors'; import type { FleetServerHost } from '../../types'; import { appContextService } from '../app_context'; import { fleetServerHostService } from '../fleet_server_host'; +import { isAgentlessEnabled } from '../utils/agentless'; import { agentPolicyService } from '../agent_policy'; @@ -45,6 +46,7 @@ export function getPreconfiguredFleetServerHostFromConfig(config?: FleetConfigTy const { fleetServerHosts: fleetServerHostsFromConfig } = config; const legacyFleetServerHostsConfig = getConfigFleetServerHosts(config); + const cloudServerHosts = getCloudFleetServersHosts(); const fleetServerHosts: FleetServerHost[] = (fleetServerHostsFromConfig || []).concat([ ...(legacyFleetServerHostsConfig @@ -57,6 +59,18 @@ export function getPreconfiguredFleetServerHostFromConfig(config?: FleetConfigTy }, ] : []), + // Include agentless Fleet Server host in ECH + ...(isAgentlessEnabled() && cloudServerHosts + ? [ + { + id: ECH_AGENTLESS_FLEET_SERVER_HOST_ID, + name: 'Internal Fleet Server for agentless', + host_urls: cloudServerHosts, + is_default: false, + is_preconfigured: true, + }, + ] + : []), ]); if (fleetServerHosts.filter((fleetServerHost) => fleetServerHost.is_default).length > 1) { diff --git a/x-pack/platform/plugins/shared/fleet/server/services/preconfiguration/outputs.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/preconfiguration/outputs.test.ts index 7219ad12042d0..17610a05f3555 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/preconfiguration/outputs.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/preconfiguration/outputs.test.ts @@ -45,6 +45,8 @@ jest.mock('../app_context', () => ({ } ), getTaskManagerStart: jest.fn(), + getCloud: jest.fn().mockReturnValue(null), + getConfig: jest.fn().mockReturnValue({}), }, })); @@ -307,6 +309,122 @@ describe('Outputs preconfiguration', () => { `); }); + it('should include ECH agentless output when agentless is enabled in cloud environment', async () => { + // Mock the app context service for this test + const originalGetCloud = appContextService.getCloud; + const originalGetConfig = appContextService.getConfig; + + jest.mocked(appContextService.getCloud).mockReturnValue({ + isCloudEnabled: true, + isServerlessEnabled: false, + elasticsearchUrl: 'https://test-es.co:9200', + } as any); + + // Mock the isAgentlessEnabled function by mocking getConfig + jest.mocked(appContextService.getConfig).mockReturnValue({ + agentless: { enabled: true }, + agents: { + elasticsearch: { + hosts: ['http://localhost:9200'], + ca_sha256: 'test-ca-sha256', + }, + }, + } as any); + + const result = getPreconfiguredOutputFromConfig({ + agents: { + elasticsearch: { + hosts: ['http://localhost:9200'], + ca_sha256: 'test-ca-sha256', + }, + }, + }); + + expect(result).toHaveLength(2); + expect(result[0]).toMatchObject({ + id: 'fleet-default-output', + name: 'default', + type: 'elasticsearch', + is_default: true, + is_default_monitoring: true, + is_preconfigured: true, + }); + expect(result[1]).toMatchObject({ + id: 'es-agentless-output', + name: 'Internal output for agentless', + type: 'elasticsearch', + hosts: ['https://test-es.co:9200'], + ca_sha256: 'test-ca-sha256', + is_default: false, + is_default_monitoring: false, + is_preconfigured: true, + }); + + // Restore original mocks + jest.mocked(appContextService.getCloud).mockImplementation(originalGetCloud); + jest.mocked(appContextService.getConfig).mockImplementation(originalGetConfig); + }); + + it('should not include ECH agentless output when agentless is disabled', async () => { + const originalGetCloud = appContextService.getCloud; + const originalGetConfig = appContextService.getConfig; + + jest.mocked(appContextService.getCloud).mockReturnValue({ + isCloudEnabled: true, + isServerlessEnabled: false, + } as any); + + jest.mocked(appContextService.getConfig).mockReturnValue({ + agentless: { enabled: false }, + agents: { + elasticsearch: { hosts: ['http://localhost:9200'] }, + }, + } as any); + + const result = getPreconfiguredOutputFromConfig({ + agents: { + elasticsearch: { hosts: ['http://localhost:9200'] }, + }, + }); + + expect(result).toHaveLength(1); + expect(result[0].id).toBe('fleet-default-output'); + + // Restore original mocks + jest.mocked(appContextService.getCloud).mockImplementation(originalGetCloud); + jest.mocked(appContextService.getConfig).mockImplementation(originalGetConfig); + }); + + it('should not include ECH agentless output in serverless environment', async () => { + const originalGetCloud = appContextService.getCloud; + const originalGetConfig = appContextService.getConfig; + + jest.mocked(appContextService.getCloud).mockReturnValue({ + isCloudEnabled: true, + isServerlessEnabled: true, + } as any); + + jest.mocked(appContextService.getConfig).mockReturnValue({ + agentless: { enabled: true }, + agents: { + elasticsearch: { hosts: ['http://localhost:9200'] }, + }, + } as any); + + const result = getPreconfiguredOutputFromConfig({ + agents: { + elasticsearch: { hosts: ['http://localhost:9200'] }, + }, + }); + + expect(result).toHaveLength(1); + expect(result[0].id).toBe('fleet-default-output'); + + // Restore original mocks + jest.mocked(appContextService.getCloud).mockImplementation(originalGetCloud); + jest.mocked(appContextService.getConfig).mockImplementation(originalGetConfig); + }); + it('should create a preconfigured ES output that does not exist', async () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; diff --git a/x-pack/platform/plugins/shared/fleet/server/services/preconfiguration/outputs.ts b/x-pack/platform/plugins/shared/fleet/server/services/preconfiguration/outputs.ts index 31a7cb73e2697..16f9b2a1417ef 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/preconfiguration/outputs.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/preconfiguration/outputs.ts @@ -25,10 +25,11 @@ import type { } from '../../../common/types'; import { normalizeHostsForAgents } from '../../../common/services'; import type { FleetConfigType } from '../../config'; -import { DEFAULT_OUTPUT_ID, DEFAULT_OUTPUT } from '../../constants'; +import { DEFAULT_OUTPUT_ID, DEFAULT_OUTPUT, ECH_AGENTLESS_OUTPUT_ID } from '../../constants'; import { outputService } from '../output'; import { agentPolicyService } from '../agent_policy'; import { appContextService } from '../app_context'; +import { isAgentlessEnabled } from '../utils/agentless'; import { isDifferent } from './utils'; @@ -50,6 +51,23 @@ export function getPreconfiguredOutputFromConfig(config?: FleetConfigType) { } as PreconfiguredOutput, ] : []), + // Include agentless output in ECH + ...(isAgentlessEnabled() && !appContextService.getCloud()?.isServerlessEnabled + ? [ + { + id: ECH_AGENTLESS_OUTPUT_ID, + name: 'Internal output for agentless', + type: 'elasticsearch' as const, + hosts: appContextService.getCloud()?.elasticsearchUrl + ? [appContextService.getCloud()!.elasticsearchUrl] + : config?.agents.elasticsearch.hosts || ['http://localhost:9200'], + ca_sha256: config?.agents.elasticsearch.ca_sha256, + is_default: false, + is_default_monitoring: false, + is_preconfigured: true, + } as PreconfiguredOutput, + ] + : []), ]); return outputs; diff --git a/x-pack/platform/plugins/shared/fleet/server/services/setup.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/setup.test.ts index d22d04fa30d49..13e809f4484e9 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/setup.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/setup.test.ts @@ -26,6 +26,7 @@ import { isPackageInstalled } from './epm/packages/install'; import { upgradeAgentPolicySchemaVersion } from './setup/upgrade_agent_policy_schema_version'; import { createCCSIndexPatterns } from './setup/fleet_synced_integrations'; import { getSpaceAwareSaveobjectsClients } from './epm/kibana/assets/saved_objects'; +import { outputService } from './output'; jest.mock('./app_context'); jest.mock('./preconfiguration'); @@ -106,6 +107,9 @@ describe('setupFleet', () => { (upgradeAgentPolicySchemaVersion as jest.Mock).mockResolvedValue(undefined); (createCCSIndexPatterns as jest.Mock).mockResolvedValue(undefined); (getSpaceAwareSaveobjectsClients as jest.Mock).mockReturnValue({}); + (outputService.ensureDefaultOutput as jest.Mock).mockResolvedValue({ + defaultOutput: { id: 'test-default-output', name: 'test' }, + }); }); afterEach(async () => { @@ -145,6 +149,14 @@ describe('setupFleet', () => { }); }); + it('should call ensureDefaultOutputs during setup', async () => { + const soClient = getMockedSoClient(); + + await setupFleet(soClient, esClient); + + expect(outputService.ensureDefaultOutput).toHaveBeenCalledWith(soClient, esClient); + }); + it('should return non fatal errors when generateKeyPair result has errors', async () => { const soClient = getMockedSoClient(); diff --git a/x-pack/solutions/security/test/cloud_security_posture_functional/config.agentless.ts b/x-pack/solutions/security/test/cloud_security_posture_functional/config.agentless.ts index 674eee6f8ee85..613847bce01a1 100644 --- a/x-pack/solutions/security/test/cloud_security_posture_functional/config.agentless.ts +++ b/x-pack/solutions/security/test/cloud_security_posture_functional/config.agentless.ts @@ -30,7 +30,9 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { `--xpack.fleet.agentless.api.tls.certificate=${KBN_CERT_PATH}`, `--xpack.fleet.agentless.api.tls.key=${KBN_KEY_PATH}`, `--xpack.fleet.agentless.api.tls.ca=${CA_CERT_PATH}`, - `--xpack.cloud.id=something-anything`, + `--xpack.cloud.id="ftr_fake_cloud_id:aGVsbG8uY29tOjQ0MyRFUzEyM2FiYyRrYm4xMjNhYmM="`, + `--xpack.cloud.base_url="https://cloud.elastic.co"`, + `--xpack.cloud.deployment_url="/deployments/deploymentId"`, `--xpack.fleet.packages.0.name=cloud_security_posture`, `--xpack.fleet.packages.0.version=${CLOUD_SECURITY_POSTURE_PACKAGE_VERSION}`, ],