Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using resource extension to show health on dashboards #4824

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -1,6 +1,6 @@
import * as React from 'react';
import { Gallery, GalleryItem } from '@patternfly/react-core';
import * as _ from 'lodash';
import { Gallery, GalleryItem } from '@patternfly/react-core';
import AlertsBody from '@console/shared/src/components/dashboard/status-card/AlertsBody';
import AlertItem from '@console/shared/src/components/dashboard/status-card/AlertItem';
import { alertURL } from '@console/internal/components/monitoring';
Expand All @@ -15,15 +15,13 @@ import {
withDashboardResources,
DashboardItemProps,
} from '@console/internal/components/dashboard/with-dashboard-resources';
import {
DATA_RESILIENCY_QUERY,
STORAGE_HEALTH_QUERIES,
StorageDashboardQuery,
} from '../../../../constants/queries';
import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook';
import { K8sResourceKind } from '@console/internal/module/k8s';
import { DATA_RESILIENCY_QUERY, StorageDashboardQuery } from '../../../../constants/queries';
import { cephClusterResource } from '../../../../constants/resources';
import { filterCephAlerts } from '../../../../selectors';
import { getCephHealthState, getDataResiliencyState } from './utils';

const cephStatusQuery = STORAGE_HEALTH_QUERIES[StorageDashboardQuery.CEPH_STATUS_QUERY];
const resiliencyProgressQuery = DATA_RESILIENCY_QUERY[StorageDashboardQuery.RESILIENCY_PROGRESS];

export const CephAlerts = withDashboardResources(
Expand Down Expand Up @@ -57,25 +55,23 @@ export const StatusCard: React.FC<DashboardItemProps> = ({
stopWatchPrometheusQuery,
prometheusResults,
}) => {
const [data, loaded, loadError] = useK8sWatchResource<K8sResourceKind[]>(cephClusterResource);

React.useEffect(() => {
watchPrometheus(cephStatusQuery);
watchPrometheus(resiliencyProgressQuery);

return () => {
stopWatchPrometheusQuery(cephStatusQuery);
stopWatchPrometheusQuery(resiliencyProgressQuery);
};
}, [watchPrometheus, stopWatchPrometheusQuery]);

const cephStatus = prometheusResults.getIn([cephStatusQuery, 'data']) as PrometheusResponse;
const cephStatusError = prometheusResults.getIn([cephStatusQuery, 'loadError']);
const resiliencyProgress = prometheusResults.getIn([
resiliencyProgressQuery,
'data',
]) as PrometheusResponse;
const resiliencyProgressError = prometheusResults.getIn([resiliencyProgressQuery, 'loadError']);

const cephHealthState = getCephHealthState([cephStatus], [cephStatusError]);
const cephHealthState = getCephHealthState({ ceph: { data, loaded, loadError } });
const dataResiliencyState = getDataResiliencyState(
[resiliencyProgress],
[resiliencyProgressError],
Expand Down
@@ -1,33 +1,34 @@
import * as _ from 'lodash';
import { PrometheusHealthHandler, ResourceHealthHandler } from '@console/plugin-sdk';
import { HealthState } from '@console/shared/src/components/dashboard/status-card/states';
import { PrometheusHealthHandler } from '@console/plugin-sdk';
import { getResiliencyProgress } from '../../../../utils';
import { WatchCephResource } from '../../../../types';

const CephHealthStatus = [
{
const CephHealthStatus = {
HEALTH_OK: {
state: HealthState.OK,
},
{
HEALTH_WARN: {
state: HealthState.WARNING,
},
{
HEALTH_ERR: {
afreen23 marked this conversation as resolved.
Show resolved Hide resolved
state: HealthState.ERROR,
},
{
state: HealthState.NOT_AVAILABLE,
},
];
};

export const getCephHealthState: PrometheusHealthHandler = (responses = [], errors = []) => {
if (errors[0]) {
return CephHealthStatus[3];
export const getCephHealthState: ResourceHealthHandler<WatchCephResource> = ({ ceph }) => {
const { data, loaded, loadError } = ceph;
const status = data?.[0]?.status?.ceph?.health;

if (loadError) {
return { state: HealthState.NOT_AVAILABLE };
}
if (!responses[0]) {
if (!loaded) {
return { state: HealthState.LOADING };
}

const value = _.get(responses[0], 'data.result[0].value[1]');
return CephHealthStatus[value] || CephHealthStatus[3];
if (data.length === 0) {
return { state: HealthState.NOT_AVAILABLE };
}
return CephHealthStatus[status] || { state: HealthState.UNKNOWN };
};

export const getDataResiliencyState: PrometheusHealthHandler = (responses = [], errors = []) => {
Expand Down
@@ -1,43 +1,20 @@
import * as React from 'react';
import { GalleryItem, Gallery } from '@patternfly/react-core';
import { withDashboardResources } from '@console/internal/components/dashboard/with-dashboard-resources';
import DashboardCard from '@console/shared/src/components/dashboard/dashboard-card/DashboardCard';
import DashboardCardTitle from '@console/shared/src/components/dashboard/dashboard-card/DashboardCardTitle';
import HealthBody from '@console/shared/src/components/dashboard/status-card/HealthBody';
import DashboardCardHeader from '@console/shared/src/components/dashboard/dashboard-card/DashboardCardHeader';
import DashboardCardBody from '@console/shared/src/components/dashboard/dashboard-card/DashboardCardBody';
import HealthItem from '@console/shared/src/components/dashboard/status-card/HealthItem';
import { referenceForModel, K8sResourceKind } from '@console/internal/module/k8s';
import { FirehoseResource, FirehoseResult } from '@console/internal/components/utils/types';
import { OCSServiceModel } from '../../../models';
import { getClusterHealth } from '../../independent-mode/utils';
import { OCS_INDEPENDENT_CR_NAME, CEPH_STORAGE_NAMESPACE } from '../../../constants';
import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook';
import { K8sResourceKind } from '@console/internal/module/k8s';
import { getCephHealthState } from '../../dashboard-page/storage-dashboard/status-card/utils';
import { cephClusterResource } from '../../../constants/resources';

const clusterResource: FirehoseResource = {
kind: referenceForModel(OCSServiceModel),
name: OCS_INDEPENDENT_CR_NAME,
namespaced: true,
namespace: CEPH_STORAGE_NAMESPACE,
isList: false,
prop: 'ocs',
};

const StatusCard = withDashboardResources((props) => {
const { watchK8sResource, stopWatchK8sResource, resources } = props;

React.useEffect(() => {
watchK8sResource(clusterResource);
return () => {
stopWatchK8sResource(clusterResource);
};
}, [watchK8sResource, stopWatchK8sResource]);
const StatusCard: React.FC = () => {
const [data, loaded, loadError] = useK8sWatchResource<K8sResourceKind[]>(cephClusterResource);

const cluster = resources?.ocs as FirehoseResult<K8sResourceKind>;
const data = cluster?.data;
const loaded = cluster?.loaded;
const error = cluster?.loadError;

const status = getClusterHealth(data, loaded, error);
const cephHealth = getCephHealthState({ ceph: { data, loaded, loadError } });

return (
<DashboardCard gradient>
Expand All @@ -48,13 +25,13 @@ const StatusCard = withDashboardResources((props) => {
<HealthBody>
<Gallery className="co-overview-status__health" gutter="md">
<GalleryItem>
<HealthItem title="OCS Cluster" state={status} />
<HealthItem title="OCS Cluster" state={cephHealth.state} />
</GalleryItem>
</Gallery>
</HealthBody>
</DashboardCardBody>
</DashboardCard>
);
});
};

export default StatusCard;
Expand Up @@ -8,10 +8,3 @@ export const cephClusterResource: FirehoseResource = {
isList: true,
prop: 'ceph',
};
export enum StorageDashboardResource {
CEPH_CLUSTER_RESOURCE = 'CEPH_CLUSTER_RESOURCE',
}

export const STORAGE_HEALTH_RESOURCES = {
[StorageDashboardResource.CEPH_CLUSTER_RESOURCE]: cephClusterResource,
};
21 changes: 12 additions & 9 deletions frontend/packages/ceph-storage-plugin/src/plugin.ts
@@ -1,14 +1,10 @@
import * as _ from 'lodash';
import * as models from './models';
import {
CAPACITY_USAGE_QUERIES,
STORAGE_HEALTH_QUERIES,
StorageDashboardQuery,
} from './constants/queries';
import { CAPACITY_USAGE_QUERIES, StorageDashboardQuery } from './constants/queries';
import {
ClusterServiceVersionAction,
DashboardsCard,
DashboardsOverviewHealthPrometheusSubsystem,
DashboardsOverviewHealthResourceSubsystem,
DashboardsOverviewUtilizationItem,
DashboardsTab,
FeatureFlag,
Expand All @@ -35,13 +31,14 @@ import { referenceForModel, referenceFor } from '@console/internal/module/k8s';
import { PersistentVolumeClaimModel } from '@console/internal/models';
import { getCephHealthState } from './components/dashboard-page/storage-dashboard/status-card/utils';
import { isClusterExpandActivity } from './components/dashboard-page/storage-dashboard/activity-card/cluster-expand-activity';
import { WatchCephResource } from './types';

type ConsumedExtensions =
| ModelFeatureFlag
| ModelDefinition
| DashboardsTab
| DashboardsCard
| DashboardsOverviewHealthPrometheusSubsystem
| DashboardsOverviewHealthResourceSubsystem<WatchCephResource>
| DashboardsOverviewUtilizationItem
| RoutePage
| ActionFeatureFlag
Expand Down Expand Up @@ -205,10 +202,16 @@ const plugin: Plugin<ConsumedExtensions> = [
},
},
{
type: 'Dashboards/Overview/Health/Prometheus',
type: 'Dashboards/Overview/Health/Resource',
properties: {
title: 'Storage',
queries: [STORAGE_HEALTH_QUERIES[StorageDashboardQuery.CEPH_STATUS_QUERY]],
resources: {
ceph: {
kind: referenceForModel(models.CephClusterModel),
namespaced: false,
isList: true,
},
},
healthHandler: getCephHealthState,
},
flags: {
Expand Down
5 changes: 5 additions & 0 deletions frontend/packages/ceph-storage-plugin/src/types.ts
@@ -0,0 +1,5 @@
import { K8sResourceKind } from '@console/internal/module/k8s';

export type WatchCephResource = {
ceph: K8sResourceKind[];
};
Expand Up @@ -28,29 +28,33 @@ const highVuln: ImageManifestVuln = {

describe('securityHealthHandler', () => {
it('returns `UNKNOWN` status if there is an error retrieving `ImageManifestVulns`', () => {
const vulnerabilities = { loaded: true, loadError: 'failed to fetch', data: [] };
const health = securityHealthHandler(null, null, vulnerabilities);
const vulnerabilities = {
imageManifestVuln: { loaded: true, loadError: 'failed to fetch', data: [] },
};
const health = securityHealthHandler(vulnerabilities);

expect(health.state).toEqual(HealthState.UNKNOWN);
});

it('returns `LOADING` status if still retrieving `ImageManifestVulns`', () => {
const vulnerabilities = { loaded: false, loadError: null, data: [] };
const health = securityHealthHandler(null, null, vulnerabilities);
const vulnerabilities = { imageManifestVuln: { loaded: false, loadError: null, data: [] } };
const health = securityHealthHandler(vulnerabilities);

expect(health.state).toEqual(HealthState.LOADING);
});

it('returns `Error` status if any `ImageManifestVulns` exist', () => {
const vulnerabilities = { loaded: true, loadError: null, data: [highVuln] };
const health = securityHealthHandler(null, null, vulnerabilities);
const vulnerabilities = {
imageManifestVuln: { loaded: true, loadError: null, data: [highVuln] },
};
const health = securityHealthHandler(vulnerabilities);

expect(health.state).toEqual(HealthState.ERROR);
});

it('returns `OK` status if no vulnerabilities', () => {
const vulnerabilities = { loaded: true, loadError: null, data: [] };
const health = securityHealthHandler(null, null, vulnerabilities);
const vulnerabilities = { imageManifestVuln: { loaded: true, loadError: null, data: [] } };
const health = securityHealthHandler(vulnerabilities);

expect(health.state).toEqual(HealthState.OK);
});
Expand Down
50 changes: 23 additions & 27 deletions frontend/packages/container-security/src/components/summary.tsx
Expand Up @@ -3,28 +3,28 @@ import * as _ from 'lodash';
import { pluralize } from '@patternfly/react-core';
import { ChartDonut } from '@patternfly/react-charts';
import { SecurityIcon } from '@patternfly/react-icons';
import { URLHealthHandler } from '@console/plugin-sdk';
import { ResourceHealthHandler } from '@console/plugin-sdk';
import { WatchK8sResults } from '@console/internal/components/utils/k8s-watch-hook';
import { HealthState } from '@console/shared/src/components/dashboard/status-card/states';
import { FirehoseResult } from '@console/internal/components/utils/types';
import { Link } from 'react-router-dom';
import { referenceForModel } from '@console/internal/module/k8s';
import { ImageManifestVuln } from '../types';
import { ImageManifestVuln, WatchImageVuln } from '../types';
import { vulnPriority } from '../const';
import { ImageManifestVulnModel } from '../models';

export const securityHealthHandler: URLHealthHandler<string> = (
k8sHealth,
error,
resource: FirehoseResult<ImageManifestVuln[]>,
) => {
if (error || _.get(resource, 'loadError')) {
export const securityHealthHandler: ResourceHealthHandler<WatchImageVuln> = ({
imageManifestVuln,
}) => {
const { data, loaded, loadError } = imageManifestVuln;

if (loadError) {
return { state: HealthState.UNKNOWN, message: 'Not available' };
}
if (!_.get(resource, 'loaded')) {
if (!loaded) {
return { state: HealthState.LOADING, message: 'Scanning in progress' };
}
if (!_.isEmpty(resource.data)) {
return { state: HealthState.ERROR, message: `${resource.data.length} vulnerabilities` };
if (!_.isEmpty(data)) {
return { state: HealthState.ERROR, message: `${data.length} vulnerabilities` };
}
return { state: HealthState.OK, message: '0 vulnerabilities' };
};
Expand All @@ -37,10 +37,14 @@ export const quayURLFor = (vuln: ImageManifestVuln) => {
return `//${base}/manifest/${vuln.spec.manifest}?tab=vulnerabilities`;
};

export const SecurityBreakdownPopup: React.FC<SecurityBreakdownPopupProps> = (props) => {
export const SecurityBreakdownPopup: React.FC<WatchK8sResults<WatchImageVuln>> = ({
imageManifestVuln,
}) => {
const resource = imageManifestVuln.data;

const vulnsFor = (severity: string) =>
props.k8sResult.data.filter((v) => _.get(v.status, 'highestSeverity') === severity);
const fixableVulns = props.k8sResult.data
resource.filter((v) => _.get(v.status, 'highestSeverity') === severity);
const fixableVulns = resource
.filter((v) => _.get(v.status, 'fixableCount', 0) > 0)
.reduce((all, v) => all.set(v.metadata.name, v), new Map<string, ImageManifestVuln>());

Expand All @@ -50,7 +54,7 @@ export const SecurityBreakdownPopup: React.FC<SecurityBreakdownPopupProps> = (pr
Container images from Quay are analyzed to identify vulnerabilities. Images from other
registries are not scanned.
</div>
{!_.isEmpty(props.k8sResult.data) ? (
{!_.isEmpty(resource) ? (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div style={{ width: '66%', marginRight: '24px' }}>
Expand All @@ -67,7 +71,7 @@ export const SecurityBreakdownPopup: React.FC<SecurityBreakdownPopupProps> = (pr
</div>
<div className="text-secondary">
{
props.k8sResult.data.filter(
resource.filter(
(v) =>
_.get(v.status, 'highestSeverity') === priority.value &&
_.get(v.status, 'fixableCount', 0) > 0,
Expand All @@ -90,7 +94,7 @@ export const SecurityBreakdownPopup: React.FC<SecurityBreakdownPopupProps> = (pr
y: vulnsFor(priority.value).length,
}))
.toArray()}
title={`${props.k8sResult.data.length} total`}
title={`${resource.length} total`}
/>
</div>
</div>
Expand All @@ -117,9 +121,7 @@ export const SecurityBreakdownPopup: React.FC<SecurityBreakdownPopupProps> = (pr
}`}
>
{pluralize(
props.k8sResult.data.filter(
({ metadata }) => metadata.name === v.metadata.name,
).length,
resource.filter(({ metadata }) => metadata.name === v.metadata.name).length,
'namespace',
)}
</Link>
Expand All @@ -136,10 +138,4 @@ export const SecurityBreakdownPopup: React.FC<SecurityBreakdownPopupProps> = (pr
);
};

export type SecurityBreakdownPopupProps = {
healthResult?: any;
healthResultError?: any;
k8sResult?: FirehoseResult<ImageManifestVuln[]>;
};

SecurityBreakdownPopup.displayName = 'SecurityBreakdownPopup';