diff --git a/x-pack/plugins/fleet/common/types/index.ts b/x-pack/plugins/fleet/common/types/index.ts index 5c385f938a69e..1984de79a6357 100644 --- a/x-pack/plugins/fleet/common/types/index.ts +++ b/x-pack/plugins/fleet/common/types/index.ts @@ -26,6 +26,9 @@ export interface FleetConfigType { host?: string; ca_sha256?: string; }; + fleet_server?: { + hosts?: string[]; + }; agentPolicyRolloutRateLimitIntervalMs: number; agentPolicyRolloutRateLimitRequestPerInterval: number; }; diff --git a/x-pack/plugins/fleet/common/types/models/agent_policy.ts b/x-pack/plugins/fleet/common/types/models/agent_policy.ts index 7e5b799e484d6..6e984b2d0b3da 100644 --- a/x-pack/plugins/fleet/common/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/agent_policy.ts @@ -66,9 +66,13 @@ export interface FullAgentPolicy { [key: string]: any; }; }; - fleet?: { - kibana: FullAgentPolicyKibanaConfig; - }; + fleet?: + | { + hosts: string[]; + } + | { + kibana: FullAgentPolicyKibanaConfig; + }; inputs: FullAgentPolicyInput[]; revision?: number; agent?: { diff --git a/x-pack/plugins/fleet/common/types/models/settings.ts b/x-pack/plugins/fleet/common/types/models/settings.ts index bb345a67bec41..d6932f9a4d83f 100644 --- a/x-pack/plugins/fleet/common/types/models/settings.ts +++ b/x-pack/plugins/fleet/common/types/models/settings.ts @@ -8,9 +8,11 @@ import type { SavedObjectAttributes } from 'src/core/public'; export interface BaseSettings { + has_seen_add_data_notice?: boolean; + fleet_server_hosts: string[]; + // TODO remove as part of https://github.com/elastic/kibana/issues/94303 kibana_urls: string[]; kibana_ca_sha256?: string; - has_seen_add_data_notice?: boolean; } export interface Settings extends BaseSettings { diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/enrollment_instructions/manual/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/enrollment_instructions/manual/index.tsx index 6f1adfc8cf9c1..a46e49233cc99 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/enrollment_instructions/manual/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/enrollment_instructions/manual/index.tsx @@ -13,6 +13,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import type { EnrollmentAPIKey } from '../../../types'; interface Props { + fleetServerHosts: string[]; kibanaUrl: string; apiKey: EnrollmentAPIKey; kibanaCASha256?: string; @@ -23,14 +24,32 @@ const CommandCode = styled.pre({ overflow: 'scroll', }); +function getfleetServerHostsEnrollArgs(apiKey: EnrollmentAPIKey, fleetServerHosts: string[]) { + return `--url=${fleetServerHosts[0]} --enrollment-token=${apiKey.api_key}`; +} + +function getKibanaUrlEnrollArgs( + apiKey: EnrollmentAPIKey, + kibanaUrl: string, + kibanaCASha256?: string +) { + return `--kibana-url=${kibanaUrl} --enrollment-token=${apiKey.api_key}${ + kibanaCASha256 ? ` --ca_sha256=${kibanaCASha256}` : '' + }`; +} + export const ManualInstructions: React.FunctionComponent = ({ kibanaUrl, apiKey, kibanaCASha256, + fleetServerHosts, }) => { - const enrollArgs = `--kibana-url=${kibanaUrl} --enrollment-token=${apiKey.api_key}${ - kibanaCASha256 ? ` --ca_sha256=${kibanaCASha256}` : '' - }`; + const fleetServerHostsNotEmpty = fleetServerHosts.length > 0; + + const enrollArgs = fleetServerHostsNotEmpty + ? getfleetServerHostsEnrollArgs(apiKey, fleetServerHosts) + : // TODO remove as part of https://github.com/elastic/kibana/issues/94303 + getKibanaUrlEnrollArgs(apiKey, kibanaUrl, kibanaCASha256); const linuxMacCommand = `./elastic-agent install -f ${enrollArgs}`; diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout.tsx deleted file mode 100644 index 146f40cd75d49..0000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout.tsx +++ /dev/null @@ -1,272 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useEffect } from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiTitle, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiSpacer, - EuiButton, - EuiFlyoutFooter, - EuiForm, - EuiFormRow, - EuiComboBox, - EuiCodeEditor, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiText } from '@elastic/eui'; -import { safeLoad } from 'js-yaml'; - -import { - useComboInput, - useStartServices, - useGetSettings, - useInput, - sendPutSettings, -} from '../hooks'; -import { useGetOutputs, sendPutOutput } from '../hooks/use_request/outputs'; -import { isDiffPathProtocol } from '../../../../common/'; - -const URL_REGEX = /^(https?):\/\/[^\s$.?#].[^\s]*$/gm; - -interface Props { - onClose: () => void; -} - -function useSettingsForm(outputId: string | undefined, onSuccess: () => void) { - const [isLoading, setIsloading] = React.useState(false); - const { notifications } = useStartServices(); - const kibanaUrlsInput = useComboInput([], (value) => { - if (value.length === 0) { - return [ - i18n.translate('xpack.fleet.settings.kibanaUrlEmptyError', { - defaultMessage: 'At least one URL is required', - }), - ]; - } - if (value.some((v) => !v.match(URL_REGEX))) { - return [ - i18n.translate('xpack.fleet.settings.kibanaUrlError', { - defaultMessage: 'Invalid URL', - }), - ]; - } - if (isDiffPathProtocol(value)) { - return [ - i18n.translate('xpack.fleet.settings.kibanaUrlDifferentPathOrProtocolError', { - defaultMessage: 'Protocol and path must be the same for each URL', - }), - ]; - } - }); - const elasticsearchUrlInput = useComboInput([], (value) => { - if (value.some((v) => !v.match(URL_REGEX))) { - return [ - i18n.translate('xpack.fleet.settings.elasticHostError', { - defaultMessage: 'Invalid URL', - }), - ]; - } - }); - - const additionalYamlConfigInput = useInput('', (value) => { - try { - safeLoad(value); - return; - } catch (error) { - return [ - i18n.translate('xpack.fleet.settings.invalidYamlFormatErrorMessage', { - defaultMessage: 'Invalid YAML: {reason}', - values: { reason: error.message }, - }), - ]; - } - }); - return { - isLoading, - onSubmit: async () => { - if ( - !kibanaUrlsInput.validate() || - !elasticsearchUrlInput.validate() || - !additionalYamlConfigInput.validate() - ) { - return; - } - - try { - setIsloading(true); - if (!outputId) { - throw new Error('Unable to load outputs'); - } - const outputResponse = await sendPutOutput(outputId, { - hosts: elasticsearchUrlInput.value, - config_yaml: additionalYamlConfigInput.value, - }); - if (outputResponse.error) { - throw outputResponse.error; - } - const settingsResponse = await sendPutSettings({ - kibana_urls: kibanaUrlsInput.value, - }); - if (settingsResponse.error) { - throw settingsResponse.error; - } - notifications.toasts.addSuccess( - i18n.translate('xpack.fleet.settings.success.message', { - defaultMessage: 'Settings saved', - }) - ); - setIsloading(false); - onSuccess(); - } catch (error) { - setIsloading(false); - notifications.toasts.addError(error, { - title: 'Error', - }); - } - }, - inputs: { - kibanaUrls: kibanaUrlsInput, - elasticsearchUrl: elasticsearchUrlInput, - additionalYamlConfig: additionalYamlConfigInput, - }, - }; -} - -export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { - const settingsRequest = useGetSettings(); - const settings = settingsRequest?.data?.item; - const outputsRequest = useGetOutputs(); - const output = outputsRequest.data?.items?.[0]; - const { inputs, onSubmit, isLoading } = useSettingsForm(output?.id, onClose); - - useEffect(() => { - if (output) { - inputs.elasticsearchUrl.setValue(output.hosts || []); - inputs.additionalYamlConfig.setValue( - output.config_yaml || - `# YAML settings here will be added to the Elasticsearch output section of each policy` - ); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [output]); - - useEffect(() => { - if (settings) { - inputs.kibanaUrls.setValue(settings.kibana_urls); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [settings]); - - const body = ( - - -

- -

-
- - - - - - - - - - - - - - - - - - - - - - -
- ); - - return ( - - - -

- -

-
-
- {body} - - - - - - - - - - - - - - -
- ); -}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout/confirm_modal.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout/confirm_modal.tsx new file mode 100644 index 0000000000000..8bef32916452f --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout/confirm_modal.tsx @@ -0,0 +1,188 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalFooter, + EuiModalBody, + EuiCallOut, + EuiButton, + EuiButtonEmpty, + EuiBasicTable, + EuiText, + EuiSpacer, +} from '@elastic/eui'; +import type { EuiBasicTableProps } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export interface SettingsConfirmModalProps { + changes: Array<{ + type: 'elasticsearch' | 'fleet_server'; + direction: 'removed' | 'added'; + urls: string[]; + }>; + onConfirm: () => void; + onClose: () => void; +} + +type Change = SettingsConfirmModalProps['changes'][0]; + +const TABLE_COLUMNS: EuiBasicTableProps['columns'] = [ + { + name: i18n.translate('xpack.fleet.settingsConfirmModal.fieldLabel', { + defaultMessage: 'Field', + }), + field: 'label', + render: (_, item) => getLabel(item), + width: '180px', + }, + { + field: 'urls', + name: i18n.translate('xpack.fleet.settingsConfirmModal.valueLabel', { + defaultMessage: 'Value', + }), + render: (_, item) => { + return ( + + {item.urls.map((url) => ( +
{url}
+ ))} +
+ ); + }, + }, +]; + +function getLabel(change: Change) { + if (change.type === 'elasticsearch' && change.direction === 'removed') { + return i18n.translate('xpack.fleet.settingsConfirmModal.elasticsearchRemovedLabel', { + defaultMessage: 'Elasticsearch hosts (old)', + }); + } + + if (change.type === 'elasticsearch' && change.direction === 'added') { + return i18n.translate('xpack.fleet.settingsConfirmModal.elasticsearchAddedLabel', { + defaultMessage: 'Elasticsearch hosts (new)', + }); + } + + if (change.type === 'fleet_server' && change.direction === 'removed') { + return i18n.translate('xpack.fleet.settingsConfirmModal.fleetServerRemovedLabel', { + defaultMessage: 'Fleet Server hosts (old)', + }); + } + + if (change.type === 'fleet_server' && change.direction === 'added') { + return i18n.translate('xpack.fleet.settingsConfirmModal.fleetServerAddedLabel', { + defaultMessage: 'Fleet Server hosts (new)', + }); + } + + return i18n.translate('xpack.fleet.settingsConfirmModal.defaultChangeLabel', { + defaultMessage: 'Unknown setting', + }); +} + +export const SettingsConfirmModal = React.memo( + ({ changes, onConfirm, onClose }) => { + const hasESChanges = changes.some((change) => change.type === 'elasticsearch'); + const hasFleetServerChanges = changes.some((change) => change.type === 'fleet_server'); + + return ( + + + + + + + + + + } + color="warning" + iconType="alert" + > + + {hasFleetServerChanges && ( +

+ + + + ), + }} + /> +

+ )} + + {hasESChanges && ( +

+ + + + ), + }} + /> +

+ )} +
+
+ + {changes.length > 0 && ( + <> + + + + )} +
+ + + + + + + + + +
+ ); + } +); diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout/index.tsx new file mode 100644 index 0000000000000..faf8707f2efc1 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout/index.tsx @@ -0,0 +1,439 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiSpacer, + EuiButton, + EuiFlyoutFooter, + EuiForm, + EuiFormRow, + EuiComboBox, + EuiCode, + EuiCodeEditor, + EuiLink, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiText } from '@elastic/eui'; +import { safeLoad } from 'js-yaml'; + +import { + useComboInput, + useStartServices, + useGetSettings, + useInput, + sendPutSettings, +} from '../../hooks'; +import { useGetOutputs, sendPutOutput } from '../../hooks/use_request/outputs'; +import { isDiffPathProtocol } from '../../../../../common/'; + +import { SettingsConfirmModal } from './confirm_modal'; +import type { SettingsConfirmModalProps } from './confirm_modal'; + +const URL_REGEX = /^(https?):\/\/[^\s$.?#].[^\s]*$/gm; + +interface Props { + onClose: () => void; +} + +function isSameArrayValue(arrayA: string[] = [], arrayB: string[] = []) { + return arrayA.length === arrayB.length && arrayA.every((val, index) => val === arrayB[index]); +} + +function useSettingsForm(outputId: string | undefined, onSuccess: () => void) { + const [isLoading, setIsloading] = React.useState(false); + const { notifications } = useStartServices(); + const kibanaUrlsInput = useComboInput([], (value) => { + if (value.length === 0) { + return [ + i18n.translate('xpack.fleet.settings.kibanaUrlEmptyError', { + defaultMessage: 'At least one URL is required', + }), + ]; + } + if (value.some((v) => !v.match(URL_REGEX))) { + return [ + i18n.translate('xpack.fleet.settings.kibanaUrlError', { + defaultMessage: 'Invalid URL', + }), + ]; + } + if (isDiffPathProtocol(value)) { + return [ + i18n.translate('xpack.fleet.settings.kibanaUrlDifferentPathOrProtocolError', { + defaultMessage: 'Protocol and path must be the same for each URL', + }), + ]; + } + }); + const fleetServerHostsInput = useComboInput([], (value) => { + // TODO enable as part of https://github.com/elastic/kibana/issues/94303 + // if (value.length === 0) { + // return [ + // i18n.translate('xpack.fleet.settings.fleetServerHostsEmptyError', { + // defaultMessage: 'At least one URL is required', + // }), + // ]; + // } + if (value.some((v) => !v.match(URL_REGEX))) { + return [ + i18n.translate('xpack.fleet.settings.fleetServerHostsError', { + defaultMessage: 'Invalid URL', + }), + ]; + } + if (value.length && isDiffPathProtocol(value)) { + return [ + i18n.translate('xpack.fleet.settings.fleetServerHostsDifferentPathOrProtocolError', { + defaultMessage: 'Protocol and path must be the same for each URL', + }), + ]; + } + }); + + const elasticsearchUrlInput = useComboInput([], (value) => { + if (value.some((v) => !v.match(URL_REGEX))) { + return [ + i18n.translate('xpack.fleet.settings.elasticHostError', { + defaultMessage: 'Invalid URL', + }), + ]; + } + }); + + const additionalYamlConfigInput = useInput('', (value) => { + try { + safeLoad(value); + return; + } catch (error) { + return [ + i18n.translate('xpack.fleet.settings.invalidYamlFormatErrorMessage', { + defaultMessage: 'Invalid YAML: {reason}', + values: { reason: error.message }, + }), + ]; + } + }); + + const validate = useCallback(() => { + if ( + !kibanaUrlsInput.validate() || + !fleetServerHostsInput.validate() || + !elasticsearchUrlInput.validate() || + !additionalYamlConfigInput.validate() + ) { + return false; + } + + return true; + }, [kibanaUrlsInput, fleetServerHostsInput, elasticsearchUrlInput, additionalYamlConfigInput]); + + return { + isLoading, + validate, + submit: async () => { + try { + setIsloading(true); + if (!outputId) { + throw new Error('Unable to load outputs'); + } + const outputResponse = await sendPutOutput(outputId, { + hosts: elasticsearchUrlInput.value, + config_yaml: additionalYamlConfigInput.value, + }); + if (outputResponse.error) { + throw outputResponse.error; + } + const settingsResponse = await sendPutSettings({ + kibana_urls: kibanaUrlsInput.value, + fleet_server_hosts: fleetServerHostsInput.value, + }); + if (settingsResponse.error) { + throw settingsResponse.error; + } + notifications.toasts.addSuccess( + i18n.translate('xpack.fleet.settings.success.message', { + defaultMessage: 'Settings saved', + }) + ); + setIsloading(false); + onSuccess(); + } catch (error) { + setIsloading(false); + notifications.toasts.addError(error, { + title: 'Error', + }); + } + }, + inputs: { + fleetServerHosts: fleetServerHostsInput, + kibanaUrls: kibanaUrlsInput, + elasticsearchUrl: elasticsearchUrlInput, + additionalYamlConfig: additionalYamlConfigInput, + }, + }; +} + +export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { + const settingsRequest = useGetSettings(); + const settings = settingsRequest?.data?.item; + const outputsRequest = useGetOutputs(); + const output = outputsRequest.data?.items?.[0]; + const { inputs, submit, validate, isLoading } = useSettingsForm(output?.id, onClose); + + const [isConfirmModalVisible, setConfirmModalVisible] = React.useState(false); + + const onSubmit = useCallback(() => { + if (validate()) { + setConfirmModalVisible(true); + } + }, [validate, setConfirmModalVisible]); + + const onConfirm = useCallback(() => { + setConfirmModalVisible(false); + submit(); + }, [submit]); + + const onConfirmModalClose = useCallback(() => { + setConfirmModalVisible(false); + }, [setConfirmModalVisible]); + + useEffect(() => { + if (output) { + inputs.elasticsearchUrl.setValue(output.hosts || []); + inputs.additionalYamlConfig.setValue(output.config_yaml || ''); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [output]); + + useEffect(() => { + if (settings) { + inputs.kibanaUrls.setValue([...settings.kibana_urls]); + inputs.fleetServerHosts.setValue([...settings.fleet_server_hosts]); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [settings]); + + const isUpdated = React.useMemo(() => { + if (!settings || !output) { + return false; + } + return ( + !isSameArrayValue(settings.kibana_urls, inputs.kibanaUrls.value) || + !isSameArrayValue(settings.fleet_server_hosts, inputs.fleetServerHosts.value) || + !isSameArrayValue(output.hosts, inputs.elasticsearchUrl.value) || + (output.config_yaml || '') !== inputs.additionalYamlConfig.value + ); + }, [settings, inputs, output]); + + const changes = React.useMemo(() => { + if (!settings || !output || !isConfirmModalVisible) { + return []; + } + + const tmpChanges: SettingsConfirmModalProps['changes'] = []; + if (!isSameArrayValue(output.hosts, inputs.elasticsearchUrl.value)) { + tmpChanges.push( + { + type: 'elasticsearch', + direction: 'removed', + urls: output.hosts || [], + }, + { + type: 'elasticsearch', + direction: 'added', + urls: inputs.elasticsearchUrl.value, + } + ); + } + + if (!isSameArrayValue(settings.fleet_server_hosts, inputs.fleetServerHosts.value)) { + tmpChanges.push( + { + type: 'fleet_server', + direction: 'removed', + urls: settings.fleet_server_hosts, + }, + { + type: 'fleet_server', + direction: 'added', + urls: inputs.fleetServerHosts.value, + } + ); + } + + return tmpChanges; + }, [settings, inputs, output, isConfirmModalVisible]); + + const body = settings && ( + + +

+ +

+
+ + + outputs, + }} + /> + + + + + + + ), + }} + /> + } + {...inputs.fleetServerHosts.formRowProps} + > + + + + + {/* // TODO remove as part of https://github.com/elastic/kibana/issues/94303 */} + + + + + + + + + + + +
+ ); + + return ( + <> + {isConfirmModalVisible && ( + + )} + + + +

+ +

+
+
+ {body} + + + + + + + + + + {isLoading ? ( + + ) : ( + + )} + + + + +
+ + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/layouts/default.tsx b/x-pack/plugins/fleet/public/applications/fleet/layouts/default.tsx index ba6367a861e9d..4a7e738ec540a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/layouts/default.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/layouts/default.tsx @@ -125,7 +125,7 @@ export const DefaultLayout: React.FunctionComponent = ({ setIsSettingsFlyoutOpen(true)}> diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/managed_instructions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/managed_instructions.tsx index 4925f60f19e26..0ca6b223b3492 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/managed_instructions.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/managed_instructions.tsx @@ -56,6 +56,7 @@ export const ManagedInstructions = React.memo(({ agentPolicies }) => { apiKey={apiKey.data.item} kibanaUrl={kibanaUrl} kibanaCASha256={kibanaCASha256} + fleetServerHosts={settings.data?.item?.fleet_server_hosts || []} /> ), }, diff --git a/x-pack/plugins/fleet/server/index.ts b/x-pack/plugins/fleet/server/index.ts index 8bad868b813ac..0178b801f4d2f 100644 --- a/x-pack/plugins/fleet/server/index.ts +++ b/x-pack/plugins/fleet/server/index.ts @@ -65,6 +65,11 @@ export const config: PluginConfigDescriptor = { host: schema.maybe(schema.string()), ca_sha256: schema.maybe(schema.string()), }), + fleet_server: schema.maybe( + schema.object({ + hosts: schema.maybe(schema.arrayOf(schema.uri({ scheme: ['http', 'https'] }))), + }) + ), agentPolicyRolloutRateLimitIntervalMs: schema.number({ defaultValue: AGENT_POLICY_ROLLOUT_RATE_LIMIT_INTERVAL_MS, }), diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index e5f0537a8c27a..87ca9782ab698 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -58,9 +58,11 @@ const getSavedObjectTypes = ( }, mappings: { properties: { + fleet_server_hosts: { type: 'keyword' }, + has_seen_add_data_notice: { type: 'boolean', index: false }, + // TODO remove as part of https://github.com/elastic/kibana/issues/94303 kibana_urls: { type: 'keyword' }, kibana_ca_sha256: { type: 'keyword' }, - has_seen_add_data_notice: { type: 'boolean', index: false }, }, }, migrations: { diff --git a/x-pack/plugins/fleet/server/services/agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policy.test.ts index 515d2b1195638..56e76130538cf 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.test.ts @@ -171,6 +171,7 @@ describe('agent policy', () => { inputs: [], revision: 1, fleet: { + hosts: ['http://localhost:5603'], kibana: { hosts: ['localhost:5603'], protocol: 'http', @@ -206,6 +207,7 @@ describe('agent policy', () => { inputs: [], revision: 1, fleet: { + hosts: ['http://localhost:5603'], kibana: { hosts: ['localhost:5603'], protocol: 'http', @@ -242,6 +244,7 @@ describe('agent policy', () => { inputs: [], revision: 1, fleet: { + hosts: ['http://localhost:5603'], kibana: { hosts: ['localhost:5603'], protocol: 'http', diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 2cafe2fe57c01..357b9150407ef 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -706,12 +706,20 @@ class AgentPolicyService { } catch (error) { throw new Error('Default settings is not setup'); } - if (!settings.kibana_urls || !settings.kibana_urls.length) - throw new Error('kibana_urls is missing'); - - fullAgentPolicy.fleet = { - kibana: getFullAgentPolicyKibanaConfig(settings.kibana_urls), - }; + if (settings.fleet_server_hosts && settings.fleet_server_hosts.length) { + fullAgentPolicy.fleet = { + hosts: settings.fleet_server_hosts, + }; + } // TODO remove as part of https://github.com/elastic/kibana/issues/94303 + else { + if (!settings.kibana_urls || !settings.kibana_urls.length) + throw new Error('kibana_urls is missing'); + + fullAgentPolicy.fleet = { + hosts: settings.kibana_urls, + kibana: getFullAgentPolicyKibanaConfig(settings.kibana_urls), + }; + } } return fullAgentPolicy; } diff --git a/x-pack/plugins/fleet/server/services/settings.ts b/x-pack/plugins/fleet/server/services/settings.ts index 03348a2fcc4bb..7658a8d71839e 100644 --- a/x-pack/plugins/fleet/server/services/settings.ts +++ b/x-pack/plugins/fleet/server/services/settings.ts @@ -27,6 +27,7 @@ export async function getSettings(soClient: SavedObjectsClientContract): Promise return { id: settingsSo.id, ...settingsSo.attributes, + fleet_server_hosts: settingsSo.attributes.fleet_server_hosts || [], }; } @@ -81,7 +82,10 @@ export function createDefaultSettings(): BaseSettings { pathname: basePath.serverBasePath, }); + const fleetServerHosts = appContextService.getConfig()?.agents?.fleet_server?.hosts ?? []; + return { kibana_urls: [cloudUrl || flagsUrl || defaultUrl].flat(), + fleet_server_hosts: fleetServerHosts, }; } diff --git a/x-pack/plugins/fleet/server/types/rest_spec/settings.ts b/x-pack/plugins/fleet/server/types/rest_spec/settings.ts index 9bbebbe86ccaa..9051d7a06efff 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/settings.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/settings.ts @@ -13,6 +13,15 @@ export const GetSettingsRequestSchema = {}; export const PutSettingsRequestSchema = { body: schema.object({ + fleet_server_hosts: schema.maybe( + schema.arrayOf(schema.uri({ scheme: ['http', 'https'] }), { + validate: (value) => { + if (value.length && isDiffPathProtocol(value)) { + return 'Protocol and path must be the same for each URL'; + } + }, + }) + ), kibana_urls: schema.maybe( schema.arrayOf(schema.uri({ scheme: ['http', 'https'] }), { validate: (value) => { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 4d2cf86c4e7d2..7fa5e66149eca 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8702,7 +8702,6 @@ "xpack.fleet.settings.elasticHostError": "無効なURL", "xpack.fleet.settings.elasticsearchUrlLabel": "Elasticsearch URL", "xpack.fleet.settings.flyoutTitle": "Fleet 設定", - "xpack.fleet.settings.globalOutputDescription": "データを送信する場所を指定します。これらの設定はすべてのElasticエージェントポリシーに適用されます。", "xpack.fleet.settings.globalOutputTitle": "グローバル出力", "xpack.fleet.settings.invalidYamlFormatErrorMessage": "無効なYAML形式:{reason}", "xpack.fleet.settings.kibanaUrlDifferentPathOrProtocolError": "各URLのプロトコルとパスは同じでなければなりません", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 10a01be56102c..af21da2b5f765 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8783,7 +8783,6 @@ "xpack.fleet.settings.elasticHostError": "URL 无效", "xpack.fleet.settings.elasticsearchUrlLabel": "Elasticsearch URL", "xpack.fleet.settings.flyoutTitle": "Fleet 设置", - "xpack.fleet.settings.globalOutputDescription": "指定将数据发送到何处。这些设置将应用于所有的 Elastic 代理策略。", "xpack.fleet.settings.globalOutputTitle": "全局输出", "xpack.fleet.settings.invalidYamlFormatErrorMessage": "YAML 无效:{reason}", "xpack.fleet.settings.kibanaUrlDifferentPathOrProtocolError": "对于每个 URL,协议和路径必须相同",