Skip to content

Commit

Permalink
[Fleet] Spacetime: Add fleet server health check to debug page (#146594)
Browse files Browse the repository at this point in the history
## Summary
Closes elastic/ingest-dev#1422
Closes #143644
Fixes #141635

### Description
Spacetime project:
- Fixing UI layout 
- Adding a new endpoint to check the connection to fleet-server. The
endpoint executes `curl -s -k <hostname>/api/status` and can be called
from dev tools as:
```
POST kbn:/api/fleet/health_check
{
  "host": $hostname
}
```
Where `$hostname` is the host configured in fleet server hosts settings
section.
- Adding a Fleet Server health check to the debug page
`app/fleet/_debug`. The host can be selected via a dropdown.
- Moving debug page outside of setup to allow debugging when fleet
couldn't initialise. I added a warning on top of the page in this
specific case - #143644
- Added some more saved objects and indices that weren't added the first
time round to allow further debugging.

### Repro steps:
To try that the page loads even when setup didn't work, I changed some
values in the code. Comment out [this
code](https://github.com/elastic/kibana/blob/main/x-pack/plugins/fleet/public/applications/fleet/app.tsx#L163-L165)
and replace it with `setInitializationError({ name: 'error', message:
'unable to initialize' });`, then set `false` in
[here](https://github.com/elastic/kibana/blob/b155134d662b8011857f7763a063ce87b3b6a91d/x-pack/plugins/fleet/public/applications/fleet/app.tsx#L179)
and the fleet UI should show a setup error, however now the debug page
should be visible.

### Screenshots
<img width="2076" alt="Screenshot 2022-11-30 at 15 25 51"
src="https://user-images.githubusercontent.com/16084106/204824818-d620aabf-83b1-4acd-9f38-6f271d17a38a.png">
<img width="1403" alt="Screenshot 2022-11-30 at 15 26 36"
src="https://user-images.githubusercontent.com/16084106/204824851-04b36d5e-e466-4f0c-9eed-b8b492f128b9.png">
<img width="2063" alt="Screenshot 2022-11-30 at 15 27 09"
src="https://user-images.githubusercontent.com/16084106/204824909-a26a8df1-38ba-4553-984f-fce13a3abf8d.png">
<img width="2110" alt="Screenshot 2022-12-01 at 17 36 57"
src="https://user-images.githubusercontent.com/16084106/205110349-a682a894-767e-47f9-beb9-7f9c39bece72.png">


### Checklist

- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
criamico and kibanamachine authored Dec 5, 2022
1 parent b5ba42b commit 035ebc4
Show file tree
Hide file tree
Showing 16 changed files with 422 additions and 104 deletions.
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 @@ -259,6 +259,7 @@ export const settingsRoutesService = {
export const appRoutesService = {
getCheckPermissionsPath: (fleetServerSetup?: boolean) => APP_API_ROUTES.CHECK_PERMISSIONS_PATTERN,
getRegenerateServiceTokenPath: () => APP_API_ROUTES.GENERATE_SERVICE_TOKEN_PATTERN,
postHealthCheckPath: () => APP_API_ROUTES.HEALTH_CHECK_PATTERN,
};

export const enrollmentAPIKeyRouteService = {
Expand Down
18 changes: 18 additions & 0 deletions x-pack/plugins/fleet/common/types/rest_spec/health_check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* 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 interface PostHealthCheckRequest {
body: {
host: string;
};
}

export interface PostHealthCheckResponse {
name: string;
host: string;
status: string;
}
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/common/types/rest_spec/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ export * from './fleet_setup';
export * from './output';
export * from './package_policy';
export * from './settings';
export * from './health_check';
9 changes: 5 additions & 4 deletions x-pack/plugins/fleet/public/applications/fleet/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ export const WithPermissionsAndSetup: React.FC = memo(({ children }) => {
const [initializationError, setInitializationError] = useState<Error | null>(null);

const isAddIntegrationsPath = !!useRouteMatch(FLEET_ROUTING_PATHS.add_integration_to_policy);
const isDebugPath = !!useRouteMatch(FLEET_ROUTING_PATHS.debug);

useEffect(() => {
(async () => {
Expand Down Expand Up @@ -193,6 +194,10 @@ export const WithPermissionsAndSetup: React.FC = memo(({ children }) => {
</ErrorLayout>
);
}
// Debug page moved outside of initialization to allow debugging when setup failed
if (isDebugPath) {
return <DebugPage setupError={initializationError} isInitialized={isInitialized} />;
}

if (!isInitialized || initializationError) {
return (
Expand Down Expand Up @@ -328,10 +333,6 @@ export const AppRoutes = memo(
<SettingsApp />
</Route>

<Route path={FLEET_ROUTING_PATHS.debug}>
<DebugPage />
</Route>

{/* TODO: Move this route to the Integrations app */}
<Route path={FLEET_ROUTING_PATHS.add_integration_to_policy}>
<CreatePackagePolicyPage />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export const FleetIndexDebugger = () => {
const indices = [
{ label: '.fleet-agents', value: '.fleet-agents' },
{ label: '.fleet-actions', value: '.fleet-actions' },
{ label: '.fleet-servers', value: '.fleet-servers' },
{ label: '.fleet-enrollment-api-keys', value: '.fleet-enrollment-api-keys' },
];
const [index, setIndex] = useState<string | undefined>();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*
* 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, useState, useMemo } from 'react';

import {
EuiSpacer,
EuiText,
EuiSuperSelect,
EuiFlexGroup,
EuiFlexItem,
EuiCallOut,
EuiHealth,
} from '@elastic/eui';

import { FormattedMessage } from '@kbn/i18n-react';
import { useQuery } from '@tanstack/react-query';

import { sendPostHealthCheck, useGetFleetServerHosts } from '../../../hooks';
import type { FleetServerHost } from '../../../types';

const POLLING_INTERVAL_S = 10; // 10 sec
const POLLING_INTERVAL_MS = POLLING_INTERVAL_S * 1000;

export const HealthCheckPanel: React.FunctionComponent = () => {
const [selectedFleetServerHost, setSelectedFleetServerHost] = useState<
FleetServerHost | undefined
>();

const { data } = useGetFleetServerHosts();
const fleetServerHosts = useMemo(() => data?.items ?? [], [data?.items]);

useEffect(() => {
const defaultHost = fleetServerHosts.find((item) => item.is_default === true);
if (defaultHost) {
setSelectedFleetServerHost(defaultHost);
}
}, [fleetServerHosts]);

const hostName = useMemo(
() => selectedFleetServerHost?.host_urls[0] || '',
[selectedFleetServerHost?.host_urls]
);

const [healthData, setHealthData] = useState<any>();

const { data: healthCheckResponse } = useQuery(
['fleetServerHealth', hostName],
() => sendPostHealthCheck({ host: hostName }),
{ refetchInterval: POLLING_INTERVAL_MS }
);
useEffect(() => {
setHealthData(healthCheckResponse);
}, [healthCheckResponse]);

const fleetServerHostsOptions = useMemo(
() => [
...fleetServerHosts.map((fleetServerHost) => {
return {
inputDisplay: `${fleetServerHost.name} (${fleetServerHost.host_urls[0]})`,
value: fleetServerHost.id,
};
}),
],
[fleetServerHosts]
);

const healthStatus = (statusValue: string) => {
if (!statusValue) return null;

let color;
switch (statusValue) {
case 'HEALTHY':
color = 'success';
break;
case 'UNHEALTHY':
color = 'warning';
break;
case 'OFFLINE':
color = 'subdued';
break;
default:
color = 'subdued';
}

return <EuiHealth color={color}>{statusValue}</EuiHealth>;
};

return (
<>
<EuiText grow={false}>
<p>
<FormattedMessage
id="xpack.fleet.debug.healthCheckPanel.description"
defaultMessage="Select the host used to enroll Fleet Server. The connection is refreshed every {interval}s."
values={{
interval: POLLING_INTERVAL_S,
}}
/>
</p>
</EuiText>

<EuiSpacer size="m" />
<EuiFlexGroup alignItems="center">
<EuiFlexItem
grow={false}
css={`
min-width: 600px;
`}
>
<EuiSuperSelect
fullWidth
data-test-subj="fleetDebug.fleetServerHostsSelect"
prepend={
<EuiText size="relative" color={''}>
<FormattedMessage
id="xpack.fleet.debug.healthCheckPanel.fleetServerHostsLabel"
defaultMessage="Fleet Server Hosts"
/>
</EuiText>
}
onChange={(fleetServerHostId) => {
setHealthData(undefined);
setSelectedFleetServerHost(
fleetServerHosts.find((fleetServerHost) => fleetServerHost.id === fleetServerHostId)
);
}}
valueOfSelected={selectedFleetServerHost?.id}
options={fleetServerHostsOptions}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{healthData?.data?.status && hostName === healthData?.data?.host ? (
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<FormattedMessage
id="xpack.fleet.debug.healthCheckPanel.status"
defaultMessage="Status:"
/>
</EuiFlexItem>
<EuiFlexItem>{healthStatus(healthData?.data?.status)}</EuiFlexItem>
</EuiFlexGroup>
) : null}
</EuiFlexItem>
</EuiFlexGroup>
{healthData?.error && (
<>
<EuiSpacer size="m" />
<EuiCallOut title="Error" color="danger">
{healthData?.error?.message ?? (
<FormattedMessage
id="xpack.fleet.debug.healthCheckPanel.fetchError"
defaultMessage="Message: {errorMessage}"
values={{
errorMessage: healthData?.error?.message,
}}
/>
)}
</EuiCallOut>
</>
)}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from './saved_object_debugger';
export * from './preconfiguration_debugger';
export * from './fleet_index_debugger';
export * from './orphaned_integration_policy_debugger';
export * from './health_check_panel';
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export const PreconfigurationDebugger: React.FunctionComponent = () => {
<p>
<FormattedMessage
id="xpack.fleet.debug.preconfigurationDebugger.description"
defaultMessage="This tool can be used to reset preconfigured policies that are managed via {codeKibanaYml}. This includes Fleet's default policies that may existin cloud environments."
defaultMessage="This tool can be used to reset preconfigured policies that are managed via {codeKibanaYml}. This includes Fleet's default policies that may exist in cloud environments."
values={{ codeKibanaYml: <EuiCode>kibana.yml</EuiCode> }}
/>
</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ import { FormattedMessage } from '@kbn/i18n-react';

import { sendRequest } from '../../../hooks';

import {
OUTPUT_SAVED_OBJECT_TYPE,
AGENT_POLICY_SAVED_OBJECT_TYPE,
PACKAGE_POLICY_SAVED_OBJECT_TYPE,
PACKAGES_SAVED_OBJECT_TYPE,
DOWNLOAD_SOURCE_SAVED_OBJECT_TYPE,
FLEET_SERVER_HOST_SAVED_OBJECT_TYPE,
} from '../../../../../../common/constants';

import { CodeBlock } from './code_block';
import { SavedObjectNamesCombo } from './saved_object_names_combo';

Expand Down Expand Up @@ -61,29 +70,41 @@ const fetchSavedObjects = async (type?: string, name?: string) => {
export const SavedObjectDebugger: React.FunctionComponent = () => {
const types = [
{
value: 'ingest-agent-policies',
value: `${AGENT_POLICY_SAVED_OBJECT_TYPE}`,
text: i18n.translate('xpack.fleet.debug.savedObjectDebugger.agentPolicyLabel', {
defaultMessage: 'Agent policy',
}),
},
{
value: 'ingest-package-policies',
value: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}`,
text: i18n.translate('xpack.fleet.debug.savedObjectDebugger.packagePolicyLabel', {
defaultMessage: 'Integration policy',
}),
},
{
value: 'ingest-outputs',
value: `${OUTPUT_SAVED_OBJECT_TYPE}`,
text: i18n.translate('xpack.fleet.debug.savedObjectDebugger.outputLabel', {
defaultMessage: 'Output',
}),
},
{
value: 'epm-packages',
value: `${PACKAGES_SAVED_OBJECT_TYPE}`,
text: i18n.translate('xpack.fleet.debug.savedObjectDebugger.packageLabel', {
defaultMessage: 'Packages',
}),
},
{
value: `${DOWNLOAD_SOURCE_SAVED_OBJECT_TYPE}`,
text: i18n.translate('xpack.fleet.debug.savedObjectDebugger.downloadSourceLabel', {
defaultMessage: 'Download Sources',
}),
},
{
value: `${FLEET_SERVER_HOST_SAVED_OBJECT_TYPE}`,
text: i18n.translate('xpack.fleet.debug.savedObjectDebugger.fleetServerHostLabel', {
defaultMessage: 'Fleet Server Hosts',
}),
},
];

const [type, setType] = useState(types[0].value);
Expand Down Expand Up @@ -153,7 +174,7 @@ export const SavedObjectDebugger: React.FunctionComponent = () => {
</EuiFlexItem>
</EuiFlexGroup>

{(status === 'error' || namesStatus === 'error') && (
{savedObjectResult && (status === 'error' || namesStatus === 'error') && (
<>
<EuiSpacer size="m" />
<EuiCallOut title="Error" color="danger">
Expand Down
Loading

0 comments on commit 035ebc4

Please sign in to comment.