[Fleet] Fleet server onboarding UI (#96867)
This commit is contained in:
parent
1cd11d4deb
commit
b96172e27c
|
@ -27,6 +27,8 @@ export const FLEET_SERVER_INDICES_VERSION = 1;
|
|||
|
||||
export const FLEET_SERVER_ARTIFACTS_INDEX = '.fleet-artifacts';
|
||||
|
||||
export const FLEET_SERVER_SERVERS_INDEX = '.fleet-servers';
|
||||
|
||||
export const FLEET_SERVER_INDICES = [
|
||||
'.fleet-actions',
|
||||
'.fleet-agents',
|
||||
|
@ -34,5 +36,5 @@ export const FLEET_SERVER_INDICES = [
|
|||
'.fleet-enrollment-api-keys',
|
||||
'.fleet-policies',
|
||||
'.fleet-policies-leader',
|
||||
'.fleet-servers',
|
||||
FLEET_SERVER_SERVERS_INDEX,
|
||||
];
|
||||
|
|
|
@ -15,6 +15,7 @@ export interface GetFleetStatusResponse {
|
|||
| 'tls_required'
|
||||
| 'api_keys'
|
||||
| 'fleet_admin_user'
|
||||
| 'fleet_server'
|
||||
| 'encrypted_saved_object_encryption_key_required'
|
||||
>;
|
||||
}
|
||||
|
|
|
@ -14,9 +14,7 @@ import type { EnrollmentAPIKey } from '../../../types';
|
|||
|
||||
interface Props {
|
||||
fleetServerHosts: string[];
|
||||
kibanaUrl: string;
|
||||
apiKey: EnrollmentAPIKey;
|
||||
kibanaCASha256?: string;
|
||||
}
|
||||
|
||||
// Otherwise the copy button is over the text
|
||||
|
@ -28,28 +26,11 @@ function getfleetServerHostsEnrollArgs(apiKey: EnrollmentAPIKey, fleetServerHost
|
|||
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 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 enrollArgs = getfleetServerHostsEnrollArgs(apiKey, fleetServerHosts);
|
||||
|
||||
const linuxMacCommand = `./elastic-agent install -f ${enrollArgs}`;
|
||||
|
||||
|
|
|
@ -55,38 +55,15 @@ function isSameArrayValue(arrayA: string[] = [], arrayB: string[] = []) {
|
|||
function useSettingsForm(outputId: string | undefined, onSuccess: () => void) {
|
||||
const [isLoading, setIsloading] = React.useState(false);
|
||||
const { notifications } = useStartServices();
|
||||
const kibanaUrlsInput = useComboInput([], (value) => {
|
||||
|
||||
const fleetServerHostsInput = useComboInput([], (value) => {
|
||||
if (value.length === 0) {
|
||||
return [
|
||||
i18n.translate('xpack.fleet.settings.kibanaUrlEmptyError', {
|
||||
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.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', {
|
||||
|
@ -129,7 +106,6 @@ function useSettingsForm(outputId: string | undefined, onSuccess: () => void) {
|
|||
|
||||
const validate = useCallback(() => {
|
||||
if (
|
||||
!kibanaUrlsInput.validate() ||
|
||||
!fleetServerHostsInput.validate() ||
|
||||
!elasticsearchUrlInput.validate() ||
|
||||
!additionalYamlConfigInput.validate()
|
||||
|
@ -138,7 +114,7 @@ function useSettingsForm(outputId: string | undefined, onSuccess: () => void) {
|
|||
}
|
||||
|
||||
return true;
|
||||
}, [kibanaUrlsInput, fleetServerHostsInput, elasticsearchUrlInput, additionalYamlConfigInput]);
|
||||
}, [fleetServerHostsInput, elasticsearchUrlInput, additionalYamlConfigInput]);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
|
@ -157,7 +133,6 @@ function useSettingsForm(outputId: string | undefined, onSuccess: () => void) {
|
|||
throw outputResponse.error;
|
||||
}
|
||||
const settingsResponse = await sendPutSettings({
|
||||
kibana_urls: kibanaUrlsInput.value,
|
||||
fleet_server_hosts: fleetServerHostsInput.value,
|
||||
});
|
||||
if (settingsResponse.error) {
|
||||
|
@ -179,7 +154,6 @@ function useSettingsForm(outputId: string | undefined, onSuccess: () => void) {
|
|||
},
|
||||
inputs: {
|
||||
fleetServerHosts: fleetServerHostsInput,
|
||||
kibanaUrls: kibanaUrlsInput,
|
||||
elasticsearchUrl: elasticsearchUrlInput,
|
||||
additionalYamlConfig: additionalYamlConfigInput,
|
||||
},
|
||||
|
@ -220,7 +194,6 @@ export const SettingFlyout: React.FunctionComponent<Props> = ({ onClose }) => {
|
|||
|
||||
useEffect(() => {
|
||||
if (settings) {
|
||||
inputs.kibanaUrls.setValue([...settings.kibana_urls]);
|
||||
inputs.fleetServerHosts.setValue([...settings.fleet_server_hosts]);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
@ -231,7 +204,6 @@ export const SettingFlyout: React.FunctionComponent<Props> = ({ onClose }) => {
|
|||
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
|
||||
|
@ -329,17 +301,6 @@ export const SettingFlyout: React.FunctionComponent<Props> = ({ onClose }) => {
|
|||
<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
|
||||
|
|
|
@ -15,6 +15,7 @@ export {
|
|||
AGENT_SAVED_OBJECT_TYPE,
|
||||
ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE,
|
||||
PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
FLEET_SERVER_PACKAGE,
|
||||
// Fleet Server index
|
||||
AGENTS_INDEX,
|
||||
ENROLLMENT_API_KEYS_INDEX,
|
||||
|
|
|
@ -18,6 +18,7 @@ export { usePagination, Pagination, PAGE_SIZE_OPTIONS } from './use_pagination';
|
|||
export { useUrlPagination } from './use_url_pagination';
|
||||
export { useSorting } from './use_sorting';
|
||||
export { useDebounce } from './use_debounce';
|
||||
export { useUrlModal } from './use_url_modal';
|
||||
export * from './use_request';
|
||||
export * from './use_input';
|
||||
export * from './use_url_params';
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 { useCallback, useMemo } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
|
||||
import { useUrlParams } from './use_url_params';
|
||||
|
||||
type Modal = 'settings';
|
||||
|
||||
/**
|
||||
* Uses URL params for pagination and also persists those to the URL as they are updated
|
||||
*/
|
||||
export const useUrlModal = () => {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const { urlParams, toUrlParams } = useUrlParams();
|
||||
|
||||
const setModal = useCallback(
|
||||
(modal: Modal | null) => {
|
||||
const newUrlParams: any = {
|
||||
...urlParams,
|
||||
modal,
|
||||
};
|
||||
|
||||
if (modal === null) {
|
||||
delete newUrlParams.modal;
|
||||
}
|
||||
history.push({
|
||||
...location,
|
||||
search: toUrlParams(newUrlParams),
|
||||
});
|
||||
},
|
||||
[history, location, toUrlParams, urlParams]
|
||||
);
|
||||
|
||||
const getModalHref = useCallback(
|
||||
(modal: Modal | null) => {
|
||||
return history.createHref({
|
||||
...location,
|
||||
search: toUrlParams({
|
||||
...urlParams,
|
||||
modal,
|
||||
}),
|
||||
});
|
||||
},
|
||||
[history, location, toUrlParams, urlParams]
|
||||
);
|
||||
|
||||
const modal: Modal | null = useMemo(() => {
|
||||
if (urlParams.modal === 'settings') {
|
||||
return urlParams.modal;
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [urlParams.modal]);
|
||||
|
||||
return {
|
||||
modal,
|
||||
setModal,
|
||||
getModalHref,
|
||||
};
|
||||
};
|
|
@ -12,7 +12,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
|
|||
|
||||
import type { Section } from '../sections';
|
||||
import { AlphaMessaging, SettingFlyout } from '../components';
|
||||
import { useLink, useConfig } from '../hooks';
|
||||
import { useLink, useConfig, useUrlModal } from '../hooks';
|
||||
|
||||
interface Props {
|
||||
showSettings?: boolean;
|
||||
|
@ -53,17 +53,18 @@ export const DefaultLayout: React.FunctionComponent<Props> = ({
|
|||
}) => {
|
||||
const { getHref } = useLink();
|
||||
const { agents } = useConfig();
|
||||
const [isSettingsFlyoutOpen, setIsSettingsFlyoutOpen] = React.useState(false);
|
||||
const { modal, setModal, getModalHref } = useUrlModal();
|
||||
|
||||
return (
|
||||
<>
|
||||
{isSettingsFlyoutOpen && (
|
||||
{modal === 'settings' && (
|
||||
<SettingFlyout
|
||||
onClose={() => {
|
||||
setIsSettingsFlyoutOpen(false);
|
||||
setModal(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Container>
|
||||
<Wrapper>
|
||||
<Nav>
|
||||
|
@ -122,7 +123,7 @@ export const DefaultLayout: React.FunctionComponent<Props> = ({
|
|||
</EuiFlexItem>
|
||||
{showSettings ? (
|
||||
<EuiFlexItem>
|
||||
<EuiButtonEmpty iconType="gear" onClick={() => setIsSettingsFlyoutOpen(true)}>
|
||||
<EuiButtonEmpty iconType="gear" href={getModalHref('settings')}>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.appNavigation.settingsButton"
|
||||
defaultMessage="Fleet settings"
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiPageBody,
|
||||
EuiPageContent,
|
||||
EuiText,
|
||||
EuiSpacer,
|
||||
EuiIcon,
|
||||
EuiCallOut,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
EuiCode,
|
||||
EuiCodeBlock,
|
||||
EuiLink,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { WithoutHeaderLayout } from '../../../layouts';
|
||||
import type { GetFleetStatusResponse } from '../../../types';
|
||||
|
||||
export const RequirementItem: React.FunctionComponent<{ isMissing: boolean }> = ({
|
||||
isMissing,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="flexStart">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
{isMissing ? (
|
||||
<EuiIcon type="crossInACircleFilled" color="danger" />
|
||||
) : (
|
||||
<EuiIcon type="checkInCircleFilled" color="success" />
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>{children}</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export const MissingESRequirementsPage: React.FunctionComponent<{
|
||||
missingRequirements: GetFleetStatusResponse['missing_requirements'];
|
||||
}> = ({ missingRequirements }) => {
|
||||
return (
|
||||
<WithoutHeaderLayout>
|
||||
<EuiPageBody restrictWidth={820}>
|
||||
<EuiPageContent hasBorder={true} hasShadow={false}>
|
||||
<EuiCallOut
|
||||
title={i18n.translate('xpack.fleet.setupPage.missingRequirementsCalloutTitle', {
|
||||
defaultMessage: 'Missing security requirements',
|
||||
})}
|
||||
color="warning"
|
||||
iconType="alert"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.setupPage.missingRequirementsCalloutDescription"
|
||||
defaultMessage="To use central management for Elastic Agents, enable the following Elasticsearch security features."
|
||||
/>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="m" />
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.setupPage.missingRequirementsElasticsearchTitle"
|
||||
defaultMessage="In your Elasticsearch policy, enable:"
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
<RequirementItem isMissing={false}>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.setupPage.elasticsearchSecurityFlagText"
|
||||
defaultMessage="{esSecurityLink}. Set {securityFlag} to {true} ."
|
||||
values={{
|
||||
esSecurityLink: (
|
||||
<EuiLink
|
||||
href="https://www.elastic.co/guide/en/elasticsearch/reference/current/configuring-security.html"
|
||||
target="_blank"
|
||||
external
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.setupPage.elasticsearchSecurityLink"
|
||||
defaultMessage="Elasticsearch security"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
securityFlag: <EuiCode>xpack.security.enabled</EuiCode>,
|
||||
true: <EuiCode>true</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
</RequirementItem>
|
||||
<EuiSpacer size="s" />
|
||||
<RequirementItem isMissing={missingRequirements.includes('api_keys')}>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.setupPage.elasticsearchApiKeyFlagText"
|
||||
defaultMessage="{apiKeyLink}. Set {apiKeyFlag} to {true} ."
|
||||
values={{
|
||||
apiKeyFlag: <EuiCode>xpack.security.authc.api_key.enabled</EuiCode>,
|
||||
true: <EuiCode>true</EuiCode>,
|
||||
apiKeyLink: (
|
||||
<EuiLink
|
||||
href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-settings.html#api-key-service-settings"
|
||||
target="_blank"
|
||||
external
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.setupPage.apiKeyServiceLink"
|
||||
defaultMessage="API key service"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</RequirementItem>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiCodeBlock isCopyable={true}>
|
||||
{`xpack.security.enabled: true
|
||||
xpack.security.authc.api_key.enabled: true`}
|
||||
</EuiCodeBlock>
|
||||
<EuiSpacer size="l" />
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.setupPage.gettingStartedText"
|
||||
defaultMessage="For more information, read our {link} guide."
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink
|
||||
href="https://www.elastic.co/guide/en/fleet/current/index.html"
|
||||
target="_blank"
|
||||
external
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.setupPage.gettingStartedLink"
|
||||
defaultMessage="Getting Started"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
</WithoutHeaderLayout>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingSpinner,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiLink,
|
||||
EuiEmptyPrompt,
|
||||
} from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { useStartServices } from '../../../hooks';
|
||||
|
||||
export const ContentWrapper = styled(EuiFlexGroup)`
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
function renderOnPremInstructions() {
|
||||
return (
|
||||
<EuiPanel
|
||||
paddingSize="none"
|
||||
grow={false}
|
||||
hasShadow={false}
|
||||
hasBorder={true}
|
||||
className="eui-textCenter"
|
||||
>
|
||||
<EuiEmptyPrompt
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.fleetServerSetup.setupTitle"
|
||||
defaultMessage="Add a Fleet Server"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.fleetServerSetup.setupText"
|
||||
defaultMessage="A Fleet Server is required before you can enroll agents with Fleet. See the Fleet User Guide for instructions on how to add a Fleet Server."
|
||||
/>
|
||||
}
|
||||
actions={
|
||||
<EuiButton
|
||||
iconSide="right"
|
||||
iconType="popout"
|
||||
fill
|
||||
isLoading={false}
|
||||
type="submit"
|
||||
href="https://ela.st/add-fleet-server"
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.fleetServerSetup.setupGuideLink"
|
||||
defaultMessage="Fleet User Guide"
|
||||
/>
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
||||
function renderCloudInstructions(deploymentUrl: string) {
|
||||
return (
|
||||
<EuiPanel
|
||||
paddingSize="none"
|
||||
grow={false}
|
||||
hasShadow={false}
|
||||
hasBorder={true}
|
||||
className="eui-textCenter"
|
||||
>
|
||||
<EuiEmptyPrompt
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.fleetServerSetup.cloudSetupTitle"
|
||||
defaultMessage="Enable APM & Fleet"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.fleetServerSetup.cloudSetupText"
|
||||
defaultMessage="A Fleet Server is required before you can enroll agents with Fleet. You can add one to your deployment by enabling APM & Fleet. For more information see the {link}"
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink href="https://ela.st/add-fleet-server" target="_blank" external>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.settings.userGuideLink"
|
||||
defaultMessage="Fleet User Guide"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<EuiButton
|
||||
iconSide="right"
|
||||
iconType="popout"
|
||||
fill
|
||||
isLoading={false}
|
||||
type="submit"
|
||||
href={deploymentUrl}
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.fleetServerSetup.cloudDeploymentLink"
|
||||
defaultMessage="Edit deployment"
|
||||
/>
|
||||
</EuiButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export const FleetServerRequirementPage = () => {
|
||||
const startService = useStartServices();
|
||||
const deploymentUrl = startService.cloud?.deploymentUrl;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContentWrapper justifyContent="center" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
{deploymentUrl ? renderCloudInstructions(deploymentUrl) : renderOnPremInstructions()}
|
||||
<EuiSpacer size="l" />
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="m" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s" color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.fleetServerSetup.waitingText"
|
||||
defaultMessage="Waiting for a Fleet Server to connect..."
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</ContentWrapper>
|
||||
<EuiSpacer size="xxl" />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { MissingESRequirementsPage } from './es_requirements_page';
|
||||
export { FleetServerRequirementPage } from './fleet_server_requirement_page';
|
|
@ -20,9 +20,13 @@ import {
|
|||
EuiFlyoutFooter,
|
||||
EuiTab,
|
||||
EuiTabs,
|
||||
EuiCallOut,
|
||||
EuiLink,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { useGetSettings, useUrlModal } from '../../../../hooks';
|
||||
import type { AgentPolicy } from '../../../../types';
|
||||
|
||||
import { ManagedInstructions } from './managed_instructions';
|
||||
|
@ -33,12 +37,61 @@ interface Props {
|
|||
agentPolicies?: AgentPolicy[];
|
||||
}
|
||||
|
||||
const MissingFleetServerHostCallout: React.FunctionComponent<{ onClose: () => void }> = ({
|
||||
onClose,
|
||||
}) => {
|
||||
const { setModal } = useUrlModal();
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={i18n.translate('xpack.fleet.agentEnrollment.missingFleetHostCalloutTitle', {
|
||||
defaultMessage: 'Missing URL for Fleet Server host',
|
||||
})}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentEnrollment.missingFleetHostCalloutText"
|
||||
defaultMessage="A URL for your Fleet Server host is required to enroll agents with Fleet. You can add this information in Fleet Settings. 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.agentEnrollment.missingFleetHostGuideLink"
|
||||
defaultMessage="Fleet User Guide"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiButton
|
||||
fill
|
||||
iconType="gear"
|
||||
onClick={() => {
|
||||
onClose();
|
||||
setModal('settings');
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentEnrollment.fleetSettingsLink"
|
||||
defaultMessage="Fleet Settings"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiCallOut>
|
||||
);
|
||||
};
|
||||
|
||||
export const AgentEnrollmentFlyout: React.FunctionComponent<Props> = ({
|
||||
onClose,
|
||||
agentPolicies,
|
||||
}) => {
|
||||
const [mode, setMode] = useState<'managed' | 'standalone'>('managed');
|
||||
|
||||
const settings = useGetSettings();
|
||||
const fleetServerHosts = settings.data?.item?.fleet_server_hosts || [];
|
||||
|
||||
return (
|
||||
<EuiFlyout onClose={onClose} size="l" maxWidth={880}>
|
||||
<EuiFlyoutHeader hasBorder aria-labelledby="FleetAgentEnrollmentFlyoutTitle">
|
||||
|
@ -74,8 +127,14 @@ export const AgentEnrollmentFlyout: React.FunctionComponent<Props> = ({
|
|||
</EuiTabs>
|
||||
</EuiFlyoutHeader>
|
||||
|
||||
<EuiFlyoutBody>
|
||||
{mode === 'managed' ? (
|
||||
<EuiFlyoutBody
|
||||
banner={
|
||||
fleetServerHosts.length === 0 ? (
|
||||
<MissingFleetServerHostCallout onClose={onClose} />
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{fleetServerHosts.length === 0 ? null : mode === 'managed' ? (
|
||||
<ManagedInstructions agentPolicies={agentPolicies} />
|
||||
) : (
|
||||
<StandaloneInstructions agentPolicies={agentPolicies} />
|
||||
|
|
|
@ -14,12 +14,12 @@ import { FormattedMessage } from '@kbn/i18n/react';
|
|||
import type { AgentPolicy } from '../../../../types';
|
||||
import {
|
||||
useGetOneEnrollmentAPIKey,
|
||||
useStartServices,
|
||||
useGetSettings,
|
||||
useLink,
|
||||
useFleetStatus,
|
||||
} from '../../../../hooks';
|
||||
import { ManualInstructions } from '../../../../components/enrollment_instructions';
|
||||
import { FleetServerRequirementPage } from '../../agent_requirements_page';
|
||||
|
||||
import { DownloadStep, AgentPolicySelectionStep } from './steps';
|
||||
|
||||
|
@ -27,22 +27,41 @@ interface Props {
|
|||
agentPolicies?: AgentPolicy[];
|
||||
}
|
||||
|
||||
export const ManagedInstructions = React.memo<Props>(({ agentPolicies }) => {
|
||||
const DefaultMissingRequirements = () => {
|
||||
const { getHref } = useLink();
|
||||
const core = useStartServices();
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentEnrollment.agentsNotInitializedText"
|
||||
defaultMessage="Before enrolling agents, {link}."
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink href={getHref('fleet')}>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentEnrollment.setUpAgentsLink"
|
||||
defaultMessage="set up central management for Elastic Agents"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const FleetServerMissingRequirements = () => {
|
||||
return <FleetServerRequirementPage />;
|
||||
};
|
||||
|
||||
export const ManagedInstructions = React.memo<Props>(({ agentPolicies }) => {
|
||||
const fleetStatus = useFleetStatus();
|
||||
|
||||
const [selectedAPIKeyId, setSelectedAPIKeyId] = useState<string | undefined>();
|
||||
|
||||
const settings = useGetSettings();
|
||||
const apiKey = useGetOneEnrollmentAPIKey(selectedAPIKeyId);
|
||||
|
||||
const kibanaUrlsSettings = settings.data?.item?.kibana_urls;
|
||||
const kibanaUrl = kibanaUrlsSettings
|
||||
? kibanaUrlsSettings[0]
|
||||
: `${window.location.origin}${core.http.basePath.get()}`;
|
||||
|
||||
const kibanaCASha256 = settings.data?.item?.kibana_ca_sha256;
|
||||
const settings = useGetSettings();
|
||||
const fleetServerHosts = settings.data?.item?.fleet_server_hosts || [];
|
||||
|
||||
const steps: EuiContainedStepProps[] = [
|
||||
DownloadStep(),
|
||||
|
@ -52,46 +71,29 @@ export const ManagedInstructions = React.memo<Props>(({ agentPolicies }) => {
|
|||
defaultMessage: 'Enroll and start the Elastic Agent',
|
||||
}),
|
||||
children: apiKey.data && (
|
||||
<ManualInstructions
|
||||
apiKey={apiKey.data.item}
|
||||
kibanaUrl={kibanaUrl}
|
||||
kibanaCASha256={kibanaCASha256}
|
||||
fleetServerHosts={settings.data?.item?.fleet_server_hosts || []}
|
||||
/>
|
||||
<ManualInstructions apiKey={apiKey.data.item} fleetServerHosts={fleetServerHosts} />
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentEnrollment.managedDescription"
|
||||
defaultMessage="Enroll an Elastic Agent in Fleet to automatically deploy updates and centrally manage the agent."
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiSpacer size="l" />
|
||||
{fleetStatus.isReady ? (
|
||||
<>
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentEnrollment.managedDescription"
|
||||
defaultMessage="Enroll an Elastic Agent in Fleet to automatically deploy updates and centrally manage the agent."
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiSteps steps={steps} />
|
||||
</>
|
||||
) : fleetStatus.missingRequirements?.length === 1 &&
|
||||
fleetStatus.missingRequirements[0] === 'fleet_server' ? (
|
||||
<FleetServerMissingRequirements />
|
||||
) : (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentEnrollment.agentsNotInitializedText"
|
||||
defaultMessage="Before enrolling agents, {link}."
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink href={getHref('fleet')}>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentEnrollment.setUpAgentsLink"
|
||||
defaultMessage="set up central management for Elastic Agents"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
<DefaultMissingRequirements />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { HashRouter as Router, Route, Switch, Redirect } from 'react-router-dom';
|
||||
|
||||
|
@ -15,21 +15,43 @@ import { useConfig, useFleetStatus, useBreadcrumbs, useCapabilities } from '../.
|
|||
import { WithoutHeaderLayout } from '../../layouts';
|
||||
|
||||
import { AgentListPage } from './agent_list_page';
|
||||
import { SetupPage } from './setup_page';
|
||||
import { FleetServerRequirementPage, MissingESRequirementsPage } from './agent_requirements_page';
|
||||
import { AgentDetailsPage } from './agent_details_page';
|
||||
import { NoAccessPage } from './error_pages/no_access';
|
||||
import { EnrollmentTokenListPage } from './enrollment_token_list_page';
|
||||
import { ListLayout } from './components/list_layout';
|
||||
|
||||
const REFRESH_INTERVAL_MS = 30000;
|
||||
|
||||
export const FleetApp: React.FunctionComponent = () => {
|
||||
useBreadcrumbs('fleet');
|
||||
|
||||
const { agents } = useConfig();
|
||||
const capabilities = useCapabilities();
|
||||
|
||||
const fleetStatus = useFleetStatus();
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!agents.enabled ||
|
||||
fleetStatus.isLoading ||
|
||||
!fleetStatus.missingRequirements ||
|
||||
!fleetStatus.missingRequirements.includes('fleet_server')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
fleetStatus.refresh();
|
||||
}, REFRESH_INTERVAL_MS);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [fleetStatus, agents.enabled]);
|
||||
|
||||
if (!agents.enabled) return null;
|
||||
if (fleetStatus.isLoading) {
|
||||
if (!fleetStatus.missingRequirements && fleetStatus.isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
|
@ -49,13 +71,16 @@ export const FleetApp: React.FunctionComponent = () => {
|
|||
);
|
||||
}
|
||||
|
||||
if (fleetStatus.isReady === false) {
|
||||
return (
|
||||
<SetupPage
|
||||
missingRequirements={fleetStatus.missingRequirements || []}
|
||||
refresh={fleetStatus.refresh}
|
||||
/>
|
||||
);
|
||||
const hasOnlyFleetServerMissingRequirement =
|
||||
fleetStatus?.missingRequirements?.length === 1 &&
|
||||
fleetStatus.missingRequirements[0] === 'fleet_server';
|
||||
|
||||
if (
|
||||
!hasOnlyFleetServerMissingRequirement &&
|
||||
fleetStatus.missingRequirements &&
|
||||
fleetStatus.missingRequirements.length > 0
|
||||
) {
|
||||
return <MissingESRequirementsPage missingRequirements={fleetStatus.missingRequirements} />;
|
||||
}
|
||||
if (!capabilities.read) {
|
||||
return <NoAccessPage />;
|
||||
|
@ -74,7 +99,11 @@ export const FleetApp: React.FunctionComponent = () => {
|
|||
</Route>
|
||||
<Route path={PAGE_ROUTING_PATHS.fleet_agent_list}>
|
||||
<ListLayout>
|
||||
<AgentListPage />
|
||||
{hasOnlyFleetServerMissingRequirement ? (
|
||||
<FleetServerRequirementPage />
|
||||
) : (
|
||||
<AgentListPage />
|
||||
)}
|
||||
</ListLayout>
|
||||
</Route>
|
||||
<Route path={PAGE_ROUTING_PATHS.fleet_enrollment_tokens}>
|
||||
|
|
|
@ -1,284 +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, { useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiPageBody,
|
||||
EuiPageContent,
|
||||
EuiForm,
|
||||
EuiText,
|
||||
EuiButton,
|
||||
EuiTitle,
|
||||
EuiSpacer,
|
||||
EuiIcon,
|
||||
EuiCallOut,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
EuiCode,
|
||||
EuiCodeBlock,
|
||||
EuiLink,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { useStartServices, sendPostFleetSetup } from '../../../hooks';
|
||||
import { WithoutHeaderLayout } from '../../../layouts';
|
||||
import type { GetFleetStatusResponse } from '../../../types';
|
||||
|
||||
export const RequirementItem: React.FunctionComponent<{ isMissing: boolean }> = ({
|
||||
isMissing,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="flexStart">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
{isMissing ? (
|
||||
<EuiIcon type="crossInACircleFilled" color="danger" />
|
||||
) : (
|
||||
<EuiIcon type="checkInCircleFilled" color="success" />
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>{children}</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export const SetupPage: React.FunctionComponent<{
|
||||
refresh: () => Promise<void>;
|
||||
missingRequirements: GetFleetStatusResponse['missing_requirements'];
|
||||
}> = ({ refresh, missingRequirements }) => {
|
||||
const [isFormLoading, setIsFormLoading] = useState<boolean>(false);
|
||||
const core = useStartServices();
|
||||
|
||||
const onSubmit = async () => {
|
||||
setIsFormLoading(true);
|
||||
try {
|
||||
await sendPostFleetSetup({ forceRecreate: true });
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
core.notifications.toasts.addDanger(error.message);
|
||||
setIsFormLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (
|
||||
!missingRequirements.includes('tls_required') &&
|
||||
!missingRequirements.includes('api_keys') &&
|
||||
!missingRequirements.includes('encrypted_saved_object_encryption_key_required')
|
||||
) {
|
||||
return (
|
||||
<WithoutHeaderLayout>
|
||||
<EuiPageBody restrictWidth={648}>
|
||||
<EuiPageContent
|
||||
verticalPosition="center"
|
||||
horizontalPosition="center"
|
||||
className="eui-textCenter"
|
||||
paddingSize="l"
|
||||
>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiIcon type="lock" color="subdued" size="xl" />
|
||||
<EuiSpacer size="m" />
|
||||
<EuiTitle size="l">
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.setupPage.enableTitle"
|
||||
defaultMessage="Enable central management for Elastic Agents"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="xl" />
|
||||
<EuiText color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.setupPage.enableText"
|
||||
defaultMessage="Central management requires an Elastic user who can create API keys and write to logs-* and metrics-*."
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiForm>
|
||||
<EuiButton onClick={onSubmit} fill isLoading={isFormLoading} type="submit">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.setupPage.enableCentralManagement"
|
||||
defaultMessage="Create user and enable central management"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiForm>
|
||||
<EuiSpacer size="m" />
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
</WithoutHeaderLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<WithoutHeaderLayout>
|
||||
<EuiPageBody restrictWidth={820}>
|
||||
<EuiPageContent>
|
||||
<EuiCallOut
|
||||
title={i18n.translate('xpack.fleet.setupPage.missingRequirementsCalloutTitle', {
|
||||
defaultMessage: 'Missing security requirements',
|
||||
})}
|
||||
color="warning"
|
||||
iconType="alert"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.setupPage.missingRequirementsCalloutDescription"
|
||||
defaultMessage="To use central management for Elastic Agents, enable the following Elasticsearch and Kibana security features."
|
||||
/>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="m" />
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.setupPage.missingRequirementsElasticsearchTitle"
|
||||
defaultMessage="In your Elasticsearch policy, enable:"
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
<RequirementItem isMissing={false}>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.setupPage.elasticsearchSecurityFlagText"
|
||||
defaultMessage="{esSecurityLink}. Set {securityFlag} to {true} ."
|
||||
values={{
|
||||
esSecurityLink: (
|
||||
<EuiLink
|
||||
href="https://www.elastic.co/guide/en/elasticsearch/reference/current/configuring-security.html"
|
||||
target="_blank"
|
||||
external
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.setupPage.elasticsearchSecurityLink"
|
||||
defaultMessage="Elasticsearch security"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
securityFlag: <EuiCode>xpack.security.enabled</EuiCode>,
|
||||
true: <EuiCode>true</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
</RequirementItem>
|
||||
<EuiSpacer size="s" />
|
||||
<RequirementItem isMissing={missingRequirements.includes('api_keys')}>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.setupPage.elasticsearchApiKeyFlagText"
|
||||
defaultMessage="{apiKeyLink}. Set {apiKeyFlag} to {true} ."
|
||||
values={{
|
||||
apiKeyFlag: <EuiCode>xpack.security.authc.api_key.enabled</EuiCode>,
|
||||
true: <EuiCode>true</EuiCode>,
|
||||
apiKeyLink: (
|
||||
<EuiLink
|
||||
href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-settings.html#api-key-service-settings"
|
||||
target="_blank"
|
||||
external
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.setupPage.apiKeyServiceLink"
|
||||
defaultMessage="API key service"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</RequirementItem>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiCodeBlock isCopyable={true}>
|
||||
{`xpack.security.enabled: true
|
||||
xpack.security.authc.api_key.enabled: true`}
|
||||
</EuiCodeBlock>
|
||||
<EuiSpacer size="l" />
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.setupPage.missingRequirementsKibanaTitle"
|
||||
defaultMessage="In your Kibana policy, enable:"
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
<RequirementItem isMissing={missingRequirements.includes('tls_required')}>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.setupPage.tlsFlagText"
|
||||
defaultMessage="{kibanaSecurityLink}. Set {securityFlag} to {true}. For development purposes, you can disable {tlsLink} by setting {tlsFlag} to {true} as an unsafe alternative."
|
||||
values={{
|
||||
kibanaSecurityLink: (
|
||||
<EuiLink
|
||||
href="https://www.elastic.co/guide/en/kibana/current/using-kibana-with-security.html"
|
||||
target="_blank"
|
||||
external
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.setupPage.kibanaSecurityLink"
|
||||
defaultMessage="Kibana security"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
securityFlag: <EuiCode>xpack.security.enabled</EuiCode>,
|
||||
tlsLink: (
|
||||
<EuiLink
|
||||
href="https://www.elastic.co/guide/en/kibana/current/configuring-tls.html"
|
||||
target="_blank"
|
||||
external
|
||||
>
|
||||
<FormattedMessage id="xpack.fleet.setupPage.tlsLink" defaultMessage="TLS" />
|
||||
</EuiLink>
|
||||
),
|
||||
tlsFlag: <EuiCode>xpack.fleet.agents.tlsCheckDisabled</EuiCode>,
|
||||
true: <EuiCode>true</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
</RequirementItem>
|
||||
<EuiSpacer size="s" />
|
||||
<RequirementItem
|
||||
isMissing={missingRequirements.includes(
|
||||
'encrypted_saved_object_encryption_key_required'
|
||||
)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.setupPage.encryptionKeyFlagText"
|
||||
defaultMessage="{encryptionKeyLink}. Set {keyFlag} to any alphanumeric value of at least 32 characters."
|
||||
values={{
|
||||
encryptionKeyLink: (
|
||||
<EuiLink
|
||||
href="https://www.elastic.co/guide/en/kibana/current/fleet-settings-kb.html"
|
||||
target="_blank"
|
||||
external
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.setupPage.kibanaEncryptionLink"
|
||||
defaultMessage="Kibana encryption key"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
keyFlag: <EuiCode>xpack.encryptedSavedObjects.encryptionKey</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
</RequirementItem>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiCodeBlock isCopyable={true}>
|
||||
{`xpack.security.enabled: true
|
||||
xpack.encryptedSavedObjects.encryptionKey: "something_at_least_32_characters"`}
|
||||
</EuiCodeBlock>
|
||||
<EuiSpacer size="l" />
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.setupPage.gettingStartedText"
|
||||
defaultMessage="For more information, read our {link} guide."
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink
|
||||
href="https://www.elastic.co/guide/en/fleet/current/index.html"
|
||||
target="_blank"
|
||||
external
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.setupPage.gettingStartedLink"
|
||||
defaultMessage="Getting Started"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
</WithoutHeaderLayout>
|
||||
);
|
||||
};
|
|
@ -23,6 +23,7 @@ import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public';
|
|||
import type { HomePublicPluginSetup } from '../../../../src/plugins/home/public';
|
||||
import { Storage } from '../../../../src/plugins/kibana_utils/public';
|
||||
import type { LicensingPluginSetup } from '../../licensing/public';
|
||||
import type { CloudSetup } from '../../cloud/public';
|
||||
import { PLUGIN_ID, setupRouteService, appRoutesService } from '../common';
|
||||
import type { CheckPermissionsResponse, PostIngestSetupResponse } from '../common';
|
||||
|
||||
|
@ -61,6 +62,7 @@ export interface FleetSetupDeps {
|
|||
licensing: LicensingPluginSetup;
|
||||
data: DataPublicPluginSetup;
|
||||
home?: HomePublicPluginSetup;
|
||||
cloud?: CloudSetup;
|
||||
}
|
||||
|
||||
export interface FleetStartDeps {
|
||||
|
@ -69,6 +71,7 @@ export interface FleetStartDeps {
|
|||
|
||||
export interface FleetStartServices extends CoreStart, FleetStartDeps {
|
||||
storage: Storage;
|
||||
cloud?: CloudSetup;
|
||||
}
|
||||
|
||||
export class FleetPlugin implements Plugin<FleetSetup, FleetStart, FleetSetupDeps, FleetStartDeps> {
|
||||
|
@ -110,6 +113,7 @@ export class FleetPlugin implements Plugin<FleetSetup, FleetStart, FleetSetupDep
|
|||
...coreStartServices,
|
||||
...startDepsServices,
|
||||
storage: this.storage,
|
||||
cloud: deps.cloud,
|
||||
};
|
||||
const { renderApp, teardownFleet } = await import('./applications/fleet');
|
||||
const unmount = renderApp(startServices, params, config, kibanaVersion, extensions);
|
||||
|
|
|
@ -50,6 +50,7 @@ export {
|
|||
DEFAULT_AGENT_POLICY,
|
||||
DEFAULT_OUTPUT,
|
||||
// Fleet Server index
|
||||
FLEET_SERVER_SERVERS_INDEX,
|
||||
ENROLLMENT_API_KEYS_INDEX,
|
||||
AGENTS_INDEX,
|
||||
PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE,
|
||||
|
|
|
@ -13,7 +13,7 @@ import { createAppContextStartContractMock, xpackMocks } from '../../mocks';
|
|||
import { appContextService } from '../../services/app_context';
|
||||
import { setupIngestManager } from '../../services/setup';
|
||||
|
||||
import { FleetSetupHandler } from './handlers';
|
||||
import { fleetSetupHandler } from './handlers';
|
||||
|
||||
jest.mock('../../services/setup', () => {
|
||||
return {
|
||||
|
@ -48,7 +48,7 @@ describe('FleetSetupHandler', () => {
|
|||
mockSetupIngestManager.mockImplementation(() =>
|
||||
Promise.resolve({ isInitialized: true, preconfigurationError: undefined })
|
||||
);
|
||||
await FleetSetupHandler(context, request, response);
|
||||
await fleetSetupHandler(context, request, response);
|
||||
|
||||
const expectedBody: PostIngestSetupResponse = { isInitialized: true };
|
||||
expect(response.customError).toHaveBeenCalledTimes(0);
|
||||
|
@ -59,7 +59,7 @@ describe('FleetSetupHandler', () => {
|
|||
mockSetupIngestManager.mockImplementation(() =>
|
||||
Promise.reject(new Error('SO method mocked to throw'))
|
||||
);
|
||||
await FleetSetupHandler(context, request, response);
|
||||
await fleetSetupHandler(context, request, response);
|
||||
|
||||
expect(response.customError).toHaveBeenCalledTimes(1);
|
||||
expect(response.customError).toHaveBeenCalledWith({
|
||||
|
@ -75,7 +75,7 @@ describe('FleetSetupHandler', () => {
|
|||
Promise.reject(new RegistryError('Registry method mocked to throw'))
|
||||
);
|
||||
|
||||
await FleetSetupHandler(context, request, response);
|
||||
await fleetSetupHandler(context, request, response);
|
||||
expect(response.customError).toHaveBeenCalledTimes(1);
|
||||
expect(response.customError).toHaveBeenCalledWith({
|
||||
statusCode: 502,
|
||||
|
|
|
@ -8,39 +8,31 @@
|
|||
import type { RequestHandler } from 'src/core/server';
|
||||
import type { TypeOf } from '@kbn/config-schema';
|
||||
|
||||
import { outputService, appContextService } from '../../services';
|
||||
import { appContextService } from '../../services';
|
||||
import type { GetFleetStatusResponse, PostIngestSetupResponse } from '../../../common';
|
||||
import { setupIngestManager, setupFleet } from '../../services/setup';
|
||||
import type { PostFleetSetupRequestSchema } from '../../types';
|
||||
import { setupFleet, setupIngestManager } from '../../services/setup';
|
||||
import { hasFleetServers } from '../../services/fleet_server';
|
||||
import { defaultIngestErrorHandler } from '../../errors';
|
||||
import type { PostFleetSetupRequestSchema } from '../../types';
|
||||
|
||||
export const getFleetStatusHandler: RequestHandler = async (context, request, response) => {
|
||||
const soClient = context.core.savedObjects.client;
|
||||
try {
|
||||
const isAdminUserSetup = (await outputService.getAdminUser(soClient)) !== null;
|
||||
const isApiKeysEnabled = await appContextService
|
||||
.getSecurity()
|
||||
.authc.apiKeys.areAPIKeysEnabled();
|
||||
const isTLSEnabled = appContextService.getHttpSetup().getServerInfo().protocol === 'https';
|
||||
const isProductionMode = appContextService.getIsProductionMode();
|
||||
const isCloud = appContextService.getCloud()?.isCloudEnabled ?? false;
|
||||
const isTLSCheckDisabled = appContextService.getConfig()?.agents?.tlsCheckDisabled ?? false;
|
||||
const isFleetServerSetup = await hasFleetServers(appContextService.getInternalUserESClient());
|
||||
const canEncrypt = appContextService.getEncryptedSavedObjectsSetup()?.canEncrypt === true;
|
||||
|
||||
const missingRequirements: GetFleetStatusResponse['missing_requirements'] = [];
|
||||
if (!isAdminUserSetup) {
|
||||
missingRequirements.push('fleet_admin_user');
|
||||
}
|
||||
if (!isApiKeysEnabled) {
|
||||
missingRequirements.push('api_keys');
|
||||
}
|
||||
if (!isTLSCheckDisabled && !isCloud && isProductionMode && !isTLSEnabled) {
|
||||
missingRequirements.push('tls_required');
|
||||
}
|
||||
|
||||
if (!canEncrypt) {
|
||||
missingRequirements.push('encrypted_saved_object_encryption_key_required');
|
||||
}
|
||||
if (!isFleetServerSetup) {
|
||||
missingRequirements.push('fleet_server');
|
||||
}
|
||||
|
||||
const body: GetFleetStatusResponse = {
|
||||
isReady: missingRequirements.length === 0,
|
||||
|
@ -55,7 +47,23 @@ export const getFleetStatusHandler: RequestHandler = async (context, request, re
|
|||
}
|
||||
};
|
||||
|
||||
export const createFleetSetupHandler: RequestHandler<
|
||||
export const fleetSetupHandler: RequestHandler = async (context, request, response) => {
|
||||
try {
|
||||
const soClient = context.core.savedObjects.client;
|
||||
const esClient = context.core.elasticsearch.client.asCurrentUser;
|
||||
const body: PostIngestSetupResponse = { isInitialized: true };
|
||||
await setupIngestManager(soClient, esClient);
|
||||
|
||||
return response.ok({
|
||||
body,
|
||||
});
|
||||
} catch (error) {
|
||||
return defaultIngestErrorHandler({ error, response });
|
||||
}
|
||||
};
|
||||
|
||||
// TODO should be removed as part https://github.com/elastic/kibana/issues/94303
|
||||
export const fleetAgentSetupHandler: RequestHandler<
|
||||
undefined,
|
||||
undefined,
|
||||
TypeOf<typeof PostFleetSetupRequestSchema.body>
|
||||
|
@ -63,10 +71,9 @@ export const createFleetSetupHandler: RequestHandler<
|
|||
try {
|
||||
const soClient = context.core.savedObjects.client;
|
||||
const esClient = context.core.elasticsearch.client.asCurrentUser;
|
||||
const body = await setupIngestManager(soClient, esClient);
|
||||
await setupFleet(soClient, esClient, {
|
||||
forceRecreate: request.body?.forceRecreate ?? false,
|
||||
});
|
||||
const body: PostIngestSetupResponse = { isInitialized: true };
|
||||
await setupIngestManager(soClient, esClient);
|
||||
await setupFleet(soClient, esClient, { forceRecreate: request.body?.forceRecreate === true });
|
||||
|
||||
return response.ok({
|
||||
body,
|
||||
|
@ -75,17 +82,3 @@ export const createFleetSetupHandler: RequestHandler<
|
|||
return defaultIngestErrorHandler({ error, response });
|
||||
}
|
||||
};
|
||||
|
||||
export const FleetSetupHandler: RequestHandler = async (context, request, response) => {
|
||||
const soClient = context.core.savedObjects.client;
|
||||
const esClient = context.core.elasticsearch.client.asCurrentUser;
|
||||
|
||||
try {
|
||||
const body: PostIngestSetupResponse = await setupIngestManager(soClient, esClient);
|
||||
return response.ok({
|
||||
body,
|
||||
});
|
||||
} catch (error) {
|
||||
return defaultIngestErrorHandler({ error, response });
|
||||
}
|
||||
};
|
||||
|
|
|
@ -11,7 +11,7 @@ import { PLUGIN_ID, AGENTS_SETUP_API_ROUTES, SETUP_API_ROUTE } from '../../const
|
|||
import type { FleetConfigType } from '../../../common';
|
||||
import { PostFleetSetupRequestSchema } from '../../types';
|
||||
|
||||
import { getFleetStatusHandler, createFleetSetupHandler, FleetSetupHandler } from './handlers';
|
||||
import { getFleetStatusHandler, fleetSetupHandler, fleetAgentSetupHandler } from './handlers';
|
||||
|
||||
export const registerFleetSetupRoute = (router: IRouter) => {
|
||||
router.post(
|
||||
|
@ -22,7 +22,7 @@ export const registerFleetSetupRoute = (router: IRouter) => {
|
|||
// and will see `Unable to initialize Ingest Manager` in the UI
|
||||
options: { tags: [`access:${PLUGIN_ID}-read`] },
|
||||
},
|
||||
FleetSetupHandler
|
||||
fleetSetupHandler
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -33,7 +33,7 @@ export const registerCreateFleetSetupRoute = (router: IRouter) => {
|
|||
validate: PostFleetSetupRequestSchema,
|
||||
options: { tags: [`access:${PLUGIN_ID}-all`] },
|
||||
},
|
||||
createFleetSetupHandler
|
||||
fleetAgentSetupHandler
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -60,7 +60,6 @@ import { outputService } from './output';
|
|||
import { agentPolicyUpdateEventHandler } from './agent_policy_update';
|
||||
import { getSettings } from './settings';
|
||||
import { normalizeKuery, escapeSearchQueryPhrase } from './saved_object';
|
||||
import { isAgentsSetup } from './agents/setup';
|
||||
import { appContextService } from './app_context';
|
||||
|
||||
const SAVED_OBJECT_TYPE = AGENT_POLICY_SAVED_OBJECT_TYPE;
|
||||
|
@ -226,7 +225,7 @@ class AgentPolicyService {
|
|||
options
|
||||
);
|
||||
|
||||
if (!agentPolicy.is_default) {
|
||||
if (!agentPolicy.is_default && !agentPolicy.is_default_fleet_server) {
|
||||
await this.triggerAgentPolicyUpdatedEvent(soClient, esClient, 'created', newSo.id);
|
||||
}
|
||||
|
||||
|
@ -603,6 +602,11 @@ class AgentPolicyService {
|
|||
agentPolicyId: string
|
||||
) {
|
||||
const esClient = appContextService.getInternalUserESClient();
|
||||
const defaultOutputId = await outputService.getDefaultOutputId(soClient);
|
||||
|
||||
if (!defaultOutputId) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.createFleetPolicyChangeFleetServer(soClient, esClient, agentPolicyId);
|
||||
|
||||
|
@ -614,11 +618,6 @@ class AgentPolicyService {
|
|||
esClient: ElasticsearchClient,
|
||||
agentPolicyId: string
|
||||
) {
|
||||
// If Agents is not setup skip the creation of POLICY_CHANGE agent actions
|
||||
// the action will be created during the fleet setup
|
||||
if (!(await isAgentsSetup(soClient))) {
|
||||
return;
|
||||
}
|
||||
const policy = await agentPolicyService.getFullAgentPolicy(soClient, agentPolicyId);
|
||||
if (!policy || !policy.revision) {
|
||||
return;
|
||||
|
@ -646,11 +645,6 @@ class AgentPolicyService {
|
|||
esClient: ElasticsearchClient,
|
||||
agentPolicyId: string
|
||||
) {
|
||||
// If Agents is not setup skip the creation of POLICY_CHANGE agent actions
|
||||
// the action will be created during the fleet setup
|
||||
if (!(await isAgentsSetup(soClient))) {
|
||||
return;
|
||||
}
|
||||
const policy = await agentPolicyService.get(soClient, agentPolicyId);
|
||||
const fullPolicy = await agentPolicyService.getFullAgentPolicy(soClient, agentPolicyId);
|
||||
if (!policy || !fullPolicy || !fullPolicy.revision) {
|
||||
|
@ -674,6 +668,29 @@ class AgentPolicyService {
|
|||
});
|
||||
}
|
||||
|
||||
public async getLatestFleetPolicy(esClient: ElasticsearchClient, agentPolicyId: string) {
|
||||
const res = await esClient.search({
|
||||
index: AGENT_POLICY_INDEX,
|
||||
ignore_unavailable: true,
|
||||
body: {
|
||||
query: {
|
||||
term: {
|
||||
policy_id: agentPolicyId,
|
||||
},
|
||||
},
|
||||
size: 1,
|
||||
sort: [{ revision_idx: { order: 'desc' } }],
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-expect-error value is number | TotalHits
|
||||
if (res.body.hits.total.value === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return res.body.hits.hits[0]._source;
|
||||
}
|
||||
|
||||
public async getFullAgentPolicy(
|
||||
soClient: SavedObjectsClientContract,
|
||||
id: string,
|
||||
|
|
|
@ -9,7 +9,7 @@ import type { KibanaRequest } from 'src/core/server';
|
|||
import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server';
|
||||
|
||||
import { generateEnrollmentAPIKey, deleteEnrollmentApiKeyForAgentPolicyId } from './api_keys';
|
||||
import { isAgentsSetup, unenrollForAgentPolicyId } from './agents';
|
||||
import { unenrollForAgentPolicyId } from './agents';
|
||||
import { agentPolicyService } from './agent_policy';
|
||||
import { appContextService } from './app_context';
|
||||
|
||||
|
@ -34,17 +34,13 @@ export async function agentPolicyUpdateEventHandler(
|
|||
action: string,
|
||||
agentPolicyId: string
|
||||
) {
|
||||
// If Agents are not setup skip this hook
|
||||
if (!(await isAgentsSetup(soClient))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// `soClient` from ingest `appContextService` is used to create policy change actions
|
||||
// to ensure encrypted SOs are handled correctly
|
||||
const internalSoClient = appContextService.getInternalUserSOClient(fakeRequest);
|
||||
|
||||
if (action === 'created') {
|
||||
await generateEnrollmentAPIKey(soClient, esClient, {
|
||||
name: 'Default',
|
||||
agentPolicyId,
|
||||
});
|
||||
await agentPolicyService.createFleetPolicyChangeAction(internalSoClient, agentPolicyId);
|
||||
|
|
|
@ -5,14 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { SavedObjectsClientContract } from 'src/core/server';
|
||||
import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server';
|
||||
|
||||
import { SO_SEARCH_LIMIT } from '../../constants';
|
||||
import { agentPolicyService } from '../agent_policy';
|
||||
import { outputService } from '../output';
|
||||
|
||||
import { getLatestConfigChangeAction } from './actions';
|
||||
|
||||
export async function isAgentsSetup(soClient: SavedObjectsClientContract): Promise<boolean> {
|
||||
const adminUser = await outputService.getAdminUser(soClient, false);
|
||||
const outputId = await outputService.getDefaultOutputId(soClient);
|
||||
|
@ -26,20 +24,18 @@ export async function isAgentsSetup(soClient: SavedObjectsClientContract): Promi
|
|||
*
|
||||
* @param soClient
|
||||
*/
|
||||
export async function ensureAgentActionPolicyChangeExists(soClient: SavedObjectsClientContract) {
|
||||
// If Agents are not setup skip
|
||||
if (!(await isAgentsSetup(soClient))) {
|
||||
return;
|
||||
}
|
||||
|
||||
export async function ensureAgentActionPolicyChangeExists(
|
||||
soClient: SavedObjectsClientContract,
|
||||
esClient: ElasticsearchClient
|
||||
) {
|
||||
const { items: agentPolicies } = await agentPolicyService.list(soClient, {
|
||||
perPage: SO_SEARCH_LIMIT,
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
agentPolicies.map(async (agentPolicy) => {
|
||||
const policyChangeActionExist = !!(await getLatestConfigChangeAction(
|
||||
soClient,
|
||||
const policyChangeActionExist = !!(await agentPolicyService.getLatestFleetPolicy(
|
||||
esClient,
|
||||
agentPolicy.id
|
||||
));
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { ResponseError } from '@elastic/elasticsearch/lib/errors';
|
||||
import type { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server';
|
||||
|
||||
import { esKuery } from '../../../../../../src/plugins/data/server';
|
||||
import type { ESSearchResponse as SearchResponse } from '../../../../../../typings/elasticsearch';
|
||||
import type { EnrollmentAPIKey, FleetServerEnrollmentAPIKey } from '../../types';
|
||||
import { ENROLLMENT_API_KEYS_INDEX } from '../../constants';
|
||||
|
@ -39,7 +40,9 @@ export async function listEnrollmentApiKeys(
|
|||
sort: 'created_at:desc',
|
||||
track_total_hits: true,
|
||||
ignore_unavailable: true,
|
||||
q: kuery,
|
||||
body: kuery
|
||||
? { query: esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kuery)) }
|
||||
: undefined,
|
||||
});
|
||||
|
||||
// @ts-expect-error @elastic/elasticsearch
|
||||
|
@ -54,6 +57,17 @@ export async function listEnrollmentApiKeys(
|
|||
};
|
||||
}
|
||||
|
||||
export async function hasEnrollementAPIKeysForPolicy(
|
||||
esClient: ElasticsearchClient,
|
||||
policyId: string
|
||||
) {
|
||||
const res = await listEnrollmentApiKeys(esClient, {
|
||||
kuery: `policy_id:"${policyId}"`,
|
||||
});
|
||||
|
||||
return res.total !== 0;
|
||||
}
|
||||
|
||||
export async function getEnrollmentAPIKey(
|
||||
esClient: ElasticsearchClient,
|
||||
id: string
|
||||
|
|
|
@ -5,10 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ElasticsearchClient } from 'kibana/server';
|
||||
import { first } from 'rxjs/operators';
|
||||
|
||||
import { appContextService } from '../app_context';
|
||||
import { licenseService } from '../license';
|
||||
import { FLEET_SERVER_SERVERS_INDEX } from '../../constants';
|
||||
|
||||
import { runFleetServerMigration } from './saved_object_migrations';
|
||||
|
||||
|
@ -21,6 +23,19 @@ export function isFleetServerSetup() {
|
|||
return _isFleetServerSetup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if at least one fleet server is connected
|
||||
*/
|
||||
export async function hasFleetServers(esClient: ElasticsearchClient) {
|
||||
const res = await esClient.search<{}, {}>({
|
||||
index: FLEET_SERVER_SERVERS_INDEX,
|
||||
ignore_unavailable: true,
|
||||
});
|
||||
|
||||
// @ts-expect-error value is number | TotalHits
|
||||
return res.body.hits.total.value > 0;
|
||||
}
|
||||
|
||||
export async function awaitIfFleetServerSetupPending() {
|
||||
if (!_isPending) {
|
||||
return;
|
||||
|
|
|
@ -14,6 +14,8 @@ import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../constants';
|
|||
|
||||
import { ensurePreconfiguredPackagesAndPolicies } from './preconfiguration';
|
||||
|
||||
jest.mock('./agent_policy_update');
|
||||
|
||||
const mockInstalledPackages = new Map();
|
||||
const mockConfiguredPolicies = new Map();
|
||||
|
||||
|
|
|
@ -24,8 +24,7 @@ import {
|
|||
ensureInstalledPackage,
|
||||
ensurePackagesCompletedInstall,
|
||||
} from './epm/packages/install';
|
||||
|
||||
import { generateEnrollmentAPIKey } from './api_keys';
|
||||
import { generateEnrollmentAPIKey, hasEnrollementAPIKeysForPolicy } from './api_keys';
|
||||
import { settingsService } from '.';
|
||||
import { awaitIfPending } from './setup_utils';
|
||||
import { createDefaultSettings } from './settings';
|
||||
|
@ -55,7 +54,6 @@ async function createSetupSideEffects(
|
|||
// packages installed by default
|
||||
ensureInstalledDefaultPackages(soClient, esClient),
|
||||
outputService.ensureDefaultOutput(soClient),
|
||||
updateFleetRoleIfExists(esClient),
|
||||
settingsService.getSettings(soClient).catch((e: any) => {
|
||||
if (e.isBoom && e.output.statusCode === 404) {
|
||||
const defaultSettings = createDefaultSettings();
|
||||
|
@ -174,23 +172,46 @@ async function createSetupSideEffects(
|
|||
}
|
||||
}
|
||||
|
||||
await ensureAgentActionPolicyChangeExists(soClient);
|
||||
await ensureDefaultEnrollmentAPIKeysExists(soClient, esClient);
|
||||
|
||||
await ensureAgentActionPolicyChangeExists(soClient, esClient);
|
||||
|
||||
return { isInitialized: true, preconfigurationError };
|
||||
}
|
||||
|
||||
async function updateFleetRoleIfExists(esClient: ElasticsearchClient) {
|
||||
try {
|
||||
await esClient.security.getRole({ name: FLEET_ENROLL_ROLE });
|
||||
} catch (e) {
|
||||
if (e.statusCode === 404) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw e;
|
||||
export async function ensureDefaultEnrollmentAPIKeysExists(
|
||||
soClient: SavedObjectsClientContract,
|
||||
esClient: ElasticsearchClient,
|
||||
options?: { forceRecreate?: boolean }
|
||||
) {
|
||||
const security = appContextService.getSecurity();
|
||||
if (!security) {
|
||||
return;
|
||||
}
|
||||
|
||||
return putFleetRole(esClient);
|
||||
if (!(await security.authc.apiKeys.areAPIKeysEnabled())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { items: agentPolicies } = await agentPolicyService.list(soClient, {
|
||||
perPage: SO_SEARCH_LIMIT,
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
agentPolicies.map(async (agentPolicy) => {
|
||||
const hasKey = await hasEnrollementAPIKeysForPolicy(esClient, agentPolicy.id);
|
||||
|
||||
if (hasKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
return generateEnrollmentAPIKey(soClient, esClient, {
|
||||
name: `Default`,
|
||||
agentPolicyId: agentPolicy.id,
|
||||
forceRecreate: true, // Always generate a new enrollment key when Fleet is being set up
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function putFleetRole(esClient: ElasticsearchClient) {
|
||||
|
@ -208,6 +229,7 @@ async function putFleetRole(esClient: ElasticsearchClient) {
|
|||
});
|
||||
}
|
||||
|
||||
// TODO Deprecated should be removed as part of https://github.com/elastic/kibana/issues/94303
|
||||
export async function setupFleet(
|
||||
soClient: SavedObjectsClientContract,
|
||||
esClient: ElasticsearchClient,
|
||||
|
@ -234,8 +256,6 @@ export async function setupFleet(
|
|||
},
|
||||
});
|
||||
|
||||
outputService.invalidateCache();
|
||||
|
||||
// save fleet admin user
|
||||
const defaultOutputId = await outputService.getDefaultOutputId(soClient);
|
||||
if (!defaultOutputId) {
|
||||
|
@ -245,31 +265,12 @@ export async function setupFleet(
|
|||
})
|
||||
);
|
||||
}
|
||||
|
||||
await outputService.updateOutput(soClient, defaultOutputId, {
|
||||
fleet_enroll_username: FLEET_ENROLL_USERNAME,
|
||||
fleet_enroll_password: password,
|
||||
});
|
||||
|
||||
const { items: agentPolicies } = await agentPolicyService.list(soClient, {
|
||||
perPage: SO_SEARCH_LIMIT,
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
agentPolicies.map((agentPolicy) => {
|
||||
return generateEnrollmentAPIKey(soClient, esClient, {
|
||||
name: `Default`,
|
||||
agentPolicyId: agentPolicy.id,
|
||||
forceRecreate: true, // Always generate a new enrollment key when Fleet is being set up
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
agentPolicies.map((agentPolicy) =>
|
||||
agentPolicyService.createFleetPolicyChangeAction(soClient, agentPolicy.id)
|
||||
)
|
||||
);
|
||||
outputService.invalidateCache();
|
||||
}
|
||||
|
||||
function generateRandomPassword() {
|
||||
|
|
|
@ -8677,30 +8677,17 @@
|
|||
"xpack.fleet.settings.flyoutTitle": "Fleet 設定",
|
||||
"xpack.fleet.settings.globalOutputTitle": "グローバル出力",
|
||||
"xpack.fleet.settings.invalidYamlFormatErrorMessage": "無効なYAML形式:{reason}",
|
||||
"xpack.fleet.settings.kibanaUrlDifferentPathOrProtocolError": "各URLのプロトコルとパスは同じでなければなりません",
|
||||
"xpack.fleet.settings.kibanaUrlEmptyError": "1つ以上のURLが必要です。",
|
||||
"xpack.fleet.settings.kibanaUrlError": "無効なURL",
|
||||
"xpack.fleet.settings.kibanaUrlLabel": "Kibana URL",
|
||||
"xpack.fleet.settings.saveButtonLabel": "設定を保存",
|
||||
"xpack.fleet.settings.success.message": "設定が保存されました",
|
||||
"xpack.fleet.setupPage.apiKeyServiceLink": "APIキーサービス",
|
||||
"xpack.fleet.setupPage.elasticsearchApiKeyFlagText": "{apiKeyLink}.{apiKeyFlag}を{true}に設定します。",
|
||||
"xpack.fleet.setupPage.elasticsearchSecurityFlagText": "{esSecurityLink}.{securityFlag}を{true}に設定します。",
|
||||
"xpack.fleet.setupPage.elasticsearchSecurityLink": "Elasticsearchセキュリティ",
|
||||
"xpack.fleet.setupPage.enableCentralManagement": "ユーザーを作成し、集中管理を有効にする",
|
||||
"xpack.fleet.setupPage.enableText": "集中管理には、APIキーを作成し、log-*とmetrics-*に書き込むことができるElasticユーザーが必要です。",
|
||||
"xpack.fleet.setupPage.enableTitle": "ElasticElasticエージェントの集中管理を有効にする",
|
||||
"xpack.fleet.setupPage.encryptionKeyFlagText": "{encryptionKeyLink}.{keyFlag}を32文字以上の英数字に設定します。",
|
||||
"xpack.fleet.setupPage.gettingStartedLink": "はじめに",
|
||||
"xpack.fleet.setupPage.gettingStartedText": "詳細については、{link}ガイドをお読みください。",
|
||||
"xpack.fleet.setupPage.kibanaEncryptionLink": "Kibana暗号化鍵",
|
||||
"xpack.fleet.setupPage.kibanaSecurityLink": "Kibanaセキュリティ",
|
||||
"xpack.fleet.setupPage.missingRequirementsCalloutDescription": "Elasticエージェントの集中管理を使用するには、次のElasticsearchとKibanaセキュリティ機能を有効にする必要があります。",
|
||||
"xpack.fleet.setupPage.missingRequirementsCalloutTitle": "不足しているセキュリティ要件",
|
||||
"xpack.fleet.setupPage.missingRequirementsElasticsearchTitle": "Elasticsearchポリシーでは、次のことができます。",
|
||||
"xpack.fleet.setupPage.missingRequirementsKibanaTitle": "Kibanaポリシーでは、次のことができます。",
|
||||
"xpack.fleet.setupPage.tlsFlagText": "{kibanaSecurityLink}.{securityFlag}を{true}に設定します。開発目的では、危険な代替として{tlsFlag}を{true}に設定して、{tlsLink}を無効化できます。",
|
||||
"xpack.fleet.setupPage.tlsLink": "TLS",
|
||||
"xpack.fleet.unenrollAgents.cancelButtonLabel": "キャンセル",
|
||||
"xpack.fleet.unenrollAgents.confirmMultipleButtonLabel": "{count}個のエージェントを登録解除",
|
||||
"xpack.fleet.unenrollAgents.confirmSingleButtonLabel": "エージェントの登録解除",
|
||||
|
|
|
@ -8762,30 +8762,17 @@
|
|||
"xpack.fleet.settings.flyoutTitle": "Fleet 设置",
|
||||
"xpack.fleet.settings.globalOutputTitle": "全局输出",
|
||||
"xpack.fleet.settings.invalidYamlFormatErrorMessage": "YAML 无效:{reason}",
|
||||
"xpack.fleet.settings.kibanaUrlDifferentPathOrProtocolError": "对于每个 URL,协议和路径必须相同",
|
||||
"xpack.fleet.settings.kibanaUrlEmptyError": "至少需要一个 URL",
|
||||
"xpack.fleet.settings.kibanaUrlError": "URL 无效",
|
||||
"xpack.fleet.settings.kibanaUrlLabel": "Kibana URL",
|
||||
"xpack.fleet.settings.saveButtonLabel": "保存设置",
|
||||
"xpack.fleet.settings.success.message": "设置已保存",
|
||||
"xpack.fleet.setupPage.apiKeyServiceLink": "API 密钥服务",
|
||||
"xpack.fleet.setupPage.elasticsearchApiKeyFlagText": "{apiKeyLink}。将 {apiKeyFlag} 设置为 {true}。",
|
||||
"xpack.fleet.setupPage.elasticsearchSecurityFlagText": "{esSecurityLink}。将 {securityFlag} 设置为 {true}。",
|
||||
"xpack.fleet.setupPage.elasticsearchSecurityLink": "Elasticsearch 安全",
|
||||
"xpack.fleet.setupPage.enableCentralManagement": "创建用户并启用集中管理",
|
||||
"xpack.fleet.setupPage.enableText": "集中管理需要可以创建 API 密钥并写入到 logs-* 和 metrics-* 的 Elastic 用户。",
|
||||
"xpack.fleet.setupPage.enableTitle": "对 Elastic 代理启用集中管理",
|
||||
"xpack.fleet.setupPage.encryptionKeyFlagText": "{encryptionKeyLink}。将 {keyFlag} 设置为至少 32 个字符的字母数字值。",
|
||||
"xpack.fleet.setupPage.gettingStartedLink": "入门",
|
||||
"xpack.fleet.setupPage.gettingStartedText": "有关更多信息,请阅读我们的{link}指南。",
|
||||
"xpack.fleet.setupPage.kibanaEncryptionLink": "Kibana 加密密钥",
|
||||
"xpack.fleet.setupPage.kibanaSecurityLink": "Kibana 安全性",
|
||||
"xpack.fleet.setupPage.missingRequirementsCalloutDescription": "要对 Elastic 代理使用集中管理,请启用下面的 Elasticsearch 和 Kibana 安全功能。",
|
||||
"xpack.fleet.setupPage.missingRequirementsCalloutTitle": "缺失安全性要求",
|
||||
"xpack.fleet.setupPage.missingRequirementsElasticsearchTitle": "在 Elasticsearch 策略中,启用:",
|
||||
"xpack.fleet.setupPage.missingRequirementsKibanaTitle": "在 Kibana 策略中,启用:",
|
||||
"xpack.fleet.setupPage.tlsFlagText": "{kibanaSecurityLink}。将 {securityFlag} 设置为 {true}。出于开发目的,作为非安全的备用方案可以通过将 {tlsFlag} 设置为 {true} 来禁用 {tlsLink}。",
|
||||
"xpack.fleet.setupPage.tlsLink": "TLS",
|
||||
"xpack.fleet.unenrollAgents.cancelButtonLabel": "取消",
|
||||
"xpack.fleet.unenrollAgents.confirmMultipleButtonLabel": "取消注册 {count} 个代理",
|
||||
"xpack.fleet.unenrollAgents.confirmSingleButtonLabel": "取消注册代理",
|
||||
|
|
|
@ -21,6 +21,7 @@ export default function (providerContext: FtrProviderContext) {
|
|||
beforeEach(async () => {
|
||||
await esArchiver.unload('fleet/empty_fleet_server');
|
||||
await esArchiver.load('fleet/agents');
|
||||
await getService('supertest').post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').send();
|
||||
});
|
||||
setupFleetAndAgents(providerContext);
|
||||
afterEach(async () => {
|
||||
|
|
|
@ -29,6 +29,7 @@ export default function (providerContext: FtrProviderContext) {
|
|||
beforeEach(async () => {
|
||||
await esArchiver.unload('fleet/empty_fleet_server');
|
||||
await esArchiver.load('fleet/agents');
|
||||
await getService('supertest').post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').send();
|
||||
const { body: accessAPIKeyBody } = await esClient.security.createApiKey({
|
||||
body: {
|
||||
name: `test access api key: ${uuid.v4()}`,
|
||||
|
|
|
@ -37,7 +37,7 @@ export default function (providerContext: FtrProviderContext) {
|
|||
.get(`/api/fleet/enrollment-api-keys`)
|
||||
.expect(200);
|
||||
|
||||
expect(apiResponse.total).to.be(4);
|
||||
expect(apiResponse.total).to.be(3);
|
||||
expect(apiResponse.list[0]).to.have.keys('id', 'api_key_id', 'name');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -64,59 +64,6 @@ export default function (providerContext: FtrProviderContext) {
|
|||
}
|
||||
});
|
||||
|
||||
it('should update the fleet_enroll role with new index permissions if one does already exist', async () => {
|
||||
try {
|
||||
await es.security.putRole({
|
||||
name: 'fleet_enroll',
|
||||
body: {
|
||||
cluster: ['monitor', 'manage_api_key'],
|
||||
indices: [
|
||||
{
|
||||
names: ['logs-*', 'metrics-*', 'traces-*'],
|
||||
privileges: ['create_doc', 'indices:admin/auto_create'],
|
||||
allow_restricted_indices: false,
|
||||
},
|
||||
],
|
||||
applications: [],
|
||||
run_as: [],
|
||||
metadata: {},
|
||||
// @ts-expect-error @elastic/elasticsearch PutRoleRequest.body doesn't declare transient_metadata property
|
||||
transient_metadata: { enabled: true },
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.meta?.statusCode !== 404) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const { body: apiResponse } = await supertest
|
||||
.post(`/api/fleet/setup`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.expect(200);
|
||||
|
||||
expect(apiResponse.isInitialized).to.be(true);
|
||||
|
||||
const { body: roleResponse } = await es.security.getRole({
|
||||
name: 'fleet_enroll',
|
||||
});
|
||||
expect(roleResponse).to.have.key('fleet_enroll');
|
||||
expect(roleResponse.fleet_enroll).to.eql({
|
||||
cluster: ['monitor', 'manage_api_key'],
|
||||
indices: [
|
||||
{
|
||||
names: ['logs-*', 'metrics-*', 'traces-*', '.logs-endpoint.diagnostic.collection-*'],
|
||||
privileges: ['auto_configure', 'create_doc'],
|
||||
allow_restricted_indices: false,
|
||||
},
|
||||
],
|
||||
applications: [],
|
||||
run_as: [],
|
||||
metadata: {},
|
||||
transient_metadata: { enabled: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('should install default packages', async () => {
|
||||
await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxxx').expect(200);
|
||||
|
||||
|
|
Loading…
Reference in a new issue