Skip to content

Commit

Permalink
Add operators status to Dashboards
Browse files Browse the repository at this point in the history
  • Loading branch information
rawagner committed Dec 11, 2019
1 parent 0365a3b commit 9f20b2f
Show file tree
Hide file tree
Showing 19 changed files with 766 additions and 102 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import * as React from 'react';
import * as _ from 'lodash';
import { Link } from 'react-router-dom';
import { referenceForModel, ClusterOperator, K8sResourceKind } from '@console/internal/module/k8s';
import { ClusterOperatorModel } from '@console/internal/models';
import { withDashboardResources } from '@console/internal/components/dashboard/with-dashboard-resources';
import {
ResourceLink,
resourcePathFromModel,
} from '@console/internal/components/utils/resource-link';
import { FirehoseResult, AsyncComponent } from '@console/internal/components/utils';
import HealthItem from '@console/shared/src/components/dashboard/status-card/HealthItem';
import * as plugins from '@console/internal/plugins';
import { uniqueResource } from '@console/internal/components/dashboard/dashboards-page/overview-dashboard/utils';
import { HealthState } from '@console/shared/src/components/dashboard/status-card/states';
import {
isDashboardsOperatorStatus,
DashboardsOperatorStatus,
OperatorStatusPriority,
OperatorTitleProps,
GetOperatorsWithStatuses,
} from '../../extensions/dashboards';
import { getOperatorsHealthState, getOperatorStatusPriority } from './status';
import { OperatorHealthPriority } from './utils';

import './operator-status.scss';

const OperatorStatusColumn: React.FC<OperatorStatusColumnProps> = ({
operator,
status,
titleLoader,
}) => {
const { title, icon } = status;
return (
<div className="co-operator-status__row">
<AsyncComponent
operator={operator}
loader={titleLoader}
className="co-operator-status__title"
/>
<div className="co-operator-status">
<div className="text-secondary">{title}</div>
<div className="co-operator-status__icon">{icon}</div>
</div>
</div>
);
};

const getClusterOperatorStatuses: GetOperatorsWithStatuses = (resources) => {
const clusterOperators = resources.clusterOperators?.data ?? [];
return _.castArray(clusterOperators).map((co) => {
return {
operator: co,
status: getOperatorStatusPriority(co as ClusterOperator),
};
});
};

const OperatorStatuses: React.FC<OperatorStatusesProps> = ({
resources,
getOperatorsWithStatuses,
title,
linkTo,
titleLoader,
}) => {
const operators = getOperatorsWithStatuses(resources);
const mostImportantStatus = Math.max(...operators.map(({ status }) => status.priority));
const mappedOperators = operators
.filter(({ status }) => status.priority === mostImportantStatus)
.sort((a, b) => a.operator.metadata.name.localeCompare(b.operator.metadata.name))
.slice(0, 5);
return (
<>
<div className="co-operator-status__row">
<div>
<span className="co-operator-status__text--bold">{title}</span>
<span className="text-secondary">{` (${operators.length})`}</span>
</div>
<div className="text-secondary">Status</div>
</div>
{_.values(resources).some((r) => r.loadError) ? (
<div className="text-secondary">Not Available</div>
) : (
mappedOperators.map(({ operator, status }) => (
<OperatorStatusColumn
key={operator.metadata.uid}
operator={operator}
status={status}
titleLoader={titleLoader}
/>
))
)}
<Link to={linkTo}>View all</Link>
</>
);
};

const ClusterOperatorLink: React.FC<OperatorTitleProps> = ({ operator, className }) => (
<ResourceLink
kind={referenceForModel(ClusterOperatorModel)}
name={operator.metadata.name}
hideIcon
className={className}
/>
);

const OperatorStatusPopup: React.FC<OperatorStatusPopupProps> = ({
resources,
additionalOperators,
}) => (
<>
<div className="co-operator-section">
Operators extend Kubernetes with additional custom resources to manage applications
</div>
{additionalOperators.map((o, index) => {
const operatorResources = o.properties.resources.reduce((acc, r) => {
acc[r.prop] = resources[uniqueResource(r, index).prop];
return acc;
}, {});
return (
<div className="co-operator-section">
<OperatorStatuses
resources={operatorResources}
getOperatorsWithStatuses={o.properties.getOperatorsWithStatuses}
title={o.properties.title}
linkTo={resourcePathFromModel(o.properties.model)}
titleLoader={o.properties.operatorTitleLoader}
/>
</div>
);
})}
<OperatorStatuses
resources={resources}
getOperatorsWithStatuses={getClusterOperatorStatuses}
title="Cluster operators"
linkTo="/settings/cluster/clusteroperators"
titleLoader={() => Promise.resolve(ClusterOperatorLink)}
/>
</>
);

const clusterOperator = {
kind: referenceForModel(ClusterOperatorModel),
namespaced: false,
isList: true,
prop: 'clusterOperators',
};

const getOperatorsState = (
clusterOperatorsStatus: plugins.SubsystemHealth,
additionalOperatorsStatuses: plugins.SubsystemHealth[],
): HealthState => {
const allStatuses = [clusterOperatorsStatus, ...additionalOperatorsStatuses];
return allStatuses.sort(
(a, b) => OperatorHealthPriority[b.state].priority - OperatorHealthPriority[a.state].priority,
)[0].state;
};

const OperatorsStatus = withDashboardResources(
({ resources, watchK8sResource, stopWatchK8sResource }) => {
const additionalOperators = React.useMemo(
() => plugins.registry.get<DashboardsOperatorStatus>(isDashboardsOperatorStatus),
[],
);
React.useEffect(() => {
watchK8sResource(clusterOperator);
additionalOperators.forEach((o, index) =>
o.properties.resources.forEach((r) => watchK8sResource(uniqueResource(r, index))),
);
return () => {
stopWatchK8sResource(clusterOperator);
additionalOperators.forEach((o, index) =>
o.properties.resources.forEach((r) => stopWatchK8sResource(uniqueResource(r, index))),
);
};
}, [watchK8sResource, stopWatchK8sResource, additionalOperators]);

const healthState = getOperatorsHealthState(resources);
const additionalStatuses = additionalOperators.map((o, index) =>
o.properties.healthHandler(
o.properties.resources.reduce((acc, r) => {
acc[r.prop] = resources[uniqueResource(r, index).prop];
return acc;
}, {}),
),
);

const PopupComponentCallback = React.useCallback(
() => <OperatorStatusPopup resources={resources} additionalOperators={additionalOperators} />,
[additionalOperators, resources],
);

return (
<HealthItem
title="Operators"
state={getOperatorsState(healthState, additionalStatuses)}
popupTitle="Operator status"
PopupComponent={PopupComponentCallback}
/>
);
},
);

export default OperatorsStatus;

type OperatorStatusPopupProps = {
resources: {
cos?: FirehoseResult<ClusterOperator[]>;
[key: string]: FirehoseResult<K8sResourceKind | K8sResourceKind[]>;
};
additionalOperators?: DashboardsOperatorStatus[];
};

type OperatorStatusColumnProps = {
operator: K8sResourceKind;
status: OperatorStatusPriority;
titleLoader: plugins.LazyLoader<any>;
};

type OperatorStatusesProps = {
resources: {
[key: string]: FirehoseResult<K8sResourceKind | K8sResourceKind[]>;
};
getOperatorsWithStatuses: GetOperatorsWithStatuses;
title: string;
linkTo: string;
titleLoader: plugins.LazyLoader<OperatorTitleProps<any>>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
.co-operator-section {
padding-bottom: var(--pf-global--spacer--sm);
}

.co-operator-status {
align-items: center;
display: flex;
padding-left: var(--pf-global--spacer--xs);
}

.co-operator-status__icon {
padding-left: var(--pf-global--spacer--xs);
}

.co-operator-status__row {
display: flex;
justify-content: space-between;
padding-bottom: var(--pf-global--spacer--xs);
}

.co-operator-status__text--bold {
font-weight: var(--pf-global--FontWeight--bold);
}

.co-operator-status__title {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;

.co-resource-item__resource-name {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import * as _ from 'lodash';
import { PrometheusHealthHandler, URLHealthHandler, SubsystemHealth } from '@console/plugin-sdk';
import {
PrometheusHealthHandler,
URLHealthHandler,
SubsystemHealth,
ResourceHealthHandler,
} from '@console/plugin-sdk';
import { HealthState } from '@console/shared/src/components/dashboard/status-card/states';
import { coFetch } from '@console/internal/co-fetch';
import {
ClusterVersionKind,
ClusterUpdateStatus,
getClusterUpdateStatus,
getClusterOperatorStatus,
OperatorStatus,
ClusterOperator,
K8sResourceKind,
} from '@console/internal/module/k8s';
import { PrometheusResponse } from '@console/internal/components/graphs';
import { humanizePercentage } from '@console/internal/components/utils/units';
import { FirehoseResult } from '@console/internal/components/utils/types';
import { OperatorStatusPriority } from '../../extensions/dashboards';
import { OperatorHealthPriority } from './utils';

export const fetchK8sHealth = async (url: string) => {
const response = await coFetch(url);
Expand Down Expand Up @@ -38,7 +50,8 @@ export const getControlPlaneComponentHealth = (
if (
error ||
(response &&
(response.status === 'success' && _.isNil(_.get(response, 'data.result[0].value[1]'))))
response.status === 'success' &&
_.isNil(_.get(response, 'data.result[0].value[1]')))
) {
return { state: HealthState.UNKNOWN, message: 'Not available' };
}
Expand Down Expand Up @@ -76,3 +89,57 @@ export const getControlPlaneHealth: PrometheusHealthHandler = (responses = [], e
}
return { state: HealthState.OK };
};

export const getOperatorStatusPriority = (co: ClusterOperator): OperatorStatusPriority => {
const status = getClusterOperatorStatus(co);
if (status === OperatorStatus.Degraded) {
return { ...OperatorHealthPriority[HealthState.WARNING], title: status };
}
if (status === OperatorStatus.Unknown) {
return { ...OperatorHealthPriority[HealthState.UNKNOWN], title: status };
}
if (status === OperatorStatus.Updating) {
return { ...OperatorHealthPriority[HealthState.UPDATING], title: status };
}
return { ...OperatorHealthPriority[HealthState.OK], title: status };
};

export const getMostImportantStatus = <R extends K8sResourceKind>(
operators: R[],
getOperatorStatus: (operator: R) => OperatorStatusPriority,
): OperatorStatusPriority => {
if (!operators.length) {
return {
...OperatorHealthPriority[HealthState.OK],
title: 'Available',
};
}
const highestPriority = Math.max(..._.values(OperatorHealthPriority).map((s) => s.priority));
let mostImportantStatus: OperatorStatusPriority;
_.forEach(operators, (o) => {
const operatorStatus = getOperatorStatus(o);
if (!mostImportantStatus || operatorStatus.priority > mostImportantStatus.priority) {
mostImportantStatus = operatorStatus;
if (mostImportantStatus.priority === highestPriority) {
return false;
}
}
return true;
});
return mostImportantStatus;
};

export const getOperatorsHealthState: ResourceHealthHandler = (resources) => {
const clusterOperators = (resources.clusterOperators || {}) as FirehoseResult<ClusterOperator[]>;
if (clusterOperators.loadError) {
return { state: HealthState.UNKNOWN };
}
if (!clusterOperators.loaded) {
return { state: HealthState.LOADING };
}
const status = getMostImportantStatus<ClusterOperator>(
clusterOperators.data,
getOperatorStatusPriority,
);
return { state: status.state };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {
HealthState,
healthStateMapping,
} from '@console/shared/src/components/dashboard/status-card/states';

export const OperatorHealthPriority = {
[HealthState.OK]: {
priority: 0,
state: HealthState.OK,
...healthStateMapping[HealthState.OK],
},
[HealthState.LOADING]: {
priority: 1,
state: HealthState.LOADING,
...healthStateMapping[HealthState.LOADING],
},
[HealthState.UPDATING]: {
priority: 2,
state: HealthState.UPDATING,
...healthStateMapping[HealthState.UPDATING],
},
[HealthState.WARNING]: {
priority: 3,
state: HealthState.WARNING,
...healthStateMapping[HealthState.WARNING],
},
[HealthState.ERROR]: {
priority: 4,
state: HealthState.ERROR,
...healthStateMapping[HealthState.ERROR],
},
};

0 comments on commit 9f20b2f

Please sign in to comment.