[Fleet] add support for fleet server urls (#94364)

This commit is contained in:
Nicolas Chaulet 2021-03-26 14:53:35 -04:00 committed by GitHub
parent d89ede9834
commit db7da2238e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 701 additions and 288 deletions

View file

@ -26,6 +26,9 @@ export interface FleetConfigType {
host?: string;
ca_sha256?: string;
};
fleet_server?: {
hosts?: string[];
};
agentPolicyRolloutRateLimitIntervalMs: number;
agentPolicyRolloutRateLimitRequestPerInterval: number;
};

View file

@ -66,9 +66,13 @@ export interface FullAgentPolicy {
[key: string]: any;
};
};
fleet?: {
kibana: FullAgentPolicyKibanaConfig;
};
fleet?:
| {
hosts: string[];
}
| {
kibana: FullAgentPolicyKibanaConfig;
};
inputs: FullAgentPolicyInput[];
revision?: number;
agent?: {

View file

@ -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 {

View file

@ -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<Props> = ({
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}`;

View file

@ -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<Props> = ({ 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 = (
<EuiForm>
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.fleet.settings.globalOutputTitle"
defaultMessage="Global output"
/>
</h3>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText color="subdued" size="s">
<FormattedMessage
id="xpack.fleet.settings.globalOutputDescription"
defaultMessage="Specify where to send data. These settings are applied to all Elastic Agent policies."
/>
</EuiText>
<EuiSpacer size="m" />
<EuiFormRow>
<EuiFormRow
label={i18n.translate('xpack.fleet.settings.kibanaUrlLabel', {
defaultMessage: 'Kibana URL',
})}
{...inputs.kibanaUrls.formRowProps}
>
<EuiComboBox noSuggestions {...inputs.kibanaUrls.props} />
</EuiFormRow>
</EuiFormRow>
<EuiSpacer size="m" />
<EuiFormRow>
<EuiFormRow
label={i18n.translate('xpack.fleet.settings.elasticsearchUrlLabel', {
defaultMessage: 'Elasticsearch URL',
})}
{...inputs.elasticsearchUrl.formRowProps}
>
<EuiComboBox noSuggestions {...inputs.elasticsearchUrl.props} />
</EuiFormRow>
</EuiFormRow>
<EuiSpacer size="m" />
<EuiFormRow fullWidth>
<EuiFormRow
{...inputs.additionalYamlConfig.formRowProps}
label={i18n.translate('xpack.fleet.settings.additionalYamlConfig', {
defaultMessage: 'Elasticsearch output configuration',
})}
fullWidth={true}
>
<EuiCodeEditor
width="100%"
mode="yaml"
theme="textmate"
setOptions={{
minLines: 10,
maxLines: 30,
tabSize: 2,
showGutter: false,
}}
{...inputs.additionalYamlConfig.props}
onChange={inputs.additionalYamlConfig.setValue}
/>
</EuiFormRow>
</EuiFormRow>
</EuiForm>
);
return (
<EuiFlyout onClose={onClose} size="l" maxWidth={640}>
<EuiFlyoutHeader hasBorder aria-labelledby="IngestManagerSettingsFlyoutTitle">
<EuiTitle size="m">
<h2 id="IngestManagerSettingsFlyoutTitle">
<FormattedMessage
id="xpack.fleet.settings.flyoutTitle"
defaultMessage="Fleet settings"
/>
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>{body}</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={onClose} flush="left">
<FormattedMessage
id="xpack.fleet.settings.cancelButtonLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton onClick={onSubmit} iconType="save" isLoading={isLoading}>
<FormattedMessage
id="xpack.fleet.settings.saveButtonLabel"
defaultMessage="Save settings"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
);
};

View file

@ -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<Change>['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 (
<EuiText size="s" color={item.direction === 'added' ? 'secondary' : 'danger'}>
{item.urls.map((url) => (
<div key={url}>{url}</div>
))}
</EuiText>
);
},
},
];
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<SettingsConfirmModalProps>(
({ changes, onConfirm, onClose }) => {
const hasESChanges = changes.some((change) => change.type === 'elasticsearch');
const hasFleetServerChanges = changes.some((change) => change.type === 'fleet_server');
return (
<EuiModal maxWidth={true} onClose={onClose}>
<EuiModalHeader>
<EuiModalHeaderTitle>
<FormattedMessage
id="xpack.fleet.settingsConfirmModal.title"
defaultMessage="Apply settings to all agent policies"
/>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiCallOut
title={
<FormattedMessage
id="xpack.fleet.settingsConfirmModal.calloutTitle"
defaultMessage="This action will update all agent policies and enrolled agents."
/>
}
color="warning"
iconType="alert"
>
<EuiText size="s">
{hasFleetServerChanges && (
<p>
<FormattedMessage
id="xpack.fleet.settingsConfirmModal.fleetServerChangedText"
defaultMessage="If agents are unable to connect at the new {fleetServerUrl}, they will log an error and report an unhealthy status. They will remain on the current agent policy version and check for updates at the old URL until they successfully connect at the new URL"
values={{
fleetServerUrl: (
<strong>
<FormattedMessage
id="xpack.fleet.settingsConfirmModal.fleetServerUrl"
defaultMessage="Fleet Server URL"
/>
</strong>
),
}}
/>
</p>
)}
{hasESChanges && (
<p>
<FormattedMessage
id="xpack.fleet.settingsConfirmModal.eserverChangedText"
defaultMessage="If agents are unable to connect at the new {elasticsearchUrl}, Fleet Server will report them as healthy but they will be unable to send data to Elasticsearch. This will not update URL that Fleet server itself uses to connect to Elasticsearch; you must manually reenroll it to update the URL"
values={{
elasticsearchUrl: (
<strong>
<FormattedMessage
id="xpack.fleet.settingsConfirmModal.elasticsearchUrl"
defaultMessage="Elasticsearch URL"
/>
</strong>
),
}}
/>
</p>
)}
</EuiText>
</EuiCallOut>
{changes.length > 0 && (
<>
<EuiSpacer size="m" />
<EuiBasicTable tableLayout="auto" columns={TABLE_COLUMNS} items={changes} />
</>
)}
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty onClick={onClose}>
<FormattedMessage
id="xpack.fleet.settingsConfirmModal.cancelButton"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
<EuiButton onClick={onConfirm} fill>
<FormattedMessage
id="xpack.fleet.settingsConfirmModal.confirmButton"
defaultMessage="Confirm changes"
/>
</EuiButton>
</EuiModalFooter>
</EuiModal>
);
}
);

View file

@ -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<Props> = ({ 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 && (
<EuiForm>
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.fleet.settings.globalOutputTitle"
defaultMessage="Global output"
/>
</h3>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText color="subdued" size="s">
<FormattedMessage
id="xpack.fleet.settings.globalOutputDescription"
defaultMessage="These settings are applied globally to the {outputs} section of all agent policies and will affect all enrolled agents if changed."
values={{
outputs: <EuiCode>outputs</EuiCode>,
}}
/>
</EuiText>
<EuiSpacer size="m" />
<EuiFormRow
fullWidth
label={i18n.translate('xpack.fleet.settings.fleetServerHostsLabel', {
defaultMessage: 'Fleet Server hosts',
})}
helpText={
<FormattedMessage
id="xpack.fleet.settings.fleetServerHostsHelpTect"
defaultMessage="Specify the URLs that your agents will use to connect to a Fleet Server. If multiple URLs exist, Fleet will show the first provided URL for enrollment purposes. For more information, see the {link}."
values={{
link: (
<EuiLink
href="https://www.elastic.co/guide/en/fleet/current/index.html"
target="_blank"
external
>
<FormattedMessage
id="xpack.fleet.settings.userGuideLink"
defaultMessage="Fleet User Guide"
/>
</EuiLink>
),
}}
/>
}
{...inputs.fleetServerHosts.formRowProps}
>
<EuiComboBox fullWidth noSuggestions {...inputs.fleetServerHosts.props} />
</EuiFormRow>
<EuiSpacer size="m" />
{/* // TODO remove as part of https://github.com/elastic/kibana/issues/94303 */}
<EuiFormRow
fullWidth
label={i18n.translate('xpack.fleet.settings.kibanaUrlLabel', {
defaultMessage: 'Kibana hosts',
})}
{...inputs.kibanaUrls.formRowProps}
>
<EuiComboBox fullWidth noSuggestions {...inputs.kibanaUrls.props} />
</EuiFormRow>
<EuiSpacer size="m" />
<EuiFormRow
fullWidth
label={i18n.translate('xpack.fleet.settings.elasticsearchUrlLabel', {
defaultMessage: 'Elasticsearch hosts',
})}
helpText={i18n.translate('xpack.fleet.settings.elasticsearchUrlsHelpTect', {
defaultMessage: 'Specify the Elasticsearch URLs where agents will send data.',
})}
{...inputs.elasticsearchUrl.formRowProps}
>
<EuiComboBox fullWidth noSuggestions {...inputs.elasticsearchUrl.props} />
</EuiFormRow>
<EuiSpacer size="m" />
<EuiFormRow
{...inputs.additionalYamlConfig.formRowProps}
label={i18n.translate('xpack.fleet.settings.additionalYamlConfig', {
defaultMessage: 'Elasticsearch output configuration (YAML)',
})}
fullWidth
>
<EuiCodeEditor
width="100%"
mode="yaml"
theme="textmate"
placeholder="# YAML settings here will be added to the Elasticsearch output section of each policy"
setOptions={{
minLines: 10,
maxLines: 30,
tabSize: 2,
showGutter: false,
}}
{...inputs.additionalYamlConfig.props}
onChange={inputs.additionalYamlConfig.setValue}
/>
</EuiFormRow>
</EuiForm>
);
return (
<>
{isConfirmModalVisible && (
<SettingsConfirmModal
changes={changes}
onConfirm={onConfirm}
onClose={onConfirmModalClose}
/>
)}
<EuiFlyout onClose={onClose} size="l" maxWidth={640}>
<EuiFlyoutHeader hasBorder aria-labelledby="IngestManagerSettingsFlyoutTitle">
<EuiTitle size="m">
<h2 id="IngestManagerSettingsFlyoutTitle">
<FormattedMessage
id="xpack.fleet.settings.flyoutTitle"
defaultMessage="Fleet settings"
/>
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>{body}</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={onClose} flush="left">
<FormattedMessage
id="xpack.fleet.settings.cancelButtonLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
disabled={!isUpdated}
onClick={onSubmit}
isLoading={isLoading}
color="primary"
fill
>
{isLoading ? (
<FormattedMessage
id="xpack.fleet.settings.saveButtonLoadingLabel"
defaultMessage="Applying settings..."
/>
) : (
<FormattedMessage
id="xpack.fleet.settings.saveButtonLabel"
defaultMessage="Save and apply settings"
/>
)}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
</>
);
};

View file

@ -125,7 +125,7 @@ export const DefaultLayout: React.FunctionComponent<Props> = ({
<EuiButtonEmpty iconType="gear" onClick={() => setIsSettingsFlyoutOpen(true)}>
<FormattedMessage
id="xpack.fleet.appNavigation.settingsButton"
defaultMessage="Settings"
defaultMessage="Fleet settings"
/>
</EuiButtonEmpty>
</EuiFlexItem>

View file

@ -56,6 +56,7 @@ export const ManagedInstructions = React.memo<Props>(({ agentPolicies }) => {
apiKey={apiKey.data.item}
kibanaUrl={kibanaUrl}
kibanaCASha256={kibanaCASha256}
fleetServerHosts={settings.data?.item?.fleet_server_hosts || []}
/>
),
},

View file

@ -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,
}),

View file

@ -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: {

View file

@ -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',

View file

@ -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');
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 = {
kibana: getFullAgentPolicyKibanaConfig(settings.kibana_urls),
};
fullAgentPolicy.fleet = {
hosts: settings.kibana_urls,
kibana: getFullAgentPolicyKibanaConfig(settings.kibana_urls),
};
}
}
return fullAgentPolicy;
}

View file

@ -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,
};
}

View file

@ -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) => {

View file

@ -8716,7 +8716,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のプロトコルとパスは同じでなければなりません",

View file

@ -8798,7 +8798,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协议和路径必须相同",