From 5750993db61c611994b09cce2cd9f3cc68a5a7d4 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Tue, 24 Jun 2025 16:06:46 -0700 Subject: [PATCH 01/11] Add new constants to explicitly define agentless hosts for ECH and Serverless, use them in handling of agentless policies --- .../constants/fleet_server_policy_config.ts | 7 +++++++ .../shared/fleet/common/constants/output.ts | 7 +++++++ .../hooks/setup_technology.ts | 16 +++++++-------- .../shared/fleet/server/constants/index.ts | 4 ++++ .../server/services/agentless_settings_ids.ts | 20 +++++++++---------- 5 files changed, 36 insertions(+), 18 deletions(-) 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..90545d990f35a 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,10 @@ 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'; + +// Agentless policies on Cloud need to use managed Fleet Server host: +// - For ECH, this is an `is_internal: true` host with the ID `internal-fleet-server` +// - For Serverless, this is the `default-fleet-server` host that is created from +// preconfiguration via project controller (and thus not editable by the user) +export const ECH_AGENTLESS_FLEET_SERVER_HOST_ID = 'internal-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..66b22b422ec83 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,13 @@ export const DEFAULT_OUTPUT: NewOutput = { export const SERVERLESS_DEFAULT_OUTPUT_ID = 'es-default-output'; +// Agentless policies on Cloud need to use managed output: +// - For ECH, this is an `is_internal: true` output with the ID `es-containerhost` +// - For Serverless, this is the `es-default-output` output that is created from +// preconfiguration via project controller (and thus not editable by the user) +export const ECH_AGENTLESS_OUTPUT_ID = 'es-containerhost'; +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 a5e90d3861546..7e5b7806aefd3 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 @@ -24,10 +24,10 @@ import { AGENTLESS_GLOBAL_TAG_NAME_TEAM, 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, @@ -147,9 +147,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); @@ -160,9 +160,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) { 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 ecb889a1fcae0..07c9cf0ac4cfe 100644 --- a/x-pack/platform/plugins/shared/fleet/server/constants/index.ts +++ b/x-pack/platform/plugins/shared/fleet/server/constants/index.ts @@ -62,6 +62,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 @@ -83,6 +85,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/agentless_settings_ids.ts b/x-pack/platform/plugins/shared/fleet/server/services/agentless_settings_ids.ts index bce8af0801248..c2266c3e57fbd 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 @@ -12,10 +12,10 @@ import pMap from 'p-map'; import { MAX_CONCURRENT_AGENT_POLICIES_OPERATIONS, SO_SEARCH_LIMIT, - DEFAULT_OUTPUT_ID, - SERVERLESS_DEFAULT_OUTPUT_ID, - DEFAULT_FLEET_SERVER_HOST_ID, - SERVERLESS_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 '../constants'; import type { AgentPolicySOAttributes } from '../types'; @@ -31,14 +31,14 @@ export async function ensureCorrectAgentlessSettingsIds(esClient: ElasticsearchC const isCloud = cloudSetup?.isCloudEnabled; const isServerless = cloudSetup?.isServerlessEnabled; const correctOutputId = isServerless - ? SERVERLESS_DEFAULT_OUTPUT_ID + ? SERVERLESS_AGENTLESS_OUTPUT_ID : isCloud - ? DEFAULT_OUTPUT_ID + ? ECH_AGENTLESS_OUTPUT_ID : undefined; const correctFleetServerId = 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; let fixOutput = false; let fixFleetServer = false; @@ -87,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 { @@ -100,7 +100,7 @@ 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( From 66f6326b452846116f6dde65f6e4ce1caf28cb4b Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Tue, 24 Jun 2025 16:46:44 -0700 Subject: [PATCH 02/11] Fix typos and add conditions for clarity --- .../components/edit_output_flyout/index.tsx | 6 +++--- .../edit_output_flyout/use_output_form.tsx | 4 ++-- .../fleet/sections/settings/index.tsx | 17 ++++++++++++----- 3 files changed, 17 insertions(+), 10 deletions(-) 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 243ca2e6bfd99..cccef396669fc 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 @@ -62,20 +62,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 + } /> ); From 9db592b9c3a54097010032f0d03ad091b20ea43e Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Wed, 25 Jun 2025 14:34:54 -0700 Subject: [PATCH 03/11] Make agentless monitoring output ID fixed --- .../hooks/setup_technology.ts | 7 ++++- .../services/agentless_settings_ids.test.ts | 2 ++ .../server/services/agentless_settings_ids.ts | 26 ++++++++++++++++--- 3 files changed, 30 insertions(+), 5 deletions(-) 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 7e5b7806aefd3..f33602e51a80c 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 @@ -208,7 +208,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/server/services/agentless_settings_ids.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/agentless_settings_ids.test.ts index bdc1e05103bf0..f741596617c97 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 @@ -61,6 +61,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', }, { @@ -73,6 +74,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 c2266c3e57fbd..b5d33cad0ee1c 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 @@ -51,7 +51,7 @@ export async function ensureCorrectAgentlessSettingsIds(esClient: ElasticsearchC const internalSoClientWithoutSpaceExtension = appContextService.getInternalUserSOClientWithoutSpaceExtension(); - const agentlessOutputIdsToFix = correctOutputId + const agentlessDataOutputIdsToFix = correctOutputId ? ( await internalSoClientWithoutSpaceExtension.find({ type: agentPolicySavedObjectType, @@ -64,6 +64,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({ @@ -79,7 +92,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 @@ -105,7 +121,8 @@ export async function ensureCorrectAgentlessSettingsIds(esClient: ElasticsearchC const allIdsToFix = Array.from( new Set([ - ...(fixOutput ? agentlessOutputIdsToFix : []), + ...(fixOutput ? agentlessDataOutputIdsToFix : []), + ...(fixOutput ? agentlessMonitoringOutputIdsToFix : []), ...(fixFleetServer ? agentlessFleetServerIdsToFix : []), ]) ); @@ -117,7 +134,7 @@ export async function ensureCorrectAgentlessSettingsIds(esClient: ElasticsearchC appContextService .getLogger() .debug( - `Fixing output and/or fleet server host IDs on agent policies: ${agentlessOutputIdsToFix}` + `Fixing output and/or fleet server host IDs on agent policies: ${agentlessDataOutputIdsToFix}` ); await pMap( @@ -129,6 +146,7 @@ export async function ensureCorrectAgentlessSettingsIds(esClient: ElasticsearchC agentPolicyId, { data_output_id: correctOutputId, + monitoring_output_id: correctOutputId, fleet_server_host_id: correctFleetServerId, }, { From 3491ad920f0b9d002b27c50dfdd0301c95cbf9fe Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Thu, 9 Oct 2025 11:05:52 -0700 Subject: [PATCH 04/11] Create new output and fleet server host programatically --- .../constants/fleet_server_policy_config.ts | 12 ++--- .../shared/fleet/common/constants/output.ts | 12 ++--- .../server/services/agentless_settings_ids.ts | 4 +- .../shared/fleet/server/services/output.ts | 44 ++++++++++++++++--- .../fleet_server_host.test.ts | 8 ++-- .../preconfiguration/fleet_server_host.ts | 38 ++++++++++++---- .../shared/fleet/server/services/setup.ts | 2 +- 7 files changed, 86 insertions(+), 34 deletions(-) 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 90545d990f35a..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 @@ -15,9 +15,11 @@ export const PROXY_URL_REGEX = /^(http[s]?|socks5):\/\/[^\s$.?#].[^\s]*$/gm; export const SERVERLESS_DEFAULT_FLEET_SERVER_HOST_ID = 'default-fleet-server'; -// Agentless policies on Cloud need to use managed Fleet Server host: -// - For ECH, this is an `is_internal: true` host with the ID `internal-fleet-server` -// - For Serverless, this is the `default-fleet-server` host that is created from -// preconfiguration via project controller (and thus not editable by the user) -export const ECH_AGENTLESS_FLEET_SERVER_HOST_ID = 'internal-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 66b22b422ec83..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,11 +28,13 @@ export const DEFAULT_OUTPUT: NewOutput = { export const SERVERLESS_DEFAULT_OUTPUT_ID = 'es-default-output'; -// Agentless policies on Cloud need to use managed output: -// - For ECH, this is an `is_internal: true` output with the ID `es-containerhost` -// - For Serverless, this is the `es-default-output` output that is created from -// preconfiguration via project controller (and thus not editable by the user) -export const ECH_AGENTLESS_OUTPUT_ID = 'es-containerhost'; +// 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'; 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 5323a9a21136d..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 @@ -117,9 +117,7 @@ export async function ensureCorrectAgentlessSettingsIds(esClient: ElasticsearchC appContextService .getLogger() - .debug( - `Fixing output and/or fleet server host IDs on agent policies: ${agentlessDataOutputIdsToFix}` - ); + .debug(`Fixing output and/or fleet server host IDs on agent policies: ${allIdsToFix}`); await pMap( allIdsToFix, 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 f1e71fc5c051e..48e07541a0aa0 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/output.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/output.ts @@ -46,6 +46,7 @@ import { OUTPUT_SAVED_OBJECT_TYPE, OUTPUT_HEALTH_DATA_STREAM, MAX_CONCURRENT_BACKFILL_OUTPUTS_PRESETS, + ECH_AGENTLESS_OUTPUT_ID, } from '../constants'; import { SO_SEARCH_LIMIT, @@ -496,30 +497,59 @@ class OutputService { } } - public async ensureDefaultOutput( + public async ensureDefaultOutputs( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient ) { + const logger = appContextService.getLogger(); + const cloudSetup = appContextService.getCloud(); + const isCloud = cloudSetup?.isCloudEnabled; + const isServerless = cloudSetup?.isServerlessEnabled; const outputs = await this.list(soClient); - const defaultOutput = outputs.items.find((o) => o.is_default); - const defaultMonitoringOutput = outputs.items.find((o) => o.is_default_monitoring); + // Ensure general default output + const currentDefaultOutput = outputs.items.find((o) => o.is_default); + const currentDefaultMonitoringOutput = outputs.items.find((o) => o.is_default_monitoring); + let defaultOutput: Output | undefined = currentDefaultOutput; - if (!defaultOutput) { + if (!currentDefaultOutput) { const newDefaultOutput = { ...DEFAULT_OUTPUT, hosts: this.getDefaultESHosts(), ca_sha256: appContextService.getConfig()!.agents.elasticsearch.ca_sha256, - is_default_monitoring: !defaultMonitoringOutput, + is_default_monitoring: !currentDefaultMonitoringOutput, } as NewOutput; - return await this.create(soClient, esClient, newDefaultOutput, { + defaultOutput = await this.create(soClient, esClient, newDefaultOutput, { id: DEFAULT_OUTPUT_ID, overwrite: true, }); } - return defaultOutput; + // Ensure default output exists for ECH agentless + if (isCloud && !isServerless) { + const defaultAgentlessOutput = outputs.items.find((o) => o.id === ECH_AGENTLESS_OUTPUT_ID); + if (!defaultAgentlessOutput) { + logger.debug('Creating default output for ECH agentless'); + const newDefaultAgentlessOutput = { + name: 'Internal output for agentless', + type: outputType.Elasticsearch, + hosts: this.getDefaultESHosts(), + ca_sha256: appContextService.getConfig()!.agents.elasticsearch.ca_sha256, + is_default: false, + is_default_monitoring: false, + is_preconfigured: true, // Fake preconfiguration status to prevent user modification + } as NewOutput; + + await this.create(soClient, esClient, newDefaultAgentlessOutput, { + id: ECH_AGENTLESS_OUTPUT_ID, + overwrite: true, + fromPreconfiguration: true, + }); + } + } + + return { defaultOutput }; } public getDefaultESHosts(): string[] { 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 2046943c56928..bcf67b6a06fe7 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 @@ -13,7 +13,7 @@ import { fleetServerHostService } from '../fleet_server_host'; import type { FleetServerHost } from '../../../common/types'; import { - createCloudFleetServerHostIfNeeded, + createCloudFleetServerHostsIfNeeded, getCloudFleetServersHosts, getPreconfiguredFleetServerHostFromConfig, createOrUpdatePreconfiguredFleetServerHosts, @@ -235,7 +235,7 @@ describe('createCloudFleetServerHostIfNeeded', () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - await createCloudFleetServerHostIfNeeded(soClient, esClient); + await createCloudFleetServerHostsIfNeeded(soClient, esClient); expect(mockedFleetServerHostService.create).not.toBeCalled(); }); @@ -260,7 +260,7 @@ describe('createCloudFleetServerHostIfNeeded', () => { id: 'test', } as any); - await createCloudFleetServerHostIfNeeded(soClient, esClient); + await createCloudFleetServerHostsIfNeeded(soClient, esClient); expect(mockedFleetServerHostService.create).not.toBeCalled(); }); @@ -288,7 +288,7 @@ describe('createCloudFleetServerHostIfNeeded', () => { attributes: {}, } as any); - await createCloudFleetServerHostIfNeeded(soClient, esClient); + await createCloudFleetServerHostsIfNeeded(soClient, esClient); expect(mockedFleetServerHostService.create).toBeCalledTimes(1); expect(mockedFleetServerHostService.create).toBeCalledWith( 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 66c8040880031..eff85cc62936b 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,7 +9,7 @@ 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'; @@ -24,12 +24,10 @@ import { hashSecret, isSecretDifferent } from './outputs'; export function getCloudFleetServersHosts() { const cloudSetup = appContextService.getCloud(); - if ( - cloudSetup && - !cloudSetup.isServerlessEnabled && - cloudSetup.isCloudEnabled && - cloudSetup.cloudHost - ) { + const isCloud = cloudSetup?.isCloudEnabled; + const isServerless = cloudSetup?.isServerlessEnabled; + + if (isCloud && !isServerless && cloudSetup.cloudHost) { // Fleet Server url are formed like this `https://.fleet. return [ `https://${cloudSetup.deploymentId}.fleet.${cloudSetup.cloudHost}${ @@ -76,7 +74,7 @@ export async function ensurePreconfiguredFleetServerHosts( esClient, preconfiguredFleetServerHosts ); - await createCloudFleetServerHostIfNeeded(soClient, esClient); + await createCloudFleetServerHostsIfNeeded(soClient, esClient); await cleanPreconfiguredFleetServerHosts(soClient, esClient, preconfiguredFleetServerHosts); } @@ -142,7 +140,7 @@ export async function createOrUpdatePreconfiguredFleetServerHosts( ); } -export async function createCloudFleetServerHostIfNeeded( +export async function createCloudFleetServerHostsIfNeeded( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient ) { @@ -151,6 +149,7 @@ export async function createCloudFleetServerHostIfNeeded( return; } + // Ensure default Fleet Server host exists for ECH const defaultFleetServerHost = await fleetServerHostService.getDefaultFleetServerHost(soClient); if (!defaultFleetServerHost) { await fleetServerHostService.create( @@ -165,6 +164,27 @@ export async function createCloudFleetServerHostIfNeeded( { id: DEFAULT_FLEET_SERVER_HOST_ID, overwrite: true, fromPreconfiguration: true } ); } + + // Ensure internal Fleet Server host exists for ECH agentless. + // This "duplicate" ensure that agentless agents use an unmodifiable + // Fleet Server host that is hidden from the user. + const agentlessFleetServerHost = await fleetServerHostService.get( + soClient, + ECH_AGENTLESS_FLEET_SERVER_HOST_ID + ); + if (!agentlessFleetServerHost) { + await fleetServerHostService.create( + soClient, + esClient, + { + name: 'Internal Fleet Server for agentless', + host_urls: cloudServerHosts, + is_default: false, + is_preconfigured: true, // Fake preconfiguration status to prevent user modification + }, + { id: ECH_AGENTLESS_FLEET_SERVER_HOST_ID, overwrite: true, fromPreconfiguration: true } + ); + } } export async function cleanPreconfiguredFleetServerHosts( diff --git a/x-pack/platform/plugins/shared/fleet/server/services/setup.ts b/x-pack/platform/plugins/shared/fleet/server/services/setup.ts index 33a8e95cde1b8..33a6420ba6c8c 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/setup.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/setup.ts @@ -172,7 +172,7 @@ async function createSetupSideEffects( getPreconfiguredOutputFromConfig(appContextService.getConfig()) ); - const defaultOutput = await outputService.ensureDefaultOutput(soClient, esClient); + const { defaultOutput } = await outputService.ensureDefaultOutputs(soClient, esClient); logger.debug('Backfilling output performance presets'); await outputService.backfillAllOutputPresets(soClient, esClient); From ce6a37bf247dc4b2997dc15518dec47c0c9e6961 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Thu, 9 Oct 2025 11:46:46 -0700 Subject: [PATCH 05/11] Add unit tests --- .../fleet/server/services/output.test.ts | 234 +++++++++++++++++- .../fleet_server_host.test.ts | 124 +++++++++- .../fleet/server/services/setup.test.ts | 12 + 3 files changed, 362 insertions(+), 8 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/server/services/output.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/output.test.ts index 47be510bfef21..b7e86a7b04c54 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/output.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/output.test.ts @@ -102,7 +102,9 @@ function getMockedSoClient( return mockOutputSO('output-test'); } case outputIdToUuid('existing-default-output'): { - return mockOutputSO('existing-default-output'); + return mockOutputSO('existing-default-output', { + is_default: true, + }); } case outputIdToUuid('existing-default-monitoring-output'): { return mockOutputSO('existing-default-monitoring-output', { @@ -242,6 +244,40 @@ function getMockedSoClient( }; } + // Handle general list() calls for ensureDefaultOutputs - only return existing outputs if we have both perPage and sortField + // This is to distinguish ensureDefaultOutputs calls from other find calls + if ( + findOptions.perPage === 10000 && + findOptions.sortField === 'is_default' && + (options?.defaultOutputId || options?.defaultOutputMonitoringId) + ) { + const savedObjects = []; + if (options?.defaultOutputId) { + savedObjects.push({ + score: 0, + ...(await soClient.get('ingest-outputs', outputIdToUuid(options.defaultOutputId))), + }); + } + if ( + options?.defaultOutputMonitoringId && + options.defaultOutputMonitoringId !== options.defaultOutputId + ) { + savedObjects.push({ + score: 0, + ...(await soClient.get( + 'ingest-outputs', + outputIdToUuid(options.defaultOutputMonitoringId) + )), + }); + } + return { + page: 1, + per_page: 10000, + saved_objects: savedObjects, + total: savedObjects.length, + }; + } + return { page: 1, per_page: 10, @@ -2706,6 +2742,202 @@ describe('Output Service', () => { }); }); + describe('ensureDefaultOutputs', () => { + beforeEach(() => { + mockedAppContextService.getEncryptedSavedObjectsSetup.mockReturnValue({ + canEncrypt: true, + } as any); + mockedAppContextService.getConfig.mockReturnValue({ + agents: { + elasticsearch: { + ca_sha256: 'test-ca-sha256', + }, + }, + } as any); + }); + + it('should create default output when none exists', async () => { + const soClient = getMockedSoClient(); + + // @ts-expect-error + mockedAppContextService.getCloud.mockReturnValue({ + isCloudEnabled: false, + isServerlessEnabled: false, + }); + + const result = await outputService.ensureDefaultOutputs(soClient, esClientMock); + + expect(result).toHaveProperty('defaultOutput'); + expect(soClient.create).toHaveBeenCalledWith( + 'ingest-outputs', + expect.objectContaining({ + name: 'default', + type: 'elasticsearch', + is_default: true, + is_default_monitoring: true, + }), + expect.objectContaining({ + id: outputIdToUuid('fleet-default-output'), + overwrite: true, + }) + ); + }); + + it('should return existing default output when one already exists', async () => { + const soClient = getMockedSoClient({ + defaultOutputId: 'existing-default-output', + }); + + // @ts-expect-error + mockedAppContextService.getCloud.mockReturnValue({ + isCloudEnabled: false, + isServerlessEnabled: false, + }); + + const result = await outputService.ensureDefaultOutputs(soClient, esClientMock); + + expect(result).toHaveProperty('defaultOutput'); + // Should not create a new default output + expect(soClient.create).not.toHaveBeenCalledWith( + 'ingest-outputs', + expect.objectContaining({ + name: 'default', + is_default: true, + }), + expect.anything() + ); + }); + + it('should create ECH agentless output in cloud environment', async () => { + const soClient = getMockedSoClient({ + defaultOutputId: 'existing-default-output', + }); + + // @ts-expect-error + mockedAppContextService.getCloud.mockReturnValue({ + isCloudEnabled: true, + isServerlessEnabled: false, + }); + + await outputService.ensureDefaultOutputs(soClient, esClientMock); + + expect(soClient.create).toHaveBeenCalledWith( + 'ingest-outputs', + expect.objectContaining({ + name: 'Internal output for agentless', + type: 'elasticsearch', + is_default: false, + is_default_monitoring: false, + is_preconfigured: true, + }), + expect.objectContaining({ + id: outputIdToUuid('es-agentless-output'), + overwrite: true, + }) + ); + }); + + it('should not create ECH agentless output in serverless environment', async () => { + const soClient = getMockedSoClient({ + defaultOutputId: 'existing-default-output', + }); + + // @ts-expect-error + mockedAppContextService.getCloud.mockReturnValue({ + isCloudEnabled: true, + isServerlessEnabled: true, + }); + + await outputService.ensureDefaultOutputs(soClient, esClientMock); + + // Should not create the agentless output + expect(soClient.create).not.toHaveBeenCalledWith( + 'ingest-outputs', + expect.objectContaining({ + name: 'Internal output for agentless', + }), + expect.anything() + ); + }); + + it('should not create ECH agentless output in non-cloud environment', async () => { + const soClient = getMockedSoClient({ + defaultOutputId: 'existing-default-output', + }); + + // @ts-expect-error + mockedAppContextService.getCloud.mockReturnValue({ + isCloudEnabled: false, + isServerlessEnabled: false, + }); + + await outputService.ensureDefaultOutputs(soClient, esClientMock); + + // Should not create the agentless output + expect(soClient.create).not.toHaveBeenCalledWith( + 'ingest-outputs', + expect.objectContaining({ + name: 'Internal output for agentless', + }), + expect.anything() + ); + }); + + it('should not create ECH agentless output when it already exists', async () => { + const soClient = getMockedSoClient({ + defaultOutputId: 'existing-default-output', + }); + + // Mock the list method to return existing agentless output + soClient.find.mockResolvedValueOnce({ + page: 1, + per_page: 10, + saved_objects: [ + mockOutputSO('existing-default-output', { is_default: true }), + mockOutputSO('es-agentless-output', { + name: 'Internal output for agentless', + is_preconfigured: true, + is_default: false, + }), + ], + total: 2, + } as any); + + // @ts-expect-error + mockedAppContextService.getCloud.mockReturnValue({ + isCloudEnabled: true, + isServerlessEnabled: false, + }); + + await outputService.ensureDefaultOutputs(soClient, esClientMock); + + // Should not create the agentless output since it already exists + expect(soClient.create).not.toHaveBeenCalledWith( + 'ingest-outputs', + expect.objectContaining({ + name: 'Internal output for agentless', + }), + expect.anything() + ); + }); + + it('should log debug message when creating ECH agentless output', async () => { + const soClient = getMockedSoClient({ + defaultOutputId: 'existing-default-output', + }); + + // @ts-expect-error + mockedAppContextService.getCloud.mockReturnValue({ + isCloudEnabled: true, + isServerlessEnabled: false, + }); + + await outputService.ensureDefaultOutputs(soClient, esClientMock); + + expect(mockedLogger.debug).toHaveBeenCalledWith('Creating default output for ECH agentless'); + }); + }); + describe('outputSavedObjectToOutput', () => { it('should return output object with parsed SSL when SSL is a valid JSON string', () => { const so = mockOutputSO('output-test', { 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 bcf67b6a06fe7..945fad8dccee4 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 @@ -226,7 +226,7 @@ describe('getCloudFleetServersHosts', () => { }); }); -describe('createCloudFleetServerHostIfNeeded', () => { +describe('createCloudFleetServerHostsIfNeeded', () => { afterEach(() => { mockedFleetServerHostService.create.mockReset(); mockedAppContextService.getCloud.mockReset(); @@ -240,7 +240,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 +249,7 @@ describe('createCloudFleetServerHostIfNeeded', () => { 'dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRjZWM2ZjI2MWE3NGJmMjRjZTMzYmI4ODExYjg0Mjk0ZiRjNmMyY2E2ZDA0MjI0OWFmMGNjN2Q3YTllOTYyNTc0Mw==', isCloudEnabled: true, deploymentId: 'deployment-id-1', + cloudHost: 'us-east-1.aws.found.io', apm: {}, onboarding: {}, isServerlessEnabled: false, @@ -256,16 +257,33 @@ describe('createCloudFleetServerHostIfNeeded', () => { projectId: undefined, }, }); + // 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 createCloudFleetServerHostsIfNeeded(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 both default and agentless fleet server hosts if none are configured', async () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; @@ -282,6 +300,10 @@ describe('createCloudFleetServerHostIfNeeded', () => { projectId: undefined, }, }); + // 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', @@ -290,16 +312,104 @@ describe('createCloudFleetServerHostIfNeeded', () => { await createCloudFleetServerHostsIfNeeded(soClient, esClient); - expect(mockedFleetServerHostService.create).toBeCalledTimes(1); - expect(mockedFleetServerHostService.create).toBeCalledWith( + expect(mockedFleetServerHostService.create).toBeCalledTimes(2); + + // Verify default Fleet Server host creation + expect(mockedFleetServerHostService.create).toHaveBeenNthCalledWith( + 1, 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 } ); + + // Verify agentless Fleet Server host creation + expect(mockedFleetServerHostService.create).toHaveBeenNthCalledWith( + 2, + expect.anything(), + expect.anything(), + expect.objectContaining({ + name: 'Internal Fleet Server for agentless', + host_urls: ['https://deployment-id-1.fleet.us-east-1.aws.found.io'], + is_default: false, + is_preconfigured: true, + }), + { id: 'internal-agentless-fleet-server', overwrite: true, fromPreconfiguration: true } + ); + }); + + it('should create only agentless fleet server host if default already exists', 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, + }, + }); + // Default exists but agentless doesn't + mockedFleetServerHostService.getDefaultFleetServerHost = jest + .fn() + .mockResolvedValue({ id: 'existing-default' } as any); + mockedFleetServerHostService.get.mockResolvedValue(null as any); + + await createCloudFleetServerHostsIfNeeded(soClient, esClient); + + expect(mockedFleetServerHostService.create).toBeCalledTimes(1); + + // Verify only agentless Fleet Server host creation + expect(mockedFleetServerHostService.create).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.objectContaining({ + name: 'Internal Fleet Server for agentless', + host_urls: ['https://deployment-id-1.fleet.us-east-1.aws.found.io'], + is_default: false, + is_preconfigured: true, + }), + { id: 'internal-agentless-fleet-server', overwrite: true, fromPreconfiguration: true } + ); + }); + + 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, + }, + }); + // Both exist + mockedFleetServerHostService.getDefaultFleetServerHost = jest + .fn() + .mockResolvedValue({ id: 'existing-default' } as any); + mockedFleetServerHostService.get.mockResolvedValue({ id: 'existing-agentless' } as any); + + await createCloudFleetServerHostsIfNeeded(soClient, esClient); + + expect(mockedFleetServerHostService.create).not.toBeCalled(); }); }); 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 6f2ffc5805884..ecc61ab57aa51 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 @@ -23,6 +23,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'); @@ -102,6 +103,9 @@ describe('setupFleet', () => { (upgradeAgentPolicySchemaVersion as jest.Mock).mockResolvedValue(undefined); (createCCSIndexPatterns as jest.Mock).mockResolvedValue(undefined); (getSpaceAwareSaveobjectsClients as jest.Mock).mockReturnValue({}); + (outputService.ensureDefaultOutputs as jest.Mock).mockResolvedValue({ + defaultOutput: { id: 'test-default-output', name: 'test' }, + }); }); afterEach(async () => { @@ -141,6 +145,14 @@ describe('setupFleet', () => { }); }); + it('should call ensureDefaultOutputs during setup', async () => { + const soClient = getMockedSoClient(); + + await setupFleet(soClient, esClient); + + expect(outputService.ensureDefaultOutputs).toHaveBeenCalledWith(soClient, esClient); + }); + it('should return non fatal errors when generateKeyPair result has errors', async () => { const soClient = getMockedSoClient(); From 774ce717db51bccc3fda5ca1f09008dfc291cf81 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Thu, 9 Oct 2025 12:55:50 -0700 Subject: [PATCH 06/11] Add check against agentless enabled also --- .../fleet/server/services/output.test.ts | 90 +++++++++---- .../shared/fleet/server/services/output.ts | 4 +- .../fleet_server_host.test.ts | 127 +++++++++++++++++- .../preconfiguration/fleet_server_host.ts | 33 +++-- 4 files changed, 211 insertions(+), 43 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/server/services/output.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/output.test.ts index b7e86a7b04c54..74a171d6ba758 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/output.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/output.test.ts @@ -2808,7 +2808,7 @@ describe('Output Service', () => { ); }); - it('should create ECH agentless output in cloud environment', async () => { + it('should create ECH agentless output when agentless is enabled', async () => { const soClient = getMockedSoClient({ defaultOutputId: 'existing-default-output', }); @@ -2818,6 +2818,14 @@ describe('Output Service', () => { isCloudEnabled: true, isServerlessEnabled: false, }); + mockedAppContextService.getConfig.mockReturnValue({ + agents: { + elasticsearch: { + ca_sha256: 'test-ca-sha256', + }, + }, + agentless: { enabled: true }, + } as any); await outputService.ensureDefaultOutputs(soClient, esClientMock); @@ -2837,7 +2845,7 @@ describe('Output Service', () => { ); }); - it('should not create ECH agentless output in serverless environment', async () => { + it('should not create ECH agentless output when agentless is disabled', async () => { const soClient = getMockedSoClient({ defaultOutputId: 'existing-default-output', }); @@ -2845,31 +2853,16 @@ describe('Output Service', () => { // @ts-expect-error mockedAppContextService.getCloud.mockReturnValue({ isCloudEnabled: true, - isServerlessEnabled: true, - }); - - await outputService.ensureDefaultOutputs(soClient, esClientMock); - - // Should not create the agentless output - expect(soClient.create).not.toHaveBeenCalledWith( - 'ingest-outputs', - expect.objectContaining({ - name: 'Internal output for agentless', - }), - expect.anything() - ); - }); - - it('should not create ECH agentless output in non-cloud environment', async () => { - const soClient = getMockedSoClient({ - defaultOutputId: 'existing-default-output', - }); - - // @ts-expect-error - mockedAppContextService.getCloud.mockReturnValue({ - isCloudEnabled: false, isServerlessEnabled: false, }); + mockedAppContextService.getConfig.mockReturnValue({ + agents: { + elasticsearch: { + ca_sha256: 'test-ca-sha256', + }, + }, + agentless: { enabled: false }, + } as any); await outputService.ensureDefaultOutputs(soClient, esClientMock); @@ -2908,6 +2901,14 @@ describe('Output Service', () => { isCloudEnabled: true, isServerlessEnabled: false, }); + mockedAppContextService.getConfig.mockReturnValue({ + agents: { + elasticsearch: { + ca_sha256: 'test-ca-sha256', + }, + }, + agentless: { enabled: true }, + } as any); await outputService.ensureDefaultOutputs(soClient, esClientMock); @@ -2931,11 +2932,50 @@ describe('Output Service', () => { isCloudEnabled: true, isServerlessEnabled: false, }); + mockedAppContextService.getConfig.mockReturnValue({ + agents: { + elasticsearch: { + ca_sha256: 'test-ca-sha256', + }, + }, + agentless: { enabled: true }, + } as any); await outputService.ensureDefaultOutputs(soClient, esClientMock); expect(mockedLogger.debug).toHaveBeenCalledWith('Creating default output for ECH agentless'); }); + + it('should not create ECH agentless output in serverless environment', async () => { + const soClient = getMockedSoClient({ + defaultOutputId: 'existing-default-output', + }); + + // @ts-expect-error + mockedAppContextService.getCloud.mockReturnValue({ + isCloudEnabled: true, + isServerlessEnabled: true, + }); + mockedAppContextService.getConfig.mockReturnValue({ + agents: { + elasticsearch: { + ca_sha256: 'test-ca-sha256', + }, + }, + agentless: { enabled: true }, + } as any); + + await outputService.ensureDefaultOutputs(soClient, esClientMock); + + // Should not create the agentless output + expect(soClient.create).not.toHaveBeenCalledWith( + 'ingest-outputs', + expect.objectContaining({ + name: 'Internal output for agentless', + }), + expect.anything() + ); + }); }); describe('outputSavedObjectToOutput', () => { 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 48e07541a0aa0..f0e7bac94d89b 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/output.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/output.ts @@ -85,6 +85,7 @@ import { } from './secrets'; import { findAgentlessPolicies } from './outputs/helpers'; import { patchUpdateDataWithRequireEncryptedAADFields } from './outputs/so_helpers'; +import { isAgentlessEnabled } from './utils/agentless'; import { canEnableSyncIntegrations, createOrUpdateFleetSyncedIntegrationsIndex, @@ -503,7 +504,6 @@ class OutputService { ) { const logger = appContextService.getLogger(); const cloudSetup = appContextService.getCloud(); - const isCloud = cloudSetup?.isCloudEnabled; const isServerless = cloudSetup?.isServerlessEnabled; const outputs = await this.list(soClient); @@ -527,7 +527,7 @@ class OutputService { } // Ensure default output exists for ECH agentless - if (isCloud && !isServerless) { + if (isAgentlessEnabled() && !isServerless) { const defaultAgentlessOutput = outputs.items.find((o) => o.id === ECH_AGENTLESS_OUTPUT_ID); if (!defaultAgentlessOutput) { logger.debug('Creating default output for ECH agentless'); 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 945fad8dccee4..42fe3809e6819 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 @@ -230,6 +230,7 @@ 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(); @@ -257,6 +258,9 @@ describe('createCloudFleetServerHostsIfNeeded', () => { projectId: undefined, }, }); + mockedAppContextService.getConfig.mockReturnValue({ + agentless: { enabled: true }, + } as any); // Default doesn't exist but agentless does mockedFleetServerHostService.getDefaultFleetServerHost = jest .fn() @@ -283,7 +287,7 @@ describe('createCloudFleetServerHostsIfNeeded', () => { ); }); - it('should create both default and agentless fleet server hosts if none are configured', async () => { + it('should create both default and agentless fleet server hosts if none are configured and agentless is enabled', async () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; @@ -300,6 +304,9 @@ describe('createCloudFleetServerHostsIfNeeded', () => { projectId: undefined, }, }); + mockedAppContextService.getConfig.mockReturnValue({ + agentless: { enabled: true }, + } as any); // Mock both getDefaultFleetServerHost and get calls to return null mockedFleetServerHostService.getDefaultFleetServerHost = jest .fn() @@ -343,7 +350,7 @@ describe('createCloudFleetServerHostsIfNeeded', () => { ); }); - it('should create only agentless fleet server host if default already exists', async () => { + it('should create only default fleet server host if agentless is disabled', async () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; @@ -360,6 +367,57 @@ describe('createCloudFleetServerHostsIfNeeded', () => { 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', + attributes: {}, + } as any); + + await createCloudFleetServerHostsIfNeeded(soClient, esClient); + + 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 only agentless fleet server host if default already exists and agentless is enabled', 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() @@ -401,6 +459,9 @@ describe('createCloudFleetServerHostsIfNeeded', () => { projectId: undefined, }, }); + mockedAppContextService.getConfig.mockReturnValue({ + agentless: { enabled: true }, + } as any); // Both exist mockedFleetServerHostService.getDefaultFleetServerHost = jest .fn() @@ -411,6 +472,68 @@ describe('createCloudFleetServerHostsIfNeeded', () => { 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 createCloudFleetServerHostsIfNeeded(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 createCloudFleetServerHostsIfNeeded(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 eff85cc62936b..eb828b170456f 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 @@ -16,6 +16,7 @@ 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'; @@ -144,6 +145,8 @@ export async function createCloudFleetServerHostsIfNeeded( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient ) { + const cloudSetup = appContextService.getCloud(); + const isServerless = cloudSetup?.isServerlessEnabled; const cloudServerHosts = getCloudFleetServersHosts(); if (!cloudServerHosts || cloudServerHosts.length === 0) { return; @@ -168,22 +171,24 @@ export async function createCloudFleetServerHostsIfNeeded( // Ensure internal Fleet Server host exists for ECH agentless. // This "duplicate" ensure that agentless agents use an unmodifiable // Fleet Server host that is hidden from the user. - const agentlessFleetServerHost = await fleetServerHostService.get( - soClient, - ECH_AGENTLESS_FLEET_SERVER_HOST_ID - ); - if (!agentlessFleetServerHost) { - await fleetServerHostService.create( + if (isAgentlessEnabled() && !isServerless) { + const agentlessFleetServerHost = await fleetServerHostService.get( soClient, - esClient, - { - name: 'Internal Fleet Server for agentless', - host_urls: cloudServerHosts, - is_default: false, - is_preconfigured: true, // Fake preconfiguration status to prevent user modification - }, - { id: ECH_AGENTLESS_FLEET_SERVER_HOST_ID, overwrite: true, fromPreconfiguration: true } + ECH_AGENTLESS_FLEET_SERVER_HOST_ID ); + if (!agentlessFleetServerHost) { + await fleetServerHostService.create( + soClient, + esClient, + { + name: 'Internal Fleet Server for agentless', + host_urls: cloudServerHosts, + is_default: false, + is_preconfigured: true, // Fake preconfiguration status to prevent user modification + }, + { id: ECH_AGENTLESS_FLEET_SERVER_HOST_ID, overwrite: true, fromPreconfiguration: true } + ); + } } } From b79b172b6e05ff07f24060758397d36cd6329624 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Thu, 9 Oct 2025 12:59:04 -0700 Subject: [PATCH 07/11] Fix type --- .../platform/plugins/shared/fleet/server/services/output.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 f0e7bac94d89b..67118a35072a3 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/output.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/output.ts @@ -510,7 +510,7 @@ class OutputService { // Ensure general default output const currentDefaultOutput = outputs.items.find((o) => o.is_default); const currentDefaultMonitoringOutput = outputs.items.find((o) => o.is_default_monitoring); - let defaultOutput: Output | undefined = currentDefaultOutput; + let defaultOutput = currentDefaultOutput; if (!currentDefaultOutput) { const newDefaultOutput = { @@ -549,7 +549,7 @@ class OutputService { } } - return { defaultOutput }; + return { defaultOutput } as { defaultOutput: Output }; } public getDefaultESHosts(): string[] { From 9f263a158df7b291bd061ee015bbbc5fa0b9fcbd Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Fri, 10 Oct 2025 11:33:28 -0700 Subject: [PATCH 08/11] Skip cleanup of the new objects --- .../server/services/agent_policy.test.ts | 2 +- .../fleet_server_host.test.ts | 105 ++++++++++++++++++ .../preconfiguration/fleet_server_host.ts | 5 +- .../services/preconfiguration/outputs.test.ts | 45 ++++++++ .../services/preconfiguration/outputs.ts | 7 +- 5 files changed, 160 insertions(+), 4 deletions(-) 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 8b6e98f2ec2ea..8a388e4776f63 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 @@ -447,7 +447,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/preconfiguration/fleet_server_host.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/preconfiguration/fleet_server_host.test.ts index 42fe3809e6819..490eb4136d138 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 @@ -9,6 +9,7 @@ import { securityMock } from '@kbn/security-plugin/server/mocks'; import { appContextService } from '../app_context'; import { fleetServerHostService } from '../fleet_server_host'; +import { ECH_AGENTLESS_FLEET_SERVER_HOST_ID } from '../../constants'; import type { FleetServerHost } from '../../../common/types'; @@ -17,6 +18,7 @@ import { getCloudFleetServersHosts, getPreconfiguredFleetServerHostFromConfig, createOrUpdatePreconfiguredFleetServerHosts, + cleanPreconfiguredFleetServerHosts, } from './fleet_server_host'; import { hashSecret } from './outputs'; @@ -877,3 +879,106 @@ describe('createOrUpdatePreconfiguredFleetServerHosts', () => { expect(mockedFleetServerHostService.update).not.toBeCalled(); }); }); + +describe('cleanPreconfiguredFleetServerHosts', () => { + afterEach(() => { + mockedFleetServerHostService.list.mockReset(); + mockedFleetServerHostService.delete.mockReset(); + mockedFleetServerHostService.update.mockReset(); + }); + + it('should not delete ECH agentless Fleet Server host during cleanup', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + mockedFleetServerHostService.list.mockResolvedValue({ + items: [ + { id: 'host1', is_preconfigured: true } as FleetServerHost, + { id: ECH_AGENTLESS_FLEET_SERVER_HOST_ID, is_preconfigured: true } as FleetServerHost, + { id: 'host2', is_preconfigured: true } as FleetServerHost, + ], + page: 1, + perPage: 10000, + total: 3, + }); + + await cleanPreconfiguredFleetServerHosts(soClient, esClient, [ + { + id: 'host1', + name: 'Host 1', + is_default: false, + is_preconfigured: true, + host_urls: ['http://host1.co:8220'], + }, + ]); + + // Should delete host2 but not ECH agentless Fleet Server host + expect(mockedFleetServerHostService.delete).toBeCalledTimes(1); + expect(mockedFleetServerHostService.delete).toBeCalledWith(soClient, esClient, 'host2', { + fromPreconfiguration: true, + }); + + // Should not attempt to delete or update ECH agentless Fleet Server host + expect(mockedFleetServerHostService.delete).not.toHaveBeenCalledWith( + soClient, + esClient, + ECH_AGENTLESS_FLEET_SERVER_HOST_ID, + expect.anything() + ); + expect(mockedFleetServerHostService.update).not.toHaveBeenCalledWith( + soClient, + esClient, + ECH_AGENTLESS_FLEET_SERVER_HOST_ID, + expect.anything(), + expect.anything() + ); + }); + + it('should not delete or update ECH agentless Fleet Server host even when default', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + mockedFleetServerHostService.list.mockResolvedValue({ + items: [ + { id: 'host1', is_preconfigured: true, is_default: true } as FleetServerHost, + { + id: ECH_AGENTLESS_FLEET_SERVER_HOST_ID, + is_preconfigured: true, + is_default: false, + } as FleetServerHost, + ], + page: 1, + perPage: 10000, + total: 2, + }); + + await cleanPreconfiguredFleetServerHosts(soClient, esClient, []); + + // Should update host1 (default) but not ECH agentless Fleet Server host + expect(mockedFleetServerHostService.update).toBeCalledTimes(1); + expect(mockedFleetServerHostService.update).toBeCalledWith( + soClient, + esClient, + 'host1', + expect.objectContaining({ + is_preconfigured: false, + }), + { fromPreconfiguration: true } + ); + + // Should not attempt to delete or update ECH agentless Fleet Server host + expect(mockedFleetServerHostService.delete).not.toHaveBeenCalledWith( + soClient, + esClient, + ECH_AGENTLESS_FLEET_SERVER_HOST_ID, + expect.anything() + ); + expect(mockedFleetServerHostService.update).not.toHaveBeenCalledWith( + soClient, + esClient, + ECH_AGENTLESS_FLEET_SERVER_HOST_ID, + expect.anything(), + expect.anything() + ); + }); +}); 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 eb828b170456f..f887328c10e7e 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 @@ -199,7 +199,10 @@ export async function cleanPreconfiguredFleetServerHosts( ) { const existingFleetServerHosts = await fleetServerHostService.list(soClient); const existingPreconfiguredHosts = existingFleetServerHosts.items.filter( - (o) => o.is_preconfigured === true + (o) => + o.is_preconfigured === true && + // Skip cleanup for ECH agentless Fleet Server host + o.id !== ECH_AGENTLESS_FLEET_SERVER_HOST_ID ); for (const existingFleetServerHost of existingPreconfiguredHosts) { 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..8db8c9125dcc1 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 @@ -12,6 +12,7 @@ import type { PreconfiguredOutput } from '../../../common/types'; import type { Output } from '../../types'; import * as agentPolicy from '../agent_policy'; import { outputService } from '../output'; +import { ECH_AGENTLESS_OUTPUT_ID } from '../../constants'; import { createOrUpdatePreconfiguredOutputs, @@ -1199,6 +1200,50 @@ describe('Outputs preconfiguration', () => { { fromPreconfiguration: true } ); }); + + it('should not delete ECH agentless output during cleanup', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + mockedOutputService.list.mockResolvedValue({ + items: [ + { id: 'output1', is_preconfigured: true } as Output, + { id: ECH_AGENTLESS_OUTPUT_ID, is_preconfigured: true } as Output, + { id: 'output2', is_preconfigured: true } as Output, + ], + page: 1, + perPage: 10000, + total: 3, + }); + await cleanPreconfiguredOutputs(soClient, esClient, [ + { + id: 'output1', + is_default: false, + is_default_monitoring: false, + name: 'Output 1', + type: 'elasticsearch', + hosts: ['http://es.co:9201'], + }, + ]); + + // Should delete output2 but not ECH agentless output + expect(mockedOutputService.delete).toBeCalledTimes(1); + expect(mockedOutputService.delete).toBeCalledWith(soClient, 'output2', { + fromPreconfiguration: true, + }); + // Should not attempt to delete or update ECH agentless output + expect(mockedOutputService.delete).not.toHaveBeenCalledWith( + soClient, + ECH_AGENTLESS_OUTPUT_ID, + expect.anything() + ); + expect(mockedOutputService.update).not.toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + ECH_AGENTLESS_OUTPUT_ID, + expect.anything(), + expect.anything() + ); + }); }); }); }); 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 162474401cf59..a2fe27bb9c601 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,7 +25,7 @@ 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'; @@ -208,7 +208,10 @@ export async function cleanPreconfiguredOutputs( ) { const existingOutputs = await outputService.list(soClient); const existingPreconfiguredOutput = existingOutputs.items.filter( - (o) => o.is_preconfigured === true + (o) => + o.is_preconfigured === true && + // Skip cleanup for ECH agentless output + o.id !== ECH_AGENTLESS_OUTPUT_ID ); const logger = appContextService.getLogger(); From 173a344bead15169f8cba0b858f11309e4ff0750 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Fri, 10 Oct 2025 12:49:52 -0700 Subject: [PATCH 09/11] Use preconfiguration to create the objects instead, clean up unnecessary changes --- .../fleet/server/services/output.test.ts | 274 +--------------- .../shared/fleet/server/services/output.ts | 45 +-- .../fleet_server_host.test.ts | 301 +++++++----------- .../preconfiguration/fleet_server_host.ts | 48 +-- .../services/preconfiguration/outputs.test.ts | 163 +++++++--- .../services/preconfiguration/outputs.ts | 23 +- .../fleet/server/services/setup.test.ts | 4 +- .../shared/fleet/server/services/setup.ts | 2 +- 8 files changed, 275 insertions(+), 585 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/server/services/output.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/output.test.ts index 74a171d6ba758..47be510bfef21 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/output.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/output.test.ts @@ -102,9 +102,7 @@ function getMockedSoClient( return mockOutputSO('output-test'); } case outputIdToUuid('existing-default-output'): { - return mockOutputSO('existing-default-output', { - is_default: true, - }); + return mockOutputSO('existing-default-output'); } case outputIdToUuid('existing-default-monitoring-output'): { return mockOutputSO('existing-default-monitoring-output', { @@ -244,40 +242,6 @@ function getMockedSoClient( }; } - // Handle general list() calls for ensureDefaultOutputs - only return existing outputs if we have both perPage and sortField - // This is to distinguish ensureDefaultOutputs calls from other find calls - if ( - findOptions.perPage === 10000 && - findOptions.sortField === 'is_default' && - (options?.defaultOutputId || options?.defaultOutputMonitoringId) - ) { - const savedObjects = []; - if (options?.defaultOutputId) { - savedObjects.push({ - score: 0, - ...(await soClient.get('ingest-outputs', outputIdToUuid(options.defaultOutputId))), - }); - } - if ( - options?.defaultOutputMonitoringId && - options.defaultOutputMonitoringId !== options.defaultOutputId - ) { - savedObjects.push({ - score: 0, - ...(await soClient.get( - 'ingest-outputs', - outputIdToUuid(options.defaultOutputMonitoringId) - )), - }); - } - return { - page: 1, - per_page: 10000, - saved_objects: savedObjects, - total: savedObjects.length, - }; - } - return { page: 1, per_page: 10, @@ -2742,242 +2706,6 @@ describe('Output Service', () => { }); }); - describe('ensureDefaultOutputs', () => { - beforeEach(() => { - mockedAppContextService.getEncryptedSavedObjectsSetup.mockReturnValue({ - canEncrypt: true, - } as any); - mockedAppContextService.getConfig.mockReturnValue({ - agents: { - elasticsearch: { - ca_sha256: 'test-ca-sha256', - }, - }, - } as any); - }); - - it('should create default output when none exists', async () => { - const soClient = getMockedSoClient(); - - // @ts-expect-error - mockedAppContextService.getCloud.mockReturnValue({ - isCloudEnabled: false, - isServerlessEnabled: false, - }); - - const result = await outputService.ensureDefaultOutputs(soClient, esClientMock); - - expect(result).toHaveProperty('defaultOutput'); - expect(soClient.create).toHaveBeenCalledWith( - 'ingest-outputs', - expect.objectContaining({ - name: 'default', - type: 'elasticsearch', - is_default: true, - is_default_monitoring: true, - }), - expect.objectContaining({ - id: outputIdToUuid('fleet-default-output'), - overwrite: true, - }) - ); - }); - - it('should return existing default output when one already exists', async () => { - const soClient = getMockedSoClient({ - defaultOutputId: 'existing-default-output', - }); - - // @ts-expect-error - mockedAppContextService.getCloud.mockReturnValue({ - isCloudEnabled: false, - isServerlessEnabled: false, - }); - - const result = await outputService.ensureDefaultOutputs(soClient, esClientMock); - - expect(result).toHaveProperty('defaultOutput'); - // Should not create a new default output - expect(soClient.create).not.toHaveBeenCalledWith( - 'ingest-outputs', - expect.objectContaining({ - name: 'default', - is_default: true, - }), - expect.anything() - ); - }); - - it('should create ECH agentless output when agentless is enabled', async () => { - const soClient = getMockedSoClient({ - defaultOutputId: 'existing-default-output', - }); - - // @ts-expect-error - mockedAppContextService.getCloud.mockReturnValue({ - isCloudEnabled: true, - isServerlessEnabled: false, - }); - mockedAppContextService.getConfig.mockReturnValue({ - agents: { - elasticsearch: { - ca_sha256: 'test-ca-sha256', - }, - }, - agentless: { enabled: true }, - } as any); - - await outputService.ensureDefaultOutputs(soClient, esClientMock); - - expect(soClient.create).toHaveBeenCalledWith( - 'ingest-outputs', - expect.objectContaining({ - name: 'Internal output for agentless', - type: 'elasticsearch', - is_default: false, - is_default_monitoring: false, - is_preconfigured: true, - }), - expect.objectContaining({ - id: outputIdToUuid('es-agentless-output'), - overwrite: true, - }) - ); - }); - - it('should not create ECH agentless output when agentless is disabled', async () => { - const soClient = getMockedSoClient({ - defaultOutputId: 'existing-default-output', - }); - - // @ts-expect-error - mockedAppContextService.getCloud.mockReturnValue({ - isCloudEnabled: true, - isServerlessEnabled: false, - }); - mockedAppContextService.getConfig.mockReturnValue({ - agents: { - elasticsearch: { - ca_sha256: 'test-ca-sha256', - }, - }, - agentless: { enabled: false }, - } as any); - - await outputService.ensureDefaultOutputs(soClient, esClientMock); - - // Should not create the agentless output - expect(soClient.create).not.toHaveBeenCalledWith( - 'ingest-outputs', - expect.objectContaining({ - name: 'Internal output for agentless', - }), - expect.anything() - ); - }); - - it('should not create ECH agentless output when it already exists', async () => { - const soClient = getMockedSoClient({ - defaultOutputId: 'existing-default-output', - }); - - // Mock the list method to return existing agentless output - soClient.find.mockResolvedValueOnce({ - page: 1, - per_page: 10, - saved_objects: [ - mockOutputSO('existing-default-output', { is_default: true }), - mockOutputSO('es-agentless-output', { - name: 'Internal output for agentless', - is_preconfigured: true, - is_default: false, - }), - ], - total: 2, - } as any); - - // @ts-expect-error - mockedAppContextService.getCloud.mockReturnValue({ - isCloudEnabled: true, - isServerlessEnabled: false, - }); - mockedAppContextService.getConfig.mockReturnValue({ - agents: { - elasticsearch: { - ca_sha256: 'test-ca-sha256', - }, - }, - agentless: { enabled: true }, - } as any); - - await outputService.ensureDefaultOutputs(soClient, esClientMock); - - // Should not create the agentless output since it already exists - expect(soClient.create).not.toHaveBeenCalledWith( - 'ingest-outputs', - expect.objectContaining({ - name: 'Internal output for agentless', - }), - expect.anything() - ); - }); - - it('should log debug message when creating ECH agentless output', async () => { - const soClient = getMockedSoClient({ - defaultOutputId: 'existing-default-output', - }); - - // @ts-expect-error - mockedAppContextService.getCloud.mockReturnValue({ - isCloudEnabled: true, - isServerlessEnabled: false, - }); - mockedAppContextService.getConfig.mockReturnValue({ - agents: { - elasticsearch: { - ca_sha256: 'test-ca-sha256', - }, - }, - agentless: { enabled: true }, - } as any); - - await outputService.ensureDefaultOutputs(soClient, esClientMock); - - expect(mockedLogger.debug).toHaveBeenCalledWith('Creating default output for ECH agentless'); - }); - - it('should not create ECH agentless output in serverless environment', async () => { - const soClient = getMockedSoClient({ - defaultOutputId: 'existing-default-output', - }); - - // @ts-expect-error - mockedAppContextService.getCloud.mockReturnValue({ - isCloudEnabled: true, - isServerlessEnabled: true, - }); - mockedAppContextService.getConfig.mockReturnValue({ - agents: { - elasticsearch: { - ca_sha256: 'test-ca-sha256', - }, - }, - agentless: { enabled: true }, - } as any); - - await outputService.ensureDefaultOutputs(soClient, esClientMock); - - // Should not create the agentless output - expect(soClient.create).not.toHaveBeenCalledWith( - 'ingest-outputs', - expect.objectContaining({ - name: 'Internal output for agentless', - }), - expect.anything() - ); - }); - }); - describe('outputSavedObjectToOutput', () => { it('should return output object with parsed SSL when SSL is a valid JSON string', () => { const so = mockOutputSO('output-test', { 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 67118a35072a3..103a4ea4ccf8c 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/output.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/output.ts @@ -46,7 +46,6 @@ import { OUTPUT_SAVED_OBJECT_TYPE, OUTPUT_HEALTH_DATA_STREAM, MAX_CONCURRENT_BACKFILL_OUTPUTS_PRESETS, - ECH_AGENTLESS_OUTPUT_ID, } from '../constants'; import { SO_SEARCH_LIMIT, @@ -85,7 +84,7 @@ import { } from './secrets'; import { findAgentlessPolicies } from './outputs/helpers'; import { patchUpdateDataWithRequireEncryptedAADFields } from './outputs/so_helpers'; -import { isAgentlessEnabled } from './utils/agentless'; + import { canEnableSyncIntegrations, createOrUpdateFleetSyncedIntegrationsIndex, @@ -498,58 +497,30 @@ class OutputService { } } - public async ensureDefaultOutputs( + public async ensureDefaultOutput( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient ) { - const logger = appContextService.getLogger(); - const cloudSetup = appContextService.getCloud(); - const isServerless = cloudSetup?.isServerlessEnabled; const outputs = await this.list(soClient); - // Ensure general default output - const currentDefaultOutput = outputs.items.find((o) => o.is_default); - const currentDefaultMonitoringOutput = outputs.items.find((o) => o.is_default_monitoring); - let defaultOutput = currentDefaultOutput; + const defaultOutput = outputs.items.find((o) => o.is_default); + const defaultMonitoringOutput = outputs.items.find((o) => o.is_default_monitoring); - if (!currentDefaultOutput) { + if (!defaultOutput) { const newDefaultOutput = { ...DEFAULT_OUTPUT, hosts: this.getDefaultESHosts(), ca_sha256: appContextService.getConfig()!.agents.elasticsearch.ca_sha256, - is_default_monitoring: !currentDefaultMonitoringOutput, + is_default_monitoring: !defaultMonitoringOutput, } as NewOutput; - defaultOutput = await this.create(soClient, esClient, newDefaultOutput, { + return await this.create(soClient, esClient, newDefaultOutput, { id: DEFAULT_OUTPUT_ID, overwrite: true, }); } - // Ensure default output exists for ECH agentless - if (isAgentlessEnabled() && !isServerless) { - const defaultAgentlessOutput = outputs.items.find((o) => o.id === ECH_AGENTLESS_OUTPUT_ID); - if (!defaultAgentlessOutput) { - logger.debug('Creating default output for ECH agentless'); - const newDefaultAgentlessOutput = { - name: 'Internal output for agentless', - type: outputType.Elasticsearch, - hosts: this.getDefaultESHosts(), - ca_sha256: appContextService.getConfig()!.agents.elasticsearch.ca_sha256, - is_default: false, - is_default_monitoring: false, - is_preconfigured: true, // Fake preconfiguration status to prevent user modification - } as NewOutput; - - await this.create(soClient, esClient, newDefaultAgentlessOutput, { - id: ECH_AGENTLESS_OUTPUT_ID, - overwrite: true, - fromPreconfiguration: true, - }); - } - } - - return { defaultOutput } as { defaultOutput: Output }; + return defaultOutput; } public getDefaultESHosts(): string[] { 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 490eb4136d138..905bb71426c36 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 @@ -9,16 +9,14 @@ import { securityMock } from '@kbn/security-plugin/server/mocks'; import { appContextService } from '../app_context'; import { fleetServerHostService } from '../fleet_server_host'; -import { ECH_AGENTLESS_FLEET_SERVER_HOST_ID } from '../../constants'; import type { FleetServerHost } from '../../../common/types'; import { - createCloudFleetServerHostsIfNeeded, + createCloudFleetServerHostIfNeeded, getCloudFleetServersHosts, getPreconfiguredFleetServerHostFromConfig, createOrUpdatePreconfiguredFleetServerHosts, - cleanPreconfiguredFleetServerHosts, } from './fleet_server_host'; import { hashSecret } from './outputs'; @@ -36,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: [ @@ -53,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: [ @@ -238,7 +335,7 @@ describe('createCloudFleetServerHostsIfNeeded', () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - await createCloudFleetServerHostsIfNeeded(soClient, esClient); + await createCloudFleetServerHostIfNeeded(soClient, esClient); expect(mockedFleetServerHostService.create).not.toBeCalled(); }); @@ -271,7 +368,7 @@ describe('createCloudFleetServerHostsIfNeeded', () => { id: 'existing-agentless', } as any); - await createCloudFleetServerHostsIfNeeded(soClient, esClient); + await createCloudFleetServerHostIfNeeded(soClient, esClient); expect(mockedFleetServerHostService.create).toBeCalledTimes(1); @@ -289,69 +386,6 @@ describe('createCloudFleetServerHostsIfNeeded', () => { ); }); - it('should create both default and agentless fleet server hosts if none are configured and agentless is enabled', 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); - // 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', - attributes: {}, - } as any); - - await createCloudFleetServerHostsIfNeeded(soClient, esClient); - - expect(mockedFleetServerHostService.create).toBeCalledTimes(2); - - // Verify default Fleet Server host creation - expect(mockedFleetServerHostService.create).toHaveBeenNthCalledWith( - 1, - 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 } - ); - - // Verify agentless Fleet Server host creation - expect(mockedFleetServerHostService.create).toHaveBeenNthCalledWith( - 2, - expect.anything(), - expect.anything(), - expect.objectContaining({ - name: 'Internal Fleet Server for agentless', - host_urls: ['https://deployment-id-1.fleet.us-east-1.aws.found.io'], - is_default: false, - is_preconfigured: true, - }), - { id: 'internal-agentless-fleet-server', overwrite: true, fromPreconfiguration: true } - ); - }); - it('should create only default fleet server host if agentless is disabled', async () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; @@ -382,7 +416,7 @@ describe('createCloudFleetServerHostsIfNeeded', () => { attributes: {}, } as any); - await createCloudFleetServerHostsIfNeeded(soClient, esClient); + await createCloudFleetServerHostIfNeeded(soClient, esClient); expect(mockedFleetServerHostService.create).toBeCalledTimes(1); @@ -400,7 +434,7 @@ describe('createCloudFleetServerHostsIfNeeded', () => { ); }); - it('should create only agentless fleet server host if default already exists and agentless is enabled', async () => { + 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; @@ -426,22 +460,10 @@ describe('createCloudFleetServerHostsIfNeeded', () => { .mockResolvedValue({ id: 'existing-default' } as any); mockedFleetServerHostService.get.mockResolvedValue(null as any); - await createCloudFleetServerHostsIfNeeded(soClient, esClient); + await createCloudFleetServerHostIfNeeded(soClient, esClient); - expect(mockedFleetServerHostService.create).toBeCalledTimes(1); - - // Verify only agentless Fleet Server host creation - expect(mockedFleetServerHostService.create).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.objectContaining({ - name: 'Internal Fleet Server for agentless', - host_urls: ['https://deployment-id-1.fleet.us-east-1.aws.found.io'], - is_default: false, - is_preconfigured: true, - }), - { id: 'internal-agentless-fleet-server', overwrite: true, fromPreconfiguration: true } - ); + // 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 () => { @@ -470,7 +492,7 @@ describe('createCloudFleetServerHostsIfNeeded', () => { .mockResolvedValue({ id: 'existing-default' } as any); mockedFleetServerHostService.get.mockResolvedValue({ id: 'existing-agentless' } as any); - await createCloudFleetServerHostsIfNeeded(soClient, esClient); + await createCloudFleetServerHostIfNeeded(soClient, esClient); expect(mockedFleetServerHostService.create).not.toBeCalled(); }); @@ -501,7 +523,7 @@ describe('createCloudFleetServerHostsIfNeeded', () => { .mockResolvedValue({ id: 'existing-default' } as any); mockedFleetServerHostService.get.mockResolvedValue(null as any); - await createCloudFleetServerHostsIfNeeded(soClient, esClient); + await createCloudFleetServerHostIfNeeded(soClient, esClient); expect(mockedFleetServerHostService.create).not.toBeCalled(); }); @@ -532,7 +554,7 @@ describe('createCloudFleetServerHostsIfNeeded', () => { .mockResolvedValue({ id: 'existing-default' } as any); mockedFleetServerHostService.get.mockResolvedValue(null as any); - await createCloudFleetServerHostsIfNeeded(soClient, esClient); + await createCloudFleetServerHostIfNeeded(soClient, esClient); expect(mockedFleetServerHostService.create).not.toBeCalled(); }); @@ -879,106 +901,3 @@ describe('createOrUpdatePreconfiguredFleetServerHosts', () => { expect(mockedFleetServerHostService.update).not.toBeCalled(); }); }); - -describe('cleanPreconfiguredFleetServerHosts', () => { - afterEach(() => { - mockedFleetServerHostService.list.mockReset(); - mockedFleetServerHostService.delete.mockReset(); - mockedFleetServerHostService.update.mockReset(); - }); - - it('should not delete ECH agentless Fleet Server host during cleanup', async () => { - const soClient = savedObjectsClientMock.create(); - const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - - mockedFleetServerHostService.list.mockResolvedValue({ - items: [ - { id: 'host1', is_preconfigured: true } as FleetServerHost, - { id: ECH_AGENTLESS_FLEET_SERVER_HOST_ID, is_preconfigured: true } as FleetServerHost, - { id: 'host2', is_preconfigured: true } as FleetServerHost, - ], - page: 1, - perPage: 10000, - total: 3, - }); - - await cleanPreconfiguredFleetServerHosts(soClient, esClient, [ - { - id: 'host1', - name: 'Host 1', - is_default: false, - is_preconfigured: true, - host_urls: ['http://host1.co:8220'], - }, - ]); - - // Should delete host2 but not ECH agentless Fleet Server host - expect(mockedFleetServerHostService.delete).toBeCalledTimes(1); - expect(mockedFleetServerHostService.delete).toBeCalledWith(soClient, esClient, 'host2', { - fromPreconfiguration: true, - }); - - // Should not attempt to delete or update ECH agentless Fleet Server host - expect(mockedFleetServerHostService.delete).not.toHaveBeenCalledWith( - soClient, - esClient, - ECH_AGENTLESS_FLEET_SERVER_HOST_ID, - expect.anything() - ); - expect(mockedFleetServerHostService.update).not.toHaveBeenCalledWith( - soClient, - esClient, - ECH_AGENTLESS_FLEET_SERVER_HOST_ID, - expect.anything(), - expect.anything() - ); - }); - - it('should not delete or update ECH agentless Fleet Server host even when default', async () => { - const soClient = savedObjectsClientMock.create(); - const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - - mockedFleetServerHostService.list.mockResolvedValue({ - items: [ - { id: 'host1', is_preconfigured: true, is_default: true } as FleetServerHost, - { - id: ECH_AGENTLESS_FLEET_SERVER_HOST_ID, - is_preconfigured: true, - is_default: false, - } as FleetServerHost, - ], - page: 1, - perPage: 10000, - total: 2, - }); - - await cleanPreconfiguredFleetServerHosts(soClient, esClient, []); - - // Should update host1 (default) but not ECH agentless Fleet Server host - expect(mockedFleetServerHostService.update).toBeCalledTimes(1); - expect(mockedFleetServerHostService.update).toBeCalledWith( - soClient, - esClient, - 'host1', - expect.objectContaining({ - is_preconfigured: false, - }), - { fromPreconfiguration: true } - ); - - // Should not attempt to delete or update ECH agentless Fleet Server host - expect(mockedFleetServerHostService.delete).not.toHaveBeenCalledWith( - soClient, - esClient, - ECH_AGENTLESS_FLEET_SERVER_HOST_ID, - expect.anything() - ); - expect(mockedFleetServerHostService.update).not.toHaveBeenCalledWith( - soClient, - esClient, - ECH_AGENTLESS_FLEET_SERVER_HOST_ID, - expect.anything(), - expect.anything() - ); - }); -}); 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 f887328c10e7e..8238ba843d83f 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 @@ -44,6 +44,7 @@ export function getPreconfiguredFleetServerHostFromConfig(config?: FleetConfigTy const { fleetServerHosts: fleetServerHostsFromConfig } = config; const legacyFleetServerHostsConfig = getConfigFleetServerHosts(config); + const cloudServerHosts = getCloudFleetServersHosts(); const fleetServerHosts: FleetServerHost[] = (fleetServerHostsFromConfig || []).concat([ ...(legacyFleetServerHostsConfig @@ -56,6 +57,18 @@ export function getPreconfiguredFleetServerHostFromConfig(config?: FleetConfigTy }, ] : []), + // Include ECH agentless Fleet Server host when agentless is enabled and not in serverless environment + ...(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) { @@ -75,7 +88,7 @@ export async function ensurePreconfiguredFleetServerHosts( esClient, preconfiguredFleetServerHosts ); - await createCloudFleetServerHostsIfNeeded(soClient, esClient); + await createCloudFleetServerHostIfNeeded(soClient, esClient); await cleanPreconfiguredFleetServerHosts(soClient, esClient, preconfiguredFleetServerHosts); } @@ -141,18 +154,15 @@ export async function createOrUpdatePreconfiguredFleetServerHosts( ); } -export async function createCloudFleetServerHostsIfNeeded( +export async function createCloudFleetServerHostIfNeeded( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient ) { - const cloudSetup = appContextService.getCloud(); - const isServerless = cloudSetup?.isServerlessEnabled; const cloudServerHosts = getCloudFleetServersHosts(); if (!cloudServerHosts || cloudServerHosts.length === 0) { return; } - // Ensure default Fleet Server host exists for ECH const defaultFleetServerHost = await fleetServerHostService.getDefaultFleetServerHost(soClient); if (!defaultFleetServerHost) { await fleetServerHostService.create( @@ -167,29 +177,6 @@ export async function createCloudFleetServerHostsIfNeeded( { id: DEFAULT_FLEET_SERVER_HOST_ID, overwrite: true, fromPreconfiguration: true } ); } - - // Ensure internal Fleet Server host exists for ECH agentless. - // This "duplicate" ensure that agentless agents use an unmodifiable - // Fleet Server host that is hidden from the user. - if (isAgentlessEnabled() && !isServerless) { - const agentlessFleetServerHost = await fleetServerHostService.get( - soClient, - ECH_AGENTLESS_FLEET_SERVER_HOST_ID - ); - if (!agentlessFleetServerHost) { - await fleetServerHostService.create( - soClient, - esClient, - { - name: 'Internal Fleet Server for agentless', - host_urls: cloudServerHosts, - is_default: false, - is_preconfigured: true, // Fake preconfiguration status to prevent user modification - }, - { id: ECH_AGENTLESS_FLEET_SERVER_HOST_ID, overwrite: true, fromPreconfiguration: true } - ); - } - } } export async function cleanPreconfiguredFleetServerHosts( @@ -199,10 +186,7 @@ export async function cleanPreconfiguredFleetServerHosts( ) { const existingFleetServerHosts = await fleetServerHostService.list(soClient); const existingPreconfiguredHosts = existingFleetServerHosts.items.filter( - (o) => - o.is_preconfigured === true && - // Skip cleanup for ECH agentless Fleet Server host - o.id !== ECH_AGENTLESS_FLEET_SERVER_HOST_ID + (o) => o.is_preconfigured === true ); for (const existingFleetServerHost of existingPreconfiguredHosts) { 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 8db8c9125dcc1..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 @@ -12,7 +12,6 @@ import type { PreconfiguredOutput } from '../../../common/types'; import type { Output } from '../../types'; import * as agentPolicy from '../agent_policy'; import { outputService } from '../output'; -import { ECH_AGENTLESS_OUTPUT_ID } from '../../constants'; import { createOrUpdatePreconfiguredOutputs, @@ -46,6 +45,8 @@ jest.mock('../app_context', () => ({ } ), getTaskManagerStart: jest.fn(), + getCloud: jest.fn().mockReturnValue(null), + getConfig: jest.fn().mockReturnValue({}), }, })); @@ -308,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; @@ -1200,50 +1317,6 @@ describe('Outputs preconfiguration', () => { { fromPreconfiguration: true } ); }); - - it('should not delete ECH agentless output during cleanup', async () => { - const soClient = savedObjectsClientMock.create(); - const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - mockedOutputService.list.mockResolvedValue({ - items: [ - { id: 'output1', is_preconfigured: true } as Output, - { id: ECH_AGENTLESS_OUTPUT_ID, is_preconfigured: true } as Output, - { id: 'output2', is_preconfigured: true } as Output, - ], - page: 1, - perPage: 10000, - total: 3, - }); - await cleanPreconfiguredOutputs(soClient, esClient, [ - { - id: 'output1', - is_default: false, - is_default_monitoring: false, - name: 'Output 1', - type: 'elasticsearch', - hosts: ['http://es.co:9201'], - }, - ]); - - // Should delete output2 but not ECH agentless output - expect(mockedOutputService.delete).toBeCalledTimes(1); - expect(mockedOutputService.delete).toBeCalledWith(soClient, 'output2', { - fromPreconfiguration: true, - }); - // Should not attempt to delete or update ECH agentless output - expect(mockedOutputService.delete).not.toHaveBeenCalledWith( - soClient, - ECH_AGENTLESS_OUTPUT_ID, - expect.anything() - ); - expect(mockedOutputService.update).not.toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - ECH_AGENTLESS_OUTPUT_ID, - expect.anything(), - expect.anything() - ); - }); }); }); }); 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 a2fe27bb9c601..d35d2cd1572f6 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 @@ -29,6 +29,7 @@ import { DEFAULT_OUTPUT_ID, DEFAULT_OUTPUT, ECH_AGENTLESS_OUTPUT_ID } from '../. 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 ECH agentless output when agentless is enabled and not in serverless environment + ...(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; @@ -208,10 +226,7 @@ export async function cleanPreconfiguredOutputs( ) { const existingOutputs = await outputService.list(soClient); const existingPreconfiguredOutput = existingOutputs.items.filter( - (o) => - o.is_preconfigured === true && - // Skip cleanup for ECH agentless output - o.id !== ECH_AGENTLESS_OUTPUT_ID + (o) => o.is_preconfigured === true ); const logger = appContextService.getLogger(); 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 ecc61ab57aa51..2ee4655ae3399 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 @@ -103,7 +103,7 @@ describe('setupFleet', () => { (upgradeAgentPolicySchemaVersion as jest.Mock).mockResolvedValue(undefined); (createCCSIndexPatterns as jest.Mock).mockResolvedValue(undefined); (getSpaceAwareSaveobjectsClients as jest.Mock).mockReturnValue({}); - (outputService.ensureDefaultOutputs as jest.Mock).mockResolvedValue({ + (outputService.ensureDefaultOutput as jest.Mock).mockResolvedValue({ defaultOutput: { id: 'test-default-output', name: 'test' }, }); }); @@ -150,7 +150,7 @@ describe('setupFleet', () => { await setupFleet(soClient, esClient); - expect(outputService.ensureDefaultOutputs).toHaveBeenCalledWith(soClient, esClient); + expect(outputService.ensureDefaultOutput).toHaveBeenCalledWith(soClient, esClient); }); it('should return non fatal errors when generateKeyPair result has errors', async () => { diff --git a/x-pack/platform/plugins/shared/fleet/server/services/setup.ts b/x-pack/platform/plugins/shared/fleet/server/services/setup.ts index 33a6420ba6c8c..33a8e95cde1b8 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/setup.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/setup.ts @@ -172,7 +172,7 @@ async function createSetupSideEffects( getPreconfiguredOutputFromConfig(appContextService.getConfig()) ); - const { defaultOutput } = await outputService.ensureDefaultOutputs(soClient, esClient); + const defaultOutput = await outputService.ensureDefaultOutput(soClient, esClient); logger.debug('Backfilling output performance presets'); await outputService.backfillAllOutputPresets(soClient, esClient); From 20efbc12b976519f9e1f4b47c93dadd451914256 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Fri, 10 Oct 2025 13:37:33 -0700 Subject: [PATCH 10/11] Put back unneeded changes --- .../services/preconfiguration/fleet_server_host.ts | 12 +++++++----- .../server/services/preconfiguration/outputs.ts | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) 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 8238ba843d83f..e0cdfc8464792 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 @@ -25,10 +25,12 @@ import { hashSecret, isSecretDifferent } from './outputs'; export function getCloudFleetServersHosts() { const cloudSetup = appContextService.getCloud(); - const isCloud = cloudSetup?.isCloudEnabled; - const isServerless = cloudSetup?.isServerlessEnabled; - - if (isCloud && !isServerless && cloudSetup.cloudHost) { + if ( + cloudSetup && + !cloudSetup.isServerlessEnabled && + cloudSetup.isCloudEnabled && + cloudSetup.cloudHost + ) { // Fleet Server url are formed like this `https://.fleet. return [ `https://${cloudSetup.deploymentId}.fleet.${cloudSetup.cloudHost}${ @@ -57,7 +59,7 @@ export function getPreconfiguredFleetServerHostFromConfig(config?: FleetConfigTy }, ] : []), - // Include ECH agentless Fleet Server host when agentless is enabled and not in serverless environment + // Include agentless Fleet Server host in ECH ...(isAgentlessEnabled() && cloudServerHosts ? [ { 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 d35d2cd1572f6..eda96c738996e 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 @@ -51,7 +51,7 @@ export function getPreconfiguredOutputFromConfig(config?: FleetConfigType) { } as PreconfiguredOutput, ] : []), - // Include ECH agentless output when agentless is enabled and not in serverless environment + // Include agentless output in ECH ...(isAgentlessEnabled() && !appContextService.getCloud()?.isServerlessEnabled ? [ { From 1b184a8362501d26857bc12f86e409bcf46986be Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Mon, 13 Oct 2025 12:20:32 -0700 Subject: [PATCH 11/11] Add correct cloud config for CSP agentless tests --- .../cloud_security_posture_functional/config.agentless.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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}`, ],