Skip to content
Merged
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/common/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export const SETTINGS_API_ROUTES = {
// App API routes
export const APP_API_ROUTES = {
CHECK_PERMISSIONS_PATTERN: `${API_ROOT}/check-permissions`,
GENERATE_SERVICE_TOKEN_PATTERN: `${API_ROOT}/service-tokens`,
};

// Agent API routes
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/common/services/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ export const settingsRoutesService = {

export const appRoutesService = {
getCheckPermissionsPath: () => APP_API_ROUTES.CHECK_PERMISSIONS_PATTERN,
getRegenerateServiceTokenPath: () => APP_API_ROUTES.GENERATE_SERVICE_TOKEN_PATTERN,
};

export const enrollmentAPIKeyRouteService = {
Expand Down
5 changes: 5 additions & 0 deletions x-pack/plugins/fleet/common/types/rest_spec/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ export interface CheckPermissionsResponse {
error?: 'MISSING_SECURITY' | 'MISSING_SUPERUSER_ROLE';
success: boolean;
}

export interface GenerateServiceTokenResponse {
name: string;
value: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { appRoutesService } from '../../services';
import type { CheckPermissionsResponse } from '../../types';
import type { CheckPermissionsResponse, GenerateServiceTokenResponse } from '../../types';

import { sendRequest } from './use_request';

Expand All @@ -16,3 +16,10 @@ export const sendGetPermissionsCheck = () => {
method: 'get',
});
};

export const sendGenerateServiceToken = () => {
return sendRequest<GenerateServiceTokenResponse>({
path: appRoutesService.getRegenerateServiceTokenPath(),
method: 'post',
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import React from 'react';
import React, { useState, useMemo, useCallback } from 'react';
import {
EuiButton,
EuiFlexGroup,
Expand All @@ -16,62 +16,229 @@ import {
EuiText,
EuiLink,
EuiEmptyPrompt,
EuiSteps,
EuiCodeBlock,
EuiCallOut,
EuiSelect,
} from '@elastic/eui';
import styled from 'styled-components';
import { FormattedMessage } from 'react-intl';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';

import { useStartServices } from '../../../hooks';
import { DownloadStep } from '../components/agent_enrollment_flyout/steps';
import { useStartServices, useGetOutputs, sendGenerateServiceToken } from '../../../hooks';

const FlexItemWithMinWidth = styled(EuiFlexItem)`
min-width: 0px;
max-width: 100%;
`;

export const ContentWrapper = styled(EuiFlexGroup)`
height: 100%;
margin: 0 auto;
max-width: 800px;
`;

function renderOnPremInstructions() {
// Otherwise the copy button is over the text
const CommandCode = styled.pre({
overflow: 'scroll',
});

type PLATFORM_TYPE = 'linux-mac' | 'windows' | 'rpm-deb';
const PLATFORM_OPTIONS: Array<{ text: string; value: PLATFORM_TYPE }> = [
{ text: 'Linux / macOS', value: 'linux-mac' },
{ text: 'Windows', value: 'windows' },
{ text: 'RPM / DEB', value: 'rpm-deb' },
];

const OnPremInstructions: React.FC = () => {
const outputsRequest = useGetOutputs();
const { notifications } = useStartServices();
const [serviceToken, setServiceToken] = useState<string>();
const [isLoadingServiceToken, setIsLoadingServiceToken] = useState<boolean>(false);
const [platform, setPlatform] = useState<PLATFORM_TYPE>('linux-mac');

const output = outputsRequest.data?.items?.[0];
const esHost = output?.hosts?.[0];

const installCommand = useMemo((): string => {
if (!serviceToken || !esHost) {
return '';
}
switch (platform) {
case 'linux-mac':
return `sudo ./elastic-agent install -f --fleet-server-es=${esHost} --fleet-server-service-token=${serviceToken}`;
case 'windows':
return `.\\elastic-agent.exe install --fleet-server-es=${esHost} --fleet-server-service-token=${serviceToken}`;
case 'rpm-deb':
return `sudo elastic-agent enroll -f --fleet-server-es=${esHost} --fleet-server-service-token=${serviceToken}`;
default:
return '';
}
}, [serviceToken, esHost, platform]);

const getServiceToken = useCallback(async () => {
setIsLoadingServiceToken(true);
try {
const { data } = await sendGenerateServiceToken();
if (data?.value) {
setServiceToken(data?.value);
}
} catch (err) {
notifications.toasts.addError(err, {
title: i18n.translate('xpack.fleet.fleetServerSetup.errorGeneratingTokenTitleText', {
defaultMessage: 'Error generating token',
}),
});
}

setIsLoadingServiceToken(false);
}, [notifications]);

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={
<EuiPanel paddingSize="l" grow={false} hasShadow={false} hasBorder={true}>
<EuiSpacer size="s" />
<EuiText className="eui-textCenter">
<h2>
<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."
id="xpack.fleet.fleetServerSetup.setupTitle"
defaultMessage="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>
}
</h2>
<EuiSpacer size="m" />
<FormattedMessage
id="xpack.fleet.fleetServerSetup.setupText"
defaultMessage="A Fleet Server is required before you can enroll agents with Fleet. See the {userGuideLink} for more information."
values={{
userGuideLink: (
<EuiLink href="https://ela.st/add-fleet-server" external>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.setupGuideLink"
defaultMessage="Fleet User Guide"
/>
</EuiLink>
),
}}
/>
</EuiText>
<EuiSpacer size="l" />
<EuiSteps
className="eui-textLeft"
steps={[
DownloadStep(),
{
title: i18n.translate('xpack.fleet.fleetServerSetup.stepGenerateServiceTokenTitle', {
defaultMessage: 'Generate a service token',
}),
children: (
<>
<EuiText>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.generateServiceTokenDescription"
defaultMessage="A service token grants Fleet Server permissions to write to Elasticsearch."
/>
</EuiText>
<EuiSpacer size="m" />
{!serviceToken ? (
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton
fill
isLoading={isLoadingServiceToken}
isDisabled={isLoadingServiceToken}
onClick={() => {
getServiceToken();
}}
>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.generateServiceTokenButton"
defaultMessage="Generate service token"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
) : (
<>
<EuiCallOut size="s">
<FormattedMessage
id="xpack.fleet.fleetServerSetup.saveServiceTokenDescription"
defaultMessage="Save your service token information. This will be shown only once."
/>
</EuiCallOut>
<EuiSpacer size="m" />
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<strong>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.serviceTokenLabel"
defaultMessage="Service token"
/>
</strong>
</EuiFlexItem>
<FlexItemWithMinWidth>
<EuiCodeBlock paddingSize="m" isCopyable>
<CommandCode>{serviceToken}</CommandCode>
</EuiCodeBlock>
</FlexItemWithMinWidth>
</EuiFlexGroup>
</>
)}
</>
),
},
{
title: i18n.translate('xpack.fleet.fleetServerSetup.stepInstallAgentTitle', {
defaultMessage: 'Install the Elastic Agent as a Fleet Server',
}),
status: !serviceToken ? 'disabled' : undefined,
children: serviceToken ? (
<>
<EuiText>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.installAgentDescription"
defaultMessage="From the agent directory, run the appropriate command to install, enroll, and start an Elastic Agent as a Fleet Server. Requires administrator privileges."
/>
</EuiText>
<EuiSpacer size="l" />
<EuiSelect
prepend={
<EuiText>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.platformSelectLabel"
defaultMessage="Platform"
/>
</EuiText>
}
options={PLATFORM_OPTIONS}
value={platform}
onChange={(e) => setPlatform(e.target.value as PLATFORM_TYPE)}
aria-label={i18n.translate(
'xpack.fleet.fleetServerSetup.platformSelectAriaLabel',
{
defaultMessage: 'Platform',
}
)}
/>
<EuiSpacer size="s" />
<EuiCodeBlock
fontSize="m"
isCopyable={true}
paddingSize="m"
language="console"
whiteSpace="pre"
>
<CommandCode>{installCommand}</CommandCode>
</EuiCodeBlock>
</>
) : null,
},
]}
/>
</EuiPanel>
);
}
};

function renderCloudInstructions(deploymentUrl: string) {
const CloudInstructions: React.FC<{ deploymentUrl: string }> = ({ deploymentUrl }) => {
return (
<EuiPanel
paddingSize="none"
Expand Down Expand Up @@ -126,19 +293,24 @@ function renderCloudInstructions(deploymentUrl: string) {
/>
</EuiPanel>
);
}
};

export const FleetServerRequirementPage = () => {
const startService = useStartServices();
const deploymentUrl = startService.cloud?.deploymentUrl;

return (
<>
<ContentWrapper justifyContent="center" alignItems="center">
<ContentWrapper gutterSize="l" justifyContent="center" alignItems="center" direction="column">
<FlexItemWithMinWidth grow={false}>
{deploymentUrl ? (
<CloudInstructions deploymentUrl={deploymentUrl} />
) : (
<OnPremInstructions />
)}
</FlexItemWithMinWidth>
<EuiFlexItem grow={false}>
{deploymentUrl ? renderCloudInstructions(deploymentUrl) : renderOnPremInstructions()}
<EuiSpacer size="l" />
<EuiFlexGroup gutterSize="s" alignItems="center" justifyContent="center">
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="m" />
</EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,12 @@ export const AgentEnrollmentFlyout: React.FunctionComponent<Props> = ({

<EuiFlyoutBody
banner={
fleetServerHosts.length === 0 ? (
fleetServerHosts.length === 0 && mode === 'managed' ? (
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also updated the Add agent flyout so that the Run standalone tab can be viewed without Fleet Server being setup. I thought this was appropriate since, well, those are standalone agents! @nchaulet maybe you can help me double check if this makes sense.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it make sense 👍

<MissingFleetServerHostCallout onClose={onClose} />
) : undefined
}
>
{fleetServerHosts.length === 0 ? null : mode === 'managed' ? (
{fleetServerHosts.length === 0 && mode === 'managed' ? null : mode === 'managed' ? (
<ManagedInstructions agentPolicies={agentPolicies} />
) : (
<StandaloneInstructions agentPolicies={agentPolicies} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export {
PutSettingsResponse,
// API schemas - app
CheckPermissionsResponse,
GenerateServiceTokenResponse,
// EPM types
AssetReference,
AssetsGroupedByServiceByType,
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/server/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export class AgentUnenrollmentError extends IngestManagerError {}
export class AgentPolicyDeletionError extends IngestManagerError {}

export class FleetSetupError extends IngestManagerError {}
export class GenerateServiceTokenError extends IngestManagerError {}

export class ArtifactsClientError extends IngestManagerError {}
export class ArtifactsClientAccessDeniedError extends IngestManagerError {
Expand Down
Loading