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

Added insights-plugin #6660

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 change: 1 addition & 0 deletions frontend/packages/console-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"@console/container-security": "0.0.0-fixed",
"@console/dev-console": "0.0.0-fixed",
"@console/internal": "0.0.0-fixed",
"@console/insights-plugin": "0.0.0-fixed",
Copy link
Contributor

Choose a reason for hiding this comment

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

@bond95 @spadgett FYI, this means Insights plugin will be included in all Console builds by default.

"@console/knative-plugin": "0.0.0-fixed",
"@console/kubevirt-plugin": "0.0.0-fixed",
"@console/local-storage-operator-plugin": "0.0.0-fixed",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ export type URLHealthHandler<R> = (

export type PrometheusHealthPopupProps = {
responses: { response: PrometheusResponse; error: any }[];
additionalResource?: FirehoseResult<K8sResourceKind | K8sResourceKind[]>;
k8sResult?: FirehoseResult<K8sResourceKind | K8sResourceKind[]>;
};

export type PrometheusHealthHandler = (
Expand Down
8 changes: 8 additions & 0 deletions frontend/packages/insights-plugin/OWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
reviewers:
- bond95
- tisnik
approvers:
- bond95
- tisnik
labels:
- component/insights
Copy link
Member

Choose a reason for hiding this comment

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

You'll need a PR to the openshift/release repo to add this label. See openshift/release#4526

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I created PR for that openshift/release#12398

19 changes: 19 additions & 0 deletions frontend/packages/insights-plugin/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "@console/insights-plugin",
"version": "0.0.0-fixed",
"description": "Insights - provide cluster health data and integrate with OpenShift Cluster Manager",
"private": true,
"main": "src/index.ts",
"scripts": {
"test": "yarn --cwd ../.. test packages/insights-plugin"
},
"dependencies": {
"@console/internal": "0.0.0-fixed",
"@console/patternfly": "0.0.0-fixed",
"@console/plugin-sdk": "0.0.0-fixed",
"@console/shared": "0.0.0-fixed"
},
"consolePlugin": {
"entry": "src/plugin.tsx"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as React from 'react';

const CriticalIcon = (props) => (
<svg viewbox="0 0 10 10" {...props}>
<polygon points="10 10, 10 3, 5 0, 0 3, 0 10, 5 8" />
</svg>
);

export default CriticalIcon;
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import * as React from 'react';
import { ChartDonut, ChartLegend, ChartLabel } from '@patternfly/react-charts';
import { riskIcons, colorScale, legendColorScale, riskSorting, mapMetrics } from './mappers';
import { PrometheusHealthPopupProps } from '@console/plugin-sdk';
import { K8sResourceKind } from '@console/internal/module/k8s';
import { ExternalLink } from '@console/internal/components/utils';
import './style.scss';

const DataComponent: React.FC<DataComponentProps> = ({ x, y, datum }) => {
const Icon = riskIcons[datum.id];
return <Icon x={x} y={y - 5} fill={legendColorScale[datum.id]} />;
};

export const InsightsPopup: React.FC<PrometheusHealthPopupProps> = ({ responses, k8sResult }) => {
const resource = mapMetrics(responses[0].response);
const clusterID = (k8sResult as K8sResourceKind)?.data?.spec?.clusterID || '';
const riskEntries = Object.entries(resource).sort(
([k1], [k2]) => riskSorting[k1] - riskSorting[k2],
);
const numberOfIssues = Object.values(resource).reduce((acc, cur) => acc + cur, 0);
const hasIssues = riskEntries.length > 0 && numberOfIssues > 0;

return (
<div className="co-insights__box">
<div className="co-status-popup__section">
Insights identifies and prioritizes risks to security, performance, availability, and
stability of your clusters.
</div>
<div className="co-status-popup__section">
{hasIssues && (
<div>
<ChartDonut
data={riskEntries.map(([k, v]) => ({
label: `${v} ${k}`,
x: k,
y: v,
}))}
title={`${numberOfIssues}`}
subTitle="Total issues"
legendData={Object.entries(resource).map(([k, v]) => ({ name: `${k}: ${v}` }))}
legendOrientation="vertical"
width={304}
height={152}
colorScale={colorScale}
constrainToVisibleArea
legendComponent={
<ChartLegend
title="Total Risk"
titleComponent={
<ChartLabel dx={13} style={{ fontWeight: 'bold', fontSize: '14px' }} />
}
data={riskEntries.map(([k, v]) => ({
name: `${v} ${k}`,
id: k,
}))}
dataComponent={<DataComponent />}
x={-13}
/>
}
padding={{
bottom: 20,
left: 145,
right: 20, // Adjusted to accommodate legend
top: 0,
}}
/>
</div>
)}
{!hasIssues && <div className="co-insights__no-rules">No Insights data to display.</div>}
</div>
<div className="co-status-popup__section">
{hasIssues && (
<>
<h6 className="pf-c-title pf-m-md">Fixable issues</h6>
<div>
<ExternalLink
href={`https://cloud.redhat.com/openshift/details/${clusterID}`}
text="View all in OpenShift Cluster Manager"
/>
</div>
</>
)}
{!hasIssues && (
<ExternalLink
href="https://docs.openshift.com/container-platform/latest/support/getting-support.html"
text="More about Insights"
/>
)}
</div>
</div>
);
};

export type DataComponentProps = {
x?: number;
y?: number;
datum?: {
id: string;
};
};

InsightsPopup.displayName = 'InsightsPopup';
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as _ from 'lodash';

import { global_palette_blue_50 as blue50 } from '@patternfly/react-tokens/dist/js/global_palette_blue_50';
import { global_palette_blue_300 as blue300 } from '@patternfly/react-tokens/dist/js/global_palette_blue_300';
import { global_palette_gold_400 as gold400 } from '@patternfly/react-tokens/dist/js/global_palette_gold_400';
import { global_palette_orange_300 as orange300 } from '@patternfly/react-tokens/dist/js/global_palette_orange_300';
import { global_palette_red_200 as red200 } from '@patternfly/react-tokens/dist/js/global_palette_red_200';

import { AngleDoubleDownIcon, AngleDoubleUpIcon, EqualsIcon } from '@patternfly/react-icons';
import CriticalIcon from './CriticalIcon';
import { PrometheusResponse } from '@console/internal/components/graphs';

export const riskIcons = {
low: AngleDoubleDownIcon,
moderate: EqualsIcon,
important: AngleDoubleUpIcon,
critical: CriticalIcon,
};

export const colorScale = [blue50.value, gold400.value, orange300.value, red200.value];

export const legendColorScale = {
low: blue300.value,
moderate: gold400.value,
important: orange300.value,
critical: red200.value,
};

export const riskSorting = {
low: 0,
moderate: 1,
important: 2,
critical: 3,
};

type Metrics = {
critical?: number;
important?: number;
low?: number;
moderate?: number;
};

export const mapMetrics = (response: PrometheusResponse): Metrics => {
const values: Metrics = {};
for (let i = 0; i < response.data.result.length; i++) {
const value = response.data?.result?.[i]?.value?.[1];
if (_.isNil(value)) {
return null;
}
const metricName = response.data?.result?.[i]?.metric?.metric;
values[metricName] = parseInt(value, 10);
}

return values;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as _ from 'lodash';
import { PrometheusHealthHandler, SubsystemHealth } from '@console/plugin-sdk';
import { HealthState } from '@console/shared/src/components/dashboard/status-card/states';
import { PrometheusResponse } from '@console/internal/components/graphs';
import { mapMetrics } from './mappers';

export const getClusterInsightsComponentStatus = (
response: PrometheusResponse,
error,
): SubsystemHealth => {
if (error) {
return {
state: HealthState.NOT_AVAILABLE,
message: 'Not available',
};
}
if (!response) {
return { state: HealthState.LOADING };
}
const values = mapMetrics(response);
if (_.isNil(values)) {
return { state: HealthState.UNKNOWN, message: 'Not available' };
}
const issuesNumber = Object.values(values).reduce((acc, cur) => acc + cur, 0);
const issueStr = `${issuesNumber} issues found`;
if (values.critical > 0) {
return { state: HealthState.ERROR, message: issueStr };
}
if (issuesNumber > 0) {
return { state: HealthState.WARNING, message: issueStr };
}
return { state: HealthState.OK, message: issueStr };
};

export const getClusterInsightsStatus: PrometheusHealthHandler = (responses, cluster) => {
const componentHealth = getClusterInsightsComponentStatus(
responses[0].response,
responses[0].error,
);
if (componentHealth.state === HealthState.LOADING || !_.get(cluster, 'loaded')) {
return { state: HealthState.LOADING };
}

return componentHealth;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.co-insights__no-rules {
color: var(--pf-global--Color--200);
}
31 changes: 31 additions & 0 deletions frontend/packages/insights-plugin/src/plugin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Plugin, DashboardsOverviewHealthPrometheusSubsystem } from '@console/plugin-sdk';
import { ClusterVersionModel } from '@console/internal/models';
import { referenceForModel } from '@console/internal/module/k8s';
import { getClusterInsightsStatus } from './components/InsightsPopup/status';

type ConsumedExtensions = DashboardsOverviewHealthPrometheusSubsystem;

const plugin: Plugin<ConsumedExtensions> = [
{
type: 'Dashboards/Overview/Health/Prometheus',
properties: {
title: 'Insights',
queries: ["health_statuses_insights{metric=~'low|moderate|important|critical'}"],
healthHandler: getClusterInsightsStatus,
additionalResource: {
kind: referenceForModel(ClusterVersionModel),
namespaced: false,
name: 'version',
isList: false,
prop: 'cluster',
},
popupComponent: () =>
import('./components/InsightsPopup/index' /* webpackChunkName: "insights-plugin" */).then(
(m) => m.InsightsPopup,
),
popupTitle: 'Insights status',
},
Copy link
Contributor

Choose a reason for hiding this comment

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

You need to gate the extension by some model contributed by Insights operator. To do so:

  1. create ModelDefinition extension
{
    type: 'ModelDefinition',
    properties: {
      models: [YourModel],
    },
  },
  1. Add FeatureFlag extension
const INSIGHTS_FLAG = 'INSIGHTS_FLAG';
{
    type: 'FeatureFlag/Model',
    properties: {
      model: YourModel,
      flag: INSIGHTS_FLAG,
    },
  },
  1. gate Dashboard Health with the above flag
Suggested change
},
flags: {
required: [InsightsFlag],
},
},

Copy link
Contributor

Choose a reason for hiding this comment

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

@bond95 As @rawagner wrote, we should enable the plugin's extensions only when the cluster has the right capability (i.e. Insights operator is installed).

Detecting Insights operator can be done via CRD detection. Use ModelDefinition extension to make Console aware of your CRD, then use FeatureFlag/Model extension to introduce a feature flag reflecting the presence of such CRD.

Every other extension can then use the flags object to declare which feature flags are required and/or disallowed in order for that particular extension to be in use. Typically, you'd want all Insights extensions to be gated by INSIGHTS_FLAG.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@rawagner @vojtechszocs What if we don't have any CRD?

Copy link
Contributor

Choose a reason for hiding this comment

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

Another option is to implement your own detection logic and use CustomFeatureFlag . Example https://github.com/openshift/console/blob/master/frontend/packages/ceph-storage-plugin/src/plugin.ts#L76

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@rawagner We had discussion with @smarterclayton and @spadgett, they suggested not to hide tab even if insights-operator is disabled.

Copy link
Contributor

Choose a reason for hiding this comment

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

@bond95 Assuming that Insights integration is implemented via cluster-level operator (i.e. not via OLM operator), it makes sense to have Insights plugin extensions active by default 👍

},
];

export default plugin;